diff --git a/assets/electrum_server_list.yml b/assets/electrum_server_list.yml new file mode 100644 index 000000000..4baf56aec --- /dev/null +++ b/assets/electrum_server_list.yml @@ -0,0 +1,2 @@ +- + uri: electrum2.hodlister.co:50002 \ No newline at end of file diff --git a/assets/fonts/Montserrat-Bold.ttf b/assets/fonts/Montserrat-Bold.ttf new file mode 100644 index 000000000..eadb3b67b Binary files /dev/null and b/assets/fonts/Montserrat-Bold.ttf differ diff --git a/assets/fonts/Montserrat-Regular.ttf b/assets/fonts/Montserrat-Regular.ttf new file mode 100644 index 000000000..211828ab7 Binary files /dev/null and b/assets/fonts/Montserrat-Regular.ttf differ diff --git a/assets/fonts/Montserrat-SemiBold.ttf b/assets/fonts/Montserrat-SemiBold.ttf new file mode 100644 index 000000000..ada91a1c2 Binary files /dev/null and b/assets/fonts/Montserrat-SemiBold.ttf differ diff --git a/assets/images/2.0x/crypto_lock_light.png b/assets/images/2.0x/crypto_lock_light.png new file mode 100644 index 000000000..2937b2012 Binary files /dev/null and b/assets/images/2.0x/crypto_lock_light.png differ diff --git a/assets/images/2.0x/wallet_name_light.png b/assets/images/2.0x/wallet_name_light.png new file mode 100644 index 000000000..b59e4c451 Binary files /dev/null and b/assets/images/2.0x/wallet_name_light.png differ diff --git a/assets/images/2.0x/wallet_type_light.png b/assets/images/2.0x/wallet_type_light.png new file mode 100644 index 000000000..791c3a87e Binary files /dev/null and b/assets/images/2.0x/wallet_type_light.png differ diff --git a/assets/images/2.0x/welcome_light.png b/assets/images/2.0x/welcome_light.png new file mode 100644 index 000000000..8386d25b0 Binary files /dev/null and b/assets/images/2.0x/welcome_light.png differ diff --git a/assets/images/3.0x/crypto_lock_light.png b/assets/images/3.0x/crypto_lock_light.png new file mode 100644 index 000000000..9ec7964cf Binary files /dev/null and b/assets/images/3.0x/crypto_lock_light.png differ diff --git a/assets/images/3.0x/wallet_name_light.png b/assets/images/3.0x/wallet_name_light.png new file mode 100644 index 000000000..5fecf8f1b Binary files /dev/null and b/assets/images/3.0x/wallet_name_light.png differ diff --git a/assets/images/3.0x/wallet_type_light.png b/assets/images/3.0x/wallet_type_light.png new file mode 100644 index 000000000..5ece2804f Binary files /dev/null and b/assets/images/3.0x/wallet_type_light.png differ diff --git a/assets/images/3.0x/welcome_light.png b/assets/images/3.0x/welcome_light.png new file mode 100644 index 000000000..8b682ed70 Binary files /dev/null and b/assets/images/3.0x/welcome_light.png differ diff --git a/assets/images/crypto_lock_light.png b/assets/images/crypto_lock_light.png new file mode 100644 index 000000000..5f3fb14a7 Binary files /dev/null and b/assets/images/crypto_lock_light.png differ diff --git a/assets/images/wallet_name_light.png b/assets/images/wallet_name_light.png new file mode 100644 index 000000000..0199c1b30 Binary files /dev/null and b/assets/images/wallet_name_light.png differ diff --git a/assets/images/wallet_type_light.png b/assets/images/wallet_type_light.png new file mode 100644 index 000000000..e36c0d3aa Binary files /dev/null and b/assets/images/wallet_type_light.png differ diff --git a/assets/images/welcome_light.png b/assets/images/welcome_light.png new file mode 100644 index 000000000..6feff85d1 Binary files /dev/null and b/assets/images/welcome_light.png differ diff --git a/cw_monero/lib/wallet_manager.dart b/cw_monero/lib/wallet_manager.dart index 63e41faa0..c5c1dd22e 100644 --- a/cw_monero/lib/wallet_manager.dart +++ b/cw_monero/lib/wallet_manager.dart @@ -182,7 +182,7 @@ Future _openWallet(Map args) async => bool _isWalletExist(String path) => isWalletExistSync(path: path); void openWallet({String path, String password, int nettype = 0}) async => - loadWallet(path: path, password: password); + loadWallet(path: path, password: password, nettype: nettype); Future openWalletAsync(Map args) async => compute(_openWallet, args); diff --git a/ios/Podfile.lock b/ios/Podfile.lock index e1b3039af..959c597e1 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -36,6 +36,8 @@ PODS: - Flutter - path_provider (0.0.1): - Flutter + - path_provider_linux (0.0.1): + - Flutter - path_provider_macos (0.0.1): - Flutter - share (0.0.1): @@ -65,6 +67,7 @@ DEPENDENCIES: - local_auth (from `.symlinks/plugins/local_auth/ios`) - package_info (from `.symlinks/plugins/package_info/ios`) - path_provider (from `.symlinks/plugins/path_provider/ios`) + - path_provider_linux (from `.symlinks/plugins/path_provider_linux/ios`) - path_provider_macos (from `.symlinks/plugins/path_provider_macos/ios`) - share (from `.symlinks/plugins/share/ios`) - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) @@ -100,6 +103,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/package_info/ios" path_provider: :path: ".symlinks/plugins/path_provider/ios" + path_provider_linux: + :path: ".symlinks/plugins/path_provider_linux/ios" path_provider_macos: :path: ".symlinks/plugins/path_provider_macos/ios" share: @@ -129,6 +134,7 @@ SPEC CHECKSUMS: MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62 path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c + path_provider_linux: 4d630dc393e1f20364f3e3b4a2ff41d9674a84e4 path_provider_macos: f760a3c5b04357c380e2fddb6f9db6f3015897e0 share: 0b2c3e82132f5888bccca3351c504d0003b3b410 shared_preferences: af6bfa751691cdc24be3045c43ec037377ada40d @@ -141,4 +147,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: f1916a43bb28badbd408be80e8e4b8652a74e93e -COCOAPODS: 1.9.1 +COCOAPODS: 1.9.3 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index d4dc0c8c9..54901a5a7 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -388,7 +388,7 @@ "$(PROJECT_DIR)/Flutter", ); MARKETING_VERSION = 3.1.28; - PRODUCT_BUNDLE_IDENTIFIER = com.fotolockr.cakewallet; + PRODUCT_BUNDLE_IDENTIFIER = com.cakewallet.cakewallet; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -524,7 +524,7 @@ "$(PROJECT_DIR)/Flutter", ); MARKETING_VERSION = 3.1.28; - PRODUCT_BUNDLE_IDENTIFIER = com.fotolockr.cakewallet; + PRODUCT_BUNDLE_IDENTIFIER = com.cakewallet.cakewallet; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -555,7 +555,7 @@ "$(PROJECT_DIR)/Flutter", ); MARKETING_VERSION = 3.1.28; - PRODUCT_BUNDLE_IDENTIFIER = com.fotolockr.cakewallet; + PRODUCT_BUNDLE_IDENTIFIER = com.cakewallet.cakewallet; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; diff --git a/lib/bitcoin/bitcoin_transaction_credentials.dart b/lib/bitcoin/bitcoin_transaction_credentials.dart new file mode 100644 index 000000000..52f2d5ec6 --- /dev/null +++ b/lib/bitcoin/bitcoin_transaction_credentials.dart @@ -0,0 +1,6 @@ +class BitcoinTransactionCredentials { + const BitcoinTransactionCredentials(this.address, this.amount); + + final String address; + final double amount; +} diff --git a/lib/bitcoin/bitcoin_transaction_history.dart b/lib/bitcoin/bitcoin_transaction_history.dart index 898153aec..1a3990bf7 100644 --- a/lib/bitcoin/bitcoin_transaction_history.dart +++ b/lib/bitcoin/bitcoin_transaction_history.dart @@ -26,26 +26,18 @@ abstract class BitcoinTransactionHistoryBase _password = password, _height = 0; - BitcoinWallet wallet; + BitcoinWalletBase wallet; final ElectrumClient eclient; final String path; final String _password; int _height; Future init() async { - // TODO: throw exeption if wallet is null; final info = await _read(); - _height = (info['height'] as int) ?? _height; - // FIXME: remove hardcoded value - transactions = ObservableList.of([ - BitcoinTransactionInfo( - id: 'test', - height: 12, - amount: 12, - direction: TransactionDirection.incoming, - date: DateTime.now(), - isPending: false) - ]); + _height = info['height'] as int ?? _height; + transactions = ObservableList.of( + info['transactions'] as List ?? + []); } @override @@ -119,7 +111,7 @@ abstract class BitcoinTransactionHistoryBase return {'transactions': transactions, 'height': height}; } catch (_) { - return {'transactions': [], 'height': 0}; + return {'transactions': [], 'height': 0}; } } diff --git a/lib/bitcoin/bitcoin_wallet.dart b/lib/bitcoin/bitcoin_wallet.dart index faff9755b..fa2ce0f0a 100644 --- a/lib/bitcoin/bitcoin_wallet.dart +++ b/lib/bitcoin/bitcoin_wallet.dart @@ -1,5 +1,11 @@ import 'dart:typed_data'; import 'dart:convert'; +import 'package:cake_wallet/bitcoin/bitcoin_transaction_credentials.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_wallet_keys.dart'; +import 'package:cake_wallet/src/domain/bitcoin/bitcoin_amount_format.dart'; +import 'package:cake_wallet/src/domain/common/crypto_currency.dart'; +import 'package:cake_wallet/src/domain/common/sync_status.dart'; +import 'package:flutter/cupertino.dart'; import 'package:mobx/mobx.dart'; import 'package:bip39/bip39.dart' as bip39; import 'package:flutter/foundation.dart'; @@ -13,13 +19,10 @@ import 'package:cake_wallet/bitcoin/electrum.dart'; import 'package:cake_wallet/bitcoin/bitcoin_balance.dart'; import 'package:cake_wallet/src/domain/common/node.dart'; import 'package:cake_wallet/core/wallet_base.dart'; +import 'package:rxdart/rxdart.dart'; part 'bitcoin_wallet.g.dart'; -/* TODO: Save balance to a wallet file. - Load balance from the wallet file in `init` method. -*/ - class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet; abstract class BitcoinWalletBase extends WalletBase with Store { @@ -31,7 +34,7 @@ abstract class BitcoinWalletBase extends WalletBase with Store { final data = json.decode(jsonSource) as Map; final mnemonic = data['mnemonic'] as String; final accountIndex = - (data['account_index'] == "null" || data['account_index'] == null) + (data['account_index'] == 'null' || data['account_index'] == null) ? 0 : int.parse(data['account_index'] as String); final _addresses = data['addresses'] as List; @@ -90,18 +93,15 @@ abstract class BitcoinWalletBase extends WalletBase with Store { this.transactionHistory, this.mnemonic, BitcoinBalance initialBalance}) { + type = WalletType.bitcoin; + currency = CryptoCurrency.btc; balance = initialBalance ?? BitcoinBalance(confirmed: 0, unconfirmed: 0); hd = bitcoin.HDWallet.fromSeed(bip39.mnemonicToSeed(mnemonic), network: bitcoin.bitcoin); addresses = initialAddresses != null ? ObservableList.of(initialAddresses) : ObservableList(); - - if (addresses.isEmpty) { - addresses.add(BitcoinAddressRecord(hd.address)); - } - - address = addresses.first.address; + syncStatus = NotConnectedSyncStatus(); _password = password; _accountIndex = accountIndex; @@ -113,8 +113,6 @@ abstract class BitcoinWalletBase extends WalletBase with Store { bitcoin.HDWallet hd; final ElectrumClient eclient; final String mnemonic; - int _accountIndex; - String _password; @override String name; @@ -128,13 +126,31 @@ abstract class BitcoinWalletBase extends WalletBase with Store { BitcoinBalance balance; @override - final type = WalletType.bitcoin; + @observable + SyncStatus syncStatus; ObservableList addresses; String get xpub => hd.base58; + @override + String get seed => mnemonic; + + @override + BitcoinWalletKeys get keys => BitcoinWalletKeys( + wif: hd.wif, privateKey: hd.privKey, publicKey: hd.pubKey); + + int _accountIndex; + String _password; + BehaviorSubject _addressUpdateSubject; + Future init() async { + if (addresses.isEmpty) { + addresses.add(BitcoinAddressRecord(_getAddress(hd: hd, index: 0))); + } + + address = addresses.first.address; + transactionHistory.wallet = this; await transactionHistory.init(); } @@ -160,14 +176,55 @@ abstract class BitcoinWalletBase extends WalletBase with Store { } } + @action @override - Future startSync() async {} + Future startSync() async { + try { + syncStatus = StartingSyncStatus(); + await _addressUpdateSubject?.close(); + _addressUpdateSubject = eclient.addressUpdate(address: address); + await transactionHistory.update(); + await _updateBalance(); + syncStatus = SyncedSyncStatus(); + } catch (e) { + print(e.toString()); + syncStatus = FailedSyncStatus(); + } + } + + @action + @override + Future connectToNode({@required Node node}) async { + try { + syncStatus = ConnectingSyncStatus(); + await eclient.connect(host: 'electrum2.hodlister.co', port: 50002); + syncStatus = ConnectedSyncStatus(); + } catch (e) { + print(e.toString); + syncStatus = FailedSyncStatus(); + } + } @override - Future connectToNode({@required Node node}) async {} + Future createTransaction(Object credentials) async { + final transactionCredentials = credentials as BitcoinTransactionCredentials; - @override - Future createTransaction(Object credentials) async {} + final txb = bitcoin.TransactionBuilder(network: bitcoin.bitcoin); + final keyPair = bitcoin.ECPair.fromWIF(hd.wif); + final transactions = transactionHistory.transactions; + transactions.sort((q, w) => q.height.compareTo(w.height)); + final prevTx = transactions.first; + + txb.setVersion(1); + txb.addInput(prevTx, 0); + txb.addOutput(transactionCredentials.address, + doubleToBitcoinAmount(transactionCredentials.amount)); + txb.sign(vin: 0, keyPair: keyPair); + final encoded = txb.build().toHex(); + + print('Enoded transaction $encoded'); + await eclient.broadcastTransaction(transactionRaw: encoded); + } @override Future save() async => @@ -181,24 +238,27 @@ abstract class BitcoinWalletBase extends WalletBase with Store { }); String _getAddress({bitcoin.HDWallet hd, int index}) => bitcoin - .P2PKH( + .P2WPKH( data: PaymentData( pubkey: Uint8List.fromList(hd.derive(index).pubKey.codeUnits))) .data .address; - Future> _fetchBalances() async { + Future _fetchBalances() async { final balances = await Future.wait( addresses.map((record) => eclient.getBalance(address: record.address))); - final balance = balances.fold({}, (Map acc, val) { - acc['confirmed'] = - (val['confirmed'] as int ?? 0) + (acc['confirmed'] ?? 0); - acc['unconfirmed'] = - (val['unconfirmed'] as int ?? 0) + (acc['unconfirmed'] ?? 0); - - return acc; - }); + final balance = balances.fold( + BitcoinBalance(confirmed: 0, unconfirmed: 0), + (BitcoinBalance acc, val) => BitcoinBalance( + confirmed: (val['confirmed'] as int ?? 0) + (acc.confirmed ?? 0), + unconfirmed: + (val['unconfirmed'] as int ?? 0) + (acc.unconfirmed ?? 0))); return balance; } + + Future _updateBalance() async { + balance = await _fetchBalances(); + await save(); + } } diff --git a/lib/bitcoin/bitcoin_wallet_keys.dart b/lib/bitcoin/bitcoin_wallet_keys.dart new file mode 100644 index 000000000..74212c74c --- /dev/null +++ b/lib/bitcoin/bitcoin_wallet_keys.dart @@ -0,0 +1,9 @@ +import 'package:flutter/foundation.dart'; + +class BitcoinWalletKeys { + const BitcoinWalletKeys({@required this.wif, @required this.privateKey, @required this.publicKey}); + + final String wif; + final String privateKey; + final String publicKey; +} \ No newline at end of file diff --git a/lib/bitcoin/electrum.dart b/lib/bitcoin/electrum.dart index 79462c658..70a01a293 100644 --- a/lib/bitcoin/electrum.dart +++ b/lib/bitcoin/electrum.dart @@ -34,6 +34,7 @@ class ElectrumClient { int _id; final Map _tasks; bool _isConnected; + Timer _aliveTimer; Future connect({@required String host, @required int port}) async { if (socket != null) { @@ -48,8 +49,7 @@ class ElectrumClient { socket.listen((List event) { try { - final Map jsoned = - json.decode(utf8.decode(event)) as Map; + final jsoned = json.decode(utf8.decode(event)) as Map; final method = jsoned['method']; if (method is String) { @@ -73,6 +73,13 @@ class ElectrumClient { }); print('Connected to ${socket.remoteAddress}'); + keepAlive(); + } + + void keepAlive() { + _aliveTimer?.cancel(); + // FIXME: Unnamed constant. + _aliveTimer = Timer.periodic(Duration(seconds: 30), (_) async => ping()); } Future ping() => call(method: 'server.ping'); @@ -122,6 +129,16 @@ class ElectrumClient { return ''; }); + Future broadcastTransaction({@required String transactionRaw}) async => + call(method: 'blockchain.transaction.broadcast', params: [transactionRaw]) + .then((dynamic result) { + if (result is String) { + return result; + } + + return ''; + }); + Future> getMerkle( {@required String hash, @required int height}) async => await call( diff --git a/lib/core/AddressLabelValidator.dart b/lib/core/address_label_validator.dart similarity index 100% rename from lib/core/AddressLabelValidator.dart rename to lib/core/address_label_validator.dart diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart new file mode 100644 index 000000000..26e239f6c --- /dev/null +++ b/lib/core/address_validator.dart @@ -0,0 +1,82 @@ +import 'package:flutter/foundation.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/core/validator.dart'; +import 'package:cake_wallet/src/domain/common/crypto_currency.dart'; + +class AddressValidator extends TextValidator { + AddressValidator({@required CryptoCurrency type}) + : super( + errorMessage: S.current.error_text_address, + pattern: getPattern(type), + length: getLength(type)); + + static String getPattern(CryptoCurrency type) { + switch (type) { + case CryptoCurrency.xmr: + return '[0-9a-zA-Z]'; + case CryptoCurrency.ada: + return '[0-9a-zA-Z]'; + case CryptoCurrency.bch: + return '[0-9a-zA-Z]'; + case CryptoCurrency.bnb: + return '[0-9a-zA-Z]'; + case CryptoCurrency.btc: + return '[0-9a-zA-Z]'; + case CryptoCurrency.dash: + return '[0-9a-zA-Z]'; + case CryptoCurrency.eos: + return '[0-9a-zA-Z]'; + case CryptoCurrency.eth: + return '[0-9a-zA-Z]'; + case CryptoCurrency.ltc: + return '[0-9a-zA-Z]'; + case CryptoCurrency.nano: + return '[0-9a-zA-Z_]'; + case CryptoCurrency.trx: + return '[0-9a-zA-Z]'; + case CryptoCurrency.usdt: + return '[0-9a-zA-Z]'; + case CryptoCurrency.xlm: + return '[0-9a-zA-Z]'; + case CryptoCurrency.xrp: + return '[0-9a-zA-Z]'; + default: + return '[0-9a-zA-Z]'; + } + } + + static List getLength(CryptoCurrency type) { + switch (type) { + case CryptoCurrency.xmr: + return [95, 106]; + case CryptoCurrency.ada: + return [59, 92, 105]; + case CryptoCurrency.bch: + return [42]; + case CryptoCurrency.bnb: + return [42]; + case CryptoCurrency.btc: + return [34, 42]; + case CryptoCurrency.dash: + return [34]; + case CryptoCurrency.eos: + return [42]; + case CryptoCurrency.eth: + return [42]; + case CryptoCurrency.ltc: + return [34]; + case CryptoCurrency.nano: + return [64, 65]; + case CryptoCurrency.trx: + return [34]; + case CryptoCurrency.usdt: + return [42]; + case CryptoCurrency.xlm: + return [56]; + case CryptoCurrency.xrp: + return [34]; + default: + return []; + } + } +} diff --git a/lib/core/contact_name_validator.dart b/lib/core/contact_name_validator.dart new file mode 100644 index 000000000..5a867dd41 --- /dev/null +++ b/lib/core/contact_name_validator.dart @@ -0,0 +1,11 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/core/validator.dart'; + +class ContactNameValidator extends TextValidator { + ContactNameValidator() + : super( + errorMessage: S.current.error_text_contact_name, + pattern: '''[^`,'"]''', + minLength: 1, + maxLength: 32); +} diff --git a/lib/core/contact_service.dart b/lib/core/contact_service.dart new file mode 100644 index 000000000..73a5bed27 --- /dev/null +++ b/lib/core/contact_service.dart @@ -0,0 +1,38 @@ +import 'package:hive/hive.dart'; +import 'package:cake_wallet/store/contact_list_store.dart'; +import 'package:cake_wallet/src/domain/common/contact.dart'; + +class ContactService { + ContactService(this.contactSource, this.contactListStore) { + _forceUpdateContactListStore(); + } + + final Box contactSource; + final ContactListStore contactListStore; + + Future add(Contact contact) async { + await contactSource.add(contact); + contactListStore.contacts.add(contact); + } + + Future update(Contact contact) async { + await contact.save(); + final index = contactListStore.contacts.indexOf(contact) ?? -1; + + if (index >= 0) { + _forceUpdateContactListStore(); + } else { + contactListStore.contacts.add(contact); + } + } + + Future delete(Contact contact) async { + await contact.delete(); + contactListStore.contacts.remove(contact); + } + + void _forceUpdateContactListStore() { + contactListStore.contacts.clear(); + contactListStore.contacts.addAll(contactSource.values); + } +} diff --git a/lib/core/key_service.dart b/lib/core/key_service.dart new file mode 100644 index 000000000..85185be72 --- /dev/null +++ b/lib/core/key_service.dart @@ -0,0 +1,25 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:cake_wallet/src/domain/common/secret_store_key.dart'; +import 'package:cake_wallet/src/domain/common/encrypt.dart'; + +class KeyService { + KeyService(this._secureStorage); + + final FlutterSecureStorage _secureStorage; + + Future getWalletPassword({String walletName}) async { + final key = generateStoreKeyFor( + key: SecretStoreKey.moneroWalletPassword, walletName: walletName); + final encodedPassword = await _secureStorage.read(key: key); + + return decodeWalletPassword(password: encodedPassword); + } + + Future saveWalletPassword({String walletName, String password}) async { + final key = generateStoreKeyFor( + key: SecretStoreKey.moneroWalletPassword, walletName: walletName); + final encodedPassword = encodeWalletPassword(password: password); + + await _secureStorage.write(key: key, value: encodedPassword); + } +} diff --git a/lib/core/monero_account_label_validator.dart b/lib/core/monero_account_label_validator.dart new file mode 100644 index 000000000..9d06a437c --- /dev/null +++ b/lib/core/monero_account_label_validator.dart @@ -0,0 +1,13 @@ +import 'package:flutter/foundation.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/core/validator.dart'; +import 'package:cake_wallet/src/domain/common/crypto_currency.dart'; + +class MoneroLabelValidator extends TextValidator { + MoneroLabelValidator({@required CryptoCurrency type}) + : super( + errorMessage: S.current.error_text_account_name, + pattern: '^[a-zA-Z0-9_]{1,15}\$', + minLength: 1, + maxLength: 15); +} diff --git a/lib/core/node_address_validator.dart b/lib/core/node_address_validator.dart new file mode 100644 index 000000000..676a768ea --- /dev/null +++ b/lib/core/node_address_validator.dart @@ -0,0 +1,10 @@ +import 'package:cake_wallet/core/validator.dart'; +import 'package:cake_wallet/generated/i18n.dart'; + +class NodeAddressValidator extends TextValidator { + NodeAddressValidator() + : super( + errorMessage: S.current.error_text_node_address, + pattern: + '^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\$|^[0-9a-zA-Z.]+\$'); +} diff --git a/lib/core/node_port_validator.dart b/lib/core/node_port_validator.dart new file mode 100644 index 000000000..b7a2f0221 --- /dev/null +++ b/lib/core/node_port_validator.dart @@ -0,0 +1,12 @@ +import 'package:flutter/foundation.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/core/validator.dart'; + +class NodePortValidator extends TextValidator { + NodePortValidator() + : super( + errorMessage: S.current.error_text_node_port, + minLength: 1, + maxLength: 5, + pattern: '^[0-9]'); +} diff --git a/lib/core/validator.dart b/lib/core/validator.dart index 826100abf..066367eed 100644 --- a/lib/core/validator.dart +++ b/lib/core/validator.dart @@ -12,11 +12,16 @@ abstract class Validator { class TextValidator extends Validator { TextValidator( - {this.minLength, this.maxLength, this.pattern, String errorMessage}) + {this.minLength, + this.maxLength, + this.pattern, + this.length, + String errorMessage}) : super(errorMessage: errorMessage); final int minLength; final int maxLength; + final List length; String pattern; @override @@ -25,8 +30,9 @@ class TextValidator extends Validator { return true; } - return value.length > minLength && - (maxLength > 0 ? (value.length <= maxLength) : true) && + return value.length > (minLength ?? 0) && + (length?.contains(value.length) ?? true) && + ((maxLength ?? 0) > 0 ? (value.length <= maxLength) : true) && (pattern != null ? match(value) : true); } diff --git a/lib/core/wallet_base.dart b/lib/core/wallet_base.dart index 6ee4d2572..dfbf31aef 100644 --- a/lib/core/wallet_base.dart +++ b/lib/core/wallet_base.dart @@ -1,19 +1,31 @@ import 'package:flutter/foundation.dart'; import 'package:cake_wallet/core/transaction_history.dart'; +import 'package:cake_wallet/src/domain/common/crypto_currency.dart'; +import 'package:cake_wallet/src/domain/common/sync_status.dart'; import 'package:cake_wallet/src/domain/common/node.dart'; import 'package:cake_wallet/src/domain/common/wallet_type.dart'; abstract class WalletBase { WalletType type; + CryptoCurrency currency; + String get name; String address; BalaceType balance; + SyncStatus syncStatus; + + String get seed; + + Object get keys; + TransactionHistoryBase transactionHistory; + String get id => walletTypeToString(type).toLowerCase() + '_' + name; + Future connectToNode({@required Node node}); Future startSync(); diff --git a/lib/core/wallet_creation_service.dart b/lib/core/wallet_creation_service.dart index c020ce266..5c5cb43df 100644 --- a/lib/core/wallet_creation_service.dart +++ b/lib/core/wallet_creation_service.dart @@ -1,6 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:cake_wallet/core/key_service.dart'; import 'package:cake_wallet/core/generate_wallet_password.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/core/wallet_credentials.dart'; @@ -8,14 +9,13 @@ import 'package:cake_wallet/bitcoin/bitcoin_wallet_service.dart'; import 'package:cake_wallet/monero/monero_wallet_service.dart'; import 'package:cake_wallet/core/wallet_service.dart'; import 'package:cake_wallet/src/domain/common/wallet_type.dart'; -import 'package:cake_wallet/src/domain/common/secret_store_key.dart'; -import 'package:cake_wallet/src/domain/common/encrypt.dart'; class WalletCreationService { WalletCreationService( {WalletType initialType, this.appStore, this.secureStorage, + this.keyService, this.sharedPreferences}) : type = initialType { if (type != null) { @@ -27,10 +27,7 @@ class WalletCreationService { final AppStore appStore; final FlutterSecureStorage secureStorage; final SharedPreferences sharedPreferences; - -// final WalletService walletService; -// final Box walletInfoSource; - + final KeyService keyService; WalletService _service; void changeWalletType({@required WalletType type}) { @@ -51,7 +48,8 @@ class WalletCreationService { Future create(WalletCredentials credentials) async { final password = generateWalletPassword(type); credentials.password = password; - await saveWalletPassword(password: password, walletName: credentials.name); + await keyService.saveWalletPassword( + password: password, walletName: credentials.name); final wallet = await _service.create(credentials); appStore.wallet = wallet; appStore.authenticationStore.allowed(); @@ -60,7 +58,8 @@ class WalletCreationService { Future restoreFromKeys(WalletCredentials credentials) async { final password = generateWalletPassword(type); credentials.password = password; - await saveWalletPassword(password: password, walletName: credentials.name); + await keyService.saveWalletPassword( + password: password, walletName: credentials.name); final wallet = await _service.restoreFromKeys(credentials); appStore.wallet = wallet; appStore.authenticationStore.allowed(); @@ -69,25 +68,10 @@ class WalletCreationService { Future restoreFromSeed(WalletCredentials credentials) async { final password = generateWalletPassword(type); credentials.password = password; - await saveWalletPassword(password: password, walletName: credentials.name); + await keyService.saveWalletPassword( + password: password, walletName: credentials.name); final wallet = await _service.restoreFromSeed(credentials); appStore.wallet = wallet; appStore.authenticationStore.allowed(); } - - Future getWalletPassword({String walletName}) async { - final key = generateStoreKeyFor( - key: SecretStoreKey.moneroWalletPassword, walletName: walletName); - final encodedPassword = await secureStorage.read(key: key); - - return decodeWalletPassword(password: encodedPassword); - } - - Future saveWalletPassword({String walletName, String password}) async { - final key = generateStoreKeyFor( - key: SecretStoreKey.moneroWalletPassword, walletName: walletName); - final encodedPassword = encodeWalletPassword(password: password); - - await secureStorage.write(key: key, value: encodedPassword); - } } diff --git a/lib/di.dart b/lib/di.dart index 237b211bd..19a8bc357 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -1,19 +1,52 @@ +import 'package:cake_wallet/core/contact_service.dart'; +import 'package:cake_wallet/src/domain/common/contact.dart'; +import 'package:cake_wallet/src/domain/common/node.dart'; +import 'package:cake_wallet/src/screens/contact/contact_list_page.dart'; +import 'package:cake_wallet/src/screens/contact/contact_page.dart'; +import 'package:cake_wallet/src/screens/nodes/node_create_or_edit_page.dart'; +import 'package:cake_wallet/src/screens/nodes/nodes_list_page.dart'; +import 'package:cake_wallet/src/screens/seed/wallet_seed_page.dart'; +import 'package:cake_wallet/src/screens/settings/settings.dart'; +import 'package:cake_wallet/src/screens/wallet_keys/wallet_keys_page.dart'; +import 'package:cake_wallet/store/contact_list_store.dart'; +import 'package:cake_wallet/store/node_list_store.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/core/auth_service.dart'; +import 'package:cake_wallet/core/key_service.dart'; +import 'package:cake_wallet/monero/monero_wallet.dart'; +import 'package:cake_wallet/src/domain/common/wallet_info.dart'; +import 'package:cake_wallet/src/screens/monero_accounts/monero_account_list_page.dart'; +import 'package:cake_wallet/src/screens/monero_accounts/monero_account_edit_or_create_page.dart'; import 'package:cake_wallet/src/screens/auth/auth_page.dart'; import 'package:cake_wallet/src/screens/dashboard/dashboard_page.dart'; import 'package:cake_wallet/src/screens/receive/receive_page.dart'; +import 'package:cake_wallet/src/screens/send/send_page.dart'; import 'package:cake_wallet/src/screens/subaddress/address_edit_or_create_page.dart'; -import 'package:cake_wallet/view_model/address_list/address_edit_or_create_view_model.dart'; +import 'package:cake_wallet/src/screens/wallet_list/wallet_list_page.dart'; +import 'package:cake_wallet/store/wallet_list_store.dart'; +import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart'; +import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart'; +import 'package:cake_wallet/view_model/node_list/node_list_view_model.dart'; +import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart'; import 'package:cake_wallet/view_model/auth_view_model.dart'; import 'package:cake_wallet/view_model/dashboard_view_model.dart'; -import 'package:cake_wallet/view_model/address_list/address_list_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; +import 'package:cake_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart'; +import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart'; +import 'package:cake_wallet/view_model/send_view_model.dart'; +import 'package:cake_wallet/view_model/settings/settings_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_keys_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_seed_view_model.dart'; +import 'package:flutter/foundation.dart'; import 'package:get_it/get_it.dart'; -import 'package:http/http.dart'; +import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:cake_wallet/view_model/wallet_restoration_from_seed_vm.dart'; -import 'package:cake_wallet/core/wallet_base.dart'; import 'package:cake_wallet/core/wallet_creation_service.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/src/domain/common/wallet_type.dart'; @@ -22,24 +55,63 @@ import 'package:cake_wallet/store/authentication_store.dart'; final getIt = GetIt.instance; -ReactionDisposer _onCurrentWalletChangeReaction; +// FIXME: Move me. -void setup() { - getIt.registerSingleton(AuthenticationStore()); - getIt.registerSingleton( - AppStore(authenticationStore: getIt.get())); - getIt.registerSingleton(FlutterSecureStorage()); +Stream _onNodesSourceChange; +NodeListStore _nodeListStore; + +NodeListStore setupNodeListStore(Box nodeSource) { + if (_nodeListStore != null) { + return _nodeListStore; + } + + _nodeListStore = NodeListStore(); + _nodeListStore.replaceValues(nodeSource.values); + _onNodesSourceChange = nodeSource.watch(); + _onNodesSourceChange + .listen((_) => _nodeListStore.replaceValues(nodeSource.values)); + + return _nodeListStore; +} + +Future setup( + {Box walletInfoSource, + Box nodeSource, + Box contactSource}) async { getIt.registerSingletonAsync( () => SharedPreferences.getInstance()); + + final settingsStore = await SettingsStoreBase.load(nodeSource: nodeSource); + + getIt.registerSingleton(FlutterSecureStorage()); + getIt.registerSingleton(AuthenticationStore()); + getIt.registerSingleton(WalletListStore()); + getIt.registerSingleton(ContactListStore()); + getIt.registerSingleton(setupNodeListStore(nodeSource)); + getIt.registerSingleton(settingsStore); + getIt.registerSingleton(AppStore( + authenticationStore: getIt.get(), + walletList: getIt.get(), + settingsStore: getIt.get(), + contactListStore: getIt.get(), + nodeListStore: getIt.get())); + getIt.registerSingleton( + ContactService(contactSource, getIt.get().contactListStore)); + getIt.registerFactory( + () => KeyService(getIt.get())); + getIt.registerFactoryParam( (type, _) => WalletCreationService( initialType: type, appStore: getIt.get(), + keyService: getIt.get(), secureStorage: getIt.get(), sharedPreferences: getIt.get())); getIt.registerFactoryParam((type, _) => - WalletNewVM(getIt.get(param1: type), type: type)); + WalletNewVM( + getIt.get(param1: type), walletInfoSource, + type: type)); getIt .registerFactoryParam((args, _) { @@ -48,14 +120,12 @@ void setup() { final mnemonic = args[2] as String; return WalletRestorationFromSeedVM( - getIt.get(param1: type), - type: type, - language: language, - seed: mnemonic); + getIt.get(param1: type), walletInfoSource, + type: type, language: language, seed: mnemonic); }); - getIt.registerFactory( - () => AddressListViewModel(wallet: getIt.get().wallet)); + getIt.registerFactory( + () => WalletAddressListViewModel(wallet: getIt.get().wallet)); getIt.registerFactory( () => DashboardViewModel(appStore: getIt.get())); @@ -68,38 +138,122 @@ void setup() { authService: getIt.get(), sharedPreferences: getIt.get())); - getIt.registerFactory(() => AuthPage( - authViewModel: getIt.get(), - onAuthenticationFinished: (isAuthenticated, __) { - if (isAuthenticated) { - getIt.get().allowed(); - } - }, - closable: false)); + getIt.registerFactory( + () => AuthPage( + authViewModel: getIt.get(), + onAuthenticationFinished: (isAuthenticated, __) { + if (isAuthenticated) { + getIt.get().allowed(); + } + }, + closable: false), + instanceName: 'login'); - getIt.registerFactory(() => DashboardPage( - walletViewModel: getIt.get(), - )); + getIt + .registerFactoryParam( + (onAuthFinished, _) => AuthPage( + authViewModel: getIt.get(), + onAuthenticationFinished: onAuthFinished, + closable: false)); - getIt.registerFactory(() => - ReceivePage(addressListViewModel: getIt.get())); + getIt.registerFactory( + () => DashboardPage(walletViewModel: getIt.get())); - getIt.registerFactoryParam( - (dynamic item, _) => AddressEditOrCreateViewModel( + getIt.registerFactory(() => ReceivePage( + addressListViewModel: getIt.get())); + + getIt.registerFactoryParam( + (dynamic item, _) => WalletAddressEditOrCreateViewModel( wallet: getIt.get().wallet, item: item)); getIt.registerFactoryParam( (dynamic item, _) => AddressEditOrCreatePage( addressEditOrCreateViewModel: - getIt.get(param1: item))); + getIt.get(param1: item))); - final appStore = getIt.get(); + getIt.registerFactory(() => SendViewModel( + getIt.get().wallet, getIt.get().settingsStore)); - _onCurrentWalletChangeReaction ??= - reaction((_) => appStore.wallet, (WalletBase wallet) async { - print('Wallet name ${wallet.name}'); - await getIt - .get() - .setString('current_wallet_name', wallet.name); + getIt.registerFactory( + () => SendPage(sendViewModel: getIt.get())); + + getIt.registerFactory(() => WalletListViewModel( + walletInfoSource, getIt.get(), getIt.get())); + + getIt.registerFactory(() => + WalletListPage(walletListViewModel: getIt.get())); + + getIt.registerFactory(() { + final wallet = getIt.get().wallet; + + if (wallet is MoneroWallet) { + return MoneroAccountListViewModel(wallet); + } + + // FIXME: throw exception. + return null; }); + + getIt.registerFactory(() => MoneroAccountListPage( + accountListViewModel: getIt.get())); + + getIt.registerFactory(() { + final wallet = getIt.get().wallet; + + if (wallet is MoneroWallet) { + return MoneroAccountEditOrCreateViewModel(wallet.accountList); + } + + // FIXME: throw exception. + return null; + }); + + getIt.registerFactory(() => MoneroAccountEditOrCreatePage( + moneroAccountCreationViewModel: + getIt.get())); + + getIt.registerFactory( + () => SettingsViewModel(getIt.get().settingsStore)); + + getIt.registerFactory(() => SettingsPage(getIt.get())); + + getIt + .registerFactory(() => WalletSeedViewModel(getIt.get().wallet)); + + getIt.registerFactoryParam( + (VoidCallback callback, _) => WalletSeedPage( + getIt.get(), + onCloseCallback: callback)); + + getIt + .registerFactory(() => WalletKeysViewModel(getIt.get().wallet)); + + getIt.registerFactory(() => WalletKeysPage(getIt.get())); + + getIt.registerFactoryParam( + (Contact contact, _) => ContactViewModel( + getIt.get(), getIt.get().wallet, + contact: contact)); + + getIt.registerFactory(() => ContactListViewModel( + getIt.get().contactListStore, getIt.get())); + + getIt.registerFactory( + () => ContactListPage(getIt.get())); + + getIt.registerFactoryParam((Contact contact, _) => + ContactPage(getIt.get(param1: contact))); + + getIt.registerFactory(() => NodeListViewModel( + getIt.get().nodeListStore, + nodeSource, + getIt.get().wallet)); + + getIt.registerFactory(() => NodeListPage(getIt.get())); + + getIt.registerFactory(() => + NodeCreateOrEditViewModel(nodeSource, getIt.get().wallet)); + + getIt.registerFactory( + () => NodeCreateOrEditPage(getIt.get())); } diff --git a/lib/generated/i18n.dart b/lib/generated/i18n.dart index e8ced3277..34f3bbf40 100644 --- a/lib/generated/i18n.dart +++ b/lib/generated/i18n.dart @@ -24,6 +24,7 @@ class S implements WidgetsLocalizations { String get account => "Account"; String get accounts => "Accounts"; String get accounts_subaddresses => "Accounts and subaddresses"; + String get addresses => "Addresses"; String get add => "Add"; String get add_new_node => "Add new node"; String get add_new_word => "Add new word"; @@ -121,6 +122,8 @@ class S implements WidgetsLocalizations { String get payment_id => "Payment ID: "; String get pending => " (pending)"; String get pin_is_incorrect => "PIN is incorrect"; + String get placeholder_contacts => "Your contacts will be displayed here"; + String get placeholder_transactions => "Your transactions will be displayed here"; String get please_make_selection => "Please make selection below to create or recover your wallet."; String get please_select => "Please select:"; String get please_try_to_connect_to_another_node => "Please try to connect to another node"; @@ -392,6 +395,8 @@ class $de extends S { @override String get wallet_list_create_new_wallet => "Neue Wallet erstellen"; @override + String get placeholder_contacts => "Ihre Kontakte werden hier angezeigt"; + @override String get card_address => "Adresse:"; @override String get seed_language_portuguese => "Portugiesisch"; @@ -462,6 +467,8 @@ class $de extends S { @override String get settings_display_balance_as => "Kontostand anzeigen als"; @override + String get placeholder_transactions => "Ihre Transaktionen werden hier angezeigt"; + @override String get trade_details_provider => "Anbieter"; @override String get seed_language_japanese => "Japanisch"; @@ -636,6 +643,8 @@ class $de extends S { @override String get accounts_subaddresses => "Konten und Unteradressen"; @override + String get addresses => "Addressen"; + @override String get wallet_name => "Walletname"; @override String get error_text_payment_id => "Die Zahlungs-ID kann nur 16 bis 64 hexadezimale Zeichen enthalten"; @@ -1008,6 +1017,8 @@ class $hi extends S { @override String get wallet_list_create_new_wallet => "नया बटुआ बनाएँ"; @override + String get placeholder_contacts => "आपके संपर्क यहां प्रदर्शित होंगे"; + @override String get card_address => "पता:"; @override String get seed_language_portuguese => "पुर्तगाली"; @@ -1078,6 +1089,8 @@ class $hi extends S { @override String get settings_display_balance_as => "के रूप में संतुलन प्रदर्शित करें"; @override + String get placeholder_transactions => "आपके लेनदेन यहां प्रदर्शित होंगे"; + @override String get trade_details_provider => "प्रदाता"; @override String get seed_language_japanese => "जापानी"; @@ -1252,6 +1265,8 @@ class $hi extends S { @override String get accounts_subaddresses => "लेखा और उपदेस"; @override + String get addresses => "पतों"; + @override String get wallet_name => "बटुए का नाम"; @override String get error_text_payment_id => "पेमेंट आईडी केवल हेक्स में 16 से 64 चार्ट तक हो सकती है"; @@ -1624,6 +1639,8 @@ class $ru extends S { @override String get wallet_list_create_new_wallet => "Создать новый кошелёк"; @override + String get placeholder_contacts => "Ваши контакты будут отображаться здесь"; + @override String get card_address => "Адрес:"; @override String get seed_language_portuguese => "Португальский"; @@ -1694,6 +1711,8 @@ class $ru extends S { @override String get settings_display_balance_as => "Отображать баланс как"; @override + String get placeholder_transactions => "Ваши транзакции будут отображаться здесь"; + @override String get trade_details_provider => "Провайдер"; @override String get seed_language_japanese => "Японский"; @@ -1868,6 +1887,8 @@ class $ru extends S { @override String get accounts_subaddresses => "Аккаунты и субадреса"; @override + String get addresses => "Адреса"; + @override String get wallet_name => "Имя кошелька"; @override String get error_text_payment_id => "Идентификатор платежа может содержать от 16 до 64 символов в hex"; @@ -2240,6 +2261,8 @@ class $ko extends S { @override String get wallet_list_create_new_wallet => "새 월렛 만들기"; @override + String get placeholder_contacts => "연락처가 여기에 표시됩니다"; + @override String get card_address => "주소:"; @override String get seed_language_portuguese => "포르투갈 인"; @@ -2310,6 +2333,8 @@ class $ko extends S { @override String get settings_display_balance_as => "잔액 표시"; @override + String get placeholder_transactions => "거래가 여기에 표시됩니다"; + @override String get trade_details_provider => "공급자"; @override String get seed_language_japanese => "일본어"; @@ -2484,6 +2509,8 @@ class $ko extends S { @override String get accounts_subaddresses => "계정 및 하위 주소"; @override + String get addresses => "구애"; + @override String get wallet_name => "지갑 이름"; @override String get error_text_payment_id => "지불 ID는 16 ~ 64 자의 16 진 문자 만 포함 할 수 있습니다"; @@ -2856,6 +2883,8 @@ class $pt extends S { @override String get wallet_list_create_new_wallet => "Criar nova carteira"; @override + String get placeholder_contacts => "Seus contatos serão exibidos aqui"; + @override String get card_address => "Endereço:"; @override String get seed_language_portuguese => "Português"; @@ -2926,6 +2955,8 @@ class $pt extends S { @override String get settings_display_balance_as => "Saldo a exibir"; @override + String get placeholder_transactions => "Suas transações serão exibidas aqui"; + @override String get trade_details_provider => "Provedor"; @override String get seed_language_japanese => "Japonês"; @@ -3100,6 +3131,8 @@ class $pt extends S { @override String get accounts_subaddresses => "Contas e sub-endereços"; @override + String get addresses => "Endereços"; + @override String get wallet_name => "Nome da carteira"; @override String get error_text_payment_id => "O ID de pagamento pode conter apenas de 16 a 64 caracteres em hexadecimal"; @@ -3472,6 +3505,8 @@ class $uk extends S { @override String get wallet_list_create_new_wallet => "Створити новий гаманець"; @override + String get placeholder_contacts => "Тут будуть показані ваші контакти"; + @override String get card_address => "Адреса:"; @override String get seed_language_portuguese => "Португальська"; @@ -3542,6 +3577,8 @@ class $uk extends S { @override String get settings_display_balance_as => "Відображати баланс як"; @override + String get placeholder_transactions => "Тут відображатимуться ваші транзакції"; + @override String get trade_details_provider => "Провайдер"; @override String get seed_language_japanese => "Японська"; @@ -3716,6 +3753,8 @@ class $uk extends S { @override String get accounts_subaddresses => "Акаунти та субадреси"; @override + String get addresses => "Адреси"; + @override String get wallet_name => "Ім'я гаманця"; @override String get error_text_payment_id => "Ідентифікатор платежу може містити від 16 до 64 символів в hex"; @@ -4088,6 +4127,8 @@ class $ja extends S { @override String get wallet_list_create_new_wallet => "新しいウォレットを作成"; @override + String get placeholder_contacts => "連絡先はここに表示されます"; + @override String get card_address => "住所:"; @override String get seed_language_portuguese => "ポルトガル語"; @@ -4158,6 +4199,8 @@ class $ja extends S { @override String get settings_display_balance_as => "残高を表示"; @override + String get placeholder_transactions => "あなたの取引はここに表示されます"; + @override String get trade_details_provider => "プロバイダー"; @override String get seed_language_japanese => "日本語"; @@ -4332,6 +4375,8 @@ class $ja extends S { @override String get accounts_subaddresses => "アカウントとサブアドレス"; @override + String get addresses => "アドレス"; + @override String get wallet_name => "ウォレット名"; @override String get error_text_payment_id => "支払いIDには、16進数で16〜64文字しか含めることができません"; @@ -4708,6 +4753,8 @@ class $pl extends S { @override String get wallet_list_create_new_wallet => "Utwórz nowy portfel"; @override + String get placeholder_contacts => "Twoje kontakty zostaną wyświetlone tutaj"; + @override String get card_address => "Adres:"; @override String get seed_language_portuguese => "Portugalski"; @@ -4778,6 +4825,8 @@ class $pl extends S { @override String get settings_display_balance_as => "Wyświetl saldo jako"; @override + String get placeholder_transactions => "Twoje transakcje zostaną wyświetlone tutaj"; + @override String get trade_details_provider => "Dostawca"; @override String get seed_language_japanese => "Japoński"; @@ -4952,6 +5001,8 @@ class $pl extends S { @override String get accounts_subaddresses => "Konta i podadresy"; @override + String get addresses => "Adresy"; + @override String get wallet_name => "Nazwa portfela"; @override String get error_text_payment_id => "ID może zawierać od 16 do 64 znaków w formacie szesnastkowym"; @@ -5324,6 +5375,8 @@ class $es extends S { @override String get wallet_list_create_new_wallet => "Crear nueva billetera"; @override + String get placeholder_contacts => "Tus contactos se mostrarán aquí"; + @override String get card_address => "Dirección:"; @override String get seed_language_portuguese => "Portugués"; @@ -5394,6 +5447,8 @@ class $es extends S { @override String get settings_display_balance_as => "Mostrar saldo como"; @override + String get placeholder_transactions => "Sus transacciones se mostrarán aquí"; + @override String get trade_details_provider => "Proveedor"; @override String get seed_language_japanese => "Japonés"; @@ -5568,6 +5623,8 @@ class $es extends S { @override String get accounts_subaddresses => "Cuentas y subdirecciones"; @override + String get addresses => "Direcciones"; + @override String get wallet_name => "Nombre de la billetera"; @override String get error_text_payment_id => "La ID de pago solo puede contener de 16 a 64 caracteres en hexadecimal"; @@ -5940,6 +5997,8 @@ class $nl extends S { @override String get wallet_list_create_new_wallet => "Maak een nieuwe portemonnee"; @override + String get placeholder_contacts => "Je contacten worden hier weergegeven"; + @override String get card_address => "Adres:"; @override String get seed_language_portuguese => "Portugees"; @@ -6010,6 +6069,8 @@ class $nl extends S { @override String get settings_display_balance_as => "Toon saldo als"; @override + String get placeholder_transactions => "Uw transacties worden hier weergegeven"; + @override String get trade_details_provider => "Leverancier"; @override String get seed_language_japanese => "Japans"; @@ -6184,6 +6245,8 @@ class $nl extends S { @override String get accounts_subaddresses => "Accounts en subadressen"; @override + String get addresses => "Adressen"; + @override String get wallet_name => "Portemonnee naam"; @override String get error_text_payment_id => "Betalings-ID kan alleen 16 tot 64 tekens bevatten in hexadecimale volgorde"; @@ -6556,6 +6619,8 @@ class $zh extends S { @override String get wallet_list_create_new_wallet => "创建新钱包"; @override + String get placeholder_contacts => "您的聯繫人將顯示在這裡"; + @override String get card_address => "地址:"; @override String get seed_language_portuguese => "葡萄牙語"; @@ -6626,6 +6691,8 @@ class $zh extends S { @override String get settings_display_balance_as => "将余额显示为"; @override + String get placeholder_transactions => "您的交易將顯示在這裡"; + @override String get trade_details_provider => "提供者"; @override String get seed_language_japanese => "日本"; @@ -6800,6 +6867,8 @@ class $zh extends S { @override String get accounts_subaddresses => "帳戶和子地址"; @override + String get addresses => "地址"; + @override String get wallet_name => "钱包名称"; @override String get error_text_payment_id => "付款ID只能包含16到64个字符(十六进制)"; diff --git a/lib/main.dart b/lib/main.dart index 706c1f91b..d4e59fbe2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -54,8 +54,6 @@ import 'package:cake_wallet/src/stores/seed_language/seed_language_store.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - setup(); - final appDir = await getApplicationDocumentsDirectory(); Hive.init(appDir.path); Hive.registerAdapter(ContactAdapter()); @@ -87,6 +85,13 @@ void main() async { final exchangeTemplates = await Hive.openBox(ExchangeTemplate.boxName); + await initialSetup( + sharedPreferences: await SharedPreferences.getInstance(), + nodes: nodes, + walletInfoSource: walletInfoSource, + contactSource: contacts, + initialMigrationVersion: 3); + final sharedPreferences = await SharedPreferences.getInstance(); final walletService = WalletService(); final walletListService = WalletListService( @@ -96,15 +101,6 @@ void main() async { sharedPreferences: sharedPreferences); final userService = UserService( sharedPreferences: sharedPreferences, secureStorage: secureStorage); -// final authenticationStore = AuthenticationStore(userService: userService); - - await initialSetup( - sharedPreferences: sharedPreferences, - walletListService: walletListService, - nodes: nodes, -// authStore: authenticationStore, - initialMigrationVersion: 2); - final settingsStore = await SettingsStoreBase.load( nodes: nodes, sharedPreferences: sharedPreferences, @@ -129,7 +125,6 @@ void main() async { final walletCreationService = WalletCreationService(); final authService = AuthService(); - setReactions( settingsStore: settingsStore, priceStore: priceStore, @@ -164,23 +159,20 @@ void main() async { } Future initialSetup( - {WalletListService walletListService, - SharedPreferences sharedPreferences, - Box nodes, -// AuthenticationStore authStore, - int initialMigrationVersion = 1, - WalletType initialWalletType = WalletType.bitcoin}) async { - await walletListService.changeWalletManger(walletType: initialWalletType); + {@required SharedPreferences sharedPreferences, + @required Box nodes, + @required Box walletInfoSource, + @required Box contactSource, + int initialMigrationVersion = 3}) async { await defaultSettingsMigration( version: initialMigrationVersion, sharedPreferences: sharedPreferences, nodes: nodes); -// await authStore.started(); + await setup( + walletInfoSource: walletInfoSource, + nodeSource: nodes, + contactSource: contactSource); await bootstrap(); -// final authenticationStore = getIt.get(); - // FIXME -// authenticationStore.state = AuthenticationState.denied; - monero_wallet.onStartup(); } @@ -216,8 +208,6 @@ class MaterialAppWithTheme extends StatelessWidget { final syncStore = Provider.of(context); final balanceStore = Provider.of(context); final theme = Provider.of(context); - final statusBarColor = - settingsStore.isDarkTheme ? Colors.black : Colors.white; final currentLanguage = Provider.of(context); final contacts = Provider.of>(context); final nodes = Provider.of>(context); @@ -225,8 +215,17 @@ class MaterialAppWithTheme extends StatelessWidget { final transactionDescriptions = Provider.of>(context); - SystemChrome.setSystemUIOverlayStyle( - SystemUiOverlayStyle(statusBarColor: statusBarColor)); + final statusBarColor = + settingsStore.isDarkTheme ? Colors.black : Colors.white; + final statusBarBrightness = + settingsStore.isDarkTheme ? Brightness.light : Brightness.dark; + final statusBarIconBrightness = + settingsStore.isDarkTheme ? Brightness.light : Brightness.dark; + + SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( + statusBarColor: statusBarColor, + statusBarBrightness: statusBarBrightness, + statusBarIconBrightness: statusBarIconBrightness)); return MaterialApp( debugShowCheckedModeBanner: false, @@ -258,4 +257,4 @@ class MaterialAppWithTheme extends StatelessWidget { authenticationStore: getIt.get(), )); } -} +} \ No newline at end of file diff --git a/lib/monero/monero_account_list.dart b/lib/monero/monero_account_list.dart new file mode 100644 index 000000000..cc49af7bb --- /dev/null +++ b/lib/monero/monero_account_list.dart @@ -0,0 +1,76 @@ +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/src/domain/monero/account.dart'; +import 'package:cw_monero/account_list.dart' as account_list; + +part 'monero_account_list.g.dart'; + +class MoneroAccountList = MoneroAccountListBase with _$MoneroAccountList; + +abstract class MoneroAccountListBase with Store { + MoneroAccountListBase() + : accounts = ObservableList(), + _isRefreshing = false, + _isUpdating = false { + refresh(); + print(account_list.accountSizeNative()); + } + + @observable + ObservableList accounts; + bool _isRefreshing; + bool _isUpdating; + + Future update() async { + if (_isUpdating) { + return; + } + + try { + _isUpdating = true; + refresh(); + final accounts = getAll(); + + if (accounts.isNotEmpty) { + this.accounts.clear(); + this.accounts.addAll(accounts); + } + + _isUpdating = false; + } catch (e) { + _isUpdating = false; + rethrow; + } + } + + List getAll() => account_list + .getAllAccount() + .map((accountRow) => Account.fromRow(accountRow)) + .toList(); + + Future addAccount({String label}) async { + await account_list.addAccount(label: label); + await update(); + } + + Future setLabelAccount({int accountIndex, String label}) async { + await account_list.setLabelForAccount( + accountIndex: accountIndex, label: label); + await update(); + } + + void refresh() { + if (_isRefreshing) { + return; + } + + try { + _isRefreshing = true; + account_list.refreshAccounts(); + _isRefreshing = false; + } catch (e) { + _isRefreshing = false; + print(e); + rethrow; + } + } +} diff --git a/lib/monero/monero_wallet.dart b/lib/monero/monero_wallet.dart index 30fb00916..e36e6cb29 100644 --- a/lib/monero/monero_wallet.dart +++ b/lib/monero/monero_wallet.dart @@ -1,18 +1,21 @@ -import 'package:cake_wallet/src/domain/common/wallet_type.dart'; import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; +import 'package:cw_monero/wallet.dart'; +import 'package:cw_monero/wallet.dart' as monero_wallet; +import 'package:cake_wallet/monero/monero_wallet_keys.dart'; import 'package:cake_wallet/monero/monero_balance.dart'; import 'package:cake_wallet/monero/monero_transaction_history.dart'; import 'package:cake_wallet/monero/monero_subaddress_list.dart'; +import 'package:cake_wallet/monero/monero_account_list.dart'; import 'package:cake_wallet/core/wallet_base.dart'; import 'package:cake_wallet/core/transaction_history.dart'; +import 'package:cake_wallet/src/domain/common/crypto_currency.dart'; +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; import 'package:cake_wallet/src/domain/common/sync_status.dart'; import 'package:cake_wallet/src/domain/monero/account.dart'; import 'package:cake_wallet/src/domain/monero/account_list.dart'; import 'package:cake_wallet/src/domain/monero/subaddress.dart'; -import 'package:cw_monero/wallet.dart'; import 'package:cake_wallet/src/domain/common/node.dart'; -import 'package:cw_monero/wallet.dart' as monero_wallet; part 'monero_wallet.g.dart'; @@ -20,15 +23,26 @@ class MoneroWallet = MoneroWalletBase with _$MoneroWallet; abstract class MoneroWalletBase extends WalletBase with Store { MoneroWalletBase({String filename, this.isRecovery = false}) - : transactionHistory = MoneroTransactionHistory() { + : transactionHistory = MoneroTransactionHistory(), + accountList = MoneroAccountList(), + subaddressList = MoneroSubaddressList() { _filename = filename; - accountList = AccountList(); - subaddressList = MoneroSubaddressList(); balance = MoneroBalance( fullBalance: monero_wallet.getFullBalance(accountIndex: 0), unlockedBalance: monero_wallet.getFullBalance(accountIndex: 0)); + currency = CryptoCurrency.xmr; + type = WalletType.monero; + _rct = reaction( + (_) => syncStatus, (SyncStatus status) => print(status.toString())); + _onAccountChangeReaction = reaction((_) => account, (Account account) { + subaddressList.update(accountIndex: account.id); + subaddress = subaddressList.subaddresses.first; + address = subaddress.address; + }); } + ReactionDisposer _rct; + @override final MoneroTransactionHistory transactionHistory; @@ -44,26 +58,33 @@ abstract class MoneroWalletBase extends WalletBase with Store { @override String get name => _filename.split('/').last; - @override - final type = WalletType.monero; - @override @observable String address; + @override + String get seed => monero_wallet.getSeed(); + + @override + MoneroWalletKeys get keys => MoneroWalletKeys( + privateSpendKey: monero_wallet.getSecretSpendKey(), + privateViewKey: monero_wallet.getSecretViewKey(), + publicSpendKey: monero_wallet.getPublicSpendKey(), + publicViewKey: monero_wallet.getPublicViewKey()); + + final MoneroSubaddressList subaddressList; + + final MoneroAccountList accountList; + bool isRecovery; - MoneroSubaddressList subaddressList; - - AccountList accountList; - String _filename; - SyncListner _listener; + ReactionDisposer _onAccountChangeReaction; Future init() async { await accountList.update(); - account = accountList.getAll().first; + account = accountList.accounts.first; subaddressList.update(accountIndex: account.id ?? 0); subaddress = subaddressList.getAll().first; balance = MoneroBalance( @@ -76,10 +97,13 @@ abstract class MoneroWalletBase extends WalletBase with Store { void close() { _listener?.stop(); + _onAccountChangeReaction?.reaction?.dispose(); } @override Future connectToNode({@required Node node}) async { + final node = Node(uri: 'xmr-node-uk.cakewallet.com:18081'); + try { syncStatus = ConnectingSyncStatus(); await monero_wallet.setupNode( @@ -147,6 +171,7 @@ abstract class MoneroWalletBase extends WalletBase with Store { _listener?.stop(); _listener = monero_wallet.setListeners( _onNewBlock, _onNeedToRefresh, _onNewTransaction); + _listener.start(); } void _askForUpdateBalance() { diff --git a/lib/monero/monero_wallet_keys.dart b/lib/monero/monero_wallet_keys.dart new file mode 100644 index 000000000..f0a96bfd3 --- /dev/null +++ b/lib/monero/monero_wallet_keys.dart @@ -0,0 +1,12 @@ +class MoneroWalletKeys { + const MoneroWalletKeys( + {this.privateSpendKey, + this.privateViewKey, + this.publicSpendKey, + this.publicViewKey}); + + final String publicViewKey; + final String privateViewKey; + final String publicSpendKey; + final String privateSpendKey; +} \ No newline at end of file diff --git a/lib/palette.dart b/lib/palette.dart index 8d06a823e..9d0c5be32 100644 --- a/lib/palette.dart +++ b/lib/palette.dart @@ -12,7 +12,9 @@ class Palette { static const Color blue = Color.fromRGBO(88, 143, 252, 1.0); static const Color darkLavender = Color.fromRGBO(225, 238, 250, 1.0); static const Color nightBlue = Color.fromRGBO(46, 57, 96, 1.0); + // FIXME: Rename. static const Color eee = Color.fromRGBO(236, 239, 245, 1.0); + static const Color xxx = Color.fromRGBO(72, 89, 109, 1); } class PaletteDark { @@ -28,4 +30,7 @@ class PaletteDark { static const Color headerNightBlue = Color.fromRGBO(41, 52, 84, 1.0); // menuHeader static const Color lightNightBlue = Color.fromRGBO(48, 59, 95, 1.0); // menuList static const Color moderatePurpleBlue = Color.fromRGBO(57, 74, 95, 1.0); // selectButtonText + // FIXME: Rename. + static const Color eee = Color.fromRGBO(236, 239, 245, 1.0); + static const Color xxx = Color.fromRGBO(72, 89, 109, 1); } \ No newline at end of file diff --git a/lib/reactions/bootstrap.dart b/lib/reactions/bootstrap.dart index 36612d704..928fb41ba 100644 --- a/lib/reactions/bootstrap.dart +++ b/lib/reactions/bootstrap.dart @@ -1,9 +1,13 @@ +import 'package:cake_wallet/core/key_service.dart'; +import 'package:cake_wallet/src/domain/common/sync_status.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/di.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:cake_wallet/bitcoin/bitcoin_wallet_service.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart'; import 'package:cake_wallet/monero/monero_wallet_service.dart'; +import 'package:cake_wallet/core/wallet_base.dart'; import 'package:cake_wallet/core/wallet_service.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/authentication_store.dart'; @@ -11,22 +15,15 @@ import 'package:cake_wallet/src/domain/common/wallet_type.dart'; import 'package:cake_wallet/src/domain/common/secret_store_key.dart'; import 'package:cake_wallet/src/domain/common/encrypt.dart'; -// FIXME: move me -Future getWalletPassword({String walletName}) async { - final secureStorage = getIt.get(); - final key = generateStoreKeyFor( - key: SecretStoreKey.moneroWalletPassword, walletName: walletName); - final encodedPassword = await secureStorage.read(key: key); - - return decodeWalletPassword(password: encodedPassword); -} - // FIXME: move me Future loadCurrentWallet() async { final appStore = getIt.get(); final name = getIt.get().getString('current_wallet_name'); - final type = WalletType.monero; // FIXME - final password = await getWalletPassword(walletName: name); + final typeRaw = + getIt.get().getInt('current_wallet_type') ?? 0; + final type = deserializeFromInt(typeRaw); + final password = + await getIt.get().getWalletPassword(walletName: name); WalletService _service; switch (type) { @@ -45,6 +42,8 @@ Future loadCurrentWallet() async { } ReactionDisposer _initialAuthReaction; +ReactionDisposer _onCurrentWalletChangeReaction; +ReactionDisposer _onWalletSyncStatusChangeReaction; Future bootstrap() async { final authenticationStore = getIt.get(); @@ -63,4 +62,24 @@ Future bootstrap() async { await loadCurrentWallet(); } }); + + _onCurrentWalletChangeReaction ??= + reaction((_) => getIt.get().wallet, (WalletBase wallet) async { + print('Wallet name ${wallet.name}'); + + _onWalletSyncStatusChangeReaction?.reaction?.dispose(); + _onWalletSyncStatusChangeReaction = when( + (_) => wallet.syncStatus is ConnectedSyncStatus, + () async => await wallet.startSync()); + + await getIt + .get() + .setString('current_wallet_name', wallet.name); + + await getIt + .get() + .setInt('current_wallet_type', serializeToInt(wallet.type)); + + await wallet.connectToNode(node: null); + }); } diff --git a/lib/router.dart b/lib/router.dart index cc2615004..23fe7ba4f 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/src/screens/seed/wallet_seed_page.dart'; import 'package:cake_wallet/view_model/wallet_new_vm.dart'; import 'package:cake_wallet/view_model/wallet_restoration_from_seed_vm.dart'; import 'package:flutter/cupertino.dart'; @@ -55,7 +56,7 @@ import 'package:cake_wallet/src/stores/price/price_store.dart'; // MARK: Import screens import 'package:cake_wallet/src/screens/auth/auth_page.dart'; -import 'package:cake_wallet/src/screens/nodes/new_node_page.dart'; +import 'package:cake_wallet/src/screens/nodes/node_create_or_edit_page.dart'; import 'package:cake_wallet/src/screens/nodes/nodes_list_page.dart'; import 'package:cake_wallet/src/screens/receive/receive_page.dart'; import 'package:cake_wallet/src/screens/subaddress/address_edit_or_create_page.dart'; @@ -70,10 +71,10 @@ import 'package:cake_wallet/src/screens/send/send_page.dart'; import 'package:cake_wallet/src/screens/disclaimer/disclaimer_page.dart'; import 'package:cake_wallet/src/screens/seed_language/seed_language_page.dart'; import 'package:cake_wallet/src/screens/transaction_details/transaction_details_page.dart'; -import 'package:cake_wallet/src/screens/accounts/account_page.dart'; -import 'package:cake_wallet/src/screens/address_book/address_book_page.dart'; -import 'package:cake_wallet/src/screens/address_book/contact_page.dart'; -import 'package:cake_wallet/src/screens/show_keys/show_keys_page.dart'; +import 'package:cake_wallet/src/screens/monero_accounts/monero_account_edit_or_create_page.dart'; +import 'package:cake_wallet/src/screens/contact/contact_list_page.dart'; +import 'package:cake_wallet/src/screens/contact/contact_page.dart'; +import 'package:cake_wallet/src/screens/wallet_keys/wallet_keys_page.dart'; import 'package:cake_wallet/src/screens/exchange_trade/exchange_confirm_page.dart'; import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_page.dart'; import 'package:cake_wallet/src/screens/subaddress/subaddress_list_page.dart'; @@ -86,7 +87,6 @@ import 'package:cake_wallet/src/screens/faq/faq_page.dart'; import 'package:cake_wallet/src/screens/trade_details/trade_details_page.dart'; import 'package:cake_wallet/src/screens/auth/create_unlock_page.dart'; import 'package:cake_wallet/src/screens/auth/create_login_page.dart'; -import 'package:cake_wallet/src/screens/seed/create_seed_page.dart'; import 'package:cake_wallet/src/screens/dashboard/create_dashboard_page.dart'; import 'package:cake_wallet/src/screens/welcome/create_welcome_page.dart'; import 'package:cake_wallet/src/screens/new_wallet/new_wallet_type_page.dart'; @@ -214,10 +214,8 @@ class Router { case Routes.seed: return MaterialPageRoute( - builder: (_) => createSeedPage( - settingsStore: settingsStore, - walletService: walletService, - callback: settings.arguments as void Function())); + builder: (_) => getIt.get( + param1: settings.arguments as VoidCallback)); case Routes.restoreWalletFromSeed: final args = settings.arguments as List; @@ -228,13 +226,7 @@ class Router { return CupertinoPageRoute( builder: (_) => - ProxyProvider( - update: (_, authStore, __) => WalletRestorationStore( - authStore: authStore, - sharedPreferences: sharedPreferences, - walletListService: walletListService), - child: RestoreWalletFromSeedPage( - type: type, language: language))); + RestoreWalletFromSeedPage(type: type, language: language)); case Routes.restoreWalletFromKeys: final args = settings.arguments as List; @@ -267,23 +259,7 @@ class Router { case Routes.send: return CupertinoPageRoute( - fullscreenDialog: true, - builder: (_) => MultiProvider(providers: [ - ProxyProvider( - update: (_, settingsStore, __) => BalanceStore( - walletService: walletService, - settingsStore: settingsStore, - priceStore: priceStore), - ), - Provider( - create: (_) => SyncStore(walletService: walletService), - ), - Provider( - create: (_) => SendStore( - walletService: walletService, - priceStore: priceStore, - transactionDescriptions: transactionDescriptions)), - ], child: SendPage())); + fullscreenDialog: true, builder: (_) => getIt.get()); case Routes.sendTemplate: return CupertinoPageRoute( @@ -330,25 +306,13 @@ class Router { case Routes.walletList: return MaterialPageRoute( fullscreenDialog: true, - builder: (_) => Provider( - create: (_) => WalletListStore( - walletListService: walletListService, - walletService: walletService), - child: WalletListPage())); + builder: (_) => getIt.get()); case Routes.auth: - return null; -// return MaterialPageRoute( -// fullscreenDialog: true, -// builder: (_) => Provider( -// create: (_) => AuthStore( -// sharedPreferences: sharedPreferences, -// userService: userService, -// walletService: walletService), -// child: AuthPage( -// onAuthenticationFinished: -// settings.arguments as OnAuthenticationFinished), -// )); + return MaterialPageRoute( + fullscreenDialog: true, + builder: (_) => getIt.get( + param1: settings.arguments as OnAuthenticationFinished)); case Routes.unlock: return MaterialPageRoute( @@ -361,17 +325,12 @@ class Router { settings.arguments as OnAuthenticationFinished)); case Routes.nodeList: - return CupertinoPageRoute(builder: (context) { - return Provider( - create: (_) => NodeListStore(nodesSource: nodes), - child: NodeListPage()); - }); + return CupertinoPageRoute( + builder: (_) => getIt.get()); case Routes.newNode: return CupertinoPageRoute( - builder: (_) => Provider( - create: (_) => NodeListStore(nodesSource: nodes), - child: NewNodePage())); + builder: (_) => getIt.get()); case Routes.login: return CupertinoPageRoute(builder: (context) { @@ -386,59 +345,25 @@ class Router { }); case Routes.accountCreation: - return CupertinoPageRoute(builder: (context) { - return Provider( - create: (_) => AccountListStore(walletService: walletService), - child: AccountPage(account: settings.arguments as Account)); - }); + return CupertinoPageRoute( + builder: (_) => getIt.get()); case Routes.addressBook: - return MaterialPageRoute(builder: (context) { - return MultiProvider( - providers: [ - Provider( - create: (_) => - AccountListStore(walletService: walletService)), - Provider(create: (_) => AddressBookStore(contacts: contacts)) - ], - child: AddressBookPage(), - ); - }); + return MaterialPageRoute( + builder: (_) => getIt.get()); case Routes.pickerAddressBook: - return MaterialPageRoute(builder: (context) { - return MultiProvider( - providers: [ - Provider( - create: (_) => - AccountListStore(walletService: walletService)), - Provider(create: (_) => AddressBookStore(contacts: contacts)) - ], - child: AddressBookPage(isEditable: false), - ); - }); + return MaterialPageRoute( + builder: (_) => getIt.get()); case Routes.addressBookAddContact: - return CupertinoPageRoute(builder: (context) { - return MultiProvider( - providers: [ - Provider( - create: (_) => - AccountListStore(walletService: walletService)), - Provider(create: (_) => AddressBookStore(contacts: contacts)) - ], - child: ContactPage(contact: settings.arguments as Contact), - ); - }); + return CupertinoPageRoute( + builder: (_) => + getIt.get(param1: settings.arguments as Contact)); case Routes.showKeys: return MaterialPageRoute( - builder: (context) { - return Provider( - create: (_) => WalletKeysStore(walletService: walletService), - child: ShowKeysPage(), - ); - }, + builder: (_) => getIt.get(), fullscreenDialog: true); case Routes.exchangeTrade: @@ -538,9 +463,7 @@ class Router { case Routes.settings: return MaterialPageRoute( - builder: (_) => Provider( - create: (_) => NodeListStore(nodesSource: nodes), - child: SettingsPage())); + builder: (_) => getIt.get()); case Routes.rescan: return MaterialPageRoute( diff --git a/lib/routes.dart b/lib/routes.dart index 79c086d3c..c9db819e8 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -19,7 +19,7 @@ class Routes { static const disclaimer = '/disclaimer'; static const readDisclaimer = '/read_disclaimer'; static const seedLanguage = '/seed_language'; - static const walletList = '/wallet_list'; + static const walletList = '/view_model.wallet_list'; static const auth = '/auth'; static const nodeList = '/node_list'; static const newNode = '/new_node_list'; diff --git a/lib/src/domain/bitcoin/bitcoin_amount_format.dart b/lib/src/domain/bitcoin/bitcoin_amount_format.dart index c816dedc2..de59547bb 100644 --- a/lib/src/domain/bitcoin/bitcoin_amount_format.dart +++ b/lib/src/domain/bitcoin/bitcoin_amount_format.dart @@ -3,4 +3,7 @@ import 'package:cake_wallet/src/domain/common/crypto_amount_format.dart'; const bitcoinAmountDivider = 100000000; double bitcoinAmountToDouble({int amount}) => - cryptoAmountToDouble(amount: amount, divider: bitcoinAmountDivider); \ No newline at end of file + cryptoAmountToDouble(amount: amount, divider: bitcoinAmountDivider); + +int doubleToBitcoinAmount(double amount) => + (amount * bitcoinAmountDivider).toInt(); diff --git a/lib/src/domain/common/contact.dart b/lib/src/domain/common/contact.dart index 350f45d32..faa4f8ecf 100644 --- a/lib/src/domain/common/contact.dart +++ b/lib/src/domain/common/contact.dart @@ -22,6 +22,12 @@ class Contact extends HiveObject { CryptoCurrency get type => CryptoCurrency.deserialize(raw: raw); + @override + bool operator ==(Object o) => o is Contact && o.key == key; + + @override + int get hashCode => key.hashCode; + void updateCryptoCurrency({@required CryptoCurrency currency}) => raw = currency.raw; } diff --git a/lib/src/domain/common/default_settings_migration.dart b/lib/src/domain/common/default_settings_migration.dart index bad9f69eb..d89223a4d 100644 --- a/lib/src/domain/common/default_settings_migration.dart +++ b/lib/src/domain/common/default_settings_migration.dart @@ -1,3 +1,5 @@ +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; +import 'package:cake_wallet/store/settings_store.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -14,7 +16,6 @@ Future defaultSettingsMigration( final currentVersion = sharedPreferences.getInt('current_default_settings_migration_version') ?? 0; - if (currentVersion >= version) { return; } @@ -28,10 +29,11 @@ Future defaultSettingsMigration( switch (version) { case 1: await sharedPreferences.setString( - 'current_fiat_currency', FiatCurrency.usd.toString()); + SettingsStoreBase.currentFiatCurrencyKey, FiatCurrency.usd.toString()); await sharedPreferences.setInt( - 'current_fee_priority', TransactionPriority.standart.raw); - await sharedPreferences.setInt('current_balance_display_mode', + SettingsStoreBase.currentTransactionPriorityKey, TransactionPriority.standart.raw); + await sharedPreferences.setInt( + SettingsStoreBase.currentBalanceDisplayModeKey, BalanceDisplayMode.availableBalance.raw); await sharedPreferences.setBool('save_recipient_address', true); await resetToDefault(nodes); @@ -44,6 +46,10 @@ Future defaultSettingsMigration( await replaceDefaultNode( sharedPreferences: sharedPreferences, nodes: nodes); + break; + case 3: + await updateNodeTypes(nodes: nodes); + await addBitcoinElectrumServerList(nodes: nodes); break; default: break; @@ -87,9 +93,11 @@ Future changeCurrentNodeToDefault( final timeZone = DateTime.now().timeZoneOffset.inHours; String nodeUri = ''; - if (timeZone >= 1) { // Eurasia + if (timeZone >= 1) { + // Eurasia nodeUri = 'xmr-node-eu.cakewallet.com:18081'; - } else if (timeZone <= -4) { // America + } else if (timeZone <= -4) { + // America nodeUri = 'xmr-node-usa-east.cakewallet.com:18081'; } @@ -121,3 +129,17 @@ Future replaceDefaultNode( await changeCurrentNodeToDefault( sharedPreferences: sharedPreferences, nodes: nodes); } + +Future updateNodeTypes({@required Box nodes}) async { + nodes.values.forEach((node) async { + if (node.type == null) { + node.type = WalletType.monero; + await node.save(); + } + }); +} + +Future addBitcoinElectrumServerList({@required Box nodes}) async { + final serverList = await loadElectrumServerList(); + await nodes.addAll(serverList); +} diff --git a/lib/src/domain/common/node.dart b/lib/src/domain/common/node.dart index 506e2aad6..f5b91547d 100644 --- a/lib/src/domain/common/node.dart +++ b/lib/src/domain/common/node.dart @@ -2,18 +2,22 @@ import 'package:flutter/foundation.dart'; import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:hive/hive.dart'; +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; import 'package:cake_wallet/src/domain/common/digest_request.dart'; part 'node.g.dart'; @HiveType(typeId: 1) class Node extends HiveObject { - Node({@required this.uri, this.login, this.password}); + Node({@required this.uri, @required WalletType type, this.login, this.password}) { + this.type = type; + } Node.fromMap(Map map) : uri = (map['uri'] ?? '') as String, login = map['login'] as String, - password = map['password'] as String; + password = map['password'] as String, + typeRaw = map['typeRaw'] as int; static const boxName = 'Nodes'; @@ -26,7 +30,29 @@ class Node extends HiveObject { @HiveField(2) String password; - Future requestNode(String uri, {String login, String password}) async { + @HiveField(3) + int typeRaw; + + WalletType get type => deserializeFromInt(typeRaw); + + set type(WalletType type) => typeRaw = serializeToInt(type); + + Future requestNode() async { + try { + switch (type) { + case WalletType.monero: + return requestMoneroNode(); + case WalletType.bitcoin: + return requestBitcoinElectrumServer(); + default: + return false; + } + } catch (_) { + return false; + } + } + + Future requestMoneroNode() async { Map resBody; if (login != null && password != null) { @@ -38,12 +64,17 @@ class Node extends HiveObject { final url = Uri.http(uri, '/json_rpc'); final headers = {'Content-type': 'application/json'}; final body = - json.encode({"jsonrpc": "2.0", "id": "0", "method": "get_info"}); + json.encode({'jsonrpc': '2.0', 'id': '0', 'method': 'get_info'}); final response = await http.post(url.toString(), headers: headers, body: body); resBody = json.decode(response.body) as Map; } - return !(resBody["result"]["offline"] as bool); + return !(resBody['result']['offline'] as bool); + } + + Future requestBitcoinElectrumServer() async { + // FIXME: IMPLEMENT ME + return true; } } diff --git a/lib/src/domain/common/node_list.dart b/lib/src/domain/common/node_list.dart index 1c318a5fc..ad5db8a5d 100644 --- a/lib/src/domain/common/node_list.dart +++ b/lib/src/domain/common/node_list.dart @@ -2,6 +2,7 @@ import 'package:flutter/services.dart'; import 'package:hive/hive.dart'; import "package:yaml/yaml.dart"; import 'package:cake_wallet/src/domain/common/node.dart'; +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; Future> loadDefaultNodes() async { final nodesRaw = await rootBundle.loadString('assets/node_list.yml'); @@ -16,15 +17,34 @@ Future> loadDefaultNodes() async { }).toList(); } +Future> loadElectrumServerList() async { + final serverListRaw = + await rootBundle.loadString('assets/electrum_server_list.yml'); + final serverList = loadYaml(serverListRaw) as YamlList; + + return serverList.map((dynamic raw) { + if (raw is Map) { + final node = Node.fromMap(raw); + node?.type = WalletType.bitcoin; + + return node; + } + + return null; + }).toList(); +} + Future resetToDefault(Box nodeSource) async { - final nodes = await loadDefaultNodes(); - final enteties = Map(); + final moneroNodes = await loadDefaultNodes(); + final bitcoinElectrumServerList = await loadElectrumServerList(); + final nodes = moneroNodes + bitcoinElectrumServerList; + final entities = {}; await nodeSource.clear(); for (var i = 0; i < nodes.length; i++) { - enteties[i] = nodes[i]; + entities[i] = nodes[i]; } - await nodeSource.putAll(enteties); + await nodeSource.putAll(entities); } diff --git a/lib/src/domain/common/wallet_type.dart b/lib/src/domain/common/wallet_type.dart index d8e386c65..f1a48ce52 100644 --- a/lib/src/domain/common/wallet_type.dart +++ b/lib/src/domain/common/wallet_type.dart @@ -20,15 +20,19 @@ int serializeToInt(WalletType type) { switch (type) { case WalletType.monero: return 0; + case WalletType.bitcoin: + return 1; default: return -1; } } -WalletType deserializeToInt(int raw) { +WalletType deserializeFromInt(int raw) { switch (raw) { case 0: return WalletType.monero; + case 1: + return WalletType.bitcoin; default: return null; } diff --git a/lib/src/reactions/set_reactions.dart b/lib/src/reactions/set_reactions.dart index a8cd25dca..b95671e95 100644 --- a/lib/src/reactions/set_reactions.dart +++ b/lib/src/reactions/set_reactions.dart @@ -23,7 +23,7 @@ void setReactions( @required SyncStore syncStore, @required WalletStore walletStore, @required WalletService walletService, - @required AuthenticationStore authenticationStore, +// @required AuthenticationStore authenticationStore, @required LoginStore loginStore}) { connectToNode(settingsStore: settingsStore, walletStore: walletStore); onSyncStatusChange( diff --git a/lib/src/screens/accounts/account_page.dart b/lib/src/screens/accounts/account_page.dart deleted file mode 100644 index c7fb2b68a..000000000 --- a/lib/src/screens/accounts/account_page.dart +++ /dev/null @@ -1,109 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:provider/provider.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/stores/account_list/account_list_store.dart'; -import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/domain/monero/account.dart'; -import 'package:cake_wallet/palette.dart'; -import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; - -class AccountPage extends BasePage { - AccountPage({this.account}); - - final Account account; - - @override - String get title => S.current.account; - - @override - Widget body(BuildContext context) => AccountForm(account); -} - -class AccountForm extends StatefulWidget { - AccountForm(this.account); - - final Account account; - - @override - AccountFormState createState() => AccountFormState(); -} - -class AccountFormState extends State { - final _formKey = GlobalKey(); - final _textController = TextEditingController(); - - @override - void initState() { - if (widget.account != null) _textController.text = widget.account.label; - super.initState(); - } - - @override - void dispose() { - _textController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final accountListStore = Provider.of(context); - - _textController.addListener(() { - if (_textController.text.isNotEmpty) { - accountListStore.setDisabledStatus(false); - } else { - accountListStore.setDisabledStatus(true); - } - }); - - return Form( - key: _formKey, - child: Container( - color: Theme.of(context).backgroundColor, - padding: EdgeInsets.all(24.0), - child: Column( - children: [ - Expanded( - child: Center( - child: BaseTextFormField( - controller: _textController, - hintText: S.of(context).account, - validator: (value) { - accountListStore.validateAccountName(value); - return accountListStore.errorMessage; - }, - ) - )), - Observer( - builder: (_) => LoadingPrimaryButton( - onPressed: () async { - if (!_formKey.currentState.validate()) { - return; - } - - if (widget.account != null) { - await accountListStore.renameAccount( - index: widget.account.id, - label: _textController.text); - } else { - await accountListStore.addAccount( - label: _textController.text); - } - Navigator.of(context).pop(_textController.text); - }, - text: - widget.account != null ? S.of(context).rename : S.of(context).add, - color: Colors.green, - textColor: Colors.white, - isLoading: accountListStore.isAccountCreating, - isDisabled: accountListStore.isDisabledStatus, - )) - ], - ), - ), - ); - } -} diff --git a/lib/src/screens/address_book/address_book_page.dart b/lib/src/screens/address_book/address_book_page.dart deleted file mode 100644 index 4a24627b4..000000000 --- a/lib/src/screens/address_book/address_book_page.dart +++ /dev/null @@ -1,293 +0,0 @@ -import 'package:provider/provider.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:flutter_slidable/flutter_slidable.dart'; -import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/domain/common/crypto_currency.dart'; -import 'package:cake_wallet/src/stores/address_book/address_book_store.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; - -class AddressBookPage extends BasePage { - AddressBookPage({this.isEditable = true}); - - final bool isEditable; - - @override - String get title => S.current.address_book; - - @override - Widget trailing(BuildContext context) { - if (!isEditable) { - return null; - } - - final addressBookStore = Provider.of(context); - - return Container( - width: 32.0, - height: 32.0, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Theme.of(context).accentTextTheme.title.backgroundColor - ), - child: Stack( - alignment: Alignment.center, - children: [ - Icon(Icons.add, - color: Theme.of(context).primaryTextTheme.title.color, - size: 22.0), - ButtonTheme( - minWidth: 32.0, - height: 32.0, - child: FlatButton( - shape: CircleBorder(), - onPressed: () async { - await Navigator.of(context) - .pushNamed(Routes.addressBookAddContact); - await addressBookStore.updateContactList(); - }, - child: Offstage()), - ) - ], - )); - } - - @override - Widget body(BuildContext context) { - final addressBookStore = Provider.of(context); - - return Container( - color: Theme.of(context).backgroundColor, - padding: EdgeInsets.only(top: 20.0, bottom: 20.0), - child: Observer( - builder: (_) => ListView.separated( - separatorBuilder: (_, __) => Container( - height: 1, - padding: EdgeInsets.only(left: 24), - color: Theme.of(context).accentTextTheme.title.backgroundColor, - child: Container( - height: 1, - color: Theme.of(context).dividerColor, - ), - ), - itemCount: addressBookStore.contactList == null - ? 0 - : addressBookStore.contactList.length, - itemBuilder: (BuildContext context, int index) { - final contact = addressBookStore.contactList[index]; - final image = _getCurrencyImage(contact.type); - - final isDrawTop = index == 0 ? true : false; - final isDrawBottom = index == addressBookStore.contactList.length - 1 ? true : false; - - final content = GestureDetector( - onTap: () async { - if (!isEditable) { - Navigator.of(context).pop(contact); - return; - } - - final isCopied = await showNameAndAddressDialog( - context, contact.name, contact.address); - - if (isCopied != null && isCopied) { - await Clipboard.setData( - ClipboardData(text: contact.address)); - Scaffold.of(context).showSnackBar( - SnackBar( - content: Text( - S.of(context).copied_to_clipboard, - style: TextStyle( - color: Colors.white - ), - ), - backgroundColor: Colors.green, - duration: Duration(milliseconds: 1500), - ), - ); - } - }, - child: Column( - children: [ - isDrawTop - ? Container( - width: double.infinity, - height: 1, - color: Theme.of(context).dividerColor, - ) - : Offstage(), - Container( - width: double.infinity, - color: Theme.of(context).accentTextTheme.title.backgroundColor, - child: Padding( - padding: const EdgeInsets.only(left: 24, top: 16, bottom: 16, right: 24), - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - image != null - ? image - : Offstage(), - Padding( - padding: image != null - ? EdgeInsets.only(left: 12) - : EdgeInsets.only(left: 0), - child: Text( - contact.name, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).primaryTextTheme.title.color - ), - ), - ) - ], - ) - ), - ), - isDrawBottom - ? Container( - width: double.infinity, - height: 1, - color: Theme.of(context).dividerColor, - ) - : Offstage(), - ], - ), - ); - - return !isEditable - ? content - : Slidable( - key: Key('${contact.key}'), - actionPane: SlidableDrawerActionPane(), - child: content, - secondaryActions: [ - IconSlideAction( - caption: S.of(context).edit, - color: Colors.blue, - icon: Icons.edit, - onTap: () async { - await Navigator.of(context).pushNamed( - Routes.addressBookAddContact, - arguments: contact); - await addressBookStore.updateContactList(); - }, - ), - IconSlideAction( - caption: S.of(context).delete, - color: Colors.red, - icon: CupertinoIcons.delete, - onTap: () async { - await showAlertDialog(context) - .then((isDelete) async { - if (isDelete != null && isDelete) { - await addressBookStore.delete( - contact: contact); - await addressBookStore.updateContactList(); - } - }); - }, - ), - ], - dismissal: SlidableDismissal( - child: SlidableDrawerDismissal(), - onDismissed: (actionType) async { - await addressBookStore.delete(contact: contact); - await addressBookStore.updateContactList(); - }, - onWillDismiss: (actionType) async { - return await showAlertDialog(context); - }, - ), - ); - }), - )); - } - - Image _getCurrencyImage(CryptoCurrency currency) { - Image image; - switch (currency) { - case CryptoCurrency.xmr: - image = Image.asset('assets/images/monero.png', height: 24, width: 24); - break; - case CryptoCurrency.ada: - image = Image.asset('assets/images/ada.png', height: 24, width: 24); - break; - case CryptoCurrency.bch: - image = Image.asset('assets/images/bch.png', height: 24, width: 24); - break; - case CryptoCurrency.bnb: - image = Image.asset('assets/images/bnb.png', height: 24, width: 24); - break; - case CryptoCurrency.btc: - image = Image.asset('assets/images/bitcoin.png', height: 24, width: 24); - break; - case CryptoCurrency.dash: - image = Image.asset('assets/images/dash.png', height: 24, width: 24); - break; - case CryptoCurrency.eos: - image = Image.asset('assets/images/eos.png', height: 24, width: 24); - break; - case CryptoCurrency.eth: - image = Image.asset('assets/images/eth.png', height: 24, width: 24); - break; - case CryptoCurrency.ltc: - image = Image.asset('assets/images/litecoin.png', height: 24, width: 24); - break; - case CryptoCurrency.nano: - image = Image.asset('assets/images/nano.png', height: 24, width: 24); - break; - case CryptoCurrency.trx: - image = Image.asset('assets/images/trx.png', height: 24, width: 24); - break; - case CryptoCurrency.usdt: - image = Image.asset('assets/images/usdt.png', height: 24, width: 24); - break; - case CryptoCurrency.xlm: - image = Image.asset('assets/images/xlm.png', height: 24, width: 24); - break; - case CryptoCurrency.xrp: - image = Image.asset('assets/images/xrp.png', height: 24, width: 24); - break; - default: - image = null; - } - return image; - } - - Future showAlertDialog(BuildContext context) async { - return await showDialog( - context: context, - builder: (BuildContext context) { - return AlertWithTwoActions( - alertTitle: S.of(context).address_remove_contact, - alertContent: S.of(context).address_remove_content, - leftButtonText: S.of(context).remove, - rightButtonText: S.of(context).cancel, - actionLeftButton: () => Navigator.of(context).pop(true), - actionRightButton: () => Navigator.of(context).pop(false) - ); - }); - } - - Future showNameAndAddressDialog( - BuildContext context, String name, String address) async { - return await showDialog( - context: context, - builder: (BuildContext context) { - return AlertWithTwoActions( - alertTitle: name, - alertContent: address, - leftButtonText: S.of(context).copy, - rightButtonText: S.of(context).cancel, - actionLeftButton: () => Navigator.of(context).pop(true), - actionRightButton: () => Navigator.of(context).pop(false) - ); - }); - } -} diff --git a/lib/src/screens/address_book/contact_page.dart b/lib/src/screens/address_book/contact_page.dart deleted file mode 100644 index bd181bf3b..000000000 --- a/lib/src/screens/address_book/contact_page.dart +++ /dev/null @@ -1,234 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:provider/provider.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/domain/common/crypto_currency.dart'; -import 'package:cake_wallet/src/domain/common/contact.dart'; -import 'package:cake_wallet/src/stores/address_book/address_book_store.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/widgets/address_text_field.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; -import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; -import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart'; -import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; - -class ContactPage extends BasePage { - ContactPage({this.contact}); - - final Contact contact; - - @override - String get title => S.current.contact; - - @override - Widget body(BuildContext context) => ContactForm(contact); -} - -class ContactForm extends StatefulWidget { - ContactForm(this.contact); - - final Contact contact; - - @override - State createState() => ContactFormState(); -} - -class ContactFormState extends State { - final _formKey = GlobalKey(); - final _contactNameController = TextEditingController(); - final _currencyTypeController = TextEditingController(); - final _addressController = TextEditingController(); - final currencies = CryptoCurrency.all; - - CryptoCurrency _selectectCrypto; - - @override - void initState() { - super.initState(); - if (widget.contact != null) { - _selectectCrypto = widget.contact.type; - _contactNameController.text = widget.contact.name; - _currencyTypeController.text = _selectectCrypto.toString(); - _addressController.text = widget.contact.address; - WidgetsBinding.instance.addPostFrameCallback(afterLayout); - } - } - - void afterLayout(dynamic _) { - final addressBookStore = Provider.of(context); - addressBookStore.setDisabledStatus(false); - } - - @override - void dispose() { - _contactNameController.dispose(); - _currencyTypeController.dispose(); - _addressController.dispose(); - super.dispose(); - } - - void onHandleControllers(AddressBookStore addressBookStore) { - if (_contactNameController.text.isNotEmpty && - _addressController.text.isNotEmpty && - _currencyTypeController.text.isNotEmpty) { - addressBookStore.setDisabledStatus(false); - } else { - addressBookStore.setDisabledStatus(true); - } - } - - @override - Widget build(BuildContext context) { - final addressBookStore = Provider.of(context); - final downArrow = Image.asset( - 'assets/images/arrow_bottom_purple_icon.png', - color: Theme.of(context).dividerColor, - height: 8); - - _contactNameController.addListener(() {onHandleControllers(addressBookStore);}); - _currencyTypeController.addListener(() {onHandleControllers(addressBookStore);}); - _addressController.addListener(() {onHandleControllers(addressBookStore);}); - - return Container( - color: Theme.of(context).backgroundColor, - child: ScrollableWithBottomSection( - contentPadding: EdgeInsets.all(24), - content: Form( - key: _formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - BaseTextFormField( - controller: _contactNameController, - hintText: S.of(context).contact_name, - validator: (value) { - addressBookStore.validateContactName(value); - return addressBookStore.errorMessage; - } - ), - Padding( - padding: EdgeInsets.only(top: 20), - child: Container( - child: InkWell( - onTap: () => _presentPicker(context), - child: IgnorePointer( - child: BaseTextFormField( - controller: _currencyTypeController, - hintText: S.of(context).settings_currency, - suffixIcon: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - downArrow - ], - ), - ) - ), - ), - ), - ), - Padding( - padding: EdgeInsets.only(top: 20), - child: AddressTextField( - controller: _addressController, - options: [AddressTextFieldOption.qrCode], - validator: (value) { - addressBookStore.validateAddress(value, - cryptoCurrency: _selectectCrypto); - return addressBookStore.errorMessage; - }, - ), - ) - ], - ), - ), - bottomSectionPadding: EdgeInsets.only( - left: 24, - right: 24, - bottom: 24 - ), - bottomSection: Row( - children: [ - Expanded( - child: PrimaryButton( - onPressed: () { - setState(() { - _selectectCrypto = null; - _contactNameController.text = ''; - _currencyTypeController.text = ''; - _addressController.text = ''; - }); - }, - text: S.of(context).reset, - color: Colors.red, - textColor: Colors.white - ), - ), - SizedBox(width: 20), - Expanded( - child: Observer( - builder: (_) => PrimaryButton( - onPressed: () async { - if (!_formKey.currentState.validate()) { - return; - } - - try { - if (widget.contact == null) { - final newContact = Contact( - name: _contactNameController.text, - address: _addressController.text, - type: _selectectCrypto); - - await addressBookStore.add(contact: newContact); - } else { - widget.contact.name = _contactNameController.text; - widget.contact.address = _addressController.text; - widget.contact - .updateCryptoCurrency(currency: _selectectCrypto); - - await addressBookStore.update( - contact: widget.contact); - } - Navigator.pop(context); - } catch (e) { - await showDialog( - context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: S.current.contact, - alertContent: e.toString(), - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop() - ); - }); - } - }, - text: S.of(context).save, - color: Colors.green, - textColor: Colors.white, - isDisabled: addressBookStore.isDisabledStatus, - ) - ) - ) - ], - )), - ); - } - - void _presentPicker(BuildContext context) { - showDialog( - builder: (_) => CurrencyPicker( - selectedAtIndex: currencies.indexOf(_selectectCrypto), - items: currencies, - title: S.of(context).please_select, - onItemSelected: (CryptoCurrency item) { - _selectectCrypto = item; - _currencyTypeController.text = _selectectCrypto.toString(); - } - ), - context: context); - } -} diff --git a/lib/src/screens/base_page.dart b/lib/src/screens/base_page.dart index 4db32ab5a..88ad87ea6 100644 --- a/lib/src/screens/base_page.dart +++ b/lib/src/screens/base_page.dart @@ -125,7 +125,7 @@ abstract class BasePage extends StatelessWidget { _isDarkTheme ? backgroundDarkColor : backgroundLightColor, resizeToAvoidBottomPadding: resizeToAvoidBottomPadding, appBar: appBar(context), - body: SafeArea(child: body(context)), + body: body(context), //SafeArea(child: ), floatingActionButton: floatingActionButton(context)); return rootWrapper?.call(context, root) ?? root; diff --git a/lib/src/screens/contact/contact_list_page.dart b/lib/src/screens/contact/contact_list_page.dart new file mode 100644 index 000000000..f07d5e50e --- /dev/null +++ b/lib/src/screens/contact/contact_list_page.dart @@ -0,0 +1,280 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/domain/common/crypto_currency.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; +import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart'; + +class ContactListPage extends BasePage { + ContactListPage(this.contactListViewModel, {this.isEditable = true}); + + final ContactListViewModel contactListViewModel; + final bool isEditable; + + @override + String get title => S.current.address_book; + + @override + Widget trailing(BuildContext context) { + if (!isEditable) { + return null; + } + + return Container( + width: 32.0, + height: 32.0, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).accentTextTheme.title.backgroundColor), + child: Stack( + alignment: Alignment.center, + children: [ + Icon(Icons.add, + color: Theme.of(context).primaryTextTheme.title.color, + size: 22.0), + ButtonTheme( + minWidth: 32.0, + height: 32.0, + child: FlatButton( + shape: CircleBorder(), + onPressed: () async { + await Navigator.of(context) + .pushNamed(Routes.addressBookAddContact); + }, + child: Offstage()), + ) + ], + )); + } + + @override + Widget body(BuildContext context) { + return Container( + color: Theme.of(context).backgroundColor, + padding: EdgeInsets.only(top: 20.0, bottom: 20.0), + child: Observer( + builder: (_) { + return contactListViewModel.contacts.isNotEmpty + ? ListView.separated( + separatorBuilder: (_, __) => Container( + height: 1, + padding: EdgeInsets.only(left: 24), + color: Theme.of(context) + .accentTextTheme + .title + .backgroundColor, + child: Container( + height: 1, + color: Theme.of(context).dividerColor, + ), + ), + itemCount: contactListViewModel.contacts.length, + itemBuilder: (BuildContext context, int index) { + final contact = contactListViewModel.contacts[index]; + final image = _getCurrencyImage(contact.type); + final content = GestureDetector( + onTap: () async { + if (!isEditable) { + Navigator.of(context).pop(contact); + return; + } + + final isCopied = await showNameAndAddressDialog( + context, contact.name, contact.address); + + if (isCopied != null && isCopied) { + await Clipboard.setData( + ClipboardData(text: contact.address)); + Scaffold.of(context).showSnackBar( + SnackBar( + content: Text( + S.of(context).copied_to_clipboard, + style: TextStyle(color: Colors.white), + ), + backgroundColor: Colors.green, + duration: Duration(milliseconds: 1500), + ), + ); + } + }, + child: Column( + children: [ + Container( + width: double.infinity, + color: Theme.of(context) + .accentTextTheme + .title + .backgroundColor, + child: Padding( + padding: const EdgeInsets.only( + left: 24, top: 16, bottom: 16, right: 24), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.center, + children: [ + image ?? Offstage(), + Padding( + padding: image != null + ? EdgeInsets.only(left: 12) + : EdgeInsets.only(left: 0), + child: Text( + contact.name, + style: TextStyle( + fontSize: 14, + color: Theme.of(context) + .primaryTextTheme + .title + .color), + ), + ) + ], + )), + ), + ], + ), + ); + + return !isEditable + ? content + : Slidable( + key: Key('${contact.key}'), + actionPane: SlidableDrawerActionPane(), + child: content, + secondaryActions: [ + IconSlideAction( + caption: S.of(context).edit, + color: Colors.blue, + icon: Icons.edit, + onTap: () async => await Navigator.of(context) + .pushNamed(Routes.addressBookAddContact, + arguments: contact), + ), + IconSlideAction( + caption: S.of(context).delete, + color: Colors.red, + icon: CupertinoIcons.delete, + onTap: () async { + final isDelete = + await showAlertDialog(context) ?? false; + + if (isDelete) { + await contactListViewModel + .delete(contact); + } + }, + ), + ], + dismissal: SlidableDismissal( + child: SlidableDrawerDismissal(), + onDismissed: (actionType) async => + await contactListViewModel.delete(contact), + onWillDismiss: (actionType) async => + showAlertDialog(context), + ), + ); + }) + : Center( + child: Text( + S.of(context).placeholder_contacts, + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context) + .primaryTextTheme + .caption + .color + .withOpacity(0.5), + fontSize: 14), + ), + ); + }, + )); + } + + Image _getCurrencyImage(CryptoCurrency currency) { + Image image; + switch (currency) { + case CryptoCurrency.xmr: + image = Image.asset('assets/images/monero.png', height: 24, width: 24); + break; + case CryptoCurrency.ada: + image = Image.asset('assets/images/ada.png', height: 24, width: 24); + break; + case CryptoCurrency.bch: + image = Image.asset('assets/images/bch.png', height: 24, width: 24); + break; + case CryptoCurrency.bnb: + image = Image.asset('assets/images/bnb.png', height: 24, width: 24); + break; + case CryptoCurrency.btc: + image = Image.asset('assets/images/bitcoin.png', height: 24, width: 24); + break; + case CryptoCurrency.dash: + image = Image.asset('assets/images/dash.png', height: 24, width: 24); + break; + case CryptoCurrency.eos: + image = Image.asset('assets/images/eos.png', height: 24, width: 24); + break; + case CryptoCurrency.eth: + image = Image.asset('assets/images/eth.png', height: 24, width: 24); + break; + case CryptoCurrency.ltc: + image = + Image.asset('assets/images/litecoin.png', height: 24, width: 24); + break; + case CryptoCurrency.nano: + image = Image.asset('assets/images/nano.png', height: 24, width: 24); + break; + case CryptoCurrency.trx: + image = Image.asset('assets/images/trx.png', height: 24, width: 24); + break; + case CryptoCurrency.usdt: + image = Image.asset('assets/images/usdt.png', height: 24, width: 24); + break; + case CryptoCurrency.xlm: + image = Image.asset('assets/images/xlm.png', height: 24, width: 24); + break; + case CryptoCurrency.xrp: + image = Image.asset('assets/images/xrp.png', height: 24, width: 24); + break; + default: + image = null; + } + return image; + } + + Future showAlertDialog(BuildContext context) async { + return await showDialog( + context: context, + builder: (BuildContext context) { + return AlertWithTwoActions( + alertTitle: S.of(context).address_remove_contact, + alertContent: S.of(context).address_remove_content, + leftButtonText: S.of(context).remove, + rightButtonText: S.of(context).cancel, + actionLeftButton: () => Navigator.of(context).pop(true), + actionRightButton: () => Navigator.of(context).pop(false)); + }); + } + + Future showNameAndAddressDialog( + BuildContext context, String name, String address) async { + return await showDialog( + context: context, + builder: (BuildContext context) { + return AlertWithTwoActions( + alertTitle: name, + alertContent: address, + leftButtonText: S.of(context).copy, + rightButtonText: S.of(context).cancel, + actionLeftButton: () => Navigator.of(context).pop(true), + actionRightButton: () => Navigator.of(context).pop(false)); + }); + } +} diff --git a/lib/src/screens/contact/contact_page.dart b/lib/src/screens/contact/contact_page.dart new file mode 100644 index 000000000..5aac1a8e7 --- /dev/null +++ b/lib/src/screens/contact/contact_page.dart @@ -0,0 +1,161 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/core/address_validator.dart'; +import 'package:cake_wallet/core/contact_name_validator.dart'; +import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart'; +import 'package:cake_wallet/view_model/contact_list/contact_view_model_state.dart'; +import 'package:cake_wallet/src/domain/common/crypto_currency.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/src/widgets/address_text_field.dart'; +import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; +import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart'; + +class ContactPage extends BasePage { + ContactPage(this.contactViewModel) + : _formKey = GlobalKey(), + _nameController = TextEditingController(), + _addressController = TextEditingController(), + _currencyTypeController = TextEditingController() { + _nameController.text = contactViewModel.name; + _addressController.text = contactViewModel.address; + _nameController + .addListener(() => contactViewModel.name = _nameController.text); + _addressController + .addListener(() => contactViewModel.address = _addressController.text); + + autorun((_) => + _currencyTypeController.text = contactViewModel.currency.toString()); + } + + @override + String get title => S.current.contact; + + final ContactViewModel contactViewModel; + final GlobalKey _formKey; + final TextEditingController _nameController; + final TextEditingController _currencyTypeController; + final TextEditingController _addressController; + + @override + Widget body(BuildContext context) { + final downArrow = Image.asset('assets/images/arrow_bottom_purple_icon.png', + color: Theme.of(context).dividerColor, height: 8); + + reaction((_) => contactViewModel.state, (ContactViewModelState state) { + if (state is ContactCreationFailure) { + _onContactSavingFailure(context, state.error); + } + + if (state is ContactSavingSuccessfully) { + _onContactSavedSuccessfully(context); + } + }); + + return Container( + color: Theme.of(context).backgroundColor, + child: ScrollableWithBottomSection( + contentPadding: EdgeInsets.all(24), + content: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + BaseTextFormField( + controller: _nameController, + hintText: S.of(context).contact_name, + validator: ContactNameValidator()), + Padding( + padding: EdgeInsets.only(top: 20), + child: Container( + child: InkWell( + onTap: () => _presentCurrencyPicker(context), + child: IgnorePointer( + child: BaseTextFormField( + controller: _currencyTypeController, + hintText: S.of(context).settings_currency, + suffixIcon: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [downArrow], + ), + )), + ), + ), + ), + Padding( + padding: EdgeInsets.only(top: 20), + child: Observer( + builder: (_) => AddressTextField( + controller: _addressController, + options: [AddressTextFieldOption.qrCode], + validator: AddressValidator( + type: contactViewModel.currency), + )), + ) + ], + ), + ), + bottomSectionPadding: + EdgeInsets.only(left: 24, right: 24, bottom: 24), + bottomSection: Row( + children: [ + Expanded( + child: PrimaryButton( + onPressed: () => contactViewModel.reset(), + text: S.of(context).reset, + color: Colors.red, + textColor: Colors.white), + ), + SizedBox(width: 20), + Expanded( + child: Observer( + builder: (_) => PrimaryButton( + onPressed: () async { + if (!_formKey.currentState.validate()) { + return; + } + + await contactViewModel.save(); + }, + text: S.of(context).save, + color: Colors.green, + textColor: Colors.white, + isDisabled: !contactViewModel.isReady))) + ], + )), + ); + } + + void _presentCurrencyPicker(BuildContext context) { + showDialog( + builder: (_) => CurrencyPicker( + selectedAtIndex: + contactViewModel.currencies.indexOf(contactViewModel.currency), + items: contactViewModel.currencies, + title: S.of(context).please_select, + onItemSelected: (CryptoCurrency item) => + contactViewModel.currency = item), + context: context); + } + + void _onContactSavingFailure(BuildContext context, String error) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.current.contact, + alertContent: error, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); + } + + void _onContactSavedSuccessfully(BuildContext context) => + Navigator.of(context).pop(); +} diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart index b587116fb..ed297aec6 100644 --- a/lib/src/screens/dashboard/dashboard_page.dart +++ b/lib/src/screens/dashboard/dashboard_page.dart @@ -1,78 +1,150 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/routes.dart'; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:cake_wallet/view_model/dashboard_view_model.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/wallet_card.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/trade_history_panel.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/menu_widget.dart'; -class DashboardPage extends StatelessWidget { +class DashboardPage extends BasePage { DashboardPage({@required this.walletViewModel}); - final DashboardViewModel walletViewModel; - final _bodyKey = GlobalKey(); + @override + Color get backgroundLightColor => Colors.transparent; @override - Widget build(BuildContext context) => - DashboardPageBody(key: _bodyKey, walletViewModel: walletViewModel); -} - -class DashboardPageBody extends StatefulWidget { - DashboardPageBody({Key key, @required this.walletViewModel}) - : super(key: key); - - final DashboardViewModel walletViewModel; + Color get backgroundDarkColor => Colors.transparent; @override - DashboardPageBodyState createState() => DashboardPageBodyState(); -} - -class DashboardPageBodyState extends State { - @override - Widget build(BuildContext context) { - final menuButton = Image.asset( - 'assets/images/header.png', - color: Theme.of(context).primaryTextTheme.title.color, - ); - - return SafeArea( - child: Scaffold( - body: Container( + Widget Function(BuildContext, Widget) get rootWrapper => + (BuildContext context, Widget scaffold) => Container( decoration: BoxDecoration( gradient: LinearGradient(colors: [ Theme.of(context).scaffoldBackgroundColor, Theme.of(context).primaryColor - ], begin: Alignment.centerLeft, end: Alignment.centerRight)), - child: Column( - children: [ - Container( - padding: EdgeInsets.only(top: 10, right: 10), - alignment: Alignment.centerRight, - child: SizedBox( - height: 44, - width: 44, - child: ButtonTheme( - minWidth: double.minPositive, - child: FlatButton( - highlightColor: Colors.transparent, - splashColor: Colors.transparent, - padding: EdgeInsets.all(0), - onPressed: () async { - await showDialog( - builder: (_) => MenuWidget(), context: context); - }, - child: menuButton), - ), - ), - ), - Padding( - padding: EdgeInsets.only(left: 20, top: 20), - child: WalletCard(walletVM: widget.walletViewModel)), - SizedBox(height: 28), - Expanded(child: TradeHistoryPanel(dashboardViewModel: widget.walletViewModel)) - ], - ), - ), + ], begin: Alignment.topLeft, end: Alignment.bottomRight)), + child: scaffold); + + @override + Widget trailing(BuildContext context) { + final menuButton = Image.asset('assets/images/header.png', + color: Theme.of(context).primaryTextTheme.title.color); + + return Container( + alignment: Alignment.centerRight, + child: SizedBox( + width: 24, + child: FlatButton( + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + padding: EdgeInsets.all(0), + onPressed: () async { + await showDialog( + builder: (_) => MenuWidget( + name: walletViewModel.name, + subname: walletViewModel.subname, + type: walletViewModel.type), + context: context); + }, + child: menuButton), ), ); } + + final DashboardViewModel walletViewModel; + final sendImage = Image.asset('assets/images/send.png'); + final exchangeImage = Image.asset('assets/images/exchange.png'); + final buyImage = Image.asset('assets/images/coins.png'); + + @override + Widget body(BuildContext context) { + return LayoutBuilder(builder: (context, constraints) { + final transactionListMinHeight = + constraints.heightConstraints().maxHeight - 345 - 32; + + return SingleChildScrollView( + child: Column(children: [ + Container( + height: 345, + child: Column(children: [ + Padding( + padding: EdgeInsets.only(left: 24, top: 10), + child: WalletCard(walletVM: walletViewModel)), + Container( + padding: EdgeInsets.only(left: 44, right: 44, top: 32), + child: Row( + children: [ + Flexible( + child: actionButton( + context: context, + image: sendImage, + title: S.of(context).send, + route: Routes.send)), + Flexible( + child: actionButton( + context: context, + image: exchangeImage, + title: S.of(context).exchange, + route: Routes.exchange)), + ], + ), + ) + ])), + SizedBox(height: 32), + ConstrainedBox( + constraints: BoxConstraints(minHeight: transactionListMinHeight), + child: TradeHistoryPanel(dashboardViewModel: walletViewModel)), +// Column(children: [ +// Text('1'), +// Text('2') +// ]) + ])); + }); + } +} + +Widget actionButton( + {BuildContext context, + @required Image image, + @required String title, + @required String route}) { + return Container( + width: MediaQuery.of(context).size.width, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + GestureDetector( + onTap: () { + if (route.isNotEmpty) { + Navigator.of(context, rootNavigator: true).pushNamed(route); + } + }, + child: Container( + height: 48, + width: 48, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Theme.of(context).primaryTextTheme.subhead.color, + shape: BoxShape.circle), + child: image, + ), + ), + Padding( + padding: EdgeInsets.only(top: 12), + child: Text( + title, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Color.fromRGBO(140, 153, 201, + 0.8) // Theme.of(context).primaryTextTheme.caption.color + ), + ), + ) + ], + ), + ); } diff --git a/lib/src/screens/dashboard/widgets/button_header.dart b/lib/src/screens/dashboard/widgets/button_header.dart index ed7cc8f4d..8292704b8 100644 --- a/lib/src/screens/dashboard/widgets/button_header.dart +++ b/lib/src/screens/dashboard/widgets/button_header.dart @@ -16,10 +16,8 @@ class ButtonHeader extends SliverPersistentHeaderDelegate { final buyImage = Image.asset('assets/images/coins.png'); @override - Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { -// final actionListStore = Provider.of(context); - final historyPanelWidth = MediaQuery.of(context).size.width; - + Widget build( + BuildContext context, double shrinkOffset, bool overlapsContent) { final _themeChanger = Provider.of(context); Image filterButton; @@ -29,74 +27,39 @@ class ButtonHeader extends SliverPersistentHeaderDelegate { filterButton = Image.asset('assets/images/filter_light_button.png'); } - double buttonsOpacity = 1 - shrinkOffset / (maxExtent - minExtent); - double buttonsHeight = maxExtent - minExtent - shrinkOffset; - - buttonsOpacity = buttonsOpacity >= 0 ? buttonsOpacity : 0; - buttonsHeight = buttonsHeight >= 0 ? buttonsHeight : 0; - - return Stack( - fit: StackFit.expand, - overflow: Overflow.visible, - children: [ - Opacity( - opacity: buttonsOpacity, - child: Container( - height: buttonsHeight, - padding: EdgeInsets.only(left: 44, right: 44), - child: Row( - children: [ - Flexible( - child: actionButton( - context: context, - image: sendImage, - title: S.of(context).send, - route: Routes.send - ) - ), - Flexible( - child: actionButton( - context: context, - image: exchangeImage, - title: S.of(context).exchange, - route: Routes.exchange - ) - ), - ], - ), - ), - ), - Positioned( - top: buttonsHeight, - left: 0, - child: ClipRRect( - borderRadius: BorderRadius.only(topLeft: Radius.circular(20), topRight: Radius.circular(20)), - child: Container( - width: historyPanelWidth, - height: 66, - padding: EdgeInsets.only(top: 20, left: 20, right: 20, bottom: 10), - color: Theme.of(context).backgroundColor, - child: Stack( - alignment: Alignment.center, - children: [ - Text( - S.of(context).transactions, - style: TextStyle( - fontSize: 20, - color: Theme.of(context).primaryTextTheme.title.color - ), - ), - Positioned( - right: 0, - child: PopupMenuButton( - itemBuilder: (context) => [ - PopupMenuItem( - enabled: false, - value: -1, - child: Text(S.of(context).transactions, - style: TextStyle( - fontWeight: FontWeight.bold, - color: Theme.of(context).primaryTextTheme.caption.color))), + return ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(24), topRight: Radius.circular(24)), + child: Container( + color: Colors.red, +// height: 75, + padding: EdgeInsets.only(top: 26, left: 20, right: 20, bottom: 10), +// color: Theme.of(context).backgroundColor, + child: Stack( + children: [ + Center( + child: Text( + S.of(context).transactions, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + color: Theme.of(context).primaryTextTheme.title.color), + )), + Positioned( + right: 0, + height: 36, + child: PopupMenuButton( + itemBuilder: (context) => [ + PopupMenuItem( + enabled: false, + value: -1, + child: Text(S.of(context).transactions, + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context) + .primaryTextTheme + .caption + .color))), // PopupMenuItem( // value: 0, // child: Observer( @@ -135,27 +98,28 @@ class ButtonHeader extends SliverPersistentHeaderDelegate { // .toggleOutgoing(), // ) // ]))), - PopupMenuItem( - value: 2, - child: - Text(S.of(context).transactions_by_date)), - PopupMenuDivider(), - PopupMenuItem( - enabled: false, - value: -1, - child: Text(S.of(context).trades, - style: TextStyle( - fontWeight: FontWeight.bold, - color: Theme.of(context).primaryTextTheme.caption.color))), - PopupMenuItem( - value: 3, - child: Observer( - builder: (_) => Row( - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, - children: [ - Text('XMR.TO'), + PopupMenuItem( + value: 2, + child: Text(S.of(context).transactions_by_date)), + PopupMenuDivider(), + PopupMenuItem( + enabled: false, + value: -1, + child: Text(S.of(context).trades, + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context) + .primaryTextTheme + .caption + .color))), + PopupMenuItem( + value: 3, + child: Observer( + builder: (_) => Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text('XMR.TO'), // Checkbox( // value: actionListStore // .tradeFilterStore @@ -167,16 +131,15 @@ class ButtonHeader extends SliverPersistentHeaderDelegate { // ExchangeProviderDescription // .xmrto), // ) - ]))), - PopupMenuItem( - value: 4, - child: Observer( - builder: (_) => Row( - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, - children: [ - Text('Change.NOW'), + ]))), + PopupMenuItem( + value: 4, + child: Observer( + builder: (_) => Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text('Change.NOW'), // Checkbox( // value: actionListStore // .tradeFilterStore @@ -188,16 +151,15 @@ class ButtonHeader extends SliverPersistentHeaderDelegate { // ExchangeProviderDescription // .changeNow), // ) - ]))), - PopupMenuItem( - value: 5, - child: Observer( - builder: (_) => Row( - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, - children: [ - Text('MorphToken'), + ]))), + PopupMenuItem( + value: 5, + child: Observer( + builder: (_) => Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text('MorphToken'), // Checkbox( // value: actionListStore // .tradeFilterStore @@ -209,42 +171,37 @@ class ButtonHeader extends SliverPersistentHeaderDelegate { // ExchangeProviderDescription // .morphToken), // ) - ]))) - ], - child: filterButton, - onSelected: (item) async { - if (item == 2) { - final List picked = - await date_rage_picker.showDatePicker( - context: context, - initialFirstDate: DateTime.now() - .subtract(Duration(days: 1)), - initialLastDate: (DateTime.now()), - firstDate: DateTime(2015), - lastDate: DateTime.now() - .add(Duration(days: 1))); + ]))) + ], + child: filterButton, + onSelected: (item) async { + if (item == 2) { + final List picked = + await date_rage_picker.showDatePicker( + context: context, + initialFirstDate: + DateTime.now().subtract(Duration(days: 1)), + initialLastDate: (DateTime.now()), + firstDate: DateTime(2015), + lastDate: DateTime.now().add(Duration(days: 1))); - if (picked != null && picked.length == 2) { + if (picked != null && picked.length == 2) { // actionListStore.transactionFilterStore // .changeStartDate(picked.first); // actionListStore.transactionFilterStore // .changeEndDate(picked.last); - } - } - }, - ), - ) - ], - ), - ), - ) - ) - ], + } + } + }, + )), + ], + ), + ), ); } @override - double get maxExtent => 174; + double get maxExtent => 164; @override double get minExtent => 66; @@ -252,12 +209,11 @@ class ButtonHeader extends SliverPersistentHeaderDelegate { @override bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) => true; - Widget actionButton({ - BuildContext context, - @required Image image, - @required String title, - @required String route}) { - + Widget actionButton( + {BuildContext context, + @required Image image, + @required String title, + @required String route}) { return Container( width: MediaQuery.of(context).size.width, child: Column( @@ -276,23 +232,24 @@ class ButtonHeader extends SliverPersistentHeaderDelegate { alignment: Alignment.center, decoration: BoxDecoration( color: Theme.of(context).primaryTextTheme.subhead.color, - shape: BoxShape.circle - ), + shape: BoxShape.circle), child: image, ), ), Padding( - padding: EdgeInsets.only(top: 10), + padding: EdgeInsets.only(top: 12), child: Text( title, style: TextStyle( fontSize: 16, - color: Theme.of(context).primaryTextTheme.caption.color - ), + fontWeight: FontWeight.w600, + color: Color.fromRGBO(140, 153, 201, + 0.8) // Theme.of(context).primaryTextTheme.caption.color + ), ), ) ], ), ); } -} \ No newline at end of file +} diff --git a/lib/src/screens/dashboard/widgets/menu_widget.dart b/lib/src/screens/dashboard/widgets/menu_widget.dart index 24bf9cd46..fdd9a704c 100644 --- a/lib/src/screens/dashboard/widgets/menu_widget.dart +++ b/lib/src/screens/dashboard/widgets/menu_widget.dart @@ -1,25 +1,28 @@ -import 'dart:async'; import 'dart:ui'; import 'package:flutter/material.dart'; -import 'package:cake_wallet/palette.dart'; +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; import 'package:cake_wallet/src/screens/dashboard/wallet_menu.dart'; -import 'package:cake_wallet/src/stores/wallet/wallet_store.dart'; -import 'package:provider/provider.dart'; class MenuWidget extends StatefulWidget { + MenuWidget({this.type, this.name, this.subname}); + + final WalletType type; + final String name; + final String subname; + @override MenuWidgetState createState() => MenuWidgetState(); } class MenuWidgetState extends State { final moneroIcon = Image.asset('assets/images/monero.png'); + final bitcoinIcon = Image.asset('assets/images/bitcoin.png'); final largeScreen = 731; double menuWidth; double screenWidth; double screenHeight; double opacity; - bool isDraw; double headerHeight; double tileHeight; @@ -32,7 +35,6 @@ class MenuWidgetState extends State { screenWidth = 0; screenHeight = 0; opacity = 0; - isDraw = false; headerHeight = 120; tileHeight = 75; @@ -59,206 +61,205 @@ class MenuWidgetState extends State { fromBottomEdge *= scale; } }); - - Timer(Duration(milliseconds: 350), () => - setState(() => isDraw = true) - ); } @override Widget build(BuildContext context) { final walletMenu = WalletMenu(context); - final walletStore = Provider.of(context); +// final walletStore = Provider.of(context); final itemCount = walletMenu.items.length; - return GestureDetector( - onTap: () => Navigator.of(context).pop(), - child: Container( - color: Colors.transparent, - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 3.0, sigmaY: 3.0), + return Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.only(left: 24), + child: Container( + height: 60, + width: 4, + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(2)), + color: Theme.of(context).hintColor), + )), + SizedBox(width: 12), + Expanded( + child: GestureDetector( + onTap: () => null, child: Container( - decoration: BoxDecoration(color: PaletteDark.darkNightBlue.withOpacity(0.75)), - child: Row( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - padding: EdgeInsets.only(left: 24), - child: isDraw - ? Container( - height: 60, - width: 4, - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(2)), - color: Theme.of(context).hintColor // - ), - ) - : Container( - height: 60, - width: 4, - ) - ), - SizedBox(width: 12), - Expanded( - child: GestureDetector( - onTap: () => null, - child: Container( - width: double.infinity, - height: double.infinity, - alignment: Alignment.centerRight, - child: AnimatedContainer( - alignment: Alignment.centerLeft, - width: menuWidth, - height: double.infinity, - duration: Duration(milliseconds: 500), - curve: Curves.fastOutSlowIn, - decoration: BoxDecoration( - borderRadius: BorderRadius.only(topLeft: Radius.circular(24), bottomLeft: Radius.circular(24)), - color: Theme.of(context).primaryTextTheme.display1.color.withOpacity(opacity) - ), - child: isDraw - ? ListView.separated( - itemBuilder: (_, index) { - - if (index == 0) { - return Container( - height: headerHeight, - padding: EdgeInsets.only( - left: 24, - top: fromTopEdge, - right: 24, - bottom: fromBottomEdge), - decoration: BoxDecoration( - borderRadius: BorderRadius.only(topLeft: Radius.circular(24)), - color: Theme.of(context).primaryTextTheme.display2.color - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - moneroIcon, - SizedBox(width: 16), - Expanded( - child: Container( - height: 40, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - walletStore.name, - style: TextStyle( - color: Theme.of(context).primaryTextTheme.title.color, - decoration: TextDecoration.none, - fontFamily: 'Lato', - fontSize: 20, - fontWeight: FontWeight.bold - ), - ), - Text( - walletStore.account.label, - style: TextStyle( - color: Theme.of(context).primaryTextTheme.caption.color, - decoration: TextDecoration.none, - fontFamily: 'Lato', - fontSize: 12 - ), - ) - ], - ), - ) - ) - ], - ), - ); - } - - index -= 1; - final item = walletMenu.items[index]; - final image = walletMenu.images[index] ?? Offstage(); - - return GestureDetector( - onTap: () { - Navigator.of(context).pop(); - walletMenu.action(index); - }, - child: index == itemCount - 1 - ? Container( - height: headerHeight, - padding: EdgeInsets.only( - left: 24, - right: 24, - top: fromBottomEdge, - bottom: fromTopEdge), - alignment: Alignment.topLeft, - decoration: BoxDecoration( - borderRadius: BorderRadius.only(bottomLeft: Radius.circular(24)), - color: Theme.of(context).primaryTextTheme.display1.color, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - image, - SizedBox(width: 16), - Expanded( - child: Text( - item, - style: TextStyle( - decoration: TextDecoration.none, - color: Theme.of(context).primaryTextTheme.title.color, - fontFamily: 'Lato', - fontSize: 20, - fontWeight: FontWeight.bold - ), - ) - ) - ], - ), - ) - : Container( - height: tileHeight, - padding: EdgeInsets.only(left: 24, right: 24), - color: Theme.of(context).primaryTextTheme.display1.color, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - image, - SizedBox(width: 16), - Expanded( - child: Text( - item, - style: TextStyle( - decoration: TextDecoration.none, - color: Theme.of(context).primaryTextTheme.title.color, - fontFamily: 'Lato', - fontSize: 20, - fontWeight: FontWeight.bold - ), - ) - ) - ], - ), + width: menuWidth, + height: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(24), + bottomLeft: Radius.circular(24)), + color: Theme.of(context).primaryTextTheme.display1.color), + child: ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(24), + bottomLeft: Radius.circular(24)), + child: ListView.separated( + itemBuilder: (_, index) { + if (index == 0) { + return Container( + height: headerHeight, + padding: EdgeInsets.only( + left: 24, + top: fromTopEdge, + right: 24, + bottom: fromBottomEdge), + decoration: BoxDecoration( + borderRadius: + BorderRadius.only(topLeft: Radius.circular(24)), + color: Theme.of(context) + .primaryTextTheme + .display2 + .color), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + _iconFor(type: widget.type), + SizedBox(width: 16), + Expanded( + child: Container( + height: 40, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: widget.subname != null + ? MainAxisAlignment.spaceBetween + : MainAxisAlignment.center, + children: [ + Text( + widget.name, + style: TextStyle( + color: Theme.of(context) + .primaryTextTheme + .title + .color, + decoration: TextDecoration.none, + fontFamily: 'Avenir Next', + fontSize: 20, + fontWeight: FontWeight.bold), ), - ); - }, - separatorBuilder: (_, index) => - Container( - height: 1, - color: Theme.of(context).dividerColor, - ), - itemCount: itemCount + 1) - : Offstage() + if (widget.subname != null) + Text( + widget.subname, + style: TextStyle( + color: Theme.of(context) + .primaryTextTheme + .caption + .color, + decoration: TextDecoration.none, + fontFamily: 'Avenir Next', + fontSize: 12), + ) + ], + ), + )) + ], + ), + ); + } + + index -= 1; + final item = walletMenu.items[index]; + final image = walletMenu.images[index] ?? Offstage(); + + return GestureDetector( + onTap: () { + Navigator.of(context).pop(); + walletMenu.action(index); + }, + child: index == itemCount - 1 + ? Container( + height: headerHeight, + padding: EdgeInsets.only( + left: 24, + right: 24, + top: fromBottomEdge, + bottom: fromTopEdge), + alignment: Alignment.topLeft, + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(24)), + color: Theme.of(context) + .primaryTextTheme + .display1 + .color, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + image, + SizedBox(width: 16), + Expanded( + child: Text( + item, + style: TextStyle( + decoration: TextDecoration.none, + color: Theme.of(context) + .primaryTextTheme + .title + .color, + fontFamily: 'Avenir Next', + fontSize: 20, + fontWeight: FontWeight.bold), + )) + ], + ), + ) + : Container( + height: tileHeight, + padding: EdgeInsets.only(left: 24, right: 24), + color: Theme.of(context) + .primaryTextTheme + .display1 + .color, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + image, + SizedBox(width: 16), + Expanded( + child: Text( + item, + style: TextStyle( + decoration: TextDecoration.none, + color: Theme.of(context) + .primaryTextTheme + .title + .color, + fontFamily: 'Avenir Next', + fontSize: 20, + fontWeight: FontWeight.bold), + )) + ], + ), + ), + ); + }, + separatorBuilder: (_, index) => Container( + height: 1, + color: Theme.of(context).dividerColor, ), - ), - ) - ) - ], - ) + itemCount: itemCount + 1), + ), ), - ), - ), + )) + ], ); } -} \ No newline at end of file + + Image _iconFor({@required WalletType type}) { + switch (type) { + case WalletType.monero: + return moneroIcon; + case WalletType.bitcoin: + return bitcoinIcon; + default: + return null; + } + } +} diff --git a/lib/src/screens/dashboard/widgets/trade_history_panel.dart b/lib/src/screens/dashboard/widgets/trade_history_panel.dart index 2aca26c57..174b9cd89 100644 --- a/lib/src/screens/dashboard/widgets/trade_history_panel.dart +++ b/lib/src/screens/dashboard/widgets/trade_history_panel.dart @@ -1,3 +1,6 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/theme_changer.dart'; +import 'package:cake_wallet/themes.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; @@ -15,11 +18,12 @@ import 'date_section_raw.dart'; import 'trade_row.dart'; import 'transaction_raw.dart'; import 'button_header.dart'; +import 'package:date_range_picker/date_range_picker.dart' as date_rage_picker; class TradeHistoryPanel extends StatefulWidget { TradeHistoryPanel({this.dashboardViewModel}); - DashboardViewModel dashboardViewModel; + final DashboardViewModel dashboardViewModel; @override TradeHistoryPanelState createState() => TradeHistoryPanelState(); @@ -49,127 +53,270 @@ class TradeHistoryPanelState extends State { @override Widget build(BuildContext context) { -// final actionListStore = Provider.of(context); -// final settingsStore = Provider.of(context); + // AnimatedContainer( +// width: MediaQuery.of(context).size.width, +// height: panelHeight, +// duration: Duration(milliseconds: 1000), +// curve: Curves.fastOutSlowIn, +// child: ) + final transactionDateFormat = DateFormat('HH:mm'); + final _themeChanger = Provider.of(context); + final filterButton = Image.asset( + _themeChanger.getTheme() == Themes.darkTheme + ? 'assets/images/filter_button.png' + : 'assets/images/filter_light_button.png', + height: 36); - return Container( - height: MediaQuery.of(context).size.height, - width: MediaQuery.of(context).size.width, - alignment: Alignment.bottomCenter, - child: AnimatedContainer( - width: MediaQuery.of(context).size.width, - height: panelHeight, - duration: Duration(milliseconds: 1000), - curve: Curves.fastOutSlowIn, - child: ClipRRect( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20)), - child: CustomScrollView( - slivers: [ - SliverPersistentHeader( - delegate: ButtonHeader(), - pinned: true, - floating: false, - ), - Observer( - key: _listObserverKey, - builder: (_) { -// final items = actionListStore.items == null -// ? [] -// : actionListStore.items; - final items = widget.dashboardViewModel.transactions; - final itemsCount = items.length + 1; - final symbol = - '\$'; // settingsStore.fiatCurrency.toString(); - var freeSpaceHeight = - MediaQuery.of(context).size.height - 496; + return ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), topRight: Radius.circular(20)), + child: Container( + color: Colors.white, + child: Column(children: [ + Container( + padding: + EdgeInsets.only(top: 32, left: 20, right: 20, bottom: 20), + color: Theme.of(context).backgroundColor, + child: Stack( + children: [ + SizedBox(height: 37), // Force stack height + Center( + child: Text(S.of(context).transactions, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + color: Theme.of(context) + .primaryTextTheme + .title + .color))), + Positioned( + right: 0, + child: PopupMenuButton( + itemBuilder: (context) => [ + PopupMenuItem( + enabled: false, + value: -1, + child: Text(S.of(context).transactions, + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context) + .primaryTextTheme + .caption + .color))), +// PopupMenuItem( +// value: 0, +// child: Observer( +// builder: (_) => Row( +// mainAxisAlignment: +// MainAxisAlignment +// .spaceBetween, +// children: [ +// Text(S.of(context).incoming), +// Checkbox( +// value: actionListStore +// .transactionFilterStore +// .displayIncoming, +// onChanged: (value) => +// actionListStore +// .transactionFilterStore +// .toggleIncoming(), +// ) +// ]))), +// PopupMenuItem( +// value: 1, +// child: Observer( +// builder: (_) => Row( +// mainAxisAlignment: +// MainAxisAlignment +// .spaceBetween, +// children: [ +// Text(S.of(context).outgoing), +// Checkbox( +// value: actionListStore +// .transactionFilterStore +// .displayOutgoing, +// onChanged: (value) => +// actionListStore +// .transactionFilterStore +// .toggleOutgoing(), +// ) +// ]))), + PopupMenuItem( + value: 2, + child: + Text(S.of(context).transactions_by_date)), + PopupMenuDivider(), + PopupMenuItem( + enabled: false, + value: -1, + child: Text(S.of(context).trades, + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context) + .primaryTextTheme + .caption + .color))), + PopupMenuItem( + value: 3, + child: Observer( + builder: (_) => Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text('XMR.TO'), +// Checkbox( +// value: actionListStore +// .tradeFilterStore +// .displayXMRTO, +// onChanged: (value) => +// actionListStore +// .tradeFilterStore +// .toggleDisplayExchange( +// ExchangeProviderDescription +// .xmrto), +// ) + ]))), + PopupMenuItem( + value: 4, + child: Observer( + builder: (_) => Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text('Change.NOW'), +// Checkbox( +// value: actionListStore +// .tradeFilterStore +// .displayChangeNow, +// onChanged: (value) => +// actionListStore +// .tradeFilterStore +// .toggleDisplayExchange( +// ExchangeProviderDescription +// .changeNow), +// ) + ]))), + PopupMenuItem( + value: 5, + child: Observer( + builder: (_) => Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text('MorphToken'), +// Checkbox( +// value: actionListStore +// .tradeFilterStore +// .displayMorphToken, +// onChanged: (value) => +// actionListStore +// .tradeFilterStore +// .toggleDisplayExchange( +// ExchangeProviderDescription +// .morphToken), +// ) + ]))) + ], + child: filterButton, + onSelected: (item) async { + if (item == 2) { + final picked = + await date_rage_picker.showDatePicker( + context: context, + initialFirstDate: DateTime.now() + .subtract(Duration(days: 1)), + initialLastDate: (DateTime.now()), + firstDate: DateTime(2015), + lastDate: DateTime.now() + .add(Duration(days: 1))); - return SliverList( - key: _listKey, - delegate: - SliverChildBuilderDelegate((context, index) { - if (index == itemsCount - 1) { - freeSpaceHeight = freeSpaceHeight >= 0 - ? freeSpaceHeight - : 0; - - return Container( - height: freeSpaceHeight, - width: MediaQuery.of(context).size.width, - color: Theme.of(context).backgroundColor); - } - - final item = items[index]; - - if (item is DateSectionItem) { - freeSpaceHeight -= 38; - return DateSectionRaw(date: item.date); - } - - if (item is TransactionListItem) { - freeSpaceHeight -= 62; - final transaction = item.transaction; - final savedDisplayMode = - BalanceDisplayMode.all; - //settingsStore -// .balanceDisplayMode; - final formattedAmount = savedDisplayMode == - BalanceDisplayMode.hiddenBalance - ? '---' - : transaction.amountFormatted(); - final formattedFiatAmount = - savedDisplayMode == - BalanceDisplayMode.hiddenBalance - ? '---' - : transaction - .fiatAmount(); // symbol ??? - - return TransactionRow( - onTap: () => Navigator.of(context) - .pushNamed(Routes.transactionDetails, - arguments: transaction), - direction: transaction.direction, - formattedDate: transactionDateFormat - .format(transaction.date), - formattedAmount: formattedAmount, - formattedFiatAmount: formattedFiatAmount, - isPending: transaction.isPending); - } - - if (item is TradeListItem) { - freeSpaceHeight -= 62; - final trade = item.trade; - final savedDisplayMode = - BalanceDisplayMode.all; - //settingsStore - // .balanceDisplayMode; - final formattedAmount = trade.amount != null - ? savedDisplayMode == - BalanceDisplayMode.hiddenBalance - ? '---' - : trade.amountFormatted() - : trade.amount; - - return TradeRow( - onTap: () => Navigator.of(context) - .pushNamed(Routes.tradeDetails, - arguments: trade), - provider: trade.provider, - from: trade.from, - to: trade.to, - createdAtFormattedDate: - transactionDateFormat - .format(trade.createdAt), - formattedAmount: formattedAmount); - } - - return Container( - color: Theme.of(context).backgroundColor); - }, childCount: itemsCount)); - }) + if (picked != null && picked.length == 2) { +// actionListStore.transactionFilterStore +// .changeStartDate(picked.first); +// actionListStore.transactionFilterStore +// .changeEndDate(picked.last); + } + } + }, + )), ], - )))); //, + ), + ), + widget.dashboardViewModel.transactions?.isNotEmpty ?? false + ? ListView.separated( + physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: widget.dashboardViewModel.transactions.length, + itemBuilder: (_, index) { + final item = + widget.dashboardViewModel.transactions[index]; + + if (item is DateSectionItem) { + return DateSectionRaw(date: item.date); + } + + if (item is TransactionListItem) { + final transaction = item.transaction; + final savedDisplayMode = BalanceDisplayMode.all; + //settingsStore +// .balanceDisplayMode; + final formattedAmount = savedDisplayMode == + BalanceDisplayMode.hiddenBalance + ? '---' + : transaction.amountFormatted(); + final formattedFiatAmount = savedDisplayMode == + BalanceDisplayMode.hiddenBalance + ? '---' + : transaction.fiatAmount(); // symbol ??? + + return TransactionRow( + onTap: () => Navigator.of(context).pushNamed( + Routes.transactionDetails, + arguments: transaction), + direction: transaction.direction, + formattedDate: transactionDateFormat + .format(transaction.date), + formattedAmount: formattedAmount, + formattedFiatAmount: formattedFiatAmount, + isPending: transaction.isPending); + } + + if (item is TradeListItem) { + final trade = item.trade; + final savedDisplayMode = BalanceDisplayMode.all; + //settingsStore + // .balanceDisplayMode; + final formattedAmount = trade.amount != null + ? savedDisplayMode == + BalanceDisplayMode.hiddenBalance + ? '---' + : trade.amountFormatted() + : trade.amount; + + return TradeRow( + onTap: () => Navigator.of(context).pushNamed( + Routes.tradeDetails, + arguments: trade), + provider: trade.provider, + from: trade.from, + to: trade.to, + createdAtFormattedDate: + transactionDateFormat.format(trade.createdAt), + formattedAmount: formattedAmount); + } + + return Container( + color: Theme.of(context).backgroundColor, + height: 1); + }, + separatorBuilder: (_, __) => + Container(height: 14, color: Colors.white), + ) + : Padding( + padding: EdgeInsets.all(20), + child: Text('Your transactions will be displayed here!', + style: TextStyle(color: Colors.grey))) + ]))); //, } } diff --git a/lib/src/screens/dashboard/widgets/transaction_raw.dart b/lib/src/screens/dashboard/widgets/transaction_raw.dart index 2baa3f5ff..5e9a054dc 100644 --- a/lib/src/screens/dashboard/widgets/transaction_raw.dart +++ b/lib/src/screens/dashboard/widgets/transaction_raw.dart @@ -24,27 +24,19 @@ class TransactionRow extends StatelessWidget { return InkWell( onTap: onTap, child: Container( - height: 60, - decoration: BoxDecoration( - color: Theme.of(context).backgroundColor, - border: Border.all( - width: 1, - color: Theme.of(context).backgroundColor - ), - ), - padding: EdgeInsets.only(top: 5, bottom: 5, left: 20, right: 20), + height: 41, + color: Theme.of(context).backgroundColor, + padding: EdgeInsets.only(left: 20, right: 20), child: Row(children: [ Container( height: 36, width: 36, decoration: BoxDecoration( - shape: BoxShape.circle, - color: Theme.of(context).primaryTextTheme.display3.color - ), - child: Image.asset( - direction == TransactionDirection.incoming - ? 'assets/images/down_arrow.png' - : 'assets/images/up_arrow.png'), + shape: BoxShape.circle, + color: Theme.of(context).primaryTextTheme.display3.color), + child: Image.asset(direction == TransactionDirection.incoming + ? 'assets/images/down_arrow.png' + : 'assets/images/up_arrow.png'), ), Expanded( child: Padding( @@ -62,28 +54,43 @@ class TransactionRow extends StatelessWidget { (isPending ? S.of(context).pending : ''), style: TextStyle( fontSize: 16, - color: Theme.of(context).primaryTextTheme.title.color - )), - Text(direction == TransactionDirection.incoming - ? formattedAmount - : '- ' + formattedAmount, + fontWeight: FontWeight.w500, + color: Theme.of(context) + .primaryTextTheme + .title + .color)), + Text( + direction == TransactionDirection.incoming + ? formattedAmount + : '- ' + formattedAmount, style: TextStyle( fontSize: 16, - color: Theme.of(context).primaryTextTheme.title.color - )) + fontWeight: FontWeight.w500, + color: Theme.of(context) + .primaryTextTheme + .title + .color)) ]), - SizedBox(height: 5,), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(formattedDate, style: TextStyle( - fontSize: 14, color: Theme.of(context).primaryTextTheme.headline.color)), - Text(direction == TransactionDirection.incoming - ? formattedFiatAmount - : '- ' + formattedFiatAmount, + fontSize: 14, + color: Theme.of(context) + .primaryTextTheme + .headline + .color)), + Text( + direction == TransactionDirection.incoming + ? formattedFiatAmount + : '- ' + formattedFiatAmount, style: TextStyle( - fontSize: 14, color: Theme.of(context).primaryTextTheme.headline.color)) + fontSize: 14, + color: Theme.of(context) + .primaryTextTheme + .headline + .color)) ]), ], ), diff --git a/lib/src/screens/dashboard/widgets/wallet_card.dart b/lib/src/screens/dashboard/widgets/wallet_card.dart index 8af700659..041f3e89f 100644 --- a/lib/src/screens/dashboard/widgets/wallet_card.dart +++ b/lib/src/screens/dashboard/widgets/wallet_card.dart @@ -1,16 +1,12 @@ import 'dart:async'; import 'package:cake_wallet/palette.dart'; -import 'package:cake_wallet/src/domain/common/wallet_type.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:cake_wallet/src/domain/common/balance_display_mode.dart'; -import 'package:cake_wallet/src/stores/balance/balance_store.dart'; import 'package:cake_wallet/src/stores/settings/settings_store.dart'; -import 'package:cake_wallet/src/stores/sync/sync_store.dart'; -import 'package:cake_wallet/src/stores/wallet/wallet_store.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/domain/common/sync_status.dart'; import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart'; @@ -20,7 +16,7 @@ import 'package:cake_wallet/view_model/dashboard_view_model.dart'; class WalletCard extends StatefulWidget { WalletCard({this.walletVM}); - DashboardViewModel walletVM; + final DashboardViewModel walletVM; @override WalletCardState createState() => WalletCardState(); @@ -70,24 +66,20 @@ class WalletCardState extends State { width: double.infinity, height: cardHeight, alignment: Alignment.centerRight, + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(14), bottomLeft: Radius.circular(14))), child: AnimatedContainer( alignment: Alignment.centerLeft, width: cardWidth, height: cardHeight, duration: Duration(milliseconds: 500), curve: Curves.fastOutSlowIn, - padding: EdgeInsets.only(top: 1, left: 1, bottom: 1), decoration: BoxDecoration( borderRadius: BorderRadius.only( - topLeft: Radius.circular(10), - bottomLeft: Radius.circular(10)), - color: Theme.of(context).focusColor, - boxShadow: [ - BoxShadow( - color: PaletteDark.darkNightBlue.withOpacity(0.5), - blurRadius: 8, - offset: Offset(5, 5)) - ]), + topLeft: Radius.circular(14), + bottomLeft: Radius.circular(14)), + color: Theme.of(context).focusColor), child: ClipRRect( borderRadius: BorderRadius.only( topLeft: Radius.circular(10), bottomLeft: Radius.circular(10)), @@ -95,21 +87,18 @@ class WalletCardState extends State { width: cardWidth, height: cardHeight, color: Theme.of(context).cardColor, - child: InkWell( - onTap: () => setState(() => isFrontSide = !isFrontSide), - child: isFrontSide - ? frontSide(colorsSync) - : backSide(colorsSync)), + child: isFrontSide + ? frontSide(colorsSync) + : InkWell( + onTap: () => setState(() => isFrontSide = true), + child: backSide(colorsSync)), ), )), ); } Widget frontSide(List colorsSync) { -// final syncStore = Provider.of(context); -// final walletStore = Provider.of(context); final settingsStore = Provider.of(context); -// final balanceStore = Provider.of(context); final triangleButton = Image.asset( 'assets/images/triangle.png', color: Theme.of(context).primaryTextTheme.title.color, @@ -121,9 +110,9 @@ class WalletCardState extends State { final status = widget.walletVM.status; final statusText = status.title(); final progress = status.progress(); - final indicatorWidth = progress * cardWidth; - final shortAddress = widget.walletVM.address - .replaceRange(4, widget.walletVM.address.length - 4, '...'); + final indicatorOffset = progress * cardWidth; + final indicatorWidth = + progress <= 1 ? cardWidth - indicatorOffset : 0.0; var descriptionText = ''; if (status is SyncingSyncStatus) { @@ -137,37 +126,26 @@ class WalletCardState extends State { return Container( width: cardWidth, height: cardHeight, + color: Colors.white, child: Stack( children: [ - Container( - height: cardHeight, - width: indicatorWidth, - decoration: BoxDecoration( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(10), - bottomLeft: Radius.circular(10)), - gradient: LinearGradient( - colors: colorsSync, - begin: Alignment.topCenter, - end: Alignment.bottomCenter)), - ), - progress != 1 + progress <= 1 ? Positioned( - left: indicatorWidth, + left: indicatorOffset, top: 0, bottom: 0, child: Container( - width: 1, + width: indicatorWidth, height: cardHeight, - color: Theme.of(context).focusColor, + color: Color.fromRGBO(227, 238, 249, 1), )) : Offstage(), isDraw ? Positioned( - left: 20, - right: 20, - top: 30, - bottom: 30, + left: 24, + right: 24, + top: 32, + bottom: 24, child: Container( child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -180,7 +158,8 @@ class WalletCardState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ InkWell( - onTap: () {}, + onTap: () => Navigator.of(context) + .pushNamed(Routes.walletList), child: Row( children: [ Text( @@ -199,11 +178,13 @@ class WalletCardState extends State { ), ), SizedBox(height: 5), - if (widget.walletVM.subname?.isNotEmpty ?? false) + if (widget.walletVM.subname?.isNotEmpty ?? + false) Text( widget.walletVM.subname, style: TextStyle( fontSize: 12, + fontWeight: FontWeight.w600, color: Theme.of(context) .primaryTextTheme .caption @@ -211,26 +192,34 @@ class WalletCardState extends State { ) ], ), - Container( - width: 98, - height: 32, - alignment: Alignment.center, - decoration: BoxDecoration( - color: Theme.of(context) - .accentTextTheme - .subtitle - .backgroundColor, - borderRadius: BorderRadius.all( - Radius.circular(16))), - child: Text( - shortAddress, - style: TextStyle( - fontSize: 12, - color: Theme.of(context) - .primaryTextTheme - .caption - .color), - ), + InkWell( + onTap: () => + setState(() => isFrontSide = false), + child: Container( + width: 98, + height: 32, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Theme.of(context) + .accentTextTheme + .subtitle + .backgroundColor, + border: Border.all( + color: Color.fromRGBO( + 219, 231, 237, 1)), + // FIXME + borderRadius: BorderRadius.all( + Radius.circular(16))), + child: Text( + 'Receive', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Theme.of(context) + .primaryTextTheme + .title + .color), + )), ) ], ), @@ -239,7 +228,7 @@ class WalletCardState extends State { key: _balanceObserverKey, builder: (_) { final balanceDisplayMode = - BalanceDisplayMode.fullBalance; + BalanceDisplayMode.availableBalance; // settingsStore.balanceDisplayMode; final symbol = settingsStore.fiatCurrency.toString(); @@ -251,7 +240,7 @@ class WalletCardState extends State { balance = widget.walletVM.balance .unlockedBalance ?? '0.0'; - fiatBalance = '\$ 123.43'; + fiatBalance = '\$ 0.00'; // '$symbol ${balanceStore.fiatUnlockedBalance}'; } @@ -260,7 +249,7 @@ class WalletCardState extends State { balance = widget.walletVM.balance .totalBalance ?? '0.0'; - fiatBalance = '\$ 123.43'; + fiatBalance = '\$ 0.00'; // '$symbol ${balanceStore.fiatFullBalance}'; } @@ -271,6 +260,8 @@ class WalletCardState extends State { MainAxisAlignment.spaceBetween, children: [ Column( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -284,25 +275,33 @@ class WalletCardState extends State { .color), ), SizedBox(height: 5), - Text( - balance, - style: TextStyle( - fontSize: 28, - color: Theme.of(context) - .primaryTextTheme - .title - .color), - ) + Container( + height: 36, + child: Text( + balance, + style: TextStyle( + fontSize: 32, + color: Theme.of(context) + .primaryTextTheme + .title + .color, + fontWeight: + FontWeight.bold), + )) ], ), Text( fiatBalance, style: TextStyle( fontSize: 14, - color: Theme.of(context) - .primaryTextTheme - .title - .color), + fontWeight: FontWeight.w600, +// FIXME +// color: Theme.of(context) +// .primaryTextTheme +// .title +// .color, + color: Color.fromRGBO( + 72, 89, 109, 1)), ) ], ); @@ -320,6 +319,7 @@ class WalletCardState extends State { statusText, style: TextStyle( fontSize: 12, + fontWeight: FontWeight.w600, color: Theme.of(context) .primaryTextTheme .caption @@ -330,6 +330,7 @@ class WalletCardState extends State { descriptionText, style: TextStyle( fontSize: 14, + fontWeight: FontWeight.w600, color: Theme.of(context) .primaryTextTheme .title @@ -351,10 +352,8 @@ class WalletCardState extends State { } Widget backSide(List colorsSync) { - final rightArrow = Image.asset( - 'assets/images/right_arrow.png', - color: Theme.of(context).primaryTextTheme.title.color, - ); + final rightArrow = Image.asset('assets/images/right_arrow.png', + color: Theme.of(context).primaryTextTheme.title.color); var messageBoxHeight = 0.0; var messageBoxWidth = cardWidth - 10; @@ -372,7 +371,7 @@ class WalletCardState extends State { width: cardWidth, height: cardHeight, padding: - EdgeInsets.only(left: 20, right: 20, top: 30, bottom: 30), + EdgeInsets.only(left: 24, right: 24, top: 32, bottom: 32), decoration: BoxDecoration( borderRadius: BorderRadius.only( topLeft: Radius.circular(10), @@ -385,10 +384,10 @@ class WalletCardState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Container( - height: 90, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -426,7 +425,8 @@ class WalletCardState extends State { child: Text( widget.walletVM.address, style: TextStyle( - fontSize: 14, + fontSize: 12, + fontWeight: FontWeight.w600, color: Theme.of(context) .primaryTextTheme .title @@ -468,9 +468,10 @@ class WalletCardState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - S.of(context).accounts_subaddresses, + S.of(context).addresses, style: TextStyle( fontSize: 14, + fontWeight: FontWeight.w600, color: Theme.of(context) .primaryTextTheme .title diff --git a/lib/src/screens/exchange/exchange_page.dart b/lib/src/screens/exchange/exchange_page.dart index d1e7813f9..c5226e8f7 100644 --- a/lib/src/screens/exchange/exchange_page.dart +++ b/lib/src/screens/exchange/exchange_page.dart @@ -10,6 +10,7 @@ import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/stores/exchange_template/exchange_template_store.dart'; import 'package:cake_wallet/src/screens/exchange/widgets/present_provider_picker.dart'; import 'package:cake_wallet/src/screens/exchange/widgets/base_exchange_widget.dart'; +import 'package:cake_wallet/src/widgets/trail_button.dart'; class ExchangePage extends BasePage { @override @@ -32,20 +33,9 @@ class ExchangePage extends BasePage { Widget trailing(BuildContext context) { final exchangeStore = Provider.of(context); - return ButtonTheme( - minWidth: double.minPositive, - highlightColor: Colors.transparent, - splashColor: Colors.transparent, - child: FlatButton( - padding: EdgeInsets.all(0), - child: Text( - S.of(context).clear, - style: TextStyle( - color: Theme.of(context).primaryTextTheme.caption.color, - fontWeight: FontWeight.w500, - fontSize: 14), - ), - onPressed: () => exchangeStore.reset()), + return TrailButton( + caption: S.of(context).reset, + onPressed: () => exchangeStore.reset() ); } diff --git a/lib/src/screens/monero_accounts/monero_account_edit_or_create_page.dart b/lib/src/screens/monero_accounts/monero_account_edit_or_create_page.dart new file mode 100644 index 000000000..96ab7fba7 --- /dev/null +++ b/lib/src/screens/monero_accounts/monero_account_edit_or_create_page.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/core/monero_account_label_validator.dart'; +import 'package:cake_wallet/view_model/monero_account_list/monero_account_edit_or_create_state.dart'; +import 'package:cake_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; + +class MoneroAccountEditOrCreatePage extends BasePage { + MoneroAccountEditOrCreatePage({@required this.moneroAccountCreationViewModel}) + : _formKey = GlobalKey(), + _textController = TextEditingController() { + _textController.addListener( + () => moneroAccountCreationViewModel.label = _textController.text); + _textController.text = moneroAccountCreationViewModel.label; + } + + final MoneroAccountEditOrCreateViewModel moneroAccountCreationViewModel; + + @override + String get title => S.current.account; + + final GlobalKey _formKey; + final TextEditingController _textController; + + @override + Widget body(BuildContext context) => + Form( + key: _formKey, + child: Container( + color: Theme.of(context).backgroundColor, + padding: EdgeInsets.all(24.0), + child: Column( + children: [ + Expanded( + child: Center( + child: BaseTextFormField( + controller: _textController, + hintText: S.of(context).account, + validator: MoneroLabelValidator(), + ))), + Observer( + builder: (_) => + LoadingPrimaryButton( + onPressed: () async { + if (!_formKey.currentState.validate()) { + return; + } + + await moneroAccountCreationViewModel.save(); + + Navigator.of(context).pop(_textController.text); + }, + text: moneroAccountCreationViewModel.isEdit + ? S.of(context).rename + : S.of(context).add, + color: Colors.green, + textColor: Colors.white, + isLoading: moneroAccountCreationViewModel.state + is AccountIsCreating, + isDisabled: + moneroAccountCreationViewModel.label?.isEmpty ?? true, + )) + ], + ), + ), + ); +} \ No newline at end of file diff --git a/lib/src/screens/accounts/account_list_page.dart b/lib/src/screens/monero_accounts/monero_account_list_page.dart similarity index 51% rename from lib/src/screens/accounts/account_list_page.dart rename to lib/src/screens/monero_accounts/monero_account_list_page.dart index 0d9a19d95..74792e4ff 100644 --- a/lib/src/screens/accounts/account_list_page.dart +++ b/lib/src/screens/monero_accounts/monero_account_list_page.dart @@ -1,33 +1,30 @@ import 'dart:ui'; -import 'package:provider/provider.dart'; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:cake_wallet/palette.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/stores/account_list/account_list_store.dart'; -import 'package:cake_wallet/src/screens/accounts/widgets/account_tile.dart'; -import 'package:cake_wallet/src/stores/wallet/wallet_store.dart'; +import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart'; +import 'package:cake_wallet/src/screens/monero_accounts/widgets/account_tile.dart'; -class AccountListPage extends StatefulWidget { - AccountListPage({@required this.accountListStore}); +class MoneroAccountListPage extends StatefulWidget { + MoneroAccountListPage({@required this.accountListViewModel}); - final AccountListStore accountListStore; + final MoneroAccountListViewModel accountListViewModel; @override - AccountListPageForm createState() => AccountListPageForm(accountListStore); + MoneroAccountListPageForm createState() => + MoneroAccountListPageForm(accountListViewModel); } -class AccountListPageForm extends State { - AccountListPageForm(this.accountListStore); +class MoneroAccountListPageForm extends State { + MoneroAccountListPageForm(this.accountListViewModel); - final AccountListStore accountListStore; + final MoneroAccountListViewModel accountListViewModel; @override Widget build(BuildContext context) { - final walletStore = Provider.of(context); - return GestureDetector( onTap: () => Navigator.of(context).pop(), child: Container( @@ -35,7 +32,8 @@ class AccountListPageForm extends State { child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 3.0, sigmaY: 3.0), child: Container( - decoration: BoxDecoration(color: PaletteDark.darkNightBlue.withOpacity(0.75)), + decoration: BoxDecoration( + color: PaletteDark.darkNightBlue.withOpacity(0.75)), child: Center( child: Column( mainAxisSize: MainAxisSize.min, @@ -46,11 +44,10 @@ class AccountListPageForm extends State { S.of(context).choose_account, textAlign: TextAlign.center, style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - decoration: TextDecoration.none, - color: Colors.white - ), + fontSize: 18, + fontWeight: FontWeight.bold, + decoration: TextDecoration.none, + color: Colors.white), ), ), Padding( @@ -61,52 +58,42 @@ class AccountListPageForm extends State { borderRadius: BorderRadius.all(Radius.circular(14)), child: Container( height: 296, - color: Theme.of(context).accentTextTheme.title.backgroundColor, + color: Theme.of(context) + .accentTextTheme + .title + .backgroundColor, child: Column( children: [ - Expanded( - child: Observer( - builder: (_) { - final accounts = accountListStore.accounts; + Expanded(child: Observer(builder: (_) { + final accounts = + widget.accountListViewModel.accounts; - return ListView.separated( - separatorBuilder: (context, index) => Divider( - color: Theme.of(context).dividerColor, - height: 1, - ), - itemCount: accounts == null ? 0 : accounts.length, - itemBuilder: (context, index) { - final account = accounts[index]; + return ListView.separated( + separatorBuilder: (context, index) => Divider( + color: Theme.of(context).dividerColor, + height: 1), + itemCount: accounts.length ?? 0, + itemBuilder: (context, index) { + final account = accounts[index]; - return Observer( - builder: (_) { - final isCurrent = walletStore.account.id == account.id; - - return AccountTile( - isCurrent: isCurrent, - accountName: account.label, - onTap: () { - if (isCurrent) { - return; - } - - walletStore.setAccount(account); - Navigator.of(context).pop(); - } - ); + return AccountTile( + isCurrent: account.isSelected, + accountName: account.label, + onTap: () { + if (account.isSelected) { + return; } - ); - }, - ); - } - ) - ), + + widget.accountListViewModel + .select(account); + Navigator.of(context).pop(); + }); + }, + ); + })), GestureDetector( - onTap: () async { - await Navigator.of(context) - .pushNamed(Routes.accountCreation); - accountListStore.updateAccountList(); - }, + onTap: () async => await Navigator.of(context) + .pushNamed(Routes.accountCreation), child: Container( height: 62, color: Colors.white, diff --git a/lib/src/screens/accounts/widgets/account_tile.dart b/lib/src/screens/monero_accounts/widgets/account_tile.dart similarity index 100% rename from lib/src/screens/accounts/widgets/account_tile.dart rename to lib/src/screens/monero_accounts/widgets/account_tile.dart diff --git a/lib/src/screens/nodes/new_node_page.dart b/lib/src/screens/nodes/new_node_page.dart deleted file mode 100644 index 2f170e0a7..000000000 --- a/lib/src/screens/nodes/new_node_page.dart +++ /dev/null @@ -1,241 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:provider/provider.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/stores/node_list/node_list_store.dart'; -import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; - -class NewNodePage extends BasePage { - @override - String get title => S.current.node_new; - - @override - Widget body(BuildContext context) => NewNodePageForm(); -} - -class NewNodePageForm extends StatefulWidget { - @override - NewNodeFormState createState() => NewNodeFormState(); -} - -class NewNodeFormState extends State { - final _formKey = GlobalKey(); - final _nodeAddressController = TextEditingController(); - final _nodePortController = TextEditingController(); - final _loginController = TextEditingController(); - final _passwordController = TextEditingController(); - - @override - void dispose() { - _nodeAddressController.dispose(); - _nodePortController.dispose(); - _loginController.dispose(); - _passwordController.dispose(); - super.dispose(); - } - - void onHandleControllers(NodeListStore nodeListStore) { - if (_nodeAddressController.text.isNotEmpty && - _nodePortController.text.isNotEmpty) { - nodeListStore.setDisabledState(false); - } else { - nodeListStore.setDisabledState(true); - } - } - - @override - Widget build(BuildContext context) { - final nodeList = Provider.of(context); - - _nodeAddressController.addListener(() {onHandleControllers(nodeList);}); - _nodePortController.addListener(() {onHandleControllers(nodeList);}); - - return Container( - padding: EdgeInsets.only(left: 24, right: 24), - child: ScrollableWithBottomSection( - contentPadding: EdgeInsets.only(bottom: 24.0), - content: Form( - key: _formKey, - child: Column( - children: [ - Row( - children: [ - Expanded( - child: TextFormField( - style: TextStyle( - fontSize: 16.0, - color: Theme.of(context).primaryTextTheme.title.color - ), - decoration: InputDecoration( - hintStyle: - TextStyle( - color: Theme.of(context).primaryTextTheme.caption.color, - fontSize: 16 - ), - hintText: S.of(context).node_address, - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).dividerColor, - width: 1.0)), - enabledBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).dividerColor, - width: 1.0))), - controller: _nodeAddressController, - validator: (value) { - nodeList.validateNodeAddress(value); - return nodeList.errorMessage; - }, - ), - ) - ], - ), - SizedBox(height: 10.0), - Row( - children: [ - Expanded( - child: TextFormField( - style: TextStyle( - fontSize: 16.0, - color: Theme.of(context).primaryTextTheme.title.color - ), - keyboardType: TextInputType.numberWithOptions( - signed: false, decimal: false), - decoration: InputDecoration( - hintStyle: - TextStyle( - color: Theme.of(context).primaryTextTheme.caption.color, - fontSize: 16 - ), - hintText: S.of(context).node_port, - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).dividerColor, - width: 1.0)), - enabledBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).dividerColor, - width: 1.0))), - controller: _nodePortController, - validator: (value) { - nodeList.validateNodePort(value); - return nodeList.errorMessage; - }, - ), - ) - ], - ), - SizedBox(height: 10.0), - Row( - children: [ - Expanded( - child: TextFormField( - style: TextStyle( - fontSize: 16.0, - color: Theme.of(context).primaryTextTheme.title.color - ), - decoration: InputDecoration( - hintStyle: - TextStyle( - color: Theme.of(context).primaryTextTheme.caption.color, - fontSize: 16 - ), - hintText: S.of(context).login, - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).dividerColor, - width: 1.0)), - enabledBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).dividerColor, - width: 1.0))), - controller: _loginController, - validator: (value) => null, - ), - ) - ], - ), - SizedBox(height: 10.0), - Row( - children: [ - Expanded( - child: TextFormField( - style: TextStyle( - fontSize: 16.0, - color: Theme.of(context).primaryTextTheme.title.color - ), - decoration: InputDecoration( - hintStyle: - TextStyle( - color: Theme.of(context).primaryTextTheme.caption.color, - fontSize: 16 - ), - hintText: S.of(context).password, - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).dividerColor, - width: 1.0)), - enabledBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).dividerColor, - width: 1.0))), - controller: _passwordController, - validator: (value) => null, - ), - ) - ], - ) - ], - ) - ), - bottomSectionPadding: EdgeInsets.only(bottom: 24), - bottomSection: Observer( - builder: (_) => Row( - children: [ - Flexible( - child: Container( - padding: EdgeInsets.only(right: 8.0), - child: PrimaryButton( - onPressed: () { - _nodeAddressController.text = ''; - _nodePortController.text = ''; - _loginController.text = ''; - _passwordController.text = ''; - }, - text: S.of(context).reset, - color: Colors.red, - textColor: Colors.white), - )), - Flexible( - child: Container( - padding: EdgeInsets.only(left: 8.0), - child: PrimaryButton( - onPressed: () async { - if (!_formKey.currentState.validate()) { - return; - } - - await nodeList.addNode( - address: _nodeAddressController.text, - port: _nodePortController.text, - login: _loginController.text, - password: _passwordController.text); - - Navigator.of(context).pop(); - }, - text: S.of(context).save, - color: Colors.green, - textColor: Colors.white, - isDisabled: nodeList.disabledState, - ), - )), - ], - ) - ), - ) - ); - } -} diff --git a/lib/src/screens/nodes/node_create_or_edit_page.dart b/lib/src/screens/nodes/node_create_or_edit_page.dart new file mode 100644 index 000000000..6c79fdc3c --- /dev/null +++ b/lib/src/screens/nodes/node_create_or_edit_page.dart @@ -0,0 +1,249 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/core/node_address_validator.dart'; +import 'package:cake_wallet/core/node_port_validator.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart'; + +class NodeCreateOrEditPage extends BasePage { + NodeCreateOrEditPage(this.nodeCreateOrEditViewModel) + : _formKey = GlobalKey(), + _addressController = TextEditingController(), + _portController = TextEditingController(), + _loginController = TextEditingController(), + _passwordController = TextEditingController() { + reaction((_) => nodeCreateOrEditViewModel.address, (String address) { + if (address != _addressController.text) { + _addressController.text = address; + } + }); + + reaction((_) => nodeCreateOrEditViewModel.port, (String port) { + if (port != _portController.text) { + _portController.text = port; + } + }); + + if (nodeCreateOrEditViewModel.hasAuthCredentials) { + reaction((_) => nodeCreateOrEditViewModel.login, (String login) { + if (login != _loginController.text) { + _loginController.text = login; + } + }); + + reaction((_) => nodeCreateOrEditViewModel.password, (String password) { + if (password != _passwordController.text) { + _passwordController.text = password; + } + }); + } + + _addressController.addListener( + () => nodeCreateOrEditViewModel.address = _addressController.text); + _portController.addListener( + () => nodeCreateOrEditViewModel.port = _portController.text); + _loginController.addListener( + () => nodeCreateOrEditViewModel.login = _loginController.text); + _passwordController.addListener( + () => nodeCreateOrEditViewModel.password = _passwordController.text); + } + + final GlobalKey _formKey; + final TextEditingController _addressController; + final TextEditingController _portController; + final TextEditingController _loginController; + final TextEditingController _passwordController; + + @override + String get title => S.current.node_new; + + final NodeCreateOrEditViewModel nodeCreateOrEditViewModel; + + @override + Widget body(BuildContext context) { + return Container( + padding: EdgeInsets.only(left: 24, right: 24), + child: ScrollableWithBottomSection( + contentPadding: EdgeInsets.only(bottom: 24.0), + content: Form( + key: _formKey, + child: Column( + children: [ + Row( + children: [ + Expanded( + child: TextFormField( + style: TextStyle( + fontSize: 16.0, + color: Theme.of(context) + .primaryTextTheme + .title + .color), + decoration: InputDecoration( + hintStyle: TextStyle( + color: Theme.of(context) + .primaryTextTheme + .caption + .color, + fontSize: 16), + hintText: S.of(context).node_address, + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).dividerColor, + width: 1.0)), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).dividerColor, + width: 1.0))), + controller: _addressController, + validator: NodeAddressValidator(), + ), + ) + ], + ), + SizedBox(height: 10.0), + Row( + children: [ + Expanded( + child: TextFormField( + style: TextStyle( + fontSize: 16.0, + color: Theme.of(context) + .primaryTextTheme + .title + .color), + keyboardType: TextInputType.numberWithOptions( + signed: false, decimal: false), + decoration: InputDecoration( + hintStyle: TextStyle( + color: Theme.of(context) + .primaryTextTheme + .caption + .color, + fontSize: 16), + hintText: S.of(context).node_port, + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).dividerColor, + width: 1.0)), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).dividerColor, + width: 1.0))), + controller: _portController, + validator: NodePortValidator(), + ), + ) + ], + ), + SizedBox(height: 10.0), + if (nodeCreateOrEditViewModel.hasAuthCredentials) ...[ + Row( + children: [ + Expanded( + child: TextFormField( + style: TextStyle( + fontSize: 16.0, + color: Theme.of(context) + .primaryTextTheme + .title + .color), + decoration: InputDecoration( + hintStyle: TextStyle( + color: Theme.of(context) + .primaryTextTheme + .caption + .color, + fontSize: 16), + hintText: S.of(context).login, + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).dividerColor, + width: 1.0)), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).dividerColor, + width: 1.0))), + controller: _loginController, + validator: (value) => null, + ), + ) + ], + ), + SizedBox(height: 10.0), + Row( + children: [ + Expanded( + child: TextFormField( + style: TextStyle( + fontSize: 16.0, + color: Theme.of(context) + .primaryTextTheme + .title + .color), + decoration: InputDecoration( + hintStyle: TextStyle( + color: Theme.of(context) + .primaryTextTheme + .caption + .color, + fontSize: 16), + hintText: S.of(context).password, + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).dividerColor, + width: 1.0)), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).dividerColor, + width: 1.0))), + controller: _passwordController, + validator: (value) => null, + ), + ) + ], + ) + ] + ], + )), + bottomSectionPadding: EdgeInsets.only(bottom: 24), + bottomSection: Observer( + builder: (_) => Row( + children: [ + Flexible( + child: Container( + padding: EdgeInsets.only(right: 8.0), + child: PrimaryButton( + onPressed: () => nodeCreateOrEditViewModel.reset(), + text: S.of(context).reset, + color: Colors.red, + textColor: Colors.white), + )), + Flexible( + child: Container( + padding: EdgeInsets.only(left: 8.0), + child: PrimaryButton( + onPressed: () async { + if (!_formKey.currentState.validate()) { + return; + } + + await nodeCreateOrEditViewModel.save(); + Navigator.of(context).pop(); + }, + text: S.of(context).save, + color: Colors.green, + textColor: Colors.white, + isDisabled: !nodeCreateOrEditViewModel.isReady, + ), + )), + ], + )), + )); + } +} diff --git a/lib/src/screens/nodes/nodes_list_page.dart b/lib/src/screens/nodes/nodes_list_page.dart index 8c0a04be6..2777e25b7 100644 --- a/lib/src/screens/nodes/nodes_list_page.dart +++ b/lib/src/screens/nodes/nodes_list_page.dart @@ -1,35 +1,30 @@ import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; -import 'package:provider/provider.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/palette.dart'; import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/screens/nodes/widgets/node_indicator.dart'; -import 'package:cake_wallet/src/stores/node_list/node_list_store.dart'; -import 'package:cake_wallet/src/stores/settings/settings_store.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/nodes/widgets/node_list_row.dart'; +import 'package:cake_wallet/src/widgets/standard_list.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; +import 'package:cake_wallet/view_model/node_list/node_list_view_model.dart'; class NodeListPage extends BasePage { - NodeListPage(); + NodeListPage(this.nodeListViewModel); @override String get title => S.current.nodes; + final NodeListViewModel nodeListViewModel; + @override Widget trailing(context) { - final nodeList = Provider.of(context); - final settings = Provider.of(context); - return Container( height: 32, - width: 72, decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(16)), - color: Theme.of(context).accentTextTheme.title.backgroundColor - ), + borderRadius: BorderRadius.all(Radius.circular(16)), + color: Theme.of(context).accentTextTheme.title.backgroundColor), child: ButtonTheme( minWidth: double.minPositive, child: FlatButton( @@ -38,24 +33,24 @@ class NodeListPage extends BasePage { context: context, builder: (BuildContext context) { return AlertWithTwoActions( - alertTitle: S.of(context).node_reset_settings_title, - alertContent: S.of(context).nodes_list_reset_to_default_message, - leftButtonText: S.of(context).reset, - rightButtonText: S.of(context).cancel, - actionLeftButton: () async { - Navigator.of(context).pop(); - await nodeList.reset(); - await settings.setCurrentNodeToDefault(); - }, - actionRightButton: () => Navigator.of(context).pop() - ); + alertTitle: S.of(context).node_reset_settings_title, + alertContent: + S.of(context).nodes_list_reset_to_default_message, + leftButtonText: S.of(context).reset, + rightButtonText: S.of(context).cancel, + actionLeftButton: () async { + Navigator.of(context).pop(); + await nodeListViewModel.reset(); + }, + actionRightButton: () => Navigator.of(context).pop()); }); }, child: Text( S.of(context).reset, textAlign: TextAlign.center, style: TextStyle( - fontSize: 10.0, + fontSize: 14.0, + fontWeight: FontWeight.w600, color: Colors.blue), )), ), @@ -63,157 +58,77 @@ class NodeListPage extends BasePage { } @override - Widget body(context) => NodeListPageBody(); -} - -class NodeListPageBody extends StatefulWidget { - @override - NodeListPageBodyState createState() => NodeListPageBodyState(); -} - -class NodeListPageBodyState extends State { - @override - Widget build(BuildContext context) { - final nodeList = Provider.of(context); - final settings = Provider.of(context); - - final trashImage = Image.asset('assets/images/trash.png', height: 32, width: 32, color: Colors.white); - - final currentColor = Theme.of(context).accentTextTheme.subtitle.decorationColor; - final notCurrentColor = Theme.of(context).accentTextTheme.title.backgroundColor; - - final currentTextColor = Colors.blue; - final notCurrentTextColor = Theme.of(context).primaryTextTheme.title.color; - + Widget body(context) { return Container( - height: double.infinity, - padding: EdgeInsets.only(top: 12), - child: Column( - mainAxisSize: MainAxisSize.max, - children: [ - NodeListRow( - title: S.of(context).add_new_node, - trailing: Icon(Icons.add, - color: Theme.of(context).primaryTextTheme.title.color, - size: 24.0), - color: Theme.of(context).accentTextTheme.title.backgroundColor, - textColor: Theme.of(context).primaryTextTheme.title.color, - onTap: () async => - await Navigator.of(context).pushNamed(Routes.newNode), - isDrawTop: true, - isDrawBottom: true), - Expanded( - child: Padding( - padding: EdgeInsets.only(top: 32), - child: Observer( - builder: (_) => ListView.separated( - separatorBuilder: (_, __) => Container( - height: 1, - padding: EdgeInsets.only(left: 24), - color: Theme.of(context).accentTextTheme.title.backgroundColor, - child: Container( - height: 1, - color: Theme.of(context).dividerColor, - ), - ), - itemCount: nodeList.nodes.length, - itemBuilder: (BuildContext context, int index) { - final node = nodeList.nodes[index]; + padding: EdgeInsets.only(top: 10), + child: Observer( + builder: (_) => SectionStandardList( + sectionCount: 2, + context: context, + itemBuilder: (_, sectionIndex, index) { + if (sectionIndex == 0) { + return NodeHeaderListRow( + title: S.of(context).add_new_node, + onTap: (_) async => + await Navigator.of(context).pushNamed(Routes.newNode)); + } - final isDrawTop = index == 0 ? true : false; - final isDrawBottom = index == nodeList.nodes.length - 1 ? true : false; + final node = nodeListViewModel.nodes[index]; + final isSelected = index == 1; // FIXME: hardcoded value. + final nodeListRow = NodeListRow( + title: node.uri, + isSelected: isSelected, + isAlive: node.requestNode(), + onTap: (_) {}); - return Observer( - builder: (_) { - final isCurrent = settings.node == null - ? false - : node.key == settings.node.key; + final dismissibleRow = Dismissible( + key: Key('${node.key}'), + confirmDismiss: (direction) async { + return await showDialog( + context: context, + builder: (BuildContext context) { + return AlertWithTwoActions( + alertTitle: S.of(context).remove_node, + alertContent: S.of(context).remove_node_message, + leftButtonText: S.of(context).remove, + rightButtonText: S.of(context).cancel, + actionLeftButton: () => + Navigator.pop(context, true), + actionRightButton: () => + Navigator.pop(context, false)); + }); + }, + onDismissed: (direction) async => + nodeListViewModel.delete(node), + direction: DismissDirection.endToStart, + background: Container( + padding: EdgeInsets.only(right: 10.0), + alignment: AlignmentDirectional.centerEnd, + color: Palette.red, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + const Icon( + CupertinoIcons.delete, + color: Colors.white, + ), + Text( + S.of(context).delete, + style: TextStyle(color: Colors.white), + ) + ], + )), + child: nodeListRow); - final content = NodeListRow( - title: node.uri, - trailing: FutureBuilder( - future: nodeList.isNodeOnline(node), - builder: (context, snapshot) { - switch (snapshot.connectionState) { - case ConnectionState.done: - return NodeIndicator( - color: snapshot.data as bool - ? Palette.green - : Palette.red); - default: - return NodeIndicator(); - } - }), - color: isCurrent ? currentColor : notCurrentColor, - textColor: isCurrent ? currentTextColor : notCurrentTextColor, - onTap: () async { - if (!isCurrent) { - await showDialog( - context: context, - builder: (BuildContext context) { - return AlertWithTwoActions( - alertTitle: S.current.nodes, - alertContent: S.of(context) - .change_current_node(node.uri), - leftButtonText: S.of(context).change, - rightButtonText: S.of(context).cancel, - actionLeftButton: () async { - Navigator.of(context).pop(); - await settings.setCurrentNode( - node: node); - }, - actionRightButton: () => Navigator.of(context).pop() - ); - }); - } - }, - isDrawTop: isDrawTop, - isDrawBottom: isDrawBottom); + return isSelected ? nodeListRow : dismissibleRow; + }, + itemCounter: (int sectionIndex) { + if (sectionIndex == 0) { + return 1; + } - return isCurrent - ? content - : Dismissible( - key: Key('${node.key}'), - confirmDismiss: (direction) async { - return await showDialog( - context: context, - builder: (BuildContext context) { - return AlertWithTwoActions( - alertTitle: S.of(context).remove_node, - alertContent: S.of(context).remove_node_message, - leftButtonText: S.of(context).remove, - rightButtonText: S.of(context).cancel, - actionLeftButton: () => - Navigator.pop(context, true), - actionRightButton: () => - Navigator.pop(context, false) - ); - }); - }, - onDismissed: (direction) async => - await nodeList.remove(node: node), - direction: DismissDirection.endToStart, - background: Container( - padding: EdgeInsets.only(right: 10.0, top: 2), - alignment: AlignmentDirectional.centerEnd, - color: Palette.red, - child: Column( - children: [ - trashImage, - Text( - S.of(context).delete, - style: TextStyle(color: Colors.white), - ) - ], - )), - child: content); - }, - ); - }) - ), - ) - ) - ], + return nodeListViewModel.nodes.length; + }), ), ); } diff --git a/lib/src/screens/nodes/widgets/node_indicator.dart b/lib/src/screens/nodes/widgets/node_indicator.dart index cf1557428..089950bed 100644 --- a/lib/src/screens/nodes/widgets/node_indicator.dart +++ b/lib/src/screens/nodes/widgets/node_indicator.dart @@ -2,16 +2,17 @@ import 'package:flutter/material.dart'; import 'package:cake_wallet/palette.dart'; class NodeIndicator extends StatelessWidget { - NodeIndicator({this.color = Palette.red}); + NodeIndicator({this.isLive = false}); - final Color color; + final bool isLive; @override Widget build(BuildContext context) { return Container( width: 8.0, height: 8.0, - decoration: BoxDecoration(shape: BoxShape.circle, color: color), + decoration: BoxDecoration( + shape: BoxShape.circle, color: isLive ? Palette.green : Palette.red), ); } } diff --git a/lib/src/screens/nodes/widgets/node_list_row.dart b/lib/src/screens/nodes/widgets/node_list_row.dart index 17a17c8a9..f5ba95d11 100644 --- a/lib/src/screens/nodes/widgets/node_list_row.dart +++ b/lib/src/screens/nodes/widgets/node_list_row.dart @@ -1,62 +1,43 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/palette.dart'; +import 'package:cake_wallet/src/screens/nodes/widgets/node_indicator.dart'; +import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; +import 'package:cake_wallet/src/widgets/standard_list.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -class NodeListRow extends StatelessWidget { - NodeListRow({ - @required this.title, - @required this.trailing, - @required this.color, - @required this.textColor, - @required this.onTap, - @required this.isDrawTop, - @required this.isDrawBottom}); +class NodeListRow extends StandardListRow { + NodeListRow( + {@required String title, + @required void Function(BuildContext context) onTap, + @required bool isSelected, + @required this.isAlive}) + : super(title: title, onTap: onTap, isSelected: isSelected); - final String title; - final Widget trailing; - final Color color; - final Color textColor; - final VoidCallback onTap; - final bool isDrawTop; - final bool isDrawBottom; + final Future isAlive; @override - Widget build(BuildContext context) { - return Column( - children: [ - isDrawTop - ? Container( - width: double.infinity, - height: 1, - color: Theme.of(context).dividerColor, - ) - : Offstage(), - Container( - width: double.infinity, - height: 56, - color: color, - child: ListTile( - contentPadding: EdgeInsets.only( - left: 24, - right: 24, - ), - title: Text( - title, - style: TextStyle( - fontSize: 14, - color: textColor - ), - textAlign: TextAlign.left), - trailing: trailing, - onTap: onTap, - ) - ), - isDrawBottom - ? Container( - width: double.infinity, - height: 1, - color: Theme.of(context).dividerColor, - ) - : Offstage(), - ], - ); + Widget buildTrailing(BuildContext context) { + return FutureBuilder( + future: isAlive, + builder: (context, snapshot) { + switch (snapshot.connectionState) { + case ConnectionState.done: + return NodeIndicator(isLive: snapshot.data as bool); + default: + return NodeIndicator(); + } + }); + } +} + +class NodeHeaderListRow extends StandardListRow { + NodeHeaderListRow({@required String title, @required void Function(BuildContext context) onTap}) + : super(title: title, onTap: onTap, isSelected: false); + + @override + Widget buildTrailing(BuildContext context) { + return Icon(Icons.add, + color: Theme.of(context).primaryTextTheme.title.color, size: 24.0); } } diff --git a/lib/src/screens/pin_code/pin_code.dart b/lib/src/screens/pin_code/pin_code.dart index 111355b2d..b046b3fef 100644 --- a/lib/src/screens/pin_code/pin_code.dart +++ b/lib/src/screens/pin_code/pin_code.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:cake_wallet/src/stores/settings/settings_store.dart'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/domain/common/biometric_auth.dart'; abstract class PinCodeWidget extends StatefulWidget { PinCodeWidget({Key key, this.onPinCodeEntered, this.hasLengthSwitcher}) @@ -29,6 +30,7 @@ class PinCodeState extends State { static const sixPinLength = 6; static const fourPinLength = 4; final _gridViewKey = GlobalKey(); + final _key = GlobalKey(); int pinLength = defaultPinLength; List pin = List.filled(defaultPinLength, null); @@ -83,9 +85,12 @@ class PinCodeState extends State { } @override - Widget build(BuildContext context) => Scaffold(body: body(context)); + Widget build(BuildContext context) => + Scaffold(key: _key, body: body(context)); Widget body(BuildContext context) { + final settingsStore = Provider.of(context); + final deleteIconImage = Image.asset( 'assets/images/delete_icon.png', color: Theme.of(context).primaryTextTheme.title.color, @@ -95,8 +100,7 @@ class PinCodeState extends State { color: Theme.of(context).primaryTextTheme.title.color, ); - return SafeArea( - child: Container( + return Container( color: Theme.of(context).backgroundColor, padding: EdgeInsets.only(left: 40.0, right: 40.0, bottom: 40.0), child: Column(children: [ @@ -105,8 +109,7 @@ class PinCodeState extends State { style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, - color: Theme.of(context).primaryTextTheme.title.color - )), + color: Theme.of(context).primaryTextTheme.title.color)), Spacer(flex: 3), Container( width: 180, @@ -138,7 +141,9 @@ class PinCodeState extends State { }, child: Text( _changePinLengthText(), - style: TextStyle(fontSize: 14.0, color: Theme.of(context).primaryTextTheme.caption.color), + style: TextStyle( + fontSize: 14.0, + color: Theme.of(context).primaryTextTheme.caption.color), )) ], Spacer(flex: 1), @@ -161,10 +166,38 @@ class PinCodeState extends State { margin: EdgeInsets.only( left: marginLeft, right: marginRight), child: FlatButton( - onPressed: () {}, + onPressed: (widget.hasLengthSwitcher || + !settingsStore + .allowBiometricalAuthentication) + ? null + : () { + // FIXME +// if (authStore != null) { +// WidgetsBinding.instance.addPostFrameCallback((_) { +// final biometricAuth = BiometricAuth(); +// biometricAuth.isAuthenticated().then( +// (isAuth) { +// if (isAuth) { +// authStore.biometricAuth(); +// _key.currentState.showSnackBar( +// SnackBar( +// content: Text(S.of(context).authenticated), +// backgroundColor: Colors.green, +// ), +// ); +// } +// } +// ); +// }); +// } + }, color: Theme.of(context).backgroundColor, shape: CircleBorder(), - child: faceImage, + child: (widget.hasLengthSwitcher || + !settingsStore + .allowBiometricalAuthentication) + ? Offstage() + : faceImage, ), ); } else if (index == 10) { @@ -195,14 +228,17 @@ class PinCodeState extends State { style: TextStyle( fontSize: 30.0, fontWeight: FontWeight.bold, - color: Theme.of(context).primaryTextTheme.title.color)), + color: Theme.of(context) + .primaryTextTheme + .title + .color)), ), ); }), ) : null)) ]), - )); + ); } void _push(int num) { diff --git a/lib/src/screens/receive/receive_page.dart b/lib/src/screens/receive/receive_page.dart index e4e588268..19cc6fecc 100644 --- a/lib/src/screens/receive/receive_page.dart +++ b/lib/src/screens/receive/receive_page.dart @@ -1,29 +1,22 @@ -import 'package:cake_wallet/palette.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:esys_flutter_share/esys_flutter_share.dart'; -import 'package:flutter_slidable/flutter_slidable.dart'; -import 'package:provider/provider.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/stores/subaddress_list/subaddress_list_store.dart'; -import 'package:cake_wallet/src/stores/wallet/wallet_store.dart'; +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart'; -import 'package:cake_wallet/src/screens/accounts/account_list_page.dart'; -import 'package:cake_wallet/src/stores/account_list/account_list_store.dart'; +import 'package:cake_wallet/src/screens/monero_accounts/monero_account_list_page.dart'; import 'package:cake_wallet/src/screens/receive/widgets/header_tile.dart'; import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; -import 'package:cake_wallet/themes.dart'; -import 'package:cake_wallet/theme_changer.dart'; import 'package:cake_wallet/core/amount_validator.dart'; import 'package:cake_wallet/src/screens/receive/widgets/address_cell.dart'; -import 'package:cake_wallet/view_model/address_list/account_list_header.dart'; -import 'package:cake_wallet/view_model/address_list/address_list_header.dart'; -import 'package:cake_wallet/view_model/address_list/address_list_item.dart'; -import 'package:cake_wallet/view_model/address_list/address_list_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_address_list/wallet_account_list_header.dart'; +import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_header.dart'; +import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; +import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; class ReceivePage extends BasePage { ReceivePage({this.addressListViewModel}) @@ -33,7 +26,7 @@ class ReceivePage extends BasePage { _formKey.currentState.validate() ? amountController.text : ''); } - final AddressListViewModel addressListViewModel; + final WalletAddressListViewModel addressListViewModel; final TextEditingController amountController; final GlobalKey _formKey; @@ -83,11 +76,6 @@ class ReceivePage extends BasePage { ); } - @override - Widget build(BuildContext context) { - return super.build(context); - } - @override Widget body(BuildContext context) { final copyImage = Image.asset('assets/images/copy_content.png', @@ -186,7 +174,7 @@ class ReceivePage extends BasePage { overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 18, - fontWeight: FontWeight.w600, + fontWeight: FontWeight.w500, color: Theme.of(context) .primaryTextTheme .title @@ -213,17 +201,11 @@ class ReceivePage extends BasePage { final item = addressListViewModel.items[index]; Widget cell = Container(); - if (item is AccountListHeader) { + if (item is WalletAccountListHeader) { cell = HeaderTile( - onTap: () async { - await showDialog( - context: context, - builder: (BuildContext context) { -// return AccountListPage( -// accountListStore: -// accountListStore); - }); - }, + onTap: () async => await showDialog( + context: context, + builder: (_) => getIt.get()), title: addressListViewModel.accountLabel, icon: Icon( Icons.arrow_forward_ios, @@ -233,11 +215,11 @@ class ReceivePage extends BasePage { )); } - if (item is AddressListHeader) { + if (item is WalletAddressListHeader) { cell = HeaderTile( onTap: () => Navigator.of(context) .pushNamed(Routes.newSubaddress), - title: S.of(context).subaddresses, + title: S.of(context).addresses, icon: Icon( Icons.add, size: 20, @@ -246,15 +228,15 @@ class ReceivePage extends BasePage { )); } - if (item is AddressListItem) { + if (item is WalletAddressListItem) { cell = Observer( builder: (_) => AddressCell.fromItem(item, isCurrent: item.address == addressListViewModel.address.address, - onTap: (_) => - addressListViewModel.address = item, - onEdit: () => Navigator.of(context) - .pushNamed(Routes.newSubaddress, arguments: item))); + onTap: (_) => addressListViewModel.address = item, + onEdit: () => Navigator.of(context).pushNamed( + Routes.newSubaddress, + arguments: item))); } return index != 0 diff --git a/lib/src/screens/receive/widgets/address_cell.dart b/lib/src/screens/receive/widgets/address_cell.dart index dfe1cf66e..5e725583b 100644 --- a/lib/src/screens/receive/widgets/address_cell.dart +++ b/lib/src/screens/receive/widgets/address_cell.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/view_model/address_list/address_list_item.dart'; +import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; class AddressCell extends StatelessWidget { - factory AddressCell.fromItem(AddressListItem item, + factory AddressCell.fromItem(WalletAddressListItem item, {@required bool isCurrent, Function(String) onTap, Function() onEdit}) => @@ -47,7 +47,7 @@ class AddressCell extends StatelessWidget { name ?? address, style: TextStyle( fontSize: name?.isNotEmpty ?? false ? 18 : 10, - fontWeight: FontWeight.bold, + fontWeight: FontWeight.w600, color: isCurrent ? currentTextColor : notCurrentTextColor, ), ), diff --git a/lib/src/screens/receive/widgets/header_tile.dart b/lib/src/screens/receive/widgets/header_tile.dart index 5688eaed7..84ed2b526 100644 --- a/lib/src/screens/receive/widgets/header_tile.dart +++ b/lib/src/screens/receive/widgets/header_tile.dart @@ -32,7 +32,7 @@ class HeaderTile extends StatelessWidget { title, style: TextStyle( fontSize: 18, - fontWeight: FontWeight.bold, + fontWeight: FontWeight.w500, color: Theme.of(context).primaryTextTheme.title.color ), ), diff --git a/lib/src/screens/root/root.dart b/lib/src/screens/root/root.dart index 48d6eadd7..dff20b6d1 100644 --- a/lib/src/screens/root/root.dart +++ b/lib/src/screens/root/root.dart @@ -20,7 +20,6 @@ import 'package:cake_wallet/src/domain/services/wallet_service.dart'; import 'package:cake_wallet/src/domain/exchange/trade.dart'; import 'package:cake_wallet/src/domain/monero/transaction_description.dart'; import 'package:cake_wallet/src/screens/auth/create_login_page.dart'; -import 'package:cake_wallet/src/screens/seed/create_seed_page.dart'; import 'package:cake_wallet/src/screens/dashboard/create_dashboard_page.dart'; import 'package:cake_wallet/src/screens/auth/auth_page.dart'; import 'package:cake_wallet/src/screens/welcome/create_welcome_page.dart'; @@ -104,13 +103,13 @@ class RootState extends State with WidgetsBindingObserver { return Observer(builder: (_) { final state = widget.authenticationStore.state; - print(state); + if (state == AuthenticationState.denied) { return createWelcomePage(); } if (state == AuthenticationState.installed) { - return getIt.get(); + return getIt.get(instanceName: 'login'); } if (state == AuthenticationState.allowed) { diff --git a/lib/src/screens/seed/create_seed_page.dart b/lib/src/screens/seed/create_seed_page.dart deleted file mode 100644 index b841f0324..000000000 --- a/lib/src/screens/seed/create_seed_page.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:cake_wallet/src/domain/services/wallet_service.dart'; -import 'package:cake_wallet/src/screens/seed/seed_page.dart'; -import 'package:cake_wallet/src/stores/settings/settings_store.dart'; -import 'package:cake_wallet/src/stores/wallet_seed/wallet_seed_store.dart'; - -Widget createSeedPage( - {@required SettingsStore settingsStore, - @required WalletService walletService, - @required void Function() callback}) => - Provider( - create: (_) => WalletSeedStore(walletService: walletService), - child: SeedPage(onCloseCallback: callback)); \ No newline at end of file diff --git a/lib/src/screens/seed/seed_page.dart b/lib/src/screens/seed/seed_page.dart deleted file mode 100644 index 2be122635..000000000 --- a/lib/src/screens/seed/seed_page.dart +++ /dev/null @@ -1,161 +0,0 @@ -import 'package:provider/provider.dart'; -import 'package:esys_flutter_share/esys_flutter_share.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/stores/wallet_seed/wallet_seed_store.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; - -class SeedPage extends BasePage { - SeedPage({this.onCloseCallback}); - - static final image = Image.asset('assets/images/crypto_lock.png'); - - @override - String get title => S.current.seed_title; - - final VoidCallback onCloseCallback; - - @override - void onClose(BuildContext context) => - onCloseCallback != null ? onCloseCallback() : Navigator.of(context).pop(); - - @override - Widget leading(BuildContext context) { - return onCloseCallback != null ? Offstage() : super.leading(context); - } - - @override - Widget trailing(BuildContext context) { - return onCloseCallback != null - ? GestureDetector( - onTap: () => onClose(context), - child: Container( - width: 70, - height: 32, - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(16)), - color: Theme.of(context).accentTextTheme.title.color - ), - child: Text( - S.of(context).seed_language_next, - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.w600, - color: Colors.blue - ), - ), - ), - ) - : Offstage(); - } - - @override - Widget body(BuildContext context) { - final walletSeedStore = Provider.of(context); - String _seed; - - return Container( - width: double.infinity, - height: double.infinity, - child: ScrollableWithBottomSection( - contentPadding: EdgeInsets.only(left: 40, right: 40, bottom: 20), - content: Column( - children: [ - Padding( - padding: EdgeInsets.only(top: 33), - child: image, - ), - Padding( - padding: EdgeInsets.only(top: 33), - child: Observer( - builder: (_) { - _seed = walletSeedStore.seed; - - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - walletSeedStore.name, - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Theme.of(context).primaryTextTheme.title.color - ), - ), - Padding( - padding: EdgeInsets.only(top: 20), - child: Text( - _seed, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).primaryTextTheme.caption.color - ), - ), - ) - ], - ); - } - ), - ) - ], - ), - bottomSectionPadding: EdgeInsets.only( - left: 24, - right: 24, - bottom: 52 - ), - bottomSection: Container( - child: Row( - mainAxisSize: MainAxisSize.max, - children: [ - Flexible( - child: Container( - padding: EdgeInsets.only(right: 8.0), - child: PrimaryButton( - onPressed: () => Share.text( - S.of(context).seed_share, - _seed, - 'text/plain'), - text: S.of(context).save, - color: Colors.green, - textColor: Colors.white), - ) - ), - Flexible( - child: Container( - padding: EdgeInsets.only(left: 8.0), - child: Builder( - builder: (context) => PrimaryButton( - onPressed: () { - Clipboard.setData( - ClipboardData(text: _seed)); - Scaffold.of(context).showSnackBar( - SnackBar( - content: Text(S - .of(context) - .copied_to_clipboard), - backgroundColor: Colors.green, - duration: Duration(milliseconds: 1500), - ), - ); - }, - text: S.of(context).copy, - color: Colors.blue, - textColor: Colors.white) - ), - ) - ) - ], - ), - ), - ), - ); - } -} diff --git a/lib/src/screens/seed/wallet_seed_page.dart b/lib/src/screens/seed/wallet_seed_page.dart new file mode 100644 index 000000000..cde60f5b0 --- /dev/null +++ b/lib/src/screens/seed/wallet_seed_page.dart @@ -0,0 +1,154 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; +import 'package:esys_flutter_share/esys_flutter_share.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/themes.dart'; +import 'package:cake_wallet/theme_changer.dart'; +import 'package:cake_wallet/view_model/wallet_seed_view_model.dart'; + +class WalletSeedPage extends BasePage { + WalletSeedPage(this.walletSeedViewModel, {this.onCloseCallback}); + + static final imageLight = Image.asset('assets/images/crypto_lock_light.png'); + static final imageDark = Image.asset('assets/images/crypto_lock.png'); + + @override + String get title => S.current.seed_title; + + final VoidCallback onCloseCallback; + final WalletSeedViewModel walletSeedViewModel; + + @override + void onClose(BuildContext context) => + onCloseCallback != null ? onCloseCallback() : Navigator.of(context).pop(); + + @override + Widget leading(BuildContext context) => + onCloseCallback != null ? Offstage() : super.leading(context); + + @override + Widget trailing(BuildContext context) { + return onCloseCallback != null + ? GestureDetector( + onTap: () => onClose(context), + child: Container( + width: 100, + height: 42, + alignment: Alignment.center, + margin: EdgeInsets.only(left: 10), + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(16)), + color: Theme.of(context).accentTextTheme.title.color), + child: Text( + S.of(context).seed_language_next, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.blue), + ), + ), + ) + : Offstage(); + } + + @override + Widget body(BuildContext context) { + final _themeChanger = Provider.of(context); + final image = + _themeChanger.getTheme() == Themes.darkTheme ? imageDark : imageLight; + + return Container( + padding: EdgeInsets.all(24), + child: Column( + children: [ + Flexible( + flex: 2, + child: AspectRatio( + aspectRatio: 1, + child: FittedBox(child: image, fit: BoxFit.fill))), + Flexible( + flex: 3, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: EdgeInsets.only(top: 33), + child: Observer(builder: (_) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + walletSeedViewModel.name, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Theme.of(context) + .primaryTextTheme + .title + .color), + ), + Padding( + padding: EdgeInsets.only(top: 20), + child: Text( + walletSeedViewModel.seed, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + color: Theme.of(context) + .primaryTextTheme + .caption + .color), + ), + ) + ], + ); + }), + ), + Row( + mainAxisSize: MainAxisSize.max, + children: [ + Flexible( + child: Container( + padding: EdgeInsets.only(right: 8.0), + child: PrimaryButton( + onPressed: () => Share.text( + S.of(context).seed_share, + walletSeedViewModel.seed, + 'text/plain'), + text: S.of(context).save, + color: Colors.green, + textColor: Colors.white), + )), + Flexible( + child: Container( + padding: EdgeInsets.only(left: 8.0), + child: Builder( + builder: (context) => PrimaryButton( + onPressed: () { + Clipboard.setData(ClipboardData( + text: walletSeedViewModel.seed)); + Scaffold.of(context).showSnackBar( + SnackBar( + content: Text( + S.of(context).copied_to_clipboard), + backgroundColor: Colors.green, + duration: Duration(milliseconds: 1500), + ), + ); + }, + text: S.of(context).copy, + color: Colors.blue, + textColor: Colors.white)), + )) + ], + ) + ], + )) + ], + )); + } +} diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index f93d634d3..20c44ba5d 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -1,5 +1,8 @@ +import 'package:cake_wallet/core/address_validator.dart'; +import 'package:cake_wallet/core/amount_validator.dart'; import 'package:cake_wallet/src/screens/auth/auth_page.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/view_model/send_view_model.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -14,7 +17,8 @@ import 'package:cake_wallet/src/stores/settings/settings_store.dart'; import 'package:cake_wallet/src/stores/balance/balance_store.dart'; import 'package:cake_wallet/src/stores/wallet/wallet_store.dart'; import 'package:cake_wallet/src/stores/send/send_store.dart'; -import 'package:cake_wallet/src/stores/send/sending_state.dart'; + +//import 'package:cake_wallet/src/stores/send/sending_state.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/domain/common/crypto_currency.dart'; import 'package:cake_wallet/src/domain/common/calculate_estimated_fee.dart'; @@ -29,8 +33,13 @@ import 'package:cake_wallet/src/screens/send/widgets/confirm_sending_alert.dart' import 'package:cake_wallet/src/screens/send/widgets/sending_alert.dart'; import 'package:cake_wallet/src/widgets/template_tile.dart'; import 'package:cake_wallet/src/stores/send_template/send_template_store.dart'; +import 'package:cake_wallet/src/widgets/trail_button.dart'; class SendPage extends BasePage { + SendPage({@required this.sendViewModel}); + + final SendViewModel sendViewModel; + @override String get title => S.current.send_title; @@ -45,35 +54,20 @@ class SendPage extends BasePage { @override Widget trailing(context) { - final sendStore = Provider.of(context); +// final sendStore = Provider.of(context); - return Container( - height: 32, - width: 82, - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(16)), - color: Theme.of(context).accentTextTheme.title.color - ), - child: ButtonTheme( - minWidth: double.minPositive, - child: FlatButton( - onPressed: () => sendStore.clear(), - child: Text( - S.of(context).clear, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 10.0, - color: Colors.blue), - )), - ), - ); + return TrailButton(caption: S.of(context).clear, onPressed: () => null); } @override - Widget body(BuildContext context) => SendForm(); + Widget body(BuildContext context) => SendForm(sendViewModel: sendViewModel); } class SendForm extends StatefulWidget { + SendForm({this.sendViewModel}); + + final SendViewModel sendViewModel; + @override State createState() => SendFormState(); } @@ -92,7 +86,7 @@ class SendFormState extends State { @override void initState() { _focusNode.addListener(() { - if (!_focusNode.hasFocus &&_addressController.text.isNotEmpty) { + if (!_focusNode.hasFocus && _addressController.text.isNotEmpty) { getOpenaliasRecord(context); } }); @@ -102,7 +96,8 @@ class SendFormState extends State { Future getOpenaliasRecord(BuildContext context) async { final sendStore = Provider.of(context); - final isOpenalias = await sendStore.isOpenaliasRecord(_addressController.text); + final isOpenalias = + await sendStore.isOpenaliasRecord(_addressController.text); if (isOpenalias) { _addressController.text = sendStore.recordAddress; @@ -111,24 +106,24 @@ class SendFormState extends State { context: context, builder: (BuildContext context) { return AlertWithOneAction( - alertTitle: S.of(context).openalias_alert_title, - alertContent: S.of(context).openalias_alert_content(sendStore.recordName), - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop() - ); + alertTitle: S.of(context).openalias_alert_title, + alertContent: + S.of(context).openalias_alert_content(sendStore.recordName), + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); }); } } @override Widget build(BuildContext context) { - final settingsStore = Provider.of(context); - final sendStore = Provider.of(context); - sendStore.settingsStore = settingsStore; - final balanceStore = Provider.of(context); - final walletStore = Provider.of(context); - final syncStore = Provider.of(context); - final sendTemplateStore = Provider.of(context); +// final settingsStore = Provider.of(context); +// final sendStore = Provider.of(context); +// sendStore.settingsStore = settingsStore; +// final balanceStore = Provider.of(context); +// final walletStore = Provider.of(context); +// final syncStore = Provider.of(context); +// final sendTemplateStore = Provider.of(context); _setEffects(context); @@ -166,110 +161,116 @@ class SendFormState extends State { AddressTextFieldOption.addressBook ], buttonColor: Theme.of(context).accentTextTheme.title.color, - validator: (value) { - sendStore.validateAddress(value, - cryptoCurrency: CryptoCurrency.xmr); - return sendStore.errorMessage; - }, + validator: widget.sendViewModel.addressValidator, ), - Observer( - builder: (_) { - return Padding( - padding: const EdgeInsets.only(top: 20), - child: TextFormField( - style: TextStyle( - fontSize: 16.0, - color: Theme.of(context).primaryTextTheme.title.color - ), - controller: _cryptoAmountController, - keyboardType: TextInputType.numberWithOptions( - signed: false, decimal: true), - inputFormatters: [ - BlacklistingTextInputFormatter( - RegExp('[\\-|\\ |\\,]')) - ], - decoration: InputDecoration( - prefixIcon: Padding( - padding: EdgeInsets.only(top: 12), - child: Text('XMR:', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Theme.of(context).primaryTextTheme.title.color, - )), - ), - suffixIcon: Padding( - padding: EdgeInsets.only( - bottom: 5 - ), - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - width: MediaQuery.of(context).size.width/2, - alignment: Alignment.centerLeft, - child: Text( - ' / ' + balanceStore.unlockedBalance, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 16, - color: Theme.of(context).primaryTextTheme.caption.color - ) + Observer(builder: (_) { + return Padding( + padding: const EdgeInsets.only(top: 20), + child: TextFormField( + style: TextStyle( + fontSize: 16.0, + color: Theme.of(context) + .primaryTextTheme + .title + .color), + controller: _cryptoAmountController, + keyboardType: TextInputType.numberWithOptions( + signed: false, decimal: true), + inputFormatters: [ + BlacklistingTextInputFormatter( + RegExp('[\\-|\\ |\\,]')) + ], + decoration: InputDecoration( + prefixIcon: Padding( + padding: EdgeInsets.only(top: 12), + child: Text('XMR:', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context) + .primaryTextTheme + .title + .color, + )), + ), + suffixIcon: Padding( + padding: EdgeInsets.only(bottom: 5), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Container( + width: + MediaQuery.of(context).size.width / 2, + alignment: Alignment.centerLeft, + child: Text( + ' / ' + widget.sendViewModel.balance, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 16, + color: Theme.of(context) + .primaryTextTheme + .caption + .color)), + ), + Container( + height: 32, + width: 32, + margin: EdgeInsets.only( + left: 12, bottom: 7, top: 4), + decoration: BoxDecoration( + color: Theme.of(context) + .accentTextTheme + .title + .color, + borderRadius: BorderRadius.all( + Radius.circular(6))), + child: InkWell( + onTap: () => null, + // widget.sendViewModel, + child: Center( + child: Text(S.of(context).all, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 9, + fontWeight: FontWeight.bold, + color: Theme.of(context) + .primaryTextTheme + .caption + .color)), ), ), - Container( - height: 32, - width: 32, - margin: EdgeInsets.only(left: 12, bottom: 7, top: 4), - decoration: BoxDecoration( - color: Theme.of(context).accentTextTheme.title.color, - borderRadius: BorderRadius.all(Radius.circular(6)) - ), - child: InkWell( - onTap: () => sendStore.setSendAll(), - child: Center( - child: Text(S.of(context).all, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 9, - fontWeight: FontWeight.bold, - color: Theme.of(context).primaryTextTheme.caption.color - ) - ), - ), - ), - ) - ], - ), + ) + ], ), - hintStyle: TextStyle( - fontSize: 16.0, - color: Theme.of(context).primaryTextTheme.title.color), - hintText: '0.0000', - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).dividerColor, - width: 1.0)), - enabledBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).dividerColor, - width: 1.0))), - validator: (value) { - sendStore.validateXMR( - value, balanceStore.unlockedBalance); - return sendStore.errorMessage; - }), - ); - } - ), + ), + hintStyle: TextStyle( + fontSize: 16.0, + color: Theme.of(context) + .primaryTextTheme + .title + .color), + hintText: '0.0000', + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).dividerColor, + width: 1.0)), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).dividerColor, + width: 1.0))), + validator: widget.sendViewModel.amountValidator), + ); + }), Padding( padding: const EdgeInsets.only(top: 20), child: TextFormField( style: TextStyle( fontSize: 16.0, - color: Theme.of(context).primaryTextTheme.title.color), + color: + Theme.of(context).primaryTextTheme.title.color), controller: _fiatAmountController, keyboardType: TextInputType.numberWithOptions( signed: false, decimal: true), @@ -281,16 +282,22 @@ class SendFormState extends State { prefixIcon: Padding( padding: EdgeInsets.only(top: 12), child: Text( - '${settingsStore.fiatCurrency.toString()}:', + '${widget.sendViewModel.fiat.toString()}:', style: TextStyle( fontSize: 16, - fontWeight: FontWeight.bold, - color: Theme.of(context).primaryTextTheme.title.color, + fontWeight: FontWeight.w500, + color: Theme.of(context) + .primaryTextTheme + .title + .color, )), ), hintStyle: TextStyle( fontSize: 16.0, - color: Theme.of(context).primaryTextTheme.caption.color), + color: Theme.of(context) + .primaryTextTheme + .caption + .color), hintText: '0.00', focusedBorder: UnderlineInputBorder( borderSide: BorderSide( @@ -310,14 +317,20 @@ class SendFormState extends State { style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, - color: Theme.of(context).primaryTextTheme.title.color, + color: Theme.of(context) + .primaryTextTheme + .title + .color, )), Text( - '${calculateEstimatedFee(priority: settingsStore.transactionPriority)} XMR', + '${widget.sendViewModel.estimatedFee} ${widget.sendViewModel.currency.toString()}', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, - color: Theme.of(context).primaryTextTheme.title.color, + color: Theme.of(context) + .primaryTextTheme + .title + .color, )) ], ), @@ -325,143 +338,140 @@ class SendFormState extends State { ]), ), ), - Padding( - padding: EdgeInsets.only( - top: 32, - left: 24, - bottom: 24 - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text( - S.of(context).send_templates, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Theme.of(context).primaryTextTheme.caption.color - ), - ) - ], - ), - ), - Container( - height: 40, - width: double.infinity, - padding: EdgeInsets.only(left: 24), - child: Observer( - builder: (_) { - final itemCount = sendTemplateStore.templates.length + 1; - - return ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: itemCount, - itemBuilder: (context, index) { - - if (index == 0) { - return GestureDetector( - onTap: () => Navigator.of(context) - .pushNamed(Routes.sendTemplate), - child: Container( - padding: EdgeInsets.only(right: 10), - child: DottedBorder( - borderType: BorderType.RRect, - dashPattern: [8, 4], - color: Theme.of(context).accentTextTheme.title.backgroundColor, - strokeWidth: 2, - radius: Radius.circular(20), - child: Container( - height: 40, - width: 75, - padding: EdgeInsets.only(left: 10, right: 10), - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(20)), - color: Colors.transparent, - ), - child: Text( - S.of(context).send_new, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: Theme.of(context).primaryTextTheme.caption.color - ), - ), - ) - ), - ), - ); - } - - index -= 1; - - final template = sendTemplateStore.templates[index]; - - return TemplateTile( - to: template.name, - amount: template.amount, - from: template.cryptoCurrency, - onTap: () { - _addressController.text = template.address; - _cryptoAmountController.text = template.amount; - getOpenaliasRecord(context); - } - ); - } - ); - } - ), - ) +// Padding( +// padding: EdgeInsets.only(top: 32, left: 24, bottom: 24), +// child: Row( +// mainAxisAlignment: MainAxisAlignment.start, +// children: [ +// Text( +// S.of(context).send_templates, +// style: TextStyle( +// fontSize: 18, +// fontWeight: FontWeight.w600, +// color: +// Theme.of(context).primaryTextTheme.caption.color), +// ) +// ], +// ), +// ), +// Container( +// height: 40, +// width: double.infinity, +// padding: EdgeInsets.only(left: 24), +// child: Observer(builder: (_) { +// final itemCount = sendTemplateStore.templates.length + 1; +// +// return ListView.builder( +// scrollDirection: Axis.horizontal, +// itemCount: itemCount, +// itemBuilder: (context, index) { +// if (index == 0) { +// return GestureDetector( +// onTap: () => Navigator.of(context) +// .pushNamed(Routes.sendTemplate), +// child: Container( +// padding: EdgeInsets.only(right: 10), +// child: DottedBorder( +// borderType: BorderType.RRect, +// dashPattern: [8, 4], +// color: Theme.of(context) +// .accentTextTheme +// .title +// .backgroundColor, +// strokeWidth: 2, +// radius: Radius.circular(20), +// child: Container( +// height: 40, +// width: 75, +// padding: EdgeInsets.only(left: 10, right: 10), +// alignment: Alignment.center, +// decoration: BoxDecoration( +// borderRadius: +// BorderRadius.all(Radius.circular(20)), +// color: Colors.transparent, +// ), +// child: Text( +// S.of(context).send_new, +// style: TextStyle( +// fontSize: 14, +// fontWeight: FontWeight.w600, +// color: Theme.of(context) +// .primaryTextTheme +// .caption +// .color), +// ), +// )), +// ), +// ); +// } +// +// index -= 1; +// +// final template = sendTemplateStore.templates[index]; +// +// return TemplateTile( +// to: template.name, +// amount: template.amount, +// from: template.cryptoCurrency, +// onTap: () { +// _addressController.text = template.address; +// _cryptoAmountController.text = template.amount; +// getOpenaliasRecord(context); +// }); +// }); +// }), +// ) ], ), bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), bottomSection: Observer(builder: (_) { return LoadingPrimaryButton( - onPressed: syncStore.status is SyncedSyncStatus - ? () async { - // Hack. Don't ask me. - FocusScope.of(context).requestFocus(FocusNode()); - - if (_formKey.currentState.validate()) { - await showDialog( - context: context, - builder: (dialogContext) { - return AlertWithTwoActions( - alertTitle: S.of(context).send_creating_transaction, - alertContent: S.of(context).confirm_sending, - leftButtonText: S.of(context).send, - rightButtonText: S.of(context).cancel, - actionLeftButton: () async { - await Navigator.of(dialogContext) - .popAndPushNamed(Routes.auth, - arguments: (bool - isAuthenticatedSuccessfully, - AuthPageState auth) { - if (!isAuthenticatedSuccessfully) { - return; - } - - Navigator.of(auth.context).pop(); - - sendStore.createTransaction( - address: _addressController.text, - paymentId: ''); - }); - }, - actionRightButton: () => - Navigator.of(context).pop() - ); - }); - } - } - : null, + onPressed: () => null, +// syncStore.status is SyncedSyncStatus +// ? () async { +// // Hack. Don't ask me. +// FocusScope.of(context).requestFocus(FocusNode()); +// +// if (_formKey.currentState.validate()) { +// await showDialog( +// context: context, +// builder: (dialogContext) { +// return AlertWithTwoActions( +// alertTitle: +// S.of(context).send_creating_transaction, +// alertContent: S.of(context).confirm_sending, +// leftButtonText: S.of(context).send, +// rightButtonText: S.of(context).cancel, +// actionLeftButton: () async { +// await Navigator.of(dialogContext) +// .popAndPushNamed(Routes.auth, arguments: +// (bool isAuthenticatedSuccessfully, +// AuthPageState auth) { +// if (!isAuthenticatedSuccessfully) { +// return; +// } +// +// Navigator.of(auth.context).pop(); +// +// sendStore.createTransaction( +// address: _addressController.text, +// paymentId: ''); +// }); +// }, +// actionRightButton: () => +// Navigator.of(context).pop()); +// }); +// } +// } +// : null, text: S.of(context).send, color: Colors.blue, textColor: Colors.white, - isLoading: sendStore.state is CreatingTransaction || - sendStore.state is TransactionCommiting, - isDisabled: !(syncStore.status is SyncedSyncStatus), - ); + isLoading: widget.sendViewModel.state is TransactionIsCreating || + widget.sendViewModel.state is TransactionCommitting, + isDisabled: + false // FIXME !(syncStore.status is SyncedSyncStatus), + ); }), ), ); @@ -472,93 +482,88 @@ class SendFormState extends State { return; } - final sendStore = Provider.of(context); +// reaction((_) => widget.sendViewModel.fiatAmount, (String amount) { +// if (amount != _fiatAmountController.text) { +// _fiatAmountController.text = amount; +// } +// }); +// +// reaction((_) => widget.sendViewModel.cryptoAmount, (String amount) { +// if (amount != _cryptoAmountController.text) { +// _cryptoAmountController.text = amount; +// } +// }); +// +// reaction((_) => widget.sendViewModel.address, (String address) { +// if (address != _addressController.text) { +// _addressController.text = address; +// } +// }); +// +// _addressController.addListener(() { +// final address = _addressController.text; +// +// if (widget.sendViewModel.address != address) { +// widget.sendViewModel.changeAddress(address); +// } +// }); - reaction((_) => sendStore.fiatAmount, (String amount) { - if (amount != _fiatAmountController.text) { - _fiatAmountController.text = amount; - } - }); +// _fiatAmountController.addListener(() { +// final fiatAmount = _fiatAmountController.text; +// +// if (sendStore.fiatAmount != fiatAmount) { +// sendStore.changeFiatAmount(fiatAmount); +// } +// }); - reaction((_) => sendStore.cryptoAmount, (String amount) { - if (amount != _cryptoAmountController.text) { - _cryptoAmountController.text = amount; - } - }); +// _cryptoAmountController.addListener(() { +// final cryptoAmount = _cryptoAmountController.text; +// +// if (sendStore.cryptoAmount != cryptoAmount) { +// sendStore.changeCryptoAmount(cryptoAmount); +// } +// }); - reaction((_) => sendStore.address, (String address) { - if (address != _addressController.text) { - _addressController.text = address; - } - }); - - _addressController.addListener(() { - final address = _addressController.text; - - if (sendStore.address != address) { - sendStore.changeAddress(address); - } - }); - - _fiatAmountController.addListener(() { - final fiatAmount = _fiatAmountController.text; - - if (sendStore.fiatAmount != fiatAmount) { - sendStore.changeFiatAmount(fiatAmount); - } - }); - - _cryptoAmountController.addListener(() { - final cryptoAmount = _cryptoAmountController.text; - - if (sendStore.cryptoAmount != cryptoAmount) { - sendStore.changeCryptoAmount(cryptoAmount); - } - }); - - reaction((_) => sendStore.state, (SendingState state) { + reaction((_) => widget.sendViewModel.state, (SendViewModelState state) { if (state is SendingFailed) { WidgetsBinding.instance.addPostFrameCallback((_) { showDialog( context: context, builder: (BuildContext context) { return AlertWithOneAction( - alertTitle: S.of(context).error, - alertContent: state.error, - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop() - ); + alertTitle: S.of(context).error, + alertContent: state.error, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); }); }); } if (state is TransactionCreatedSuccessfully) { - WidgetsBinding.instance.addPostFrameCallback((_) { - showDialog( - context: context, - builder: (BuildContext context) { - return ConfirmSendingAlert( - alertTitle: S.of(context).confirm_sending, - amount: S.of(context).send_amount, - amountValue: sendStore.pendingTransaction.amount, - fee: S.of(context).send_fee, - feeValue: sendStore.pendingTransaction.fee, - leftButtonText: S.of(context).ok, - rightButtonText: S.of(context).cancel, - actionLeftButton: () { - Navigator.of(context).pop(); - sendStore.commitTransaction(); - showDialog( - context: context, - builder: (BuildContext context) { - return SendingAlert(sendStore: sendStore); - } - ); - }, - actionRightButton: () => Navigator.of(context).pop() - ); - }); - }); +// WidgetsBinding.instance.addPostFrameCallback((_) { +// showDialog( +// context: context, +// builder: (BuildContext context) { +// return ConfirmSendingAlert( +// alertTitle: S.of(context).confirm_sending, +// amount: S.of(context).send_amount, +// amountValue: sendStore.pendingTransaction.amount, +// fee: S.of(context).send_fee, +// feeValue: sendStore.pendingTransaction.fee, +// leftButtonText: S.of(context).ok, +// rightButtonText: S.of(context).cancel, +// actionLeftButton: () { +// Navigator.of(context).pop(); +// sendStore.commitTransaction(); +// showDialog( +// context: context, +// builder: (BuildContext context) { +// return SendingAlert(sendStore: sendStore); +// }); +// }, +// actionRightButton: () => Navigator.of(context).pop()); +// }); +// }); } if (state is TransactionCommitted) { diff --git a/lib/src/screens/send/widgets/confirm_sending_alert.dart b/lib/src/screens/send/widgets/confirm_sending_alert.dart index 373180863..663de3a16 100644 --- a/lib/src/screens/send/widgets/confirm_sending_alert.dart +++ b/lib/src/screens/send/widgets/confirm_sending_alert.dart @@ -55,7 +55,7 @@ class ConfirmSendingAlert extends BaseAlertDialog { style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, - color: Colors.white, + color: Theme.of(context).primaryTextTheme.title.color, decoration: TextDecoration.none, ), ), @@ -64,7 +64,7 @@ class ConfirmSendingAlert extends BaseAlertDialog { style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, - color: Colors.white, + color: Theme.of(context).primaryTextTheme.title.color, decoration: TextDecoration.none, ), ) @@ -79,7 +79,7 @@ class ConfirmSendingAlert extends BaseAlertDialog { style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, - color: Colors.white, + color: Theme.of(context).primaryTextTheme.title.color, decoration: TextDecoration.none, ), ), @@ -88,7 +88,7 @@ class ConfirmSendingAlert extends BaseAlertDialog { style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, - color: Colors.white, + color: Theme.of(context).primaryTextTheme.title.color, decoration: TextDecoration.none, ), ) diff --git a/lib/src/screens/settings/settings.dart b/lib/src/screens/settings/settings.dart index 75d01a7b6..c1f4cfabe 100644 --- a/lib/src/screens/settings/settings.dart +++ b/lib/src/screens/settings/settings.dart @@ -1,438 +1,72 @@ -import 'package:cake_wallet/src/screens/auth/auth_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; -import 'package:url_launcher/url_launcher.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:provider/provider.dart'; -import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/domain/common/balance_display_mode.dart'; -import 'package:cake_wallet/src/domain/common/fiat_currency.dart'; -import 'package:cake_wallet/src/domain/common/transaction_priority.dart'; -import 'package:cake_wallet/src/stores/settings/settings_store.dart'; -import 'package:cake_wallet/src/stores/action_list/action_list_display_mode.dart'; +import 'package:cake_wallet/view_model/settings/settings_view_model.dart'; +import 'package:cake_wallet/view_model/settings/link_list_item.dart'; +import 'package:cake_wallet/view_model/settings/picker_list_item.dart'; +import 'package:cake_wallet/view_model/settings/regular_list_item.dart'; +import 'package:cake_wallet/view_model/settings/switcher_list_item.dart'; +import 'package:cake_wallet/src/screens/settings/widgets/settings_link_provider_cell.dart'; +import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arrow.dart'; +import 'package:cake_wallet/src/screens/settings/widgets/settings_picker_cell.dart'; +import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart'; +import 'package:cake_wallet/src/widgets/standard_list.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/screens/settings/attributes.dart'; -import 'package:cake_wallet/src/screens/disclaimer/disclaimer_page.dart'; -import 'package:cake_wallet/src/screens/settings/items/settings_item.dart'; -import 'package:cake_wallet/src/screens/settings/items/item_headers.dart'; -import 'package:cake_wallet/src/widgets/picker.dart'; -// Settings widgets -import 'package:cake_wallet/src/screens/settings/widgets/settings_arrow_list_row.dart'; -import 'package:cake_wallet/src/screens/settings/widgets/settings_header_list_row.dart'; -import 'package:cake_wallet/src/screens/settings/widgets/settings_link_list_row.dart'; -import 'package:cake_wallet/src/screens/settings/widgets/settings_switch_list_row.dart'; -import 'package:cake_wallet/src/screens/settings/widgets/settings_text_list_row.dart'; -import 'package:cake_wallet/src/screens/settings/widgets/settings_raw_widget_list_row.dart'; class SettingsPage extends BasePage { + SettingsPage(this.settingsViewModel); + + final SettingsViewModel settingsViewModel; + @override String get title => S.current.settings_title; @override Widget body(BuildContext context) { - return SettingsForm(); - } -} - -class SettingsForm extends StatefulWidget { - @override - SettingsFormState createState() => SettingsFormState(); -} - -class SettingsFormState extends State { - final _telegramImage = Image.asset('assets/images/Telegram.png'); - final _twitterImage = Image.asset('assets/images/Twitter.png'); - final _changeNowImage = Image.asset('assets/images/change_now.png'); - final _xmrBtcImage = Image.asset('assets/images/xmr_btc.png'); - final _morphImage = Image.asset('assets/images/morph_icon.png'); - - final _emailUrl = 'mailto:support@cakewallet.com'; - final _telegramUrl = 'https:t.me/cakewallet_bot'; - final _twitterUrl = 'https:twitter.com/CakewalletXMR'; - final _changeNowUrl = 'mailto:support@changenow.io'; - final _xmrToUrl = 'mailto:support@xmr.to'; - final _morphUrl = 'mailto:support@morphtoken.com'; - - final _items = List(); - - void _launchUrl(String url) async { - if (await canLaunch(url)) await launch(url); - } - - void _setSettingsList() { - final settingsStore = Provider.of(context); - - settingsStore.setItemHeaders(); - - _items.addAll([ - SettingsItem( - onTaped: () => _setBalance(context), - title: ItemHeaders.displayBalanceAs, - widget: Observer( - builder: (_) => Text( - settingsStore.balanceDisplayMode.toString(), - textAlign: TextAlign.right, - style: TextStyle( - fontSize: 14.0, - color: Theme.of(context).primaryTextTheme.caption.color), - )), - attribute: Attributes.widget), - SettingsItem( - onTaped: () => _setCurrency(context), - title: ItemHeaders.currency, - widget: Observer( - builder: (_) => Text( - settingsStore.fiatCurrency.toString(), - textAlign: TextAlign.right, - style: TextStyle( - fontSize: 14.0, - color: Theme.of(context).primaryTextTheme.caption.color), - )), - attribute: Attributes.widget), - SettingsItem( - onTaped: () => _setTransactionPriority(context), - title: ItemHeaders.feePriority, - widget: Observer( - builder: (_) => Text( - settingsStore.transactionPriority.toString(), - textAlign: TextAlign.right, - style: TextStyle( - fontSize: 14.0, - color: Theme.of(context).primaryTextTheme.caption.color), - )), - attribute: Attributes.widget), - SettingsItem( - title: ItemHeaders.saveRecipientAddress, - attribute: Attributes.switcher), - SettingsItem(title: '', attribute: Attributes.header), - SettingsItem( - onTaped: () { - Navigator.of(context).pushNamed(Routes.auth, - arguments: (bool isAuthenticatedSuccessfully, - AuthPageState auth) => - isAuthenticatedSuccessfully - ? Navigator.of(context).popAndPushNamed(Routes.setupPin, - arguments: - (BuildContext setupPinContext, String _) => - Navigator.of(context).pop()) - : null); - }, - title: ItemHeaders.changePIN, - attribute: Attributes.arrow), - SettingsItem( - onTaped: () => Navigator.pushNamed(context, Routes.changeLanguage), - title: ItemHeaders.changeLanguage, - attribute: Attributes.arrow), - SettingsItem( - title: ItemHeaders.allowBiometricalAuthentication, - attribute: Attributes.switcher), - SettingsItem(title: ItemHeaders.darkMode, attribute: Attributes.switcher), - SettingsItem( - widgetBuilder: (context) { - return PopupMenuButton( - itemBuilder: (context) => [ - PopupMenuItem( - value: ActionListDisplayMode.transactions, - child: Observer( - builder: (_) => Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text(S - .of(context) - .settings_transactions, - style: TextStyle( - color: Theme.of(context).primaryTextTheme.title.color - ), - ), - Checkbox( - value: settingsStore - .actionlistDisplayMode - .contains(ActionListDisplayMode - .transactions), - onChanged: (value) => settingsStore - .toggleTransactionsDisplay(), - ) - ]))), - PopupMenuItem( - value: ActionListDisplayMode.trades, - child: Observer( - builder: (_) => Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - S.of(context).settings_trades, - style: TextStyle( - color: Theme.of(context).primaryTextTheme.title.color - ), - ), - Checkbox( - value: settingsStore - .actionlistDisplayMode - .contains( - ActionListDisplayMode.trades), - onChanged: (value) => settingsStore - .toggleTradesDisplay(), - ) - ]))) - ], - child: Container( - height: 56, - padding: EdgeInsets.only(left: 24, right: 24), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(S.of(context).settings_display_on_dashboard_list, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).primaryTextTheme.title.color)), - Observer(builder: (_) { - var title = ''; - - if (settingsStore.actionlistDisplayMode.length == - ActionListDisplayMode.values.length) { - title = S.of(context).settings_all; - } - - if (title.isEmpty && - settingsStore.actionlistDisplayMode - .contains(ActionListDisplayMode.trades)) { - title = S.of(context).settings_only_trades; - } - - if (title.isEmpty && - settingsStore.actionlistDisplayMode.contains( - ActionListDisplayMode.transactions)) { - title = S.of(context).settings_only_transactions; - } - - if (title.isEmpty) { - title = S.of(context).settings_none; - } - - return Text(title, - style: TextStyle( - fontSize: 14.0, - color: Theme.of(context).primaryTextTheme.caption.color)); - }) - ]), - )); - }, - attribute: Attributes.rawWidget), - SettingsItem(title: '', attribute: Attributes.header), - SettingsItem( - onTaped: () => _launchUrl(_emailUrl), - title: 'Email', - link: 'support@cakewallet.com', - image: null, - attribute: Attributes.link), - SettingsItem( - onTaped: () => _launchUrl(_telegramUrl), - title: 'Telegram', - link: 'Cake_Wallet', - image: _telegramImage, - attribute: Attributes.link), - SettingsItem( - onTaped: () => _launchUrl(_twitterUrl), - title: 'Twitter', - link: '@CakeWalletXMR', - image: _twitterImage, - attribute: Attributes.link), - SettingsItem( - onTaped: () => _launchUrl(_changeNowUrl), - title: 'ChangeNow', - link: 'support@changenow.io', - image: _changeNowImage, - attribute: Attributes.link), - SettingsItem( - onTaped: () => _launchUrl(_morphUrl), - title: 'Morph', - link: 'support@morphtoken.com', - image: _morphImage, - attribute: Attributes.link), - SettingsItem( - onTaped: () => _launchUrl(_xmrToUrl), - title: 'XMR.to', - link: 'support@xmr.to', - image: _xmrBtcImage, - attribute: Attributes.link), - SettingsItem( - onTaped: () { - Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) => DisclaimerPage())); - }, - title: ItemHeaders.termsAndConditions, - attribute: Attributes.arrow), - SettingsItem( - onTaped: () => Navigator.pushNamed(context, Routes.faq), - title: ItemHeaders.faq, - attribute: Attributes.arrow) - ]); - setState(() {}); - } - - void _afterLayout(dynamic _) => _setSettingsList(); - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback(_afterLayout); - } - - Widget _getWidget(SettingsItem item) { - switch (item.attribute) { - case Attributes.arrow: - return SettingsArrowListRow( - onTaped: item.onTaped, - title: item.title, - ); - case Attributes.header: - return SettingsHeaderListRow( - title: item.title, - ); - case Attributes.link: - return SettingsLinktListRow( - onTaped: item.onTaped, - title: item.title, - link: item.link, - image: item.image, - ); - case Attributes.switcher: - return SettingsSwitchListRow( - title: item.title, - ); - case Attributes.widget: - return SettingsTextListRow( - onTaped: item.onTaped, - title: item.title, - widget: item.widget, - ); - case Attributes.rawWidget: - return SettingRawWidgetListRow(widgetBuilder: item.widgetBuilder); - default: - return Offstage(); - } - } - - @override - Widget build(BuildContext context) { - final settingsStore = Provider.of(context); - settingsStore.setItemHeaders(); - - final shortDivider = Container( - height: 1, - padding: EdgeInsets.only(left: 24), - color: Theme.of(context).accentTextTheme.title.backgroundColor, - child: Container( - height: 1, - color: Theme.of(context).dividerColor, - ), - ); - - final longDivider = Container( - height: 1, - color: Theme.of(context).dividerColor, - ); - - return Container( - padding: EdgeInsets.only(top: 12), - child: SingleChildScrollView( - child: Column( - children: [ - longDivider, - ListView.builder( - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: _items.length, - itemBuilder: (context, index) { - final item = _items[index]; - - Widget divider; - - if (item.attribute == Attributes.header || item == _items.last) { - divider = longDivider; - } else if (_items[index + 1].attribute == Attributes.header){ - divider = longDivider; - } else { - divider = shortDivider; - } - - return Column( - children: [ - _getWidget(item), - divider - ], - ); - }), - Padding( - padding: EdgeInsets.only(top: 12), - child: ListTile( - title: Center( - child: Text( - settingsStore.itemHeaders[ItemHeaders.version], - style: TextStyle( - fontSize: 14.0, - color: Theme.of(context).primaryTextTheme.caption.color) - ), - ), - ), - ) - ], - )), - ); - } - - Future _setBalance(BuildContext context) async { - final settingsStore = Provider.of(context); - final items = BalanceDisplayMode.all; - final selectedItem = items.indexOf(settingsStore.balanceDisplayMode); - - await showDialog( - builder: (_) => Picker( - items: items, - selectedAtIndex: selectedItem, - title: S.of(context).please_select, - mainAxisAlignment: MainAxisAlignment.center, - onItemSelected: (BalanceDisplayMode mode) async => - await settingsStore.setCurrentBalanceDisplayMode( - balanceDisplayMode: mode)), - context: context); - } - - Future _setCurrency(BuildContext context) async { - final settingsStore = Provider.of(context); - final items = FiatCurrency.all; - final selectedItem = items.indexOf(settingsStore.fiatCurrency); - - await showDialog( - builder: (_) => Picker( - items: items, - selectedAtIndex: selectedItem, - title: S.of(context).please_select, - mainAxisAlignment: MainAxisAlignment.center, - onItemSelected: (FiatCurrency currency) async => - await settingsStore.setCurrentFiatCurrency(currency: currency)), - context: context); - } - - Future _setTransactionPriority(BuildContext context) async { - final settingsStore = Provider.of(context); - final items = TransactionPriority.all; - final selectedItem = items.indexOf(settingsStore.transactionPriority); - - await showDialog( - builder: (_) => Picker( - items: items, - selectedAtIndex: selectedItem, - title: S.of(context).please_select, - mainAxisAlignment: MainAxisAlignment.center, - onItemSelected: (TransactionPriority priority) async => - await settingsStore.setCurrentTransactionPriority( - priority: priority)), - context: context); + return SectionStandardList( + sectionCount: settingsViewModel.sections.length, + itemCounter: (int sectionIndex) { + if (sectionIndex < settingsViewModel.sections.length) { + return settingsViewModel.sections[sectionIndex].length; + } + + return 0; + }, + itemBuilder: (_, sectionIndex, itemIndex) { + final item = settingsViewModel.sections[sectionIndex][itemIndex]; + + if (item is PickerListItem) { + return Observer(builder: (_) { + return SettingsPickerCell( + title: item.title, + selectedItem: item.selectedItem(), + items: item.items); + }); + } + + if (item is SwitcherListItem) { + return Observer(builder: (_) { + return SettingsSwitcherCell( + title: item.title, + value: item.value(), + onValueChange: item.onValueChange); + }); + } + + if (item is RegularListItem) { + return SettingsCellWithArrow(title: item.title); + } + + if (item is LinkListItem) { + return SettingsLinkProviderCell( + title: item.title, + icon: item.icon, + link: item.link, + linkTitle: item.linkTitle); + } + + return Container(); + }); } } diff --git a/lib/src/screens/settings/widgets/settings_arrow_list_row.dart b/lib/src/screens/settings/widgets/settings_arrow_list_row.dart deleted file mode 100644 index cf49a4370..000000000 --- a/lib/src/screens/settings/widgets/settings_arrow_list_row.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:provider/provider.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:cake_wallet/src/stores/settings/settings_store.dart'; - -class SettingsArrowListRow extends StatelessWidget { - SettingsArrowListRow({@required this.onTaped, this.title}); - - final VoidCallback onTaped; - final String title; - - @override - Widget build(BuildContext context) { - final settingsStore = Provider.of(context); - final _cakeArrowImage = Image.asset('assets/images/select_arrow.png', - color: Theme.of(context).primaryTextTheme.caption.color); - - return Container( - color: Theme.of(context).accentTextTheme.title.backgroundColor, - child: ListTile( - contentPadding: EdgeInsets.only(left: 24.0, right: 24.0), - title: Observer( - builder: (_) => Text( - settingsStore.itemHeaders[title], - style: TextStyle( - fontSize: 14.0, - color: Theme.of(context).primaryTextTheme.title.color), - )), - trailing: _cakeArrowImage, - onTap: onTaped), - ); - } -} diff --git a/lib/src/screens/settings/widgets/settings_cell_with_arrow.dart b/lib/src/screens/settings/widgets/settings_cell_with_arrow.dart new file mode 100644 index 000000000..6e55aa5ee --- /dev/null +++ b/lib/src/screens/settings/widgets/settings_cell_with_arrow.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; +import 'package:cake_wallet/src/widgets/standard_list.dart'; + +class SettingsCellWithArrow extends StandardListRow { + SettingsCellWithArrow({@required String title}) + : super(title: title, isSelected: false); + + @override + Widget buildTrailing(BuildContext context) => + Image.asset('assets/images/select_arrow.png', + color: Theme.of(context).primaryTextTheme.caption.color); +} \ No newline at end of file diff --git a/lib/src/screens/settings/widgets/settings_header_list_row.dart b/lib/src/screens/settings/widgets/settings_header_list_row.dart deleted file mode 100644 index beae21431..000000000 --- a/lib/src/screens/settings/widgets/settings_header_list_row.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:cake_wallet/src/stores/settings/settings_store.dart'; -import 'package:provider/provider.dart'; - -class SettingsHeaderListRow extends StatelessWidget { - SettingsHeaderListRow({this.title}); - - final String title; - - @override - Widget build(BuildContext context) { - final settingsStore = Provider.of(context); - - return Column( - children: [ - SizedBox( - height: 20.0, - ), - Container( - padding: EdgeInsets.only(left: 20.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Observer( - builder: (_) => Text( - title.isNotEmpty - ? settingsStore.itemHeaders[title] - : '', - style: TextStyle( - fontSize: 15.0, - color: Theme.of(context).primaryTextTheme.caption.color), - )) - ], - ), - ), - SizedBox( - height: 14.0, - ), - ], - ); - } -} diff --git a/lib/src/screens/settings/widgets/settings_link_list_row.dart b/lib/src/screens/settings/widgets/settings_link_list_row.dart deleted file mode 100644 index c0d632161..000000000 --- a/lib/src/screens/settings/widgets/settings_link_list_row.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:flutter/material.dart'; - -class SettingsLinktListRow extends StatelessWidget { - SettingsLinktListRow( - {@required this.onTaped, this.title, this.link, this.image}); - - final VoidCallback onTaped; - final String title; - final String link; - final Image image; - - @override - Widget build(BuildContext context) { - return Container( - color: Theme.of(context).accentTextTheme.title.backgroundColor, - child: ListTile( - contentPadding: EdgeInsets.only(left: 24.0, right: 24.0), - title: Row( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - image != null ? image : Offstage(), - Container( - padding: image != null ? EdgeInsets.only(left: 10) : null, - child: Text( - title, - style: TextStyle( - fontSize: 14.0, - color: Theme.of(context).primaryTextTheme.title.color), - ), - ) - ], - ), - trailing: Text( - link, - style: TextStyle(fontSize: 14.0, color: Colors.blue), - ), - onTap: onTaped, - ), - ); - } -} diff --git a/lib/src/screens/settings/widgets/settings_link_provider_cell.dart b/lib/src/screens/settings/widgets/settings_link_provider_cell.dart new file mode 100644 index 000000000..3babf3097 --- /dev/null +++ b/lib/src/screens/settings/widgets/settings_link_provider_cell.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:cake_wallet/src/widgets/standard_list.dart'; + +class SettingsLinkProviderCell extends StandardListRow { + SettingsLinkProviderCell( + {@required String title, + @required this.icon, + @required this.link, + @required this.linkTitle}) + : super(title: title, isSelected: false); + + final String icon; + final String link; + final String linkTitle; + + @override + Widget buildLeading(BuildContext context) => + icon != null ? Image.asset(icon) : null; + + @override + Widget buildTrailing(BuildContext context) => Text(linkTitle, + style: TextStyle( + fontSize: 14.0, fontWeight: FontWeight.w500, color: Colors.blue)); +} \ No newline at end of file diff --git a/lib/src/screens/settings/widgets/settings_picker_cell.dart b/lib/src/screens/settings/widgets/settings_picker_cell.dart new file mode 100644 index 000000000..a94bdfe0d --- /dev/null +++ b/lib/src/screens/settings/widgets/settings_picker_cell.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:cake_wallet/src/widgets/picker.dart'; +import 'package:cake_wallet/src/widgets/standard_list.dart'; +import 'package:cake_wallet/generated/i18n.dart'; + +class SettingsPickerCell extends StandardListRow { + SettingsPickerCell({@required String title, this.selectedItem, this.items}) + : super( + title: title, + isSelected: false, + onTap: (BuildContext context) async { + final selectedAtIndex = items.indexOf(selectedItem); + + await showDialog( + context: context, + builder: (_) => Picker( + items: items, + selectedAtIndex: selectedAtIndex, + title: S.current.please_select, + mainAxisAlignment: MainAxisAlignment.center, + onItemSelected: (Object _) {})); + }); + + final ItemType selectedItem; + final List items; + + @override + Widget buildTrailing(BuildContext context) { + return Text( + selectedItem.toString(), + textAlign: TextAlign.right, + style: TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.w500, + color: Theme.of(context).primaryTextTheme.caption.color), + ); + } +} \ No newline at end of file diff --git a/lib/src/screens/settings/widgets/settings_picker_row.dart b/lib/src/screens/settings/widgets/settings_picker_row.dart new file mode 100644 index 000000000..462dedb83 --- /dev/null +++ b/lib/src/screens/settings/widgets/settings_picker_row.dart @@ -0,0 +1,9 @@ +import 'package:flutter/cupertino.dart'; + +class SettingsPickerRaw extends StatelessWidget { + @override + Widget build(BuildContext context) { + // TODO: implement build + throw UnimplementedError(); + } +} \ No newline at end of file diff --git a/lib/src/screens/settings/widgets/settings_raw_widget_list_row.dart b/lib/src/screens/settings/widgets/settings_raw_widget_list_row.dart deleted file mode 100644 index 01c3f77d1..000000000 --- a/lib/src/screens/settings/widgets/settings_raw_widget_list_row.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:flutter/material.dart'; - -class SettingRawWidgetListRow extends StatelessWidget { - SettingRawWidgetListRow({@required this.widgetBuilder}); - - final WidgetBuilder widgetBuilder; - - @override - Widget build(BuildContext context) { - return Container( - color: Theme.of(context).accentTextTheme.title.backgroundColor, - child: widgetBuilder(context) ?? Container(), - ); - } -} diff --git a/lib/src/screens/settings/widgets/settings_switch_list_row.dart b/lib/src/screens/settings/widgets/settings_switch_list_row.dart deleted file mode 100644 index 36abcc163..000000000 --- a/lib/src/screens/settings/widgets/settings_switch_list_row.dart +++ /dev/null @@ -1,76 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/theme_changer.dart'; -import 'package:cake_wallet/themes.dart'; -import 'package:cake_wallet/src/stores/settings/settings_store.dart'; -import 'package:cake_wallet/src/widgets/standart_switch.dart'; - -class SettingsSwitchListRow extends StatelessWidget { - SettingsSwitchListRow({@required this.title}); - - final String title; - - Widget _getSwitch(BuildContext context) { - final settingsStore = Provider.of(context); - final _themeChanger = Provider.of(context); - - if (settingsStore.itemHeaders[title] == - S.of(context).settings_save_recipient_address) { - return Observer( - builder: (_) => StandartSwitch( - value: settingsStore.shouldSaveRecipientAddress, - onTaped: () { - final _currentValue = !settingsStore.shouldSaveRecipientAddress; - settingsStore.setSaveRecipientAddress( - shouldSaveRecipientAddress: _currentValue); - })); - } - - if (settingsStore.itemHeaders[title] == - S.of(context).settings_allow_biometrical_authentication) { - return Observer( - builder: (_) => StandartSwitch( - value: settingsStore.allowBiometricalAuthentication, - onTaped: () { - final _currentValue = - !settingsStore.allowBiometricalAuthentication; - settingsStore.setAllowBiometricalAuthentication( - allowBiometricalAuthentication: _currentValue); - })); - } - - if (settingsStore.itemHeaders[title] == S.of(context).settings_dark_mode) { - return Observer( - builder: (_) => StandartSwitch( - value: settingsStore.isDarkTheme, - onTaped: () { - final _currentValue = !settingsStore.isDarkTheme; - settingsStore.saveDarkTheme(isDarkTheme: _currentValue); - _themeChanger.setTheme( - _currentValue ? Themes.darkTheme : Themes.lightTheme); - })); - } - - return null; - } - - @override - Widget build(BuildContext context) { - final settingsStore = Provider.of(context); - - return Container( - color: Theme.of(context).accentTextTheme.title.backgroundColor, - child: ListTile( - contentPadding: EdgeInsets.only(left: 24.0, right: 24.0), - title: Observer( - builder: (_) => Text(settingsStore.itemHeaders[title], - style: TextStyle( - fontSize: 14, - color: Theme.of(context).primaryTextTheme.title.color)), - ), - trailing: _getSwitch(context)), - ); - } -} diff --git a/lib/src/screens/settings/widgets/settings_switcher_cell.dart b/lib/src/screens/settings/widgets/settings_switcher_cell.dart new file mode 100644 index 000000000..db42274e0 --- /dev/null +++ b/lib/src/screens/settings/widgets/settings_switcher_cell.dart @@ -0,0 +1,16 @@ +import 'package:flutter/cupertino.dart'; +import 'package:cake_wallet/src/widgets/standard_list.dart'; +import 'package:cake_wallet/src/widgets/standart_switch.dart'; + +class SettingsSwitcherCell extends StandardListRow { + SettingsSwitcherCell( + {@required String title, @required this.value, this.onValueChange}) + : super(title: title, isSelected: false); + + final bool value; + final void Function(bool value) onValueChange; + + @override + Widget buildTrailing(BuildContext context) => + StandartSwitch(value: value, onTaped: () => onValueChange(!value)); +} diff --git a/lib/src/screens/settings/widgets/settings_text_list_row.dart b/lib/src/screens/settings/widgets/settings_text_list_row.dart deleted file mode 100644 index 1115c68d6..000000000 --- a/lib/src/screens/settings/widgets/settings_text_list_row.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:provider/provider.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:cake_wallet/src/stores/settings/settings_store.dart'; - -class SettingsTextListRow extends StatelessWidget { - SettingsTextListRow({@required this.onTaped, this.title, this.widget}); - - final VoidCallback onTaped; - final String title; - final Widget widget; - - @override - Widget build(BuildContext context) { - final settingsStore = Provider.of(context); - - return Container( - color: Theme.of(context).accentTextTheme.title.backgroundColor, - child: ListTile( - contentPadding: EdgeInsets.only(left: 24.0, right: 24.0), - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Observer( - builder: (_) => Text( - settingsStore.itemHeaders[title], - style: TextStyle( - fontSize: 14.0, - color: Theme.of(context).primaryTextTheme.title.color), - )), - ), - Flexible( - child: widget - ) - ], - ), - onTap: onTaped, - ), - ); - } -} diff --git a/lib/src/screens/show_keys/show_keys_page.dart b/lib/src/screens/show_keys/show_keys_page.dart deleted file mode 100644 index 2f5e529c9..000000000 --- a/lib/src/screens/show_keys/show_keys_page.dart +++ /dev/null @@ -1,80 +0,0 @@ -import 'package:provider/provider.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/stores/wallet/wallet_keys_store.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/widgets/standart_list_row.dart'; - -class ShowKeysPage extends BasePage { - @override - String get title => S.current.wallet_keys; - - @override - Widget body(BuildContext context) { - final walletKeysStore = Provider.of(context); - - return Container( - padding: EdgeInsets.only(top: 20.0, bottom: 20.0), - child: Observer( - builder: (_) { - final keysMap = { - S.of(context).view_key_public: walletKeysStore.publicViewKey, - S.of(context).spend_key_private: walletKeysStore.privateSpendKey - }; - - if (walletKeysStore.privateViewKey.isNotEmpty) { - keysMap[S.of(context).view_key_private] = - walletKeysStore.privateViewKey; - } - - if (walletKeysStore.publicSpendKey.isNotEmpty) { - keysMap[S.of(context).spend_key_public] = - walletKeysStore.publicSpendKey; - } - - return ListView.separated( - separatorBuilder: (context, index) => Container( - height: 1, - padding: EdgeInsets.only(left: 24), - color: Theme.of(context).accentTextTheme.title.backgroundColor, - child: Container( - height: 1, - color: Theme.of(context).dividerColor, - ), - ), - itemCount: keysMap.length, - itemBuilder: (BuildContext context, int index) { - final key = keysMap.keys.elementAt(index); - final value = keysMap.values.elementAt(index); - - final isDrawTop = index == 0 ? true : false; - final isDrawBottom = index == keysMap.length - 1 ? true : false; - - return GestureDetector( - onTap: () { - Clipboard.setData(ClipboardData(text: value)); - Scaffold.of(context).showSnackBar(SnackBar( - content: Text( - S.of(context).copied_key_to_clipboard(key), - textAlign: TextAlign.center, - style: TextStyle(color: Colors.white), - ), - backgroundColor: Colors.green, - duration: Duration(seconds: 1), - )); - }, - child: StandartListRow( - title: key + ':', - value: value, - isDrawTop: isDrawTop, - isDrawBottom: isDrawBottom, - ), - ); - }); - }, - )); - } -} diff --git a/lib/src/screens/subaddress/address_edit_or_create_page.dart b/lib/src/screens/subaddress/address_edit_or_create_page.dart index e242ddcec..84c492196 100644 --- a/lib/src/screens/subaddress/address_edit_or_create_page.dart +++ b/lib/src/screens/subaddress/address_edit_or_create_page.dart @@ -3,8 +3,8 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/view_model/address_list/address_edit_or_create_view_model.dart'; -import 'package:cake_wallet/core/AddressLabelValidator.dart'; +import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart'; +import 'package:cake_wallet/core/address_label_validator.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; @@ -21,7 +21,7 @@ class AddressEditOrCreatePage extends BasePage { print(addressEditOrCreateViewModel.label); } - final AddressEditOrCreateViewModel addressEditOrCreateViewModel; + final WalletAddressEditOrCreateViewModel addressEditOrCreateViewModel; final GlobalKey _formKey; final TextEditingController _labelController; diff --git a/lib/src/screens/transaction_details/transaction_details_page.dart b/lib/src/screens/transaction_details/transaction_details_page.dart index 38600cee7..4351089ed 100644 --- a/lib/src/screens/transaction_details/transaction_details_page.dart +++ b/lib/src/screens/transaction_details/transaction_details_page.dart @@ -1,52 +1,21 @@ -import 'package:cake_wallet/src/domain/monero/monero_transaction_info.dart'; -import 'package:provider/provider.dart'; +import 'package:intl/intl.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart'; +import 'package:cake_wallet/src/domain/monero/monero_transaction_info.dart'; import 'package:cake_wallet/src/domain/common/transaction_info.dart'; -import 'package:cake_wallet/src/stores/settings/settings_store.dart'; -import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart'; import 'package:cake_wallet/src/widgets/standart_list_row.dart'; +import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/palette.dart'; class TransactionDetailsPage extends BasePage { - TransactionDetailsPage({this.transactionInfo}); - - final TransactionInfo transactionInfo; - - @override - String get title => S.current.transaction_details_title; - - @override - Widget body(BuildContext context) { - final settingsStore = Provider.of(context); - - return TransactionDetailsForm( - transactionInfo: transactionInfo, settingsStore: settingsStore); - } -} - -class TransactionDetailsForm extends StatefulWidget { - TransactionDetailsForm( - {@required this.transactionInfo, @required this.settingsStore}); - - final TransactionInfo transactionInfo; - final SettingsStore settingsStore; - - @override - TransactionDetailsFormState createState() => TransactionDetailsFormState(); -} - -class TransactionDetailsFormState extends State { - final _items = List(); - - @override - void initState() { - final _dateFormat = widget.settingsStore.getCurrentDateFormat( - formatUSA: "yyyy.MM.dd, HH:mm", formatDefault: "dd.MM.yyyy, HH:mm"); - final tx = widget.transactionInfo; + TransactionDetailsPage({this.transactionInfo}) : _items = [] { + // FIXME +// final _dateFormat = widget.settingsStore.getCurrentDateFormat( +// formatUSA: "yyyy.MM.dd, HH:mm", formatDefault: "dd.MM.yyyy, HH:mm"); + final dateFormat = DateFormat('dd.MM.yyyy, HH:mm'); + final tx = transactionInfo; if (tx is MoneroTransactionInfo) { final items = [ @@ -54,7 +23,31 @@ class TransactionDetailsFormState extends State { title: S.current.transaction_details_transaction_id, value: tx.id), StandartListItem( title: S.current.transaction_details_date, - value: _dateFormat.format(tx.date)), + value: dateFormat.format(tx.date)), + StandartListItem( + title: S.current.transaction_details_height, value: '${tx.height}'), + StandartListItem( + title: S.current.transaction_details_amount, + value: tx.amountFormatted()) + ]; + // FIXME +// if (widget.settingsStore.shouldSaveRecipientAddress && +// tx.recipientAddress != null) { +// items.add(StandartListItem( +// title: S.current.transaction_details_recipient_address, +// value: tx.recipientAddress)); +// } + + _items.addAll(items); + } + + if (tx is BitcoinTransactionInfo) { + final items = [ + StandartListItem( + title: S.current.transaction_details_transaction_id, value: tx.id), + StandartListItem( + title: S.current.transaction_details_date, + value: dateFormat.format(tx.date)), StandartListItem( title: S.current.transaction_details_height, value: '${tx.height}'), StandartListItem( @@ -62,33 +55,31 @@ class TransactionDetailsFormState extends State { value: tx.amountFormatted()) ]; - if (widget.settingsStore.shouldSaveRecipientAddress && - tx.recipientAddress != null) { - items.add(StandartListItem( - title: S.current.transaction_details_recipient_address, - value: tx.recipientAddress)); - } - _items.addAll(items); } - - super.initState(); } @override - Widget build(BuildContext context) { + String get title => S.current.transaction_details_title; + + final TransactionInfo transactionInfo; + + final List _items; + + @override + Widget body(BuildContext context) { return Container( padding: EdgeInsets.only(top: 20, bottom: 20), child: ListView.separated( separatorBuilder: (context, index) => Container( - height: 1, - padding: EdgeInsets.only(left: 24), - color: Theme.of(context).accentTextTheme.title.backgroundColor, - child: Container( - height: 1, - color: Theme.of(context).dividerColor, - ), - ), + height: 1, + padding: EdgeInsets.only(left: 24), + color: Theme.of(context).accentTextTheme.title.backgroundColor, + child: Container( + height: 1, + color: Theme.of(context).dividerColor, + ), + ), itemCount: _items.length, itemBuilder: (context, index) { final item = _items[index]; @@ -108,8 +99,7 @@ class TransactionDetailsFormState extends State { ), ); }, - child: - StandartListRow( + child: StandartListRow( title: '${item.title}:', value: item.value, isDrawTop: isDrawTop, diff --git a/lib/src/screens/wallet_keys/wallet_keys_page.dart b/lib/src/screens/wallet_keys/wallet_keys_page.dart new file mode 100644 index 000000000..e4a9954e8 --- /dev/null +++ b/lib/src/screens/wallet_keys/wallet_keys_page.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/widgets/standart_list_row.dart'; +import 'package:cake_wallet/view_model/wallet_keys_view_model.dart'; + +class WalletKeysPage extends BasePage { + WalletKeysPage(this.walletKeysViewModel); + + @override + String get title => S.current.wallet_keys; + + final WalletKeysViewModel walletKeysViewModel; + + @override + Widget body(BuildContext context) { + return Container( + padding: EdgeInsets.only(top: 20.0, bottom: 20.0), + child: Observer( + builder: (_) { + return ListView.separated( + separatorBuilder: (context, index) => Container( + height: 1, + padding: EdgeInsets.only(left: 24), + color: Theme.of(context) + .accentTextTheme + .title + .backgroundColor, + child: Container( + height: 1, + color: Theme.of(context).dividerColor, + ), + ), + itemCount: walletKeysViewModel.items.length, + itemBuilder: (BuildContext context, int index) { + final item = walletKeysViewModel.items[index]; + + return GestureDetector( + onTap: () { + Clipboard.setData(ClipboardData(text: item.value)); + Scaffold.of(context).showSnackBar(SnackBar( + content: Text( + S.of(context).copied_key_to_clipboard(item.title), + textAlign: TextAlign.center, + style: TextStyle(color: Colors.white), + ), + backgroundColor: Colors.green, + duration: Duration(seconds: 1), + )); + }, + child: StandartListRow( + title: item.title + ':', + value: item.value, + ), + ); + }); + }, + )); + } +} diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index ddea976db..6b8e047cf 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -1,177 +1,188 @@ import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:provider/provider.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/palette.dart'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; +import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; -import 'package:cake_wallet/src/stores/wallet_list/wallet_list_store.dart'; -import 'package:cake_wallet/src/stores/wallet/wallet_store.dart'; import 'package:cake_wallet/src/screens/wallet_list/wallet_menu.dart'; import 'package:cake_wallet/src/screens/wallet_list/widgets/wallet_tile.dart'; class WalletListPage extends BasePage { + WalletListPage({this.walletListViewModel}); + + final WalletListViewModel walletListViewModel; + @override - Widget body(BuildContext context) => WalletListBody(); + Widget body(BuildContext context) => + WalletListBody(walletListViewModel: walletListViewModel); } class WalletListBody extends StatefulWidget { + WalletListBody({this.walletListViewModel}); + + final WalletListViewModel walletListViewModel; + @override WalletListBodyState createState() => WalletListBodyState(); } class WalletListBodyState extends State { - final moneroIcon = Image.asset('assets/images/monero.png', height: 24, width: 24); - WalletListStore _walletListStore; - ScrollController scrollController = ScrollController(); + final moneroIcon = + Image.asset('assets/images/monero.png', height: 24, width: 24); + final bitcoinIcon = + Image.asset('assets/images/bitcoin.png', height: 24, width: 24); + final scrollController = ScrollController(); @override Widget build(BuildContext context) { - final walletStore = Provider.of(context); - _walletListStore = Provider.of(context); - final newWalletImage = Image.asset('assets/images/new_wallet.png', - height: 12, - width: 12, - color: Palette.oceanBlue); + height: 12, width: 12, color: Palette.oceanBlue); final restoreWalletImage = Image.asset('assets/images/restore_wallet.png', height: 12, width: 12, color: Theme.of(context).primaryTextTheme.title.color); return SafeArea( - child: Container( - padding: EdgeInsets.only(top: 16), - child: ScrollableWithBottomSection( - contentPadding: EdgeInsets.only(bottom: 20), - content: Container( - child: Observer( - builder: (_) => ListView.separated( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - separatorBuilder: (_, index) => Divider( - color: Theme.of(context).backgroundColor, - height: 16), - itemCount: _walletListStore.wallets.length, - itemBuilder: (__, index) { - final wallet = _walletListStore.wallets[index]; - final screenWidth = MediaQuery.of(context).size.width; + child: Container( + padding: EdgeInsets.only(top: 16), + child: ScrollableWithBottomSection( + contentPadding: EdgeInsets.only(bottom: 20), + content: Container( + child: Observer( + builder: (_) => ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + separatorBuilder: (_, index) => Divider( + color: Theme.of(context).backgroundColor, height: 16), + itemCount: widget.walletListViewModel.wallets.length, + itemBuilder: (__, index) { + final wallet = widget.walletListViewModel.wallets[index]; + final screenWidth = MediaQuery.of(context).size.width; +// String shortAddress = ''; - final isCurrentWallet = - _walletListStore.isCurrentWallet(wallet); +// if (wallet.isCurrent) { +// shortAddress = wallet.address; +// shortAddress = shortAddress.replaceRange( +// 4, shortAddress.length - 4, '...'); +// } - String shortAddress = ''; + final walletMenu = WalletMenu(context, widget.walletListViewModel); + final items = + walletMenu.generateItemsForWalletMenu(wallet.isCurrent); + final colors = walletMenu + .generateColorsForWalletMenu(wallet.isCurrent); + final images = walletMenu + .generateImagesForWalletMenu(wallet.isCurrent); - if (isCurrentWallet) { - shortAddress = walletStore.subaddress.address; - shortAddress = shortAddress.replaceRange(4, shortAddress.length - 4, '...'); - } + return Container( + height: 108, + width: double.infinity, + child: CustomScrollView( + scrollDirection: Axis.horizontal, + controller: scrollController, + slivers: [ + SliverPersistentHeader( + pinned: false, + floating: true, + delegate: WalletTile( + min: screenWidth - 228, + max: screenWidth, + image: _imageFor(type: wallet.type), + walletName: wallet.name, + walletAddress: '', //shortAddress, + isCurrent: wallet.isCurrent), + ), + SliverList( + delegate: + SliverChildBuilderDelegate((context, index) { + final item = items[index]; + final color = colors[index]; + final image = images[index]; - final walletMenu = WalletMenu(context); - final items = walletMenu.generateItemsForWalletMenu(isCurrentWallet); - final colors = walletMenu.generateColorsForWalletMenu(isCurrentWallet); - final images = walletMenu.generateImagesForWalletMenu(isCurrentWallet); + final radius = index == 0 ? 12.0 : 0.0; - return Container( - height: 108, - width: double.infinity, - child: CustomScrollView( - scrollDirection: Axis.horizontal, - controller: scrollController, - slivers: [ - SliverPersistentHeader( - pinned: false, - floating: true, - delegate: WalletTile( - min: screenWidth - 228, - max: screenWidth, - image: moneroIcon, - walletName: wallet.name, - walletAddress: shortAddress, - isCurrent: isCurrentWallet + return GestureDetector( + onTap: () { + scrollController.animateTo(0.0, + duration: Duration(milliseconds: 500), + curve: Curves.fastOutSlowIn); + walletMenu.action( + walletMenu.listItems.indexOf(item), + wallet, + wallet.isCurrent); + }, + child: Container( + height: 108, + width: 108, + color: Theme.of(context).backgroundColor, + child: Container( + padding: EdgeInsets.only(left: 5, right: 5), + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(radius), + bottomLeft: Radius.circular(radius)), + color: color), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + image, + SizedBox(height: 5), + Text( + item, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + color: Colors.white), + ) + ], + ), + ), + ), ), - ), - SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - - final item = items[index]; - final color = colors[index]; - final image = images[index]; - - final radius = index == 0 ? 12.0 : 0.0; - - return GestureDetector( - onTap: () { - scrollController.animateTo(0.0, duration: Duration(milliseconds: 500), curve: Curves.fastOutSlowIn); - walletMenu.action( - walletMenu.listItems.indexOf(item), wallet, isCurrentWallet); - }, - child: Container( - height: 108, - width: 108, - color: Theme.of(context).backgroundColor, - child: Container( - padding: EdgeInsets.only(left: 5, right: 5), - decoration: BoxDecoration( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(radius), - bottomLeft: Radius.circular(radius) - ), - color: color - ), - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - image, - SizedBox(height: 5), - Text( - item, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - color: Colors.white - ), - ) - ], - ), - ), - ), - ), - ); - }, - childCount: items.length - ) - ) - ], - ), - ); - }), - ), + ); + }, childCount: items.length)) + ], + ), + ); + }), ), - bottomSection: Column(children: [ - PrimaryImageButton( - onPressed: () => Navigator.of(context).pushNamed(Routes.newWalletType), - image: newWalletImage, - text: S.of(context).wallet_list_create_new_wallet, - color: Colors.white, - textColor: Palette.oceanBlue, - borderColor: Palette.oceanBlue, - ), - SizedBox(height: 10.0), - PrimaryImageButton( - onPressed: () => - Navigator.of(context).pushNamed(Routes.restoreWalletOptions), + ), + bottomSection: Column(children: [ + PrimaryImageButton( + onPressed: () => + Navigator.of(context).pushNamed(Routes.newWalletType), + image: newWalletImage, + text: S.of(context).wallet_list_create_new_wallet, + color: Colors.white, + textColor: Palette.oceanBlue, + borderColor: Palette.oceanBlue, + ), + SizedBox(height: 10.0), + PrimaryImageButton( + onPressed: () => Navigator.of(context) + .pushNamed(Routes.restoreWalletType), image: restoreWalletImage, text: S.of(context).wallet_list_restore_wallet, color: Theme.of(context).primaryTextTheme.overline.color, textColor: Theme.of(context).primaryTextTheme.title.color) - ])), - ) - ); + ])), + )); + } + + Image _imageFor({WalletType type}) { + switch (type) { + case WalletType.bitcoin: + return bitcoinIcon; + case WalletType.monero: + return moneroIcon; + default: + return null; + } } } diff --git a/lib/src/screens/wallet_list/wallet_menu.dart b/lib/src/screens/wallet_list/wallet_menu.dart index a6f512e02..b6654d0e8 100644 --- a/lib/src/screens/wallet_list/wallet_menu.dart +++ b/lib/src/screens/wallet_list/wallet_menu.dart @@ -1,14 +1,16 @@ +import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/routes.dart'; import 'package:provider/provider.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/stores/wallet_list/wallet_list_store.dart'; -import 'package:cake_wallet/src/domain/common/wallet_description.dart'; +import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; import 'package:cake_wallet/src/screens/auth/auth_page.dart'; class WalletMenu { - WalletMenu(this.context); + WalletMenu(this.context, this.walletListViewModel); + final WalletListViewModel walletListViewModel; final BuildContext context; final List listItems = [ @@ -65,9 +67,7 @@ class WalletMenu { return images; } - void action(int index, WalletDescription wallet, bool isCurrentWallet) { - final _walletListStore = Provider.of(context); - + void action(int index, WalletListItem wallet, bool isCurrentWallet) { switch (index) { case 0: Navigator.of(context).pushNamed(Routes.auth, arguments: @@ -79,7 +79,7 @@ class WalletMenu { try { auth.changeProcessText( S.of(context).wallet_list_loading_wallet(wallet.name)); - await _walletListStore.loadWallet(wallet); + await walletListViewModel.loadWallet(wallet); auth.close(); Navigator.of(context).pop(); } catch (e) { @@ -109,7 +109,7 @@ class WalletMenu { try { auth.changeProcessText( S.of(context).wallet_list_removing_wallet(wallet.name)); - await _walletListStore.remove(wallet); +// await _walletListStore.remove(wallet); auth.close(); } catch (e) { auth.changeProcessText(S diff --git a/lib/src/screens/wallet_list/widgets/wallet_tile.dart b/lib/src/screens/wallet_list/widgets/wallet_tile.dart index f1d1c14d0..42a2301a9 100644 --- a/lib/src/screens/wallet_list/widgets/wallet_tile.dart +++ b/lib/src/screens/wallet_list/widgets/wallet_tile.dart @@ -20,11 +20,10 @@ class WalletTile extends SliverPersistentHeaderDelegate { @override Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { - - double opacity = 1 - shrinkOffset / (max - min); + var opacity = 1 - shrinkOffset / (max - min); opacity = opacity >= 0 ? opacity : 0; - double panelWidth = 12 * opacity; + var panelWidth = 12 * opacity; panelWidth = panelWidth < 12 ? 0 : 12; final currentColor = isCurrent diff --git a/lib/src/screens/welcome/welcome_page.dart b/lib/src/screens/welcome/welcome_page.dart index 256fab4b6..46a4fb89f 100644 --- a/lib/src/screens/welcome/welcome_page.dart +++ b/lib/src/screens/welcome/welcome_page.dart @@ -4,11 +4,14 @@ import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:provider/provider.dart'; +import 'package:cake_wallet/themes.dart'; +import 'package:cake_wallet/theme_changer.dart'; class WelcomePage extends BasePage { static const aspectRatioImage = 1.25; - final welcomeImage = Image.asset('assets/images/welcome.png'); + final welcomeImageLight = Image.asset('assets/images/welcome_light.png'); + final welcomeImageDark = Image.asset('assets/images/welcome.png'); @override Widget build(BuildContext context) { @@ -20,6 +23,10 @@ class WelcomePage extends BasePage { @override Widget body(BuildContext context) { + final _themeChanger = Provider.of(context); + final welcomeImage = _themeChanger.getTheme() == Themes.darkTheme + ? welcomeImageDark : welcomeImageLight; + final newWalletImage = Image.asset('assets/images/new_wallet.png', height: 12, width: 12, @@ -30,89 +37,97 @@ class WelcomePage extends BasePage { color: Theme.of(context).primaryTextTheme.title.color); return Container( - padding: EdgeInsets.only(top: 20), - child: ScrollableWithBottomSection( - contentPadding: EdgeInsets.only(bottom: 20), - content: Column( - children: [ - AspectRatio( + padding: EdgeInsets.all(24), + child: Column( + children: [ + Flexible( + flex: 2, + child: AspectRatio( aspectRatio: aspectRatioImage, - child: FittedBox(child: welcomeImage, fit: BoxFit.fill)), - Padding( - padding: EdgeInsets.only(left: 24, right: 24, top: 40), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - S.of(context).welcome, - style: TextStyle( - fontSize: 18, - color: Theme.of(context).primaryTextTheme.caption.color, - ), - textAlign: TextAlign.center, - ), - Padding( - padding: EdgeInsets.only(top: 10), - child: Text( - S.of(context).cake_wallet, - style: TextStyle( - fontSize: 36, - fontWeight: FontWeight.bold, - color: Theme.of(context).primaryTextTheme.title.color, + child: FittedBox(child: welcomeImage, fit: BoxFit.fill) + ) + ), + Flexible( + flex: 3, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + children: [ + Padding( + padding: EdgeInsets.only(top: 24), + child: Text( + S.of(context).welcome, + style: TextStyle( + fontSize: 18, + color: Theme.of(context).primaryTextTheme.caption.color, + ), + textAlign: TextAlign.center, ), - textAlign: TextAlign.center, ), - ), - Padding( - padding: EdgeInsets.only(top: 14), - child: Text( - S.of(context).first_wallet_text, + Padding( + padding: EdgeInsets.only(top: 10), + child: Text( + S.of(context).cake_wallet, + style: TextStyle( + fontSize: 36, + fontWeight: FontWeight.bold, + color: Theme.of(context).primaryTextTheme.title.color, + ), + textAlign: TextAlign.center, + ), + ), + Padding( + padding: EdgeInsets.only(top: 14), + child: Text( + S.of(context).first_wallet_text, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Theme.of(context).primaryTextTheme.caption.color, + ), + textAlign: TextAlign.center, + ), + ), + ], + ), + Column( + children: [ + Text( + S.of(context).please_make_selection, style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, + fontSize: 12, color: Theme.of(context).primaryTextTheme.caption.color, ), textAlign: TextAlign.center, ), - ), - ], - ), + Padding( + padding: EdgeInsets.only(top: 24), + child: PrimaryImageButton( + onPressed: () => Navigator.pushNamed(context, Routes.newWalletFromWelcome), + image: newWalletImage, + text: S.of(context).create_new, + color: Colors.white, + textColor: Palette.oceanBlue, + borderColor: Palette.oceanBlue, + ), + ), + Padding( + padding: EdgeInsets.only(top: 10), + child: PrimaryImageButton( + onPressed: () => Navigator.pushNamed(context, Routes.restoreOptions), + image: restoreWalletImage, + text: S.of(context).restore_wallet, + color: Theme.of(context).primaryTextTheme.overline.color, + textColor: Theme.of(context).primaryTextTheme.title.color), + ) + ], + ) + ], ) - ], - ), - bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 20), - bottomSection: Column(children: [ - Text( - S.of(context).please_make_selection, - style: TextStyle( - fontSize: 12, - color: Theme.of(context).primaryTextTheme.caption.color, - ), - textAlign: TextAlign.center, - ), - Padding( - padding: EdgeInsets.only(top: 24), - child: PrimaryImageButton( - onPressed: () => Navigator.pushNamed(context, Routes.newWalletFromWelcome), - image: newWalletImage, - text: S.of(context).create_new, - color: Colors.white, - textColor: Palette.oceanBlue, - borderColor: Palette.oceanBlue, - ), - ), - Padding( - padding: EdgeInsets.only(top: 10), - child: PrimaryImageButton( - onPressed: () => Navigator.pushNamed(context, Routes.restoreOptions), - image: restoreWalletImage, - text: S.of(context).restore_wallet, - color: Theme.of(context).primaryTextTheme.overline.color, - textColor: Theme.of(context).primaryTextTheme.title.color), ) - ]), - ), + ], + ) ); } } diff --git a/lib/src/stores/address_book/address_book_store.dart b/lib/src/stores/address_book/address_book_store.dart index 638d833ba..6537f24c6 100644 --- a/lib/src/stores/address_book/address_book_store.dart +++ b/lib/src/stores/address_book/address_book_store.dart @@ -1,111 +1,111 @@ -import 'package:mobx/mobx.dart'; -import 'package:flutter/foundation.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/domain/common/contact.dart'; -import 'package:cake_wallet/src/domain/common/crypto_currency.dart'; -import 'package:hive/hive.dart'; - -part 'address_book_store.g.dart'; - -class AddressBookStore = AddressBookStoreBase with _$AddressBookStore; - -abstract class AddressBookStoreBase with Store { - AddressBookStoreBase({@required this.contacts}) { - updateContactList(); - isDisabledStatus = true; - } - - @observable - List contactList; - - @observable - bool isDisabledStatus; - - @observable - bool isValid; - - @observable - String errorMessage; - - Box contacts; - - @action - Future add({Contact contact}) async => contacts.add(contact); - - @action - Future updateContactList() async => contactList = contacts.values.toList(); - - @action - Future update({Contact contact}) async => contact.save(); - - @action - Future delete({Contact contact}) async => await contact.delete(); - - @action - void setDisabledStatus(bool isDisabled) { - isDisabledStatus = isDisabled; - } - - void validateContactName(String value) { - const pattern = '''^[^`,'"]{1,32}\$'''; - final regExp = RegExp(pattern); - isValid = regExp.hasMatch(value); - errorMessage = isValid ? null : S.current.error_text_contact_name; - } - - void validateAddress(String value, {CryptoCurrency cryptoCurrency}) { - // XMR (95, 106), ADA (59, 92, 105), BCH (42), BNB (42), BTC (34, 42), DASH (34), EOS (42), - // ETH (42), LTC (34), NANO (64, 65), TRX (34), USDT (42), XLM (56), XRP (34) - const pattern = '^[0-9a-zA-Z]{95}\$|^[0-9a-zA-Z]{34}\$|^[0-9a-zA-Z]{42}\$|^[0-9a-zA-Z]{56}\$|^[0-9a-zA-Z]{59}\$|^[0-9a-zA-Z_]{64}\$|^[0-9a-zA-Z_]{65}\$|^[0-9a-zA-Z]{92}\$|^[0-9a-zA-Z]{105}\$|^[0-9a-zA-Z]{106}\$'; - final regExp = RegExp(pattern); - isValid = regExp.hasMatch(value); - if (isValid && cryptoCurrency != null) { - switch (cryptoCurrency) { - case CryptoCurrency.xmr: - isValid = (value.length == 95)||(value.length == 106); - break; - case CryptoCurrency.ada: - isValid = (value.length == 59)||(value.length == 92)||(value.length == 105); - break; - case CryptoCurrency.bch: - isValid = (value.length == 42); - break; - case CryptoCurrency.bnb: - isValid = (value.length == 42); - break; - case CryptoCurrency.btc: - isValid = (value.length == 34)||(value.length == 42); - break; - case CryptoCurrency.dash: - isValid = (value.length == 34); - break; - case CryptoCurrency.eos: - isValid = (value.length == 42); - break; - case CryptoCurrency.eth: - isValid = (value.length == 42); - break; - case CryptoCurrency.ltc: - isValid = (value.length == 34); - break; - case CryptoCurrency.nano: - isValid = (value.length == 64)||(value.length == 65); - break; - case CryptoCurrency.trx: - isValid = (value.length == 34); - break; - case CryptoCurrency.usdt: - isValid = (value.length == 42); - break; - case CryptoCurrency.xlm: - isValid = (value.length == 56); - break; - case CryptoCurrency.xrp: - isValid = (value.length == 34); - break; - } - } - - errorMessage = isValid ? null : S.current.error_text_address; - } -} +//import 'package:mobx/mobx.dart'; +//import 'package:flutter/foundation.dart'; +//import 'package:cake_wallet/generated/i18n.dart'; +//import 'package:cake_wallet/src/domain/common/contact.dart'; +//import 'package:cake_wallet/src/domain/common/crypto_currency.dart'; +//import 'package:hive/hive.dart'; +// +//part 'address_book_store.g.dart'; +// +//class AddressBookStore = AddressBookStoreBase with _$AddressBookStore; +// +//abstract class AddressBookStoreBase with Store { +// AddressBookStoreBase({@required this.contacts}) { +// updateContactList(); +// isDisabledStatus = true; +// } +// +// @observable +// List contactList; +// +// @observable +// bool isDisabledStatus; +// +// @observable +// bool isValid; +// +// @observable +// String errorMessage; +// +// Box contacts; +// +// @action +// Future add({Contact contact}) async => contacts.add(contact); +// +// @action +// Future updateContactList() async => contactList = contacts.values.toList(); +// +// @action +// Future update({Contact contact}) async => contact.save(); +// +// @action +// Future delete({Contact contact}) async => await contact.delete(); +// +// @action +// void setDisabledStatus(bool isDisabled) { +// isDisabledStatus = isDisabled; +// } +// +// void validateContactName(String value) { +// const pattern = '''^[^`,'"]{1,32}\$'''; +// final regExp = RegExp(pattern); +// isValid = regExp.hasMatch(value); +// errorMessage = isValid ? null : S.current.error_text_contact_name; +// } +// +// void validateAddress(String value, {CryptoCurrency cryptoCurrency}) { +// // XMR (95, 106), ADA (59, 92, 105), BCH (42), BNB (42), BTC (34, 42), DASH (34), EOS (42), +// // ETH (42), LTC (34), NANO (64, 65), TRX (34), USDT (42), XLM (56), XRP (34) +// const pattern = '^[0-9a-zA-Z]{95}\$|^[0-9a-zA-Z]{34}\$|^[0-9a-zA-Z]{42}\$|^[0-9a-zA-Z]{56}\$|^[0-9a-zA-Z]{59}\$|^[0-9a-zA-Z_]{64}\$|^[0-9a-zA-Z_]{65}\$|^[0-9a-zA-Z]{92}\$|^[0-9a-zA-Z]{105}\$|^[0-9a-zA-Z]{106}\$'; +// final regExp = RegExp(pattern); +// isValid = regExp.hasMatch(value); +// if (isValid && cryptoCurrency != null) { +// switch (cryptoCurrency) { +// case CryptoCurrency.xmr: +// isValid = (value.length == 95)||(value.length == 106); +// break; +// case CryptoCurrency.ada: +// isValid = (value.length == 59)||(value.length == 92)||(value.length == 105); +// break; +// case CryptoCurrency.bch: +// isValid = (value.length == 42); +// break; +// case CryptoCurrency.bnb: +// isValid = (value.length == 42); +// break; +// case CryptoCurrency.btc: +// isValid = (value.length == 34)||(value.length == 42); +// break; +// case CryptoCurrency.dash: +// isValid = (value.length == 34); +// break; +// case CryptoCurrency.eos: +// isValid = (value.length == 42); +// break; +// case CryptoCurrency.eth: +// isValid = (value.length == 42); +// break; +// case CryptoCurrency.ltc: +// isValid = (value.length == 34); +// break; +// case CryptoCurrency.nano: +// isValid = (value.length == 64)||(value.length == 65); +// break; +// case CryptoCurrency.trx: +// isValid = (value.length == 34); +// break; +// case CryptoCurrency.usdt: +// isValid = (value.length == 42); +// break; +// case CryptoCurrency.xlm: +// isValid = (value.length == 56); +// break; +// case CryptoCurrency.xrp: +// isValid = (value.length == 34); +// break; +// } +// } +// +// errorMessage = isValid ? null : S.current.error_text_address; +// } +//} diff --git a/lib/src/stores/exchange/exchange_store.dart b/lib/src/stores/exchange/exchange_store.dart index fe7a32f16..9ac6ef05c 100644 --- a/lib/src/stores/exchange/exchange_store.dart +++ b/lib/src/stores/exchange/exchange_store.dart @@ -33,10 +33,8 @@ abstract class ExchangeStoreBase with Store { provider = initialProvider; depositCurrency = initialDepositCurrency; receiveCurrency = initialReceiveCurrency; - /*isDepositAddressEnabled = !(depositCurrency == walletStore.type); - isReceiveAddressEnabled = !(receiveCurrency == walletStore.type);*/ - isDepositAddressEnabled = true; - isReceiveAddressEnabled = true; + isDepositAddressEnabled = !(depositCurrency == walletStore.type); + isReceiveAddressEnabled = !(receiveCurrency == walletStore.type); depositAmount = ''; receiveAmount = ''; depositAddress = ''; @@ -109,16 +107,16 @@ abstract class ExchangeStoreBase with Store { void changeDepositCurrency({CryptoCurrency currency}) { depositCurrency = currency; _onPairChange(); - /*isDepositAddressEnabled = !(depositCurrency == walletStore.type); - isReceiveAddressEnabled = !(receiveCurrency == walletStore.type);*/ + isDepositAddressEnabled = !(depositCurrency == walletStore.type); + isReceiveAddressEnabled = !(receiveCurrency == walletStore.type); } @action void changeReceiveCurrency({CryptoCurrency currency}) { receiveCurrency = currency; _onPairChange(); - /*isDepositAddressEnabled = !(depositCurrency == walletStore.type); - isReceiveAddressEnabled = !(receiveCurrency == walletStore.type);*/ + isDepositAddressEnabled = !(depositCurrency == walletStore.type); + isReceiveAddressEnabled = !(receiveCurrency == walletStore.type); } @action @@ -242,8 +240,8 @@ abstract class ExchangeStoreBase with Store { receiveCurrency = CryptoCurrency.btc; depositAddress = depositCurrency == walletStore.type ? walletStore.address : ''; receiveAddress = receiveCurrency == walletStore.type ? walletStore.address : ''; - /*isDepositAddressEnabled = !(depositCurrency == walletStore.type); - isReceiveAddressEnabled = !(receiveCurrency == walletStore.type);*/ + isDepositAddressEnabled = !(depositCurrency == walletStore.type); + isReceiveAddressEnabled = !(receiveCurrency == walletStore.type); _onPairChange(); } diff --git a/lib/src/stores/node_list/node_list_store.dart b/lib/src/stores/node_list/node_list_store.dart index 454f2ac1f..20442477e 100644 --- a/lib/src/stores/node_list/node_list_store.dart +++ b/lib/src/stores/node_list/node_list_store.dart @@ -72,8 +72,7 @@ abstract class NodeListBase with Store { Future isNodeOnline(Node node) async { try { - return await node.requestNode(node.uri, - login: node.login, password: node.password); + return await node.requestNode(); } catch (e) { return false; } diff --git a/lib/src/widgets/address_text_field.dart b/lib/src/widgets/address_text_field.dart index 388719999..5a0cf41a2 100644 --- a/lib/src/widgets/address_text_field.dart +++ b/lib/src/widgets/address_text_field.dart @@ -38,103 +38,115 @@ class AddressTextField extends StatelessWidget { @override Widget build(BuildContext context) { - return TextFormField( - onFieldSubmitted: (_) => FocusScope.of(context).unfocus(), - enabled: isActive, - controller: controller, - focusNode: focusNode, - style: TextStyle( - fontSize: 16, - color: Theme.of(context).primaryTextTheme.title.color - ), - decoration: InputDecoration( - suffixIcon: SizedBox( - width: prefixIconWidth * options.length + - (spaceBetweenPrefixIcons * options.length), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - SizedBox(width: 5), - if (this.options.contains(AddressTextFieldOption.qrCode)) ...[ - Container( - width: prefixIconWidth, - height: prefixIconHeight, - padding: EdgeInsets.only(top: 0), - child: InkWell( - onTap: () async => _presentQRScanner(context), - child: Container( - padding: EdgeInsets.all(8), - decoration: BoxDecoration( - color: buttonColor ?? Theme.of(context).accentTextTheme.title.color, - borderRadius: - BorderRadius.all(Radius.circular(6))), - child: Image.asset('assets/images/qr_code_icon.png')), - )) - ], - if (this - .options - .contains(AddressTextFieldOption.addressBook)) ...[ - Container( - width: prefixIconWidth, - height: prefixIconHeight, - padding: EdgeInsets.only(top: 0), - child: InkWell( - onTap: () async => _presetAddressBookPicker(context), - child: Container( - padding: EdgeInsets.all(8), - decoration: BoxDecoration( - color: buttonColor ?? Theme.of(context).accentTextTheme.title.color, - borderRadius: - BorderRadius.all(Radius.circular(6))), - child: Image.asset( - 'assets/images/open_book.png')), - )) - ], - if (this - .options - .contains(AddressTextFieldOption.subaddressList)) ...[ - Container( - width: prefixIconWidth, - height: prefixIconHeight, - padding: EdgeInsets.only(top: 0), - child: InkWell( - onTap: () async => _presetSubaddressListPicker(context), - child: Container( - padding: EdgeInsets.all(8), - decoration: BoxDecoration( - color: buttonColor ?? Theme.of(context).accentTextTheme.title.color, - borderRadius: - BorderRadius.all(Radius.circular(6))), - child: Image.asset( - 'assets/images/receive_icon_raw.png')), - )) - ], - ], + return Stack( + children: [ + TextFormField( + onFieldSubmitted: (_) => FocusScope.of(context).unfocus(), + enabled: isActive, + controller: controller, + focusNode: focusNode, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).primaryTextTheme.title.color ), - ), - hintStyle: TextStyle( - fontSize: 16, - color: Theme.of(context).primaryTextTheme.caption.color - ), - hintText: placeholder ?? S.current.widgets_address, - focusedBorder: isBorderExist - ? UnderlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).dividerColor, - width: 1.0)) - : InputBorder.none, - disabledBorder: isBorderExist - ? UnderlineInputBorder( - borderSide: - BorderSide(color: Theme.of(context).dividerColor, width: 1.0)) - : InputBorder.none, - enabledBorder: isBorderExist - ? UnderlineInputBorder( - borderSide: + decoration: InputDecoration( + suffixIcon: SizedBox( + width: prefixIconWidth * options.length + + (spaceBetweenPrefixIcons * options.length), + ), + hintStyle: TextStyle( + fontSize: 16, + color: Theme.of(context).primaryTextTheme.caption.color + ), + hintText: placeholder ?? S.current.widgets_address, + focusedBorder: isBorderExist + ? UnderlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).dividerColor, + width: 1.0)) + : InputBorder.none, + disabledBorder: isBorderExist + ? UnderlineInputBorder( + borderSide: BorderSide(color: Theme.of(context).dividerColor, width: 1.0)) - : InputBorder.none, - ), - validator: validator, + : InputBorder.none, + enabledBorder: isBorderExist + ? UnderlineInputBorder( + borderSide: + BorderSide(color: Theme.of(context).dividerColor, width: 1.0)) + : InputBorder.none, + ), + validator: validator, + ), + Positioned( + bottom: 10, + right: 0, + child: SizedBox( + width: prefixIconWidth * options.length + + (spaceBetweenPrefixIcons * options.length), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox(width: 5), + if (this.options.contains(AddressTextFieldOption.qrCode)) ...[ + Container( + width: prefixIconWidth, + height: prefixIconHeight, + padding: EdgeInsets.only(top: 0), + child: InkWell( + onTap: () async => _presentQRScanner(context), + child: Container( + padding: EdgeInsets.all(8), + decoration: BoxDecoration( + color: buttonColor ?? Theme.of(context).accentTextTheme.title.color, + borderRadius: + BorderRadius.all(Radius.circular(6))), + child: Image.asset('assets/images/qr_code_icon.png')), + )) + ], + if (this + .options + .contains(AddressTextFieldOption.addressBook)) ...[ + Container( + width: prefixIconWidth, + height: prefixIconHeight, + padding: EdgeInsets.only(top: 0), + child: InkWell( + onTap: () async => _presetAddressBookPicker(context), + child: Container( + padding: EdgeInsets.all(8), + decoration: BoxDecoration( + color: buttonColor ?? Theme.of(context).accentTextTheme.title.color, + borderRadius: + BorderRadius.all(Radius.circular(6))), + child: Image.asset( + 'assets/images/open_book.png')), + )) + ], + if (this + .options + .contains(AddressTextFieldOption.subaddressList)) ...[ + Container( + width: prefixIconWidth, + height: prefixIconHeight, + padding: EdgeInsets.only(top: 0), + child: InkWell( + onTap: () async => _presetSubaddressListPicker(context), + child: Container( + padding: EdgeInsets.all(8), + decoration: BoxDecoration( + color: buttonColor ?? Theme.of(context).accentTextTheme.title.color, + borderRadius: + BorderRadius.all(Radius.circular(6))), + child: Image.asset( + 'assets/images/receive_icon_raw.png')), + )) + ], + ], + ), + ) + ) + ], ); } diff --git a/lib/src/widgets/standard_list.dart b/lib/src/widgets/standard_list.dart new file mode 100644 index 000000000..930e59b7c --- /dev/null +++ b/lib/src/widgets/standard_list.dart @@ -0,0 +1,172 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class StandardListRow extends StatelessWidget { + StandardListRow( + {@required this.title, @required this.isSelected, this.onTap}); + + final String title; + final bool isSelected; + final void Function(BuildContext context) onTap; + + @override + Widget build(BuildContext context) { + final leading = buildLeading(context); + final trailing = buildTrailing(context); + + return InkWell( + onTap: () => onTap?.call(context), + child: Container( + color: _backgroundColor(context), + height: 56, + padding: EdgeInsets.only(left: 24, right: 24), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (leading != null) leading, + buildCenter(context, hasLeftOffset: leading != null), + if (trailing != null) trailing + ]))); + } + + Widget buildLeading(BuildContext context) => null; + + Widget buildCenter(BuildContext context, {@required bool hasLeftOffset}) { + // FIXME: find better way for keep text on left side. + return Expanded( + child: Row(mainAxisAlignment: MainAxisAlignment.start, children: [ + if (hasLeftOffset) SizedBox(width: 10), + Text(title, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: _titleColor(context))) + ])); + } + + Widget buildTrailing(BuildContext context) => null; + + Color _titleColor(BuildContext context) => isSelected + ? Color.fromRGBO(20, 200, 71, 1) + : Theme.of(context).primaryTextTheme.title.color; + + Color _backgroundColor(BuildContext context) { +// return Theme.of(context).accentTextTheme.subtitle.decorationColor; + return Theme.of(context).accentTextTheme.title.backgroundColor; + } +} + +class SectionHeaderListRow extends StatelessWidget { + @override + Widget build(BuildContext context) => Column(children: [ + StandardListSeparator(), + Container(width: double.infinity, height: 40, color: Colors.white), + StandardListSeparator() + ]); +} + +class StandardListSeparator extends StatelessWidget { + StandardListSeparator({this.padding}); + + final EdgeInsets padding; + + @override + Widget build(BuildContext context) { + return Container( + height: 1, + padding: padding, + color: Theme.of(context).accentTextTheme.title.backgroundColor, + child: Container(height: 1, color: Color.fromRGBO(219, 227, 243, 1))); + } +} + +class StandardList extends StatelessWidget { + StandardList({@required this.itemCount, @required this.itemBuilder}); + + final int itemCount; + final IndexedWidgetBuilder itemBuilder; + + @override + Widget build(BuildContext context) { + return ListView.separated( + separatorBuilder: (_, __) => + StandardListSeparator(padding: EdgeInsets.only(left: 24)), + itemCount: itemCount, + itemBuilder: itemBuilder); + } +} + +class SectionStandardListItem { + SectionStandardListItem({this.hasFullSeparator = false, this.child}); + + final bool hasFullSeparator; + final Widget child; +} + +class SectionStandardList extends StatelessWidget { + SectionStandardList( + {@required this.itemCounter, + @required this.itemBuilder, + @required this.sectionCount, + BuildContext context}) + : totalRows = transform(context, sectionCount, itemCounter, itemBuilder); + + final int sectionCount; + final int Function(int sectionIndex) itemCounter; + final Widget Function(BuildContext context, int sectionIndex, int itemIndex) + itemBuilder; + final List totalRows; + + static List transform( + BuildContext context, + int sectionCount, + int Function(int sectionIndex) itemCounter, + Widget Function(BuildContext context, int sectionIndex, int itemIndex) + itemBuilder) { + final items = []; + + for (var sectionIndex = 0; sectionIndex < sectionCount; sectionIndex++) { + if (sectionIndex == 0) { + items.add(StandardListSeparator()); + } + + final itemCount = itemCounter(sectionIndex); + + for (var itemIndex = 0; itemIndex < itemCount; itemIndex++) { + final item = itemBuilder(context, sectionIndex, itemIndex); + + items.add(item); + } + + items.add(sectionIndex + 1 != sectionCount + ? SectionHeaderListRow() + : StandardListSeparator()); + } + + return items; + } + + @override + Widget build(BuildContext context) { + return ListView.separated( + separatorBuilder: (_, index) { + final row = totalRows[index]; + + if (row is StandardListSeparator || row is SectionHeaderListRow) { + return Container(); + } + + final nextRow = totalRows[index + 1]; + + // If current row is pre last and last row is separator. + if (nextRow is StandardListSeparator || + nextRow is SectionHeaderListRow) { + return Container(); + } + + return StandardListSeparator(padding: EdgeInsets.only(left: 24)); + }, + itemCount: totalRows.length, + itemBuilder: (_, index) => totalRows[index]); + } +} diff --git a/lib/src/widgets/standart_list_row.dart b/lib/src/widgets/standart_list_row.dart index df4e376f4..ca6797911 100644 --- a/lib/src/widgets/standart_list_row.dart +++ b/lib/src/widgets/standart_list_row.dart @@ -1,7 +1,11 @@ import 'package:flutter/material.dart'; class StandartListRow extends StatelessWidget { - StandartListRow({this.title, this.value, this.isDrawTop, this.isDrawBottom}); + StandartListRow( + {this.title, + this.value, + this.isDrawTop = false, + this.isDrawBottom = false}); final String title; final String value; @@ -13,17 +17,18 @@ class StandartListRow extends StatelessWidget { return Column( children: [ isDrawTop - ? Container( - width: double.infinity, - height: 1, - color: Theme.of(context).dividerColor, - ) - : Offstage(), + ? Container( + width: double.infinity, + height: 1, + color: Theme.of(context).dividerColor, + ) + : Offstage(), Container( width: double.infinity, color: Theme.of(context).accentTextTheme.title.backgroundColor, child: Padding( - padding: const EdgeInsets.only(left: 24, top: 16, bottom: 16, right: 24), + padding: + const EdgeInsets.only(left: 24, top: 16, bottom: 16, right: 24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -31,26 +36,30 @@ class StandartListRow extends StatelessWidget { style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, - color: Theme.of(context).primaryTextTheme.caption.color), + color: + Theme.of(context).primaryTextTheme.caption.color), textAlign: TextAlign.left), Padding( padding: const EdgeInsets.only(top: 12), child: Text(value, style: TextStyle( fontSize: 16, - fontWeight: FontWeight.w600, - color: Theme.of(context).primaryTextTheme.title.color)), + fontWeight: FontWeight.w500, + color: Theme.of(context) + .primaryTextTheme + .title + .color)), ) ]), ), ), isDrawBottom - ? Container( - width: double.infinity, - height: 1, - color: Theme.of(context).dividerColor, - ) - : Offstage(), + ? Container( + width: double.infinity, + height: 1, + color: Theme.of(context).dividerColor, + ) + : Offstage(), ], ); } diff --git a/lib/src/widgets/trail_button.dart b/lib/src/widgets/trail_button.dart new file mode 100644 index 000000000..d9752e410 --- /dev/null +++ b/lib/src/widgets/trail_button.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +class TrailButton extends StatelessWidget { + TrailButton({ + @required this.caption, + @required this.onPressed + }); + + final String caption; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + + return ButtonTheme( + minWidth: double.minPositive, + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + child: FlatButton( + padding: EdgeInsets.all(0), + child: Text( + caption, + style: TextStyle( + color: Theme.of(context).primaryTextTheme.caption.color, + fontWeight: FontWeight.w500, + fontSize: 14), + ), + onPressed: onPressed), + ); + } +} \ No newline at end of file diff --git a/lib/store/app_store.dart b/lib/store/app_store.dart index 9a41be319..7b0cefe74 100644 --- a/lib/store/app_store.dart +++ b/lib/store/app_store.dart @@ -1,16 +1,33 @@ import 'package:mobx/mobx.dart'; import 'package:cake_wallet/core/wallet_base.dart'; +import 'package:cake_wallet/store/wallet_list_store.dart'; import 'package:cake_wallet/store/authentication_store.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/store/node_list_store.dart'; +import 'package:cake_wallet/store/contact_list_store.dart'; part 'app_store.g.dart'; class AppStore = AppStoreBase with _$AppStore; abstract class AppStoreBase with Store { - AppStoreBase({this.authenticationStore}); + AppStoreBase( + {this.authenticationStore, + this.walletList, + this.settingsStore, + this.contactListStore, + this.nodeListStore}); AuthenticationStore authenticationStore; @observable WalletBase wallet; + + WalletListStore walletList; + + SettingsStore settingsStore; + + ContactListStore contactListStore; + + NodeListStore nodeListStore; } diff --git a/lib/store/contact_list_store.dart b/lib/store/contact_list_store.dart new file mode 100644 index 000000000..1f923e8cc --- /dev/null +++ b/lib/store/contact_list_store.dart @@ -0,0 +1,12 @@ +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/src/domain/common/contact.dart'; + +part 'contact_list_store.g.dart'; + +class ContactListStore = ContactListStoreBase with _$ContactListStore; + +abstract class ContactListStoreBase with Store { + ContactListStoreBase() : contacts = ObservableList(); + + final ObservableList contacts; +} diff --git a/lib/store/node_list_store.dart b/lib/store/node_list_store.dart new file mode 100644 index 000000000..2c72cbe99 --- /dev/null +++ b/lib/store/node_list_store.dart @@ -0,0 +1,17 @@ +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/src/domain/common/node.dart'; + +part 'node_list_store.g.dart'; + +class NodeListStore = NodeListStoreBase with _$NodeListStore; + +abstract class NodeListStoreBase with Store { + NodeListStoreBase() : nodes = ObservableList(); + + final ObservableList nodes; + + void replaceValues(Iterable newNodes) { + nodes.clear(); + nodes.addAll(newNodes); + } +} diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart new file mode 100644 index 000000000..91e658fdd --- /dev/null +++ b/lib/store/settings_store.dart @@ -0,0 +1,154 @@ +import 'package:cake_wallet/di.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hive/hive.dart'; +import 'package:mobx/mobx.dart'; +import 'package:devicelocale/devicelocale.dart'; +import 'package:package_info/package_info.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:cake_wallet/src/domain/common/language.dart'; +import 'package:cake_wallet/src/domain/common/balance_display_mode.dart'; +import 'package:cake_wallet/src/domain/common/fiat_currency.dart'; +import 'package:cake_wallet/src/domain/common/node.dart'; +import 'package:cake_wallet/src/domain/common/transaction_priority.dart'; +import 'package:cake_wallet/src/stores/action_list/action_list_display_mode.dart'; + +part 'settings_store.g.dart'; + +class SettingsStore = SettingsStoreBase with _$SettingsStore; + +abstract class SettingsStoreBase with Store { + SettingsStoreBase( + {@required SharedPreferences sharedPreferences, + @required Box nodeSource, + @required FiatCurrency initialFiatCurrency, + @required TransactionPriority initialTransactionPriority, + @required BalanceDisplayMode initialBalanceDisplayMode, + @required bool initialSaveRecipientAddress, + @required bool initialAllowBiometricalAuthentication, + @required bool initialDarkTheme, + @required int initialPinLength, + @required String initialLanguageCode, + @required String initialCurrentLocale, + @required this.node, + @required this.appVersion, + this.actionlistDisplayMode}) { + fiatCurrency = initialFiatCurrency; + transactionPriority = initialTransactionPriority; + balanceDisplayMode = initialBalanceDisplayMode; + shouldSaveRecipientAddress = initialSaveRecipientAddress; + allowBiometricalAuthentication = initialAllowBiometricalAuthentication; + isDarkTheme = initialDarkTheme; + defaultPinLength = initialPinLength; + languageCode = initialLanguageCode; + currentLocale = initialCurrentLocale; + itemHeaders = {}; + +// actionlistDisplayMode.observe( +// (dynamic _) => _sharedPreferences.setInt(displayActionListModeKey, +// serializeActionlistDisplayModes(actionlistDisplayMode)), +// fireImmediately: false); + + _sharedPreferences = sharedPreferences; + _nodeSource = nodeSource; + } + + static const currentNodeIdKey = 'current_node_id'; + static const currentFiatCurrencyKey = 'current_fiat_currency'; + static const currentTransactionPriorityKey = 'current_fee_priority'; + static const currentBalanceDisplayModeKey = 'current_balance_display_mode'; + static const shouldSaveRecipientAddressKey = 'save_recipient_address'; + static const allowBiometricalAuthenticationKey = + 'allow_biometrical_authentication'; + static const currentDarkTheme = 'dark_theme'; + static const displayActionListModeKey = 'display_list_mode'; + static const currentPinLength = 'current_pin_length'; + static const currentLanguageCode = 'language_code'; + + @observable + Node node; + + @observable + FiatCurrency fiatCurrency; + + @observable + ObservableList actionlistDisplayMode; + + @observable + TransactionPriority transactionPriority; + + @observable + BalanceDisplayMode balanceDisplayMode; + + @observable + bool shouldSaveRecipientAddress; + + @observable + bool allowBiometricalAuthentication; + + @observable + bool isDarkTheme; + + @observable + int defaultPinLength; + + @observable + Map itemHeaders; + + String languageCode; + + String currentLocale; + + String appVersion; + + SharedPreferences _sharedPreferences; + Box _nodeSource; + + static Future load( + {@required Box nodeSource, + FiatCurrency initialFiatCurrency = FiatCurrency.usd, + TransactionPriority initialTransactionPriority = TransactionPriority.slow, + BalanceDisplayMode initialBalanceDisplayMode = + BalanceDisplayMode.availableBalance}) async { + final sharedPreferences = await getIt.getAsync(); + final currentFiatCurrency = FiatCurrency( + symbol: sharedPreferences.getString(currentFiatCurrencyKey)); + final currentTransactionPriority = TransactionPriority.deserialize( + raw: sharedPreferences.getInt(currentTransactionPriorityKey)); + final currentBalanceDisplayMode = BalanceDisplayMode.deserialize( + raw: sharedPreferences.getInt(currentBalanceDisplayModeKey)); + final shouldSaveRecipientAddress = + sharedPreferences.getBool(shouldSaveRecipientAddressKey); + final allowBiometricalAuthentication = + sharedPreferences.getBool(allowBiometricalAuthenticationKey) ?? false; + final savedDarkTheme = sharedPreferences.getBool(currentDarkTheme) ?? false; + final actionListDisplayMode = ObservableList(); + actionListDisplayMode.addAll(deserializeActionlistDisplayModes( + sharedPreferences.getInt(displayActionListModeKey) ?? + 11)); // FIXME: Unnamed constant. + final defaultPinLength = sharedPreferences.getInt(currentPinLength) ?? + 4; // FIXME: Unnamed constant. + final savedLanguageCode = + sharedPreferences.getString(currentLanguageCode) ?? + await Language.localeDetection(); + final initialCurrentLocale = await Devicelocale.currentLocale; + final nodeId = sharedPreferences.getInt(currentNodeIdKey); + final node = nodeSource.get(nodeId); + final packageInfo = await PackageInfo.fromPlatform(); + + return SettingsStore( + sharedPreferences: sharedPreferences, + node: node, + nodeSource: nodeSource, + appVersion: packageInfo.version, + initialFiatCurrency: currentFiatCurrency, + initialTransactionPriority: currentTransactionPriority, + initialBalanceDisplayMode: currentBalanceDisplayMode, + initialSaveRecipientAddress: shouldSaveRecipientAddress, + initialAllowBiometricalAuthentication: allowBiometricalAuthentication, + initialDarkTheme: savedDarkTheme, + actionlistDisplayMode: actionListDisplayMode, + initialPinLength: defaultPinLength, + initialLanguageCode: savedLanguageCode, + initialCurrentLocale: initialCurrentLocale); + } +} diff --git a/lib/store/wallet_list_store.dart b/lib/store/wallet_list_store.dart index e256c6ae5..18eeb3984 100644 --- a/lib/store/wallet_list_store.dart +++ b/lib/store/wallet_list_store.dart @@ -1,10 +1,13 @@ import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/src/domain/common/wallet_description.dart'; part 'wallet_list_store.g.dart'; class WalletListStore = WalletListStoreBase with _$WalletListStore; abstract class WalletListStoreBase with Store { + WalletListStoreBase() : wallets = ObservableList(); + @observable - Object state; + ObservableList wallets; } \ No newline at end of file diff --git a/lib/themes.dart b/lib/themes.dart index 26a62956a..6112394dc 100644 --- a/lib/themes.dart +++ b/lib/themes.dart @@ -4,7 +4,7 @@ import 'palette.dart'; class Themes { static final ThemeData lightTheme = ThemeData( - fontFamily: 'Lato', + fontFamily: 'Avenir Next', brightness: Brightness.light, backgroundColor: Colors.white, focusColor: Colors.white, // wallet card border @@ -73,7 +73,7 @@ class Themes { static final ThemeData darkTheme = ThemeData( - fontFamily: 'Lato', + fontFamily: 'Avenir Next', brightness: Brightness.dark, backgroundColor: PaletteDark.darkNightBlue, focusColor: PaletteDark.lightDistantBlue, // wallet card border diff --git a/lib/view_model/contact_list/contact_list_view_model.dart b/lib/view_model/contact_list/contact_list_view_model.dart new file mode 100644 index 000000000..a3f526a86 --- /dev/null +++ b/lib/view_model/contact_list/contact_list_view_model.dart @@ -0,0 +1,20 @@ +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/core/contact_service.dart'; +import 'package:cake_wallet/store/contact_list_store.dart'; +import 'package:cake_wallet/src/domain/common/contact.dart'; + +part 'contact_list_view_model.g.dart'; + +class ContactListViewModel = ContactListViewModelBase with _$ContactListViewModel; + +abstract class ContactListViewModelBase with Store { + ContactListViewModelBase(this.addressBookStore, this.contactService); + + final ContactListStore addressBookStore; + final ContactService contactService; + + @computed + ObservableList get contacts => addressBookStore.contacts; + + Future delete(Contact contact) async => contactService.delete(contact); +} \ No newline at end of file diff --git a/lib/view_model/contact_list/contact_view_model.dart b/lib/view_model/contact_list/contact_view_model.dart new file mode 100644 index 000000000..5e7ccdf9f --- /dev/null +++ b/lib/view_model/contact_list/contact_view_model.dart @@ -0,0 +1,69 @@ +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/core/wallet_base.dart'; +import 'package:cake_wallet/core/contact_service.dart'; +import 'package:cake_wallet/src/domain/common/contact.dart'; +import 'package:cake_wallet/src/domain/common/crypto_currency.dart'; +import 'package:cake_wallet/view_model/contact_list/contact_view_model_state.dart'; + +part 'contact_view_model.g.dart'; + +class ContactViewModel = ContactViewModelBase with _$ContactViewModel; + +abstract class ContactViewModelBase with Store { + ContactViewModelBase(this._contactService, this._wallet, {Contact contact}) + : state = InitialContactViewModelState(), + currencies = CryptoCurrency.all, + _contact = contact { + name = _contact?.name; + address = _contact?.address; + currency = _contact?.type ?? _wallet.currency; + } + + @observable + ContactViewModelState state; + + @observable + String name; + + @observable + String address; + + @observable + CryptoCurrency currency; + + @computed + bool get isReady => + (name?.isNotEmpty ?? false) && (address?.isNotEmpty ?? false); + + final List currencies; + final ContactService _contactService; + final WalletBase _wallet; + final Contact _contact; + + @action + void reset() { + address = ''; + name = ''; + currency = _wallet.currency; + } + + Future save() async { + try { + state = ContactIsCreating(); + + if (_contact != null) { + _contact.name = name; + _contact.address = address; + _contact.updateCryptoCurrency(currency: currency); + await _contactService.update(_contact); + } else { + await _contactService + .add(Contact(name: name, address: address, type: currency)); + } + + state = ContactSavingSuccessfully(); + } catch (e) { + state = ContactCreationFailure(e.toString()); + } + } +} diff --git a/lib/view_model/contact_list/contact_view_model_state.dart b/lib/view_model/contact_list/contact_view_model_state.dart new file mode 100644 index 000000000..ebdc31569 --- /dev/null +++ b/lib/view_model/contact_list/contact_view_model_state.dart @@ -0,0 +1,13 @@ +abstract class ContactViewModelState {} + +class InitialContactViewModelState extends ContactViewModelState {} + +class ContactIsCreating extends ContactViewModelState {} + +class ContactSavingSuccessfully extends ContactViewModelState {} + +class ContactCreationFailure extends ContactViewModelState { + ContactCreationFailure(this.error); + + final String error; +} \ No newline at end of file diff --git a/lib/view_model/dashboard_view_model.dart b/lib/view_model/dashboard_view_model.dart index 086b4ff29..ded80b3bc 100644 --- a/lib/view_model/dashboard_view_model.dart +++ b/lib/view_model/dashboard_view_model.dart @@ -1,4 +1,6 @@ import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart'; +import 'package:cake_wallet/monero/monero_wallet.dart'; import 'package:cake_wallet/src/domain/common/transaction_direction.dart'; import 'package:cake_wallet/src/domain/common/transaction_info.dart'; import 'package:cake_wallet/src/stores/action_list/transaction_list_item.dart'; @@ -22,13 +24,17 @@ class WalletBalace { abstract class DashboardViewModelBase with Store { DashboardViewModelBase({this.appStore}) { name = appStore.wallet?.name; - balance = WalletBalace(unlockedBalance: '0.001', totalBalance: '0.005'); - status = SyncedSyncStatus(); - type = WalletType.bitcoin; wallet ??= appStore.wallet; - _reaction = reaction((_) => appStore.wallet, _onWalletChange); + type = wallet.type; transactions = ObservableList.of(wallet.transactionHistory.transactions .map((transaction) => TransactionListItem(transaction: transaction))); + _reaction = reaction((_) => appStore.wallet, _onWalletChange); + + final _wallet = wallet; + + if (_wallet is MoneroWallet) { + subname = _wallet.account?.label; + } } @observable @@ -40,14 +46,110 @@ abstract class DashboardViewModelBase with Store { @computed String get address => wallet.address; - @observable - WalletBalace balance; + @computed + SyncStatus get status => wallet.syncStatus; - @observable - SyncStatus status; + @computed + WalletBalace get balance { + final wallet = this.wallet; + + if (wallet is MoneroWallet) { + return WalletBalace( + unlockedBalance: wallet.balance.formattedUnlockedBalance, + totalBalance: wallet.balance.formattedFullBalance); + } + + if (wallet is BitcoinWallet) { + return WalletBalace( + unlockedBalance: wallet.balance.confirmedFormatted, + totalBalance: wallet.balance.unconfirmedFormatted); + } + } @observable ObservableList transactions; +// ObservableList.of([ +// TransactionListItem(transaction: BitcoinTransactionInfo( +// id: '', +// height: 0, +// amount: 0, +// direction: TransactionDirection.incoming, +// date: DateTime.now(), +// isPending: false +// )), +// TransactionListItem(transaction: BitcoinTransactionInfo( +// id: '', +// height: 0, +// amount: 0, +// direction: TransactionDirection.incoming, +// date: DateTime.now(), +// isPending: false +// )), +// TransactionListItem(transaction: BitcoinTransactionInfo( +// id: '', +// height: 0, +// amount: 0, +// direction: TransactionDirection.incoming, +// date: DateTime.now(), +// isPending: false +// )), +// TransactionListItem(transaction: BitcoinTransactionInfo( +// id: '', +// height: 0, +// amount: 0, +// direction: TransactionDirection.incoming, +// date: DateTime.now(), +// isPending: false +// )), +// TransactionListItem(transaction: BitcoinTransactionInfo( +// id: '', +// height: 0, +// amount: 0, +// direction: TransactionDirection.incoming, +// date: DateTime.now(), +// isPending: false +// )), +// TransactionListItem(transaction: BitcoinTransactionInfo( +// id: '', +// height: 0, +// amount: 0, +// direction: TransactionDirection.incoming, +// date: DateTime.now(), +// isPending: false +// )), +// TransactionListItem(transaction: BitcoinTransactionInfo( +// id: '', +// height: 0, +// amount: 0, +// direction: TransactionDirection.incoming, +// date: DateTime.now(), +// isPending: false +// )), +// TransactionListItem(transaction: BitcoinTransactionInfo( +// id: '', +// height: 0, +// amount: 0, +// direction: TransactionDirection.incoming, +// date: DateTime.now(), +// isPending: false +// )), +// TransactionListItem(transaction: BitcoinTransactionInfo( +// id: '', +// height: 0, +// amount: 0, +// direction: TransactionDirection.incoming, +// date: DateTime.now(), +// isPending: false +// )), +// TransactionListItem(transaction: BitcoinTransactionInfo( +// id: '', +// height: 0, +// amount: 0, +// direction: TransactionDirection.incoming, +// date: DateTime.now(), +// isPending: false +// )), +// ]); @observable String subname; @@ -63,6 +165,5 @@ abstract class DashboardViewModelBase with Store { transactions.clear(); transactions.addAll(wallet.transactionHistory.transactions .map((transaction) => TransactionListItem(transaction: transaction))); - balance = WalletBalace(unlockedBalance: '0.001', totalBalance: '0.005'); } } diff --git a/lib/view_model/monero_account_list/account_list_item.dart b/lib/view_model/monero_account_list/account_list_item.dart new file mode 100644 index 000000000..68463f0c3 --- /dev/null +++ b/lib/view_model/monero_account_list/account_list_item.dart @@ -0,0 +1,10 @@ +import 'package:flutter/foundation.dart'; + +class AccountListItem { + AccountListItem( + {@required this.label, @required this.id, this.isSelected = false}); + + final String label; + final int id; + final bool isSelected; +} diff --git a/lib/view_model/monero_account_list/monero_account_edit_or_create_state.dart b/lib/view_model/monero_account_list/monero_account_edit_or_create_state.dart new file mode 100644 index 000000000..664b1cd99 --- /dev/null +++ b/lib/view_model/monero_account_list/monero_account_edit_or_create_state.dart @@ -0,0 +1,15 @@ +import 'package:flutter/foundation.dart'; + +abstract class MoneroAccountEditOrCreateState {} + +class InitialAccountCreationState extends MoneroAccountEditOrCreateState {} + +class AccountIsCreating extends MoneroAccountEditOrCreateState {} + +class AccountCreatedSuccessfully extends MoneroAccountEditOrCreateState {} + +class AccountCreationFailure extends MoneroAccountEditOrCreateState { + AccountCreationFailure({@required this.error}); + + final String error; +} \ No newline at end of file diff --git a/lib/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart b/lib/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart new file mode 100644 index 000000000..151a966ac --- /dev/null +++ b/lib/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart @@ -0,0 +1,45 @@ +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/monero/monero_account_list.dart'; +import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart'; +import 'package:cake_wallet/view_model/monero_account_list/monero_account_edit_or_create_state.dart'; + +part 'monero_account_edit_or_create_view_model.g.dart'; + +class MoneroAccountEditOrCreateViewModel = MoneroAccountEditOrCreateViewModelBase + with _$MoneroAccountEditOrCreateViewModel; + +abstract class MoneroAccountEditOrCreateViewModelBase with Store { + MoneroAccountEditOrCreateViewModelBase(this._moneroAccountList, + {AccountListItem accountListItem}) + : state = InitialAccountCreationState(), + isEdit = accountListItem != null, + _accountListItem = accountListItem; + + final bool isEdit; + + @observable + MoneroAccountEditOrCreateState state; + + @observable + String label; + + final MoneroAccountList _moneroAccountList; + final AccountListItem _accountListItem; + + Future save() async { + try { + state = AccountIsCreating(); + + if (_accountListItem != null) { + await _moneroAccountList.setLabelAccount( + accountIndex: _accountListItem.id, label: label); + } else { + await _moneroAccountList.addAccount(label: label); + } + + state = AccountCreatedSuccessfully(); + } catch (e) { + state = AccountCreationFailure(error: e.toString()); + } + } +} diff --git a/lib/view_model/monero_account_list/monero_account_list_view_model.dart b/lib/view_model/monero_account_list/monero_account_list_view_model.dart new file mode 100644 index 000000000..e7ad6e4e9 --- /dev/null +++ b/lib/view_model/monero_account_list/monero_account_list_view_model.dart @@ -0,0 +1,26 @@ +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/src/domain/monero/account.dart'; +import 'package:cake_wallet/monero/monero_wallet.dart'; +import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart'; + +part 'monero_account_list_view_model.g.dart'; + +class MoneroAccountListViewModel = MoneroAccountListViewModelBase + with _$MoneroAccountListViewModel; + +abstract class MoneroAccountListViewModelBase with Store { + MoneroAccountListViewModelBase(this._moneroWallet); + + @computed + List get accounts => _moneroWallet.accountList.accounts + .map((acc) => AccountListItem( + label: acc.label, + id: acc.id, + isSelected: acc.id == _moneroWallet.account.id)) + .toList(); + + final MoneroWallet _moneroWallet; + + void select(AccountListItem item) => + _moneroWallet.account = Account(id: item.id, label: item.label); +} diff --git a/lib/view_model/node_list/node_create_or_edit_view_model.dart b/lib/view_model/node_list/node_create_or_edit_view_model.dart new file mode 100644 index 000000000..bf219e26b --- /dev/null +++ b/lib/view_model/node_list/node_create_or_edit_view_model.dart @@ -0,0 +1,71 @@ +import 'package:hive/hive.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/core/wallet_base.dart'; +import 'package:cake_wallet/src/domain/common/node.dart'; +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; +import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model_state.dart'; + +part 'node_create_or_edit_view_model.g.dart'; + +class NodeCreateOrEditViewModel = NodeCreateOrEditViewModelBase + with _$NodeCreateOrEditViewModel; + +abstract class NodeCreateOrEditViewModelBase with Store { + NodeCreateOrEditViewModelBase(this._nodeSource, this._wallet) + : state = InitialNodeCreateOrEditViewModelState(); + + @observable + NodeCreateOrEditViewModelState state; + + @observable + String address; + + @observable + String port; + + @observable + String login; + + @observable + String password; + + @computed + bool get isReady => + (address?.isNotEmpty ?? false) && (port?.isNotEmpty ?? false); + + bool get hasAuthCredentials => _wallet.type == WalletType.monero; + + String get uri { + var uri = address; + + if (port != null && port.isNotEmpty) { + uri += ':' + port; + } + + return uri; + } + + final WalletBase _wallet; + final Box _nodeSource; + + @action + void reset() { + address = ''; + port = ''; + login = ''; + password = ''; + } + + @action + Future save() async { + try { + state = NodeIsCreating(); + final node = + Node(uri: uri, type: _wallet.type, login: login, password: password); + await _nodeSource.add(node); + state = NodeCreatedSuccessfully(); + } catch (e) { + state = NodeCreateOrEditViewModelFailure(e.toString()); + } + } +} diff --git a/lib/view_model/node_list/node_create_or_edit_view_model_state.dart b/lib/view_model/node_list/node_create_or_edit_view_model_state.dart new file mode 100644 index 000000000..811b8bf8e --- /dev/null +++ b/lib/view_model/node_list/node_create_or_edit_view_model_state.dart @@ -0,0 +1,14 @@ +abstract class NodeCreateOrEditViewModelState {} + +class InitialNodeCreateOrEditViewModelState + extends NodeCreateOrEditViewModelState {} + +class NodeIsCreating extends NodeCreateOrEditViewModelState {} + +class NodeCreatedSuccessfully extends NodeCreateOrEditViewModelState {} + +class NodeCreateOrEditViewModelFailure extends NodeCreateOrEditViewModelState { + NodeCreateOrEditViewModelFailure(this.error); + + final String error; +} diff --git a/lib/view_model/node_list/node_list_view_model.dart b/lib/view_model/node_list/node_list_view_model.dart new file mode 100644 index 000000000..6821ae574 --- /dev/null +++ b/lib/view_model/node_list/node_list_view_model.dart @@ -0,0 +1,26 @@ +import 'package:hive/hive.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/core/wallet_base.dart'; +import 'package:cake_wallet/src/domain/common/node.dart'; +import 'package:cake_wallet/src/domain/common/node_list.dart'; +import 'package:cake_wallet/store/node_list_store.dart'; + +part 'node_list_view_model.g.dart'; + +class NodeListViewModel = NodeListViewModelBase with _$NodeListViewModel; + +abstract class NodeListViewModelBase with Store { + NodeListViewModelBase(this._nodeListStore, this._nodeSource, this._wallet); + + @computed + ObservableList get nodes => ObservableList.of( + _nodeListStore.nodes.where((node) => node.type == _wallet.type)); + + final WalletBase _wallet; + final Box _nodeSource; + final NodeListStore _nodeListStore; + + Future reset() async => await resetToDefault(_nodeSource); + + Future delete(Node node) async => node.delete(); +} diff --git a/lib/view_model/send_view_model.dart b/lib/view_model/send_view_model.dart new file mode 100644 index 000000000..eada27683 --- /dev/null +++ b/lib/view_model/send_view_model.dart @@ -0,0 +1,94 @@ +import 'package:cake_wallet/core/address_validator.dart'; +import 'package:cake_wallet/core/amount_validator.dart'; +import 'package:cake_wallet/core/validator.dart'; +import 'package:cake_wallet/core/wallet_base.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart'; +import 'package:cake_wallet/monero/monero_wallet.dart'; +import 'package:cake_wallet/src/domain/common/balance.dart'; +import 'package:cake_wallet/src/domain/common/calculate_estimated_fee.dart'; +import 'package:cake_wallet/src/domain/common/crypto_currency.dart'; +import 'package:cake_wallet/src/domain/common/fiat_currency.dart'; +import 'package:cake_wallet/src/domain/common/transaction_priority.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:flutter/foundation.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/monero/monero_wallet_service.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_wallet_creation_credentials.dart'; +import 'package:cake_wallet/core/wallet_creation_service.dart'; +import 'package:cake_wallet/core/wallet_credentials.dart'; +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; +import 'package:cake_wallet/view_model/wallet_creation_vm.dart'; + +part 'send_view_model.g.dart'; + +abstract class SendViewModelState {} + +class InitialSendViewModelState extends SendViewModelState {} + +class TransactionIsCreating extends SendViewModelState {} + +class TransactionCreatedSuccessfully extends SendViewModelState {} + +class TransactionCommitting extends SendViewModelState {} + +class TransactionCommitted extends SendViewModelState {} + +class SendingFailed extends SendViewModelState { + SendingFailed({@required this.error}); + + String error; +} + +class SendViewModel = SendViewModelBase with _$SendViewModel; + +abstract class SendViewModelBase with Store { + SendViewModelBase(this._wallet, this._settingsStore) + : state = InitialSendViewModelState(); + + @observable + SendViewModelState state; + + @observable + String fiatAmount; + + @observable + String cryptoAmount; + + @observable + String address; + + FiatCurrency get fiat => _settingsStore.fiatCurrency; + + TransactionPriority get transactionPriority => + _settingsStore.transactionPriority; + + double get estimatedFee => + calculateEstimatedFee(priority: transactionPriority); + + CryptoCurrency get currency => _wallet.currency; + + Validator get amountValidator => AmountValidator(type: _wallet.type); + + Validator get addressValidator => AddressValidator(type: _wallet.currency); + + @computed + String get balance { + if (_wallet is MoneroWallet) { + _wallet.balance.formattedUnlockedBalance; + } + + if (_wallet is BitcoinWallet) { + _wallet.balance.confirmedFormatted; + } + + return '0.0'; + } + + WalletBase _wallet; + + SettingsStore _settingsStore; + + Future createTransaction() async {} + + Future commitTransaction() async {} +} diff --git a/lib/view_model/settings/link_list_item.dart b/lib/view_model/settings/link_list_item.dart new file mode 100644 index 000000000..bf282d2ca --- /dev/null +++ b/lib/view_model/settings/link_list_item.dart @@ -0,0 +1,15 @@ +import 'package:flutter/foundation.dart'; +import 'package:cake_wallet/view_model/settings/settings_list_item.dart'; + +class LinkListItem extends SettingsListItem { + LinkListItem( + {@required String title, + @required this.link, + @required this.linkTitle, + this.icon}) + : super(title); + + final String icon; + final String link; + final String linkTitle; +} \ No newline at end of file diff --git a/lib/view_model/settings/picker_list_item.dart b/lib/view_model/settings/picker_list_item.dart new file mode 100644 index 000000000..532f90a0d --- /dev/null +++ b/lib/view_model/settings/picker_list_item.dart @@ -0,0 +1,13 @@ +import 'package:flutter/foundation.dart'; +import 'package:cake_wallet/view_model/settings/settings_list_item.dart'; + +class PickerListItem extends SettingsListItem { + PickerListItem( + {@required String title, + @required this.selectedItem, + @required this.items}) + : super(title); + + final ItemType Function() selectedItem; + final List items; +} diff --git a/lib/view_model/settings/regular_list_item.dart b/lib/view_model/settings/regular_list_item.dart new file mode 100644 index 000000000..49fcebf56 --- /dev/null +++ b/lib/view_model/settings/regular_list_item.dart @@ -0,0 +1,8 @@ +import 'package:flutter/material.dart'; +import 'package:cake_wallet/view_model/settings/settings_list_item.dart'; + +class RegularListItem extends SettingsListItem { + RegularListItem({@required String title, this.handler}) : super(title); + + final void Function(BuildContext context) handler; +} \ No newline at end of file diff --git a/lib/view_model/settings/settings_list_item.dart b/lib/view_model/settings/settings_list_item.dart new file mode 100644 index 000000000..d0d3ec4d2 --- /dev/null +++ b/lib/view_model/settings/settings_list_item.dart @@ -0,0 +1,5 @@ +abstract class SettingsListItem { + SettingsListItem(this.title); + + final String title; +} diff --git a/lib/view_model/settings/settings_view_model.dart b/lib/view_model/settings/settings_view_model.dart new file mode 100644 index 000000000..892b8493e --- /dev/null +++ b/lib/view_model/settings/settings_view_model.dart @@ -0,0 +1,185 @@ +import 'package:flutter/cupertino.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/src/domain/common/balance_display_mode.dart'; +import 'package:cake_wallet/src/domain/common/fiat_currency.dart'; +import 'package:cake_wallet/src/domain/common/node.dart'; +import 'package:cake_wallet/src/domain/common/transaction_priority.dart'; +import 'package:cake_wallet/src/stores/action_list/action_list_display_mode.dart'; +import 'package:cake_wallet/src/screens/auth/auth_page.dart'; +import 'package:cake_wallet/view_model/settings/link_list_item.dart'; +import 'package:cake_wallet/view_model/settings/picker_list_item.dart'; +import 'package:cake_wallet/view_model/settings/regular_list_item.dart'; +import 'package:cake_wallet/view_model/settings/settings_list_item.dart'; +import 'package:cake_wallet/view_model/settings/switcher_list_item.dart'; + +part 'settings_view_model.g.dart'; + +class SettingsViewModel = SettingsViewModelBase with _$SettingsViewModel; + +abstract class SettingsViewModelBase with Store { + SettingsViewModelBase(this._settingsStore) : itemHeaders = {} { + sections = [ + [ + PickerListItem( + title: S.current.settings_display_balance_as, + items: BalanceDisplayMode.all, + selectedItem: () => balanceDisplayMode), + PickerListItem( + title: S.current.settings_currency, + items: FiatCurrency.all, + selectedItem: () => fiatCurrency), + PickerListItem( + title: S.current.settings_fee_priority, + items: TransactionPriority.all, + selectedItem: () => transactionPriority), + SwitcherListItem( + title: S.current.settings_save_recipient_address, + value: () => shouldSaveRecipientAddress, + onValueChange: (bool value) => shouldSaveRecipientAddress = value) + ], + [ + RegularListItem( + title: S.current.settings_change_pin, + handler: (BuildContext context) { + Navigator.of(context).pushNamed(Routes.auth, + arguments: (bool isAuthenticatedSuccessfully, + AuthPageState auth) => + isAuthenticatedSuccessfully + ? Navigator.of(context).popAndPushNamed( + Routes.setupPin, + arguments: + (BuildContext setupPinContext, String _) => + Navigator.of(context).pop()) + : null); + }), + RegularListItem( + title: S.current.settings_change_language, + handler: (BuildContext context) => + Navigator.of(context).pushNamed(Routes.changeLanguage), + ), + SwitcherListItem( + title: S.current.settings_allow_biometrical_authentication, + value: () => allowBiometricalAuthentication, + onValueChange: (bool value) => + allowBiometricalAuthentication = value), + SwitcherListItem( + title: S.current.settings_dark_mode, + value: () => _settingsStore.isDarkTheme, + onValueChange: (bool value) { + // FIXME: Implement me + }) + ], + [ + LinkListItem( + title: 'Email', + linkTitle: 'support@cakewallet.com', + link: 'mailto:support@cakewallet.com'), + LinkListItem( + title: 'Telegram', + icon: 'assets/images/Telegram.png', + linkTitle: 'Cake_Wallet', + link: 'https:t.me/cakewallet_bot'), + LinkListItem( + title: 'Twitter', + icon: 'assets/images/Twitter.png', + linkTitle: '@CakeWalletXMR', + link: 'https:twitter.com/CakewalletXMR'), + LinkListItem( + title: 'ChangeNow', + icon: 'assets/images/change_now.png', + linkTitle: 'support@changenow.io', + link: 'mailto:support@changenow.io'), + LinkListItem( + title: 'Morph', + icon: 'assets/images/morph_icon.png', + linkTitle: 'support@morphtoken.com', + link: 'mailto:support@morphtoken.com'), + LinkListItem( + title: 'XMR.to', + icon: 'assets/images/xmr_btc.png', + linkTitle: 'support@xmr.to', + link: 'mailto:support@xmr.to'), + RegularListItem( + title: S.current.settings_terms_and_conditions, + handler: (BuildContext context) => + Navigator.of(context).pushNamed(Routes.disclaimer), + ) + ] + ]; + } + + @computed + Node get node => _settingsStore.node; + + @computed + FiatCurrency get fiatCurrency => _settingsStore.fiatCurrency; + + @computed + ObservableList get actionlistDisplayMode => + _settingsStore.actionlistDisplayMode; + + @computed + TransactionPriority get transactionPriority => + _settingsStore.transactionPriority; + + @computed + BalanceDisplayMode get balanceDisplayMode => + _settingsStore.balanceDisplayMode; + + @computed + bool get shouldSaveRecipientAddress => + _settingsStore.shouldSaveRecipientAddress; + + @action + set shouldSaveRecipientAddress(bool value) => + _settingsStore.shouldSaveRecipientAddress = value; + + @computed + bool get allowBiometricalAuthentication => + _settingsStore.allowBiometricalAuthentication; + + @action + set allowBiometricalAuthentication(bool value) => + _settingsStore.allowBiometricalAuthentication = value; + +// @observable +// bool isDarkTheme; +// +// @observable +// int defaultPinLength; + +// @observable + final Map itemHeaders; + List> sections; + final SettingsStore _settingsStore; + + @action + void toggleTransactionsDisplay() => + actionlistDisplayMode.contains(ActionListDisplayMode.transactions) + ? _hideTransaction() + : _showTransaction(); + + @action + void toggleTradesDisplay() => + actionlistDisplayMode.contains(ActionListDisplayMode.trades) + ? _hideTrades() + : _showTrades(); + + @action + void _hideTransaction() => + actionlistDisplayMode.remove(ActionListDisplayMode.transactions); + + @action + void _hideTrades() => + actionlistDisplayMode.remove(ActionListDisplayMode.trades); + + @action + void _showTransaction() => + actionlistDisplayMode.add(ActionListDisplayMode.transactions); + + @action + void _showTrades() => actionlistDisplayMode.add(ActionListDisplayMode.trades); +} diff --git a/lib/view_model/settings/switcher_list_item.dart b/lib/view_model/settings/switcher_list_item.dart new file mode 100644 index 000000000..cf181302d --- /dev/null +++ b/lib/view_model/settings/switcher_list_item.dart @@ -0,0 +1,13 @@ +import 'package:flutter/foundation.dart'; +import 'package:cake_wallet/view_model/settings/settings_list_item.dart'; + +class SwitcherListItem extends SettingsListItem { + SwitcherListItem( + {@required String title, + @required this.value, + @required this.onValueChange}) + : super(title); + + final bool Function() value; + final void Function(bool value) onValueChange; +} \ No newline at end of file diff --git a/lib/view_model/address_list/account_list_header.dart b/lib/view_model/wallet_address_list/wallet_account_list_header.dart similarity index 51% rename from lib/view_model/address_list/account_list_header.dart rename to lib/view_model/wallet_address_list/wallet_account_list_header.dart index 46e86ef86..197c020b4 100644 --- a/lib/view_model/address_list/account_list_header.dart +++ b/lib/view_model/wallet_address_list/wallet_account_list_header.dart @@ -1,3 +1,3 @@ import 'package:cake_wallet/utils/list_item.dart'; -class AccountListHeader extends ListItem {} \ No newline at end of file +class WalletAccountListHeader extends ListItem {} \ No newline at end of file diff --git a/lib/view_model/address_list/address_edit_or_create_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart similarity index 85% rename from lib/view_model/address_list/address_edit_or_create_view_model.dart rename to lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart index 25e2095b1..1db6e6822 100644 --- a/lib/view_model/address_list/address_edit_or_create_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart @@ -4,10 +4,10 @@ import 'package:cake_wallet/core/wallet_base.dart'; import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart'; import 'package:cake_wallet/monero/monero_wallet.dart'; -part 'address_edit_or_create_view_model.g.dart'; +part 'wallet_address_edit_or_create_view_model.g.dart'; -class AddressEditOrCreateViewModel = AddressEditOrCreateViewModelBase - with _$AddressEditOrCreateViewModel; +class WalletAddressEditOrCreateViewModel = WalletAddressEditOrCreateViewModelBase + with _$WalletAddressEditOrCreateViewModel; abstract class AddressEditOrCreateState {} @@ -23,8 +23,8 @@ class AddressEditOrCreateStateFailure extends AddressEditOrCreateState { String error; } -abstract class AddressEditOrCreateViewModelBase with Store { - AddressEditOrCreateViewModelBase({@required WalletBase wallet, dynamic item}) +abstract class WalletAddressEditOrCreateViewModelBase with Store { + WalletAddressEditOrCreateViewModelBase({@required WalletBase wallet, dynamic item}) : isEdit = item != null, state = AddressEditOrCreateStateInitial(), label = item?.name as String, diff --git a/lib/view_model/address_list/address_list_header.dart b/lib/view_model/wallet_address_list/wallet_address_list_header.dart similarity index 51% rename from lib/view_model/address_list/address_list_header.dart rename to lib/view_model/wallet_address_list/wallet_address_list_header.dart index f52a7715f..0f19166b7 100644 --- a/lib/view_model/address_list/address_list_header.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_header.dart @@ -1,3 +1,3 @@ import 'package:cake_wallet/utils/list_item.dart'; -class AddressListHeader extends ListItem {} \ No newline at end of file +class WalletAddressListHeader extends ListItem {} \ No newline at end of file diff --git a/lib/view_model/address_list/address_list_item.dart b/lib/view_model/wallet_address_list/wallet_address_list_item.dart similarity index 64% rename from lib/view_model/address_list/address_list_item.dart rename to lib/view_model/wallet_address_list/wallet_address_list_item.dart index a7c232c5c..c6d8915ab 100644 --- a/lib/view_model/address_list/address_list_item.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_item.dart @@ -1,8 +1,8 @@ import 'package:flutter/foundation.dart'; import 'package:cake_wallet/utils/list_item.dart'; -class AddressListItem extends ListItem { - const AddressListItem({@required this.address, this.name, this.id}) +class WalletAddressListItem extends ListItem { + const WalletAddressListItem({@required this.address, this.name, this.id}) : super(); final int id; diff --git a/lib/view_model/address_list/address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart similarity index 72% rename from lib/view_model/address_list/address_list_view_model.dart rename to lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index 32dbeb7a2..e36dc18cb 100644 --- a/lib/view_model/address_list/address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -4,14 +4,14 @@ import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart'; import 'package:cake_wallet/monero/monero_wallet.dart'; import 'package:cake_wallet/core/wallet_base.dart'; import 'package:cake_wallet/utils/list_item.dart'; -import 'package:cake_wallet/view_model/address_list/account_list_header.dart'; -import 'package:cake_wallet/view_model/address_list/address_list_header.dart'; -import 'package:cake_wallet/view_model/address_list/address_list_item.dart'; +import 'package:cake_wallet/view_model/wallet_address_list/wallet_account_list_header.dart'; +import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_header.dart'; +import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; -part 'address_list_view_model.g.dart'; +part 'wallet_address_list_view_model.g.dart'; -class AddressListViewModel = AddressListViewModelBase - with _$AddressListViewModel; +class WalletAddressListViewModel = WalletAddressListViewModelBase + with _$WalletAddressListViewModel; abstract class PaymentURI { PaymentURI({this.amount, this.address}); @@ -52,8 +52,8 @@ class BitcoinURI extends PaymentURI { } } -abstract class AddressListViewModelBase with Store { - AddressListViewModelBase({@required WalletBase wallet}) { +abstract class WalletAddressListViewModelBase with Store { + WalletAddressListViewModelBase({@required WalletBase wallet}) { hasAccounts = _wallet is MoneroWallet; _wallet = wallet; _init(); @@ -63,7 +63,7 @@ abstract class AddressListViewModelBase with Store { String amount; @computed - AddressListItem get address => AddressListItem(address: _wallet.address); + WalletAddressListItem get address => WalletAddressListItem(address: _wallet.address); @computed PaymentURI get uri { @@ -89,7 +89,7 @@ abstract class AddressListViewModelBase with Store { if (wallet is MoneroWallet) { addressList.addAll(wallet.subaddressList.subaddresses.map((subaddress) => - AddressListItem( + WalletAddressListItem( id: subaddress.id, name: subaddress.label, address: subaddress.address))); @@ -97,14 +97,14 @@ abstract class AddressListViewModelBase with Store { if (wallet is BitcoinWallet) { final bitcoinAddresses = wallet.addresses.map( - (addr) => AddressListItem(name: addr.label, address: addr.address)); + (addr) => WalletAddressListItem(name: addr.label, address: addr.address)); addressList.addAll(bitcoinAddresses); } return addressList; } - set address(AddressListItem address) => _wallet.address = address.address; + set address(WalletAddressListItem address) => _wallet.address = address.address; bool hasAccounts; @@ -127,9 +127,9 @@ abstract class AddressListViewModelBase with Store { _baseItems = []; if (_wallet is MoneroWallet) { - _baseItems.add(AccountListHeader()); + _baseItems.add(WalletAccountListHeader()); } - _baseItems.add(AddressListHeader()); + _baseItems.add(WalletAddressListHeader()); } } diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index 6a77de6f3..a105f388e 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -1,4 +1,6 @@ +import 'package:cake_wallet/src/domain/common/wallet_info.dart'; import 'package:flutter/foundation.dart'; +import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/core/wallet_credentials.dart'; import 'package:cake_wallet/src/domain/common/wallet_type.dart'; @@ -9,7 +11,8 @@ part 'wallet_creation_vm.g.dart'; class WalletCreationVM = WalletCreationVMBase with _$WalletCreationVM; abstract class WalletCreationVMBase with Store { - WalletCreationVMBase({@required this.type}) { + WalletCreationVMBase(this._walletInfoSource, + {@required this.type, @required this.isRecovery}) { state = InitialWalletCreationState(); name = ''; } @@ -20,12 +23,24 @@ abstract class WalletCreationVMBase with Store { @observable WalletCreationState state; - WalletType type; + final WalletType type; + + final bool isRecovery; + + Box _walletInfoSource; Future create({dynamic options}) async { try { state = WalletCreating(); await process(getCredentials(options)); + final id = walletTypeToString(type).toLowerCase() + '_' + name; + final walletInfo = WalletInfo( + id: id, + name: name, + type: type, + isRecovery: isRecovery, + restoreHeight: 0); + await _walletInfoSource.add(walletInfo); state = WalletCreatedSuccessfully(); } catch (e) { state = WalletCreationFailure(error: e.toString()); diff --git a/lib/view_model/wallet_keys_view_model.dart b/lib/view_model/wallet_keys_view_model.dart new file mode 100644 index 000000000..a9fde286c --- /dev/null +++ b/lib/view_model/wallet_keys_view_model.dart @@ -0,0 +1,42 @@ +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/core/wallet_base.dart'; +import 'package:cake_wallet/monero/monero_wallet.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart'; +import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart'; + +part 'wallet_keys_view_model.g.dart'; + +class WalletKeysViewModel = WalletKeysViewModelBase with _$WalletKeysViewModel; + +abstract class WalletKeysViewModelBase with Store { + WalletKeysViewModelBase(WalletBase wallet) + : items = ObservableList() { + if (wallet is MoneroWallet) { + final keys = wallet.keys; + + items.addAll([ + StandartListItem( + title: S.current.spend_key_public, value: keys.publicSpendKey), + StandartListItem( + title: S.current.spend_key_private, value: keys.privateSpendKey), + StandartListItem( + title: S.current.view_key_public, value: keys.publicViewKey), + StandartListItem( + title: S.current.view_key_private, value: keys.privateViewKey), + ]); + } + + if (wallet is BitcoinWallet) { + final keys = wallet.keys; + + items.addAll([ + StandartListItem(title: 'WIF', value: keys.wif), + StandartListItem(title: 'Public key', value: keys.publicKey), + StandartListItem(title: 'Private key', value: keys.privateKey) + ]); + } + } + + final ObservableList items; +} diff --git a/lib/view_model/wallet_list/wallet_list_item.dart b/lib/view_model/wallet_list/wallet_list_item.dart new file mode 100644 index 000000000..669a2be79 --- /dev/null +++ b/lib/view_model/wallet_list/wallet_list_item.dart @@ -0,0 +1,11 @@ +import 'package:flutter/foundation.dart'; +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; + +class WalletListItem { + const WalletListItem( + {@required this.name, @required this.type, this.isCurrent = false}); + + final String name; + final WalletType type; + final bool isCurrent; +} diff --git a/lib/view_model/wallet_list/wallet_list_view_model.dart b/lib/view_model/wallet_list/wallet_list_view_model.dart new file mode 100644 index 000000000..e84959e91 --- /dev/null +++ b/lib/view_model/wallet_list/wallet_list_view_model.dart @@ -0,0 +1,55 @@ +import 'package:hive/hive.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/core/key_service.dart'; +import 'package:cake_wallet/core/wallet_service.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_wallet_service.dart'; +import 'package:cake_wallet/monero/monero_wallet_service.dart'; +import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; +import 'package:cake_wallet/src/domain/common/wallet_info.dart'; + +part 'wallet_list_view_model.g.dart'; + +class WalletListViewModel = WalletListViewModelBase with _$WalletListViewModel; + +abstract class WalletListViewModelBase with Store { + WalletListViewModelBase( + this._walletInfoSource, this._appStore, this._keyService) { + wallets = ObservableList(); + wallets.addAll(_walletInfoSource.values.map((info) => WalletListItem( + name: info.name, + type: info.type, + isCurrent: info.name == _appStore.wallet.name && + info.type == _appStore.wallet.type))); + } + + @observable + ObservableList wallets; + + final AppStore _appStore; + final Box _walletInfoSource; + final KeyService _keyService; + + @action + Future loadWallet(WalletListItem wallet) async { + final password = + await _keyService.getWalletPassword(walletName: wallet.name); + final walletService = _getWalletService(wallet.type); + _appStore.wallet = await walletService.openWallet(wallet.name, password); + } + + @action + Future remove(WalletListItem wallet) async {} + + WalletService _getWalletService(WalletType type) { + switch (type) { + case WalletType.monero: + return MoneroWalletService(); + case WalletType.bitcoin: + return BitcoinWalletService(); + default: + return null; + } + } +} diff --git a/lib/view_model/wallet_new_vm.dart b/lib/view_model/wallet_new_vm.dart index 1e0bba29f..feccb0848 100644 --- a/lib/view_model/wallet_new_vm.dart +++ b/lib/view_model/wallet_new_vm.dart @@ -1,9 +1,11 @@ import 'package:flutter/foundation.dart'; +import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/monero/monero_wallet_service.dart'; import 'package:cake_wallet/bitcoin/bitcoin_wallet_creation_credentials.dart'; import 'package:cake_wallet/core/wallet_creation_service.dart'; import 'package:cake_wallet/core/wallet_credentials.dart'; +import 'package:cake_wallet/src/domain/common/wallet_info.dart'; import 'package:cake_wallet/src/domain/common/wallet_type.dart'; import 'package:cake_wallet/view_model/wallet_creation_vm.dart'; @@ -12,9 +14,9 @@ part 'wallet_new_vm.g.dart'; class WalletNewVM = WalletNewVMBase with _$WalletNewVM; abstract class WalletNewVMBase extends WalletCreationVM with Store { - WalletNewVMBase(this._walletCreationService, {@required WalletType type}) + WalletNewVMBase(this._walletCreationService, Box walletInfoSource, {@required WalletType type}) : selectedMnemonicLanguage = '', - super(type: type); + super(walletInfoSource, type: type, isRecovery: false); @observable String selectedMnemonicLanguage; diff --git a/lib/view_model/wallet_restoration_from_seed_vm.dart b/lib/view_model/wallet_restoration_from_seed_vm.dart index 107f0ecec..ac9495bbb 100644 --- a/lib/view_model/wallet_restoration_from_seed_vm.dart +++ b/lib/view_model/wallet_restoration_from_seed_vm.dart @@ -1,4 +1,5 @@ import 'package:flutter/foundation.dart'; +import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/monero/monero_wallet_service.dart'; import 'package:cake_wallet/bitcoin/bitcoin_wallet_creation_credentials.dart'; @@ -7,6 +8,7 @@ import 'package:cake_wallet/core/wallet_creation_service.dart'; import 'package:cake_wallet/core/wallet_credentials.dart'; import 'package:cake_wallet/src/domain/common/wallet_type.dart'; import 'package:cake_wallet/view_model/wallet_creation_vm.dart'; +import 'package:cake_wallet/src/domain/common/wallet_info.dart'; part 'wallet_restoration_from_seed_vm.g.dart'; @@ -15,9 +17,9 @@ class WalletRestorationFromSeedVM = WalletRestorationFromSeedVMBase abstract class WalletRestorationFromSeedVMBase extends WalletCreationVM with Store { - WalletRestorationFromSeedVMBase(this._walletCreationService, + WalletRestorationFromSeedVMBase(this._walletCreationService, Box walletInfoSource, {@required WalletType type, @required this.language, this.seed}) - : super(type: type); + : super(walletInfoSource, type: type, isRecovery: true); @observable String seed; diff --git a/lib/view_model/wallet_seed_view_model.dart b/lib/view_model/wallet_seed_view_model.dart new file mode 100644 index 000000000..d82b82e1e --- /dev/null +++ b/lib/view_model/wallet_seed_view_model.dart @@ -0,0 +1,18 @@ +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/core/wallet_base.dart'; + +part 'wallet_seed_view_model.g.dart'; + +class WalletSeedViewModel = WalletSeedViewModelBase with _$WalletSeedViewModel; + +abstract class WalletSeedViewModelBase with Store { + WalletSeedViewModelBase(WalletBase wallet) + : name = wallet.name, + seed = wallet.seed; + + @observable + String name; + + @observable + String seed; +} diff --git a/pubspec.lock b/pubspec.lock index 8e2af0a3a..7f3229605 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -280,7 +280,7 @@ packages: name: encrypt url: "https://pub.dartlang.org" source: hosted - version: "4.0.1" + version: "4.0.2" esys_flutter_share: dependency: "direct main" description: @@ -295,6 +295,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.1.3" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "5.2.0" fixnum: dependency: transitive description: @@ -475,7 +482,7 @@ packages: name: js url: "https://pub.dartlang.org" source: hosted - version: "0.6.1+1" + version: "0.6.2" json_annotation: dependency: transitive description: @@ -566,7 +573,7 @@ packages: name: package_info url: "https://pub.dartlang.org" source: hosted - version: "0.4.0+18" + version: "0.4.1" package_resolver: dependency: transitive description: @@ -608,7 +615,14 @@ packages: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "1.6.9" + version: "1.6.11" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.1+1" path_provider_macos: dependency: transitive description: @@ -672,6 +686,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.4.0" + process: + dependency: transitive + description: + name: process + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.13" protobuf: dependency: transitive description: @@ -706,7 +727,7 @@ packages: name: qr url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.0" quiver: dependency: transitive description: @@ -741,7 +762,7 @@ packages: name: shared_preferences_macos url: "https://pub.dartlang.org" source: hosted - version: "0.0.1+9" + version: "0.0.1+10" shared_preferences_platform_interface: dependency: transitive description: @@ -762,7 +783,7 @@ packages: name: shelf url: "https://pub.dartlang.org" source: hosted - version: "0.7.5" + version: "0.7.7" shelf_web_socket: dependency: transitive description: @@ -858,7 +879,7 @@ packages: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "5.4.10" + version: "5.4.11" url_launcher_macos: dependency: transitive description: @@ -908,6 +929,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.0" xml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 4d2bcf468..7e3b3639d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -59,7 +59,7 @@ dependencies: basic_utils: ^1.0.8 bitcoin_flutter: ^2.0.0 get_it: ^4.0.2 - + dev_dependencies: flutter_test: sdk: flutter @@ -74,7 +74,7 @@ dependency_overrides: dartx: ^0.3.0 flutter_icons: - image_path: "assets/images/app_logo.png" + image_path: "assets/images/app_logo.png" android: true ios: true @@ -92,6 +92,7 @@ flutter: assets: - assets/images/ - assets/node_list.yml + - assets/electrum_server_list.yml - assets/text/ - assets/faq/ @@ -101,6 +102,11 @@ flutter: - asset: assets/fonts/Lato-Regular.ttf - asset: assets/fonts/Lato-Bold.ttf - asset: assets/fonts/Lato-Semibold.ttf + - family: Montserrat + fonts: + - asset: assets/fonts/Montserrat-Regular.ttf + - asset: assets/fonts/Montserrat-Bold.ttf + - asset: assets/fonts/Montserrat-SemiBold.ttf # To add assets to your application, add an assets section, like this: diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 67b950f3f..d5734da89 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -374,5 +374,8 @@ "openalias_alert_content" : "Sie senden Geld an\n${recipient_name}", "card_address" : "Adresse:", - "buy" : "Kaufen" + "buy" : "Kaufen", + + "placeholder_transactions" : "Ihre Transaktionen werden hier angezeigt", + "placeholder_contacts" : "Ihre Kontakte werden hier angezeigt" } \ No newline at end of file diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 1331b2590..b78aad690 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -374,5 +374,8 @@ "openalias_alert_content" : "You will be sending funds to\n${recipient_name}", "card_address" : "Address:", - "buy" : "Buy" + "buy" : "Buy", + + "placeholder_transactions" : "Your transactions will be displayed here", + "placeholder_contacts" : "Your contacts will be displayed here" } \ No newline at end of file diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index ad98663f5..47df624e1 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -374,5 +374,8 @@ "openalias_alert_content" : "Enviará fondos a\n${recipient_name}", "card_address" : "Dirección:", - "buy" : "Comprar" + "buy" : "Comprar", + + "placeholder_transactions" : "Sus transacciones se mostrarán aquí", + "placeholder_contacts" : "Tus contactos se mostrarán aquí" } \ No newline at end of file diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 26120d830..309918d7f 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -374,5 +374,8 @@ "openalias_alert_content" : "आपको धनराशि भेजी जाएगी\n${recipient_name}", "card_address" : "पता:", - "buy" : "खरीदें" + "buy" : "खरीदें", + + "placeholder_transactions" : "आपके लेनदेन यहां प्रदर्शित होंगे", + "placeholder_contacts" : "आपके संपर्क यहां प्रदर्शित होंगे" } \ No newline at end of file diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 235bd492a..91dc1e5ab 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -374,5 +374,8 @@ "openalias_alert_content" : "に送金します\n${recipient_name}", "card_address" : "住所:", - "buy" : "購入" + "buy" : "購入", + + "placeholder_transactions" : "あなたの取引はここに表示されます", + "placeholder_contacts" : "連絡先はここに表示されます" } \ No newline at end of file diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index f897addfb..896aae13a 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -374,5 +374,8 @@ "openalias_alert_content" : "당신은에 자금을 보낼 것입니다\n${recipient_name}", "card_address" : "주소:", - "buy" : "구입" + "buy" : "구입", + + "placeholder_transactions" : "거래가 여기에 표시됩니다", + "placeholder_contacts" : "연락처가 여기에 표시됩니다" } \ No newline at end of file diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index eecb0ba7f..f53b1d0b6 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -374,5 +374,8 @@ "openalias_alert_content" : "U stuurt geld naar\n${recipient_name}", "card_address" : "Adres:", - "buy" : "Kopen" + "buy" : "Kopen", + + "placeholder_transactions" : "Uw transacties worden hier weergegeven", + "placeholder_contacts" : "Je contacten worden hier weergegeven" } \ No newline at end of file diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 66d2b69bb..9a8685746 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -374,5 +374,8 @@ "openalias_alert_content" : "Będziesz wysyłać środki na\n${recipient_name}", "card_address" : "Adres:", - "buy" : "Kup" + "buy" : "Kup", + + "placeholder_transactions" : "Twoje transakcje zostaną wyświetlone tutaj", + "placeholder_contacts" : "Twoje kontakty zostaną wyświetlone tutaj" } \ No newline at end of file diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 9e8ac9a0a..9e86dbd3a 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -374,5 +374,8 @@ "openalias_alert_content" : "Você enviará fundos para\n${recipient_name}", "card_address" : "Endereço:", - "buy" : "Comprar" + "buy" : "Comprar", + + "placeholder_transactions" : "Suas transações serão exibidas aqui", + "placeholder_contacts" : "Seus contatos serão exibidos aqui" } diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 73f5637c3..2ac29cd73 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -374,5 +374,8 @@ "openalias_alert_content" : "Вы будете отправлять средства\n${recipient_name}", "card_address" : "Адрес:", - "buy" : "Купить" + "buy" : "Купить", + + "placeholder_transactions" : "Ваши транзакции будут отображаться здесь", + "placeholder_contacts" : "Ваши контакты будут отображаться здесь" } \ No newline at end of file diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 04fec4432..cb22979fe 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -374,5 +374,8 @@ "openalias_alert_content" : "Ви будете відправляти кошти\n${recipient_name}", "card_address" : "Адреса:", - "buy" : "Купити" + "buy" : "Купити", + + "placeholder_transactions" : "Тут відображатимуться ваші транзакції", + "placeholder_contacts" : "Тут будуть показані ваші контакти" } \ No newline at end of file diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index f82ba4d13..026880af4 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -374,5 +374,8 @@ "openalias_alert_content" : "您將匯款至\n${recipient_name}", "card_address" : "地址:", - "buy" : "購買" + "buy" : "購買", + + "placeholder_transactions" : "您的交易將顯示在這裡", + "placeholder_contacts" : "您的聯繫人將顯示在這裡" } \ No newline at end of file