diff --git a/.gitignore b/.gitignore index 4102cec3e..0e6907b4f 100644 --- a/.gitignore +++ b/.gitignore @@ -89,4 +89,6 @@ android/key.properties **/tool/.secrets-prod.json **/lib/.secrets.g.dart -vendor/ \ No newline at end of file +vendor/ + +android/app/.cxx/** diff --git a/assets/electrum_server_list.yml b/assets/electrum_server_list.yml index 4baf56aec..0331f42ea 100644 --- a/assets/electrum_server_list.yml +++ b/assets/electrum_server_list.yml @@ -1,2 +1,4 @@ - - uri: electrum2.hodlister.co:50002 \ No newline at end of file + uri: electrum2.hodlister.co:50002 +- + uri: bitcoin.electrumx.multicoin.co:50002 \ No newline at end of file diff --git a/assets/images/2.0x/back_vector.png b/assets/images/2.0x/back_vector.png new file mode 100644 index 000000000..8d9239aa9 Binary files /dev/null and b/assets/images/2.0x/back_vector.png differ diff --git a/assets/images/2.0x/question_mark.png b/assets/images/2.0x/question_mark.png new file mode 100644 index 000000000..37f5edfcc Binary files /dev/null and b/assets/images/2.0x/question_mark.png differ diff --git a/assets/images/3.0x/back_vector.png b/assets/images/3.0x/back_vector.png new file mode 100644 index 000000000..d01030e5d Binary files /dev/null and b/assets/images/3.0x/back_vector.png differ diff --git a/assets/images/3.0x/question_mark.png b/assets/images/3.0x/question_mark.png new file mode 100644 index 000000000..7b08436f2 Binary files /dev/null and b/assets/images/3.0x/question_mark.png differ diff --git a/assets/images/back_vector.png b/assets/images/back_vector.png new file mode 100644 index 000000000..8717a3463 Binary files /dev/null and b/assets/images/back_vector.png differ diff --git a/assets/images/question_mark.png b/assets/images/question_mark.png new file mode 100644 index 000000000..8a4b4fa70 Binary files /dev/null and b/assets/images/question_mark.png differ diff --git a/ios/Flutter/.last_build_id b/ios/Flutter/.last_build_id new file mode 100644 index 000000000..8e3e3da7e --- /dev/null +++ b/ios/Flutter/.last_build_id @@ -0,0 +1 @@ +a2dce69f54a78f5b00e19850e4b2d402 \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 959c597e1..64e81a4e8 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -44,6 +44,8 @@ PODS: - Flutter - shared_preferences (0.0.1): - Flutter + - shared_preferences_linux (0.0.1): + - Flutter - shared_preferences_macos (0.0.1): - Flutter - shared_preferences_web (0.0.1): @@ -51,6 +53,8 @@ PODS: - SwiftProtobuf (1.8.0) - url_launcher (0.0.1): - Flutter + - url_launcher_linux (0.0.1): + - Flutter - url_launcher_macos (0.0.1): - Flutter - url_launcher_web (0.0.1): @@ -71,9 +75,11 @@ DEPENDENCIES: - path_provider_macos (from `.symlinks/plugins/path_provider_macos/ios`) - share (from `.symlinks/plugins/share/ios`) - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) + - shared_preferences_linux (from `.symlinks/plugins/shared_preferences_linux/ios`) - shared_preferences_macos (from `.symlinks/plugins/shared_preferences_macos/ios`) - shared_preferences_web (from `.symlinks/plugins/shared_preferences_web/ios`) - url_launcher (from `.symlinks/plugins/url_launcher/ios`) + - url_launcher_linux (from `.symlinks/plugins/url_launcher_linux/ios`) - url_launcher_macos (from `.symlinks/plugins/url_launcher_macos/ios`) - url_launcher_web (from `.symlinks/plugins/url_launcher_web/ios`) @@ -111,12 +117,16 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/share/ios" shared_preferences: :path: ".symlinks/plugins/shared_preferences/ios" + shared_preferences_linux: + :path: ".symlinks/plugins/shared_preferences_linux/ios" shared_preferences_macos: :path: ".symlinks/plugins/shared_preferences_macos/ios" shared_preferences_web: :path: ".symlinks/plugins/shared_preferences_web/ios" url_launcher: :path: ".symlinks/plugins/url_launcher/ios" + url_launcher_linux: + :path: ".symlinks/plugins/url_launcher_linux/ios" url_launcher_macos: :path: ".symlinks/plugins/url_launcher_macos/ios" url_launcher_web: @@ -138,10 +148,12 @@ SPEC CHECKSUMS: path_provider_macos: f760a3c5b04357c380e2fddb6f9db6f3015897e0 share: 0b2c3e82132f5888bccca3351c504d0003b3b410 shared_preferences: af6bfa751691cdc24be3045c43ec037377ada40d + shared_preferences_linux: afefbfe8d921e207f01ede8b60373d9e3b566b78 shared_preferences_macos: f3f29b71ccbb56bf40c9dd6396c9acf15e214087 shared_preferences_web: 141cce0c3ed1a1c5bf2a0e44f52d31eeb66e5ea9 SwiftProtobuf: 2cbd9409689b7df170d82a92a33443c8e3e14a70 url_launcher: 6fef411d543ceb26efce54b05a0a40bfd74cbbef + url_launcher_linux: ac237cb7a8058736e4aae38bdbcc748a4b394cc0 url_launcher_macos: fd7894421cd39320dce5f292fc99ea9270b2a313 url_launcher_web: e5527357f037c87560776e36436bf2b0288b965c diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 54901a5a7..cf8308171 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -319,7 +319,6 @@ /* Begin XCBuildConfiguration section */ 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -373,7 +372,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + CURRENT_PROJECT_VERSION = 12; DEVELOPMENT_TEAM = 32J6BB6VUS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -387,7 +386,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 3.1.28; + MARKETING_VERSION = 3.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.cakewallet.cakewallet; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -399,7 +398,6 @@ }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -455,7 +453,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -509,7 +506,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + CURRENT_PROJECT_VERSION = 12; DEVELOPMENT_TEAM = 32J6BB6VUS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -523,7 +520,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 3.1.28; + MARKETING_VERSION = 3.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.cakewallet.cakewallet; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -540,7 +537,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + CURRENT_PROJECT_VERSION = 12; DEVELOPMENT_TEAM = 32J6BB6VUS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -554,7 +551,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 3.1.28; + MARKETING_VERSION = 3.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.cakewallet.cakewallet; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 40f292e53..65000c894 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -19,9 +19,11 @@ <key>CFBundleSignature</key> <string>????</string> <key>CFBundleVersion</key> - <string>$(FLUTTER_BUILD_NUMBER)</string> + <string>$(CURRENT_PROJECT_VERSION)</string> <key>LSRequiresIPhoneOS</key> <true/> + <key>NSCameraUsageDescription</key> + <string>Cake Wallet requires access to your phone’s camera.</string> <key>UILaunchStoryboardName</key> <string>LaunchScreen</string> <key>UIMainStoryboardFile</key> diff --git a/lib/bitcoin/bitcoin_address_record.dart b/lib/bitcoin/bitcoin_address_record.dart index cd2e21495..98cd1c9da 100644 --- a/lib/bitcoin/bitcoin_address_record.dart +++ b/lib/bitcoin/bitcoin_address_record.dart @@ -1,17 +1,19 @@ import 'dart:convert'; class BitcoinAddressRecord { - BitcoinAddressRecord(this.address, {this.label}); + BitcoinAddressRecord(this.address, {this.label, this.index}); factory BitcoinAddressRecord.fromJSON(String jsonSource) { final decoded = json.decode(jsonSource) as Map; return BitcoinAddressRecord(decoded['address'] as String, - label: decoded['label'] as String); + label: decoded['label'] as String, index: decoded['index'] as int); } final String address; + int index; String label; - String toJSON() => json.encode({'label': label, 'address': address}); + String toJSON() => + json.encode({'label': label, 'address': address, 'index': index}); } diff --git a/lib/bitcoin/bitcoin_transaction_credentials.dart b/lib/bitcoin/bitcoin_transaction_credentials.dart index 52f2d5ec6..874da1263 100644 --- a/lib/bitcoin/bitcoin_transaction_credentials.dart +++ b/lib/bitcoin/bitcoin_transaction_credentials.dart @@ -1,6 +1,9 @@ +import 'package:cake_wallet/src/domain/common/transaction_priority.dart'; + class BitcoinTransactionCredentials { - const BitcoinTransactionCredentials(this.address, this.amount); + BitcoinTransactionCredentials(this.address, this.amount, this.priority); final String address; final double amount; + TransactionPriority priority; } diff --git a/lib/bitcoin/bitcoin_transaction_history.dart b/lib/bitcoin/bitcoin_transaction_history.dart index 1a3990bf7..4a18d18c1 100644 --- a/lib/bitcoin/bitcoin_transaction_history.dart +++ b/lib/bitcoin/bitcoin_transaction_history.dart @@ -6,8 +6,6 @@ import 'package:cake_wallet/bitcoin/file.dart'; import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart'; import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart'; import 'package:cake_wallet/bitcoin/electrum.dart'; -import 'package:cake_wallet/src/domain/common/transaction_info.dart'; -import 'package:cake_wallet/src/domain/common/transaction_direction.dart'; part 'bitcoin_transaction_history.g.dart'; @@ -24,100 +22,176 @@ abstract class BitcoinTransactionHistoryBase {this.eclient, String dirPath, @required String password}) : path = '$dirPath/$_transactionsHistoryFileName', _password = password, - _height = 0; + _height = 0, + _isUpdating = false { + transactions = ObservableMap<String, BitcoinTransactionInfo>(); + } BitcoinWalletBase wallet; final ElectrumClient eclient; final String path; final String _password; int _height; + bool _isUpdating; Future<void> init() async { - final info = await _read(); - _height = info['height'] as int ?? _height; - transactions = ObservableList.of( - info['transactions'] as List<BitcoinTransactionInfo> ?? - <BitcoinTransactionInfo>[]); + await _load(); } @override Future update() async { - await super.update(); - _updateHeight(); + if (_isUpdating) { + return; + } + + try { + _isUpdating = true; + final txs = await fetchTransactions(); + await add(txs); + _isUpdating = false; + } catch (_) { + _isUpdating = false; + rethrow; + } } @override - Future<List<BitcoinTransactionInfo>> fetchTransactions() async { - final addresses = wallet.addresses; + Future<Map<String, BitcoinTransactionInfo>> fetchTransactions() async { final histories = - addresses.map((record) => eclient.getHistory(address: record.address)); + wallet.scriptHashes.map((scriptHash) => eclient.getHistory(scriptHash)); final _historiesWithDetails = await Future.wait(histories) .then((histories) => histories - .map((h) => h.where((tx) => (tx['height'] as int) > _height)) +// .map((h) => h.where((tx) { +// final height = tx['height'] as int ?? 0; +// // FIXME: Filter only needed transactions +// final _tx = get(tx['tx_hash'] as String); +// +// return height == 0 || height > _height; +// })) .expand((i) => i) .toList()) .then((histories) => histories.map((tx) => fetchTransactionInfo( hash: tx['tx_hash'] as String, height: tx['height'] as int))); final historiesWithDetails = await Future.wait(_historiesWithDetails); - return historiesWithDetails - .map((info) => BitcoinTransactionInfo.fromHexAndHeader( - info['raw'] as String, info['header'] as Map<String, Object>, - addresses: addresses.map((record) => record.address).toList())) - .toList(); + return historiesWithDetails.fold<Map<String, BitcoinTransactionInfo>>( + <String, BitcoinTransactionInfo>{}, (acc, tx) { + acc[tx.id] = tx; + return acc; + }); } - Future<Map<String, Object>> fetchTransactionInfo( + Future<BitcoinTransactionInfo> fetchTransactionInfo( {@required String hash, @required int height}) async { - final rawFetching = eclient.getTransactionRaw(hash: hash); - final headerFetching = eclient.getHeader(height: height); - final result = await Future.wait([rawFetching, headerFetching]); - final raw = result.first as String; - final header = result[1] as Map<String, Object>; - - return {'raw': raw, 'header': header}; + final tx = await eclient.getTransactionExpanded(hash: hash); + return BitcoinTransactionInfo.fromElectrumVerbose(tx, + height: height, addresses: wallet.addresses); } - Future<void> add(List<BitcoinTransactionInfo> transactions) async { - this.transactions.addAll(transactions); + Future<void> add(Map<String, BitcoinTransactionInfo> transactionsList) async { + transactionsList.entries.forEach((entry) { + _updateOrInsert(entry.value); + + if (entry.value.height > _height) { + _height = entry.value.height; + } + }); + await save(); } Future<void> addOne(BitcoinTransactionInfo tx) async { - transactions.add(tx); + _updateOrInsert(tx); + + if (tx.height > _height) { + _height = tx.height; + } + await save(); } - Future<void> save() async => writeData( - path: path, - password: _password, - data: json.encode({'height': _height, 'transactions': transactions})); + BitcoinTransactionInfo get(String id) => transactions[id]; + + Future<void> save() async { + final data = json.encode({'height': _height, 'transactions': transactions}); + + print('data'); + print(data); + + await writeData(path: path, password: _password, data: data); + } + + @override + void updateAsync({void Function() onFinished}) { + fetchTransactionsAsync((transaction) => _updateOrInsert(transaction), + onFinished: onFinished); + } + + @override + void fetchTransactionsAsync( + void Function(BitcoinTransactionInfo transaction) onTransactionLoaded, + {void Function() onFinished}) async { + final histories = await Future.wait(wallet.scriptHashes + .map((scriptHash) async => await eclient.getHistory(scriptHash))); + final transactionsCount = + histories.fold<int>(0, (acc, m) => acc + m.length); + var counter = 0; + + final batches = histories.map((metaList) => + _fetchBatchOfTransactions(metaList, onTransactionLoaded: (transaction) { + onTransactionLoaded(transaction); + counter += 1; + + if (counter == transactionsCount) { + onFinished?.call(); + } + })); + + await Future.wait(batches); + } + + Future<void> _fetchBatchOfTransactions( + Iterable<Map<String, dynamic>> metaList, + {void Function(BitcoinTransactionInfo tranasaction) + onTransactionLoaded}) async => + metaList.forEach((txMeta) => fetchTransactionInfo( + hash: txMeta['tx_hash'] as String, + height: txMeta['height'] as int) + .then((transaction) => onTransactionLoaded(transaction))); Future<Map<String, Object>> _read() async { + final content = await read(path: path, password: _password); + return json.decode(content) as Map<String, Object>; + } + + Future<void> _load() async { try { - final content = await read(path: path, password: _password); - final jsoned = json.decode(content) as Map<String, Object>; - final height = jsoned['height'] as int; - final transactions = (jsoned['transactions'] as List<dynamic>) - .map((dynamic row) { - if (row is Map<String, Object>) { - return BitcoinTransactionInfo.fromJson(row); - } + final content = await _read(); + final txs = content['transactions'] as Map<String, Object> ?? {}; - return null; - }) - .where((el) => el != null) - .toList(); + txs.entries.forEach((entry) { + final val = entry.value; - return {'transactions': transactions, 'height': height}; - } catch (_) { - return {'transactions': <BitcoinTransactionInfo>[], 'height': 0}; + if (val is Map<String, Object>) { + final tx = BitcoinTransactionInfo.fromJson(val); + _updateOrInsert(tx); + } + }); + + _height = content['height'] as int; + } catch (_) {} + } + + void _updateOrInsert(BitcoinTransactionInfo transaction) { + if (transactions[transaction.id] == null) { + transactions[transaction.id] = transaction; + } else { + final originalTx = transactions[transaction.id]; + originalTx.confirmations = transaction.confirmations; + originalTx.amount = transaction.amount; + originalTx.height = transaction.height; + originalTx.date ??= transaction.date; + originalTx.isPending = transaction.isPending; } } - - void _updateHeight() { - final newHeight = transactions.fold( - 0, (int acc, val) => val.height > acc ? val.height : acc); - _height = newHeight > _height ? newHeight : _height; - } } diff --git a/lib/bitcoin/bitcoin_transaction_info.dart b/lib/bitcoin/bitcoin_transaction_info.dart index 583d1615c..4198ca9e2 100644 --- a/lib/bitcoin/bitcoin_transaction_info.dart +++ b/lib/bitcoin/bitcoin_transaction_info.dart @@ -1,7 +1,9 @@ -import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart'; import 'package:flutter/foundation.dart'; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; +import 'package:cake_wallet/bitcoin/bitcoin_address_record.dart'; import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData; +import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart'; +import 'package:cake_wallet/src/domain/bitcoin/bitcoin_amount_format.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/domain/common/format_amount.dart'; @@ -13,42 +15,97 @@ class BitcoinTransactionInfo extends TransactionInfo { @required int amount, @required TransactionDirection direction, @required bool isPending, - @required DateTime date}) { + @required DateTime date, + @required int confirmations}) { this.height = height; this.amount = amount; this.direction = direction; this.date = date; this.isPending = isPending; + this.confirmations = confirmations; } - factory BitcoinTransactionInfo.fromHexAndHeader( - String hex, Map<String, Object> header, - {List<String> addresses}) { + factory BitcoinTransactionInfo.fromElectrumVerbose(Map<String, Object> obj, + {@required List<BitcoinAddressRecord> addresses, @required int height}) { + final addressesSet = addresses.map((addr) => addr.address).toSet(); + final id = obj['txid'] as String; + final vins = obj['vin'] as List<Object> ?? []; + final vout = (obj['vout'] as List<Object> ?? []); + final date = obj['time'] is int + ? DateTime.fromMillisecondsSinceEpoch((obj['time'] as int) * 1000) + : DateTime.now(); + final confirmations = obj['confirmations'] as int ?? 0; + var direction = TransactionDirection.incoming; + + for (dynamic vin in vins) { + final vout = vin['vout'] as int; + final out = vin['tx']['vout'][vout] as Map; + final outAddresses = + (out['scriptPubKey']['addresses'] as List<Object>)?.toSet(); + + if (outAddresses?.intersection(addressesSet)?.isNotEmpty ?? false) { + direction = TransactionDirection.outgoing; + break; + } + } + + final amount = vout.fold(0, (int acc, dynamic out) { + final outAddresses = + out['scriptPubKey']['addresses'] as List<Object> ?? []; + final ntrs = outAddresses.toSet().intersection(addressesSet); + var amount = acc; + + if ((direction == TransactionDirection.incoming && ntrs.isNotEmpty) || + (direction == TransactionDirection.outgoing && ntrs.isEmpty)) { + amount += doubleToBitcoinAmount(out['value'] as double ?? 0.0); + } + + return amount; + }); + + return BitcoinTransactionInfo( + id: id, + height: height, + isPending: false, + direction: direction, + amount: amount, + date: date, + confirmations: confirmations); + } + + factory BitcoinTransactionInfo.fromHexAndHeader(String hex, + {List<String> addresses, int height, int timestamp, int confirmations}) { final tx = bitcoin.Transaction.fromHex(hex); var exist = false; var amount = 0; - tx.outs.forEach((out) { - try { - final p2pkh = bitcoin.P2PKH( - data: PaymentData(output: out.script), network: bitcoin.bitcoin); - exist = addresses.contains(p2pkh.data.address); + if (addresses != null) { + tx.outs.forEach((out) { + try { + final p2pkh = bitcoin.P2PKH( + data: PaymentData(output: out.script), network: bitcoin.bitcoin); + exist = addresses.contains(p2pkh.data.address); - if (exist) { - amount += out.value; - } - } catch (_) {} - }); + if (exist) { + amount += out.value; + } + } catch (_) {} + }); + } + + final date = timestamp != null + ? DateTime.fromMillisecondsSinceEpoch(timestamp * 1000) + : DateTime.now(); // FIXME: Get transaction is pending return BitcoinTransactionInfo( id: tx.getId(), - height: header['block_height'] as int, + height: height, isPending: false, direction: TransactionDirection.incoming, amount: amount, - date: DateTime.fromMillisecondsSinceEpoch( - (header['timestamp'] as int) * 1000)); + date: date, + confirmations: confirmations); } factory BitcoinTransactionInfo.fromJson(Map<String, dynamic> data) { @@ -58,7 +115,8 @@ class BitcoinTransactionInfo extends TransactionInfo { amount: data['amount'] as int, direction: parseTransactionDirectionFromInt(data['direction'] as int), date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int), - isPending: data['isPending'] as bool); + isPending: data['isPending'] as bool, + confirmations: data['confirmations'] as int); } final String id; @@ -66,7 +124,8 @@ class BitcoinTransactionInfo extends TransactionInfo { String _fiatAmount; @override - String amountFormatted() => '${formatAmount(bitcoinAmountToString(amount: amount))} BTC'; + String amountFormatted() => + '${formatAmount(bitcoinAmountToString(amount: amount))} BTC'; @override String fiatAmount() => _fiatAmount ?? ''; @@ -75,13 +134,14 @@ class BitcoinTransactionInfo extends TransactionInfo { void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); Map<String, dynamic> toJson() { - final m = Map<String, dynamic>(); + final m = <String, dynamic>{}; m['id'] = id; m['height'] = height; m['amount'] = amount; m['direction'] = direction.index; m['date'] = date.millisecondsSinceEpoch; m['isPending'] = isPending; + m['confirmations'] = confirmations; return m; } } diff --git a/lib/bitcoin/bitcoin_transaction_no_inputs_exception.dart b/lib/bitcoin/bitcoin_transaction_no_inputs_exception.dart new file mode 100644 index 000000000..2e925fdb3 --- /dev/null +++ b/lib/bitcoin/bitcoin_transaction_no_inputs_exception.dart @@ -0,0 +1,4 @@ +class BitcoinTransactionNoInputsException implements Exception { + @override + String toString() => 'No inputs for the transaction.'; +} \ No newline at end of file diff --git a/lib/bitcoin/bitcoin_transaction_wrong_balance_exception.dart b/lib/bitcoin/bitcoin_transaction_wrong_balance_exception.dart new file mode 100644 index 000000000..9d1401818 --- /dev/null +++ b/lib/bitcoin/bitcoin_transaction_wrong_balance_exception.dart @@ -0,0 +1,4 @@ +class BitcoinTransactionWrongBalanceException implements Exception { + @override + String toString() => 'Wrong balance. Not enough BTC on your balance.'; +} \ No newline at end of file diff --git a/lib/bitcoin/bitcoin_unspent.dart b/lib/bitcoin/bitcoin_unspent.dart new file mode 100644 index 000000000..bacc03dd4 --- /dev/null +++ b/lib/bitcoin/bitcoin_unspent.dart @@ -0,0 +1,17 @@ +import 'package:cake_wallet/bitcoin/bitcoin_address_record.dart'; + +class BitcoinUnspent { + BitcoinUnspent(this.address, this.hash, this.value, this.vout); + + factory BitcoinUnspent.fromJSON( + BitcoinAddressRecord address, Map<String, dynamic> json) => + BitcoinUnspent(address, json['tx_hash'] as String, json['value'] as int, + json['tx_pos'] as int); + + final BitcoinAddressRecord address; + final String hash; + final int value; + final int vout; + + bool get isP2wpkh => address.address.startsWith('bc1'); +} diff --git a/lib/bitcoin/bitcoin_wallet.dart b/lib/bitcoin/bitcoin_wallet.dart index fa2ce0f0a..4fc46f8fa 100644 --- a/lib/bitcoin/bitcoin_wallet.dart +++ b/lib/bitcoin/bitcoin_wallet.dart @@ -1,10 +1,21 @@ +import 'dart:async'; import 'dart:typed_data'; import 'dart:convert'; import 'package:cake_wallet/bitcoin/bitcoin_transaction_credentials.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_transaction_no_inputs_exception.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_transaction_wrong_balance_exception.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_unspent.dart'; import 'package:cake_wallet/bitcoin/bitcoin_wallet_keys.dart'; +import 'package:cake_wallet/bitcoin/pending_bitcoin_transaction.dart'; +import 'package:cake_wallet/bitcoin/script_hash.dart'; +import 'package:cake_wallet/bitcoin/utils.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:cake_wallet/src/domain/common/transaction_direction.dart'; +import 'package:cake_wallet/src/domain/common/transaction_priority.dart'; +import 'package:cw_monero/transaction_history.dart'; import 'package:flutter/cupertino.dart'; import 'package:mobx/mobx.dart'; import 'package:bip39/bip39.dart' as bip39; @@ -20,12 +31,41 @@ 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'; +import 'package:hex/hex.dart'; +import 'package:cake_wallet/di.dart'; +import 'package:shared_preferences/shared_preferences.dart'; part 'bitcoin_wallet.g.dart'; class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet; abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store { + BitcoinWalletBase._internal( + {@required this.eclient, + @required this.path, + @required String password, + @required this.name, + List<BitcoinAddressRecord> initialAddresses, + int accountIndex = 0, + this.transactionHistory, + this.mnemonic, + BitcoinBalance initialBalance}) + : balance = + initialBalance ?? BitcoinBalance(confirmed: 0, unconfirmed: 0), + hd = bitcoin.HDWallet.fromSeed(bip39.mnemonicToSeed(mnemonic), + network: bitcoin.bitcoin), + addresses = initialAddresses != null + ? ObservableList<BitcoinAddressRecord>.of(initialAddresses) + : ObservableList<BitcoinAddressRecord>(), + syncStatus = NotConnectedSyncStatus(), + _password = password, + _accountIndex = accountIndex, + _addressesKeys = {} { + type = WalletType.bitcoin; + currency = CryptoCurrency.btc; + _scripthashesUpdateSubject = {}; + } + static BitcoinWallet fromJSON( {@required String password, @required String name, @@ -37,12 +77,12 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store { (data['account_index'] == 'null' || data['account_index'] == null) ? 0 : int.parse(data['account_index'] as String); - final _addresses = data['addresses'] as List; + final _addresses = data['addresses'] as List ?? <Object>[]; final addresses = <BitcoinAddressRecord>[]; final balance = BitcoinBalance.fromJSON(data['balance'] as String) ?? BitcoinBalance(confirmed: 0, unconfirmed: 0); - _addresses?.forEach((Object el) { + _addresses.forEach((Object el) { if (el is String) { addresses.add(BitcoinAddressRecord.fromJSON(el)); } @@ -83,34 +123,10 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store { transactionHistory: history); } - BitcoinWalletBase._internal( - {@required this.eclient, - @required this.path, - @required String password, - @required this.name, - List<BitcoinAddressRecord> initialAddresses, - int accountIndex = 0, - 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<BitcoinAddressRecord>.of(initialAddresses) - : ObservableList<BitcoinAddressRecord>(); - syncStatus = NotConnectedSyncStatus(); - - _password = password; - _accountIndex = accountIndex; - } - @override final BitcoinTransactionHistory transactionHistory; final String path; - bitcoin.HDWallet hd; + final bitcoin.HDWallet hd; final ElectrumClient eclient; final String mnemonic; @@ -131,6 +147,11 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store { ObservableList<BitcoinAddressRecord> addresses; + Map<String, bitcoin.ECPair> _addressesKeys; + + List<String> get scriptHashes => + addresses.map((addr) => scriptHash(addr.address)).toList(); + String get xpub => hd.base58; @override @@ -142,11 +163,13 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store { int _accountIndex; String _password; - BehaviorSubject<Object> _addressUpdateSubject; + Map<String, BehaviorSubject<Object>> _scripthashesUpdateSubject; Future<void> init() async { if (addresses.isEmpty) { - addresses.add(BitcoinAddressRecord(_getAddress(hd: hd, index: 0))); + final index = 0; + addresses + .add(BitcoinAddressRecord(_getAddress(index: index), index: index)); } address = addresses.first.address; @@ -156,9 +179,8 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store { Future<BitcoinAddressRecord> generateNewAddress({String label}) async { _accountIndex += 1; - final address = BitcoinAddressRecord( - _getAddress(hd: hd, index: _accountIndex), - label: label); + final address = BitcoinAddressRecord(_getAddress(index: _accountIndex), + index: _accountIndex, label: label); addresses.add(address); await save(); @@ -181,9 +203,9 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store { Future<void> startSync() async { try { syncStatus = StartingSyncStatus(); - await _addressUpdateSubject?.close(); - _addressUpdateSubject = eclient.addressUpdate(address: address); - await transactionHistory.update(); + transactionHistory.updateAsync( + onFinished: () => print('transactionHistory update finished!')); + _subscribeForUpdates(); await _updateBalance(); syncStatus = SyncedSyncStatus(); } catch (e) { @@ -197,39 +219,103 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store { Future<void> connectToNode({@required Node node}) async { try { syncStatus = ConnectingSyncStatus(); - await eclient.connect(host: 'electrum2.hodlister.co', port: 50002); + await eclient.connectToUri(node.uri); + eclient.onConnectionStatusChange = (bool isConnected) { + if (!isConnected) { + syncStatus = LostConnectionSyncStatus(); + } + }; syncStatus = ConnectedSyncStatus(); } catch (e) { - print(e.toString); + print(e.toString()); syncStatus = FailedSyncStatus(); } } @override - Future<void> createTransaction(Object credentials) async { + Future<PendingBitcoinTransaction> createTransaction( + Object credentials) async { final transactionCredentials = credentials as BitcoinTransactionCredentials; - + final inputs = <BitcoinUnspent>[]; + final fee = _feeMultiplier(transactionCredentials.priority); + final amount = transactionCredentials.amount != null + ? doubleToBitcoinAmount(transactionCredentials.amount) + : balance.total - fee; + final totalAmount = amount + fee; 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; + var leftAmount = totalAmount; + final changeAddress = address; + var totalInputAmount = 0; + + final unspent = addresses.map((address) => eclient + .getListUnspentWithAddress(address.address) + .then((unspent) => unspent + .map((unspent) => BitcoinUnspent.fromJSON(address, unspent)))); + + for (final unptsFutures in unspent) { + final utxs = await unptsFutures; + + for (final utx in utxs) { + final inAmount = utx.value > totalAmount ? totalAmount : utx.value; + leftAmount = leftAmount - inAmount; + totalInputAmount += inAmount; + inputs.add(utx); + + if (leftAmount <= 0) { + break; + } + } + + if (leftAmount <= 0) { + break; + } + } + + if (inputs.isEmpty) { + throw BitcoinTransactionNoInputsException(); + } + + if (amount <= 0 || totalInputAmount < amount) { + throw BitcoinTransactionWrongBalanceException(); + } + + final changeValue = totalInputAmount - amount - fee; 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); + inputs.forEach((input) { + if (input.isP2wpkh) { + final p2wpkh = bitcoin + .P2WPKH( + data: generatePaymentData(hd: hd, index: input.address.index), + network: bitcoin.bitcoin) + .data; + + txb.addInput(input.hash, input.vout, null, p2wpkh.output); + } else { + txb.addInput(input.hash, input.vout); + } + }); + + txb.addOutput(transactionCredentials.address, amount); + + if (changeValue > 0) { + txb.addOutput(changeAddress, changeValue); + } + + for (var i = 0; i < inputs.length; i++) { + final input = inputs[i]; + final keyPair = generateKeyPair(hd: hd, index: input.address.index); + final witnessValue = input.isP2wpkh ? input.value : null; + + txb.sign(vin: i, keyPair: keyPair, witnessValue: witnessValue); + } + + return PendingBitcoinTransaction(txb.build(), + eclient: eclient, amount: amount, fee: fee) + ..addListener((transaction) => transactionHistory.addOne(transaction)); } - @override - Future<void> save() async => - await write(path: path, password: _password, data: toJSON()); - String toJSON() => json.encode({ 'mnemonic': mnemonic, 'account_index': _accountIndex.toString(), @@ -237,16 +323,31 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store { 'balance': balance?.toJSON() }); - String _getAddress({bitcoin.HDWallet hd, int index}) => bitcoin - .P2WPKH( - data: PaymentData( - pubkey: Uint8List.fromList(hd.derive(index).pubKey.codeUnits))) - .data - .address; + @override + double calculateEstimatedFee(TransactionPriority priority) => + bitcoinAmountToDouble(amount: _feeMultiplier(priority)); + + @override + Future<void> save() async => + await write(path: path, password: _password, data: toJSON()); + + bitcoin.ECPair keyPairFor({@required int index}) => + generateKeyPair(hd: hd, index: index); + + void _subscribeForUpdates() { + scriptHashes.forEach((sh) async { + await _scripthashesUpdateSubject[sh]?.close(); + _scripthashesUpdateSubject[sh] = eclient.scripthashUpdate(sh); + _scripthashesUpdateSubject[sh].listen((event) async { + transactionHistory.updateAsync(); + await _updateBalance(); + }); + }); + } Future<BitcoinBalance> _fetchBalances() async { final balances = await Future.wait( - addresses.map((record) => eclient.getBalance(address: record.address))); + scriptHashes.map((sHash) => eclient.getBalance(sHash))); final balance = balances.fold( BitcoinBalance(confirmed: 0, unconfirmed: 0), (BitcoinBalance acc, val) => BitcoinBalance( @@ -261,4 +362,20 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store { balance = await _fetchBalances(); await save(); } + + String _getAddress({@required int index}) => + generateAddress(hd: hd, index: index); + + int _feeMultiplier(TransactionPriority priority) { + switch (priority) { + case TransactionPriority.slow: + return 6000; + case TransactionPriority.regular: + return 9000; + case TransactionPriority.fast: + return 15000; + default: + return 0; + } + } } diff --git a/lib/bitcoin/bitcoin_wallet_service.dart b/lib/bitcoin/bitcoin_wallet_service.dart index d5827df18..cb2d241fb 100644 --- a/lib/bitcoin/bitcoin_wallet_service.dart +++ b/lib/bitcoin/bitcoin_wallet_service.dart @@ -1,5 +1,4 @@ import 'dart:io'; -import 'dart:convert'; import 'package:bip39/bip39.dart' as bip39; import 'package:cake_wallet/bitcoin/file.dart'; import 'package:cake_wallet/bitcoin/bitcoin_wallet_creation_credentials.dart'; @@ -49,10 +48,9 @@ class BitcoinWalletService extends WalletService< } @override - Future<void> remove(String wallet) { - // TODO: implement remove - throw UnimplementedError(); - } + Future<void> remove(String wallet) async => + File(await pathForWalletDir(name: wallet, type: WalletType.bitcoin)) + .delete(recursive: true); @override Future<BitcoinWallet> restoreFromKeys( diff --git a/lib/bitcoin/electrum.dart b/lib/bitcoin/electrum.dart index 70a01a293..bd1ac09ac 100644 --- a/lib/bitcoin/electrum.dart +++ b/lib/bitcoin/electrum.dart @@ -1,17 +1,19 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'dart:typed_data'; +import 'package:cake_wallet/bitcoin/script_hash.dart'; import 'package:flutter/foundation.dart'; import 'package:rxdart/rxdart.dart'; String jsonrpcparams(List<Object> params) { final _params = params?.map((val) => '"${val.toString()}"')?.join(','); - return "[$_params]"; + return '[$_params]'; } String jsonrpc( {String method, List<Object> params, int id, double version = 2.0}) => - '{"jsonrpc": "$version", "method": "$method", "id": "$id", "params": ${jsonrpcparams(params)}}\n'; + '{"jsonrpc": "$version", "method": "$method", "id": "$id", "params": ${json.encode(params)}}\n'; class SocketTask { SocketTask({this.completer, this.isSubscription, this.subject}); @@ -31,58 +33,66 @@ class ElectrumClient { bool get isConnected => _isConnected; Socket socket; + void Function(bool) onConnectionStatusChange; int _id; final Map<String, SocketTask> _tasks; bool _isConnected; Timer _aliveTimer; - Future<void> connect({@required String host, @required int port}) async { - if (socket != null) { - await socket.close(); - } + Future<void> connectToUri(String uri) async { + final _uri = Uri.parse(uri); + final host = _uri.scheme; + final port = int.parse(_uri.path); + await connect(host: host, port: port); + } - final start = DateTime.now(); + Future<void> connect({@required String host, @required int port}) async { + try { + await socket?.close(); + } catch (_) {} socket = await SecureSocket.connect(host, port, timeout: connectionTimeout); + _setIsConnected(true); - _isConnected = true; - - socket.listen((List<int> event) { + socket.listen((Uint8List event) { try { - final jsoned = json.decode(utf8.decode(event)) as Map<String, Object>; + final jsoned = + json.decode(utf8.decode(event.toList())) as Map<String, Object>; + print(jsoned); final method = jsoned['method']; + final id = jsoned['id'] as String; + final params = jsoned['result']; if (method is String) { _methodHandler(method: method, request: jsoned); return; } - final id = jsoned['id'] as String; - final params = jsoned['result']; - _finish(id, params); } catch (e) { print(e); } }, onError: (Object error) { - print('ElectrumClient error: ${error.toString()}'); - }, onDone: () { - final end = DateTime.now(); - final diff = end.millisecondsSinceEpoch - start.millisecondsSinceEpoch; - print('On done: $diff'); - }); - - print('Connected to ${socket.remoteAddress}'); + print(error.toString()); + _setIsConnected(false); + }, onDone: () => _setIsConnected(false)); keepAlive(); } void keepAlive() { _aliveTimer?.cancel(); // FIXME: Unnamed constant. - _aliveTimer = Timer.periodic(Duration(seconds: 30), (_) async => ping()); + _aliveTimer = Timer.periodic(Duration(seconds: 2), (_) async => ping()); } - Future<void> ping() => call(method: 'server.ping'); + Future<void> ping() async { + try { + await callWithTimeout(method: 'server.ping'); + _setIsConnected(true); + } on RequestFailedTimeoutException catch (_) { + _setIsConnected(false); + } + } Future<List<String>> version() => call(method: 'server.version').then((dynamic result) { @@ -93,18 +103,18 @@ class ElectrumClient { return []; }); - Future<Map<String, Object>> getBalance({String address}) => - call(method: 'blockchain.address.get_balance', params: [address]) + Future<Map<String, Object>> getBalance(String scriptHash) => + call(method: 'blockchain.scripthash.get_balance', params: [scriptHash]) .then((dynamic result) { if (result is Map<String, Object>) { return result; } - return Map<String, Object>(); + return <String, Object>{}; }); - Future<List<Map<String, dynamic>>> getHistory({String address}) => - call(method: 'blockchain.address.get_history', params: [address]) + Future<List<Map<String, dynamic>>> getHistory(String scriptHash) => + call(method: 'blockchain.scripthash.get_history', params: [scriptHash]) .then((dynamic result) { if (result is List) { return result.map((dynamic val) { @@ -112,24 +122,91 @@ class ElectrumClient { return val; } - return Map<String, Object>(); + return <String, Object>{}; }).toList(); } return []; }); - Future<String> getTransactionRaw({@required String hash}) async => - call(method: 'blockchain.transaction.get', params: [hash]) + Future<List<Map<String, dynamic>>> getListUnspentWithAddress( + String address) => + call( + method: 'blockchain.scripthash.listunspent', + params: [scriptHash(address)]).then((dynamic result) { + if (result is List) { + return result.map((dynamic val) { + if (val is Map<String, Object>) { + val['address'] = address; + return val; + } + + return <String, Object>{}; + }).toList(); + } + + return []; + }); + + Future<List<Map<String, dynamic>>> getListUnspent(String scriptHash) => + call(method: 'blockchain.scripthash.listunspent', params: [scriptHash]) .then((dynamic result) { - if (result is String) { + if (result is List) { + return result.map((dynamic val) { + if (val is Map<String, Object>) { + return val; + } + + return <String, Object>{}; + }).toList(); + } + + return []; + }); + + Future<List<Map<String, dynamic>>> getMempool(String scriptHash) => + call(method: 'blockchain.scripthash.get_mempool', params: [scriptHash]) + .then((dynamic result) { + if (result is List) { + return result.map((dynamic val) { + if (val is Map<String, Object>) { + return val; + } + + return <String, Object>{}; + }).toList(); + } + + return []; + }); + + Future<Map<String, Object>> getTransactionRaw( + {@required String hash}) async => + call(method: 'blockchain.transaction.get', params: [hash, true]) + .then((dynamic result) { + if (result is Map<String, Object>) { return result; } - return ''; + return <String, Object>{}; }); - Future<String> broadcastTransaction({@required String transactionRaw}) async => + Future<Map<String, Object>> getTransactionExpanded( + {@required String hash}) async { + final originalTx = await getTransactionRaw(hash: hash); + final vins = originalTx['vin'] as List<Object>; + + for (dynamic vin in vins) { + if (vin is Map<String, Object>) { + vin['tx'] = await getTransactionRaw(hash: vin['txid'] as String); + } + } + + return originalTx; + } + + Future<String> broadcastTransaction( + {@required String transactionRaw}) async => call(method: 'blockchain.transaction.broadcast', params: [transactionRaw]) .then((dynamic result) { if (result is String) { @@ -163,11 +240,11 @@ class ElectrumClient { return 0; }); - BehaviorSubject<Object> addressUpdate({@required String address}) => + BehaviorSubject<Object> scripthashUpdate(String scripthash) => subscribe<Object>( - id: 'blockchain.address.subscribe:$address', - method: 'blockchain.address.subscribe', - params: [address]); + id: 'blockchain.scripthash.subscribe:$scripthash', + method: 'blockchain.scripthash.subscribe', + params: [scripthash]); BehaviorSubject<T> subscribe<T>( {@required String id, @@ -190,6 +267,25 @@ class ElectrumClient { return completer.future; } + Future<dynamic> callWithTimeout( + {String method, + List<Object> params = const [], + int timeout = 2000}) async { + final completer = Completer<dynamic>(); + _id += 1; + final id = _id; + _regisryTask(id, completer); + socket.write(jsonrpc(method: method, id: _id, params: params)); + + Timer(Duration(milliseconds: timeout), () { + if (!completer.isCompleted) { + completer.completeError(RequestFailedTimeoutException(method, _id)); + } + }); + + return completer.future; + } + void request({String method, List<Object> params = const []}) { _id += 1; socket.write(jsonrpc(method: method, id: _id, params: params)); @@ -206,7 +302,9 @@ class ElectrumClient { return; } - _tasks[id]?.completer?.complete(data); + if (!(_tasks[id]?.completer?.isCompleted ?? false)) { + _tasks[id]?.completer?.complete(data); + } if (!(_tasks[id]?.isSubscription ?? false)) { _tasks[id] = null; @@ -218,18 +316,30 @@ class ElectrumClient { void _methodHandler( {@required String method, @required Map<String, Object> request}) { switch (method) { - case 'blockchain.address.subscribe': + case 'blockchain.scripthash.subscribe': final params = request['params'] as List<dynamic>; - final address = params.first as String; - final id = 'blockchain.address.subscribe:$address'; - - if (_tasks[id] != null) { - _tasks[id].subject.add(params.last); - } + final scripthash = params.first as String; + final id = 'blockchain.scripthash.subscribe:$scripthash'; + _tasks[id]?.subject?.add(params.last); break; default: break; } } + + void _setIsConnected(bool isConnected) { + if (_isConnected != isConnected) { + onConnectionStatusChange?.call(isConnected); + } + + _isConnected = isConnected; + } +} + +class RequestFailedTimeoutException implements Exception { + RequestFailedTimeoutException(this.method, this.id); + + final String method; + final int id; } diff --git a/lib/bitcoin/pending_bitcoin_transaction.dart b/lib/bitcoin/pending_bitcoin_transaction.dart new file mode 100644 index 000000000..64b317169 --- /dev/null +++ b/lib/bitcoin/pending_bitcoin_transaction.dart @@ -0,0 +1,47 @@ +import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart'; +import 'package:cake_wallet/src/domain/common/transaction_direction.dart'; +import 'package:flutter/foundation.dart'; +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; +import 'package:cake_wallet/core/pending_transaction.dart'; +import 'package:cake_wallet/bitcoin/electrum.dart'; + +class PendingBitcoinTransaction with PendingTransaction { + PendingBitcoinTransaction(this._tx, + {@required this.eclient, @required this.amount, @required this.fee}) + : _listeners = <void Function(BitcoinTransactionInfo transaction)>[]; + + final bitcoin.Transaction _tx; + final ElectrumClient eclient; + final int amount; + final int fee; + + String get id => _tx.getId(); + + @override + String get amountFormatted => bitcoinAmountToString(amount: amount); + + @override + String get feeFormatted => bitcoinAmountToString(amount: fee); + + final List<void Function(BitcoinTransactionInfo transaction)> _listeners; + + @override + Future<void> commit() async { + await eclient.broadcastTransaction(transactionRaw: _tx.toHex()); + _listeners?.forEach((listener) => listener(transactionInfo())); + } + + void addListener( + void Function(BitcoinTransactionInfo transaction) listener) => + _listeners.add(listener); + + BitcoinTransactionInfo transactionInfo() => BitcoinTransactionInfo( + id: id, + height: 0, + amount: amount, + direction: TransactionDirection.outgoing, + date: DateTime.now(), + isPending: true, + confirmations: 0); +} diff --git a/lib/bitcoin/script_hash.dart b/lib/bitcoin/script_hash.dart new file mode 100644 index 000000000..b252a0700 --- /dev/null +++ b/lib/bitcoin/script_hash.dart @@ -0,0 +1,18 @@ +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; +import 'package:crypto/crypto.dart'; + +String scriptHash(String address) { + final outputScript = bitcoin.Address.addressToOutputScript(address); + final splitted = sha256.convert(outputScript).toString().split(''); + var res = ''; + + for (var i = splitted.length - 1; i >= 0; i--) { + final char = splitted[i]; + i--; + final nextChar = splitted[i]; + res += nextChar; + res += char; + } + + return res; +} \ No newline at end of file diff --git a/lib/bitcoin/utils.dart b/lib/bitcoin/utils.dart new file mode 100644 index 000000000..257c8b176 --- /dev/null +++ b/lib/bitcoin/utils.dart @@ -0,0 +1,26 @@ +import 'dart:typed_data'; +import 'package:flutter/foundation.dart'; +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; +import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData; +import 'package:hex/hex.dart'; + +bitcoin.PaymentData generatePaymentData( + {@required bitcoin.HDWallet hd, @required int index}) => + PaymentData( + pubkey: Uint8List.fromList(HEX.decode(hd.derive(index).pubKey))); + +bitcoin.ECPair generateKeyPair( + {@required bitcoin.HDWallet hd, + @required int index, + bitcoin.NetworkType network}) => + bitcoin.ECPair.fromWIF(hd.derive(index).wif, + network: network ?? bitcoin.bitcoin); + +String generateAddress({@required bitcoin.HDWallet hd, @required int index}) => + bitcoin + .P2WPKH( + data: PaymentData( + pubkey: + Uint8List.fromList(HEX.decode(hd.derive(index).pubKey)))) + .data + .address; diff --git a/lib/core/amount_validator.dart b/lib/core/amount_validator.dart index 8ed81e0a6..cee4fe8e1 100644 --- a/lib/core/amount_validator.dart +++ b/lib/core/amount_validator.dart @@ -14,10 +14,10 @@ class AmountValidator extends TextValidator { static String _pattern(WalletType type) { switch (type) { case WalletType.monero: - return '^([0-9]+([.][0-9]{0,12})?|[.][0-9]{1,12}|ALL)\$'; + return '^([0-9]+([.\,][0-9]{0,12})?|[.\,][0-9]{1,12})\$'; case WalletType.bitcoin: // FIXME: Incorrect pattern for bitcoin - return '^([0-9]+([.][0-9]{0,12})?|[.][0-9]{1,12}|ALL)\$'; + return '^([0-9]+([.\,][0-9]{0,12})?|[.\,][0-9]{1,12})\$'; default: return ''; } diff --git a/lib/core/pending_transaction.dart b/lib/core/pending_transaction.dart new file mode 100644 index 000000000..c7adb4478 --- /dev/null +++ b/lib/core/pending_transaction.dart @@ -0,0 +1,6 @@ +mixin PendingTransaction { + String get amountFormatted; + String get feeFormatted; + + Future<void> commit(); +} \ No newline at end of file diff --git a/lib/core/transaction_history.dart b/lib/core/transaction_history.dart index 78711905e..dd2c5a0f2 100644 --- a/lib/core/transaction_history.dart +++ b/lib/core/transaction_history.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/src/domain/common/transaction_info.dart'; @@ -5,7 +6,7 @@ abstract class TransactionHistoryBase<TransactionType extends TransactionInfo> { TransactionHistoryBase() : _isUpdating = false; @observable - ObservableList<TransactionType> transactions; + ObservableMap<String, TransactionType> transactions; bool _isUpdating; @@ -24,5 +25,15 @@ abstract class TransactionHistoryBase<TransactionType extends TransactionInfo> { } } - Future<List<TransactionType>> fetchTransactions(); -} \ No newline at end of file + void updateAsync({void Function() onFinished}) { + fetchTransactionsAsync( + (transaction) => transactions[transaction.id] = transaction, + onFinished: onFinished); + } + + void fetchTransactionsAsync( + void Function(TransactionType transaction) onTransactionLoaded, + {void Function() onFinished}); + + Future<Map<String, TransactionType>> fetchTransactions(); +} diff --git a/lib/core/wallet_base.dart b/lib/core/wallet_base.dart index dfbf31aef..3d0246251 100644 --- a/lib/core/wallet_base.dart +++ b/lib/core/wallet_base.dart @@ -1,5 +1,7 @@ import 'package:flutter/foundation.dart'; +import 'package:cake_wallet/core/pending_transaction.dart'; import 'package:cake_wallet/core/transaction_history.dart'; +import 'package:cake_wallet/src/domain/common/transaction_priority.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'; @@ -30,7 +32,9 @@ abstract class WalletBase<BalaceType> { Future<void> startSync(); - Future<void> createTransaction(Object credentials); + Future<PendingTransaction> createTransaction(Object credentials); + + double calculateEstimatedFee(TransactionPriority priority); Future<void> save(); } diff --git a/lib/di.dart b/lib/di.dart index dabb7c4e4..7c08d9e49 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -4,6 +4,8 @@ import 'package:cake_wallet/src/domain/common/node.dart'; import 'package:cake_wallet/src/domain/exchange/trade.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/exchange_trade/exchange_confirm_page.dart'; +import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_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'; @@ -27,9 +29,13 @@ 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/src/screens/wallet_list/wallet_list_page.dart'; +import 'package:cake_wallet/store/theme_changer_store.dart'; import 'package:cake_wallet/store/wallet_list_store.dart'; +import 'package:cake_wallet/utils/mobx.dart'; +import 'package:cake_wallet/theme_changer.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/exchange/exchange_trade_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'; @@ -39,7 +45,7 @@ import 'package:cake_wallet/view_model/dashboard/balance_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/send/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'; @@ -81,8 +87,20 @@ NodeListStore setupNodeListStore(Box<Node> nodeSource) { _nodeListStore = NodeListStore(); _nodeListStore.replaceValues(nodeSource.values); _onNodesSourceChange = nodeSource.watch(); - _onNodesSourceChange - .listen((_) => _nodeListStore.replaceValues(nodeSource.values)); + _onNodesSourceChange.listen((event) { +// print(event); + + if (event.deleted) { + _nodeListStore.nodes.removeWhere((n) { + return n.key != null ? n.key == event.key : true; + }); + } + + if (event.value is Node) { + final val = event.value as Node; + _nodeListStore.nodes.add(val); + } + }); return _nodeListStore; } @@ -114,10 +132,8 @@ Future setup( getIt.registerSingleton<ContactService>( ContactService(contactSource, getIt.get<AppStore>().contactListStore)); getIt.registerSingleton<TradesStore>(TradesStore( - tradesSource: tradesSource, - settingsStore: getIt.get<SettingsStore>())); - getIt.registerSingleton<TradeFilterStore>( - TradeFilterStore(wallet: getIt.get<AppStore>().wallet)); + tradesSource: tradesSource, settingsStore: getIt.get<SettingsStore>())); + getIt.registerSingleton<TradeFilterStore>(TradeFilterStore()); getIt.registerSingleton<TransactionFilterStore>(TransactionFilterStore()); getIt.registerSingleton<FiatConvertationStore>(FiatConvertationStore()); getIt.registerSingleton<SendTemplateStore>( @@ -155,20 +171,17 @@ Future setup( getIt.registerFactory<WalletAddressListViewModel>( () => WalletAddressListViewModel(wallet: getIt.get<AppStore>().wallet)); - getIt.registerFactory( - () => BalanceViewModel( - wallet: getIt.get<AppStore>().wallet, - settingsStore: getIt.get<SettingsStore>(), - fiatConvertationStore: getIt.get<FiatConvertationStore>())); + getIt.registerFactory(() => BalanceViewModel( + wallet: getIt.get<AppStore>().wallet, + settingsStore: getIt.get<SettingsStore>(), + fiatConvertationStore: getIt.get<FiatConvertationStore>())); - getIt.registerFactory( - () => DashboardViewModel( - balanceViewModel: getIt.get<BalanceViewModel>(), - appStore: getIt.get<AppStore>(), - tradesStore: getIt.get<TradesStore>(), - tradeFilterStore: getIt.get<TradeFilterStore>(), - transactionFilterStore: getIt.get<TransactionFilterStore>() - )); + getIt.registerFactory(() => DashboardViewModel( + balanceViewModel: getIt.get<BalanceViewModel>(), + appStore: getIt.get<AppStore>(), + tradesStore: getIt.get<TradesStore>(), + tradeFilterStore: getIt.get<TradeFilterStore>(), + transactionFilterStore: getIt.get<TransactionFilterStore>())); getIt.registerFactory<AuthService>(() => AuthService( secureStorage: getIt.get<FlutterSecureStorage>(), @@ -196,10 +209,9 @@ Future setup( onAuthenticationFinished: onAuthFinished, closable: false)); - getIt.registerFactory<DashboardPage>( - () => DashboardPage( - walletViewModel: getIt.get<DashboardViewModel>(), - addressListViewModel: getIt.get<WalletAddressListViewModel>())); + getIt.registerFactory<DashboardPage>(() => DashboardPage( + walletViewModel: getIt.get<DashboardViewModel>(), + addressListViewModel: getIt.get<WalletAddressListViewModel>())); getIt.registerFactory<ReceivePage>(() => ReceivePage( addressListViewModel: getIt.get<WalletAddressListViewModel>())); @@ -213,17 +225,17 @@ Future setup( addressEditOrCreateViewModel: getIt.get<WalletAddressEditOrCreateViewModel>(param1: item))); + // getIt.get<SendTemplateStore>() getIt.registerFactory<SendViewModel>(() => SendViewModel( getIt.get<AppStore>().wallet, getIt.get<AppStore>().settingsStore, - getIt.get<FiatConvertationStore>(), - getIt.get<SendTemplateStore>())); + getIt.get<FiatConvertationStore>())); getIt.registerFactory( () => SendPage(sendViewModel: getIt.get<SendViewModel>())); - getIt.registerFactory( - () => SendTemplatePage(sendViewModel: getIt.get<SendViewModel>())); + // getIt.registerFactory( + // () => SendTemplatePage(sendViewModel: getIt.get<SendViewModel>())); getIt.registerFactory(() => WalletListViewModel( walletInfoSource, getIt.get<AppStore>(), getIt.get<KeyService>())); @@ -260,8 +272,10 @@ Future setup( moneroAccountCreationViewModel: getIt.get<MoneroAccountEditOrCreateViewModel>())); - getIt.registerFactory( - () => SettingsViewModel(getIt.get<AppStore>().settingsStore)); + getIt.registerFactory(() { + final appStore = getIt.get<AppStore>(); + return SettingsViewModel(appStore.settingsStore, appStore.wallet); + }); getIt.registerFactory(() => SettingsPage(getIt.get<SettingsViewModel>())); @@ -292,10 +306,11 @@ Future setup( getIt.registerFactoryParam<ContactPage, Contact, void>((Contact contact, _) => ContactPage(getIt.get<ContactViewModel>(param1: contact))); - getIt.registerFactory(() => NodeListViewModel( - getIt.get<AppStore>().nodeListStore, - nodeSource, - getIt.get<AppStore>().wallet)); + getIt.registerFactory(() { + final appStore = getIt.get<AppStore>(); + return NodeListViewModel(appStore.nodeListStore, nodeSource, + appStore.wallet, appStore.settingsStore); + }); getIt.registerFactory(() => NodeListPage(getIt.get<NodeListViewModel>())); @@ -305,16 +320,30 @@ Future setup( getIt.registerFactory( () => NodeCreateOrEditPage(getIt.get<NodeCreateOrEditViewModel>())); - getIt.registerFactory(() => - ExchangeViewModel( - wallet: getIt.get<AppStore>().wallet, - exchangeTemplateStore: getIt.get<ExchangeTemplateStore>(), - trades: tradesSource - )); + getIt.registerFactory(() => ExchangeViewModel( + wallet: getIt.get<AppStore>().wallet, + exchangeTemplateStore: getIt.get<ExchangeTemplateStore>(), + trades: tradesSource, + tradesStore: getIt.get<TradesStore>())); - getIt.registerFactory(() => - ExchangePage(getIt.get<ExchangeViewModel>())); + getIt.registerFactory(() => ExchangeTradeViewModel( + wallet: getIt.get<AppStore>().wallet, + trades: tradesSource, + tradesStore: getIt.get<TradesStore>())); - getIt.registerFactory(() => - ExchangeTemplatePage(getIt.get<ExchangeViewModel>())); + getIt.registerFactory(() => ExchangePage(getIt.get<ExchangeViewModel>())); + + getIt.registerFactory( + () => ExchangeConfirmPage(tradesStore: getIt.get<TradesStore>())); + + getIt.registerFactory(() => ExchangeTradePage( + exchangeTradeViewModel: getIt.get<ExchangeTradeViewModel>())); + + getIt.registerFactory( + () => ExchangeTemplatePage(getIt.get<ExchangeViewModel>())); +} + +void setupThemeChangerStore(ThemeChanger themeChanger) { + getIt.registerSingleton<ThemeChangerStore>( + ThemeChangerStore(themeChanger: themeChanger)); } diff --git a/lib/generated/i18n.dart b/lib/generated/i18n.dart index 4c9a59474..5408734ae 100644 --- a/lib/generated/i18n.dart +++ b/lib/generated/i18n.dart @@ -96,7 +96,7 @@ class S implements WidgetsLocalizations { String get expired => "Expired"; String get faq => "FAQ"; String get fetching => "Fetching"; - String get filters => "Filters"; + String get filters => "Filter"; String get first_wallet_text => "Awesome wallet for Monero"; String get full_balance => "Full Balance"; String get hidden_balance => "Hidden Balance"; @@ -312,8 +312,8 @@ class S implements WidgetsLocalizations { String error_text_limits_loading_failed(String provider) => "Trade for ${provider} is not created. Limits loading failed"; String error_text_maximum_limit(String provider, String max, String currency) => "Trade for ${provider} is not created. Amount is more then maximum: ${max} ${currency}"; String error_text_minimal_limit(String provider, String min, String currency) => "Trade for ${provider} is not created. Amount is less then minimal: ${min} ${currency}"; - String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "By pressing confirm, you will be sending ${fetchingLabel} ${from} from your wallet called ${walletName} to the address shown above. Or you can send from your external wallet to the above address/QR code.\n\nPlease press confirm to continue or go back to change the amounts.\n\n"; - String exchange_result_description(String fetchingLabel, String from) => "Please send ${fetchingLabel} ${from} to the address shown above.\n\n"; + String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "By pressing confirm, you will be sending ${fetchingLabel} ${from} from your wallet called ${walletName} to the address shown above. Or you can send from your external wallet to the above address/QR code.\n\nPlease press confirm to continue or go back to change the amounts."; + String exchange_result_description(String fetchingLabel, String from) => "Please send ${fetchingLabel} ${from} to the address shown above."; String failed_authentication(String state_error) => "Failed authentication. ${state_error}"; String max_value(String value, String currency) => "Max: ${value} ${currency}"; String min_value(String value, String currency) => "Min: ${value} ${currency}"; @@ -948,11 +948,11 @@ class $de extends S { @override String Blocks_remaining(String status) => "${status} Verbleibende Blöcke"; @override - String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "Durch Drücken von Bestätigen wird gesendet ${fetchingLabel} ${from} von Ihrer Brieftasche aus angerufen ${walletName} an die oben angegebene Adresse. Oder Sie können von Ihrem externen Portemonnaie an die oben angegebene Adresse / QR-Code senden.\n\nBitte bestätigen Sie, um fortzufahren, oder gehen Sie zurück, um die Beträge zu änderns.\n\n"; + String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "Durch Drücken von Bestätigen wird gesendet ${fetchingLabel} ${from} von Ihrer Brieftasche aus angerufen ${walletName} an die oben angegebene Adresse. Oder Sie können von Ihrem externen Portemonnaie an die oben angegebene Adresse / QR-Code senden.\n\nBitte bestätigen Sie, um fortzufahren, oder gehen Sie zurück, um die Beträge zu änderns."; @override String error_text_limits_loading_failed(String provider) => "Handel für ${provider} wird nicht erstellt. Das Laden der Limits ist fehlgeschlagen"; @override - String exchange_result_description(String fetchingLabel, String from) => "Bitte senden ${fetchingLabel} ${from} an die oben angegebene Adresse.\n\n'"; + String exchange_result_description(String fetchingLabel, String from) => "Bitte senden ${fetchingLabel} ${from} an die oben angegebene Adresse.'"; @override String commit_transaction_amount_fee(String amount, String fee) => "Transaktion festschreiben\nMenge: ${amount}\nGebühr: ${fee}"; @override @@ -1216,7 +1216,7 @@ class $hi extends S { @override String get estimated => "अनुमानित"; @override - String get filters => "फिल्टर"; + String get filters => "फ़िल्टर"; @override String get settings_current_node => "वर्तमान नोड"; @override @@ -1580,11 +1580,11 @@ class $hi extends S { @override String Blocks_remaining(String status) => "${status} शेष रहते हैं"; @override - String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "पुष्टि दबाकर, आप भेज रहे होंगे ${fetchingLabel} ${from} अपने बटुए से ${walletName} ऊपर दिखाए गए पते पर। या आप अपने बाहरी वॉलेट से उपरोक्त पते / क्यूआर कोड पर भेज सकते हैं।\n\nकृपया जारी रखने या राशि बदलने के लिए वापस जाने के लिए पुष्टि करें दबाएं.\n\n"; + String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "पुष्टि दबाकर, आप भेज रहे होंगे ${fetchingLabel} ${from} अपने बटुए से ${walletName} ऊपर दिखाए गए पते पर। या आप अपने बाहरी वॉलेट से उपरोक्त पते / क्यूआर कोड पर भेज सकते हैं।\n\nकृपया जारी रखने या राशि बदलने के लिए वापस जाने के लिए पुष्टि करें दबाएं."; @override String error_text_limits_loading_failed(String provider) => "व्यापार ${provider} के लिए नहीं बनाया गया है। लोडिंग की सीमाएं विफल रहीं"; @override - String exchange_result_description(String fetchingLabel, String from) => "कृपया भेजें ${fetchingLabel} ${from} ऊपर दिखाए गए पते पर\n\n'"; + String exchange_result_description(String fetchingLabel, String from) => "कृपया भेजें ${fetchingLabel} ${from} ऊपर दिखाए गए पते पर"; @override String commit_transaction_amount_fee(String amount, String fee) => "लेन-देन करें\nरकम: ${amount}\nशुल्क: ${fee}"; @override @@ -1848,7 +1848,7 @@ class $ru extends S { @override String get estimated => "Примерно"; @override - String get filters => "Фильтры"; + String get filters => "Фильтр"; @override String get settings_current_node => "Текущая нода"; @override @@ -2212,11 +2212,11 @@ class $ru extends S { @override String Blocks_remaining(String status) => "${status} Осталось блоков"; @override - String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "Нажимая подтвердить, вы отправите ${fetchingLabel} ${from} с вашего кошелька ${walletName} на адрес указанный выше. Или вы можете отправить со своего внешнего кошелька на вышеуказанный адрес/QR-код.\n\nПожалуйста, нажмите подтвердить для продолжения, или вернитесь назад для изменения суммы.\n\n"; + String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "Нажимая подтвердить, вы отправите ${fetchingLabel} ${from} с вашего кошелька ${walletName} на адрес указанный выше. Или вы можете отправить со своего внешнего кошелька на вышеуказанный адрес/QR-код.\n\nПожалуйста, нажмите подтвердить для продолжения, или вернитесь назад для изменения суммы."; @override String error_text_limits_loading_failed(String provider) => "Сделка для ${provider} не создана. Ошибка загрузки лимитов"; @override - String exchange_result_description(String fetchingLabel, String from) => "Пожалуйста отправьте ${fetchingLabel} ${from} на адрес, указанный выше.\n\n'"; + String exchange_result_description(String fetchingLabel, String from) => "Пожалуйста отправьте ${fetchingLabel} ${from} на адрес, указанный выше."; @override String commit_transaction_amount_fee(String amount, String fee) => "Подтвердить транзакцию \nСумма: ${amount}\nКомиссия: ${fee}"; @override @@ -2844,11 +2844,11 @@ class $ko extends S { @override String Blocks_remaining(String status) => "${status} 남은 블록"; @override - String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "확인을 누르면 전송됩니다 ${fetchingLabel} ${from} 지갑에서 ${walletName} 위에 표시된 주소로. 또는 외부 지갑에서 위의 주소 / QR 코드로 보낼 수 있습니다.\n\n확인을 눌러 계속하거나 금액을 변경하려면 돌아가십시오.\n\n"; + String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "확인을 누르면 전송됩니다 ${fetchingLabel} ${from} 지갑에서 ${walletName} 위에 표시된 주소로. 또는 외부 지갑에서 위의 주소 / QR 코드로 보낼 수 있습니다.\n\n확인을 눌러 계속하거나 금액을 변경하려면 돌아가십시오."; @override String error_text_limits_loading_failed(String provider) => "거래 ${provider} 가 생성되지 않습니다. 로딩 실패"; @override - String exchange_result_description(String fetchingLabel, String from) => "보내주세요 ${fetchingLabel} ${from} 위에 표시된 주소로.\n\n'"; + String exchange_result_description(String fetchingLabel, String from) => "보내주세요 ${fetchingLabel} ${from} 위에 표시된 주소로."; @override String commit_transaction_amount_fee(String amount, String fee) => "커밋 거래\n양: ${amount}\n보수: ${fee}"; @override @@ -3112,7 +3112,7 @@ class $pt extends S { @override String get estimated => "Estimado"; @override - String get filters => "Filtros"; + String get filters => "Filtro"; @override String get settings_current_node => "Nó atual"; @override @@ -3476,11 +3476,11 @@ class $pt extends S { @override String Blocks_remaining(String status) => "${status} blocos restantes"; @override - String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "Ao confirmar, você enviará ${fetchingLabel} ${from} da sua carteira ${walletName} para o endereço exibido acima. Você também pode enviar com uma carteira externa para o endereço/código QR acima.\n\nPressione Confirmar para continuar ou volte para alterar os valores.\n\n"; + String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "Ao confirmar, você enviará ${fetchingLabel} ${from} da sua carteira ${walletName} para o endereço exibido acima. Você também pode enviar com uma carteira externa para o endereço/código QR acima.\n\nPressione Confirmar para continuar ou volte para alterar os valores."; @override String error_text_limits_loading_failed(String provider) => "A troca por ${provider} não é criada. Falha no carregamento dos limites"; @override - String exchange_result_description(String fetchingLabel, String from) => "Por favor, envie ${fetchingLabel} ${from} para o endereço mostrado acima.\n\n'"; + String exchange_result_description(String fetchingLabel, String from) => "Por favor, envie ${fetchingLabel} ${from} para o endereço mostrado acima."; @override String commit_transaction_amount_fee(String amount, String fee) => "Confirmar transação\nQuantia: ${amount}\nTaxa: ${fee}"; @override @@ -3744,7 +3744,7 @@ class $uk extends S { @override String get estimated => "Приблизно "; @override - String get filters => "Фільтри"; + String get filters => "Фільтр"; @override String get settings_current_node => "Поточний вузол"; @override @@ -4108,11 +4108,11 @@ class $uk extends S { @override String Blocks_remaining(String status) => "${status} Залишилось блоків"; @override - String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "Натиснувши підтвердити, ви відправите ${fetchingLabel} ${from} з вашого гаманця ${walletName} на адресу вказану вище. Або ви можете відправити зі свого зовнішнього гаманця на вищевказану адресу/QR-код.\n\nБудь ласка, натисніть підтвердити для продовження або поверніться назад щоб змінити суму.\n\n"; + String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "Натиснувши підтвердити, ви відправите ${fetchingLabel} ${from} з вашого гаманця ${walletName} на адресу вказану вище. Або ви можете відправити зі свого зовнішнього гаманця на вищевказану адресу/QR-код.\n\nБудь ласка, натисніть підтвердити для продовження або поверніться назад щоб змінити суму."; @override String error_text_limits_loading_failed(String provider) => "Операція для ${provider} не створена. Помилка завантаження лімітів"; @override - String exchange_result_description(String fetchingLabel, String from) => "Будь ласка, відправте ${fetchingLabel} ${from} на адресу, вказану вище.\n\n'"; + String exchange_result_description(String fetchingLabel, String from) => "Будь ласка, відправте ${fetchingLabel} ${from} на адресу, вказану вище."; @override String commit_transaction_amount_fee(String amount, String fee) => "Підтвердити транзакцію \nСума: ${amount}\nКомісія: ${fee}"; @override @@ -4376,7 +4376,7 @@ class $ja extends S { @override String get estimated => "推定"; @override - String get filters => "フィルター"; + String get filters => "フィルタ"; @override String get settings_current_node => "現在のノード"; @override @@ -4740,11 +4740,11 @@ class $ja extends S { @override String Blocks_remaining(String status) => "${status} 残りのブロック"; @override - String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "確認を押すと、送信されます ${fetchingLabel} ${from} と呼ばれるあなたの財布から ${walletName} 上記のアドレスへ. または、外部ウォレットから上記のアドレス/ QRコードに送信できます.\n\n確認を押して続行するか、戻って金額を変更してください.\n\n"; + String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "確認を押すと、送信されます ${fetchingLabel} ${from} と呼ばれるあなたの財布から ${walletName} 上記のアドレスへ. または、外部ウォレットから上記のアドレス/ QRコードに送信できます.\n\n確認を押して続行するか、戻って金額を変更してください."; @override String error_text_limits_loading_failed(String provider) => "${provider} の取引は作成されません。 制限の読み込みに失敗しました"; @override - String exchange_result_description(String fetchingLabel, String from) => "送信してください ${fetchingLabel} ${from} 上記のアドレスへ.\n\n'"; + String exchange_result_description(String fetchingLabel, String from) => "送信してください ${fetchingLabel} ${from} 上記のアドレスへ."; @override String commit_transaction_amount_fee(String amount, String fee) => "トランザクションをコミット\n量: ${amount}\n費用: ${fee}"; @override @@ -5012,7 +5012,7 @@ class $pl extends S { @override String get estimated => "Oszacowano"; @override - String get filters => "Filtry"; + String get filters => "Filtr"; @override String get settings_current_node => "Bieżący węzeł"; @override @@ -5376,11 +5376,11 @@ class $pl extends S { @override String Blocks_remaining(String status) => "${status} Bloki pozostałe"; @override - String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "Naciskając Potwierdź, wyślesz ${fetchingLabel} ${from} z twojego portfela ${walletName} z twojego portfela. Lub możesz wysłać z zewnętrznego portfela na powyższy adres / kod QR.\n\nNaciśnij Potwierdź, aby kontynuować lub wróć, aby zmienić kwoty.\n\n"; + String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "Naciskając Potwierdź, wyślesz ${fetchingLabel} ${from} z twojego portfela ${walletName} z twojego portfela. Lub możesz wysłać z zewnętrznego portfela na powyższy adres / kod QR.\n\nNaciśnij Potwierdź, aby kontynuować lub wróć, aby zmienić kwoty."; @override String error_text_limits_loading_failed(String provider) => "Wymiana dla ${provider} nie została utworzona. Ładowanie limitów nie powiodło się"; @override - String exchange_result_description(String fetchingLabel, String from) => "Proszę wyślij ${fetchingLabel} ${from} na adres podany powyżej.\n\n'"; + String exchange_result_description(String fetchingLabel, String from) => "Proszę wyślij ${fetchingLabel} ${from} na adres podany powyżej."; @override String commit_transaction_amount_fee(String amount, String fee) => "Zatwierdź transakcję\nIlość: ${amount}\nOpłata: ${fee}"; @override @@ -5644,7 +5644,7 @@ class $es extends S { @override String get estimated => "Estimado"; @override - String get filters => "Filtros"; + String get filters => "Filtrar"; @override String get settings_current_node => "Nodo actual"; @override @@ -6008,11 +6008,11 @@ class $es extends S { @override String Blocks_remaining(String status) => "${status} Bloques restantes"; @override - String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "Al presionar confirmar, enviará ${fetchingLabel} ${from} desde su billetera llamada ${walletName} a la dirección que se muestra arriba. O puede enviar desde su billetera externa a la dirección / código QR anterior.\n\nPresione confirmar para continuar o regrese para cambiar los montos.\n\n"; + String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "Al presionar confirmar, enviará ${fetchingLabel} ${from} desde su billetera llamada ${walletName} a la dirección que se muestra arriba. O puede enviar desde su billetera externa a la dirección / código QR anterior.\n\nPresione confirmar para continuar o regrese para cambiar los montos."; @override String error_text_limits_loading_failed(String provider) => "El comercio por ${provider} no se crea. Límites de carga fallidos"; @override - String exchange_result_description(String fetchingLabel, String from) => "Envíe ${fetchingLabel} ${from} a la dirección que se muestra arriba.\n\n'"; + String exchange_result_description(String fetchingLabel, String from) => "Envíe ${fetchingLabel} ${from} a la dirección que se muestra arriba."; @override String commit_transaction_amount_fee(String amount, String fee) => "Confirmar transacción\nCantidad: ${amount}\nCuota: ${fee}"; @override @@ -6276,7 +6276,7 @@ class $nl extends S { @override String get estimated => "Geschatte"; @override - String get filters => "Filters"; + String get filters => "Filter"; @override String get settings_current_node => "Huidige knooppunt"; @override @@ -6640,11 +6640,11 @@ class $nl extends S { @override String Blocks_remaining(String status) => "${status} Resterende blokken"; @override - String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "Door op bevestigen te drukken, wordt u verzonden ${fetchingLabel} ${from} uit je portemonnee genoemd ${walletName} naar bovenstaand adres. Of u kunt uw externe portemonnee naar bovenstaand adres / QR-code sturen.\n\nDruk op bevestigen om door te gaan of terug te gaan om de bedragen te wijzigen.\n\n"; + String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "Door op bevestigen te drukken, wordt u verzonden ${fetchingLabel} ${from} uit je portemonnee genoemd ${walletName} naar bovenstaand adres. Of u kunt uw externe portemonnee naar bovenstaand adres / QR-code sturen.\n\nDruk op bevestigen om door te gaan of terug te gaan om de bedragen te wijzigen."; @override String error_text_limits_loading_failed(String provider) => "Ruil voor ${provider} is niet gemaakt. Beperkingen laden mislukt"; @override - String exchange_result_description(String fetchingLabel, String from) => "Zend alstublieft ${fetchingLabel} ${from} naar bovenstaand adres.\n\n'"; + String exchange_result_description(String fetchingLabel, String from) => "Zend alstublieft ${fetchingLabel} ${from} naar bovenstaand adres."; @override String commit_transaction_amount_fee(String amount, String fee) => "Verricht transactie\nBedrag: ${amount}\nhonorarium: ${fee}"; @override @@ -6908,7 +6908,7 @@ class $zh extends S { @override String get estimated => "估计的"; @override - String get filters => "筛选器"; + String get filters => "過濾"; @override String get settings_current_node => "当前节点"; @override @@ -7272,11 +7272,11 @@ class $zh extends S { @override String Blocks_remaining(String status) => "${status} 剩余的块"; @override - String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "点击确认 您将发送 ${fetchingLabel} ${from} 从你的钱包里 ${walletName} 到上面显示的地址. 或者,您也可以从外部钱包发送上述地址/ QR码。\n\n请按确认继续或返回以更改金额\n\n"; + String exchange_result_confirm(String fetchingLabel, String from, String walletName) => "点击确认 您将发送 ${fetchingLabel} ${from} 从你的钱包里 ${walletName} 到上面显示的地址. 或者,您也可以从外部钱包发送上述地址/ QR码。\n\n请按确认继续或返回以更改金额"; @override String error_text_limits_loading_failed(String provider) => "未創建 ${provider} 交易。 限制加載失敗"; @override - String exchange_result_description(String fetchingLabel, String from) => "请发送 ${fetchingLabel} ${from} 到上面显示的地址.\n\n'"; + String exchange_result_description(String fetchingLabel, String from) => "请发送 ${fetchingLabel} ${from} 到上面显示的地址."; @override String commit_transaction_amount_fee(String amount, String fee) => "提交交易\n量: ${amount}\nFee: ${fee}"; @override diff --git a/lib/main.dart b/lib/main.dart index 912099978..700ec9c2b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/reactions/bootstrap.dart'; +import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/authentication_store.dart'; import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/bitcoin/bitcoin_wallet_service.dart'; @@ -52,6 +53,8 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/domain/common/language.dart'; import 'package:cake_wallet/src/stores/seed_language/seed_language_store.dart'; +bool isThemeChangerRegistered = false; + void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -129,7 +132,7 @@ void main() async { fiatConvertationService: fiatConvertationService, templates: templates, exchangeTemplates: exchangeTemplates, - initialMigrationVersion: 3); + initialMigrationVersion: 4); setReactions( settingsStore: settingsStore, @@ -169,11 +172,11 @@ Future<void> initialSetup( @required Box<Node> nodes, @required Box<WalletInfo> walletInfoSource, @required Box<Contact> contactSource, - @required Box<Trade> tradesSource, + @required Box<Trade> tradesSource, @required FiatConvertationService fiatConvertationService, @required Box<Template> templates, @required Box<ExchangeTemplate> exchangeTemplates, - int initialMigrationVersion = 3}) async { + int initialMigrationVersion = 4}) async { await defaultSettingsMigration( version: initialMigrationVersion, sharedPreferences: sharedPreferences, @@ -197,7 +200,8 @@ class CakeWalletApp extends StatelessWidget { @override Widget build(BuildContext context) { - final settingsStore = Provider.of<SettingsStore>(context); + //final settingsStore = Provider.of<SettingsStore>(context); + final settingsStore = getIt.get<AppStore>().settingsStore; return ChangeNotifierProvider<ThemeChanger>( create: (_) => ThemeChanger( @@ -228,12 +232,20 @@ class MaterialAppWithTheme extends StatelessWidget { final transactionDescriptions = Provider.of<Box<TransactionDescription>>(context); - final statusBarColor = - settingsStore.isDarkTheme ? Colors.black : Colors.white; + if (!isThemeChangerRegistered) { + setupThemeChangerStore(theme); + isThemeChangerRegistered = true; + } + + /*final statusBarColor = + settingsStore.isDarkTheme ? Colors.black : Colors.white;*/ + final _settingsStore = getIt.get<AppStore>().settingsStore; + + final statusBarColor = Colors.transparent; final statusBarBrightness = - settingsStore.isDarkTheme ? Brightness.light : Brightness.dark; + _settingsStore.isDarkTheme ? Brightness.light : Brightness.dark; final statusBarIconBrightness = - settingsStore.isDarkTheme ? Brightness.light : Brightness.dark; + _settingsStore.isDarkTheme ? Brightness.light : Brightness.dark; SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( statusBarColor: statusBarColor, @@ -270,4 +282,4 @@ class MaterialAppWithTheme extends StatelessWidget { authenticationStore: getIt.get<AuthenticationStore>(), )); } -} \ No newline at end of file +} diff --git a/lib/monero/monero_transaction_history.dart b/lib/monero/monero_transaction_history.dart index 7eeeef423..cd3484389 100644 --- a/lib/monero/monero_transaction_history.dart +++ b/lib/monero/monero_transaction_history.dart @@ -20,12 +20,29 @@ class MoneroTransactionHistory = MoneroTransactionHistoryBase abstract class MoneroTransactionHistoryBase extends TransactionHistoryBase<MoneroTransactionInfo> with Store { MoneroTransactionHistoryBase() { - transactions = ObservableList<MoneroTransactionInfo>(); + transactions = ObservableMap<String, MoneroTransactionInfo>(); } @override - Future<List<MoneroTransactionInfo>> fetchTransactions() async { + Future<Map<String, MoneroTransactionInfo>> fetchTransactions() async { monero_transaction_history.refreshTransactions(); - return _getAllTransactions(null); + return _getAllTransactions(null).fold<Map<String, MoneroTransactionInfo>>( + <String, MoneroTransactionInfo>{}, + (Map<String, MoneroTransactionInfo> acc, MoneroTransactionInfo tx) { + acc[tx.id] = tx; + return acc; + }); } + + @override + void updateAsync({void Function() onFinished}) { + fetchTransactionsAsync( + (transaction) => transactions[transaction.id] = transaction, + onFinished: onFinished); + } + + @override + void fetchTransactionsAsync( + void Function(MoneroTransactionInfo transaction) onTransactionLoaded, + {void Function() onFinished}) {} } diff --git a/lib/monero/monero_wallet.dart b/lib/monero/monero_wallet.dart index e36e6cb29..a98e76230 100644 --- a/lib/monero/monero_wallet.dart +++ b/lib/monero/monero_wallet.dart @@ -16,6 +16,9 @@ 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:cake_wallet/src/domain/common/node.dart'; +import 'package:cake_wallet/core/pending_transaction.dart'; +import 'package:cake_wallet/src/domain/common/transaction_priority.dart'; +import 'package:cake_wallet/src/domain/common/calculate_fiat_amount.dart' as cfa; part 'monero_wallet.g.dart'; @@ -133,7 +136,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance> with Store { } @override - Future<void> createTransaction(Object credentials) async { + Future<PendingTransaction> createTransaction(Object credentials) async { // final _credentials = credentials as MoneroTransactionCreationCredentials; // final transactionDescription = await transaction_history.createTransaction( // address: _credentials.address, @@ -146,6 +149,33 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance> with Store { // transactionDescription); } + @override + double calculateEstimatedFee(TransactionPriority priority) { + // FIXME: hardcoded value; + + if (priority == TransactionPriority.slow) { + return 0.00002459; + } + + if (priority == TransactionPriority.regular) { + return 0.00012305; + } + + if (priority == TransactionPriority.medium) { + return 0.00024503; + } + + if (priority == TransactionPriority.fast) { + return 0.00061453; + } + + if (priority == TransactionPriority.fastest) { + return 0.0260216; + } + + return 0; + } + @override Future<void> save() async { // if (_isSaving) { diff --git a/lib/palette.dart b/lib/palette.dart index 0eebf247f..809c4f085 100644 --- a/lib/palette.dart +++ b/lib/palette.dart @@ -9,11 +9,20 @@ class Palette { static const Color lavender = Color.fromRGBO(237, 245, 252, 1.0); static const Color oceanBlue = Color.fromRGBO(30, 52, 78, 1.0); static const Color lightBlueGrey = Color.fromRGBO(118, 131, 169, 1.0); - static const Color periwinkle = Color.fromRGBO(197, 208, 230, 1.0); + static const Color periwinkle = Color.fromRGBO(195, 210, 227, 1.0); 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 darkLavender = Color.fromRGBO(229, 238, 250, 1.0); static const Color nightBlue = Color.fromRGBO(46, 57, 96, 1.0); + static const Color moderateOrangeYellow = Color.fromRGBO(245, 134, 82, 1.0); + static const Color moderateOrange = Color.fromRGBO(235, 117, 63, 1.0); + static const Color shineGreen = Color.fromRGBO(76, 189, 87, 1.0); + static const Color moderateGreen = Color.fromRGBO(45, 158, 56, 1.0); + static const Color cornflower = Color.fromRGBO(85, 147, 240, 1.0); + static const Color royalBlue = Color.fromRGBO(43, 114, 221, 1.0); + static const Color lightRed = Color.fromRGBO(227, 87, 87, 1.0); + static const Color persianRed = Color.fromRGBO(206, 55, 55, 1.0); + // NEW DESIGN static const Color blueCraiola = Color.fromRGBO(69, 110, 255, 1.0); static const Color darkBlueCraiola = Color.fromRGBO(53, 86, 136, 1.0); @@ -29,6 +38,9 @@ class Palette { static const Color gray = Color.fromRGBO(112, 147, 186, 1.0); static const Color wildPeriwinkle = Color.fromRGBO(219, 227, 243, 1.0); static const Color darkGray = Color.fromRGBO(122, 147, 186, 1.0); + static const Color shadowWhite = Color.fromRGBO(242, 245, 255, 1.0); + static const Color niagara = Color.fromRGBO(152, 172, 201, 1.0); + static const Color alizarinRed = Color.fromRGBO(233, 45, 45, 1.0); // FIXME: Rename. static const Color eee = Color.fromRGBO(236, 239, 245, 1.0); @@ -76,6 +88,8 @@ class PaletteDark { static const Color dividerColor = Color.fromRGBO(48, 59, 95, 1.0); static const Color violetBlue = Color.fromRGBO(59, 72, 119, 1.0); static const Color distantBlue = Color.fromRGBO(72, 85, 131, 1.0); + static const Color moderateVioletBlue = Color.fromRGBO(62, 73, 113, 1.0); + static const Color deepVioletBlue = Color.fromRGBO(52, 66, 104, 1.0); // FIXME: Rename. static const Color eee = Color.fromRGBO(236, 239, 245, 1.0); diff --git a/lib/reactions/bootstrap.dart b/lib/reactions/bootstrap.dart index b190697c5..797f2cfb6 100644 --- a/lib/reactions/bootstrap.dart +++ b/lib/reactions/bootstrap.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:cake_wallet/core/key_service.dart'; import 'package:cake_wallet/src/domain/common/sync_status.dart'; import 'package:mobx/mobx.dart'; @@ -49,8 +51,10 @@ ReactionDisposer _initialAuthReaction; ReactionDisposer _onCurrentWalletChangeReaction; ReactionDisposer _onWalletSyncStatusChangeReaction; ReactionDisposer _onCurrentFiatCurrencyChangeDisposer; +Timer _reconnectionTimer; -Future<void> bootstrap({FiatConvertationService fiatConvertationService}) async { +Future<void> bootstrap( + {FiatConvertationService fiatConvertationService}) async { final authenticationStore = getIt.get<AuthenticationStore>(); final settingsStore = getIt.get<SettingsStore>(); final fiatConvertationStore = getIt.get<FiatConvertationStore>(); @@ -72,12 +76,21 @@ Future<void> bootstrap({FiatConvertationService fiatConvertationService}) async _onCurrentWalletChangeReaction ??= reaction((_) => getIt.get<AppStore>().wallet, (WalletBase wallet) async { - print('Wallet name ${wallet.name}'); - _onWalletSyncStatusChangeReaction?.reaction?.dispose(); - _onWalletSyncStatusChangeReaction = when( + _reconnectionTimer?.cancel(); + _onWalletSyncStatusChangeReaction = reaction( (_) => wallet.syncStatus is ConnectedSyncStatus, - () async => await wallet.startSync()); + (Object _) async => await wallet.startSync()); + + _reconnectionTimer = Timer.periodic(Duration(seconds: 5), (_) async { + if (wallet.syncStatus is LostConnectionSyncStatus || + wallet.syncStatus is FailedSyncStatus) { + try { + await wallet.connectToNode( + node: settingsStore.getCurrentNode(wallet.type)); + } catch (_) {} + } + }); await getIt .get<SharedPreferences>() @@ -87,30 +100,24 @@ Future<void> bootstrap({FiatConvertationService fiatConvertationService}) async .get<SharedPreferences>() .setInt('current_wallet_type', serializeToInt(wallet.type)); - await wallet.connectToNode(node: null); - + final node = settingsStore.getCurrentNode(wallet.type); final cryptoCurrency = wallet.currency; final fiatCurrency = settingsStore.fiatCurrency; + await wallet.connectToNode(node: node); + final price = await fiatConvertationService.getPrice( - crypto: cryptoCurrency, - fiat: fiatCurrency - ); + crypto: cryptoCurrency, fiat: fiatCurrency); fiatConvertationStore.setPrice(price); }); - // - - _onCurrentFiatCurrencyChangeDisposer ??= - reaction((_) => settingsStore.fiatCurrency, - (FiatCurrency fiatCurrency) async { + _onCurrentFiatCurrencyChangeDisposer ??= reaction( + (_) => settingsStore.fiatCurrency, (FiatCurrency fiatCurrency) async { final cryptoCurrency = getIt.get<AppStore>().wallet.currency; final price = await fiatConvertationService.getPrice( - crypto: cryptoCurrency, - fiat: fiatCurrency - ); + crypto: cryptoCurrency, fiat: fiatCurrency); fiatConvertationStore.setPrice(price); }); diff --git a/lib/router.dart b/lib/router.dart index 6c03dc1dd..68e010293 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -358,7 +358,9 @@ class Router { case Routes.exchangeTrade: return CupertinoPageRoute<void>( - builder: (_) => MultiProvider( + builder: (_) => getIt.get<ExchangeTradePage>()); + + /*MultiProvider( providers: [ ProxyProvider<SettingsStore, ExchangeTradeStore>( update: (_, settingsStore, __) => ExchangeTradeStore( @@ -374,12 +376,13 @@ class Router { priceStore: priceStore)), ], child: ExchangeTradePage(), - )); + ));*/ case Routes.exchangeConfirm: return MaterialPageRoute<void>( - builder: (_) => - ExchangeConfirmPage(trade: settings.arguments as Trade)); + builder: (_) => getIt.get<ExchangeConfirmPage>()); + + //ExchangeConfirmPage(trade: settings.arguments as Trade)); case Routes.tradeDetails: return MaterialPageRoute<void>(builder: (context) { diff --git a/lib/src/domain/common/default_settings_migration.dart b/lib/src/domain/common/default_settings_migration.dart index d89223a4d..4ad41dff2 100644 --- a/lib/src/domain/common/default_settings_migration.dart +++ b/lib/src/domain/common/default_settings_migration.dart @@ -29,15 +29,19 @@ Future defaultSettingsMigration( switch (version) { case 1: await sharedPreferences.setString( - SettingsStoreBase.currentFiatCurrencyKey, FiatCurrency.usd.toString()); + SettingsStoreBase.currentFiatCurrencyKey, + FiatCurrency.usd.toString()); await sharedPreferences.setInt( - SettingsStoreBase.currentTransactionPriorityKey, TransactionPriority.standart.raw); + SettingsStoreBase.currentTransactionPriorityKey, + TransactionPriority.standart.raw); await sharedPreferences.setInt( SettingsStoreBase.currentBalanceDisplayModeKey, BalanceDisplayMode.availableBalance.raw); await sharedPreferences.setBool('save_recipient_address', true); await resetToDefault(nodes); - await changeCurrentNodeToDefault( + await changeMoneroCurrentNodeToDefault( + sharedPreferences: sharedPreferences, nodes: nodes); + await changeBitcoinCurrentElectrumServerToDefault( sharedPreferences: sharedPreferences, nodes: nodes); break; @@ -50,6 +54,11 @@ Future defaultSettingsMigration( case 3: await updateNodeTypes(nodes: nodes); await addBitcoinElectrumServerList(nodes: nodes); + + break; + case 4: + await changeBitcoinCurrentElectrumServerToDefault( + sharedPreferences: sharedPreferences, nodes: nodes); break; default: break; @@ -69,10 +78,11 @@ Future defaultSettingsMigration( Future<void> replaceNodesMigration({@required Box<Node> nodes}) async { final replaceNodes = <String, Node>{ 'eu-node.cakewallet.io:18081': - Node(uri: 'xmr-node-eu.cakewallet.com:18081'), - 'node.cakewallet.io:18081': - Node(uri: 'xmr-node-usa-east.cakewallet.com:18081'), - 'node.xmr.ru:13666': Node(uri: 'node.monero.net:18081') + Node(uri: 'xmr-node-eu.cakewallet.com:18081', type: WalletType.monero), + 'node.cakewallet.io:18081': Node( + uri: 'xmr-node-usa-east.cakewallet.com:18081', type: WalletType.monero), + 'node.xmr.ru:13666': + Node(uri: 'node.monero.net:18081', type: WalletType.monero) }; nodes.values.forEach((Node node) async { @@ -87,11 +97,27 @@ Future<void> replaceNodesMigration({@required Box<Node> nodes}) async { }); } -Future<void> changeCurrentNodeToDefault( +Future<void> changeMoneroCurrentNodeToDefault( {@required SharedPreferences sharedPreferences, @required Box<Node> nodes}) async { + final node = getMoneroDefaultNode(nodes: nodes); + final nodeId = node?.key as int ?? 0; // 0 - England + + await sharedPreferences.setInt('current_node_id', nodeId); +} + +Node getBitcoinDefaultElectrumServer({@required Box<Node> nodes}) { + final uri = 'bitcoin.electrumx.multicoin.co:50002'; + + return nodes.values + .firstWhere((Node node) => node.uri == uri, orElse: () => null) ?? + nodes.values.firstWhere((node) => node.type == WalletType.bitcoin, + orElse: () => null); +} + +Node getMoneroDefaultNode({@required Box<Node> nodes}) { final timeZone = DateTime.now().timeZoneOffset.inHours; - String nodeUri = ''; + var nodeUri = ''; if (timeZone >= 1) { // Eurasia @@ -101,11 +127,18 @@ Future<void> changeCurrentNodeToDefault( nodeUri = 'xmr-node-usa-east.cakewallet.com:18081'; } - final node = nodes.values.firstWhere((Node node) => node.uri == nodeUri) ?? + return nodes.values + .firstWhere((Node node) => node.uri == nodeUri, orElse: () => null) ?? nodes.values.first; - final nodeId = node != null ? node.key as int : 0; // 0 - England +} - await sharedPreferences.setInt('current_node_id', nodeId); +Future<void> changeBitcoinCurrentElectrumServerToDefault( + {@required SharedPreferences sharedPreferences, + @required Box<Node> nodes}) async { + final server = getBitcoinDefaultElectrumServer(nodes: nodes); + final serverId = server?.key as int ?? 0; + + await sharedPreferences.setInt('current_node_id_btc', serverId); } Future<void> replaceDefaultNode( @@ -126,7 +159,7 @@ Future<void> replaceDefaultNode( return; } - await changeCurrentNodeToDefault( + await changeMoneroCurrentNodeToDefault( sharedPreferences: sharedPreferences, nodes: nodes); } diff --git a/lib/src/domain/common/node.dart b/lib/src/domain/common/node.dart index f5b91547d..b47f002e9 100644 --- a/lib/src/domain/common/node.dart +++ b/lib/src/domain/common/node.dart @@ -9,12 +9,16 @@ part 'node.g.dart'; @HiveType(typeId: 1) class Node extends HiveObject { - Node({@required this.uri, @required WalletType type, 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, + : uri = map['uri'] as String ?? '', login = map['login'] as String, password = map['password'] as String, typeRaw = map['typeRaw'] as int; diff --git a/lib/src/domain/common/node_list.dart b/lib/src/domain/common/node_list.dart index ad5db8a5d..67bfe6eac 100644 --- a/lib/src/domain/common/node_list.dart +++ b/lib/src/domain/common/node_list.dart @@ -10,7 +10,10 @@ Future<List<Node>> loadDefaultNodes() async { return nodes.map((dynamic raw) { if (raw is Map) { - return Node.fromMap(raw); + final node = Node.fromMap(raw); + node?.type = WalletType.monero; + + return node; } return null; @@ -38,13 +41,7 @@ Future resetToDefault(Box<Node> nodeSource) async { final moneroNodes = await loadDefaultNodes(); final bitcoinElectrumServerList = await loadElectrumServerList(); final nodes = moneroNodes + bitcoinElectrumServerList; - final entities = <int, Node>{}; await nodeSource.clear(); - - for (var i = 0; i < nodes.length; i++) { - entities[i] = nodes[i]; - } - - await nodeSource.putAll(entities); + await nodeSource.addAll(nodes); } diff --git a/lib/src/domain/common/sync_status.dart b/lib/src/domain/common/sync_status.dart index 3e2eae4a2..3ee2c6b83 100644 --- a/lib/src/domain/common/sync_status.dart +++ b/lib/src/domain/common/sync_status.dart @@ -73,3 +73,11 @@ class ConnectedSyncStatus extends SyncStatus { @override String title() => S.current.sync_status_connected; } + +class LostConnectionSyncStatus extends SyncStatus { + @override + double progress() => 1.0; + + @override + String title() => S.current.sync_status_failed_connect; +} \ No newline at end of file diff --git a/lib/src/domain/common/transaction_info.dart b/lib/src/domain/common/transaction_info.dart index d954fb72f..e5da3bed6 100644 --- a/lib/src/domain/common/transaction_info.dart +++ b/lib/src/domain/common/transaction_info.dart @@ -1,11 +1,13 @@ import 'package:cake_wallet/src/domain/common/transaction_direction.dart'; abstract class TransactionInfo extends Object { + String id; int amount; TransactionDirection direction; bool isPending; DateTime date; int height; + int confirmations; String amountFormatted(); String fiatAmount(); void changeFiatAmount(String amount); diff --git a/lib/src/screens/contact/contact_list_page.dart b/lib/src/screens/contact/contact_list_page.dart index f07d5e50e..40b38cca4 100644 --- a/lib/src/screens/contact/contact_list_page.dart +++ b/lib/src/screens/contact/contact_list_page.dart @@ -9,6 +9,7 @@ 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'; +import 'package:cake_wallet/src/widgets/standard_list.dart'; class ContactListPage extends BasePage { ContactListPage(this.contactListViewModel, {this.isEditable = true}); @@ -30,7 +31,7 @@ class ContactListPage extends BasePage { height: 32.0, decoration: BoxDecoration( shape: BoxShape.circle, - color: Theme.of(context).accentTextTheme.title.backgroundColor), + color: Theme.of(context).accentTextTheme.caption.color), child: Stack( alignment: Alignment.center, children: <Widget>[ @@ -54,30 +55,30 @@ class ContactListPage extends BasePage { @override Widget body(BuildContext context) { + final shortDivider = Container( + height: 1, + padding: EdgeInsets.only(left: 24), + color: Theme.of(context).backgroundColor, + child: Container( + height: 1, + color: Theme.of(context).primaryTextTheme.title.backgroundColor, + ), + ); + 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( + ? SectionStandardList( + sectionCount: 1, + context: context, + itemCounter: (int sectionIndex) => contactListViewModel.contacts.length, + itemBuilder: (_, sectionIndex, index) { + final contact = contactListViewModel.contacts[index]; + final image = _getCurrencyImage(contact.type); + final content = Builder( + builder: (context) => GestureDetector( onTap: () async { if (!isEditable) { Navigator.of(context).pop(contact); @@ -106,10 +107,6 @@ class ContactListPage extends BasePage { children: <Widget>[ 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), @@ -117,7 +114,7 @@ class ContactListPage extends BasePage { mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: - CrossAxisAlignment.center, + CrossAxisAlignment.center, children: <Widget>[ image ?? Offstage(), Padding( @@ -128,6 +125,7 @@ class ContactListPage extends BasePage { contact.name, style: TextStyle( fontSize: 14, + fontWeight: FontWeight.normal, color: Theme.of(context) .primaryTextTheme .title @@ -139,57 +137,55 @@ class ContactListPage extends BasePage { ), ], ), - ); + ) + ); - return !isEditable - ? content - : Slidable( - key: Key('${contact.key}'), - actionPane: SlidableDrawerActionPane(), - child: content, - secondaryActions: <Widget>[ - 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; + return !isEditable + ? content + : Slidable( + key: Key('${contact.key}'), + actionPane: SlidableDrawerActionPane(), + child: content, + secondaryActions: <Widget>[ + 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), - ), - ); - }) + 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), + color: Colors.grey, fontSize: 14), ), ); diff --git a/lib/src/screens/contact/contact_page.dart b/lib/src/screens/contact/contact_page.dart index 5aac1a8e7..67f83cb66 100644 --- a/lib/src/screens/contact/contact_page.dart +++ b/lib/src/screens/contact/contact_page.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/palette.dart'; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; @@ -30,7 +31,7 @@ class ContactPage extends BasePage { .addListener(() => contactViewModel.address = _addressController.text); autorun((_) => - _currencyTypeController.text = contactViewModel.currency.toString()); + _currencyTypeController.text = contactViewModel.currency?.toString()??''); } @override @@ -45,7 +46,7 @@ class ContactPage extends BasePage { @override Widget body(BuildContext context) { final downArrow = Image.asset('assets/images/arrow_bottom_purple_icon.png', - color: Theme.of(context).dividerColor, height: 8); + color: Theme.of(context).primaryTextTheme.overline.color, height: 8); reaction((_) => contactViewModel.state, (ContactViewModelState state) { if (state is ContactCreationFailure) { @@ -57,79 +58,83 @@ class ContactPage extends BasePage { } }); - return Container( - color: Theme.of(context).backgroundColor, - child: ScrollableWithBottomSection( - contentPadding: EdgeInsets.all(24), - content: Form( - key: _formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - children: <Widget>[ - 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: <Widget>[downArrow], - ), - )), - ), + return ScrollableWithBottomSection( + contentPadding: EdgeInsets.all(24), + content: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: <Widget>[ + 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: <Widget>[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: <Widget>[ - 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))) + Padding( + padding: EdgeInsets.only(top: 20), + child: Observer( + builder: (_) => AddressTextField( + controller: _addressController, + options: [AddressTextFieldOption.qrCode], + buttonColor: Theme.of(context).accentTextTheme.display2.color, + iconColor: PaletteDark.gray, + borderColor: Theme.of(context).primaryTextTheme.title.backgroundColor, + validator: AddressValidator( + type: contactViewModel.currency), + )), + ) ], - )), - ); + ), + ), + bottomSectionPadding: + EdgeInsets.only(left: 24, right: 24, bottom: 24), + bottomSection: Row( + children: <Widget>[ + Expanded( + child: PrimaryButton( + onPressed: () { + contactViewModel.reset(); + _nameController.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; + } + + await contactViewModel.save(); + }, + text: S.of(context).save, + color: Palette.blueCraiola, + textColor: Colors.white, + isDisabled: !contactViewModel.isReady))) + ], + )); } void _presentCurrencyPicker(BuildContext context) { diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart index 495d58cec..df93362b4 100644 --- a/lib/src/screens/dashboard/dashboard_page.dart +++ b/lib/src/screens/dashboard/dashboard_page.dart @@ -27,25 +27,20 @@ class DashboardPage extends BasePage { @override Widget Function(BuildContext, Widget) get rootWrapper => - (BuildContext context, Widget scaffold) => Container( + (BuildContext context, Widget scaffold) => Container( decoration: BoxDecoration( gradient: LinearGradient(colors: [ - Theme.of(context).accentColor, - Theme.of(context).scaffoldBackgroundColor, - Theme.of(context).primaryColor, - ], - begin: Alignment.topRight, - end: Alignment.bottomLeft)), + Theme.of(context).accentColor, + Theme.of(context).scaffoldBackgroundColor, + Theme.of(context).primaryColor, + ], begin: Alignment.topRight, end: Alignment.bottomLeft)), child: scaffold); @override bool get resizeToAvoidBottomPadding => false; @override - Widget get endDrawer => MenuWidget( - name: walletViewModel.name, - subname: walletViewModel.subname, - type: walletViewModel.type); + Widget get endDrawer => MenuWidget(walletViewModel); @override Widget middle(BuildContext context) { @@ -54,20 +49,18 @@ class DashboardPage extends BasePage { @override Widget trailing(BuildContext context) { - final menuButton = Image.asset('assets/images/menu.png', - color: Colors.white); + final menuButton = + Image.asset('assets/images/menu.png', color: Colors.white); return Container( - alignment: Alignment.centerRight, - width: 40, - child: FlatButton( - highlightColor: Colors.transparent, - splashColor: Colors.transparent, - padding: EdgeInsets.all(0), - onPressed: () => onOpenEndDrawer(), - child: menuButton - ) - ); + alignment: Alignment.centerRight, + width: 40, + child: FlatButton( + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + padding: EdgeInsets.all(0), + onPressed: () => onOpenEndDrawer(), + child: menuButton)); } final DashboardViewModel walletViewModel; @@ -85,77 +78,64 @@ class DashboardPage extends BasePage { @override Widget body(BuildContext context) { - _setEffects(); return SafeArea( - child: Column( - mainAxisSize: MainAxisSize.max, - children: <Widget>[ - Expanded( + child: Column( + mainAxisSize: MainAxisSize.max, + children: <Widget>[ + Expanded( child: PageView.builder( - controller: controller, - itemCount: pages.length, - itemBuilder: (context, index) { - return pages[index]; - } - ) - ), - Padding( - padding: EdgeInsets.only( - bottom: 24 - ), + controller: controller, + itemCount: pages.length, + itemBuilder: (context, index) { + return pages[index]; + })), + Padding( + padding: EdgeInsets.only(bottom: 24), child: SmoothPageIndicator( controller: controller, - count: pages.length, + count: pages.length, effect: ColorTransitionEffect( - spacing: 6.0, - radius: 6.0, - dotWidth: 6.0, - dotHeight: 6.0, - dotColor: Theme.of(context).indicatorColor, - activeDotColor: Colors.white - ), - ) - ), - Container( - width: double.infinity, - padding: EdgeInsets.only( - left: 45, - right: 45, - bottom: 24 - ), - child: Row( - children: <Widget>[ - Flexible( - child: ActionButton( - image: sendImage, - title: S.of(context).send, - route: Routes.send, - alignment: Alignment.centerLeft, - ), + spacing: 6.0, + radius: 6.0, + dotWidth: 6.0, + dotHeight: 6.0, + dotColor: Theme.of(context).indicatorColor, + activeDotColor: Colors.white), + )), + Container( + width: double.infinity, + padding: EdgeInsets.only(left: 45, right: 45, bottom: 24), + child: Row( + children: <Widget>[ + Flexible( + child: ActionButton( + image: sendImage, + title: S.of(context).send, + route: Routes.send, + alignment: Alignment.centerLeft, ), - Flexible( - child: ActionButton( + ), + Flexible( + child: ActionButton( image: exchangeImage, title: S.of(context).exchange, - route: Routes.exchange - ), + route: Routes.exchange), + ), + Flexible( + child: ActionButton( + image: receiveImage, + title: S.of(context).receive, + route: Routes.receive, + alignment: Alignment.centerRight, ), - Flexible( - child: ActionButton( - image: receiveImage, - title: S.of(context).receive, - route: Routes.receive, - alignment: Alignment.centerRight, - ), - ) - ], - ), - ) - ], - ) - ); + ) + ], + ), + ) + ], + )); } void _setEffects() { diff --git a/lib/src/screens/dashboard/wallet_menu.dart b/lib/src/screens/dashboard/wallet_menu.dart index a3c4666f0..970859339 100644 --- a/lib/src/screens/dashboard/wallet_menu.dart +++ b/lib/src/screens/dashboard/wallet_menu.dart @@ -6,8 +6,10 @@ import 'package:cake_wallet/src/stores/wallet/wallet_store.dart'; import 'package:cake_wallet/src/screens/auth/auth_page.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; +// FIXME: terrible design + class WalletMenu { - WalletMenu(this.context); + WalletMenu(this.context, this.reconnect); final List<String> items = [ S.current.reconnect, @@ -30,6 +32,7 @@ class WalletMenu { ]; final BuildContext context; + final Future<void> Function() reconnect; void action(int index) { switch (index) { @@ -70,8 +73,6 @@ class WalletMenu { } Future<void> _presentReconnectAlert(BuildContext context) async { - final walletStore = Provider.of<WalletStore>(context); - await showDialog<void>( context: context, builder: (BuildContext context) { @@ -80,12 +81,11 @@ class WalletMenu { alertContent: S.of(context).reconnect_alert_text, leftButtonText: S.of(context).ok, rightButtonText: S.of(context).cancel, - actionLeftButton: () { - walletStore.reconnect(); + actionLeftButton: () async { + await reconnect?.call(); Navigator.of(context).pop(); }, - actionRightButton: () => Navigator.of(context).pop() - ); + actionRightButton: () => Navigator.of(context).pop()); }); } } diff --git a/lib/src/screens/dashboard/widgets/filter_tile.dart b/lib/src/screens/dashboard/widgets/filter_tile.dart new file mode 100644 index 000000000..3dbd5b5bc --- /dev/null +++ b/lib/src/screens/dashboard/widgets/filter_tile.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; + +class FilterTile extends StatelessWidget { + FilterTile({@required this.child}); + + final Widget child; + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + padding: EdgeInsets.only( + top: 18, + bottom: 18, + left: 24, + right: 24 + ), + child: child, + ); + } +} \ No newline at end of file diff --git a/lib/src/screens/dashboard/widgets/filter_widget.dart b/lib/src/screens/dashboard/widgets/filter_widget.dart new file mode 100644 index 000000000..d5197c355 --- /dev/null +++ b/lib/src/screens/dashboard/widgets/filter_widget.dart @@ -0,0 +1,155 @@ +import 'dart:ui'; +import 'package:cake_wallet/palette.dart'; +import 'package:cake_wallet/src/screens/dashboard/widgets/filter_tile.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:cake_wallet/src/widgets/alert_background.dart'; +import 'package:cake_wallet/src/widgets/alert_close_button.dart'; +import 'package:cake_wallet/src/widgets/checkbox_widget.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:date_range_picker/date_range_picker.dart' as date_rage_picker; + +class FilterWidget extends StatelessWidget { + FilterWidget({@required this.dashboardViewModel}); + + final DashboardViewModel dashboardViewModel; + final backVector = Image.asset('assets/images/back_vector.png', + color: Palette.darkBlueCraiola + ); + + @override + Widget build(BuildContext context) { + return AlertBackground( + child: Stack( + alignment: Alignment.center, + children: <Widget>[ + Column( + mainAxisSize: MainAxisSize.min, + children: <Widget>[ + Text( + S.of(context).filters, + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + fontFamily: 'Poppins', + decoration: TextDecoration.none, + ), + ), + Padding( + padding: EdgeInsets.only( + left: 24, + right: 24, + top: 24 + ), + child: ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(14)), + child: Container( + color: Theme.of(context).textTheme.body2.decorationColor, + child: ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: dashboardViewModel.filterItems.length, + separatorBuilder: (context, _) => Container( + height: 1, + color: Theme.of(context).accentTextTheme.subhead.backgroundColor, + ), + itemBuilder: (_, index1) { + final title = dashboardViewModel.filterItems.keys.elementAt(index1); + final section = dashboardViewModel.filterItems.values.elementAt(index1); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: <Widget>[ + Padding( + padding: EdgeInsets.only( + top: 20, + left: 24, + right: 24 + ), + child: Text( + title, + style: TextStyle( + color: Theme.of(context).accentTextTheme.subhead.color, + fontSize: 16, + fontWeight: FontWeight.w500, + fontFamily: 'Poppins', + decoration: TextDecoration.none + ), + ), + ), + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: section.length, + separatorBuilder: (context, _) => Container( + height: 1, + padding: EdgeInsets.only(left: 24), + color: Theme.of(context).textTheme.body2.decorationColor, + child: Container( + height: 1, + color: Theme.of(context).accentTextTheme.subhead.backgroundColor, + ), + ), + itemBuilder: (_, index2) { + + final item = section[index2]; + final content = item.onChanged != null + ? CheckboxWidget( + value: item.value, + caption: item.caption, + onChanged: item.onChanged + ) + : GestureDetector( + onTap: () async { + final List<DateTime> 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) { + dashboardViewModel.transactionFilterStore + .changeStartDate(picked.first); + dashboardViewModel.transactionFilterStore + .changeEndDate(picked.last); + } + }, + child: Padding( + padding: EdgeInsets.only(left: 32), + child: Text( + item.caption, + style: TextStyle( + color: Theme.of(context).primaryTextTheme.title.color, + fontSize: 18, + fontFamily: 'Poppins', + fontWeight: FontWeight.w500, + decoration: TextDecoration.none + ), + ), + ), + ); + + return FilterTile(child: content); + }, + ) + ], + ); + }, + ), + ), + ), + ), + ], + ), + AlertCloseButton(image: backVector) + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/src/screens/dashboard/widgets/header_row.dart b/lib/src/screens/dashboard/widgets/header_row.dart index 7542e7371..a878ff11f 100644 --- a/lib/src/screens/dashboard/widgets/header_row.dart +++ b/lib/src/screens/dashboard/widgets/header_row.dart @@ -1,9 +1,7 @@ +import 'package:cake_wallet/src/screens/dashboard/widgets/filter_widget.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; -import 'package:cake_wallet/src/domain/exchange/exchange_provider_description.dart'; -import 'package:date_range_picker/date_range_picker.dart' as date_rage_picker; -import 'package:flutter_mobx/flutter_mobx.dart'; class HeaderRow extends StatelessWidget { HeaderRow({this.dashboardViewModel}); @@ -31,148 +29,13 @@ class HeaderRow extends StatelessWidget { color: Colors.white ), ), - PopupMenuButton<int>( - itemBuilder: (context) => [ - PopupMenuItem( - enabled: false, - value: -1, - child: Text(S.of(context).transactions, - style: TextStyle( - fontWeight: FontWeight.bold, - color: Theme.of(context).primaryTextTheme.title.color))), - PopupMenuItem( - value: 0, - child: Observer( - builder: (_) => Row( - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, - children: [ - Text(S.of(context).incoming, - style: TextStyle( - color: Theme.of(context).primaryTextTheme.title.color - ), - ), - Checkbox( - value: dashboardViewModel - .transactionFilterStore - .displayIncoming, - onChanged: (value) => dashboardViewModel - .transactionFilterStore - .toggleIncoming() - ) - ]))), - PopupMenuItem( - value: 1, - child: Observer( - builder: (_) => Row( - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, - children: [ - Text(S.of(context).outgoing, - style: TextStyle( - color: Theme.of(context).primaryTextTheme.title.color - ) - ), - Checkbox( - value: dashboardViewModel - .transactionFilterStore - .displayOutgoing, - onChanged: (value) => dashboardViewModel - .transactionFilterStore - .toggleOutgoing(), - ) - ]))), - PopupMenuItem( - value: 2, - child: - Text(S.of(context).transactions_by_date, - style: TextStyle( - color: Theme.of(context).primaryTextTheme.title.color - ) - )), - PopupMenuDivider(), - PopupMenuItem( - enabled: false, - value: -1, - child: Text(S.of(context).trades, - style: TextStyle( - fontWeight: FontWeight.bold, - color: Theme.of(context).primaryTextTheme.title.color))), - PopupMenuItem( - value: 3, - child: Observer( - builder: (_) => Row( - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, - children: [ - Text('XMR.TO', - style: TextStyle( - color: Theme.of(context).primaryTextTheme.title.color - ) - ), - Checkbox( - value: dashboardViewModel - .tradeFilterStore - .displayXMRTO, - onChanged: (value) => dashboardViewModel - .tradeFilterStore - .toggleDisplayExchange( - ExchangeProviderDescription - .xmrto), - ) - ]))), - PopupMenuItem( - value: 4, - child: Observer( - builder: (_) => Row( - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, - children: [ - Text('Change.NOW', - style: TextStyle( - color: Theme.of(context).primaryTextTheme.title.color - ) - ), - Checkbox( - value: dashboardViewModel - .tradeFilterStore - .displayChangeNow, - onChanged: (value) => dashboardViewModel - .tradeFilterStore - .toggleDisplayExchange( - ExchangeProviderDescription - .changeNow), - ) - ]))), - PopupMenuItem( - value: 5, - child: Observer( - builder: (_) => Row( - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, - children: [ - Text('MorphToken', - style: TextStyle( - color: Theme.of(context).primaryTextTheme.title.color - ) - ), - Checkbox( - value: dashboardViewModel - .tradeFilterStore - .displayMorphToken, - onChanged: (value) => dashboardViewModel - .tradeFilterStore - .toggleDisplayExchange( - ExchangeProviderDescription - .morphToken), - ) - ]))) - ], + GestureDetector( + onTap: () { + showDialog<void>( + context: context, + builder: (context) => FilterWidget(dashboardViewModel: dashboardViewModel) + ); + }, child: Container( height: 36, width: 36, @@ -182,27 +45,7 @@ class HeaderRow extends StatelessWidget { ), child: filterIcon, ), - 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))); - - if (picked != null && picked.length == 2) { - dashboardViewModel.transactionFilterStore - .changeStartDate(picked.first); - dashboardViewModel.transactionFilterStore - .changeEndDate(picked.last); - } - } - }, - ), + ) ], ), ); diff --git a/lib/src/screens/dashboard/widgets/menu_widget.dart b/lib/src/screens/dashboard/widgets/menu_widget.dart index 834026779..0e965aba7 100644 --- a/lib/src/screens/dashboard/widgets/menu_widget.dart +++ b/lib/src/screens/dashboard/widgets/menu_widget.dart @@ -1,29 +1,30 @@ import 'dart:ui'; -import 'package:cake_wallet/palette.dart'; import 'package:flutter/material.dart'; +import 'package:cake_wallet/palette.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cake_wallet/src/domain/common/wallet_type.dart'; import 'package:cake_wallet/src/screens/dashboard/wallet_menu.dart'; +import 'package:flutter/rendering.dart'; + +// FIXME: terrible design. class MenuWidget extends StatefulWidget { - MenuWidget({this.type, this.name, this.subname}); + MenuWidget(this.dashboardViewModel); - final WalletType type; - final String name; - final String subname; + final DashboardViewModel dashboardViewModel; @override MenuWidgetState createState() => MenuWidgetState(); } class MenuWidgetState extends State<MenuWidget> { - final moneroIcon = Image.asset('assets/images/monero_menu.png'); - final bitcoinIcon = Image.asset('assets/images/bitcoin_menu.png'); + Image moneroIcon; + Image bitcoinIcon; final largeScreen = 731; double menuWidth; double screenWidth; double screenHeight; - double opacity; double headerHeight; double tileHeight; @@ -35,12 +36,11 @@ class MenuWidgetState extends State<MenuWidget> { menuWidth = 0; screenWidth = 0; screenHeight = 0; - opacity = 0; - headerHeight = 125; + headerHeight = 137; tileHeight = 75; fromTopEdge = 50; - fromBottomEdge = 21; + fromBottomEdge = 30; super.initState(); WidgetsBinding.instance.addPostFrameCallback(afterLayout); @@ -52,7 +52,6 @@ class MenuWidgetState extends State<MenuWidget> { setState(() { menuWidth = screenWidth; - opacity = 1; if (screenHeight > largeScreen) { final scale = screenHeight / largeScreen; @@ -66,145 +65,163 @@ class MenuWidgetState extends State<MenuWidget> { @override Widget build(BuildContext context) { - final walletMenu = WalletMenu(context); + final walletMenu = + WalletMenu(context, () async => widget.dashboardViewModel.reconnect()); final itemCount = walletMenu.items.length; - return SafeArea( - child: Row( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: <Widget>[ - Padding( + moneroIcon = Image.asset('assets/images/monero_menu.png', + color: Theme.of(context).accentTextTheme.overline.decorationColor); + bitcoinIcon = Image.asset('assets/images/bitcoin_menu.png', + color: Theme.of(context).accentTextTheme.overline.decorationColor); + + return Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: <Widget>[ + Padding( padding: EdgeInsets.only(left: 24), child: Container( height: 60, width: 4, decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(2)), - color: PaletteDark.gray), + borderRadius: BorderRadius.all(Radius.circular(2)), + color: PaletteDark.gray), )), - SizedBox(width: 12), - Expanded( - child: ClipRRect( + SizedBox(width: 12), + Expanded( + child: ClipRRect( borderRadius: BorderRadius.only( - topLeft: Radius.circular(24), - bottomLeft: Radius.circular(24)), + topLeft: Radius.circular(24), + bottomLeft: Radius.circular(24)), child: Container( - width: menuWidth, - height: double.infinity, color: Theme.of(context).textTheme.body2.decorationColor, - child: SingleChildScrollView( - child: Column( - children: <Widget>[ - Container( - height: headerHeight, - color: Theme.of(context).textTheme.body2.color, - padding: EdgeInsets.only( - left: 24, - top: fromTopEdge, - right: 24, - bottom: fromBottomEdge), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: <Widget>[ - _iconFor(type: widget.type), - SizedBox(width: 12), - Expanded( - child: Container( - height: 42, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: widget.subname != null - ? MainAxisAlignment.spaceBetween - : MainAxisAlignment.center, - children: <Widget>[ - Text( - widget.name, - style: TextStyle( - color: Theme.of(context).textTheme - .display2.color, - fontSize: 16, - fontWeight: FontWeight.bold), - ), - if (widget.subname != null) - Text( - widget.subname, - style: TextStyle( - color: Theme.of(context) - .primaryTextTheme - .caption.color, - fontWeight: FontWeight.w500, - fontSize: 12), - ) - ], - ), - )) - ], - ), - ), - Container( - height: 1, - color: Theme.of(context).primaryTextTheme.caption.decorationColor, - ), - ListView.separated( - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemBuilder: (_, index) { - - final item = walletMenu.items[index]; - final image = walletMenu.images[index] ?? Offstage(); - final isLastTile = index == itemCount - 1; - - return GestureDetector( - onTap: () { - Navigator.of(context).pop(); - walletMenu.action(index); - }, - child: Container( - height: isLastTile - ? headerHeight - : tileHeight, - padding: isLastTile - ? EdgeInsets.only( - left: 24, - right: 24, - top: fromBottomEdge, - bottom: fromTopEdge) - : EdgeInsets.only(left: 24, right: 24), - alignment: isLastTile ? Alignment.topLeft : null, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, + child: ListView.separated( + padding: EdgeInsets.only(top: 0), + itemBuilder: (_, index) { + if (index == 0) { + return Container( + height: headerHeight, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Theme.of(context) + .accentTextTheme + .display1 + .color, + Theme.of(context) + .accentTextTheme + .display1 + .decorationColor, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight), + ), + padding: EdgeInsets.only( + left: 24, + top: fromTopEdge, + right: 24, + bottom: fromBottomEdge), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: <Widget>[ + _iconFor(type: widget.dashboardViewModel.type), + SizedBox(width: 12), + Expanded( + child: Container( + height: 42, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + mainAxisAlignment: + widget.dashboardViewModel.subname != + null + ? MainAxisAlignment.spaceBetween + : MainAxisAlignment.center, children: <Widget>[ - image, - SizedBox(width: 16), - Expanded( - child: Text( - item, - style: TextStyle( - color: Theme.of(context).textTheme - .display2.color, - fontSize: 16, - fontWeight: FontWeight.bold), - )) + Text( + widget.dashboardViewModel.name, + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold), + ), + if (widget.dashboardViewModel.subname != + null) + Text( + widget.dashboardViewModel.subname, + style: TextStyle( + color: Theme.of(context) + .accentTextTheme + .overline + .decorationColor, + fontWeight: FontWeight.w500, + fontSize: 12), + ) ], ), - ) - ); - }, - separatorBuilder: (_, index) => Container( - height: 1, - color: Theme.of(context).primaryTextTheme.caption.decorationColor, + )) + ], ), - itemCount: itemCount) - ], - ), - ), - ), - ) - ) - ], - ) + ); + } + + index--; + + final item = walletMenu.items[index]; + final image = walletMenu.images[index] ?? Offstage(); + final isLastTile = index == itemCount - 1; + + return GestureDetector( + onTap: () { + Navigator.of(context).pop(); + walletMenu.action(index); + }, + child: Container( + color: Theme.of(context) + .textTheme + .body2 + .decorationColor, + height: isLastTile ? headerHeight : tileHeight, + padding: isLastTile + ? EdgeInsets.only( + left: 24, + right: 24, + top: fromBottomEdge, + //bottom: fromTopEdge + ) + : EdgeInsets.only(left: 24, right: 24), + alignment: isLastTile ? Alignment.topLeft : null, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: <Widget>[ + image, + SizedBox(width: 16), + Expanded( + child: Text( + item, + style: TextStyle( + color: Theme.of(context) + .textTheme + .display2 + .color, + fontSize: 16, + fontWeight: FontWeight.bold), + )) + ], + ), + )); + }, + separatorBuilder: (_, index) => Container( + height: 1, + color: Theme.of(context) + .primaryTextTheme + .caption + .decorationColor, + ), + itemCount: itemCount + 1), + ))) + ], ); } diff --git a/lib/src/screens/exchange/exchange_page.dart b/lib/src/screens/exchange/exchange_page.dart index cc47033b4..51765380e 100644 --- a/lib/src/screens/exchange/exchange_page.dart +++ b/lib/src/screens/exchange/exchange_page.dart @@ -18,10 +18,13 @@ class ExchangePage extends BasePage { String get title => S.current.exchange; @override - Color get backgroundLightColor => PaletteDark.wildVioletBlue; + Color get titleColor => Colors.white; @override - Color get backgroundDarkColor => PaletteDark.wildVioletBlue; + Color get backgroundLightColor => Colors.transparent; + + @override + Color get backgroundDarkColor => Colors.transparent; @override Widget middle(BuildContext context) => @@ -36,5 +39,21 @@ class ExchangePage extends BasePage { @override Widget body(BuildContext context) => - BaseExchangeWidget(exchangeViewModel: exchangeViewModel); + BaseExchangeWidget( + exchangeViewModel: exchangeViewModel, + leading: leading(context), + middle: middle(context), + trailing: trailing(context), + ); + + @override + Widget build(BuildContext context) { + return Scaffold( + resizeToAvoidBottomPadding: resizeToAvoidBottomPadding, + body: Container( + color: Theme.of(context).backgroundColor, + child: body(context) + ) + ); + } } diff --git a/lib/src/screens/exchange/exchange_template_page.dart b/lib/src/screens/exchange/exchange_template_page.dart index 139a1e4cd..81bb54d1c 100644 --- a/lib/src/screens/exchange/exchange_template_page.dart +++ b/lib/src/screens/exchange/exchange_template_page.dart @@ -1,7 +1,6 @@ import 'dart:ui'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:cake_wallet/palette.dart'; import 'package:cake_wallet/src/screens/base_page.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'; @@ -17,10 +16,13 @@ class ExchangeTemplatePage extends BasePage { String get title => S.current.exchange_new_template; @override - Color get backgroundLightColor => PaletteDark.wildVioletBlue; + Color get titleColor => Colors.white; @override - Color get backgroundDarkColor => PaletteDark.wildVioletBlue; + Color get backgroundLightColor => Colors.transparent; + + @override + Color get backgroundDarkColor => Colors.transparent; @override Widget trailing(BuildContext context) => @@ -28,5 +30,22 @@ class ExchangeTemplatePage extends BasePage { @override Widget body(BuildContext context) => - BaseExchangeWidget(exchangeViewModel: exchangeViewModel, isTemplate: true); + BaseExchangeWidget( + exchangeViewModel: exchangeViewModel, + leading: leading(context), + middle: middle(context), + trailing: trailing(context), + isTemplate: true + ); + + @override + Widget build(BuildContext context) { + return Scaffold( + resizeToAvoidBottomPadding: resizeToAvoidBottomPadding, + body: Container( + color: Theme.of(context).backgroundColor, + child: body(context) + ) + ); + } } \ No newline at end of file diff --git a/lib/src/screens/exchange/widgets/base_exchange_widget.dart b/lib/src/screens/exchange/widgets/base_exchange_widget.dart index cde58e226..44e0162ae 100644 --- a/lib/src/screens/exchange/widgets/base_exchange_widget.dart +++ b/lib/src/screens/exchange/widgets/base_exchange_widget.dart @@ -25,29 +25,43 @@ import 'package:cake_wallet/core/amount_validator.dart'; class BaseExchangeWidget extends StatefulWidget { BaseExchangeWidget({ - @ required this.exchangeViewModel, + @required this.exchangeViewModel, + this.leading, + this.middle, + this.trailing, this.isTemplate = false, }); final ExchangeViewModel exchangeViewModel; + final Widget leading; + final Widget middle; + final Widget trailing; final bool isTemplate; @override - BaseExchangeWidgetState createState() => - BaseExchangeWidgetState( - exchangeViewModel: exchangeViewModel, - isTemplate: isTemplate - ); + BaseExchangeWidgetState createState() => BaseExchangeWidgetState( + exchangeViewModel: exchangeViewModel, + leading: leading, + middle: middle, + trailing: trailing, + isTemplate: isTemplate); } class BaseExchangeWidgetState extends State<BaseExchangeWidget> { BaseExchangeWidgetState({ - @ required this.exchangeViewModel, - @ required this.isTemplate, + @required this.exchangeViewModel, + @required this.leading, + @required this.middle, + @required this.trailing, + @required this.isTemplate, }); final ExchangeViewModel exchangeViewModel; + final Widget leading; + final Widget middle; + final Widget trailing; final bool isTemplate; + final double topPanelHeight = 290; final depositKey = GlobalKey<ExchangeCardState>(); final receiveKey = GlobalKey<ExchangeCardState>(); @@ -68,264 +82,329 @@ class BaseExchangeWidgetState extends State<BaseExchangeWidget> { ); final depositWalletName = - exchangeViewModel.depositCurrency == CryptoCurrency.xmr - ? exchangeViewModel.wallet.name - : null; + exchangeViewModel.depositCurrency == CryptoCurrency.xmr + ? exchangeViewModel.wallet.name + : null; final receiveWalletName = - exchangeViewModel.receiveCurrency == CryptoCurrency.xmr - ? exchangeViewModel.wallet.name - : null; + exchangeViewModel.receiveCurrency == CryptoCurrency.xmr + ? exchangeViewModel.wallet.name + : null; - WidgetsBinding.instance.addPostFrameCallback( - (_) => _setReactions(context, exchangeViewModel)); + WidgetsBinding.instance + .addPostFrameCallback((_) => _setReactions(context, exchangeViewModel)); - return Container( - color: PaletteDark.backgroundColor, - child: Form( - key: _formKey, - child: ScrollableWithBottomSection( - contentPadding: EdgeInsets.only(bottom: 24), - content: Column( - children: <Widget>[ - TopPanel( - color: PaletteDark.darkNightBlue, - edgeInsets: EdgeInsets.only(bottom: 32), - widget: Column( - children: <Widget>[ - TopPanel( - edgeInsets: EdgeInsets.fromLTRB(24, 29, 24, 32), - color: PaletteDark.wildVioletBlue, - widget: Observer( - builder: (_) => ExchangeCard( - key: depositKey, - title: S.of(context).you_will_send, - initialCurrency: exchangeViewModel.depositCurrency, - initialWalletName: depositWalletName, - initialAddress: - exchangeViewModel.depositCurrency == exchangeViewModel.wallet.currency - ? exchangeViewModel.wallet.address - : exchangeViewModel.depositAddress, - initialIsAmountEditable: true, - initialIsAddressEditable: exchangeViewModel.isDepositAddressEnabled, - isAmountEstimated: false, - currencies: CryptoCurrency.all, - onCurrencySelected: (currency) => - exchangeViewModel.changeDepositCurrency(currency: currency), - imageArrow: arrowBottomPurple, - currencyButtonColor: PaletteDark.wildVioletBlue, - addressButtonsColor: PaletteDark.moderateBlue, - currencyValueValidator: AmountValidator( - type: exchangeViewModel.wallet.type), - addressTextFieldValidator: AddressValidator( - type: exchangeViewModel.depositCurrency), - ), - ) - ), - Padding( - padding: EdgeInsets.only(top: 29, left: 24, right: 24), - child: Observer( - builder: (_) => ExchangeCard( - key: receiveKey, - title: S.of(context).you_will_get, - initialCurrency: exchangeViewModel.receiveCurrency, - initialWalletName: receiveWalletName, - initialAddress: - exchangeViewModel.receiveCurrency == exchangeViewModel.wallet.currency - ? exchangeViewModel.wallet.address - : exchangeViewModel.receiveAddress, - initialIsAmountEditable: false, - initialIsAddressEditable: exchangeViewModel.isReceiveAddressEnabled, - isAmountEstimated: true, - currencies: CryptoCurrency.all, - onCurrencySelected: (currency) => exchangeViewModel - .changeReceiveCurrency(currency: currency), - imageArrow: arrowBottomCakeGreen, - currencyButtonColor: PaletteDark.darkNightBlue, - addressButtonsColor: PaletteDark.moderateBlue, - currencyValueValidator: AmountValidator( - type: exchangeViewModel.wallet.type), - addressTextFieldValidator: AddressValidator( - type: exchangeViewModel.receiveCurrency), - )), - ) - ], - ) - ), - isTemplate - ? Offstage() - : Padding( - padding: EdgeInsets.only( - top: 30, - left: 24, - bottom: 24 - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, + return Form( + key: _formKey, + child: ScrollableWithBottomSection( + contentPadding: EdgeInsets.only(bottom: 24), + content: Column( + children: <Widget>[ + TopPanel( + gradient: LinearGradient(colors: [ + Theme.of(context).primaryTextTheme.body1.color, + Theme.of(context).primaryTextTheme.body1.decorationColor, + ], stops: [ + 0.35, + 1.0 + ], begin: Alignment.topLeft, end: Alignment.bottomRight), + edgeInsets: EdgeInsets.only(bottom: 32), + widget: Column( children: <Widget>[ - Text( - S.of(context).send_templates, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: PaletteDark.darkCyanBlue - ), + TopPanel( + edgeInsets: EdgeInsets.all(0), + gradient: LinearGradient( + colors: [ + Theme.of(context) + .primaryTextTheme + .subtitle + .color, + Theme.of(context) + .primaryTextTheme + .subtitle + .decorationColor, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight), + widget: Column( + children: <Widget>[ + CupertinoNavigationBar( + leading: leading, + middle: middle, + trailing: trailing, + backgroundColor: Colors.transparent, + border: null, + ), + Padding( + padding: EdgeInsets.fromLTRB(24, 29, 24, 32), + child: Observer( + builder: (_) => ExchangeCard( + key: depositKey, + title: S.of(context).you_will_send, + initialCurrency: + exchangeViewModel.depositCurrency, + initialWalletName: depositWalletName, + initialAddress: exchangeViewModel + .depositCurrency == + exchangeViewModel.wallet.currency + ? exchangeViewModel.wallet.address + : exchangeViewModel.depositAddress, + initialIsAmountEditable: true, + initialIsAddressEditable: exchangeViewModel + .isDepositAddressEnabled, + isAmountEstimated: false, + currencies: CryptoCurrency.all, + onCurrencySelected: (currency) => + exchangeViewModel.changeDepositCurrency( + currency: currency), + imageArrow: arrowBottomPurple, + currencyButtonColor: Colors.transparent, + addressButtonsColor: + Theme.of(context).focusColor, + borderColor: Theme.of(context) + .primaryTextTheme + .body2 + .color, + currencyValueValidator: AmountValidator( + type: exchangeViewModel.wallet.type), + addressTextFieldValidator: AddressValidator( + type: + exchangeViewModel.depositCurrency), + ), + ), + ) + ], + )), + Padding( + padding: EdgeInsets.only(top: 29, left: 24, right: 24), + child: Observer( + builder: (_) => ExchangeCard( + key: receiveKey, + title: S.of(context).you_will_get, + initialCurrency: + exchangeViewModel.receiveCurrency, + initialWalletName: receiveWalletName, + initialAddress: + exchangeViewModel.receiveCurrency == + exchangeViewModel.wallet.currency + ? exchangeViewModel.wallet.address + : exchangeViewModel.receiveAddress, + initialIsAmountEditable: false, + initialIsAddressEditable: + exchangeViewModel.isReceiveAddressEnabled, + isAmountEstimated: true, + currencies: CryptoCurrency.all, + onCurrencySelected: (currency) => + exchangeViewModel.changeReceiveCurrency( + currency: currency), + imageArrow: arrowBottomCakeGreen, + currencyButtonColor: Colors.transparent, + addressButtonsColor: + Theme.of(context).focusColor, + borderColor: Theme.of(context) + .primaryTextTheme + .body2 + .decorationColor, + currencyValueValidator: AmountValidator( + type: exchangeViewModel.wallet.type), + addressTextFieldValidator: AddressValidator( + type: exchangeViewModel.receiveCurrency), + )), ) ], - ), - ), - isTemplate - ? Offstage() - : Container( - height: 40, - width: double.infinity, - padding: EdgeInsets.only(left: 24), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: <Widget>[ - GestureDetector( - onTap: () => Navigator.of(context) - .pushNamed(Routes.exchangeTemplate), - child: Container( - padding: EdgeInsets.only(left: 1, right: 10), - child: DottedBorder( - borderType: BorderType.RRect, - dashPattern: [6, 4], - color: PaletteDark.darkCyanBlue, - strokeWidth: 2, - radius: Radius.circular(20), - child: Container( - height: 34, - 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: PaletteDark.darkCyanBlue - ), - ), - ) - ), - ), - ), - Observer( - builder: (_) { - final templates = exchangeViewModel.templates; - final itemCount = exchangeViewModel.templates.length; - - return ListView.builder( - scrollDirection: Axis.horizontal, - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: itemCount, - itemBuilder: (context, index) { - final template = templates[index]; - - return TemplateTile( - key: UniqueKey(), - amount: template.amount, - from: template.depositCurrency, - to: template.receiveCurrency, - onTap: () { - applyTemplate(exchangeViewModel, template); - }, - onRemove: () { - showDialog<void>( - context: context, - builder: (dialogContext) { - return AlertWithTwoActions( - alertTitle: S.of(context).template, - alertContent: S.of(context).confirm_delete_template, - leftButtonText: S.of(context).delete, - rightButtonText: S.of(context).cancel, - actionLeftButton: () { - Navigator.of(dialogContext).pop(); - exchangeViewModel.exchangeTemplateStore.remove(template: template); - exchangeViewModel.exchangeTemplateStore.update(); - }, - actionRightButton: () => Navigator.of(dialogContext).pop() - ); - } - ); - }, - ); - } - ); - } - ), - ], - ) - ) - ) - ], - ), - bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), - bottomSection: Column(children: <Widget>[ - Padding( - padding: EdgeInsets.only(bottom: 15), - child: Observer(builder: (_) { - final description = - exchangeViewModel.provider is XMRTOExchangeProvider - ? S.of(context).amount_is_guaranteed - : S.of(context).amount_is_estimate; - return Center( - child: Text( - description, - style: TextStyle( - color: PaletteDark.darkCyanBlue, - fontWeight: FontWeight.w500, - fontSize: 12 + )), + isTemplate + ? Offstage() + : Padding( + padding: EdgeInsets.only(top: 30, left: 24, bottom: 24), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: <Widget>[ + Text( + S.of(context).send_templates, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Theme.of(context) + .primaryTextTheme + .display4 + .color), + ) + ], ), ), - ); - }), - ), isTemplate - ? PrimaryButton( - onPressed: () { - if (_formKey.currentState.validate()) { - exchangeViewModel.exchangeTemplateStore.addTemplate( - amount: exchangeViewModel.depositAmount, - depositCurrency: exchangeViewModel.depositCurrency.toString(), - receiveCurrency: exchangeViewModel.receiveCurrency.toString(), - provider: exchangeViewModel.provider.toString(), - depositAddress: exchangeViewModel.depositAddress, - receiveAddress: exchangeViewModel.receiveAddress - ); - exchangeViewModel.exchangeTemplateStore.update(); - Navigator.of(context).pop(); - } - }, - text: S.of(context).save, - color: Colors.green, - textColor: Colors.white - ) - : Observer( - builder: (_) => LoadingPrimaryButton( - text: S.of(context).exchange, + ? Offstage() + : Container( + height: 40, + width: double.infinity, + padding: EdgeInsets.only(left: 24), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: <Widget>[ + GestureDetector( + onTap: () => Navigator.of(context) + .pushNamed(Routes.exchangeTemplate), + child: Container( + padding: EdgeInsets.only(left: 1, right: 10), + child: DottedBorder( + borderType: BorderType.RRect, + dashPattern: [6, 4], + color: Theme.of(context) + .primaryTextTheme + .display2 + .decorationColor, + strokeWidth: 2, + radius: Radius.circular(20), + child: Container( + height: 34, + 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 + .display3 + .color), + ), + )), + ), + ), + Observer(builder: (_) { + final templates = exchangeViewModel.templates; + final itemCount = + exchangeViewModel.templates.length; + + return ListView.builder( + scrollDirection: Axis.horizontal, + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: itemCount, + itemBuilder: (context, index) { + final template = templates[index]; + + return TemplateTile( + key: UniqueKey(), + amount: template.amount, + from: template.depositCurrency, + to: template.receiveCurrency, + onTap: () { + applyTemplate( + exchangeViewModel, template); + }, + onRemove: () { + showDialog<void>( + context: context, + builder: (dialogContext) { + return AlertWithTwoActions( + alertTitle: + S.of(context).template, + alertContent: S + .of(context) + .confirm_delete_template, + leftButtonText: + S.of(context).delete, + rightButtonText: + S.of(context).cancel, + actionLeftButton: () { + Navigator.of( + dialogContext) + .pop(); + exchangeViewModel + .exchangeTemplateStore + .remove( + template: + template); + exchangeViewModel + .exchangeTemplateStore + .update(); + }, + actionRightButton: () => + Navigator.of( + dialogContext) + .pop()); + }); + }, + ); + }); + }), + ], + ))) + ], + ), + bottomSectionPadding: + EdgeInsets.only(left: 24, right: 24, bottom: 24), + bottomSection: Column(children: <Widget>[ + Padding( + padding: EdgeInsets.only(bottom: 15), + child: Observer(builder: (_) { + final description = + exchangeViewModel.provider is XMRTOExchangeProvider + ? S.of(context).amount_is_guaranteed + : S.of(context).amount_is_estimate; + return Center( + child: Text( + description, + style: TextStyle( + color: Theme.of(context) + .primaryTextTheme + .display4 + .decorationColor, + fontWeight: FontWeight.w500, + fontSize: 12), + ), + ); + }), + ), + isTemplate + ? PrimaryButton( onPressed: () { if (_formKey.currentState.validate()) { - exchangeViewModel.createTrade(); + exchangeViewModel.exchangeTemplateStore.addTemplate( + amount: exchangeViewModel.depositAmount, + depositCurrency: + exchangeViewModel.depositCurrency.toString(), + receiveCurrency: + exchangeViewModel.receiveCurrency.toString(), + provider: exchangeViewModel.provider.toString(), + depositAddress: exchangeViewModel.depositAddress, + receiveAddress: exchangeViewModel.receiveAddress); + exchangeViewModel.exchangeTemplateStore.update(); + Navigator.of(context).pop(); } }, - color: Colors.blue, - textColor: Colors.white, - isLoading: exchangeViewModel.tradeState is TradeIsCreating, - )), - ]), - )), - ); + text: S.of(context).save, + color: Colors.green, + textColor: Colors.white) + : Observer( + builder: (_) => LoadingPrimaryButton( + text: S.of(context).exchange, + onPressed: () { + if (_formKey.currentState.validate()) { + exchangeViewModel.createTrade(); + } + }, + color: Palette.blueCraiola, + textColor: Colors.white, + isLoading: + exchangeViewModel.tradeState is TradeIsCreating, + )), + ]), + )); } - void applyTemplate(ExchangeViewModel exchangeViewModel, - ExchangeTemplate template) { + void applyTemplate( + ExchangeViewModel exchangeViewModel, ExchangeTemplate template) { exchangeViewModel.changeDepositCurrency( currency: CryptoCurrency.fromString(template.depositCurrency)); exchangeViewModel.changeReceiveCurrency( @@ -374,27 +453,29 @@ class BaseExchangeWidgetState extends State<BaseExchangeWidget> { key.currentState.changeLimits(min: min, max: max); } - _onCurrencyChange(exchangeViewModel.receiveCurrency, exchangeViewModel, receiveKey); - _onCurrencyChange(exchangeViewModel.depositCurrency, exchangeViewModel, depositKey); + _onCurrencyChange( + exchangeViewModel.receiveCurrency, exchangeViewModel, receiveKey); + _onCurrencyChange( + exchangeViewModel.depositCurrency, exchangeViewModel, depositKey); reaction( - (_) => exchangeViewModel.wallet.name, - (String _) => _onWalletNameChange( + (_) => exchangeViewModel.wallet.name, + (String _) => _onWalletNameChange( exchangeViewModel, exchangeViewModel.receiveCurrency, receiveKey)); reaction( - (_) => exchangeViewModel.wallet.name, - (String _) => _onWalletNameChange( - exchangeViewModel, exchangeViewModel.depositCurrency, depositKey)); + (_) => exchangeViewModel.wallet.name, + (String _) => _onWalletNameChange( + exchangeViewModel, exchangeViewModel.depositCurrency, depositKey)); reaction( - (_) => exchangeViewModel.receiveCurrency, - (CryptoCurrency currency) => + (_) => exchangeViewModel.receiveCurrency, + (CryptoCurrency currency) => _onCurrencyChange(currency, exchangeViewModel, receiveKey)); reaction( - (_) => exchangeViewModel.depositCurrency, - (CryptoCurrency currency) => + (_) => exchangeViewModel.depositCurrency, + (CryptoCurrency currency) => _onCurrencyChange(currency, exchangeViewModel, depositKey)); reaction((_) => exchangeViewModel.depositAmount, (String amount) { @@ -409,7 +490,8 @@ class BaseExchangeWidgetState extends State<BaseExchangeWidget> { } }); - reaction((_) => exchangeViewModel.isDepositAddressEnabled, (bool isEnabled) { + reaction((_) => exchangeViewModel.isDepositAddressEnabled, + (bool isEnabled) { depositKey.currentState.isAddressEditable(isEditable: isEnabled); }); @@ -425,7 +507,8 @@ class BaseExchangeWidgetState extends State<BaseExchangeWidget> { } }); - reaction((_) => exchangeViewModel.isReceiveAddressEnabled, (bool isEnabled) { + reaction((_) => exchangeViewModel.isReceiveAddressEnabled, + (bool isEnabled) { receiveKey.currentState.isAddressEditable(isEditable: isEnabled); }); @@ -439,14 +522,12 @@ class BaseExchangeWidgetState extends State<BaseExchangeWidget> { alertTitle: S.of(context).error, alertContent: state.error, buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop() - ); + buttonAction: () => Navigator.of(context).pop()); }); }); } if (state is TradeIsCreatedSuccessfully) { - Navigator.of(context) - .pushNamed(Routes.exchangeConfirm, arguments: state.trade); + Navigator.of(context).pushNamed(Routes.exchangeConfirm); } }); @@ -474,20 +555,22 @@ class BaseExchangeWidgetState extends State<BaseExchangeWidget> { }); depositAddressController.addListener( - () => exchangeViewModel.depositAddress = depositAddressController.text); + () => exchangeViewModel.depositAddress = depositAddressController.text); depositAmountController.addListener(() { if (depositAmountController.text != exchangeViewModel.depositAmount) { - exchangeViewModel.changeDepositAmount(amount: depositAmountController.text); + exchangeViewModel.changeDepositAmount( + amount: depositAmountController.text); } }); receiveAddressController.addListener( - () => exchangeViewModel.receiveAddress = receiveAddressController.text); + () => exchangeViewModel.receiveAddress = receiveAddressController.text); receiveAmountController.addListener(() { if (receiveAmountController.text != exchangeViewModel.receiveAmount) { - exchangeViewModel.changeReceiveAmount(amount: receiveAmountController.text); + exchangeViewModel.changeReceiveAmount( + amount: receiveAmountController.text); } }); @@ -505,34 +588,31 @@ class BaseExchangeWidgetState extends State<BaseExchangeWidget> { } void _onCurrencyChange(CryptoCurrency currency, - ExchangeViewModel exchangeViewModel, - GlobalKey<ExchangeCardState> key) { + ExchangeViewModel exchangeViewModel, GlobalKey<ExchangeCardState> key) { final isCurrentTypeWallet = currency == exchangeViewModel.wallet.currency; key.currentState.changeSelectedCurrency(currency); - key.currentState - .changeWalletName(isCurrentTypeWallet - ? exchangeViewModel.wallet.name : null); + key.currentState.changeWalletName( + isCurrentTypeWallet ? exchangeViewModel.wallet.name : null); - key.currentState - .changeAddress(address: isCurrentTypeWallet - ? exchangeViewModel.wallet.address : ''); + key.currentState.changeAddress( + address: isCurrentTypeWallet ? exchangeViewModel.wallet.address : ''); key.currentState.changeAmount(amount: ''); } void _onWalletNameChange(ExchangeViewModel exchangeViewModel, - CryptoCurrency currency, - GlobalKey<ExchangeCardState> key) { + CryptoCurrency currency, GlobalKey<ExchangeCardState> key) { final isCurrentTypeWallet = currency == exchangeViewModel.wallet.currency; if (isCurrentTypeWallet) { key.currentState.changeWalletName(exchangeViewModel.wallet.name); - key.currentState.addressController.text = exchangeViewModel.wallet.address; + key.currentState.addressController.text = + exchangeViewModel.wallet.address; } else if (key.currentState.addressController.text == exchangeViewModel.wallet.address) { key.currentState.changeWalletName(null); key.currentState.addressController.text = null; } } -} \ No newline at end of file +} diff --git a/lib/src/screens/exchange/widgets/currency_picker.dart b/lib/src/screens/exchange/widgets/currency_picker.dart index 32398de65..2cc106b2c 100644 --- a/lib/src/screens/exchange/widgets/currency_picker.dart +++ b/lib/src/screens/exchange/widgets/currency_picker.dart @@ -1,8 +1,10 @@ import 'dart:ui'; +import 'package:cake_wallet/palette.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:cake_wallet/palette.dart'; import 'package:cake_wallet/src/domain/common/crypto_currency.dart'; +import 'package:cake_wallet/src/widgets/alert_background.dart'; +import 'package:cake_wallet/src/widgets/alert_close_button.dart'; class CurrencyPicker extends StatelessWidget { CurrencyPicker({ @@ -16,104 +18,103 @@ class CurrencyPicker extends StatelessWidget { final List<CryptoCurrency> items; final String title; final Function(CryptoCurrency) onItemSelected; + final closeButton = Image.asset('assets/images/close.png', + color: Palette.darkBlueCraiola, + ); @override Widget build(BuildContext context) { - return GestureDetector( - onTap: () => Navigator.of(context).pop(), - child: Container( - color: Colors.transparent, - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 3.0, sigmaY: 3.0), - child: Container( - decoration: BoxDecoration(color: PaletteDark.darkNightBlue.withOpacity(0.75)), - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: <Widget>[ - Container( - padding: EdgeInsets.only(left: 24, right: 24), - child: Text( - title, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - decoration: TextDecoration.none, - color: Colors.white - ), + return AlertBackground( + child: Stack( + alignment: Alignment.center, + children: <Widget>[ + Column( + mainAxisSize: MainAxisSize.min, + children: <Widget>[ + Container( + padding: EdgeInsets.only(left: 24, right: 24), + child: Text( + title, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 18, + fontFamily: 'Poppins', + fontWeight: FontWeight.bold, + decoration: TextDecoration.none, + color: Colors.white ), ), - Padding( - padding: EdgeInsets.only(top: 24), - child: GestureDetector( - onTap: () => null, - child: ClipRRect( - borderRadius: BorderRadius.all(Radius.circular(14)), - child: Container( - height: 400, - width: 300, - color: Theme.of(context).dividerColor, - child: GridView.count( - shrinkWrap: true, - crossAxisCount: 3, - childAspectRatio: 1.25, - physics: const NeverScrollableScrollPhysics(), - crossAxisSpacing: 1, - mainAxisSpacing: 1, - children: List.generate(15, (index) { + ), + Padding( + padding: EdgeInsets.only(top: 24), + child: GestureDetector( + onTap: () => null, + child: ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(14)), + child: Container( + height: 400, + width: 300, + color: Theme.of(context).accentTextTheme.title.backgroundColor, + child: GridView.count( + shrinkWrap: true, + crossAxisCount: 3, + childAspectRatio: 1.25, + physics: const NeverScrollableScrollPhysics(), + crossAxisSpacing: 1, + mainAxisSpacing: 1, + children: List.generate(15, (index) { - if (index == 14) { - return Container( - color: Theme.of(context).primaryTextTheme.display1.color, - ); - } + if (index == 14) { + return Container( + color: Theme.of(context).accentTextTheme.title.color, + ); + } - final item = items[index]; - final isItemSelected = index == selectedAtIndex; + final item = items[index]; + final isItemSelected = index == selectedAtIndex; - final color = isItemSelected - ? Theme.of(context).accentTextTheme.subtitle.decorationColor - : Theme.of(context).primaryTextTheme.display1.color; - final textColor = isItemSelected - ? Colors.blue - : Theme.of(context).primaryTextTheme.title.color; + final color = isItemSelected + ? Theme.of(context).textTheme.body2.color + : Theme.of(context).accentTextTheme.title.color; + final textColor = isItemSelected + ? Palette.blueCraiola + : Theme.of(context).primaryTextTheme.title.color; - return GestureDetector( - onTap: () { - if (onItemSelected == null) { - return; - } - Navigator.of(context).pop(); - onItemSelected(item); - }, - child: Container( - color: color, - child: Center( - child: Text( - item.toString(), - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - decoration: TextDecoration.none, - color: textColor - ), + return GestureDetector( + onTap: () { + if (onItemSelected == null) { + return; + } + Navigator.of(context).pop(); + onItemSelected(item); + }, + child: Container( + color: color, + child: Center( + child: Text( + item.toString(), + style: TextStyle( + fontSize: 18, + fontFamily: 'Poppins', + fontWeight: FontWeight.w600, + decoration: TextDecoration.none, + color: textColor ), ), ), - ); - }) - ), + ), + ); + }) ), ), ), - ) - ], - ), - ) - ), - ), - ), + ), + ) + ], + ), + AlertCloseButton(image: closeButton) + ], + ) ); } } \ No newline at end of file diff --git a/lib/src/screens/exchange/widgets/exchange_card.dart b/lib/src/screens/exchange/widgets/exchange_card.dart index 548383176..9be446fb8 100644 --- a/lib/src/screens/exchange/widgets/exchange_card.dart +++ b/lib/src/screens/exchange/widgets/exchange_card.dart @@ -5,7 +5,6 @@ import 'package:cake_wallet/src/domain/common/crypto_currency.dart'; import 'package:cake_wallet/src/widgets/address_text_field.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/palette.dart'; class ExchangeCard extends StatefulWidget { ExchangeCard( @@ -22,6 +21,7 @@ class ExchangeCard extends StatefulWidget { this.imageArrow, this.currencyButtonColor = Colors.transparent, this.addressButtonsColor = Colors.transparent, + this.borderColor = Colors.transparent, this.currencyValueValidator, this.addressTextFieldValidator}) : super(key: key); @@ -38,6 +38,7 @@ class ExchangeCard extends StatefulWidget { final Image imageArrow; final Color currencyButtonColor; final Color addressButtonsColor; + final Color borderColor; final FormFieldValidator<String> currencyValueValidator; final FormFieldValidator<String> addressTextFieldValidator; @@ -58,10 +59,6 @@ class ExchangeCardState extends State<ExchangeCard> { bool _isAddressEditable; bool _isAmountEstimated; - final copyImage = Image.asset('assets/images/copy_content.png', - height: 16, width: 16, - color: Colors.white); - @override void initState() { _title = widget.title; @@ -115,6 +112,10 @@ class ExchangeCardState extends State<ExchangeCard> { @override Widget build(BuildContext context) { + final copyImage = Image.asset('assets/images/copy_content.png', + height: 16, width: 16, + color: Theme.of(context).primaryTextTheme.display2.color); + return Container( width: double.infinity, color: Colors.transparent, @@ -129,7 +130,7 @@ class ExchangeCardState extends State<ExchangeCard> { style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, - color: PaletteDark.lightBlueGrey + color: Theme.of(context).textTheme.headline.color ), ) ], @@ -145,11 +146,12 @@ class ExchangeCardState extends State<ExchangeCard> { keyboardType: TextInputType.numberWithOptions( signed: false, decimal: true), inputFormatters: [ + LengthLimitingTextInputFormatter(15), BlacklistingTextInputFormatter( RegExp('[\\-|\\ |\\,]')) ], hintText: '0.0000', - borderColor: PaletteDark.blueGrey, + borderColor: widget.borderColor, textStyle: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, @@ -158,7 +160,7 @@ class ExchangeCardState extends State<ExchangeCard> { placeholderTextStyle: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, - color: PaletteDark.lightBlueGrey + color: Theme.of(context).textTheme.subhead.decorationColor ), validator: _isAmountEditable ? widget.currencyValueValidator @@ -206,7 +208,7 @@ class ExchangeCardState extends State<ExchangeCard> { style: TextStyle( fontSize: 10, height: 1.2, - color: PaletteDark.lightBlueGrey), + color: Theme.of(context).textTheme.subhead.decorationColor), ) : Offstage(), _min != null ? SizedBox(width: 10) : Offstage(), @@ -217,39 +219,45 @@ class ExchangeCardState extends State<ExchangeCard> { style: TextStyle( fontSize: 10, height: 1.2, - color: PaletteDark.lightBlueGrey)) + color: Theme.of(context).textTheme.subhead.decorationColor)) : Offstage(), ]), ), - Padding( + _isAddressEditable + ? Offstage() + : Padding( padding: EdgeInsets.only(top: 20), child: Text( - _isAddressEditable - ? S.of(context).widgets_address - : S.of(context).refund_address, + S.of(context).refund_address, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, - color: PaletteDark.lightBlueGrey + color: Theme.of(context).textTheme.subhead.decorationColor ), ) ), _isAddressEditable - ? AddressTextField( + ? Padding( + padding: EdgeInsets.only(top: 20), + child: AddressTextField( controller: addressController, options: [ AddressTextFieldOption.paste, AddressTextFieldOption.qrCode, AddressTextFieldOption.addressBook, ], - placeholder: '', isBorderExist: false, textStyle: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white), + hintStyle: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Theme.of(context).textTheme.subhead.decorationColor), buttonColor: widget.addressButtonsColor, validator: widget.addressTextFieldValidator, + ), ) : Padding( padding: EdgeInsets.only(top: 10), diff --git a/lib/src/screens/exchange/widgets/present_provider_picker.dart b/lib/src/screens/exchange/widgets/present_provider_picker.dart index 79fbb80cd..24b0f73a3 100644 --- a/lib/src/screens/exchange/widgets/present_provider_picker.dart +++ b/lib/src/screens/exchange/widgets/present_provider_picker.dart @@ -5,7 +5,6 @@ import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/widgets/picker.dart'; import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart'; -import 'package:cake_wallet/palette.dart'; class PresentProviderPicker extends StatelessWidget { PresentProviderPicker({@required this.exchangeViewModel}); @@ -41,7 +40,7 @@ class PresentProviderPicker extends StatelessWidget { style: TextStyle( fontSize: 10.0, fontWeight: FontWeight.w500, - color: PaletteDark.lightBlueGrey))) + color: Theme.of(context).textTheme.headline.color))) ], ), SizedBox(width: 5), diff --git a/lib/src/screens/exchange_trade/exchange_confirm_page.dart b/lib/src/screens/exchange_trade/exchange_confirm_page.dart index 97d3b316c..7cdb61c47 100644 --- a/lib/src/screens/exchange_trade/exchange_confirm_page.dart +++ b/lib/src/screens/exchange_trade/exchange_confirm_page.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/store/dashboard/trades_store.dart'; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; @@ -6,10 +7,12 @@ 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/src/domain/exchange/trade.dart'; +import 'package:cake_wallet/palette.dart'; class ExchangeConfirmPage extends BasePage { - ExchangeConfirmPage({@required this.trade}); + ExchangeConfirmPage({@required this.tradesStore}) : trade = tradesStore.trade; + final TradesStore tradesStore; final Trade trade; @override @@ -17,93 +20,102 @@ class ExchangeConfirmPage extends BasePage { @override Widget body(BuildContext context) { - final copyImage = Image.asset('assets/images/copy_content.png', - color: Theme.of(context).primaryTextTheme.title.color); - return Container( - padding: EdgeInsets.all(24), + padding: EdgeInsets.fromLTRB(24, 0, 24, 24), child: Column( children: <Widget>[ Expanded( - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: <Widget>[ - Text( - S.of(context).exchange_result_write_down_trade_id, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.w600, - color: Theme.of(context).primaryTextTheme.title.color), - ), - Padding( - padding: EdgeInsets.only(top: 60), + child: Column( + children: <Widget>[ + Flexible( + child: Center( child: Text( - S.of(context).trade_id, + S.of(context).exchange_result_write_down_trade_id, textAlign: TextAlign.center, style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.w600, - color: Theme.of(context).primaryTextTheme.title.color), + fontSize: 18.0, + fontWeight: FontWeight.w500, + color: Theme.of(context).primaryTextTheme.title.color), ), + ) + ), + Container( + height: 178, + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(30)), + border: Border.all( + width: 1, + color: Theme.of(context).accentTextTheme.caption.color + ), + color: Theme.of(context).accentTextTheme.title.color ), - Padding( - padding: EdgeInsets.only(top: 24), - child: Builder( - builder: (context) => GestureDetector( - onTap: () { - Clipboard.setData(ClipboardData(text: trade.id)); - Scaffold.of(context).showSnackBar(SnackBar( - content: Text( - S.of(context).copied_to_clipboard, - textAlign: TextAlign.center, - style: TextStyle(color: Colors.white), - ), - backgroundColor: Colors.green, - duration: Duration(milliseconds: 1500), - )); - }, - child: Container( - height: 60, - padding: EdgeInsets.only(left: 24, right: 24), - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(30)), - color: Theme.of(context).accentTextTheme.title.backgroundColor - ), - child: Row( - mainAxisSize: MainAxisSize.max, + child: Column( + children: <Widget>[ + Expanded( + child: Padding( + padding: EdgeInsets.all(24), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ - Expanded( - child: Text( - trade.id, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Theme.of(context).primaryTextTheme.title.color - ), + Text( + S.of(context).trade_id, + style: TextStyle( + fontSize: 12.0, + fontWeight: FontWeight.w500, + color: Theme.of(context).primaryTextTheme.overline.color + ), + ), + Text( + trade.id, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + color: Theme.of(context).primaryTextTheme.title.color ), ), - Padding( - padding: EdgeInsets.only(left: 12), - child: copyImage, - ) ], ), + ) + ), + Padding( + padding: EdgeInsets.fromLTRB(10, 0, 10, 10), + child: Builder( + builder: (context) => PrimaryButton( + onPressed: () { + Clipboard.setData(ClipboardData(text: trade.id)); + Scaffold.of(context).showSnackBar(SnackBar( + content: Text( + S.of(context).copied_to_clipboard, + textAlign: TextAlign.center, + style: TextStyle(color: Colors.white), + ), + backgroundColor: Colors.green, + duration: Duration(milliseconds: 1500), + )); + }, + text: S.of(context).copy_id, + color: Theme.of(context).accentTextTheme.caption.backgroundColor, + textColor: Theme.of(context).primaryTextTheme.title.color + ), ), ) - ), - ) - ], - ), - )), + ], + ), + ), + Flexible( + child: Offstage() + ), + ], + ) + ), PrimaryButton( onPressed: () => Navigator.of(context) - .pushReplacementNamed(Routes.exchangeTrade, arguments: trade), + .pushReplacementNamed(Routes.exchangeTrade), text: S.of(context).saved_the_trade_id, - color: Colors.green, + color: Palette.blueCraiola, textColor: Colors.white) ], ), diff --git a/lib/src/screens/exchange_trade/exchange_trade_item.dart b/lib/src/screens/exchange_trade/exchange_trade_item.dart new file mode 100644 index 000000000..4de324e9c --- /dev/null +++ b/lib/src/screens/exchange_trade/exchange_trade_item.dart @@ -0,0 +1,13 @@ +import 'package:flutter/cupertino.dart'; + +class ExchangeTradeItem { + ExchangeTradeItem({ + @required this.title, + @required this.data, + @required this.isCopied, + }); + + String title; + String data; + bool isCopied; +} \ No newline at end of file diff --git a/lib/src/screens/exchange_trade/exchange_trade_page.dart b/lib/src/screens/exchange_trade/exchange_trade_page.dart index 05602a7fd..61b1b024d 100644 --- a/lib/src/screens/exchange_trade/exchange_trade_page.dart +++ b/lib/src/screens/exchange_trade/exchange_trade_page.dart @@ -1,5 +1,10 @@ +import 'package:cake_wallet/palette.dart'; import 'package:cake_wallet/src/domain/common/crypto_currency.dart'; import 'package:cake_wallet/src/domain/exchange/exchange_provider_description.dart'; +import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_item.dart'; +import 'package:cake_wallet/src/screens/exchange_trade/information_page.dart'; +import 'package:cake_wallet/src/widgets/standart_list_row.dart'; +import 'package:cake_wallet/view_model/exchange/exchange_trade_view_model.dart'; import 'package:mobx/mobx.dart'; import 'package:provider/provider.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; @@ -19,15 +24,62 @@ import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; +void showInformation(ExchangeTradeViewModel exchangeTradeViewModel, BuildContext context) { + final fetchingLabel = S.current.fetching; + final trade = exchangeTradeViewModel.trade; + final walletName = exchangeTradeViewModel.wallet.name; + + final information = exchangeTradeViewModel.isSendable + ? S.current.exchange_result_confirm( + trade.amount ?? fetchingLabel, + trade.from.toString(), + walletName) + : S.current.exchange_result_description( + trade.amount ?? fetchingLabel, trade.from.toString()); + + showDialog<void>( + context: context, + builder: (_) => InformationPage(information: information) + ); +} + class ExchangeTradePage extends BasePage { + ExchangeTradePage({@required this.exchangeTradeViewModel}); + + final ExchangeTradeViewModel exchangeTradeViewModel; + @override String get title => S.current.exchange; @override - Widget body(BuildContext context) => ExchangeTradeForm(); + Widget trailing(BuildContext context) { + final questionImage = Image.asset('assets/images/question_mark.png', + color: Theme.of(context).primaryTextTheme.title.color); + + return SizedBox( + height: 20.0, + width: 20.0, + child: ButtonTheme( + minWidth: double.minPositive, + child: FlatButton( + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + padding: EdgeInsets.all(0), + onPressed: () => showInformation(exchangeTradeViewModel, context), + child: questionImage), + ), + ); + } + + @override + Widget body(BuildContext context) => ExchangeTradeForm(exchangeTradeViewModel); } class ExchangeTradeForm extends StatefulWidget { + ExchangeTradeForm(this.exchangeTradeViewModel); + + final ExchangeTradeViewModel exchangeTradeViewModel; + @override ExchangeTradeState createState() => ExchangeTradeState(); } @@ -39,263 +91,139 @@ class ExchangeTradeState extends State<ExchangeTradeForm> { bool _effectsInstalled = false; @override - Widget build(BuildContext context) { - final tradeStore = Provider.of<ExchangeTradeStore>(context); - final sendStore = Provider.of<SendStore>(context); - final walletStore = Provider.of<WalletStore>(context); + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback(afterLayout); + } - _setEffects(context); + void afterLayout(dynamic _) { + showInformation(widget.exchangeTradeViewModel, context); + } + + @override + Widget build(BuildContext context) { + final copyImage = Image.asset('assets/images/copy_content.png', + height: 16, width: 16, + color: Theme.of(context).primaryTextTheme.overline.color); + + //_setEffects(context); return Container( child: ScrollableWithBottomSection( - contentPadding: EdgeInsets.only(left: 24, right: 24, top: 24), + contentPadding: EdgeInsets.only(top: 10, bottom: 16), content: Observer(builder: (_) { - final trade = tradeStore.trade; - final walletName = walletStore.name; + final trade = widget.exchangeTradeViewModel.trade; return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ - Container( - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: <Widget>[ - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: <Widget>[ - Text( - S.of(context).id, - style: TextStyle( - height: 2, - fontWeight: FontWeight.bold, - fontSize: 14.0, - color: Theme.of(context).primaryTextTheme.title.color), - ), - Text( - '${trade.id ?? fetchingLabel}', - style: TextStyle( - fontSize: 14.0, - height: 2, - color: Theme.of(context).primaryTextTheme.caption.color), - ) - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: <Widget>[ - Text( - S.of(context).amount, - style: TextStyle( - height: 2, - fontWeight: FontWeight.bold, - fontSize: 14.0, - color: Theme.of(context).primaryTextTheme.title.color), - ), - Text( - '${trade.amount ?? fetchingLabel}', - style: TextStyle( - fontSize: 14.0, - height: 2, - color: Theme.of(context).primaryTextTheme.caption.color), - ) - ], - ), - trade.extraId != null - ? Row( - mainAxisAlignment: MainAxisAlignment.start, - children: <Widget>[ - Text( - S.of(context).payment_id, - style: TextStyle( - height: 2, - fontWeight: FontWeight.bold, - fontSize: 14.0, - color: Theme.of(context).primaryTextTheme.title.color), - ), - Text( - '${trade.extraId ?? fetchingLabel}', - style: TextStyle( - fontSize: 14.0, - height: 2, - color: Theme.of(context).primaryTextTheme.caption.color), - ) - ], - ) - : Container(), - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: <Widget>[ - Text( - S.of(context).status, - style: TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.bold, - color: Theme.of(context).primaryTextTheme.title.color, - height: 2), - ), - Text( - '${trade.state ?? fetchingLabel}', - style: TextStyle( - fontSize: 14.0, - height: 2, - color: Theme.of(context).primaryTextTheme.caption.color), - ) - ], - ), - trade.expiredAt != null - ? Row( - mainAxisAlignment: MainAxisAlignment.start, - children: <Widget>[ - Text( - S.of(context).offer_expires_in, - style: TextStyle( - fontSize: 14.0, - color: Theme.of(context).primaryTextTheme.title.color), - ), - TimerWidget(trade.expiredAt, - color: Theme.of(context).primaryTextTheme.caption.color) - ], - ) - : Container(), - ], - ), - ], - ), + trade.expiredAt != null + ? Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: <Widget>[ + Text( + S.of(context).offer_expires_in, + style: TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.w500, + color: Theme.of(context).primaryTextTheme.overline.color), + ), + TimerWidget(trade.expiredAt, + color: Theme.of(context).primaryTextTheme.title.color) + ]) + : Offstage(), + Padding( + padding: EdgeInsets.only(top: 32), + child: Row(children: <Widget>[ + Spacer(flex: 3), + Flexible( + flex: 4, + child: Center( + child: AspectRatio( + aspectRatio: 1.0, + child: QrImage( + data: trade.inputAddress ?? fetchingLabel, + backgroundColor: Colors.transparent, + foregroundColor: Theme.of(context) + .accentTextTheme.subtitle.color, + )))), + Spacer(flex: 3) + ]), ), Padding( - padding: EdgeInsets.only(top: 20), - child: Row( - children: <Widget>[ - Spacer( - flex: 1, - ), - Flexible( - flex: 1, - child: Center( - child: AspectRatio( - aspectRatio: 1.0, - child: QrImage( - data: trade.inputAddress ?? fetchingLabel, - backgroundColor: Colors.transparent, - foregroundColor: Theme.of(context).primaryTextTheme.display4.color, - ), - ), - )), - Spacer( - flex: 1, - ) - ], - ), - ), - SizedBox( - height: 20.0, - ), - Center( - child: Text( - S.of(context).trade_is_powered_by(trade.provider != null - ? trade.provider.title - : fetchingLabel), - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.bold, - color: Theme.of(context).primaryTextTheme.title.color), - ), - ), - Container( - padding: EdgeInsets.only(top: 20, bottom: 20), - child: Center( - child: Text( - trade.inputAddress ?? fetchingLabel, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14.0, - color: Theme.of(context).primaryTextTheme.caption.color), + padding: EdgeInsets.only(top: 16), + child: ListView.separated( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: widget.exchangeTradeViewModel.items.length, + separatorBuilder: (context, index) => Container( + height: 1, + color: Theme.of(context).accentTextTheme.subtitle.backgroundColor, ), + itemBuilder: (context, index) { + final item = widget.exchangeTradeViewModel.items[index]; + String value; + + final content = Observer( + builder: (_) { + switch (index) { + case 0: + value = '${widget.exchangeTradeViewModel.trade.id ?? fetchingLabel}'; + break; + case 1: + value = '${widget.exchangeTradeViewModel.trade.amount ?? fetchingLabel}'; + break; + case 2: + value = '${widget.exchangeTradeViewModel.trade.state ?? fetchingLabel}'; + break; + case 3: + value = widget.exchangeTradeViewModel.trade.inputAddress ?? fetchingLabel; + break; + } + + return StandartListRow( + title: item.title, + value: value, + valueFontSize: 14, + image: item.isCopied ? copyImage : null, + ); + } + ); + + return item.isCopied + ? Builder( + builder: (context) => + GestureDetector( + onTap: () { + Clipboard.setData(ClipboardData(text: value)); + Scaffold.of(context).showSnackBar(SnackBar( + content: Text( + S.of(context).copied_to_clipboard, + textAlign: TextAlign.center, + style: TextStyle(color: Colors.white), + ), + backgroundColor: Colors.green, + duration: Duration(milliseconds: 1500), + )); + }, + child: content, + ) + ) + : content; + }, ), ), - Container( - child: Row( - children: <Widget>[ - Flexible( - child: Container( - padding: EdgeInsets.only(right: 5.0), - child: Builder( - builder: (context) => PrimaryButton( - onPressed: () { - Clipboard.setData(ClipboardData(text: trade.inputAddress)); - Scaffold.of(context).showSnackBar(SnackBar( - content: Text( - S.of(context).copied_to_clipboard, - textAlign: TextAlign.center, - style: TextStyle(color: Colors.white), - ), - backgroundColor: Colors.green, - duration: Duration(milliseconds: 1500), - )); - }, - text: S.of(context).copy_address, - color: Theme.of(context).accentTextTheme.title.backgroundColor, - textColor: Theme.of(context).primaryTextTheme.title.color) - ), - )), - Flexible( - child: Container( - padding: EdgeInsets.only(left: 5.0), - child: Builder( - builder: (context) => PrimaryButton( - onPressed: () { - Clipboard.setData(ClipboardData(text: trade.id)); - Scaffold.of(context).showSnackBar(SnackBar( - content: Text( - S.of(context).copied_to_clipboard, - textAlign: TextAlign.center, - style: TextStyle(color: Colors.white), - ), - backgroundColor: Colors.green, - duration: Duration(milliseconds: 1500), - )); - }, - text: S.of(context).copy_id, - color: Theme.of(context).accentTextTheme.title.backgroundColor, - textColor: Theme.of(context).primaryTextTheme.title.color) - ), - )) - ], - ), - ), - Container( - padding: EdgeInsets.only(top: 20), - child: Text( - tradeStore.isSendable - ? S.of(context).exchange_result_confirm( - trade.amount ?? fetchingLabel, - trade.from.toString(), - walletName) - : S.of(context).exchange_result_description( - trade.amount ?? fetchingLabel, trade.from.toString()), - textAlign: TextAlign.left, - style: TextStyle( - fontSize: 13.0, - color: Theme.of(context).primaryTextTheme.title.color), - ), - ), - Text( - S.of(context).exchange_result_write_down_ID, - textAlign: TextAlign.left, - style: TextStyle( - fontSize: 13.0, - color: Theme.of(context).primaryTextTheme.title.color), - ) ], ); }), - bottomSectionPadding: EdgeInsets.all(24), - bottomSection: Observer( + bottomSectionPadding: EdgeInsets.fromLTRB(24, 0, 24, 24), + bottomSection: PrimaryButton( + onPressed: () {}, + text: S.of(context).confirm, + color: Palette.blueCraiola, + textColor: Colors.white + ) + /*Observer( builder: (_) => tradeStore.trade.from == CryptoCurrency.xmr && !(sendStore.state is TransactionCommitted) ? LoadingPrimaryButton( @@ -312,7 +240,7 @@ class ExchangeTradeState extends State<ExchangeTradeForm> { : S.of(context).send_xmr, color: Colors.blue, textColor: Colors.white) - : Offstage()), + : Offstage()),*/ ), ); } @@ -322,7 +250,7 @@ class ExchangeTradeState extends State<ExchangeTradeForm> { return; } - final sendStore = Provider.of<SendStore>(context); + /*final sendStore = Provider.of<SendStore>(context); reaction((_) => sendStore.state, (SendingState state) { if (state is SendingFailed) { @@ -376,7 +304,7 @@ class ExchangeTradeState extends State<ExchangeTradeForm> { }); }); } - }); + });*/ _effectsInstalled = true; } diff --git a/lib/src/screens/exchange_trade/information_page.dart b/lib/src/screens/exchange_trade/information_page.dart new file mode 100644 index 000000000..bf622cb86 --- /dev/null +++ b/lib/src/screens/exchange_trade/information_page.dart @@ -0,0 +1,57 @@ +import 'dart:ui'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:cake_wallet/src/widgets/alert_background.dart'; + +class InformationPage extends StatelessWidget { + InformationPage({@required this.information}); + + final String information; + + @override + Widget build(BuildContext context) { + return AlertBackground( + child: Center( + child: Container( + margin: EdgeInsets.only( + left: 24, + right: 24 + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(30)), + color: Theme.of(context).textTheme.body2.decorationColor + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: <Widget>[ + Container( + padding: EdgeInsets.fromLTRB(24, 28, 24, 24), + child: Text( + information, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + fontFamily: 'Poppins', + decoration: TextDecoration.none, + color: Theme.of(context).accentTextTheme.caption.decorationColor + ), + ), + ), + Padding( + padding: EdgeInsets.fromLTRB(10, 0, 10, 10), + child: PrimaryButton( + onPressed: () => Navigator.of(context).pop(), + text: S.of(context).send_got_it, + color: Theme.of(context).accentTextTheme.caption.backgroundColor, + textColor: Theme.of(context).primaryTextTheme.title.color + ), + ) + ], + ), + ), + ) + ); + } +} diff --git a/lib/src/screens/exchange_trade/widgets/timer_widget.dart b/lib/src/screens/exchange_trade/widgets/timer_widget.dart index 713148fed..e826993fd 100644 --- a/lib/src/screens/exchange_trade/widgets/timer_widget.dart +++ b/lib/src/screens/exchange_trade/widgets/timer_widget.dart @@ -53,10 +53,16 @@ class TimerWidgetState extends State<TimerWidget> { Widget build(BuildContext context) { return _isExpired ? Text(S.of(context).expired, - style: TextStyle(fontSize: 14.0, color: Colors.red)) + style: TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.w500, + color: Colors.red)) : Text( S.of(context).time(_minutes.toString(), _seconds.toString()), - style: TextStyle(fontSize: 14.0, color: widget.color), + style: TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.w500, + color: widget.color), ); } diff --git a/lib/src/screens/monero_accounts/monero_account_list_page.dart b/lib/src/screens/monero_accounts/monero_account_list_page.dart index 8c3a5503e..0f4d493fe 100644 --- a/lib/src/screens/monero_accounts/monero_account_list_page.dart +++ b/lib/src/screens/monero_accounts/monero_account_list_page.dart @@ -27,7 +27,9 @@ class MoneroAccountListPage extends StatelessWidget { } final MoneroAccountListViewModel accountListViewModel; - final closeIcon = Image.asset('assets/images/close.png'); + final closeIcon = Image.asset('assets/images/close.png', + color: Palette.darkBlueCraiola, + ); ScrollController controller; double backgroundHeight; diff --git a/lib/src/screens/nodes/node_create_or_edit_page.dart b/lib/src/screens/nodes/node_create_or_edit_page.dart index 6c79fdc3c..d4b4a5e01 100644 --- a/lib/src/screens/nodes/node_create_or_edit_page.dart +++ b/lib/src/screens/nodes/node_create_or_edit_page.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/palette.dart'; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; @@ -6,6 +7,7 @@ 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/widgets/base_text_form_field.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'; @@ -77,32 +79,11 @@ class NodeCreateOrEditPage extends BasePage { Row( children: <Widget>[ 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))), + child: BaseTextFormField( controller: _addressController, + hintText: S.of(context).node_address, validator: NodeAddressValidator(), - ), + ) ) ], ), @@ -110,34 +91,13 @@ class NodeCreateOrEditPage extends BasePage { Row( children: <Widget>[ Expanded( - child: TextFormField( - style: TextStyle( - fontSize: 16.0, - color: Theme.of(context) - .primaryTextTheme - .title - .color), + child: BaseTextFormField( + controller: _portController, + hintText: S.of(context).node_port, 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(), - ), + ) ) ], ), @@ -146,32 +106,10 @@ class NodeCreateOrEditPage extends BasePage { Row( children: <Widget>[ 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))), + child: BaseTextFormField( controller: _loginController, - validator: (value) => null, - ), + hintText: S.of(context).login, + ) ) ], ), @@ -179,32 +117,10 @@ class NodeCreateOrEditPage extends BasePage { Row( children: <Widget>[ 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))), + child: BaseTextFormField( controller: _passwordController, - validator: (value) => null, - ), + hintText: S.of(context).password, + ) ) ], ) @@ -237,7 +153,7 @@ class NodeCreateOrEditPage extends BasePage { Navigator.of(context).pop(); }, text: S.of(context).save, - color: Colors.green, + color: Palette.blueCraiola, 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 2777e25b7..ab715058c 100644 --- a/lib/src/screens/nodes/nodes_list_page.dart +++ b/lib/src/screens/nodes/nodes_list_page.dart @@ -24,7 +24,7 @@ class NodeListPage extends BasePage { height: 32, decoration: BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(16)), - color: Theme.of(context).accentTextTheme.title.backgroundColor), + color: Theme.of(context).accentTextTheme.caption.color), child: ButtonTheme( minWidth: double.minPositive, child: FlatButton( @@ -51,7 +51,7 @@ class NodeListPage extends BasePage { style: TextStyle( fontSize: 14.0, fontWeight: FontWeight.w600, - color: Colors.blue), + color: Palette.blueCraiola), )), ), ); @@ -74,15 +74,41 @@ class NodeListPage extends BasePage { } final node = nodeListViewModel.nodes[index]; - final isSelected = index == 1; // FIXME: hardcoded value. final nodeListRow = NodeListRow( - title: node.uri, - isSelected: isSelected, - isAlive: node.requestNode(), - onTap: (_) {}); + title: node.value.uri, + isSelected: node.isSelected, + isAlive: node.value.requestNode(), + onTap: (_) async { + if (node.isSelected) { + return; + } + + await showDialog<void>( + context: context, + builder: (BuildContext context) { + return AlertDialog( + content: Text( + S.of(context).change_current_node(node.value.uri), + textAlign: TextAlign.center, + ), + actions: <Widget>[ + FlatButton( + onPressed: () => Navigator.pop(context), + child: Text(S.of(context).cancel)), + FlatButton( + onPressed: () async { + Navigator.of(context).pop(); + await nodeListViewModel + .setAsCurrent(node.value); + }, + child: Text(S.of(context).change)), + ], + ); + }); + }); final dismissibleRow = Dismissible( - key: Key('${node.key}'), + key: Key('${node.value.key}'), confirmDismiss: (direction) async { return await showDialog( context: context, @@ -99,7 +125,7 @@ class NodeListPage extends BasePage { }); }, onDismissed: (direction) async => - nodeListViewModel.delete(node), + nodeListViewModel.delete(node.value), direction: DismissDirection.endToStart, background: Container( padding: EdgeInsets.only(right: 10.0), @@ -120,7 +146,7 @@ class NodeListPage extends BasePage { )), child: nodeListRow); - return isSelected ? nodeListRow : dismissibleRow; + return node.isSelected ? nodeListRow : dismissibleRow; }, itemCounter: (int sectionIndex) { if (sectionIndex == 0) { diff --git a/lib/src/screens/nodes/widgets/node_list_row.dart b/lib/src/screens/nodes/widgets/node_list_row.dart index f5ba95d11..f32577f2c 100644 --- a/lib/src/screens/nodes/widgets/node_list_row.dart +++ b/lib/src/screens/nodes/widgets/node_list_row.dart @@ -23,9 +23,9 @@ class NodeListRow extends StandardListRow { builder: (context, snapshot) { switch (snapshot.connectionState) { case ConnectionState.done: - return NodeIndicator(isLive: snapshot.data as bool); + return NodeIndicator(isLive: (snapshot.data as bool)??false); default: - return NodeIndicator(); + return NodeIndicator(isLive: false); } }); } @@ -38,6 +38,6 @@ class NodeHeaderListRow extends StandardListRow { @override Widget buildTrailing(BuildContext context) { return Icon(Icons.add, - color: Theme.of(context).primaryTextTheme.title.color, size: 24.0); + color: Theme.of(context).accentTextTheme.subhead.color, size: 24.0); } } diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index 7902370b0..187523de2 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -1,28 +1,643 @@ -import 'package:cake_wallet/view_model/send_view_model.dart'; +import 'dart:ui'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:mobx/mobx.dart'; import 'package:cake_wallet/palette.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/auth/auth_page.dart'; +import 'package:cake_wallet/src/widgets/address_text_field.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/view_model/send/send_view_model.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/widgets/top_panel.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; +import 'package:cake_wallet/src/screens/send/widgets/confirm_sending_alert.dart'; +import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; +import 'package:cake_wallet/src/widgets/trail_button.dart'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/send/widgets/base_send_widget.dart'; +// FIXME: Refactor this screen. + class SendPage extends BasePage { SendPage({@required this.sendViewModel}); final SendViewModel sendViewModel; + // ??? @override - String get title => sendViewModel.pageTitle; + String get title => 'SEND'; @override - Color get backgroundLightColor => PaletteDark.nightBlue; + Color get titleColor => Colors.white; @override - Color get backgroundDarkColor => PaletteDark.nightBlue; + Color get backgroundLightColor => Colors.transparent; @override bool get resizeToAvoidBottomPadding => false; @override - Widget body(BuildContext context) => - BaseSendWidget(sendViewModel: sendViewModel); + Widget trailing(context) => TrailButton( + caption: S.of(context).clear, onPressed: () => sendViewModel.reset()); + Color get backgroundDarkColor => Colors.transparent; + + // @override + // State<StatefulWidget> createState() => SendFormState(); + + final _addressController = TextEditingController(); + final _cryptoAmountController = TextEditingController(); + final _fiatAmountController = TextEditingController(); + + final _focusNode = FocusNode(); + + bool _effectsInstalled = false; + + final _formKey = GlobalKey<FormState>(); + + // @override + // void initState() { + // _focusNode.addListener(() { + // if (!_focusNode.hasFocus && _addressController.text.isNotEmpty) { + // getOpenaliasRecord(context); + // } + // }); + + // super.initState(); + // } + + Future<void> getOpenaliasRecord(BuildContext context) async { +// final sendStore = Provider.of<SendStore>(context); +// final isOpenalias = +// await sendStore.isOpenaliasRecord(_addressController.text); +// +// if (isOpenalias) { +// _addressController.text = sendStore.recordAddress; +// +// await showDialog<void>( +// 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()); +// }); +// } + } + + // @override + // Widget body(BuildContext context) { + // return super.build(context); + // } + + @override + Widget body(BuildContext context) => BaseSendWidget( + sendViewModel: sendViewModel, + leading: leading(context), + middle: middle(context), + ); + + @override + Widget build(BuildContext context) { + return Scaffold( + resizeToAvoidBottomPadding: resizeToAvoidBottomPadding, + body: Container( + color: Theme.of(context).backgroundColor, child: body(context))); + } + +// @override +// Widget build(BuildContext context) { +// _setEffects(context); + +// return Container( +// color: Theme.of(context).backgroundColor, +// child: ScrollableWithBottomSection( +// contentPadding: EdgeInsets.only(bottom: 24), +// content: Column( +// children: <Widget>[ +// TopPanel( +// color: Theme.of(context).accentTextTheme.title.backgroundColor, +// widget: Form( +// key: _formKey, +// child: Column(children: <Widget>[ +// AddressTextField( +// controller: _addressController, +// placeholder: 'Address', +// //S.of(context).send_monero_address, FIXME: placeholder for btc and xmr address text field. +// focusNode: _focusNode, +// onURIScanned: (uri) { +// var address = ''; +// var amount = ''; + +// if (uri != null) { +// address = uri.path; +// amount = uri.queryParameters['tx_amount']; +// } else { +// address = uri.toString(); +// } + +// _addressController.text = address; +// _cryptoAmountController.text = amount; +// }, +// options: [ +// AddressTextFieldOption.qrCode, +// AddressTextFieldOption.addressBook +// ], +// buttonColor: Theme.of(context).accentTextTheme.title.color, +// validator: widget.sendViewModel.addressValidator, +// ), +// Padding( +// padding: const EdgeInsets.only(top: 20), +// child: TextFormField( +// onChanged: (value) => +// widget.sendViewModel.setCryptoAmount(value), +// 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( +// '${widget.sendViewModel.currency.toString()}:', +// style: TextStyle( +// fontSize: 16, +// fontWeight: FontWeight.w500, +// color: Theme.of(context) +// .primaryTextTheme +// .title +// .color, +// )), +// ), +// suffixIcon: Padding( +// padding: EdgeInsets.only(bottom: 5), +// child: 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: () => widget.sendViewModel.setAll(), +// 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: (String value) { +// if (widget.sendViewModel.all) { +// return null; +// } + +// return widget.sendViewModel.amountValidator +// .call(value); +// }), +// ), +// Padding( +// padding: const EdgeInsets.only(top: 20), +// child: TextFormField( +// onChanged: (value) => +// widget.sendViewModel.setFiatAmount(value), +// style: TextStyle( +// fontSize: 16.0, +// color: +// Theme.of(context).primaryTextTheme.title.color), +// controller: _fiatAmountController, +// keyboardType: TextInputType.numberWithOptions( +// signed: false, decimal: true), +// // inputFormatters: [ +// // BlacklistingTextInputFormatter( +// // RegExp('[\\-|\\ |\\,]')) +// // ], +// decoration: InputDecoration( +// prefixIcon: Padding( +// padding: EdgeInsets.only(top: 12), +// child: Text( +// '${widget.sendViewModel.fiat.toString()}:', +// style: TextStyle( +// fontSize: 16, +// fontWeight: FontWeight.w500, +// color: Theme.of(context) +// .primaryTextTheme +// .title +// .color, +// )), +// ), +// hintStyle: TextStyle( +// fontSize: 16.0, +// color: Theme.of(context) +// .primaryTextTheme +// .caption +// .color), +// hintText: '0.00', +// focusedBorder: UnderlineInputBorder( +// borderSide: BorderSide( +// color: Theme.of(context).dividerColor, +// width: 1.0)), +// enabledBorder: UnderlineInputBorder( +// borderSide: BorderSide( +// color: Theme.of(context).dividerColor, +// width: 1.0)))), +// ), +// Padding( +// padding: const EdgeInsets.only(top: 20), +// child: Row( +// mainAxisAlignment: MainAxisAlignment.spaceBetween, +// children: <Widget>[ +// Text(S.of(context).send_estimated_fee, +// style: TextStyle( +// fontSize: 12, +// fontWeight: FontWeight.w600, +// color: Theme.of(context) +// .primaryTextTheme +// .title +// .color, +// )), +// Text( +// '${widget.sendViewModel.estimatedFee} ${widget.sendViewModel.currency.toString()}', +// style: TextStyle( +// fontSize: 12, +// fontWeight: FontWeight.w600, +// color: Theme.of(context) +// .primaryTextTheme +// .title +// .color, +// )) +// ], +// ), +// ) +// ]), +// ), +// ), +// // Padding( +// // padding: EdgeInsets.only(top: 32, left: 24, bottom: 24), +// // child: Row( +// // mainAxisAlignment: MainAxisAlignment.start, +// // children: <Widget>[ +// // 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: () async { +// FocusScope.of(context).requestFocus(FocusNode()); + +// if (!_formKey.currentState.validate()) { +// return; +// } + +// await showDialog<void>( +// 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(); +// widget.sendViewModel.createTransaction(); +// }); +// }, +// actionRightButton: () => Navigator.of(context).pop()); +// }); +// }, +// text: S.of(context).send, +// color: Colors.blue, +// textColor: Colors.white, +// isLoading: widget.sendViewModel.state is TransactionIsCreating || +// widget.sendViewModel.state is TransactionCommitting, +// isDisabled: !widget.sendViewModel.isReadyForSend); +// }), +// ), +// ); +// } + +// void _setEffects(BuildContext context) { +// if (_effectsInstalled) { +// return; +// } + +// reaction((_) => widget.sendViewModel.all, (bool all) { +// if (all) { +// _cryptoAmountController.text = S.current.all; +// _fiatAmountController.text = null; +// } +// }); + +// reaction((_) => widget.sendViewModel.fiatAmount, (String amount) { +// if (amount != _fiatAmountController.text) { +// _fiatAmountController.text = amount; +// } +// }); + +// reaction((_) => widget.sendViewModel.cryptoAmount, (String amount) { +// if (widget.sendViewModel.all && amount != S.current.all) { +// widget.sendViewModel.all = false; +// } + +// 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.address = address; +// } +// }); + +// reaction((_) => widget.sendViewModel.state, (SendViewModelState state) { +// if (state is SendingFailed) { +// WidgetsBinding.instance.addPostFrameCallback((_) { +// showDialog<void>( +// 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()); +// }); +// }); +// } + +// if (state is TransactionCreatedSuccessfully) { +// WidgetsBinding.instance.addPostFrameCallback((_) { +// showDialog<void>( +// context: context, +// builder: (BuildContext context) { +// return ConfirmSendingAlert( +// alertTitle: S.of(context).confirm_sending, +// amount: S.of(context).send_amount, +// amountValue: +// widget.sendViewModel.pendingTransaction.amountFormatted, +// fee: S.of(context).send_fee, +// feeValue: +// widget.sendViewModel.pendingTransaction.feeFormatted, +// leftButtonText: S.of(context).ok, +// rightButtonText: S.of(context).cancel, +// actionLeftButton: () { +// Navigator.of(context).pop(); +// widget.sendViewModel.commitTransaction(); +// showDialog<void>( +// context: context, +// builder: (BuildContext context) { +// return Observer(builder: (_) { +// final state = widget.sendViewModel.state; + +// if (state is TransactionCommitted) { +// return Stack( +// children: <Widget>[ +// Container( +// color: Theme.of(context).backgroundColor, +// child: Center( +// child: Image.asset( +// 'assets/images/birthday_cake.png'), +// ), +// ), +// Center( +// child: Padding( +// padding: EdgeInsets.only( +// top: 220, left: 24, right: 24), +// child: Text( +// S.of(context).send_success, +// textAlign: TextAlign.center, +// style: TextStyle( +// fontSize: 22, +// fontWeight: FontWeight.bold, +// color: Theme.of(context) +// .primaryTextTheme +// .title +// .color, +// decoration: TextDecoration.none, +// ), +// ), +// ), +// ), +// Positioned( +// left: 24, +// right: 24, +// bottom: 24, +// child: PrimaryButton( +// onPressed: () => +// Navigator.of(context).pop(), +// text: S.of(context).send_got_it, +// color: Colors.blue, +// textColor: Colors.white)) +// ], +// ); +// } + +// return Stack( +// children: <Widget>[ +// Container( +// color: Theme.of(context).backgroundColor, +// child: Center( +// child: Image.asset( +// 'assets/images/birthday_cake.png'), +// ), +// ), +// BackdropFilter( +// filter: ImageFilter.blur( +// sigmaX: 3.0, sigmaY: 3.0), +// child: Container( +// decoration: BoxDecoration( +// color: Theme.of(context) +// .backgroundColor +// .withOpacity(0.25)), +// child: Center( +// child: Padding( +// padding: EdgeInsets.only(top: 220), +// child: Text( +// S.of(context).send_sending, +// textAlign: TextAlign.center, +// style: TextStyle( +// fontSize: 22, +// fontWeight: FontWeight.bold, +// color: Theme.of(context) +// .primaryTextTheme +// .title +// .color, +// decoration: TextDecoration.none, +// ), +// ), +// ), +// ), +// ), +// ) +// ], +// ); +// }); +// }); +// }, +// actionRightButton: () => Navigator.of(context).pop()); +// }); +// }); +// } + +// if (state is TransactionCommitted) { +// WidgetsBinding.instance.addPostFrameCallback((_) { +// _addressController.text = ''; +// _cryptoAmountController.text = ''; +// }); +// } +// }); + +// _effectsInstalled = true; +// } + +// Widget body(BuildContext context) => BaseSendWidget( +// sendViewModel: sendViewModel, +// leading: leading(context), +// middle: middle(context), +// ); + +// @override +// Widget build(BuildContext context) { +// return Scaffold( +// resizeToAvoidBottomPadding: resizeToAvoidBottomPadding, +// body: Container( +// color: Theme.of(context).backgroundColor, child: body(context))); +// } } diff --git a/lib/src/screens/send/send_template_page.dart b/lib/src/screens/send/send_template_page.dart index 19f9f8ca8..5487dc71e 100644 --- a/lib/src/screens/send/send_template_page.dart +++ b/lib/src/screens/send/send_template_page.dart @@ -1,9 +1,8 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:cake_wallet/palette.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/view_model/send_view_model.dart'; +import 'package:cake_wallet/view_model/send/send_view_model.dart'; import 'package:cake_wallet/src/screens/send/widgets/base_send_widget.dart'; class SendTemplatePage extends BasePage { @@ -15,15 +14,29 @@ class SendTemplatePage extends BasePage { String get title => S.current.exchange_new_template; @override - Color get backgroundLightColor => PaletteDark.nightBlue; + Color get titleColor => Colors.white; @override - Color get backgroundDarkColor => PaletteDark.nightBlue; + Color get backgroundLightColor => Colors.transparent; + + @override + Color get backgroundDarkColor => Colors.transparent; @override bool get resizeToAvoidBottomPadding => false; @override - Widget body(BuildContext context) => - BaseSendWidget(sendViewModel: sendViewModel, isTemplate: true); -} \ No newline at end of file + Widget body(BuildContext context) => BaseSendWidget( + sendViewModel: sendViewModel, + leading: leading(context), + middle: middle(context), + isTemplate: true); + + @override + Widget build(BuildContext context) { + return Scaffold( + resizeToAvoidBottomPadding: resizeToAvoidBottomPadding, + body: Container( + color: Theme.of(context).backgroundColor, child: body(context))); + } +} diff --git a/lib/src/screens/send/widgets/base_send_widget.dart b/lib/src/screens/send/widgets/base_send_widget.dart index 1bc5a5359..bfbf3ae9a 100644 --- a/lib/src/screens/send/widgets/base_send_widget.dart +++ b/lib/src/screens/send/widgets/base_send_widget.dart @@ -1,8 +1,12 @@ +import 'dart:ui'; +import 'package:cake_wallet/src/domain/common/transaction_priority.dart'; +import 'package:cake_wallet/src/widgets/picker.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:cake_wallet/view_model/send_view_model.dart'; +import 'package:cake_wallet/view_model/send/send_view_model.dart'; import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:mobx/mobx.dart'; @@ -20,13 +24,16 @@ import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; import 'package:cake_wallet/routes.dart'; class BaseSendWidget extends StatelessWidget { - BaseSendWidget({ - @required this.sendViewModel, - this.isTemplate = false - }); + BaseSendWidget( + {@required this.sendViewModel, + @required this.leading, + @required this.middle, + this.isTemplate = false}); final SendViewModel sendViewModel; final bool isTemplate; + final Widget leading; + final Widget middle; final _addressController = TextEditingController(); final _cryptoAmountController = TextEditingController(); @@ -39,81 +46,99 @@ class BaseSendWidget extends StatelessWidget { @override Widget build(BuildContext context) { - _setEffects(context); - return Container( - color: PaletteDark.backgroundColor, - child: ScrollableWithBottomSection( - contentPadding: EdgeInsets.only(bottom: 24), - content: Column( - children: <Widget>[ - TopPanel( - color: PaletteDark.nightBlue, - edgeInsets: EdgeInsets.fromLTRB(24, 24, 24, 32), - widget: Form( - key: _formKey, - child: Column(children: <Widget>[ - isTemplate - ? BaseTextFormField( - controller: _nameController, - hintText: S.of(context).send_name, - borderColor: PaletteDark.lightVioletBlue, - textStyle: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Colors.white - ), - placeholderTextStyle: TextStyle( - color: PaletteDark.darkCyanBlue, - fontWeight: FontWeight.w500, - fontSize: 14), - validator: sendViewModel.templateValidator, - ) - : Offstage(), - Padding( - padding: EdgeInsets.only(top: isTemplate ? 20 : 0), - child: AddressTextField( - controller: _addressController, - placeholder: S.of(context).send_address( - sendViewModel.cryptoCurrencyTitle), - focusNode: _focusNode, - onURIScanned: (uri) { - var address = ''; - var amount = ''; + return ScrollableWithBottomSection( + contentPadding: EdgeInsets.only(bottom: 24), + content: Column( + children: <Widget>[ + TopPanel( + edgeInsets: EdgeInsets.all(0), + gradient: LinearGradient(colors: [ + Theme.of(context).primaryTextTheme.subhead.color, + Theme.of(context).primaryTextTheme.subhead.decorationColor, + ], begin: Alignment.topLeft, end: Alignment.bottomRight), + widget: Form( + key: _formKey, + child: Column(children: <Widget>[ + CupertinoNavigationBar( + leading: leading, + middle: middle, + backgroundColor: Colors.transparent, + border: null, + ), + Padding( + padding: EdgeInsets.fromLTRB(24, 24, 24, 32), + child: Column( + children: <Widget>[ + isTemplate + ? BaseTextFormField( + controller: _nameController, + hintText: S.of(context).send_name, + borderColor: Theme.of(context) + .primaryTextTheme + .headline + .color, + textStyle: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.white), + placeholderTextStyle: TextStyle( + color: Theme.of(context) + .primaryTextTheme + .headline + .decorationColor, + fontWeight: FontWeight.w500, + fontSize: 14), + validator: sendViewModel.templateValidator, + ) + : Offstage(), + Padding( + padding: EdgeInsets.only(top: isTemplate ? 20 : 0), + child: AddressTextField( + controller: _addressController, + // placeholder: S + // .of(context) + // .send_address(sendViewModel.cryptoCurrencyTitle), + focusNode: _focusNode, + onURIScanned: (uri) { + var address = ''; + var amount = ''; - if (uri != null) { - address = uri.path; - amount = uri.queryParameters['tx_amount']; - } else { - address = uri.toString(); - } + if (uri != null) { + address = uri.path; + amount = uri.queryParameters['tx_amount']; + } else { + address = uri.toString(); + } - _addressController.text = address; - _cryptoAmountController.text = amount; - }, - options: [ - AddressTextFieldOption.paste, - AddressTextFieldOption.qrCode, - AddressTextFieldOption.addressBook - ], - buttonColor: PaletteDark.buttonNightBlue, - borderColor: PaletteDark.lightVioletBlue, - textStyle: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Colors.white + _addressController.text = address; + _cryptoAmountController.text = amount; + }, + options: [ + AddressTextFieldOption.paste, + AddressTextFieldOption.qrCode, + AddressTextFieldOption.addressBook + ], + buttonColor: + Theme.of(context).primaryTextTheme.display1.color, + borderColor: + Theme.of(context).primaryTextTheme.headline.color, + textStyle: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.white), + hintStyle: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Theme.of(context) + .primaryTextTheme + .headline + .decorationColor), + validator: sendViewModel.addressValidator, + ), ), - hintStyle: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: PaletteDark.darkCyanBlue - ), - validator: sendViewModel.addressValidator, - ), - ), - Observer( - builder: (_) { + Observer(builder: (_) { return Padding( padding: const EdgeInsets.only(top: 20), child: BaseTextFormField( @@ -126,287 +151,340 @@ class BaseSendWidget extends StatelessWidget { ], prefixIcon: Padding( padding: EdgeInsets.only(top: 9), - child: Text(sendViewModel.currency.title + ':', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Colors.white, - )), + child: + Text(sendViewModel.currency.title + ':', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.white, + )), ), suffixIcon: isTemplate - ? Offstage() - : Padding( - padding: EdgeInsets.only(bottom: 2), - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: <Widget>[ - Container( - width: MediaQuery.of(context).size.width/2, - alignment: Alignment.centerLeft, - child: Text( - ' / ' + sendViewModel.balance, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 14, - color: PaletteDark.darkCyanBlue - ) - ), - ), - Container( - height: 34, - width: 34, - margin: EdgeInsets.only(left: 12, bottom: 8), + ? Offstage() + : Container( + height: 32, + width: 32, + margin: EdgeInsets.only( + left: 14, top: 4, bottom: 10), decoration: BoxDecoration( - color: PaletteDark.buttonNightBlue, - borderRadius: BorderRadius.all(Radius.circular(6)) - ), + color: Theme.of(context) + .primaryTextTheme + .display1 + .color, + borderRadius: BorderRadius.all( + Radius.circular(6))), child: InkWell( - onTap: () => sendViewModel.setSendAll(), + onTap: () => + sendViewModel.setSendAll(), child: Center( child: Text(S.of(context).all, textAlign: TextAlign.center, style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, - color: PaletteDark.lightBlueGrey - ) - ), + color: Theme.of(context) + .primaryTextTheme + .display1 + .decorationColor)), ), ), - ) - ], - ), - ), + ), hintText: '0.0000', - borderColor: PaletteDark.lightVioletBlue, + borderColor: Theme.of(context) + .primaryTextTheme + .headline + .color, textStyle: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, - color: Colors.white - ), + color: Colors.white), placeholderTextStyle: TextStyle( - color: PaletteDark.darkCyanBlue, + color: Theme.of(context) + .primaryTextTheme + .headline + .decorationColor, fontWeight: FontWeight.w500, fontSize: 14), - validator: sendViewModel.amountValidator - ) - ); - } + validator: sendViewModel.amountValidator)); + }), + isTemplate + ? Offstage() + : Observer( + builder: (_) => Padding( + padding: EdgeInsets.only(top: 10), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: <Widget>[ + Expanded( + child: Text( + S.of(context).available_balance + ':', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Theme.of(context) + .primaryTextTheme + .headline + .decorationColor), + )), + Text( + sendViewModel.balance, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Theme.of(context) + .primaryTextTheme + .headline + .decorationColor), + ) + ], + ), + )), + Padding( + padding: const EdgeInsets.only(top: 20), + child: BaseTextFormField( + controller: _fiatAmountController, + keyboardType: TextInputType.numberWithOptions( + signed: false, decimal: true), + inputFormatters: [ + BlacklistingTextInputFormatter( + RegExp('[\\-|\\ |\\,]')) + ], + prefixIcon: Padding( + padding: EdgeInsets.only(top: 9), + child: Text(sendViewModel.fiat.title + ':', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.white, + )), + ), + hintText: '0.00', + borderColor: Theme.of(context) + .primaryTextTheme + .headline + .color, + textStyle: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.white), + placeholderTextStyle: TextStyle( + color: Theme.of(context) + .primaryTextTheme + .headline + .decorationColor, + fontWeight: FontWeight.w500, + fontSize: 14), + )), + isTemplate + ? Offstage() + : Observer( + builder: (_) => GestureDetector( + onTap: () => + _setTransactionPriority(context), + child: Container( + padding: EdgeInsets.only(top: 24), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: <Widget>[ + Text(S.of(context).send_estimated_fee, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + //color: Theme.of(context).primaryTextTheme.display2.color, + color: Colors.white)), + Container( + child: Row( + children: <Widget>[ + Text( + sendViewModel.estimatedFee + .toString() + + ' ' + + sendViewModel + .currency.title, + style: TextStyle( + fontSize: 12, + fontWeight: + FontWeight.w600, + //color: Theme.of(context).primaryTextTheme.display2.color, + color: Colors.white)), + Padding( + padding: + EdgeInsets.only(left: 5), + child: Icon( + Icons.arrow_forward_ios, + size: 12, + color: Colors.white, + ), + ) + ], + ), + ) + ], + ), + ), + )) + ], ), - Padding( - padding: const EdgeInsets.only(top: 20), - child: BaseTextFormField( - controller: _fiatAmountController, - keyboardType: TextInputType.numberWithOptions( - signed: false, decimal: true), - inputFormatters: [ - BlacklistingTextInputFormatter( - RegExp('[\\-|\\ |\\,]')) - ], - prefixIcon: Padding( - padding: EdgeInsets.only(top: 9), - child: Text( - sendViewModel.fiat.title + ':', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Colors.white, - )), - ), - hintText: '0.00', - borderColor: PaletteDark.lightVioletBlue, - textStyle: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Colors.white - ), - placeholderTextStyle: TextStyle( - color: PaletteDark.darkCyanBlue, - fontWeight: FontWeight.w500, - fontSize: 14), + ) + ]), + ), + ), + isTemplate + ? Offstage() + : Padding( + padding: EdgeInsets.only(top: 30, left: 24, bottom: 24), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: <Widget>[ + Text( + S.of(context).send_templates, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Theme.of(context) + .primaryTextTheme + .display4 + .color), ) + ], ), - isTemplate - ? Offstage() - : Padding( - padding: const EdgeInsets.only(top: 24), + ), + isTemplate + ? Offstage() + : Container( + height: 40, + width: double.infinity, + padding: EdgeInsets.only(left: 24), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ - Text(S.of(context).send_estimated_fee, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - color: Colors.white, - )), - Text( - sendViewModel.estimatedFee.toString() + ' ' - + sendViewModel.currency.title, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: Colors.white, - )) + GestureDetector( + onTap: () => Navigator.of(context) + .pushNamed(Routes.sendTemplate), + child: Container( + padding: EdgeInsets.only(left: 1, right: 10), + child: DottedBorder( + borderType: BorderType.RRect, + dashPattern: [6, 4], + color: Theme.of(context) + .primaryTextTheme + .display2 + .decorationColor, + strokeWidth: 2, + radius: Radius.circular(20), + child: Container( + height: 34, + 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 + .display3 + .color), + ), + )), + ), + ), + // Observer( + // builder: (_) { + // final templates = sendViewModel.templates; + // final itemCount = templates.length; + + // return ListView.builder( + // scrollDirection: Axis.horizontal, + // shrinkWrap: true, + // physics: NeverScrollableScrollPhysics(), + // itemCount: itemCount, + // itemBuilder: (context, index) { + // final template = templates[index]; + + // return TemplateTile( + // key: UniqueKey(), + // to: template.name, + // amount: template.amount, + // from: template.cryptoCurrency, + // onTap: () { + // _addressController.text = template.address; + // _cryptoAmountController.text = template.amount; + // getOpenaliasRecord(context); + // }, + // onRemove: () { + // showDialog<void>( + // context: context, + // builder: (dialogContext) { + // return AlertWithTwoActions( + // alertTitle: S.of(context).template, + // alertContent: S.of(context).confirm_delete_template, + // leftButtonText: S.of(context).delete, + // rightButtonText: S.of(context).cancel, + // actionLeftButton: () { + // Navigator.of(dialogContext).pop(); + // sendViewModel.sendTemplateStore.remove(template: template); + // sendViewModel.sendTemplateStore.update(); + // }, + // actionRightButton: () => Navigator.of(dialogContext).pop() + // ); + // } + // ); + // }, + // ); + // } + // ); + // } + // ) ], ), - ) - ]), - ), - ), - isTemplate - ? Offstage() - : Padding( - padding: EdgeInsets.only( - top: 30, - left: 24, - bottom: 24 - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: <Widget>[ - Text( - S.of(context).send_templates, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: PaletteDark.darkCyanBlue - ), - ) - ], - ), - ), - isTemplate - ? Offstage() - : Container( - height: 40, - width: double.infinity, - padding: EdgeInsets.only(left: 24), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: <Widget>[ - GestureDetector( - onTap: () => Navigator.of(context) - .pushNamed(Routes.sendTemplate), - child: Container( - padding: EdgeInsets.only(left: 1, right: 10), - child: DottedBorder( - borderType: BorderType.RRect, - dashPattern: [6, 4], - color: PaletteDark.darkCyanBlue, - strokeWidth: 2, - radius: Radius.circular(20), - child: Container( - height: 34, - 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: PaletteDark.darkCyanBlue - ), - ), - ) - ), - ), - ), - Observer( - builder: (_) { - final templates = sendViewModel.templates; - final itemCount = templates.length; - - return ListView.builder( - scrollDirection: Axis.horizontal, - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: itemCount, - itemBuilder: (context, index) { - final template = templates[index]; - - return TemplateTile( - key: UniqueKey(), - to: template.name, - amount: template.amount, - from: template.cryptoCurrency, - onTap: () { - _addressController.text = template.address; - _cryptoAmountController.text = template.amount; - getOpenaliasRecord(context); - }, - onRemove: () { - showDialog<void>( - context: context, - builder: (dialogContext) { - return AlertWithTwoActions( - alertTitle: S.of(context).template, - alertContent: S.of(context).confirm_delete_template, - leftButtonText: S.of(context).delete, - rightButtonText: S.of(context).cancel, - actionLeftButton: () { - Navigator.of(dialogContext).pop(); - sendViewModel.sendTemplateStore.remove(template: template); - sendViewModel.sendTemplateStore.update(); - }, - actionRightButton: () => Navigator.of(dialogContext).pop() - ); - } - ); - }, - ); - } - ); - } - ) - ], - ), - ), - ) - ], - ), - bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), - bottomSection: isTemplate - ? PrimaryButton( - onPressed: () { - if (_formKey.currentState.validate()) { - sendViewModel.sendTemplateStore.addTemplate( - name: _nameController.text, - address: _addressController.text, - cryptoCurrency: sendViewModel.currency.title, - amount: _cryptoAmountController.text - ); - sendViewModel.sendTemplateStore.update(); - Navigator.of(context).pop(); - } - }, - text: S.of(context).save, - color: Colors.green, - textColor: Colors.white) - : Observer(builder: (_) { - return LoadingPrimaryButton( + ), + ) + ], + ), + bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), + bottomSection: isTemplate + ? PrimaryButton( onPressed: () { if (_formKey.currentState.validate()) { - print('SENT!!!'); + // sendViewModel.sendTemplateStore.addTemplate( + // name: _nameController.text, + // address: _addressController.text, + // cryptoCurrency: sendViewModel.currency.title, + // amount: _cryptoAmountController.text); + // sendViewModel.sendTemplateStore.update(); + Navigator.of(context).pop(); } }, - text: S.of(context).send, - color: Colors.blue, - textColor: Colors.white, - isLoading: sendViewModel.state is TransactionIsCreating || - sendViewModel.state is TransactionCommitting, - isDisabled: - false // FIXME !(syncStore.status is SyncedSyncStatus), - ); - }), - ), + text: S.of(context).save, + color: Colors.green, + textColor: Colors.white) + : Observer(builder: (_) { + return LoadingPrimaryButton( + onPressed: () { + if (_formKey.currentState.validate()) { + print('SENT!!!'); + } + }, + text: S.of(context).send, + color: Palette.blueCraiola, + textColor: Colors.white, + isLoading: sendViewModel.state is TransactionIsCreating || + sendViewModel.state is TransactionCommitting, + isDisabled: + false // FIXME !(syncStore.status is SyncedSyncStatus), + ); + }), ); } @@ -415,6 +493,13 @@ class BaseSendWidget extends StatelessWidget { return; } + reaction((_) => sendViewModel.sendAll, (bool all) { + if (all) { + _cryptoAmountController.text = S.current.all; + _fiatAmountController.text = null; + } + }); + reaction((_) => sendViewModel.fiatAmount, (String amount) { if (amount != _fiatAmountController.text) { _fiatAmountController.text = amount; @@ -422,6 +507,10 @@ class BaseSendWidget extends StatelessWidget { }); reaction((_) => sendViewModel.cryptoAmount, (String amount) { + if (sendViewModel.sendAll && amount != S.current.all) { + sendViewModel.sendAll = false; + } + if (amount != _cryptoAmountController.text) { _cryptoAmountController.text = amount; } @@ -437,29 +526,7 @@ class BaseSendWidget extends StatelessWidget { final address = _addressController.text; if (sendViewModel.address != address) { - sendViewModel.changeAddress(address); - } - }); - - _fiatAmountController.addListener(() { - final fiatAmount = _fiatAmountController.text; - - if (sendViewModel.fiatAmount != fiatAmount) { - sendViewModel.changeFiatAmount(fiatAmount); - } - }); - - _cryptoAmountController.addListener(() { - final cryptoAmount = _cryptoAmountController.text; - - if (sendViewModel.cryptoAmount != cryptoAmount) { - sendViewModel.changeCryptoAmount(cryptoAmount); - } - }); - - _focusNode.addListener(() { - if (!_focusNode.hasFocus && _addressController.text.isNotEmpty) { - getOpenaliasRecord(context); + sendViewModel.address = address; } }); @@ -469,44 +536,126 @@ class BaseSendWidget extends StatelessWidget { showDialog<void>( context: context, builder: (BuildContext context) { - return AlertDialog( - title: Text(S.of(context).error), - content: Text(state.error), - actions: <Widget>[ - FlatButton( - child: Text(S.of(context).ok), - onPressed: () => Navigator.of(context).pop()) - ], - ); + return AlertWithOneAction( + 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<void>( -// 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<void>( -// context: context, -// builder: (BuildContext context) { -// return SendingAlert(sendStore: sendStore); -// }); -// }, -// actionRightButton: () => Navigator.of(context).pop()); -// }); -// }); + WidgetsBinding.instance.addPostFrameCallback((_) { + showDialog<void>( + context: context, + builder: (BuildContext context) { + return ConfirmSendingAlert( + alertTitle: S.of(context).confirm_sending, + amount: S.of(context).send_amount, + amountValue: + sendViewModel.pendingTransaction.amountFormatted, + fee: S.of(context).send_fee, + feeValue: sendViewModel.pendingTransaction.feeFormatted, + leftButtonText: S.of(context).ok, + rightButtonText: S.of(context).cancel, + actionLeftButton: () { + Navigator.of(context).pop(); + sendViewModel.commitTransaction(); + showDialog<void>( + context: context, + builder: (BuildContext context) { + return Observer(builder: (_) { + final state = sendViewModel.state; + + if (state is TransactionCommitted) { + return Stack( + children: <Widget>[ + Container( + color: Theme.of(context).backgroundColor, + child: Center( + child: Image.asset( + 'assets/images/birthday_cake.png'), + ), + ), + Center( + child: Padding( + padding: EdgeInsets.only( + top: 220, left: 24, right: 24), + child: Text( + S.of(context).send_success, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Theme.of(context) + .primaryTextTheme + .title + .color, + decoration: TextDecoration.none, + ), + ), + ), + ), + Positioned( + left: 24, + right: 24, + bottom: 24, + child: PrimaryButton( + onPressed: () => + Navigator.of(context).pop(), + text: S.of(context).send_got_it, + color: Colors.blue, + textColor: Colors.white)) + ], + ); + } + + return Stack( + children: <Widget>[ + Container( + color: Theme.of(context).backgroundColor, + child: Center( + child: Image.asset( + 'assets/images/birthday_cake.png'), + ), + ), + BackdropFilter( + filter: ImageFilter.blur( + sigmaX: 3.0, sigmaY: 3.0), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context) + .backgroundColor + .withOpacity(0.25)), + child: Center( + child: Padding( + padding: EdgeInsets.only(top: 220), + child: Text( + S.of(context).send_sending, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Theme.of(context) + .primaryTextTheme + .title + .color, + decoration: TextDecoration.none, + ), + ), + ), + ), + ), + ) + ], + ); + }); + }); + }, + actionRightButton: () => Navigator.of(context).pop()); + }); + }); } if (state is TransactionCommitted) { @@ -521,21 +670,40 @@ class BaseSendWidget extends StatelessWidget { } Future<void> getOpenaliasRecord(BuildContext context) async { - final isOpenalias = await sendViewModel.isOpenaliasRecord(_addressController.text); + // final isOpenalias = + // await sendViewModel.isOpenaliasRecord(_addressController.text); - if (isOpenalias) { - _addressController.text = sendViewModel.recordAddress; + // if (isOpenalias) { + // _addressController.text = sendViewModel.recordAddress; - await showDialog<void>( - context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: S.of(context).openalias_alert_title, - alertContent: S.of(context).openalias_alert_content(sendViewModel.recordName), - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop() - ); - }); - } + // await showDialog<void>( + // context: context, + // builder: (BuildContext context) { + // return AlertWithOneAction( + // alertTitle: S.of(context).openalias_alert_title, + // alertContent: S + // .of(context) + // .openalias_alert_content(sendViewModel.recordName), + // buttonText: S.of(context).ok, + // buttonAction: () => Navigator.of(context).pop()); + // }); + // } } -} \ No newline at end of file + + Future<void> _setTransactionPriority(BuildContext context) async { + final items = TransactionPriority.all; + final selectedItem = items.indexOf(sendViewModel.transactionPriority); + + await showDialog<void>( + builder: (_) => Picker( + items: items, + selectedAtIndex: selectedItem, + title: S.of(context).please_select, + mainAxisAlignment: MainAxisAlignment.center, + onItemSelected: (TransactionPriority priority) => null, + // sendViewModel.setTransactionPriority(priority), + isAlwaysShowScrollThumb: true, + ), + context: context); + } +} diff --git a/lib/src/screens/send/widgets/sending_alert.dart b/lib/src/screens/send/widgets/sending_alert.dart index db6803ead..08848fb6f 100644 --- a/lib/src/screens/send/widgets/sending_alert.dart +++ b/lib/src/screens/send/widgets/sending_alert.dart @@ -1,6 +1,6 @@ import 'dart:ui'; -import 'package:cake_wallet/src/stores/send/sending_state.dart'; import 'package:flutter/material.dart'; +import 'package:cake_wallet/src/stores/send/sending_state.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/stores/send/send_store.dart'; import 'package:cake_wallet/generated/i18n.dart'; diff --git a/lib/src/screens/settings/settings.dart b/lib/src/screens/settings/settings.dart index c1f4cfabe..e35508d81 100644 --- a/lib/src/screens/settings/settings.dart +++ b/lib/src/screens/settings/settings.dart @@ -39,9 +39,11 @@ class SettingsPage extends BasePage { if (item is PickerListItem) { return Observer(builder: (_) { return SettingsPickerCell<dynamic>( - title: item.title, - selectedItem: item.selectedItem(), - items: item.items); + title: item.title, + selectedItem: item.selectedItem(), + items: item.items, + onItemSelected: (dynamic value) => item.onItemSelected(value), + ); }); } diff --git a/lib/src/screens/settings/widgets/settings_picker_cell.dart b/lib/src/screens/settings/widgets/settings_picker_cell.dart index a94bdfe0d..1fc574b7a 100644 --- a/lib/src/screens/settings/widgets/settings_picker_cell.dart +++ b/lib/src/screens/settings/widgets/settings_picker_cell.dart @@ -4,25 +4,30 @@ import 'package:cake_wallet/src/widgets/standard_list.dart'; import 'package:cake_wallet/generated/i18n.dart'; class SettingsPickerCell<ItemType> extends StandardListRow { - SettingsPickerCell({@required String title, this.selectedItem, this.items}) + SettingsPickerCell( + {@required String title, + this.selectedItem, + this.items, + this.onItemSelected}) : super( - title: title, - isSelected: false, - onTap: (BuildContext context) async { - final selectedAtIndex = items.indexOf(selectedItem); + title: title, + isSelected: false, + onTap: (BuildContext context) async { + final selectedAtIndex = items.indexOf(selectedItem); - await showDialog<void>( - context: context, - builder: (_) => Picker( - items: items, - selectedAtIndex: selectedAtIndex, - title: S.current.please_select, - mainAxisAlignment: MainAxisAlignment.center, - onItemSelected: (Object _) {})); - }); + await showDialog<void>( + context: context, + builder: (_) => Picker( + items: items, + selectedAtIndex: selectedAtIndex, + title: S.current.please_select, + mainAxisAlignment: MainAxisAlignment.center, + onItemSelected: (ItemType item) => onItemSelected?.call(item))); + }); final ItemType selectedItem; final List<ItemType> items; + final void Function(ItemType item) onItemSelected; @override Widget buildTrailing(BuildContext context) { @@ -35,4 +40,4 @@ class SettingsPickerCell<ItemType> extends StandardListRow { color: Theme.of(context).primaryTextTheme.caption.color), ); } -} \ No newline at end of file +} diff --git a/lib/src/screens/transaction_details/transaction_details_page.dart b/lib/src/screens/transaction_details/transaction_details_page.dart index bed01d5ee..bb2a80e33 100644 --- a/lib/src/screens/transaction_details/transaction_details_page.dart +++ b/lib/src/screens/transaction_details/transaction_details_page.dart @@ -1,3 +1,4 @@ + import 'package:intl/intl.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -48,6 +49,9 @@ class TransactionDetailsPage extends BasePage { StandartListItem( title: S.current.transaction_details_date, value: dateFormat.format(tx.date)), + StandartListItem( + title: 'Confirmations', + value: tx.confirmations?.toString()), StandartListItem( title: S.current.transaction_details_height, value: '${tx.height}'), StandartListItem( diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index 6b8e047cf..b5f2f9adc 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -37,11 +37,14 @@ class WalletListBodyState extends State<WalletListBody> { final bitcoinIcon = Image.asset('assets/images/bitcoin.png', height: 24, width: 24); final scrollController = ScrollController(); + final double tileHeight = 60; @override Widget build(BuildContext context) { final newWalletImage = Image.asset('assets/images/new_wallet.png', - height: 12, width: 12, color: Palette.oceanBlue); + height: 12, + width: 12, + color: Theme.of(context).accentTextTheme.headline.decorationColor); final restoreWalletImage = Image.asset('assets/images/restore_wallet.png', height: 12, width: 12, @@ -58,19 +61,11 @@ class WalletListBodyState extends State<WalletListBody> { shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), separatorBuilder: (_, index) => Divider( - color: Theme.of(context).backgroundColor, height: 16), + color: Theme.of(context).backgroundColor, height: 32), itemCount: widget.walletListViewModel.wallets.length, itemBuilder: (__, index) { final wallet = widget.walletListViewModel.wallets[index]; final screenWidth = MediaQuery.of(context).size.width; -// String shortAddress = ''; - -// if (wallet.isCurrent) { -// shortAddress = wallet.address; -// shortAddress = shortAddress.replaceRange( -// 4, shortAddress.length - 4, '...'); -// } - final walletMenu = WalletMenu(context, widget.walletListViewModel); final items = walletMenu.generateItemsForWalletMenu(wallet.isCurrent); @@ -80,7 +75,7 @@ class WalletListBodyState extends State<WalletListBody> { .generateImagesForWalletMenu(wallet.isCurrent); return Container( - height: 108, + height: tileHeight, width: double.infinity, child: CustomScrollView( scrollDirection: Axis.horizontal, @@ -90,7 +85,7 @@ class WalletListBodyState extends State<WalletListBody> { pinned: false, floating: true, delegate: WalletTile( - min: screenWidth - 228, + min: screenWidth - 170, max: screenWidth, image: _imageFor(type: wallet.type), walletName: wallet.name, @@ -101,10 +96,11 @@ class WalletListBodyState extends State<WalletListBody> { delegate: SliverChildBuilderDelegate((context, index) { final item = items[index]; - final color = colors[index]; final image = images[index]; + final firstColor = colors[index*2]; + final secondColor = colors[index*2 + 1]; - final radius = index == 0 ? 12.0 : 0.0; + final radius = index == 0 ? 10.0 : 0.0; return GestureDetector( onTap: () { @@ -117,8 +113,8 @@ class WalletListBodyState extends State<WalletListBody> { wallet.isCurrent); }, child: Container( - height: 108, - width: 108, + height: tileHeight, + width: 80, color: Theme.of(context).backgroundColor, child: Container( padding: EdgeInsets.only(left: 5, right: 5), @@ -126,18 +122,27 @@ class WalletListBodyState extends State<WalletListBody> { borderRadius: BorderRadius.only( topLeft: Radius.circular(radius), bottomLeft: Radius.circular(radius)), - color: color), + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + firstColor, + secondColor + ] + ) + ), child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: <Widget>[ image, - SizedBox(height: 5), + SizedBox(height: 2), Text( item, textAlign: TextAlign.center, style: TextStyle( - fontSize: 12, + fontSize: 7, + fontWeight: FontWeight.w500, color: Colors.white), ) ], @@ -159,9 +164,8 @@ class WalletListBodyState extends State<WalletListBody> { 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, + color: Theme.of(context).accentTextTheme.subtitle.decorationColor, + textColor: Theme.of(context).accentTextTheme.headline.decorationColor, ), SizedBox(height: 10.0), PrimaryImageButton( @@ -169,7 +173,7 @@ class WalletListBodyState extends State<WalletListBody> { .pushNamed(Routes.restoreWalletType), image: restoreWalletImage, text: S.of(context).wallet_list_restore_wallet, - color: Theme.of(context).primaryTextTheme.overline.color, + color: Theme.of(context).accentTextTheme.caption.color, textColor: Theme.of(context).primaryTextTheme.title.color) ])), )); diff --git a/lib/src/screens/wallet_list/wallet_menu.dart b/lib/src/screens/wallet_list/wallet_menu.dart index b6654d0e8..7ac1c1986 100644 --- a/lib/src/screens/wallet_list/wallet_menu.dart +++ b/lib/src/screens/wallet_list/wallet_menu.dart @@ -6,6 +6,7 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/stores/wallet_list/wallet_list_store.dart'; import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; import 'package:cake_wallet/src/screens/auth/auth_page.dart'; +import 'package:cake_wallet/palette.dart'; class WalletMenu { WalletMenu(this.context, this.walletListViewModel); @@ -20,18 +21,25 @@ class WalletMenu { S.current.rescan ]; - final List<Color> listColors = [ - Colors.blue, - Colors.orange, - Colors.red, - Colors.green + final List<Color> firstColors = [ + Palette.cornflower, + Palette.moderateOrangeYellow, + Palette.lightRed, + Palette.shineGreen + ]; + + final List<Color> secondColors = [ + Palette.royalBlue, + Palette.moderateOrange, + Palette.persianRed, + Palette.moderateGreen ]; final List<Image> listImages = [ - Image.asset('assets/images/load.png', height: 32, width: 32, color: Colors.white), - Image.asset('assets/images/eye_action.png', height: 32, width: 32, color: Colors.white), - Image.asset('assets/images/trash.png', height: 32, width: 32, color: Colors.white), - Image.asset('assets/images/scanner.png', height: 32, width: 32, color: Colors.white) + Image.asset('assets/images/load.png', height: 24, width: 24, color: Colors.white), + Image.asset('assets/images/eye_action.png', height: 24, width: 24, color: Colors.white), + Image.asset('assets/images/trash.png', height: 24, width: 24, color: Colors.white), + Image.asset('assets/images/scanner.png', height: 24, width: 24, color: Colors.white) ]; List<String> generateItemsForWalletMenu(bool isCurrentWallet) { @@ -46,18 +54,30 @@ class WalletMenu { } List<Color> generateColorsForWalletMenu(bool isCurrentWallet) { - final colors = List<Color>(); + final colors = <Color>[]; - if (!isCurrentWallet) colors.add(listColors[0]); - if (isCurrentWallet) colors.add(listColors[1]); - if (!isCurrentWallet) colors.add(listColors[2]); - if (isCurrentWallet) colors.add(listColors[3]); + if (!isCurrentWallet) { + colors.add(firstColors[0]); + colors.add(secondColors[0]); + } + if (isCurrentWallet) { + colors.add(firstColors[1]); + colors.add(secondColors[1]); + } + if (!isCurrentWallet) { + colors.add(firstColors[2]); + colors.add(secondColors[2]); + } + if (isCurrentWallet) { + colors.add(firstColors[3]); + colors.add(secondColors[3]); + } return colors; } List<Image> generateImagesForWalletMenu(bool isCurrentWallet) { - final images = List<Image>(); + final images = <Image>[]; if (!isCurrentWallet) images.add(listImages[0]); if (isCurrentWallet) images.add(listImages[1]); @@ -109,7 +129,7 @@ class WalletMenu { try { auth.changeProcessText( S.of(context).wallet_list_removing_wallet(wallet.name)); -// await _walletListStore.remove(wallet); + await walletListViewModel.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 42a2301a9..4e4165341 100644 --- a/lib/src/screens/wallet_list/widgets/wallet_tile.dart +++ b/lib/src/screens/wallet_list/widgets/wallet_tile.dart @@ -17,17 +17,18 @@ class WalletTile extends SliverPersistentHeaderDelegate { final String walletName; final String walletAddress; final bool isCurrent; + final double tileHeight = 60; @override Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { var opacity = 1 - shrinkOffset / (max - min); opacity = opacity >= 0 ? opacity : 0; - var panelWidth = 12 * opacity; - panelWidth = panelWidth < 12 ? 0 : 12; + var panelWidth = 10 * opacity; + panelWidth = panelWidth < 10 ? 0 : 10; final currentColor = isCurrent - ? Theme.of(context).accentTextTheme.caption.color + ? Theme.of(context).accentTextTheme.subtitle.decorationColor : Theme.of(context).backgroundColor; return Stack( @@ -38,7 +39,7 @@ class WalletTile extends SliverPersistentHeaderDelegate { top: 0, right: max - 4, child: Container( - height: 108, + height: tileHeight, width: 4, decoration: BoxDecoration( borderRadius: BorderRadius.only(topRight: Radius.circular(4), bottomRight: Radius.circular(4)), @@ -48,13 +49,30 @@ class WalletTile extends SliverPersistentHeaderDelegate { ), Positioned( top: 0, - right: 12, + right: 10, child: Container( - height: 108, - width: max - 16, + height: tileHeight, + width: max - 14, padding: EdgeInsets.only(left: 20, right: 20), color: Theme.of(context).backgroundColor, - child: Column( + alignment: Alignment.centerLeft, + child: Row( + //mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: <Widget>[ + image, + SizedBox(width: 10), + Text( + walletName, + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.w600, + color: Theme.of(context).primaryTextTheme.title.color + ), + ) + ], + ), + /*Column( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ @@ -92,7 +110,7 @@ class WalletTile extends SliverPersistentHeaderDelegate { ) : Offstage() ], - ), + ),*/ ), ), Positioned( @@ -101,29 +119,18 @@ class WalletTile extends SliverPersistentHeaderDelegate { child: Opacity( opacity: opacity, child: Container( - height: 108, + height: tileHeight, width: panelWidth, - padding: EdgeInsets.only( - top: 1, - left: 1, - bottom: 1 - ), decoration: BoxDecoration( - borderRadius: BorderRadius.only(topLeft: Radius.circular(12), bottomLeft: Radius.circular(12)), - color: Theme.of(context).accentTextTheme.subtitle.color - ), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.only(topLeft: Radius.circular(12), bottomLeft: Radius.circular(12)), - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Theme.of(context).accentTextTheme.caption.backgroundColor, - Theme.of(context).accentTextTheme.caption.decorationColor - ] - ) - ), + borderRadius: BorderRadius.only(topLeft: Radius.circular(10), bottomLeft: Radius.circular(10)), + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Theme.of(context).accentTextTheme.headline.color, + Theme.of(context).accentTextTheme.headline.backgroundColor + ] + ) ), ), ) diff --git a/lib/src/stores/settings/settings_store.dart b/lib/src/stores/settings/settings_store.dart index b3fe22e5a..21b56a967 100644 --- a/lib/src/stores/settings/settings_store.dart +++ b/lib/src/stores/settings/settings_store.dart @@ -42,7 +42,7 @@ abstract class SettingsStoreBase with Store { _sharedPreferences = sharedPreferences; _nodes = nodes; allowBiometricalAuthentication = initialAllowBiometricalAuthentication; - isDarkTheme = initialDarkTheme; + isDarkTheme = true; defaultPinLength = initialPinLength; languageCode = initialLanguageCode; currentLocale = initialCurrentLocale; @@ -143,7 +143,7 @@ abstract class SettingsStoreBase with Store { bool allowBiometricalAuthentication; @observable - bool isDarkTheme; + bool isDarkTheme = true; @observable int defaultPinLength; @@ -285,7 +285,7 @@ abstract class SettingsStoreBase with Store { } Future setCurrentNodeToDefault() async { - await changeCurrentNodeToDefault(sharedPreferences: _sharedPreferences, nodes: _nodes); +// await changeCurrentNodeToDefault(sharedPreferences: _sharedPreferences, nodes: _nodes); await loadSettings(); } diff --git a/lib/src/widgets/address_text_field.dart b/lib/src/widgets/address_text_field.dart index 3e96a9e76..a1a7f343d 100644 --- a/lib/src/widgets/address_text_field.dart +++ b/lib/src/widgets/address_text_field.dart @@ -1,4 +1,3 @@ -import 'package:cake_wallet/palette.dart'; import 'package:cake_wallet/routes.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/generated/i18n.dart'; @@ -23,6 +22,7 @@ class AddressTextField extends StatelessWidget { this.isBorderExist = true, this.buttonColor, this.borderColor, + this.iconColor, this.textStyle, this.hintStyle, this.validator}); @@ -40,6 +40,7 @@ class AddressTextField extends StatelessWidget { final bool isBorderExist; final Color buttonColor; final Color borderColor; + final Color iconColor; final TextStyle textStyle; final TextStyle hintStyle; FocusNode focusNode; @@ -55,7 +56,7 @@ class AddressTextField extends StatelessWidget { focusNode: focusNode, style: textStyle ?? TextStyle( fontSize: 16, - color: Colors.white + color: Theme.of(context).primaryTextTheme.title.color ), decoration: InputDecoration( suffixIcon: SizedBox( @@ -64,7 +65,7 @@ class AddressTextField extends StatelessWidget { ), hintStyle: hintStyle ?? TextStyle( fontSize: 16, - color: PaletteDark.darkCyanBlue + color: Theme.of(context).hintColor ), hintText: placeholder ?? S.current.widgets_address, focusedBorder: isBorderExist @@ -112,7 +113,9 @@ class AddressTextField extends StatelessWidget { borderRadius: BorderRadius.all(Radius.circular(6))), child: Image.asset( - 'assets/images/duplicate.png')), + 'assets/images/duplicate.png', + color: iconColor ?? Theme.of(context).primaryTextTheme.display1.decorationColor, + )), )), ], if (this.options.contains(AddressTextFieldOption.qrCode)) ...[ @@ -128,7 +131,9 @@ class AddressTextField extends StatelessWidget { color: buttonColor ?? Theme.of(context).accentTextTheme.title.color, borderRadius: BorderRadius.all(Radius.circular(6))), - child: Image.asset('assets/images/qr_code_icon.png')), + child: Image.asset('assets/images/qr_code_icon.png', + color: iconColor ?? Theme.of(context).primaryTextTheme.display1.decorationColor, + )), )) ], if (this @@ -147,7 +152,9 @@ class AddressTextField extends StatelessWidget { borderRadius: BorderRadius.all(Radius.circular(6))), child: Image.asset( - 'assets/images/open_book.png')), + 'assets/images/open_book.png', + color: iconColor ?? Theme.of(context).primaryTextTheme.display1.decorationColor, + )), )) ], if (this @@ -166,7 +173,9 @@ class AddressTextField extends StatelessWidget { borderRadius: BorderRadius.all(Radius.circular(6))), child: Image.asset( - 'assets/images/receive_icon_raw.png')), + 'assets/images/receive_icon_raw.png', + color: iconColor ?? Theme.of(context).primaryTextTheme.display1.decorationColor, + )), )), ], ], diff --git a/lib/src/widgets/alert_with_one_action.dart b/lib/src/widgets/alert_with_one_action.dart index 704243591..e7f1a0d15 100644 --- a/lib/src/widgets/alert_with_one_action.dart +++ b/lib/src/widgets/alert_with_one_action.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:cake_wallet/src/widgets/base_alert_dialog.dart'; +import 'package:cake_wallet/palette.dart'; class AlertWithOneAction extends BaseAlertDialog { AlertWithOneAction({ @@ -31,13 +32,7 @@ class AlertWithOneAction extends BaseAlertDialog { width: 300, height: 52, padding: EdgeInsets.only(left: 12, right: 12), - decoration: BoxDecoration( - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(24), - bottomRight: Radius.circular(24) - ), - color: Colors.white - ), + color: Palette.blueCraiola, child: ButtonTheme( minWidth: double.infinity, child: FlatButton( @@ -50,7 +45,7 @@ class AlertWithOneAction extends BaseAlertDialog { style: TextStyle( fontSize: 15, fontWeight: FontWeight.w600, - color: Colors.blue, + color: Colors.white, decoration: TextDecoration.none, ), )), diff --git a/lib/src/widgets/base_alert_dialog.dart b/lib/src/widgets/base_alert_dialog.dart index 8a10d1d3f..14b5a58a4 100644 --- a/lib/src/widgets/base_alert_dialog.dart +++ b/lib/src/widgets/base_alert_dialog.dart @@ -17,6 +17,7 @@ class BaseAlertDialog extends StatelessWidget { textAlign: TextAlign.center, style: TextStyle( fontSize: 20, + fontFamily: 'Poppins', fontWeight: FontWeight.w600, color: Theme.of(context).primaryTextTheme.title.color, decoration: TextDecoration.none, @@ -30,7 +31,7 @@ class BaseAlertDialog extends StatelessWidget { textAlign: TextAlign.center, style: TextStyle( fontSize: 16, - fontWeight: FontWeight.w600, + fontFamily: 'Poppins', color: Theme.of(context).primaryTextTheme.title.color, decoration: TextDecoration.none, ), @@ -38,55 +39,39 @@ class BaseAlertDialog extends StatelessWidget { } Widget actionButtons(BuildContext context) { - return Container( - width: 300, - height: 52, - decoration: BoxDecoration( - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(24), - bottomRight: Radius.circular(24) - ), - color: Colors.white - ), - child: Row( - mainAxisSize: MainAxisSize.max, - children: <Widget>[ - Flexible( + return Row( + mainAxisSize: MainAxisSize.max, + children: <Widget>[ + Flexible( child: Container( - padding: EdgeInsets.only(left: 12, right: 12), - decoration: BoxDecoration( - borderRadius: BorderRadius.only(bottomLeft: Radius.circular(24)), - ), + height: 52, + padding: EdgeInsets.only(left: 6, right: 6), + color: Palette.blueCraiola, child: ButtonTheme( minWidth: double.infinity, child: FlatButton( - onPressed: actionLeft, - highlightColor: Colors.transparent, - splashColor: Colors.transparent, - child: Text( - leftActionButtonText, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w600, - color: Colors.blue, - decoration: TextDecoration.none, - ), - )), + onPressed: actionLeft, + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + child: Text( + leftActionButtonText, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 15, + fontFamily: 'Poppins', + fontWeight: FontWeight.w600, + color: Colors.white, + decoration: TextDecoration.none, + ), + )), ), ) - ), - Container( - height: 52, - width: 1, - color: Colors.grey.withOpacity(0.2), - ), - Flexible( + ), + Flexible( child: Container( - padding: EdgeInsets.only(left: 12, right: 12), - decoration: BoxDecoration( - borderRadius: BorderRadius.only(bottomRight: Radius.circular(24)), - ), + height: 52, + padding: EdgeInsets.only(left: 6, right: 6), + color: Palette.alizarinRed, child: ButtonTheme( minWidth: double.infinity, child: FlatButton( @@ -98,16 +83,16 @@ class BaseAlertDialog extends StatelessWidget { textAlign: TextAlign.center, style: TextStyle( fontSize: 15, + fontFamily: 'Poppins', fontWeight: FontWeight.w600, - color: Colors.red, + color: Colors.white, decoration: TextDecoration.none, ), )), ), ) - ) - ], - ), + ) + ], ); } @@ -126,44 +111,30 @@ class BaseAlertDialog extends StatelessWidget { child: Center( child: GestureDetector( onTap: () => null, - child: Container( - width: 300, - height: 257, - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(24)), - color: Theme.of(context).accentTextTheme.title.backgroundColor - ), - child: Column( - children: <Widget>[ - Container( - width: 300, - height: 77, - padding: EdgeInsets.only(left: 24, right: 24), - decoration: BoxDecoration( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(24), - topRight: Radius.circular(24) + child: ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(30)), + child: Container( + width: 300, + color: Theme.of(context).accentTextTheme.title.decorationColor, + child: Column( + mainAxisSize: MainAxisSize.min, + children: <Widget>[ + Container( + padding: EdgeInsets.fromLTRB(24, 32, 24, 32), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: <Widget>[ + title(context), + Padding( + padding: EdgeInsets.only(top: 8), + child: content(context), + ) + ], ), ), - child: Center( - child: title(context), - ), - ), - Container( - width: 300, - height: 1, - color: Theme.of(context).dividerColor, - ), - Container( - width: 300, - height: 127, - padding: EdgeInsets.all(24), - child: Center( - child: content(context), - ), - ), - actionButtons(context) - ], + actionButtons(context) + ], + ), ), ), ), diff --git a/lib/src/widgets/base_text_form_field.dart b/lib/src/widgets/base_text_form_field.dart index 21ae78cc5..c1c34345b 100644 --- a/lib/src/widgets/base_text_form_field.dart +++ b/lib/src/widgets/base_text_form_field.dart @@ -21,7 +21,8 @@ class BaseTextFormField extends StatelessWidget { this.enabled = true, this.validator, this.textStyle, - this.placeholderTextStyle}); + this.placeholderTextStyle, + this.maxLength}); final TextEditingController controller; final TextInputType keyboardType; @@ -42,6 +43,7 @@ class BaseTextFormField extends StatelessWidget { final FormFieldValidator<String> validator; final TextStyle placeholderTextStyle; final TextStyle textStyle; + final int maxLength; @override Widget build(BuildContext context) { @@ -54,6 +56,7 @@ class BaseTextFormField extends StatelessWidget { maxLines: maxLines, inputFormatters: inputFormatters, enabled: enabled, + maxLength: maxLength, style: textStyle ?? TextStyle( fontSize: 16.0, color: textColor ?? Theme.of(context).primaryTextTheme.title.color), diff --git a/lib/src/widgets/checkbox_widget.dart b/lib/src/widgets/checkbox_widget.dart new file mode 100644 index 000000000..9bb8b74a8 --- /dev/null +++ b/lib/src/widgets/checkbox_widget.dart @@ -0,0 +1,81 @@ +import 'dart:ui'; +import 'package:cake_wallet/palette.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class CheckboxWidget extends StatefulWidget { + CheckboxWidget({ + @required this.value, + @required this.caption, + @required this.onChanged}); + + final bool value; + final String caption; + final Function(bool) onChanged; + + @override + CheckboxWidgetState createState() => CheckboxWidgetState(value, caption, onChanged); +} + +class CheckboxWidgetState extends State<CheckboxWidget> { + CheckboxWidgetState(this.value, this.caption, this.onChanged); + + bool value; + String caption; + Function(bool) onChanged; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + value = !value; + onChanged(value); + setState(() {}); + }, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + children: <Widget>[ + Container( + height: 16, + width: 16, + decoration: BoxDecoration( + color: value + ? Palette.blueCraiola + : Theme.of(context).accentTextTheme.subhead.decorationColor, + borderRadius: BorderRadius.all(Radius.circular(2)), + border: Border.all( + color: value + ? Palette.blueCraiola + : Theme.of(context).accentTextTheme.overline.color, + width: 1 + ) + ), + child: value + ? Center( + child: Icon( + Icons.done, + color: Colors.white, + size: 14, + ), + ) + : Offstage(), + ), + Padding( + padding: EdgeInsets.only(left: 16), + child: Text( + caption, + style: TextStyle( + color: Theme.of(context).primaryTextTheme.title.color, + fontSize: 18, + fontFamily: 'Poppins', + fontWeight: FontWeight.w500, + decoration: TextDecoration.none + ), + ), + ) + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/src/widgets/picker.dart b/lib/src/widgets/picker.dart index e9573c1e6..732c2118d 100644 --- a/lib/src/widgets/picker.dart +++ b/lib/src/widgets/picker.dart @@ -1,15 +1,20 @@ import 'dart:ui'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:cake_wallet/src/widgets/alert_background.dart'; +import 'package:cake_wallet/src/widgets/cake_scrollbar.dart'; +import 'package:cake_wallet/src/widgets/alert_close_button.dart'; import 'package:cake_wallet/palette.dart'; -class Picker<Item extends Object> extends StatelessWidget { +class Picker<Item extends Object> extends StatefulWidget { Picker({ @required this.selectedAtIndex, @required this.items, this.images, @required this.title, @required this.onItemSelected, - this.mainAxisAlignment = MainAxisAlignment.start + this.mainAxisAlignment = MainAxisAlignment.start, + this.isAlwaysShowScrollThumb = false }); final int selectedAtIndex; @@ -18,59 +23,87 @@ class Picker<Item extends Object> extends StatelessWidget { final String title; final Function(Item) onItemSelected; final MainAxisAlignment mainAxisAlignment; + final bool isAlwaysShowScrollThumb; + + @override + PickerState createState() => PickerState<Item>(items, images, onItemSelected); +} + +class PickerState<Item> extends State<Picker> { + PickerState(this.items, this.images, this.onItemSelected); + + final Function(Item) onItemSelected; + final List<Item> items; + final List<Image> images; + + final closeButton = Image.asset('assets/images/close.png', + color: Palette.darkBlueCraiola, + ); + ScrollController controller = ScrollController(); + + final double backgroundHeight = 193; + final double thumbHeight = 72; + double fromTop = 0; @override Widget build(BuildContext context) { - return GestureDetector( - onTap: () => Navigator.of(context).pop(), - child: Container( - color: Colors.transparent, - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 3.0, sigmaY: 3.0), - child: Container( - decoration: BoxDecoration(color: PaletteDark.darkNightBlue.withOpacity(0.75)), - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: <Widget>[ - Container( - padding: EdgeInsets.only(left: 24, right: 24), - child: Text( - title, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - decoration: TextDecoration.none, - color: Colors.white - ), - ), + controller.addListener(() { + fromTop = controller.hasClients + ? (controller.offset / controller.position.maxScrollExtent * (backgroundHeight - thumbHeight)) + : 0; + setState(() {}); + }); + + return AlertBackground( + child: Stack( + alignment: Alignment.center, + children: <Widget>[ + Column( + mainAxisSize: MainAxisSize.min, + children: <Widget>[ + Container( + padding: EdgeInsets.only(left: 24, right: 24), + child: Text( + widget.title, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 18, + fontFamily: 'Poppins', + fontWeight: FontWeight.bold, + decoration: TextDecoration.none, + color: Colors.white ), - Padding( - padding: EdgeInsets.only(left: 24, right: 24, top: 24), - child: GestureDetector( - onTap: () => null, - child: ClipRRect( - borderRadius: BorderRadius.all(Radius.circular(14)), - child: Container( - height: 233, - color: Theme.of(context).accentTextTheme.title.backgroundColor, - child: ListView.separated( + ), + ), + Padding( + padding: EdgeInsets.only(left: 24, right: 24, top: 24), + child: GestureDetector( + onTap: () => null, + child: ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(14)), + child: Container( + height: 233, + color: Theme.of(context).accentTextTheme.title.color, + child: Stack( + alignment: Alignment.center, + children: <Widget>[ + ListView.separated( + controller: controller, separatorBuilder: (context, index) => Divider( - color: Theme.of(context).dividerColor, + color: Theme.of(context).accentTextTheme.title.backgroundColor, height: 1, ), itemCount: items == null ? 0 : items.length, itemBuilder: (context, index) { final item = items[index]; final image = images != null? images[index] : null; - final isItemSelected = index == selectedAtIndex; + final isItemSelected = index == widget.selectedAtIndex; final color = isItemSelected - ? Theme.of(context).accentTextTheme.subtitle.decorationColor - : Colors.transparent; + ? Theme.of(context).textTheme.body2.color + : Theme.of(context).accentTextTheme.title.color; final textColor = isItemSelected - ? Colors.blue + ? Palette.blueCraiola : Theme.of(context).primaryTextTheme.title.color; return GestureDetector( @@ -87,21 +120,22 @@ class Picker<Item extends Object> extends StatelessWidget { color: color, child: Row( mainAxisSize: MainAxisSize.max, - mainAxisAlignment: mainAxisAlignment, + mainAxisAlignment: widget.mainAxisAlignment, crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ image != null - ? image - : Offstage(), + ? image + : Offstage(), Padding( padding: EdgeInsets.only( - left: image != null ? 12 : 0 + left: image != null ? 12 : 0 ), child: Text( item.toString(), style: TextStyle( fontSize: 18, - fontWeight: FontWeight.bold, + fontFamily: 'Poppins', + fontWeight: FontWeight.w600, color: textColor, decoration: TextDecoration.none, ), @@ -112,17 +146,25 @@ class Picker<Item extends Object> extends StatelessWidget { ), ); }, + ), + widget.isAlwaysShowScrollThumb + ? CakeScrollbar( + backgroundHeight: backgroundHeight, + thumbHeight: thumbHeight, + fromTop: fromTop ) - ), - ), + : Offstage(), + ], + ) ), - ) - ], - ), - ) - ), - ), - ), + ), + ), + ) + ], + ), + AlertCloseButton(image: closeButton) + ], + ) ); } -} +} \ No newline at end of file diff --git a/lib/src/widgets/standard_list.dart b/lib/src/widgets/standard_list.dart index 930e59b7c..9ad338870 100644 --- a/lib/src/widgets/standard_list.dart +++ b/lib/src/widgets/standard_list.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/palette.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -39,7 +40,7 @@ class StandardListRow extends StatelessWidget { Text(title, style: TextStyle( fontSize: 14, - fontWeight: FontWeight.w500, + fontWeight: FontWeight.normal, color: _titleColor(context))) ])); } @@ -47,21 +48,24 @@ class StandardListRow extends StatelessWidget { Widget buildTrailing(BuildContext context) => null; Color _titleColor(BuildContext context) => isSelected - ? Color.fromRGBO(20, 200, 71, 1) + ? Palette.blueCraiola : Theme.of(context).primaryTextTheme.title.color; Color _backgroundColor(BuildContext context) { // return Theme.of(context).accentTextTheme.subtitle.decorationColor; - return Theme.of(context).accentTextTheme.title.backgroundColor; + return Theme.of(context).backgroundColor; } } class SectionHeaderListRow extends StatelessWidget { @override Widget build(BuildContext context) => Column(children: [ - StandardListSeparator(), - Container(width: double.infinity, height: 40, color: Colors.white), - StandardListSeparator() + StandardListSeparator(padding: EdgeInsets.only(left: 24)), + Container( + width: double.infinity, + height: 40, + color: Theme.of(context).backgroundColor), + //StandardListSeparator(padding: EdgeInsets.only(left: 24)) ]); } @@ -75,8 +79,10 @@ class StandardListSeparator extends StatelessWidget { return Container( height: 1, padding: padding, - color: Theme.of(context).accentTextTheme.title.backgroundColor, - child: Container(height: 1, color: Color.fromRGBO(219, 227, 243, 1))); + color: Theme.of(context).backgroundColor, + child: Container( + height: 1, + color: Theme.of(context).primaryTextTheme.title.backgroundColor)); } } @@ -126,9 +132,9 @@ class SectionStandardList extends StatelessWidget { final items = <Widget>[]; for (var sectionIndex = 0; sectionIndex < sectionCount; sectionIndex++) { - if (sectionIndex == 0) { + /*if (sectionIndex == 0) { items.add(StandardListSeparator()); - } + }*/ final itemCount = itemCounter(sectionIndex); @@ -140,7 +146,7 @@ class SectionStandardList extends StatelessWidget { items.add(sectionIndex + 1 != sectionCount ? SectionHeaderListRow() - : StandardListSeparator()); + : StandardListSeparator(padding: EdgeInsets.only(left: 24))); } return items; diff --git a/lib/src/widgets/standart_list_row.dart b/lib/src/widgets/standart_list_row.dart index d573e6e5f..d29e84065 100644 --- a/lib/src/widgets/standart_list_row.dart +++ b/lib/src/widgets/standart_list_row.dart @@ -1,13 +1,20 @@ +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; class StandartListRow extends StatelessWidget { StandartListRow( {this.title, this.value, + this.titleFontSize = 14, + this.valueFontSize = 16, + this.image, this.isDrawBottom = false}); final String title; final String value; + final double titleFontSize; + final double valueFontSize; + final Image image; final bool isDrawBottom; @override @@ -19,27 +26,42 @@ class StandartListRow extends StatelessWidget { color: Theme.of(context).backgroundColor, child: Padding( padding: - const EdgeInsets.only(left: 24, top: 16, bottom: 16, right: 24), + const EdgeInsets.only(left: 24, top: 16, bottom: 16, right: 24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Text(title, style: TextStyle( - fontSize: 14, + fontSize: titleFontSize, fontWeight: FontWeight.w500, color: Theme.of(context).primaryTextTheme.overline.color), textAlign: TextAlign.left), Padding( padding: const EdgeInsets.only(top: 12), - child: Text(value, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context) - .primaryTextTheme - .title - .color)), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: <Widget>[ + Expanded( + child: Text(value, + style: TextStyle( + fontSize: valueFontSize, + fontWeight: FontWeight.w500, + color: Theme.of(context) + .primaryTextTheme + .title + .color)), + ), + image != null + ? Padding( + padding: EdgeInsets.only(left: 24), + child: image, + ) + : Offstage() + ], + ), ) ]), ), diff --git a/lib/src/widgets/template_tile.dart b/lib/src/widgets/template_tile.dart index ab6724ece..fbff8b6c1 100644 --- a/lib/src/widgets/template_tile.dart +++ b/lib/src/widgets/template_tile.dart @@ -47,8 +47,7 @@ class TemplateTileState extends State<TemplateTile> { @override Widget build(BuildContext context) { - //final color = isRemovable ? Colors.white : Theme.of(context).primaryTextTheme.title.color; - final color = Colors.white; + final color = isRemovable ? Colors.white : Theme.of(context).primaryTextTheme.title.color; final toIcon = Image.asset('assets/images/to_icon.png', color: color); final content = Row( @@ -106,7 +105,7 @@ class TemplateTileState extends State<TemplateTile> { child: Container( height: 40, padding: EdgeInsets.only(left: 24, right: 24), - color: PaletteDark.darkVioletBlue, + color: Theme.of(context).primaryTextTheme.display3.decorationColor, child: content, ), ), diff --git a/lib/src/widgets/top_panel.dart b/lib/src/widgets/top_panel.dart index 5e2cc2c91..d456d1a39 100644 --- a/lib/src/widgets/top_panel.dart +++ b/lib/src/widgets/top_panel.dart @@ -2,25 +2,28 @@ import 'package:flutter/material.dart'; class TopPanel extends StatefulWidget { TopPanel({ - @required this.color, @required this.widget, - this.edgeInsets = const EdgeInsets.all(24) + this.edgeInsets = const EdgeInsets.all(24), + this.color, + this.gradient }); final Color color; final Widget widget; final EdgeInsets edgeInsets; + final Gradient gradient; @override - TopPanelState createState() => TopPanelState(color, widget, edgeInsets); + TopPanelState createState() => TopPanelState(widget, edgeInsets, color, gradient); } class TopPanelState extends State<TopPanel> { - TopPanelState(this._color, this._widget, this._edgeInsets); + TopPanelState(this._widget, this._edgeInsets, this._color, this._gradient); final Color _color; final Widget _widget; final EdgeInsets _edgeInsets; + final Gradient _gradient; @override Widget build(BuildContext context) { @@ -32,7 +35,8 @@ class TopPanelState extends State<TopPanel> { bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24) ), - color: _color + color: _color, + gradient: _gradient ), child: _widget, ); diff --git a/lib/src/widgets/trail_button.dart b/lib/src/widgets/trail_button.dart index 65cfc14dc..a7c4caecb 100644 --- a/lib/src/widgets/trail_button.dart +++ b/lib/src/widgets/trail_button.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:cake_wallet/palette.dart'; class TrailButton extends StatelessWidget { TrailButton({ @@ -22,7 +21,7 @@ class TrailButton extends StatelessWidget { child: Text( caption, style: TextStyle( - color: PaletteDark.lightBlueGrey, + color: Theme.of(context).textTheme.subhead.decorationColor, fontWeight: FontWeight.w500, fontSize: 14), ), diff --git a/lib/store/dashboard/trade_filter_store.dart b/lib/store/dashboard/trade_filter_store.dart index 2e1c7ed62..181f9b0f0 100644 --- a/lib/store/dashboard/trade_filter_store.dart +++ b/lib/store/dashboard/trade_filter_store.dart @@ -11,8 +11,7 @@ abstract class TradeFilterStoreBase with Store { TradeFilterStoreBase( {this.displayXMRTO = true, this.displayChangeNow = true, - this.displayMorphToken = true, - this.wallet}); + this.displayMorphToken = true}); @observable bool displayXMRTO; @@ -23,8 +22,6 @@ abstract class TradeFilterStoreBase with Store { @observable bool displayMorphToken; - WalletBase wallet; - @action void toggleDisplayExchange(ExchangeProviderDescription provider) { switch (provider) { @@ -40,13 +37,13 @@ abstract class TradeFilterStoreBase with Store { } } - List<TradeListItem> filtered({List<TradeListItem> trades}) { + List<TradeListItem> filtered({List<TradeListItem> trades, WalletBase wallet}) { final _trades = trades.where((item) => item.trade.walletId == wallet.id).toList(); final needToFilter = !displayChangeNow || !displayXMRTO || !displayMorphToken; return needToFilter - ? trades + ? _trades .where((item) => (displayXMRTO && item.trade.provider == ExchangeProviderDescription.xmrto) || diff --git a/lib/store/dashboard/trades_store.dart b/lib/store/dashboard/trades_store.dart index 4f526c637..26a6b292b 100644 --- a/lib/store/dashboard/trades_store.dart +++ b/lib/store/dashboard/trades_store.dart @@ -27,6 +27,12 @@ abstract class TradesStoreBase with Store { @observable List<TradeListItem> trades; + @observable + Trade trade; + + @action + void setTrade(Trade trade) => this.trade = trade; + @action Future updateTradeList() async => trades = tradesSource.values.map((trade) => TradeListItem( diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 91e658fdd..f27943be4 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; @@ -29,8 +30,9 @@ abstract class SettingsStoreBase with Store { @required int initialPinLength, @required String initialLanguageCode, @required String initialCurrentLocale, - @required this.node, +// @required this.node, @required this.appVersion, + @required Map<WalletType, Node> nodes, this.actionlistDisplayMode}) { fiatCurrency = initialFiatCurrency; transactionPriority = initialTransactionPriority; @@ -42,17 +44,13 @@ abstract class SettingsStoreBase with Store { languageCode = initialLanguageCode; currentLocale = initialCurrentLocale; itemHeaders = {}; - -// actionlistDisplayMode.observe( -// (dynamic _) => _sharedPreferences.setInt(displayActionListModeKey, -// serializeActionlistDisplayModes(actionlistDisplayMode)), -// fireImmediately: false); - _sharedPreferences = sharedPreferences; _nodeSource = nodeSource; + _nodes = nodes; } static const currentNodeIdKey = 'current_node_id'; + static const currentBitcoinElectrumSererIdKey = 'current_node_id_btc'; static const currentFiatCurrencyKey = 'current_fiat_currency'; static const currentTransactionPriorityKey = 'current_fee_priority'; static const currentBalanceDisplayModeKey = 'current_balance_display_mode'; @@ -64,8 +62,8 @@ abstract class SettingsStoreBase with Store { static const currentPinLength = 'current_pin_length'; static const currentLanguageCode = 'language_code'; - @observable - Node node; +// @observable +// Node node; @observable FiatCurrency fiatCurrency; @@ -103,6 +101,26 @@ abstract class SettingsStoreBase with Store { SharedPreferences _sharedPreferences; Box<Node> _nodeSource; + Map<WalletType, Node> _nodes; + + Node getCurrentNode(WalletType walletType) => _nodes[walletType]; + + Future<void> setCurrentNode(Node node, WalletType walletType) async { + switch (walletType) { + case WalletType.bitcoin: + await _sharedPreferences.setInt( + currentBitcoinElectrumSererIdKey, node.key as int); + break; + case WalletType.monero: + await _sharedPreferences.setInt(currentNodeIdKey, node.key as int); + break; + default: + break; + } + + _nodes[walletType] = node; + } + static Future<SettingsStore> load( {@required Box<Node> nodeSource, FiatCurrency initialFiatCurrency = FiatCurrency.usd, @@ -132,12 +150,18 @@ abstract class SettingsStoreBase with Store { await Language.localeDetection(); final initialCurrentLocale = await Devicelocale.currentLocale; final nodeId = sharedPreferences.getInt(currentNodeIdKey); - final node = nodeSource.get(nodeId); + final bitcoinElectrumServerId = + sharedPreferences.getInt(currentBitcoinElectrumSererIdKey); + final moneroNode = nodeSource.get(nodeId); + final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId); final packageInfo = await PackageInfo.fromPlatform(); return SettingsStore( sharedPreferences: sharedPreferences, - node: node, + nodes: { + WalletType.monero: moneroNode, + WalletType.bitcoin: bitcoinElectrumServer + }, nodeSource: nodeSource, appVersion: packageInfo.version, initialFiatCurrency: currentFiatCurrency, diff --git a/lib/store/theme_changer_store.dart b/lib/store/theme_changer_store.dart new file mode 100644 index 000000000..8c2e366ca --- /dev/null +++ b/lib/store/theme_changer_store.dart @@ -0,0 +1,13 @@ +import 'package:cake_wallet/theme_changer.dart'; +import 'package:mobx/mobx.dart'; + +part 'theme_changer_store.g.dart'; + +class ThemeChangerStore = ThemeChangerStoreBase with _$ThemeChangerStore; + +abstract class ThemeChangerStoreBase with Store { + ThemeChangerStoreBase({this.themeChanger}); + + @observable + ThemeChanger themeChanger; +} \ No newline at end of file diff --git a/lib/themes.dart b/lib/themes.dart index f3d25b4f1..a25a61de1 100644 --- a/lib/themes.dart +++ b/lib/themes.dart @@ -80,61 +80,92 @@ class Themes { color: Palette.darkGray, // transaction/trade details titles decorationColor: Colors.white.withOpacity(0.5), // placeholder ), - - - - subhead: TextStyle( - color: Colors.white.withOpacity(0.5) // send, exchange, buy buttons on dashboard page + color: Palette.blueCraiola, // first gradient color (send page) + decorationColor: Palette.pinkFlamingo // second gradient color (send page) ), headline: TextStyle( - color: Palette.lightBlueGrey // historyPanelText + color: Colors.white.withOpacity(0.5), // text field border color (send page) + decorationColor: Colors.white.withOpacity(0.5), // text field hint color (send page) ), display1: TextStyle( - color: Colors.white // menuList + color: Colors.white.withOpacity(0.2), // text field button color (send page) + decorationColor: Colors.white // text field button icon color (send page) ), display2: TextStyle( - color: Palette.lavender // menuHeader + color: Colors.white.withOpacity(0.5), // estimated fee (send page) + decorationColor: Palette.shadowWhite // template dotted border (send page) ), display3: TextStyle( - color: Palette.lavender // historyPanelButton + color: Palette.darkBlueCraiola, // template new text (send page) + decorationColor: Palette.shadowWhite // template background color (send page) ), display4: TextStyle( - color: Palette.oceanBlue // QR code + color: Palette.darkBlueCraiola, // template title (send page) + decorationColor: Palette.niagara // receive amount text (exchange page) + ), + subtitle: TextStyle( + color: Palette.blueCraiola, // first gradient color top panel (exchange page) + decorationColor: Palette.pinkFlamingo // second gradient color top panel (exchange page) + ), + body1: TextStyle( + color: Palette.blueCraiola.withOpacity(0.7), // first gradient color bottom panel (exchange page) + decorationColor: Palette.pinkFlamingo.withOpacity(0.7) // second gradient color bottom panel (exchange page) + ), + body2: TextStyle( + color: Colors.white.withOpacity(0.5), // text field border on top panel (exchange page) + decorationColor: Colors.white.withOpacity(0.5), // text field border on bottom panel (exchange page) + ) + ), + focusColor: Colors.white.withOpacity(0.2), // text field button (exchange page) + accentTextTheme: TextTheme( + title: TextStyle( + color: Colors.white, // picker background + backgroundColor: Palette.periwinkleCraiola, // picker divider + decorationColor: Colors.white // dialog background + ), + caption: TextStyle( + color: Palette.moderateLavender, // container (confirm exchange) + backgroundColor: Palette.moderateLavender, // button background (confirm exchange) + decorationColor: Palette.darkBlueCraiola, // text color (information page) + ), + subtitle: TextStyle( + color: Palette.darkBlueCraiola, // QR code (exchange trade page) + backgroundColor: Palette.wildPeriwinkle, // divider (exchange trade page) + decorationColor: Palette.blueCraiola // crete new wallet button background (wallet list page) + ), + headline: TextStyle( + color: Palette.moderateLavender, // first gradient color of wallet action buttons (wallet list page) + backgroundColor: Palette.moderateLavender, // second gradient color of wallet action buttons (wallet list page) + decorationColor: Colors.white // restore wallet button text color (wallet list page) + ), + subhead: TextStyle( + color: Palette.darkGray, // titles color (filter widget) + backgroundColor: Palette.periwinkle, // divider color (filter widget) + decorationColor: Colors.white // checkbox background (filter widget) + ), + overline: TextStyle( + color: Palette.wildPeriwinkle, // checkbox bounds (filter widget) + decorationColor: Colors.white, // menu subname + ), + display1: TextStyle( + color: Palette.blueCraiola, // first gradient color (menu header) + decorationColor: Palette.pinkFlamingo // second gradient color(menu header) + ), + display2: TextStyle( + color: Palette.shadowWhite, // action button color (address text field) ), ), - focusColor: Colors.white, // wallet card border - cardColor: Palette.blueAlice, cardTheme: CardTheme( color: Colors.white, // synced card start ), - accentTextTheme: TextTheme( - title: TextStyle( - color: Palette.darkLavender, // top panel - backgroundColor: Palette.lavender, // bottom panel - decorationColor: PaletteDark.distantBlue // select button background color - ), - caption: TextStyle( - color: Palette.blue, // current wallet label - backgroundColor: Colors.white, // gradient start, wallet label - decorationColor: Palette.lavender, // gradient end, wallet label - ), - subtitle: TextStyle( - color: Palette.lightBlueGrey, // border color, wallet label - backgroundColor: Palette.lavender, // address field, wallet card - decorationColor: Palette.darkLavender // selected item - ), - headline: TextStyle( - color: Palette.darkLavender, // faq background - backgroundColor: Palette.lavender // faq extension - ) - ), + ); @@ -182,7 +213,7 @@ class Themes { decorationColor: PaletteDark.nightBlue // background of tiles (receive page) ), display3: TextStyle( - color: Colors.blue, // text color of current tile (receive page) + color: Palette.blueCraiola, // text color of current tile (receive page) decorationColor: PaletteDark.lightOceanBlue // background of current tile (receive page) ), display4: TextStyle( @@ -215,32 +246,85 @@ class Themes { color: PaletteDark.lightBlueGrey, // transaction/trade details titles decorationColor: Colors.grey, // placeholder ), - - - subhead: TextStyle( - color: PaletteDark.lightDistantBlue // send, exchange, buy buttons on dashboard page + color: PaletteDark.darkNightBlue, // first gradient color (send page) + decorationColor: PaletteDark.darkNightBlue // second gradient color (send page) ), headline: TextStyle( - color: PaletteDark.pigeonBlue // historyPanelText + color: PaletteDark.lightVioletBlue, // text field border color (send page) + decorationColor: PaletteDark.darkCyanBlue, // text field hint color (send page) ), display1: TextStyle( - color: PaletteDark.lightNightBlue // menuList + color: PaletteDark.buttonNightBlue, // text field button color (send page) + decorationColor: PaletteDark.gray // text field button icon color (send page) ), display2: TextStyle( - color: PaletteDark.headerNightBlue // menuHeader + color: Colors.white, // estimated fee (send page) + decorationColor: PaletteDark.darkCyanBlue // template dotted border (send page) ), display3: TextStyle( - color: PaletteDark.moderateNightBlue // historyPanelButton + color: PaletteDark.darkCyanBlue, // template new text (send page) + decorationColor: PaletteDark.darkVioletBlue // template background color (send page) ), display4: TextStyle( - color: PaletteDark.gray // QR code + color: PaletteDark.cyanBlue, // template title (send page) + decorationColor: PaletteDark.darkCyanBlue // receive amount text (exchange page) + ), + subtitle: TextStyle( + color: PaletteDark.wildVioletBlue, // first gradient color top panel (exchange page) + decorationColor: PaletteDark.wildVioletBlue // second gradient color top panel (exchange page) + ), + body1: TextStyle( + color: PaletteDark.darkNightBlue, // first gradient color bottom panel (exchange page) + decorationColor: PaletteDark.darkNightBlue // second gradient color bottom panel (exchange page) + ), + body2: TextStyle( + color: PaletteDark.blueGrey, // text field border on top panel (exchange page) + decorationColor: PaletteDark.moderateVioletBlue, // text field border on bottom panel (exchange page) + ) + ), + focusColor: PaletteDark.moderateBlue, // text field button (exchange page) + accentTextTheme: TextTheme( + title: TextStyle( + color: PaletteDark.nightBlue, // picker background + backgroundColor: PaletteDark.dividerColor, // picker divider + decorationColor: PaletteDark.darkNightBlue // dialog background + ), + caption: TextStyle( + color: PaletteDark.nightBlue, // container (confirm exchange) + backgroundColor: PaletteDark.deepVioletBlue, // button background (confirm exchange) + decorationColor: Palette.darkLavender, // text color (information page) + ), + subtitle: TextStyle( + color: PaletteDark.lightBlueGrey, // QR code (exchange trade page) + backgroundColor: PaletteDark.deepVioletBlue, // divider (exchange trade page) + decorationColor: Colors.white // crete new wallet button background (wallet list page) + ), + headline: TextStyle( + color: PaletteDark.distantBlue, // first gradient color of wallet action buttons (wallet list page) + backgroundColor: PaletteDark.distantNightBlue, // second gradient color of wallet action buttons (wallet list page) + decorationColor: Palette.darkBlueCraiola // restore wallet button text color (wallet list page) + ), + subhead: TextStyle( + color: Colors.white, // titles color (filter widget) + backgroundColor: PaletteDark.darkOceanBlue, // divider color (filter widget) + decorationColor: PaletteDark.wildVioletBlue.withOpacity(0.3) // checkbox background (filter widget) + ), + overline: TextStyle( + color: PaletteDark.wildVioletBlue, // checkbox bounds (filter widget) + decorationColor: PaletteDark.darkCyanBlue, // menu subname + ), + display1: TextStyle( + color: PaletteDark.deepPurpleBlue, // first gradient color (menu header) + decorationColor: PaletteDark.deepPurpleBlue // second gradient color(menu header) + ), + display2: TextStyle( + color: PaletteDark.nightBlue, // action button color (address text field) ), ), - focusColor: PaletteDark.lightDistantBlue, // wallet card border cardColor: PaletteDark.darkNightBlue, cardTheme: CardTheme( color: PaletteDark.moderateBlue, // synced card start @@ -248,27 +332,7 @@ class Themes { - accentTextTheme: TextTheme( - title: TextStyle( - color: PaletteDark.moderateBlue, // top panel - backgroundColor: PaletteDark.lightNightBlue, // bottom panel - decorationColor: Colors.white // select button background color - ), - caption: TextStyle( - color: Colors.white, // current wallet label - backgroundColor: PaletteDark.distantBlue, // gradient start, wallet label - decorationColor: PaletteDark.nightBlue, // gradient end, wallet label - ), - subtitle: TextStyle( - color: PaletteDark.darkNightBlue, // border color, wallet label - backgroundColor: PaletteDark.violetBlue, // address field, wallet card - decorationColor: PaletteDark.headerNightBlue // selected item - ), - headline: TextStyle( - color: PaletteDark.lightNightBlue, // faq background - backgroundColor: PaletteDark.headerNightBlue // faq extension - ) - ), + ); } \ No newline at end of file diff --git a/lib/utils/mobx.dart b/lib/utils/mobx.dart new file mode 100644 index 000000000..0065a939d --- /dev/null +++ b/lib/utils/mobx.dart @@ -0,0 +1,46 @@ +import 'package:mobx/mobx.dart'; + +void connectDifferent<T, Y>( + ObservableList<T> source, ObservableList<Y> dest, Y Function(T) transform, + {bool Function(T) filter}) { + source.observe((ListChange<T> change) { +// switch (change.type) { +// case OperationType.add: +// final _values = change.added; +// Iterable<T> values; + +// if (filter != null) { +// values = _values.where(filter); +// } + +// dest.addAll(values.map((e) => transform(e))); +// break; +// case OperationType.remove: +// change.removed.forEach((element) { +// dest.remove(element); +// }); + +// // dest.removeAt(change.index); +// break; +// case OperationType.update: +// // change.index +// break; +// } + }); +} + +void connect<T>(ObservableList<T> source, ObservableList<T> dest) { + source.observe((ListChange<T> change) { +// switch (change.type) { +// case OperationType.add: +// dest.addAll(change.added); +// break; +// case OperationType.remove: +// dest.removeAt(change.index); +// break; +// case OperationType.update: +// // change.index +// break; +// } + }); +} diff --git a/lib/view_model/contact_list/contact_view_model.dart b/lib/view_model/contact_list/contact_view_model.dart index 5e7ccdf9f..addb56606 100644 --- a/lib/view_model/contact_list/contact_view_model.dart +++ b/lib/view_model/contact_list/contact_view_model.dart @@ -16,7 +16,7 @@ abstract class ContactViewModelBase with Store { _contact = contact { name = _contact?.name; address = _contact?.address; - currency = _contact?.type ?? _wallet.currency; + currency = _contact?.type; //_wallet.currency; } @observable @@ -33,7 +33,8 @@ abstract class ContactViewModelBase with Store { @computed bool get isReady => - (name?.isNotEmpty ?? false) && (address?.isNotEmpty ?? false); + (name?.isNotEmpty ?? false) && (currency?.toString()?.isNotEmpty ?? false) + && (address?.isNotEmpty ?? false); final List<CryptoCurrency> currencies; final ContactService _contactService; @@ -44,7 +45,8 @@ abstract class ContactViewModelBase with Store { void reset() { address = ''; name = ''; - currency = _wallet.currency; + //currency = _wallet.currency; + currency = null; } Future save() async { diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index 62f7a44ef..83a5a7275 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -35,9 +35,11 @@ abstract class BalanceViewModelBase with Store { if (_wallet is BitcoinWallet) { return WalletBalance( - unlockedBalance: _wallet.balance.confirmedFormatted, - totalBalance: _wallet.balance.unconfirmedFormatted); + unlockedBalance: _wallet.balance.totalFormatted, + totalBalance: _wallet.balance.totalFormatted); } + + return null; } String _getFiatBalance({double price, String cryptoAmount}) { diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 65deb5f92..ebf47f252 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -8,6 +8,7 @@ import 'package:cake_wallet/src/domain/common/transaction_info.dart'; import 'package:cake_wallet/src/domain/exchange/exchange_provider_description.dart'; import 'package:cake_wallet/src/domain/exchange/trade.dart'; import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; +import 'package:cake_wallet/view_model/dashboard/filter_item.dart'; import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart'; import 'package:cake_wallet/view_model/dashboard/transaction_list_item.dart'; import 'package:cake_wallet/view_model/dashboard/action_list_item.dart'; @@ -28,23 +29,57 @@ part 'dashboard_view_model.g.dart'; class DashboardViewModel = DashboardViewModelBase with _$DashboardViewModel; abstract class DashboardViewModelBase with Store { - DashboardViewModelBase({ - this.balanceViewModel, - this.appStore, - this.tradesStore, - this.tradeFilterStore, - this.transactionFilterStore}) { + DashboardViewModelBase( + {this.balanceViewModel, + this.appStore, + this.tradesStore, + this.tradeFilterStore, + this.transactionFilterStore}) { + filterItems = { + S.current.transactions: [ + FilterItem( + value: transactionFilterStore.displayIncoming, + caption: S.current.incoming, + onChanged: (value) => transactionFilterStore.toggleIncoming()), + FilterItem( + value: transactionFilterStore.displayOutgoing, + caption: S.current.outgoing, + onChanged: (value) => transactionFilterStore.toggleOutgoing()), + FilterItem( + value: false, + caption: S.current.transactions_by_date, + onChanged: null), + ], + S.current.trades: [ + FilterItem( + value: tradeFilterStore.displayXMRTO, + caption: 'XMR.TO', + onChanged: (value) => tradeFilterStore + .toggleDisplayExchange(ExchangeProviderDescription.xmrto)), + FilterItem( + value: tradeFilterStore.displayChangeNow, + caption: 'Change.NOW', + onChanged: (value) => tradeFilterStore + .toggleDisplayExchange(ExchangeProviderDescription.changeNow)), + FilterItem( + value: tradeFilterStore.displayMorphToken, + caption: 'MorphToken', + onChanged: (value) => tradeFilterStore + .toggleDisplayExchange(ExchangeProviderDescription.morphToken)), + ] + }; name = appStore.wallet?.name; wallet ??= appStore.wallet; type = wallet.type; - transactions = ObservableList.of(wallet.transactionHistory.transactions + transactions = ObservableList.of(wallet + .transactionHistory.transactions.values .map((transaction) => TransactionListItem( - transaction: transaction, - price: price, - fiatCurrency: appStore.settingsStore.fiatCurrency, - displayMode: balanceDisplayMode))); + transaction: transaction, + price: price, + fiatCurrency: appStore.settingsStore.fiatCurrency, + displayMode: balanceDisplayMode))); _reaction = reaction((_) => appStore.wallet, _onWalletChange); @@ -78,15 +113,11 @@ abstract class DashboardViewModelBase with Store { var statusText = ''; if (status is SyncingSyncStatus) { - statusText = S.current - .Blocks_remaining( - status.toString()); + statusText = S.current.Blocks_remaining(status.toString()); } - if (status is FailedSyncStatus) { - statusText = S - .current - .please_try_to_connect_to_another_node; + if (status is FailedSyncStatus || status is LostConnectionSyncStatus) { + statusText = S.current.please_try_to_connect_to_another_node; } return statusText; @@ -106,9 +137,8 @@ abstract class DashboardViewModelBase with Store { List<ActionListItem> get items { final _items = <ActionListItem>[]; - _items - .addAll(transactionFilterStore.filtered(transactions: transactions)); - _items.addAll(tradeFilterStore.filtered(trades: trades)); + _items.addAll(transactionFilterStore.filtered(transactions: transactions)); + _items.addAll(tradeFilterStore.filtered(trades: trades, wallet: wallet)); return formattedItemsList(_items); } @@ -125,16 +155,23 @@ abstract class DashboardViewModelBase with Store { TransactionFilterStore transactionFilterStore; + Map<String, List<FilterItem>> filterItems; + ReactionDisposer _reaction; + Future<void> reconnect() async { + final node = appStore.settingsStore.getCurrentNode(wallet.type); + await wallet.connectToNode(node: node); + } + void _onWalletChange(WalletBase wallet) { name = wallet.name; transactions.clear(); - transactions.addAll(wallet.transactionHistory.transactions - .map((transaction) => TransactionListItem( - transaction: transaction, - price: price, - fiatCurrency: appStore.settingsStore.fiatCurrency, - displayMode: balanceDisplayMode))); + transactions.addAll(wallet.transactionHistory.transactions.values.map( + (transaction) => TransactionListItem( + transaction: transaction, + price: price, + fiatCurrency: appStore.settingsStore.fiatCurrency, + displayMode: balanceDisplayMode))); } } diff --git a/lib/view_model/dashboard/filter_item.dart b/lib/view_model/dashboard/filter_item.dart new file mode 100644 index 000000000..33500b345 --- /dev/null +++ b/lib/view_model/dashboard/filter_item.dart @@ -0,0 +1,7 @@ +class FilterItem { + FilterItem({this.value, this.caption, this.onChanged}); + + bool value; + String caption; + Function(bool) onChanged; +} \ No newline at end of file diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart new file mode 100644 index 000000000..f773cab37 --- /dev/null +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -0,0 +1,93 @@ +import 'dart:async'; +import 'package:cake_wallet/core/wallet_base.dart'; +import 'package:cake_wallet/src/domain/exchange/changenow/changenow_exchange_provider.dart'; +import 'package:cake_wallet/src/domain/exchange/exchange_provider.dart'; +import 'package:cake_wallet/src/domain/exchange/exchange_provider_description.dart'; +import 'package:cake_wallet/src/domain/exchange/morphtoken/morphtoken_exchange_provider.dart'; +import 'package:cake_wallet/src/domain/exchange/trade.dart'; +import 'package:cake_wallet/src/domain/exchange/xmrto/xmrto_exchange_provider.dart'; +import 'package:cake_wallet/store/dashboard/trades_store.dart'; +import 'package:hive/hive.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_item.dart'; +import 'package:cake_wallet/generated/i18n.dart'; + +part 'exchange_trade_view_model.g.dart'; + +class ExchangeTradeViewModel = ExchangeTradeViewModelBase with _$ExchangeTradeViewModel; + +abstract class ExchangeTradeViewModelBase with Store { + ExchangeTradeViewModelBase({this.wallet, this.trades, this.tradesStore}) { + trade = tradesStore.trade; + + isSendable = trade.from == wallet.currency || + trade.provider == ExchangeProviderDescription.xmrto; + + switch (trade.provider) { + case ExchangeProviderDescription.xmrto: + _provider = XMRTOExchangeProvider(); + break; + case ExchangeProviderDescription.changeNow: + _provider = ChangeNowExchangeProvider(); + break; + case ExchangeProviderDescription.morphToken: + _provider = MorphTokenExchangeProvider(trades: trades); + break; + } + + items = ObservableList<ExchangeTradeItem>(); + items.addAll([ + ExchangeTradeItem( + title: S.current.id, + data: '${trade.id}', + isCopied: true), + ExchangeTradeItem( + title: S.current.amount, + data: '${trade.amount}', + isCopied: false), + ExchangeTradeItem( + title: S.current.status, + data: '${trade.state}', + isCopied: false), + ExchangeTradeItem( + title: S.current.widgets_address + ':', + data: trade.inputAddress, + isCopied: true), + ]); + + _updateTrade(); + _timer = Timer.periodic(Duration(seconds: 20), (_) async => _updateTrade()); + } + + final WalletBase wallet; + final Box<Trade> trades; + final TradesStore tradesStore; + + @observable + Trade trade; + + @observable + bool isSendable; + + @observable + ObservableList<ExchangeTradeItem> items; + + ExchangeProvider _provider; + + Timer _timer; + + @action + Future<void> _updateTrade() async { + try { + final updatedTrade = await _provider.findTradeById(id: trade.id); + + if (updatedTrade.createdAt == null && trade.createdAt != null) { + updatedTrade.createdAt = trade.createdAt; + } + + trade = updatedTrade; + } catch (e) { + print(e.toString()); + } + } +} \ No newline at end of file diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 4fb5a1dbf..7ab3a1044 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -1,9 +1,11 @@ import 'package:cake_wallet/core/wallet_base.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/exchange/exchange_provider.dart'; import 'package:cake_wallet/src/domain/exchange/limits.dart'; import 'package:cake_wallet/src/domain/exchange/trade.dart'; import 'package:cake_wallet/src/stores/exchange/limits_state.dart'; +import 'package:cake_wallet/store/dashboard/trades_store.dart'; import 'package:intl/intl.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/generated/i18n.dart'; @@ -24,17 +26,18 @@ part 'exchange_view_model.g.dart'; class ExchangeViewModel = ExchangeViewModelBase with _$ExchangeViewModel; abstract class ExchangeViewModelBase with Store { - ExchangeViewModelBase({this.wallet, this.trades, this.exchangeTemplateStore}) { + ExchangeViewModelBase( + {this.wallet, + this.trades, + this.exchangeTemplateStore, + this.tradesStore}) { providerList = [ XMRTOExchangeProvider(), ChangeNowExchangeProvider(), MorphTokenExchangeProvider(trades: trades) ]; - provider = providerList[ 0 ]; - - depositCurrency = CryptoCurrency.xmr; - receiveCurrency = CryptoCurrency.btc; + _initialPairBasedOnWallet(); isDepositAddressEnabled = !(depositCurrency == wallet.currency); isReceiveAddressEnabled = !(receiveCurrency == wallet.currency); depositAmount = ''; @@ -44,12 +47,14 @@ abstract class ExchangeViewModelBase with Store { limitsState = LimitsInitialState(); tradeState = ExchangeTradeStateInitial(); _cryptoNumberFormat = NumberFormat()..maximumFractionDigits = 12; + provider = providersForCurrentPair().first; loadLimits(); } final WalletBase wallet; final Box<Trade> trades; final ExchangeTemplateStore exchangeTemplateStore; + final TradesStore tradesStore; @observable ExchangeProvider provider; @@ -133,8 +138,11 @@ abstract class ExchangeViewModelBase with Store { provider .calculateAmount( - from: depositCurrency, to: receiveCurrency, amount: _amount) - .then((amount) => _cryptoNumberFormat.format(amount).toString().replaceAll(RegExp("\\,"), "")) + from: depositCurrency, to: receiveCurrency, amount: _amount) + .then((amount) => _cryptoNumberFormat + .format(amount) + .toString() + .replaceAll(RegExp('\\,'), '')) .then((amount) => depositAmount = amount); } @@ -151,8 +159,11 @@ abstract class ExchangeViewModelBase with Store { final _amount = double.parse(amount); provider .calculateAmount( - from: depositCurrency, to: receiveCurrency, amount: _amount) - .then((amount) => _cryptoNumberFormat.format(amount).toString().replaceAll(RegExp("\\,"), "")) + from: depositCurrency, to: receiveCurrency, amount: _amount) + .then((amount) => _cryptoNumberFormat + .format(amount) + .toString() + .replaceAll(RegExp('\\,'), '')) .then((amount) => receiveAmount = amount); } @@ -210,16 +221,19 @@ abstract class ExchangeViewModelBase with Store { if (limitsState is LimitsLoadedSuccessfully && amount != null) { if (double.parse(amount) < limits.min) { - tradeState = TradeIsCreatedFailure(error: S.current.error_text_minimal_limit('${provider.description}', - '${limits.min}', currency.toString())); + tradeState = TradeIsCreatedFailure( + error: S.current.error_text_minimal_limit('${provider.description}', + '${limits.min}', currency.toString())); } else if (limits.max != null && double.parse(amount) > limits.max) { - tradeState = TradeIsCreatedFailure(error: S.current.error_text_maximum_limit('${provider.description}', - '${limits.max}', currency.toString())); + tradeState = TradeIsCreatedFailure( + error: S.current.error_text_maximum_limit('${provider.description}', + '${limits.max}', currency.toString())); } else { try { tradeState = TradeIsCreating(); final trade = await provider.createTrade(request: request); trade.walletId = wallet.id; + tradesStore.setTrade(trade); await trades.add(trade); tradeState = TradeIsCreatedSuccessfully(trade: trade); } catch (e) { @@ -227,9 +241,10 @@ abstract class ExchangeViewModelBase with Store { } } } else { - tradeState = TradeIsCreatedFailure(error: S.current.error_text_limits_loading_failed("${provider.description}")); + tradeState = TradeIsCreatedFailure( + error: S.current + .error_text_limits_loading_failed('${provider.description}')); } - } @action @@ -253,9 +268,9 @@ abstract class ExchangeViewModelBase with Store { {CryptoCurrency from, CryptoCurrency to}) { final providers = providerList .where((provider) => provider.pairList - .where((pair) => - pair.from == depositCurrency && pair.to == receiveCurrency) - .isNotEmpty) + .where((pair) => + pair.from == depositCurrency && pair.to == receiveCurrency) + .isNotEmpty) .toList(); return providers; @@ -264,12 +279,12 @@ abstract class ExchangeViewModelBase with Store { void _onPairChange() { final isPairExist = provider.pairList .where((pair) => - pair.from == depositCurrency && pair.to == receiveCurrency) + pair.from == depositCurrency && pair.to == receiveCurrency) .isNotEmpty; if (!isPairExist) { final provider = - _providerForPair(from: depositCurrency, to: receiveCurrency); + _providerForPair(from: depositCurrency, to: receiveCurrency); if (provider != null) { changeProvider(provider: provider); @@ -287,4 +302,18 @@ abstract class ExchangeViewModelBase with Store { return providers.isNotEmpty ? providers[0] : null; } -} \ No newline at end of file + void _initialPairBasedOnWallet() { + switch (wallet.type) { + case WalletType.monero: + depositCurrency = CryptoCurrency.xmr; + receiveCurrency = CryptoCurrency.btc; + break; + case WalletType.bitcoin: + depositCurrency = CryptoCurrency.btc; + receiveCurrency = CryptoCurrency.xmr; + break; + default: + break; + } + } +} diff --git a/lib/view_model/node_list/node_list_view_model.dart b/lib/view_model/node_list/node_list_view_model.dart index 6821ae574..73f2b934e 100644 --- a/lib/view_model/node_list/node_list_view_model.dart +++ b/lib/view_model/node_list/node_list_view_model.dart @@ -1,26 +1,92 @@ +import 'package:flutter/foundation.dart'; 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'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/src/domain/common/default_settings_migration.dart'; +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; +import 'package:cake_wallet/utils/mobx.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); +class ItemCell<Item> { + ItemCell(this.value, {@required this.isSelected}); - @computed - ObservableList<Node> get nodes => ObservableList<Node>.of( - _nodeListStore.nodes.where((node) => node.type == _wallet.type)); + final Item value; + final bool isSelected; +} + +abstract class NodeListViewModelBase with Store { + NodeListViewModelBase( + this._nodeListStore, this._nodeSource, this._wallet, this._settingsStore) + : nodes = ObservableList<ItemCell<Node>>() { + final currentNode = _settingsStore.getCurrentNode(_wallet.type); + final values = _nodeListStore.nodes; + nodes.clear(); + nodes.addAll(values.where((Node node) => node.type == _wallet.type).map( + (Node val) => + ItemCell<Node>(val, isSelected: val.key == currentNode.key))); + connectDifferent( + _nodeListStore.nodes, + nodes, + (Node val) => + ItemCell<Node>(val, isSelected: val.key == currentNode.key), + filter: (Node val) { + return val.type == _wallet.type; + }); + } + + ObservableList<ItemCell<Node>> nodes; final WalletBase _wallet; final Box<Node> _nodeSource; final NodeListStore _nodeListStore; + final SettingsStore _settingsStore; - Future<void> reset() async => await resetToDefault(_nodeSource); + Future<void> reset() async { + await resetToDefault(_nodeSource); - Future<void> delete(Node node) async => node.delete(); + Node node; + + switch (_wallet.type) { + case WalletType.bitcoin: + node = getBitcoinDefaultElectrumServer(nodes: _nodeSource); + break; + case WalletType.monero: + node = getMoneroDefaultNode( + nodes: _nodeSource, + ); + break; + default: + break; + } + + await _wallet.connectToNode(node: node); + } + + Future<void> delete(Node node) async => _nodeSource.delete(node.key); + + Future<void> setAsCurrent(Node node) async { + await _wallet.connectToNode(node: node); + await _settingsStore.setCurrentNode(node, _wallet.type); + _updateCurrentNode(); + } + + void _updateCurrentNode() { + final currentNode = _settingsStore.getCurrentNode(_wallet.type); + + for (var i = 0; i < nodes.length; i++) { + final item = nodes[i]; + final isSelected = item.value.key == currentNode.key; + + if (item.isSelected != isSelected) { + nodes[i] = ItemCell<Node>(item.value, isSelected: isSelected); + } + } + } } diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart new file mode 100644 index 000000000..b03ca02d1 --- /dev/null +++ b/lib/view_model/send/send_view_model.dart @@ -0,0 +1,182 @@ +import 'package:intl/intl.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/core/template_validator.dart'; +import 'package:cake_wallet/src/domain/common/calculate_fiat_amount.dart'; +import 'package:cake_wallet/store/dashboard/fiat_convertation_store.dart'; +import 'package:cake_wallet/core/address_validator.dart'; +import 'package:cake_wallet/core/amount_validator.dart'; +import 'package:cake_wallet/core/pending_transaction.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/sync_status.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:cake_wallet/view_model/send/send_view_model_state.dart'; +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_transaction_credentials.dart'; + +part 'send_view_model.g.dart'; + +class SendViewModel = SendViewModelBase with _$SendViewModel; + +abstract class SendViewModelBase with Store { + SendViewModelBase( + this._wallet, this._settingsStore, this._fiatConversationStore) + : state = InitialSendViewModelState(), + _cryptoNumberFormat = NumberFormat()..maximumFractionDigits = 12, + // FIXME: need to be based on wallet type. + sendAll = false; + + @observable + SendViewModelState state; + + @observable + String fiatAmount; + + @observable + String cryptoAmount; + + @observable + String address; + + @observable + bool sendAll; + + FiatCurrency get fiat => _settingsStore.fiatCurrency; + + TransactionPriority get transactionPriority => + _settingsStore.transactionPriority; + + double get estimatedFee => + _wallet.calculateEstimatedFee(_settingsStore.transactionPriority); + + CryptoCurrency get currency => _wallet.currency; + + Validator get amountValidator => AmountValidator(type: _wallet.type); + + Validator get addressValidator => AddressValidator(type: _wallet.currency); + + Validator get templateValidator => TemplateValidator(); + + PendingTransaction pendingTransaction; + + @computed + String get balance { + if (_wallet is MoneroWallet) { + _wallet.balance.formattedUnlockedBalance; + } + + if (_wallet is BitcoinWallet) { + _wallet.balance.confirmedFormatted; + } + + return '0.0'; + } + + @computed + bool get isReadyForSend => _wallet.syncStatus is SyncedSyncStatus; + + final WalletBase _wallet; + final SettingsStore _settingsStore; + final FiatConvertationStore _fiatConversationStore; + final NumberFormat _cryptoNumberFormat; + + @action + void setSendAll() => sendAll = true; + + @action + void reset() { + cryptoAmount = ''; + fiatAmount = ''; + address = ''; + } + + @action + Future<void> createTransaction() async { + try { + state = TransactionIsCreating(); + pendingTransaction = await _wallet.createTransaction(_credentials()); + state = TransactionCreatedSuccessfully(); + } catch (e) { + state = SendingFailed(error: e.toString()); + } + } + + @action + Future<void> commitTransaction() async { + try { + state = TransactionCommitting(); + await pendingTransaction.commit(); + state = TransactionCommitted(); + } catch (e) { + state = SendingFailed(error: e.toString()); + } + } + + @action + void setCryptoAmount(String amount) { + // FIXME: hardcoded value. + if (amount.toUpperCase() != 'ALL') { + sendAll = false; + } + + cryptoAmount = amount; + _updateFiatAmount(); + } + + @action + void setFiatAmount(String amount) { + fiatAmount = amount; + _updateCryptoAmount(); + } + + @action + void _updateFiatAmount() { + try { + final fiat = calculateFiatAmount( + price: _fiatConversationStore.price, + cryptoAmount: cryptoAmount.replaceAll(',', '.')); + if (fiatAmount != fiat) { + fiatAmount = fiat; + } + } catch (_) { + fiatAmount = ''; + } + } + + @action + void _updateCryptoAmount() { + try { + final crypto = double.parse(fiatAmount.replaceAll(',', '.')) / + _fiatConversationStore.price; + final cryptoAmountTmp = _cryptoNumberFormat.format(crypto); + + if (cryptoAmount != cryptoAmountTmp) { + cryptoAmount = cryptoAmountTmp; + } + } catch (e) { + cryptoAmount = ''; + } + } + + Object _credentials() { + final amount = + !sendAll ? double.parse(cryptoAmount.replaceAll(',', '.')) : null; + + switch (_wallet.type) { + case WalletType.bitcoin: + return BitcoinTransactionCredentials( + address, amount, _settingsStore.transactionPriority); + case WalletType.monero: + // FIXME: Wrong credentials + return BitcoinTransactionCredentials( + address, amount, _settingsStore.transactionPriority); + default: + return null; + } + } +} diff --git a/lib/view_model/send/send_view_model_state.dart b/lib/view_model/send/send_view_model_state.dart new file mode 100644 index 000000000..b43e95378 --- /dev/null +++ b/lib/view_model/send/send_view_model_state.dart @@ -0,0 +1,18 @@ +import 'package:flutter/foundation.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; +} \ No newline at end of file diff --git a/lib/view_model/send_view_model.dart b/lib/view_model/send_view_model.dart index f5018a2b0..0942c7841 100644 --- a/lib/view_model/send_view_model.dart +++ b/lib/view_model/send_view_model.dart @@ -193,6 +193,11 @@ abstract class SendViewModelBase with Store { fiatAmount = ''; } + @action + void setTransactionPriority(TransactionPriority transactionPriority) { + _settingsStore.transactionPriority = transactionPriority; + } + final WalletBase _wallet; final SettingsStore _settingsStore; diff --git a/lib/view_model/settings/picker_list_item.dart b/lib/view_model/settings/picker_list_item.dart index 532f90a0d..1dfc9c06d 100644 --- a/lib/view_model/settings/picker_list_item.dart +++ b/lib/view_model/settings/picker_list_item.dart @@ -4,10 +4,19 @@ import 'package:cake_wallet/view_model/settings/settings_list_item.dart'; class PickerListItem<ItemType> extends SettingsListItem { PickerListItem( {@required String title, - @required this.selectedItem, - @required this.items}) - : super(title); + @required this.selectedItem, + @required this.items, + void Function(ItemType item) onItemSelected}) + : _onItemSelected = onItemSelected, + super(title); final ItemType Function() selectedItem; final List<ItemType> items; + final void Function(ItemType item) _onItemSelected; + + void onItemSelected(dynamic item) { + if (item is ItemType) { + _onItemSelected?.call(item); + } + } } diff --git a/lib/view_model/settings/settings_view_model.dart b/lib/view_model/settings/settings_view_model.dart index 892b8493e..308026c9f 100644 --- a/lib/view_model/settings/settings_view_model.dart +++ b/lib/view_model/settings/settings_view_model.dart @@ -1,3 +1,8 @@ +import 'package:cake_wallet/core/wallet_base.dart'; +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/store/theme_changer_store.dart'; +import 'package:cake_wallet/themes.dart'; import 'package:flutter/cupertino.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/routes.dart'; @@ -20,7 +25,9 @@ part 'settings_view_model.g.dart'; class SettingsViewModel = SettingsViewModelBase with _$SettingsViewModel; abstract class SettingsViewModelBase with Store { - SettingsViewModelBase(this._settingsStore) : itemHeaders = {} { + SettingsViewModelBase(this._settingsStore, WalletBase wallet) + : itemHeaders = {}, + _walletType = wallet.type { sections = [ [ PickerListItem( @@ -33,12 +40,14 @@ abstract class SettingsViewModelBase with Store { selectedItem: () => fiatCurrency), PickerListItem( title: S.current.settings_fee_priority, - items: TransactionPriority.all, - selectedItem: () => transactionPriority), + items: _transactionPriorities(wallet.type), + selectedItem: () => transactionPriority, + onItemSelected: (TransactionPriority priority) => + _settingsStore.transactionPriority = priority), SwitcherListItem( title: S.current.settings_save_recipient_address, value: () => shouldSaveRecipientAddress, - onValueChange: (bool value) => shouldSaveRecipientAddress = value) + onValueChange: (bool value) => setShouldSaveRecipientAddress(value)) ], [ RegularListItem( @@ -64,12 +73,16 @@ abstract class SettingsViewModelBase with Store { title: S.current.settings_allow_biometrical_authentication, value: () => allowBiometricalAuthentication, onValueChange: (bool value) => - allowBiometricalAuthentication = value), + setAllowBiometricalAuthentication(value)), SwitcherListItem( title: S.current.settings_dark_mode, value: () => _settingsStore.isDarkTheme, onValueChange: (bool value) { - // FIXME: Implement me + _settingsStore.isDarkTheme = value; + getIt + .get<ThemeChangerStore>() + .themeChanger + .setTheme(value ? Themes.darkTheme : Themes.lightTheme); }) ], [ @@ -112,7 +125,7 @@ abstract class SettingsViewModelBase with Store { } @computed - Node get node => _settingsStore.node; + Node get node => _settingsStore.getCurrentNode(_walletType); @computed FiatCurrency get fiatCurrency => _settingsStore.fiatCurrency; @@ -133,28 +146,22 @@ abstract class SettingsViewModelBase with Store { 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<String, String> itemHeaders; - List<List<SettingsListItem>> sections; final SettingsStore _settingsStore; + final WalletType _walletType; + List<List<SettingsListItem>> sections; + + @action + void setShouldSaveRecipientAddress(bool value) => + _settingsStore.shouldSaveRecipientAddress = value; + + @action + void setAllowBiometricalAuthentication(bool value) => + _settingsStore.allowBiometricalAuthentication = value; @action void toggleTransactionsDisplay() => @@ -182,4 +189,24 @@ abstract class SettingsViewModelBase with Store { @action void _showTrades() => actionlistDisplayMode.add(ActionListDisplayMode.trades); + +// +// @observable +// int defaultPinLength; +// bool isDarkTheme; + + static List<TransactionPriority> _transactionPriorities(WalletType type) { + switch (type) { + case WalletType.monero: + return TransactionPriority.all; + case WalletType.bitcoin: + return [ + TransactionPriority.slow, + TransactionPriority.regular, + TransactionPriority.fast + ]; + default: + return []; + } + } } diff --git a/lib/view_model/wallet_list/wallet_list_item.dart b/lib/view_model/wallet_list/wallet_list_item.dart index 669a2be79..5e2210c7a 100644 --- a/lib/view_model/wallet_list/wallet_list_item.dart +++ b/lib/view_model/wallet_list/wallet_list_item.dart @@ -3,9 +3,10 @@ import 'package:cake_wallet/src/domain/common/wallet_type.dart'; class WalletListItem { const WalletListItem( - {@required this.name, @required this.type, this.isCurrent = false}); + {@required this.name, @required this.type, @required this.key, this.isCurrent = false}); final String name; final WalletType type; final bool isCurrent; + final dynamic key; } diff --git a/lib/view_model/wallet_list/wallet_list_view_model.dart b/lib/view_model/wallet_list/wallet_list_view_model.dart index e84959e91..15683d1f1 100644 --- a/lib/view_model/wallet_list/wallet_list_view_model.dart +++ b/lib/view_model/wallet_list/wallet_list_view_model.dart @@ -17,11 +17,7 @@ abstract class WalletListViewModelBase with Store { WalletListViewModelBase( this._walletInfoSource, this._appStore, this._keyService) { wallets = ObservableList<WalletListItem>(); - wallets.addAll(_walletInfoSource.values.map((info) => WalletListItem( - name: info.name, - type: info.type, - isCurrent: info.name == _appStore.wallet.name && - info.type == _appStore.wallet.type))); + _updateList(); } @observable @@ -40,7 +36,12 @@ abstract class WalletListViewModelBase with Store { } @action - Future<void> remove(WalletListItem wallet) async {} + Future<void> remove(WalletListItem wallet) async { + final walletService = _getWalletService(wallet.type); + await walletService.remove(wallet.name); + await _walletInfoSource.delete(wallet.key); + _updateList(); + } WalletService _getWalletService(WalletType type) { switch (type) { @@ -52,4 +53,14 @@ abstract class WalletListViewModelBase with Store { return null; } } + + void _updateList() { + wallets.clear(); + wallets.addAll(_walletInfoSource.values.map((info) => WalletListItem( + name: info.name, + type: info.type, + key: info.key, + isCurrent: info.name == _appStore.wallet.name && + info.type == _appStore.wallet.type))); + } } diff --git a/pubspec.lock b/pubspec.lock index 344397bf6..ab4e1d14e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,13 +1,20 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: - analyzer: + _fe_analyzer_shared: dependency: transitive + description: + name: _fe_analyzer_shared + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.0" + analyzer: + dependency: "direct overridden" description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "0.36.4" + version: "0.39.14" archive: dependency: transitive description: @@ -28,14 +35,14 @@ packages: name: asn1lib url: "https://pub.dartlang.org" source: hosted - version: "0.6.4" + version: "0.6.5" async: dependency: transitive description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.4.1" + version: "2.4.2" auto_size_text: dependency: "direct main" description: @@ -105,7 +112,7 @@ packages: name: build url: "https://pub.dartlang.org" source: hosted - version: "1.2.2" + version: "1.3.0" build_config: dependency: transitive description: @@ -121,26 +128,26 @@ packages: source: hosted version: "2.1.4" build_resolvers: - dependency: transitive + dependency: "direct dev" description: name: build_resolvers url: "https://pub.dartlang.org" source: hosted - version: "1.2.1" + version: "1.3.11" build_runner: dependency: "direct dev" description: name: build_runner url: "https://pub.dartlang.org" source: hosted - version: "1.9.0" + version: "1.10.1" build_runner_core: dependency: transitive description: name: build_runner_core url: "https://pub.dartlang.org" source: hosted - version: "5.1.0" + version: "6.0.1" built_collection: dependency: transitive description: @@ -161,7 +168,7 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "0.3.1" + version: "1.0.0" charcode: dependency: transitive description: @@ -176,6 +183,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.2" + cli_util: + dependency: transitive + description: + name: cli_util + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" clock: dependency: transitive description: @@ -189,14 +203,14 @@ packages: name: code_builder url: "https://pub.dartlang.org" source: hosted - version: "3.3.0" + version: "3.4.1" collection: dependency: transitive description: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.14.12" + version: "1.14.13" convert: dependency: transitive description: @@ -210,14 +224,14 @@ packages: name: crypto url: "https://pub.dartlang.org" source: hosted - version: "2.1.4" + version: "2.1.5" csslib: dependency: transitive description: name: csslib url: "https://pub.dartlang.org" source: hosted - version: "0.16.1" + version: "0.16.2" cupertino_icons: dependency: "direct main" description: @@ -245,7 +259,7 @@ packages: name: dartx url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" + version: "0.5.0" date_range_picker: dependency: "direct main" description: @@ -273,7 +287,7 @@ packages: name: dotted_border url: "https://pub.dartlang.org" source: hosted - version: "1.0.5" + version: "1.0.6" encrypt: dependency: "direct main" description: @@ -288,6 +302,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.2" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" ffi: dependency: transitive description: @@ -301,7 +322,7 @@ packages: name: file url: "https://pub.dartlang.org" source: hosted - version: "5.2.0" + version: "5.2.1" fixnum: dependency: transitive description: @@ -339,7 +360,7 @@ packages: name: flutter_mobx url: "https://pub.dartlang.org" source: hosted - version: "0.3.0+1" + version: "1.1.0+2" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -360,7 +381,7 @@ packages: name: flutter_slidable url: "https://pub.dartlang.org" source: hosted - version: "0.5.4" + version: "0.5.7" flutter_test: dependency: "direct dev" description: flutter @@ -371,20 +392,13 @@ packages: description: flutter source: sdk version: "0.0.0" - front_end: - dependency: transitive - description: - name: front_end - url: "https://pub.dartlang.org" - source: hosted - version: "0.1.19" get_it: dependency: "direct main" description: name: get_it url: "https://pub.dartlang.org" source: hosted - version: "4.0.2" + version: "4.0.4" glob: dependency: transitive description: @@ -412,21 +426,21 @@ packages: name: hive url: "https://pub.dartlang.org" source: hosted - version: "1.4.1+1" + version: "1.4.4" hive_flutter: dependency: "direct main" description: name: hive_flutter url: "https://pub.dartlang.org" source: hosted - version: "0.3.0+2" + version: "0.3.1" hive_generator: dependency: "direct dev" description: name: hive_generator url: "https://pub.dartlang.org" source: hosted - version: "0.7.0+2" + version: "0.7.1" html: dependency: transitive description: @@ -440,7 +454,7 @@ packages: name: http url: "https://pub.dartlang.org" source: hosted - version: "0.12.1" + version: "0.12.2" http_multi_server: dependency: transitive description: @@ -461,7 +475,7 @@ packages: name: image url: "https://pub.dartlang.org" source: hosted - version: "2.1.12" + version: "2.1.14" intl: dependency: "direct main" description: @@ -490,20 +504,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.1" - kernel: - dependency: transitive - description: - name: kernel - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.19" local_auth: dependency: "direct main" description: name: local_auth url: "https://pub.dartlang.org" source: hosted - version: "0.6.2+3" + version: "0.6.3+1" logging: dependency: transitive description: @@ -517,7 +524,7 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.6" + version: "0.12.8" meta: dependency: transitive description: @@ -531,21 +538,21 @@ packages: name: mime url: "https://pub.dartlang.org" source: hosted - version: "0.9.6+3" + version: "0.9.7" mobx: dependency: "direct main" description: name: mobx url: "https://pub.dartlang.org" source: hosted - version: "0.3.10" + version: "1.2.1+2" mobx_codegen: dependency: "direct dev" description: name: mobx_codegen url: "https://pub.dartlang.org" source: hosted - version: "0.3.3+1" + version: "1.1.0+1" node_interop: dependency: transitive description: @@ -573,14 +580,7 @@ packages: name: package_info url: "https://pub.dartlang.org" source: hosted - version: "0.4.1" - package_resolver: - dependency: transitive - description: - name: package_resolver - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.10" + version: "0.4.3" password: dependency: "direct main" description: @@ -594,14 +594,14 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.6.4" + version: "1.7.0" path_drawing: dependency: transitive description: name: path_drawing url: "https://pub.dartlang.org" source: hosted - version: "0.4.1" + version: "0.4.1+1" path_parsing: dependency: transitive description: @@ -615,14 +615,14 @@ packages: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "1.6.11" + version: "1.6.14" path_provider_linux: dependency: transitive description: name: path_provider_linux url: "https://pub.dartlang.org" source: hosted - version: "0.0.1+1" + version: "0.0.1+2" path_provider_macos: dependency: transitive description: @@ -636,7 +636,7 @@ packages: name: path_provider_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.0.3" pedantic: dependency: "direct dev" description: @@ -650,7 +650,7 @@ packages: name: petitparser url: "https://pub.dartlang.org" source: hosted - version: "2.4.0" + version: "3.0.4" platform: dependency: transitive description: @@ -748,14 +748,21 @@ packages: name: share url: "https://pub.dartlang.org" source: hosted - version: "0.6.4+3" + version: "0.6.5" shared_preferences: dependency: "direct main" description: name: shared_preferences url: "https://pub.dartlang.org" source: hosted - version: "0.5.7+3" + version: "0.5.10" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.2+2" shared_preferences_macos: dependency: transitive description: @@ -783,7 +790,7 @@ packages: name: shelf url: "https://pub.dartlang.org" source: hosted - version: "0.7.7" + version: "0.7.9" shelf_web_socket: dependency: transitive description: @@ -823,7 +830,7 @@ packages: name: stack_trace url: "https://pub.dartlang.org" source: hosted - version: "1.9.3" + version: "1.9.5" stream_channel: dependency: transitive description: @@ -858,7 +865,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.15" + version: "0.2.17" time: dependency: transitive description: @@ -879,14 +886,21 @@ packages: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.1.6" + version: "1.2.0" url_launcher: dependency: "direct main" description: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "5.4.11" + version: "5.5.1" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.1+1" url_launcher_macos: dependency: transitive description: @@ -900,14 +914,14 @@ packages: name: url_launcher_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.7" + version: "1.0.8" url_launcher_web: dependency: transitive description: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "0.1.1+6" + version: "0.1.3" uuid: dependency: "direct main" description: @@ -949,7 +963,7 @@ packages: name: xml url: "https://pub.dartlang.org" source: hosted - version: "3.6.1" + version: "4.2.0" yaml: dependency: "direct main" description: @@ -958,5 +972,5 @@ packages: source: hosted version: "2.2.1" sdks: - dart: ">=2.7.0 <3.0.0" + dart: ">=2.9.0-14.0.dev <3.0.0" flutter: ">=1.12.13+hotfix.5 <2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index d99925e2e..d85e0fec0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,8 +34,8 @@ dependencies: barcode_scan: any http: ^0.12.0+2 path_provider: ^1.3.0 - mobx: ^0.3.7 - flutter_mobx: 0.3.0+1 + mobx: ^1.2.1+2 + flutter_mobx: ^1.1.0+2 flutter_slidable: ^0.5.3 share: ^0.6.2+1 esys_flutter_share: ^1.0.2 @@ -43,8 +43,8 @@ dependencies: dio: 3.0.7 cw_monero: path: ./cw_monero - hive: ^1.4.1+1 - hive_flutter: ^0.3.0+2 + hive: ^1.4.2 + hive_flutter: ^0.3.1 local_auth: ^0.6.1 package_info: ^0.4.0+13 devicelocale: ^0.2.1 @@ -64,15 +64,17 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - build_runner: ^1.3.1 - mobx_codegen: 0.3.3+1 + build_runner: 1.10.1 + build_resolvers: ^1.3.10 + mobx_codegen: ^1.1.0+1 hive_generator: ^0.7.0+2 flutter_launcher_icons: ^0.7.4 pedantic: ^1.8.0 # Fix for hive https://github.com/hivedb/hive/issues/247#issuecomment-606838497 dependency_overrides: - dartx: ^0.3.0 + dartx: ^0.5.0 + analyzer: 0.39.14 flutter_icons: image_path: "assets/images/app_logo.png" diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index cc97e8a72..68095ca01 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -88,8 +88,8 @@ "offer_expires_in" : "Angebot läuft ab in: ", "trade_is_powered_by" : "Dieser Handel wird betrieben von ${provider}", "copy_address" : "Adresse kopieren", - "exchange_result_confirm" : "Durch Drücken von Bestätigen wird gesendet ${fetchingLabel} ${from} von Ihrer Brieftasche aus angerufen ${walletName} an die oben angegebene Adresse. Oder Sie können von Ihrem externen Portemonnaie an die oben angegebene Adresse / QR-Code senden.\n\nBitte bestätigen Sie, um fortzufahren, oder gehen Sie zurück, um die Beträge zu änderns.\n\n", - "exchange_result_description" : "Bitte senden ${fetchingLabel} ${from} an die oben angegebene Adresse.\n\n'", + "exchange_result_confirm" : "Durch Drücken von Bestätigen wird gesendet ${fetchingLabel} ${from} von Ihrer Brieftasche aus angerufen ${walletName} an die oben angegebene Adresse. Oder Sie können von Ihrem externen Portemonnaie an die oben angegebene Adresse / QR-Code senden.\n\nBitte bestätigen Sie, um fortzufahren, oder gehen Sie zurück, um die Beträge zu änderns.", + "exchange_result_description" : "Bitte senden ${fetchingLabel} ${from} an die oben angegebene Adresse.'", "exchange_result_write_down_ID" : "*Bitte kopieren oder notieren Sie Ihren oben gezeigten Ausweis.", "confirm" : "Bestätigen", "confirm_sending" : "Bestätigen Sie das Senden", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 362e493bf..277ce28ba 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -43,7 +43,7 @@ "outgoing" : "Outgoing", "transactions_by_date" : "Transactions by date", "trades" : "Trades", - "filters" : "Filters", + "filters" : "Filter", "today" : "Today", "yesterday" : "Yesterday", "received" : "Received", @@ -88,8 +88,8 @@ "offer_expires_in" : "Offer expires in: ", "trade_is_powered_by" : "This trade is powered by ${provider}", "copy_address" : "Copy Address", - "exchange_result_confirm" : "By pressing confirm, you will be sending ${fetchingLabel} ${from} from your wallet called ${walletName} to the address shown above. Or you can send from your external wallet to the above address/QR code.\n\nPlease press confirm to continue or go back to change the amounts.\n\n", - "exchange_result_description" : "Please send ${fetchingLabel} ${from} to the address shown above.\n\n", + "exchange_result_confirm" : "By pressing confirm, you will be sending ${fetchingLabel} ${from} from your wallet called ${walletName} to the address shown above. Or you can send from your external wallet to the above address/QR code.\n\nPlease press confirm to continue or go back to change the amounts.", + "exchange_result_description" : "Please send ${fetchingLabel} ${from} to the address shown above.", "exchange_result_write_down_ID" : "*Please copy or write down your ID shown above.", "confirm" : "Confirm", "confirm_sending" : "Confirm sending", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index cfaeb7a42..f6cc257f6 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -43,7 +43,7 @@ "outgoing" : "Saliente", "transactions_by_date" : "Transacciones por fecha", "trades" : "Cambios", - "filters" : "Filtros", + "filters" : "Filtrar", "today" : "Hoy", "yesterday" : "Ayer", "received" : "Recibido", @@ -88,8 +88,8 @@ "offer_expires_in" : "Oferta expira en: ", "trade_is_powered_by" : "Este comercio es impulsado por ${provider}", "copy_address" : "Copiar dirección ", - "exchange_result_confirm" : "Al presionar confirmar, enviará ${fetchingLabel} ${from} desde su billetera llamada ${walletName} a la dirección que se muestra arriba. O puede enviar desde su billetera externa a la dirección / código QR anterior.\n\nPresione confirmar para continuar o regrese para cambiar los montos.\n\n", - "exchange_result_description" : "Envíe ${fetchingLabel} ${from} a la dirección que se muestra arriba.\n\n'", + "exchange_result_confirm" : "Al presionar confirmar, enviará ${fetchingLabel} ${from} desde su billetera llamada ${walletName} a la dirección que se muestra arriba. O puede enviar desde su billetera externa a la dirección / código QR anterior.\n\nPresione confirmar para continuar o regrese para cambiar los montos.", + "exchange_result_description" : "Envíe ${fetchingLabel} ${from} a la dirección que se muestra arriba.", "exchange_result_write_down_ID" : "*Copie o escriba su identificación que se muestra arriba.", "confirm" : "Confirmar", "confirm_sending" : "Confirmar envío", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 7e43ebbc9..9e0e1a032 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -43,7 +43,7 @@ "outgoing" : "निवर्तमान", "transactions_by_date" : "तारीख से लेन-देन", "trades" : "ट्रेडों", - "filters" : "फिल्टर", + "filters" : "फ़िल्टर", "today" : "आज", "yesterday" : "बिता कल", "received" : "प्राप्त किया", @@ -88,8 +88,8 @@ "offer_expires_in" : "में ऑफर समाप्त हो रहा है: ", "trade_is_powered_by" : "यह व्यापार द्वारा संचालित है ${provider}", "copy_address" : "पता कॉपी करें", - "exchange_result_confirm" : "पुष्टि दबाकर, आप भेज रहे होंगे ${fetchingLabel} ${from} अपने बटुए से ${walletName} ऊपर दिखाए गए पते पर। या आप अपने बाहरी वॉलेट से उपरोक्त पते / क्यूआर कोड पर भेज सकते हैं।\n\nकृपया जारी रखने या राशि बदलने के लिए वापस जाने के लिए पुष्टि करें दबाएं.\n\n", - "exchange_result_description" : "कृपया भेजें ${fetchingLabel} ${from} ऊपर दिखाए गए पते पर\n\n'", + "exchange_result_confirm" : "पुष्टि दबाकर, आप भेज रहे होंगे ${fetchingLabel} ${from} अपने बटुए से ${walletName} ऊपर दिखाए गए पते पर। या आप अपने बाहरी वॉलेट से उपरोक्त पते / क्यूआर कोड पर भेज सकते हैं।\n\nकृपया जारी रखने या राशि बदलने के लिए वापस जाने के लिए पुष्टि करें दबाएं.", + "exchange_result_description" : "कृपया भेजें ${fetchingLabel} ${from} ऊपर दिखाए गए पते पर", "exchange_result_write_down_ID" : "*कृपया ऊपर दिखाए गए अपने ID को कॉपी या लिख लें.", "confirm" : "की पुष्टि करें", "confirm_sending" : "भेजने की पुष्टि करें", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 173317288..a1822f806 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -43,7 +43,7 @@ "outgoing" : "発信", "transactions_by_date" : "日付ごとの取引", "trades" : "取引", - "filters" : "フィルター", + "filters" : "フィルタ", "today" : "今日", "yesterday" : "昨日", "received" : "受け取った", @@ -88,8 +88,8 @@ "offer_expires_in" : "で有効期限が切れます: ", "trade_is_powered_by" : "この取引は ${provider}", "copy_address" : "住所をコピー", - "exchange_result_confirm" : "確認を押すと、送信されます ${fetchingLabel} ${from} と呼ばれるあなたの財布から ${walletName} 上記のアドレスへ. または、外部ウォレットから上記のアドレス/ QRコードに送信できます.\n\n確認を押して続行するか、戻って金額を変更してください.\n\n", - "exchange_result_description" : "送信してください ${fetchingLabel} ${from} 上記のアドレスへ.\n\n'", + "exchange_result_confirm" : "確認を押すと、送信されます ${fetchingLabel} ${from} と呼ばれるあなたの財布から ${walletName} 上記のアドレスへ. または、外部ウォレットから上記のアドレス/ QRコードに送信できます.\n\n確認を押して続行するか、戻って金額を変更してください.", + "exchange_result_description" : "送信してください ${fetchingLabel} ${from} 上記のアドレスへ.", "exchange_result_write_down_ID" : "*上記のIDをコピーまたは書き留めてください.", "confirm" : "確認する", "confirm_sending" : "送信を確認", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 3c817739c..31d7f6cfd 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -88,8 +88,8 @@ "offer_expires_in" : "쿠폰 만료일: ", "trade_is_powered_by" : "이 거래는 ${provider}", "copy_address" : "주소 복사", - "exchange_result_confirm" : "확인을 누르면 전송됩니다 ${fetchingLabel} ${from} 지갑에서 ${walletName} 위에 표시된 주소로. 또는 외부 지갑에서 위의 주소 / QR 코드로 보낼 수 있습니다.\n\n확인을 눌러 계속하거나 금액을 변경하려면 돌아가십시오.\n\n", - "exchange_result_description" : "보내주세요 ${fetchingLabel} ${from} 위에 표시된 주소로.\n\n'", + "exchange_result_confirm" : "확인을 누르면 전송됩니다 ${fetchingLabel} ${from} 지갑에서 ${walletName} 위에 표시된 주소로. 또는 외부 지갑에서 위의 주소 / QR 코드로 보낼 수 있습니다.\n\n확인을 눌러 계속하거나 금액을 변경하려면 돌아가십시오.", + "exchange_result_description" : "보내주세요 ${fetchingLabel} ${from} 위에 표시된 주소로.", "exchange_result_write_down_ID" : "*위에 표시된 ID를 복사하거나 적어 두십시오.", "confirm" : "확인", "confirm_sending" : "전송 확인", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 3e2e4cb0c..542be0271 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -43,7 +43,7 @@ "outgoing" : "Uitgaande", "transactions_by_date" : "Transacties op datum", "trades" : "Trades", - "filters" : "Filters", + "filters" : "Filter", "today" : "Vandaag", "yesterday" : "Gisteren", "received" : "Ontvangen", @@ -88,8 +88,8 @@ "offer_expires_in" : "Aanbieding verloopt over: ", "trade_is_powered_by" : "Deze transactie wordt mogelijk gemaakt door ${provider}", "copy_address" : "Adres kopiëren", - "exchange_result_confirm" : "Door op bevestigen te drukken, wordt u verzonden ${fetchingLabel} ${from} uit je portemonnee genoemd ${walletName} naar bovenstaand adres. Of u kunt uw externe portemonnee naar bovenstaand adres / QR-code sturen.\n\nDruk op bevestigen om door te gaan of terug te gaan om de bedragen te wijzigen.\n\n", - "exchange_result_description" : "Zend alstublieft ${fetchingLabel} ${from} naar bovenstaand adres.\n\n'", + "exchange_result_confirm" : "Door op bevestigen te drukken, wordt u verzonden ${fetchingLabel} ${from} uit je portemonnee genoemd ${walletName} naar bovenstaand adres. Of u kunt uw externe portemonnee naar bovenstaand adres / QR-code sturen.\n\nDruk op bevestigen om door te gaan of terug te gaan om de bedragen te wijzigen.", + "exchange_result_description" : "Zend alstublieft ${fetchingLabel} ${from} naar bovenstaand adres.", "exchange_result_write_down_ID" : "*Kopieer of noteer uw hierboven getoonde ID.", "confirm" : "Bevestigen", "confirm_sending" : "Bevestig verzending", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index e992deb50..da2bd6f2e 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -43,7 +43,7 @@ "outgoing" : "Towarzyski", "transactions_by_date" : "Transakcje według daty", "trades" : "Transakcje", - "filters" : "Filtry", + "filters" : "Filtr", "today" : "Dzisiaj", "yesterday" : "Wczoraj", "received" : "Odebrane", @@ -88,8 +88,8 @@ "offer_expires_in" : "Oferta wygasa za ", "trade_is_powered_by" : "Ten handel jest zasilany przez ${provider}", "copy_address" : "Skopiuj adress", - "exchange_result_confirm" : "Naciskając Potwierdź, wyślesz ${fetchingLabel} ${from} z twojego portfela ${walletName} z twojego portfela. Lub możesz wysłać z zewnętrznego portfela na powyższy adres / kod QR.\n\nNaciśnij Potwierdź, aby kontynuować lub wróć, aby zmienić kwoty.\n\n", - "exchange_result_description" : "Proszę wyślij ${fetchingLabel} ${from} na adres podany powyżej.\n\n'", + "exchange_result_confirm" : "Naciskając Potwierdź, wyślesz ${fetchingLabel} ${from} z twojego portfela ${walletName} z twojego portfela. Lub możesz wysłać z zewnętrznego portfela na powyższy adres / kod QR.\n\nNaciśnij Potwierdź, aby kontynuować lub wróć, aby zmienić kwoty.", + "exchange_result_description" : "Proszę wyślij ${fetchingLabel} ${from} na adres podany powyżej.", "exchange_result_write_down_ID" : "*Skopiuj lub zanotuj swój identyfikator pokazany powyżej.", "confirm" : "Potwierdzać", "confirm_sending" : "Potwierdź wysłanie", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 59919ecbf..7f4df2a4a 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -43,7 +43,7 @@ "outgoing" : "Enviadas", "transactions_by_date" : "Transações por data", "trades" : "Trocas", - "filters" : "Filtros", + "filters" : "Filtro", "today" : "Hoje", "yesterday" : "Ontem", "received" : "Recebida", @@ -88,8 +88,8 @@ "offer_expires_in" : "A oferta expira em: ", "trade_is_powered_by" : "Troca realizada por ${provider}", "copy_address" : "Copiar endereço", - "exchange_result_confirm" : "Ao confirmar, você enviará ${fetchingLabel} ${from} da sua carteira ${walletName} para o endereço exibido acima. Você também pode enviar com uma carteira externa para o endereço/código QR acima.\n\nPressione Confirmar para continuar ou volte para alterar os valores.\n\n", - "exchange_result_description" : "Por favor, envie ${fetchingLabel} ${from} para o endereço mostrado acima.\n\n'", + "exchange_result_confirm" : "Ao confirmar, você enviará ${fetchingLabel} ${from} da sua carteira ${walletName} para o endereço exibido acima. Você também pode enviar com uma carteira externa para o endereço/código QR acima.\n\nPressione Confirmar para continuar ou volte para alterar os valores.", + "exchange_result_description" : "Por favor, envie ${fetchingLabel} ${from} para o endereço mostrado acima.", "exchange_result_write_down_ID" : "*Copie ou anote seu ID mostrado acima.", "confirm" : "Confirmar", "confirm_sending" : "Confirmar o envio", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 81a2c76b8..b5f82e379 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -43,7 +43,7 @@ "outgoing" : "Исходящие", "transactions_by_date" : "Сортировать по дате", "trades" : "Сделки", - "filters" : "Фильтры", + "filters" : "Фильтр", "today" : "Сегодня", "yesterday" : "Вчера", "received" : "Полученные", @@ -88,8 +88,8 @@ "offer_expires_in" : "Предложение истекает через: ", "trade_is_powered_by" : "Сделка выполнена через ${provider}", "copy_address" : "Cкопировать адрес", - "exchange_result_confirm" : "Нажимая подтвердить, вы отправите ${fetchingLabel} ${from} с вашего кошелька ${walletName} на адрес указанный выше. Или вы можете отправить со своего внешнего кошелька на вышеуказанный адрес/QR-код.\n\nПожалуйста, нажмите подтвердить для продолжения, или вернитесь назад для изменения суммы.\n\n", - "exchange_result_description" : "Пожалуйста отправьте ${fetchingLabel} ${from} на адрес, указанный выше.\n\n'", + "exchange_result_confirm" : "Нажимая подтвердить, вы отправите ${fetchingLabel} ${from} с вашего кошелька ${walletName} на адрес указанный выше. Или вы можете отправить со своего внешнего кошелька на вышеуказанный адрес/QR-код.\n\nПожалуйста, нажмите подтвердить для продолжения, или вернитесь назад для изменения суммы.", + "exchange_result_description" : "Пожалуйста отправьте ${fetchingLabel} ${from} на адрес, указанный выше.", "exchange_result_write_down_ID" : "*Пожалуйста, скопируйте или запишите ID, указанный выше.", "confirm" : "Подтвердить", "confirm_sending" : "Подтвердить отправку", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 31268c9d3..bcfc96688 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -43,7 +43,7 @@ "outgoing" : "Вихідні", "transactions_by_date" : "Сортувати по даті", "trades" : "Торгові операції", - "filters" : "Фільтри", + "filters" : "Фільтр", "today" : "Сьогодні", "yesterday" : "Вчора", "received" : "Отримані", @@ -88,8 +88,8 @@ "offer_expires_in" : "Пропозиція закінчиться через: ", "trade_is_powered_by" : "Операція виконана через ${provider}", "copy_address" : "Cкопіювати адресу", - "exchange_result_confirm" : "Натиснувши підтвердити, ви відправите ${fetchingLabel} ${from} з вашого гаманця ${walletName} на адресу вказану вище. Або ви можете відправити зі свого зовнішнього гаманця на вищевказану адресу/QR-код.\n\nБудь ласка, натисніть підтвердити для продовження або поверніться назад щоб змінити суму.\n\n", - "exchange_result_description" : "Будь ласка, відправте ${fetchingLabel} ${from} на адресу, вказану вище.\n\n'", + "exchange_result_confirm" : "Натиснувши підтвердити, ви відправите ${fetchingLabel} ${from} з вашого гаманця ${walletName} на адресу вказану вище. Або ви можете відправити зі свого зовнішнього гаманця на вищевказану адресу/QR-код.\n\nБудь ласка, натисніть підтвердити для продовження або поверніться назад щоб змінити суму.", + "exchange_result_description" : "Будь ласка, відправте ${fetchingLabel} ${from} на адресу, вказану вище.", "exchange_result_write_down_ID" : "*Будь ласка, скопіюйте або запишіть ID, вказаний вище.", "confirm" : "Підтвердити", "confirm_sending" : "Підтвердити відправлення", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 0450aa214..0f5794a10 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -43,7 +43,7 @@ "outgoing" : "外向", "transactions_by_date" : "按日期交易", "trades" : "交易", - "filters" : "筛选器", + "filters" : "過濾", "today" : "今天", "yesterday" : "昨天", "received" : "已收到", @@ -88,8 +88,8 @@ "offer_expires_in" : "优惠有效期至 ", "trade_is_powered_by" : "该交易由 ${provider}", "copy_address" : "复制地址", - "exchange_result_confirm" : "点击确认 您将发送 ${fetchingLabel} ${from} 从你的钱包里 ${walletName} 到上面显示的地址. 或者,您也可以从外部钱包发送上述地址/ QR码。\n\n请按确认继续或返回以更改金额\n\n", - "exchange_result_description" : "请发送 ${fetchingLabel} ${from} 到上面显示的地址.\n\n'", + "exchange_result_confirm" : "点击确认 您将发送 ${fetchingLabel} ${from} 从你的钱包里 ${walletName} 到上面显示的地址. 或者,您也可以从外部钱包发送上述地址/ QR码。\n\n请按确认继续或返回以更改金额", + "exchange_result_description" : "请发送 ${fetchingLabel} ${from} 到上面显示的地址.", "exchange_result_write_down_ID" : "*请复制或写下您上面显示的ID.", "confirm" : "确认", "confirm_sending" : "确认发送",