From 81cee186dbc7c7a78d37d96ef1d18666e3d094ea Mon Sep 17 00:00:00 2001 From: M Date: Sat, 20 Jun 2020 10:10:00 +0300 Subject: [PATCH] TMP 4 --- .gitignore | 4 +- .gitmodules | 6 + lib/bitcoin/bitcoin_address_record.dart | 17 + lib/bitcoin/bitcoin_balance.dart | 23 +- lib/bitcoin/bitcoin_transaction_history.dart | 126 ++-- lib/bitcoin/bitcoin_transaction_info.dart | 2 +- lib/bitcoin/bitcoin_wallet.dart | 316 ++++----- lib/bitcoin/bitcoin_wallet.manager.dart | 59 -- .../bitcoin_wallet_creation_credentials.dart | 21 + lib/bitcoin/bitcoin_wallet_service.dart | 79 +++ lib/bitcoin/file.dart | 5 +- lib/core/AddressLabelValidator.dart | 12 + lib/core/amount_validator.dart | 24 + lib/core/app_service.dart | 16 - lib/core/auth_service.dart | 42 +- lib/{src/stores/auth => core}/auth_state.dart | 0 lib/core/bitcoin_transaction_history.dart | 121 ---- lib/core/bitcoin_wallet.dart | 149 ---- lib/core/bitcoin_wallet_list_service.dart | 103 --- lib/core/generate_wallet_password.dart | 12 + lib/core/mnemonic_length.dart | 17 + lib/core/seed_validator.dart | 73 ++ lib/core/setup_pin_code_state.dart | 15 - lib/core/transaction_history.dart | 9 +- lib/core/validator.dart | 39 ++ lib/core/wallet_base.dart | 16 +- lib/core/wallet_creation_service.dart | 100 ++- lib/core/wallet_credentials.dart | 4 +- lib/core/wallet_list_service.dart | 16 - lib/core/wallet_service.dart | 17 + lib/di.dart | 105 +++ lib/main.dart | 39 +- lib/{core => monero}/monero_balance.dart | 0 lib/monero/monero_subaddress_list.dart | 76 ++ .../monero_transaction_history.dart | 10 +- lib/{core => monero}/monero_wallet.dart | 53 +- .../monero_wallet_service.dart} | 41 +- lib/palette.dart | 1 + lib/reactions/bootstrap.dart | 66 ++ lib/router.dart | 184 +++-- lib/routes.dart | 1 + lib/src/domain/common/balance.dart | 4 +- lib/src/domain/common/mnemonic_item.dart | 11 + lib/src/domain/common/mnemotic_item.dart | 17 - .../domain/services/wallet_list_service.dart | 3 +- lib/src/reactions/set_reactions.dart | 8 +- lib/src/screens/auth/auth_page.dart | 117 ++-- lib/src/screens/auth/create_login_page.dart | 25 +- lib/src/screens/auth/create_unlock_page.dart | 21 +- lib/src/screens/base_page.dart | 31 +- lib/src/screens/dashboard/dashboard_page.dart | 44 +- .../dashboard/widgets/button_header.dart | 152 ++-- .../widgets/trade_history_panel.dart | 245 +++---- .../dashboard/widgets/wallet_card.dart | 657 +++++++++--------- .../screens/new_wallet/new_wallet_page.dart | 195 ++---- .../new_wallet/new_wallet_type_page.dart | 131 ++-- lib/src/screens/receive/receive_page.dart | 553 +++++++-------- .../screens/receive/widgets/address_cell.dart | 70 ++ .../restore_wallet_from_seed_details.dart | 147 ++-- .../restore_wallet_from_seed_page.dart | 41 +- .../restore/restore_wallet_options_page.dart | 68 +- lib/src/screens/root/root.dart | 110 +-- .../seed_language/seed_language_page.dart | 94 ++- .../widgets/seed_language_picker.dart | 198 +++--- .../address_edit_or_create_page.dart | 75 ++ .../subaddress/new_subaddress_page.dart | 130 ---- .../subaddress/subaddress_list_page.dart | 8 +- lib/src/stores/auth/auth_store.dart | 198 +++--- .../authentication/authentication_store.dart | 4 +- .../wallet_restoration_store.dart | 56 +- lib/src/widgets/base_text_form_field.dart | 82 ++- lib/src/widgets/blockchain_height_widget.dart | 46 +- lib/src/widgets/seed_language_selector.dart | 47 ++ lib/src/widgets/seed_widget.dart | 495 +++++++------ lib/store/app_store.dart | 16 + lib/store/authentication_store.dart | 23 + lib/store/wallet_list_store.dart | 10 + lib/themes.dart | 8 +- lib/utils/list_item.dart | 3 + lib/utils/list_section.dart | 5 + .../address_list/account_list_header.dart | 3 + .../address_edit_or_create_view_model.dart | 93 +++ .../address_list/address_list_header.dart | 3 + .../address_list/address_list_item.dart | 14 + .../address_list/address_list_view_model.dart | 135 ++++ lib/view_model/auth_state.dart | 20 + lib/view_model/auth_view_model.dart | 97 +++ lib/view_model/dashboard_view_model.dart | 68 ++ lib/view_model/wallet_creation_state.dart | 15 + lib/view_model/wallet_creation_vm.dart | 40 ++ lib/view_model/wallet_new_vm.dart | 42 ++ .../wallet_restoration_from_seed_vm.dart | 52 ++ pubspec.lock | 7 + pubspec.yaml | 1 + 94 files changed, 3771 insertions(+), 2986 deletions(-) create mode 100644 .gitmodules create mode 100644 lib/bitcoin/bitcoin_address_record.dart delete mode 100644 lib/bitcoin/bitcoin_wallet.manager.dart create mode 100644 lib/bitcoin/bitcoin_wallet_creation_credentials.dart create mode 100644 lib/bitcoin/bitcoin_wallet_service.dart create mode 100644 lib/core/AddressLabelValidator.dart create mode 100644 lib/core/amount_validator.dart delete mode 100644 lib/core/app_service.dart rename lib/{src/stores/auth => core}/auth_state.dart (100%) delete mode 100644 lib/core/bitcoin_transaction_history.dart delete mode 100644 lib/core/bitcoin_wallet.dart delete mode 100644 lib/core/bitcoin_wallet_list_service.dart create mode 100644 lib/core/generate_wallet_password.dart create mode 100644 lib/core/mnemonic_length.dart create mode 100644 lib/core/seed_validator.dart delete mode 100644 lib/core/setup_pin_code_state.dart delete mode 100644 lib/core/wallet_list_service.dart create mode 100644 lib/core/wallet_service.dart create mode 100644 lib/di.dart rename lib/{core => monero}/monero_balance.dart (100%) create mode 100644 lib/monero/monero_subaddress_list.dart rename lib/{core => monero}/monero_transaction_history.dart (72%) rename lib/{core => monero}/monero_wallet.dart (81%) rename lib/{core/monero_wallet_list_service.dart => monero/monero_wallet_service.dart} (78%) create mode 100644 lib/reactions/bootstrap.dart create mode 100644 lib/src/domain/common/mnemonic_item.dart delete mode 100644 lib/src/domain/common/mnemotic_item.dart create mode 100644 lib/src/screens/receive/widgets/address_cell.dart create mode 100644 lib/src/screens/subaddress/address_edit_or_create_page.dart delete mode 100644 lib/src/screens/subaddress/new_subaddress_page.dart create mode 100644 lib/store/app_store.dart create mode 100644 lib/store/authentication_store.dart create mode 100644 lib/store/wallet_list_store.dart create mode 100644 lib/utils/list_item.dart create mode 100644 lib/utils/list_section.dart create mode 100644 lib/view_model/address_list/account_list_header.dart create mode 100644 lib/view_model/address_list/address_edit_or_create_view_model.dart create mode 100644 lib/view_model/address_list/address_list_header.dart create mode 100644 lib/view_model/address_list/address_list_item.dart create mode 100644 lib/view_model/address_list/address_list_view_model.dart create mode 100644 lib/view_model/auth_state.dart create mode 100644 lib/view_model/auth_view_model.dart create mode 100644 lib/view_model/dashboard_view_model.dart create mode 100644 lib/view_model/wallet_creation_state.dart create mode 100644 lib/view_model/wallet_creation_vm.dart create mode 100644 lib/view_model/wallet_new_vm.dart create mode 100644 lib/view_model/wallet_restoration_from_seed_vm.dart diff --git a/.gitignore b/.gitignore index 7a496dcc..4102cec3 100644 --- a/.gitignore +++ b/.gitignore @@ -87,4 +87,6 @@ cw_monero/cw_monero/android/.cxx/ android/key.properties **/tool/.secrets-prod.json -**/lib/.secrets.g.dart \ No newline at end of file +**/lib/.secrets.g.dart + +vendor/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..cdd040e6 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "inject.dart"] + path = inject.dart + url = https://github.com/google/inject.dart +[submodule ".vendor/inject.dart"] + path = .vendor/inject.dart + url = https://github.com/google/inject.dart diff --git a/lib/bitcoin/bitcoin_address_record.dart b/lib/bitcoin/bitcoin_address_record.dart new file mode 100644 index 00000000..cd2e2149 --- /dev/null +++ b/lib/bitcoin/bitcoin_address_record.dart @@ -0,0 +1,17 @@ +import 'dart:convert'; + +class BitcoinAddressRecord { + BitcoinAddressRecord(this.address, {this.label}); + + factory BitcoinAddressRecord.fromJSON(String jsonSource) { + final decoded = json.decode(jsonSource) as Map; + + return BitcoinAddressRecord(decoded['address'] as String, + label: decoded['label'] as String); + } + + final String address; + String label; + + String toJSON() => json.encode({'label': label, 'address': address}); +} diff --git a/lib/bitcoin/bitcoin_balance.dart b/lib/bitcoin/bitcoin_balance.dart index 8f9d06fa..a1bc5c84 100644 --- a/lib/bitcoin/bitcoin_balance.dart +++ b/lib/bitcoin/bitcoin_balance.dart @@ -1,14 +1,35 @@ +import 'dart:convert'; + import 'package:flutter/foundation.dart'; import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart'; import 'package:cake_wallet/src/domain/common/balance.dart'; class BitcoinBalance extends Balance { - BitcoinBalance({@required this.confirmed, @required this.unconfirmed}); + const BitcoinBalance({@required this.confirmed, @required this.unconfirmed}) : super(); + + factory BitcoinBalance.fromJSON(String jsonSource) { + if (jsonSource == null) { + return null; + } + + final decoded = json.decode(jsonSource) as Map; + + return BitcoinBalance( + confirmed: decoded['confirmed'] as int ?? 0, + unconfirmed: decoded['unconfirmed'] as int ?? 0); + } final int confirmed; final int unconfirmed; + int get total => confirmed + unconfirmed; + String get confirmedFormatted => bitcoinAmountToString(amount: confirmed); + String get unconfirmedFormatted => bitcoinAmountToString(amount: unconfirmed); + String get totalFormatted => bitcoinAmountToString(amount: total); + + String toJSON() => + json.encode({'confirmed': confirmed, 'unconfirmed': unconfirmed}); } diff --git a/lib/bitcoin/bitcoin_transaction_history.dart b/lib/bitcoin/bitcoin_transaction_history.dart index cbea45ef..898153ae 100644 --- a/lib/bitcoin/bitcoin_transaction_history.dart +++ b/lib/bitcoin/bitcoin_transaction_history.dart @@ -1,78 +1,64 @@ import 'dart:convert'; -import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart'; import 'package:flutter/foundation.dart'; -import 'package:rxdart/rxdart.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/core/transaction_history.dart'; +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_history.dart'; import 'package:cake_wallet/src/domain/common/transaction_info.dart'; -import 'package:cake_wallet/bitcoin/file.dart'; +import 'package:cake_wallet/src/domain/common/transaction_direction.dart'; + +part 'bitcoin_transaction_history.g.dart'; -class BitcoinTransactionHistory extends TransactionHistory { - BitcoinTransactionHistory( - {@required this.eclient, - @required this.path, - @required String password, - @required this.wallet}) - : _transactions = BehaviorSubject>.seeded([]), +// TODO: Think about another transaction store for bitcoin transaction history.. + +const _transactionsHistoryFileName = 'transactions.json'; + +class BitcoinTransactionHistory = BitcoinTransactionHistoryBase + with _$BitcoinTransactionHistory; + +abstract class BitcoinTransactionHistoryBase + extends TransactionHistoryBase with Store { + BitcoinTransactionHistoryBase( + {this.eclient, String dirPath, @required String password}) + : path = '$dirPath/$_transactionsHistoryFileName', _password = password, _height = 0; - final BitcoinWallet wallet; + BitcoinWallet wallet; final ElectrumClient eclient; final String path; final String _password; int _height; - @override - Observable> get transactions => _transactions.stream; - List get transactionsAll => _transactions.value; - final BehaviorSubject> _transactions; - bool _isUpdating = false; - Future init() async { + // TODO: throw exeption if wallet is null; final info = await _read(); _height = (info['height'] as int) ?? _height; - _transactions.value = info['transactions'] as List; + // FIXME: remove hardcoded value + transactions = ObservableList.of([ + BitcoinTransactionInfo( + id: 'test', + height: 12, + amount: 12, + direction: TransactionDirection.incoming, + date: DateTime.now(), + isPending: false) + ]); } - @override - Future> getAll() async => _transactions.value; - @override Future update() async { - if (_isUpdating) { - return; - } - - try { - _isUpdating = true; - final newTransasctions = await fetchTransactions(); - _transactions.value = _transactions.value + newTransasctions; - _updateHeight(); - await save(); - _isUpdating = false; - } catch (e) { - _isUpdating = false; - rethrow; - } - } - - Future> 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; - - return {'raw': raw, 'header': header}; + await super.update(); + _updateHeight(); } + @override Future> fetchTransactions() async { - final addresses = wallet.getAddresses(); + final addresses = wallet.addresses; final histories = - addresses.map((address) => eclient.getHistory(address: address)); + addresses.map((record) => eclient.getHistory(address: record.address)); final _historiesWithDetails = await Future.wait(histories) .then((histories) => histories .map((h) => h.where((tx) => (tx['height'] as int) > _height)) @@ -85,35 +71,35 @@ class BitcoinTransactionHistory extends TransactionHistory { return historiesWithDetails .map((info) => BitcoinTransactionInfo.fromHexAndHeader( info['raw'] as String, info['header'] as Map, - addresses: addresses)) + addresses: addresses.map((record) => record.address).toList())) .toList(); } + Future> 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; + + return {'raw': raw, 'header': header}; + } + Future add(List transactions) async { - final txs = await getAll() - ..addAll(transactions); - await writeData( - path: path, - password: _password, - data: json - .encode(txs.map((tx) => (tx as BitcoinTransactionInfo).toJson()))); + this.transactions.addAll(transactions); + await save(); } Future addOne(BitcoinTransactionInfo tx) async { - final txs = await getAll() - ..add(tx); - await writeData( - path: path, - password: _password, - data: json - .encode(txs.map((tx) => (tx as BitcoinTransactionInfo).toJson()))); + transactions.add(tx); + await save(); } Future save() async => writeData( path: path, password: _password, - data: json - .encode({'height': _height, 'transactions': _transactions.value})); + data: json.encode({'height': _height, 'transactions': transactions})); Future> _read() async { try { @@ -133,13 +119,13 @@ class BitcoinTransactionHistory extends TransactionHistory { return {'transactions': transactions, 'height': height}; } catch (_) { - return {'transactions': List(), 'height': 0}; + return {'transactions': [], 'height': 0}; } } void _updateHeight() { - final int newHeight = _transactions.value - .fold(0, (acc, val) => val.height > acc ? val.height : acc); + 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 92e0276d..5850f19d 100644 --- a/lib/bitcoin/bitcoin_transaction_info.dart +++ b/lib/bitcoin/bitcoin_transaction_info.dart @@ -66,7 +66,7 @@ class BitcoinTransactionInfo extends TransactionInfo { String amountFormatted() => bitcoinAmountToString(amount: amount); @override - String fiatAmount() => ''; + String fiatAmount() => '\$ 24.5'; Map toJson() { final m = Map(); diff --git a/lib/bitcoin/bitcoin_wallet.dart b/lib/bitcoin/bitcoin_wallet.dart index c38f3ac6..faff9755 100644 --- a/lib/bitcoin/bitcoin_wallet.dart +++ b/lib/bitcoin/bitcoin_wallet.dart @@ -1,251 +1,184 @@ -import 'dart:async'; -import 'dart:convert'; import 'dart:typed_data'; -import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart'; -import 'package:cake_wallet/bitcoin/bitcoin_balance.dart'; -import 'package:cake_wallet/src/domain/common/sync_status.dart'; -import 'package:flutter/foundation.dart'; -import 'package:rxdart/rxdart.dart'; +import 'dart:convert'; +import 'package:mobx/mobx.dart'; import 'package:bip39/bip39.dart' as bip39; +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:cake_wallet/src/domain/common/wallet_type.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_transaction_history.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_address_record.dart'; import 'package:cake_wallet/bitcoin/file.dart'; import 'package:cake_wallet/bitcoin/electrum.dart'; -import 'package:cake_wallet/bitcoin/bitcoin_transaction_history.dart'; -import 'package:cake_wallet/src/domain/common/pathForWallet.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_balance.dart'; import 'package:cake_wallet/src/domain/common/node.dart'; -import 'package:cake_wallet/src/domain/common/pending_transaction.dart'; -import 'package:cake_wallet/src/domain/common/transaction_creation_credentials.dart'; -import 'package:cake_wallet/src/domain/common/transaction_history.dart'; -import 'package:cake_wallet/src/domain/common/wallet.dart'; -import 'package:cake_wallet/src/domain/common/wallet_type.dart'; +import 'package:cake_wallet/core/wallet_base.dart'; -class BitcoinWallet extends Wallet { - BitcoinWallet( - {@required this.hdwallet, - @required this.eclient, - @required this.path, - @required String password, - int accountIndex = 0, - this.mnemonic}) - : _accountIndex = accountIndex, - _password = password, - _syncStatus = BehaviorSubject(), - _onBalanceChange = BehaviorSubject(), - _onAddressChange = BehaviorSubject(), - _onNameChange = BehaviorSubject(); - - @override - Observable get onBalanceChange => _onBalanceChange.stream; +part 'bitcoin_wallet.g.dart'; - @override - Observable get syncStatus => _syncStatus.stream; - - @override - String get name => path.split('/').last ?? ''; - @override - String get address => hdwallet.address; - String get xpub => hdwallet.base58; +/* TODO: Save balance to a wallet file. + Load balance from the wallet file in `init` method. +*/ - final String path; - final bitcoin.HDWallet hdwallet; - final ElectrumClient eclient; - final String mnemonic; - BitcoinTransactionHistory history; - - final BehaviorSubject _syncStatus; - final BehaviorSubject _onBalanceChange; - final BehaviorSubject _onAddressChange; - final BehaviorSubject _onNameChange; - BehaviorSubject _addressUpdatesSubject; - StreamSubscription _addressUpdatesSubscription; - final String _password; - int _accountIndex; +class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet; - static Future load( - {@required String name, @required String password}) async { - final walletDirPath = - await pathForWalletDir(name: name, type: WalletType.bitcoin); - final walletPath = '$walletDirPath/$name'; - final walletJSONRaw = await read(path: walletPath, password: password); - final jsoned = json.decode(walletJSONRaw) as Map; - final mnemonic = jsoned['mnemonic'] as String; +abstract class BitcoinWalletBase extends WalletBase with Store { + static BitcoinWallet fromJSON( + {@required String password, + @required String name, + @required String dirPath, + String jsonSource}) { + final data = json.decode(jsonSource) as Map; + final mnemonic = data['mnemonic'] as String; final accountIndex = - (jsoned['account_index'] == "null" || jsoned['account_index'] == null) + (data['account_index'] == "null" || data['account_index'] == null) ? 0 - : int.parse(jsoned['account_index'] as String); + : int.parse(data['account_index'] as String); + final _addresses = data['addresses'] as List; + final addresses = []; + final balance = BitcoinBalance.fromJSON(data['balance'] as String) ?? + BitcoinBalance(confirmed: 0, unconfirmed: 0); + + _addresses?.forEach((Object el) { + if (el is String) { + addresses.add(BitcoinAddressRecord.fromJSON(el)); + } + }); - return await build( + return BitcoinWalletBase.build( + dirPath: dirPath, mnemonic: mnemonic, password: password, name: name, - accountIndex: accountIndex); + accountIndex: accountIndex, + initialAddresses: addresses, + initialBalance: balance); } - static Future build( + static BitcoinWallet build( {@required String mnemonic, @required String password, @required String name, - int accountIndex = 0}) async { - final hd = bitcoin.HDWallet.fromSeed(bip39.mnemonicToSeed(mnemonic), - network: bitcoin.bitcoin); - final walletDirPath = - await pathForWalletDir(name: name, type: WalletType.bitcoin); - final walletPath = '$walletDirPath/$name'; - final historyPath = '$walletDirPath/transactions.json'; + @required String dirPath, + List initialAddresses, + BitcoinBalance initialBalance, + int accountIndex = 0}) { + final walletPath = '$dirPath/$name'; final eclient = ElectrumClient(); - final wallet = BitcoinWallet( - hdwallet: hd, + final history = BitcoinTransactionHistory( + eclient: eclient, dirPath: dirPath, password: password); + + return BitcoinWallet._internal( eclient: eclient, path: walletPath, + name: name, mnemonic: mnemonic, password: password, - accountIndex: accountIndex); - final history = BitcoinTransactionHistory( - eclient: eclient, - path: historyPath, - password: password, - wallet: wallet); - wallet.history = history; - await history.init(); - await wallet.updateInfo(); - - return wallet; - } - - List getAddresses() => _accountIndex == 0 - ? [address] - : List.generate( - _accountIndex, (i) => _getAddress(hd: hdwallet, index: i)); - - Future newAddress() async { - _accountIndex += 1; - final address = _getAddress(hd: hdwallet, index: _accountIndex); - await save(); - - return address; + accountIndex: accountIndex, + initialAddresses: initialAddresses, + initialBalance: initialBalance, + transactionHistory: history); } - @override - Future close() async { - await _addressUpdatesSubscription?.cancel(); - } + BitcoinWalletBase._internal( + {@required this.eclient, + @required this.path, + @required String password, + @required this.name, + List 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.of(initialAddresses) + : ObservableList(); - @override - Future connectToNode( - {Node node, bool useSSL = false, bool isLightWallet = false}) async { - try { - // FIXME: Hardcoded server address - // final uri = Uri.parse(node.uri); - // https://electrum2.hodlister.co:50002 - await eclient.connect(host: 'electrum2.hodlister.co', port: 50002); - _syncStatus.value = ConnectedSyncStatus(); - } catch (e) { - print(e.toString()); - _syncStatus.value = FailedSyncStatus(); + if (addresses.isEmpty) { + addresses.add(BitcoinAddressRecord(hd.address)); } - } - @override - Future createTransaction( - TransactionCreationCredentials credentials) async { - final txb = bitcoin.TransactionBuilder(network: bitcoin.bitcoin); - final transactions = history.transactionsAll; - history.transactionsAll.sort((q, w) => q.height.compareTo(w.height)); - final prevTx = transactions.first; - - txb.setVersion(1); - txb.addInput(prevTx, 0); - txb.addOutput('address', 112); - txb.sign(vin: null, keyPair: null); - - final hex = txb.build().toHex(); - - // broadcast transaction to electrum - return null; - } + address = addresses.first.address; - @override - Future getAddress() async => address; + _password = password; + _accountIndex = accountIndex; + } @override - Future getCurrentHeight() async => 0; + final BitcoinTransactionHistory transactionHistory; + final String path; + bitcoin.HDWallet hd; + final ElectrumClient eclient; + final String mnemonic; + int _accountIndex; + String _password; @override - Future getFilename() async => path.split('/').last ?? ''; + String name; @override - Future getFullBalance() async => - bitcoinAmountToString(amount: _onBalanceChange.value.total); + @observable + String address; @override - TransactionHistory getHistory() => history; + @observable + BitcoinBalance balance; @override - Future> getKeys() async => - {'publicKey': hdwallet.pubKey, 'privateKey': hdwallet.privKey}; + final type = WalletType.bitcoin; - @override - Future getName() async => path.split('/').last ?? ''; + ObservableList addresses; - @override - Future getNodeHeight() async => 0; + String get xpub => hd.base58; - @override - Future getSeed() async => mnemonic; + Future init() async { + await transactionHistory.init(); + } - @override - WalletType getType() => WalletType.bitcoin; + Future generateNewAddress({String label}) async { + _accountIndex += 1; + final address = BitcoinAddressRecord( + _getAddress(hd: hd, index: _accountIndex), + label: label); + addresses.add(address); - @override - Future getUnlockedBalance() async => - bitcoinAmountToString(amount: _onBalanceChange.value.total); + await save(); - @override - Future isConnected() async => eclient.isConnected; + return address; + } - @override - Observable get onAddressChange => _onAddressChange.stream; + Future updateAddress(String address, {String label}) async { + for (final addr in addresses) { + if (addr.address == address) { + addr.label = label; + await save(); + break; + } + } + } @override - Observable get onNameChange => _onNameChange.stream; + Future startSync() async {} @override - Future rescan({int restoreHeight = 0}) { - // TODO: implement rescan - return null; - } + Future connectToNode({@required Node node}) async {} @override - Future startSync() async { - _addressUpdatesSubject = eclient.addressUpdate(address: address); - _addressUpdatesSubscription = - _addressUpdatesSubject.listen((obj) => print('new obj: $obj')); - _onBalanceChange.value = await fetchBalance(); - getHistory().update(); - } + Future createTransaction(Object credentials) async {} @override - Future updateInfo() async { - _onNameChange.value = await getName(); - // _addressUpdatesSubject = eclient.addressUpdate(address: address); - // _addressUpdatesSubscription = - // _addressUpdatesSubject.listen((obj) => print('new obj: $obj')); - _onBalanceChange.value = BitcoinBalance(confirmed: 0, unconfirmed: 0); - print(await getKeys()); - } - - Future fetchBalance() async { - final balance = await _fetchBalances(); - - return BitcoinBalance( - confirmed: balance['confirmed'], unconfirmed: balance['unconfirmed']); - } + Future save() async => + await write(path: path, password: _password, data: toJSON()); - Future save() async => await write( - path: path, - password: _password, - obj: {'mnemonic': mnemonic, 'account_index': _accountIndex.toString()}); + String toJSON() => json.encode({ + 'mnemonic': mnemonic, + 'account_index': _accountIndex.toString(), + 'addresses': addresses.map((addr) => addr.toJSON()).toList(), + 'balance': balance?.toJSON() + }); String _getAddress({bitcoin.HDWallet hd, int index}) => bitcoin .P2PKH( @@ -256,9 +189,8 @@ class BitcoinWallet extends Wallet { Future> _fetchBalances() async { final balances = await Future.wait( - getAddresses().map((address) => eclient.getBalance(address: address))); - final balance = - balances.fold(Map(), (Map acc, val) { + addresses.map((record) => eclient.getBalance(address: record.address))); + final balance = balances.fold({}, (Map acc, val) { acc['confirmed'] = (val['confirmed'] as int ?? 0) + (acc['confirmed'] ?? 0); acc['unconfirmed'] = diff --git a/lib/bitcoin/bitcoin_wallet.manager.dart b/lib/bitcoin/bitcoin_wallet.manager.dart deleted file mode 100644 index 5f94b610..00000000 --- a/lib/bitcoin/bitcoin_wallet.manager.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'dart:io'; -import 'package:bip39/bip39.dart' as bip39; -import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart'; -import 'package:cake_wallet/src/domain/common/pathForWallet.dart'; -import 'package:cake_wallet/src/domain/common/wallet.dart'; -import 'package:cake_wallet/src/domain/common/wallet_description.dart'; -import 'package:cake_wallet/src/domain/common/wallet_type.dart'; -import 'package:cake_wallet/src/domain/common/wallets_manager.dart'; - -class BitcoinWalletManager extends WalletsManager { - @override - Future create(String name, String password, String language) async { - final wallet = await BitcoinWallet.build( - mnemonic: bip39.generateMnemonic(), password: password, name: name); - await wallet.save(); - - return wallet; - } - - @override - Future isWalletExit(String name) async => - File(await pathForWallet(name: name, type: WalletType.bitcoin)) - .existsSync(); - - @override - Future openWallet(String name, String password) async { - return BitcoinWallet.load( - name: name, password: password); - } - - @override - Future remove(WalletDescription wallet) async { - final path = await pathForWalletDir(name: wallet.name, type: wallet.type); - final f = File(path); - - if (!f.existsSync()) { - return; - } - - f.deleteSync(); - } - - @override - Future restoreFromKeys(String name, String password, String language, - int restoreHeight, String address, String viewKey, String spendKey) { - // TODO: implement restoreFromKeys - return null; - } - - @override - Future restoreFromSeed( - String name, String password, String seed, int restoreHeight) async { - final wallet = await BitcoinWallet.build( - name: name, password: password, mnemonic: seed); - await wallet.save(); - - return wallet; - } -} diff --git a/lib/bitcoin/bitcoin_wallet_creation_credentials.dart b/lib/bitcoin/bitcoin_wallet_creation_credentials.dart new file mode 100644 index 00000000..051a5700 --- /dev/null +++ b/lib/bitcoin/bitcoin_wallet_creation_credentials.dart @@ -0,0 +1,21 @@ +import 'package:cake_wallet/core/wallet_credentials.dart'; + +class BitcoinNewWalletCredentials extends WalletCredentials { + BitcoinNewWalletCredentials({String name}) : super(name: name); +} + +class BitcoinRestoreWalletFromSeedCredentials extends WalletCredentials { + BitcoinRestoreWalletFromSeedCredentials( + {String name, String password, this.mnemonic}) + : super(name: name, password: password); + + final String mnemonic; +} + +class BitcoinRestoreWalletFromWIFCredentials extends WalletCredentials { + BitcoinRestoreWalletFromWIFCredentials( + {String name, String password, this.wif}) + : super(name: name, password: password); + + final String wif; +} diff --git a/lib/bitcoin/bitcoin_wallet_service.dart b/lib/bitcoin/bitcoin_wallet_service.dart new file mode 100644 index 00000000..d5827df1 --- /dev/null +++ b/lib/bitcoin/bitcoin_wallet_service.dart @@ -0,0 +1,79 @@ +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'; +import 'package:cake_wallet/core/wallet_service.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart'; +import 'package:cake_wallet/src/domain/common/pathForWallet.dart'; +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; + +class BitcoinWalletService extends WalletService< + BitcoinNewWalletCredentials, + BitcoinRestoreWalletFromSeedCredentials, + BitcoinRestoreWalletFromWIFCredentials> { + @override + Future create(BitcoinNewWalletCredentials credentials) async { + final dirPath = await pathForWalletDir( + type: WalletType.bitcoin, name: credentials.name); + final wallet = BitcoinWalletBase.build( + dirPath: dirPath, + mnemonic: bip39.generateMnemonic(), + password: credentials.password, + name: credentials.name); + await wallet.save(); + await wallet.init(); + + return wallet; + } + + @override + Future isWalletExit(String name) async => + File(await pathForWallet(name: name, type: WalletType.bitcoin)) + .existsSync(); + + @override + Future openWallet(String name, String password) async { + final walletDirPath = + await pathForWalletDir(name: name, type: WalletType.bitcoin); + final walletPath = '$walletDirPath/$name'; + final walletJSONRaw = await read(path: walletPath, password: password); + final wallet = BitcoinWalletBase.fromJSON( + password: password, + name: name, + dirPath: walletDirPath, + jsonSource: walletJSONRaw); + await wallet.init(); + + return wallet; + } + + @override + Future remove(String wallet) { + // TODO: implement remove + throw UnimplementedError(); + } + + @override + Future restoreFromKeys( + BitcoinRestoreWalletFromWIFCredentials credentials) async { + // TODO: implement restoreFromKeys + throw UnimplementedError(); + } + + @override + Future restoreFromSeed( + BitcoinRestoreWalletFromSeedCredentials credentials) async { + final dirPath = await pathForWalletDir( + type: WalletType.bitcoin, name: credentials.name); + final wallet = BitcoinWalletBase.build( + dirPath: dirPath, + name: credentials.name, + password: credentials.password, + mnemonic: credentials.mnemonic); + await wallet.save(); + await wallet.init(); + + return wallet; + } +} diff --git a/lib/bitcoin/file.dart b/lib/bitcoin/file.dart index 054eb38b..ddb6c23c 100644 --- a/lib/bitcoin/file.dart +++ b/lib/bitcoin/file.dart @@ -7,12 +7,11 @@ import 'package:flutter/foundation.dart'; Future write( {@required String path, @required String password, - @required Map obj}) async { - final jsoned = json.encode(obj); + @required String data}) async { final keys = extractKeys(password); final key = encrypt.Key.fromBase64(keys.first); final iv = encrypt.IV.fromBase64(keys.last); - final encrypted = await encode(key: key, iv: iv, data: jsoned); + final encrypted = await encode(key: key, iv: iv, data: data); final f = File(path); f.writeAsStringSync(encrypted); } diff --git a/lib/core/AddressLabelValidator.dart b/lib/core/AddressLabelValidator.dart new file mode 100644 index 00000000..903b2e50 --- /dev/null +++ b/lib/core/AddressLabelValidator.dart @@ -0,0 +1,12 @@ +import 'package:cake_wallet/core/validator.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; + +class AddressLabelValidator extends TextValidator { + AddressLabelValidator({WalletType type}) + : super( + errorMessage: S.current.error_text_subaddress_name, + pattern: '''^[^`,'"]{1,20}\$''', + minLength: 1, + maxLength: 20); +} diff --git a/lib/core/amount_validator.dart b/lib/core/amount_validator.dart new file mode 100644 index 00000000..0394e924 --- /dev/null +++ b/lib/core/amount_validator.dart @@ -0,0 +1,24 @@ +import 'package:cake_wallet/core/validator.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; + +class AmountValidator extends TextValidator { + AmountValidator({WalletType type}) + : super( + errorMessage: S.current.error_text_amount, + pattern: _pattern(type), + minLength: 0, + maxLength: 0); + + static String _pattern(WalletType type) { + switch (type) { + case WalletType.monero: + 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})\$'; + default: + return ''; + } + } +} diff --git a/lib/core/app_service.dart b/lib/core/app_service.dart deleted file mode 100644 index 201f1eb6..00000000 --- a/lib/core/app_service.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:mobx/mobx.dart'; -import 'package:cake_wallet/core/auth_service.dart'; -import 'package:cake_wallet/core/wallet_base.dart'; -import 'package:cake_wallet/core/wallet_creation_service.dart'; - -part 'app_service.g.dart'; - -class AppService = AppServiceBase with _$AppService; - -abstract class AppServiceBase with Store { - AppServiceBase({this.walletCreationService, this.authService, this.wallet}); - - WalletCreationService walletCreationService; - AuthService authService; - WalletBase wallet; -} \ No newline at end of file diff --git a/lib/core/auth_service.dart b/lib/core/auth_service.dart index 6589f45c..f66f44a8 100644 --- a/lib/core/auth_service.dart +++ b/lib/core/auth_service.dart @@ -1,21 +1,41 @@ import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; -import 'package:cake_wallet/core/setup_pin_code_state.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:cake_wallet/src/domain/common/secret_store_key.dart'; +import 'package:cake_wallet/src/domain/common/encrypt.dart'; -part 'auth_service.g.dart'; +class AuthService with Store { + AuthService({this.secureStorage, this.sharedPreferences}); -class AuthService = AuthServiceBase with _$AuthService; + final FlutterSecureStorage secureStorage; + final SharedPreferences sharedPreferences; -abstract class AuthServiceBase with Store { - @observable - SetupPinCodeState setupPinCodeState; + Future setPassword(String password) async { + final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword); + final encodedPassword = encodedPinCode(pin: password); + await secureStorage.write(key: key, value: encodedPassword); + } + + Future canAuthenticate() async { + final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword); + final walletName = sharedPreferences.getString('current_wallet_name') ?? ''; + var password = ''; - Future setupPinCode({@required String pin}) async {} + try { + password = await secureStorage.read(key: key); + } catch (e) { + print(e); + } - Future authenticate({@required String pin}) async { - return false; + return walletName.isNotEmpty && password.isNotEmpty; } - void resetSetupPinCodeState() => - setupPinCodeState = InitialSetupPinCodeState(); + Future authenticate(String pin) async { + final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword); + final encodedPin = await secureStorage.read(key: key); + final decodedPin = decodedPinCode(pin: encodedPin); + + return decodedPin == pin; + } } diff --git a/lib/src/stores/auth/auth_state.dart b/lib/core/auth_state.dart similarity index 100% rename from lib/src/stores/auth/auth_state.dart rename to lib/core/auth_state.dart diff --git a/lib/core/bitcoin_transaction_history.dart b/lib/core/bitcoin_transaction_history.dart deleted file mode 100644 index fb5aaaa2..00000000 --- a/lib/core/bitcoin_transaction_history.dart +++ /dev/null @@ -1,121 +0,0 @@ -import 'dart:convert'; -import 'package:flutter/foundation.dart'; -import 'package:mobx/mobx.dart'; -import 'package:cake_wallet/core/transaction_history.dart'; -import 'package:cake_wallet/core/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/bitcoin/file.dart'; - -part 'bitcoin_transaction_history.g.dart'; - -// TODO: Think about another transaction store for bitcoin transaction history.. - -const _transactionsHistoryFileName = 'transactions.json'; - -class BitcoinTransactionHistory = BitcoinTransactionHistoryBase - with _$BitcoinTransactionHistory; - -abstract class BitcoinTransactionHistoryBase - extends TranasctionHistoryBase with Store { - BitcoinTransactionHistoryBase( - {this.eclient, String dirPath, @required String password}) - : path = '$dirPath/$_transactionsHistoryFileName', - _password = password, - _height = 0; - - BitcoinWallet wallet; - final ElectrumClient eclient; - final String path; - final String _password; - int _height; - - Future init() async { - // TODO: throw exeption if wallet is null; - final info = await _read(); - _height = (info['height'] as int) ?? _height; - transactions = info['transactions'] as List; - } - - @override - Future update() async { - await super.update(); - _updateHeight(); - } - - @override - Future> fetchTransactions() async { - final addresses = wallet.getAddresses(); - final histories = - addresses.map((address) => eclient.getHistory(address: address)); - final _historiesWithDetails = await Future.wait(histories) - .then((histories) => histories - .map((h) => h.where((tx) => (tx['height'] as int) > _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, - addresses: addresses)) - .toList(); - } - - Future> 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; - - return {'raw': raw, 'header': header}; - } - - Future add(List transactions) async { - this.transactions.addAll(transactions); - await save(); - } - - Future addOne(BitcoinTransactionInfo tx) async { - transactions.add(tx); - await save(); - } - - Future save() async => writeData( - path: path, - password: _password, - data: json.encode({'height': _height, 'transactions': transactions})); - - Future> _read() async { - try { - final content = await read(path: path, password: _password); - final jsoned = json.decode(content) as Map; - final height = jsoned['height'] as int; - final transactions = (jsoned['transactions'] as List) - .map((dynamic row) { - if (row is Map) { - return BitcoinTransactionInfo.fromJson(row); - } - - return null; - }) - .where((el) => el != null) - .toList(); - - return {'transactions': transactions, 'height': height}; - } catch (_) { - return {'transactions': [], 'height': 0}; - } - } - - 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/core/bitcoin_wallet.dart b/lib/core/bitcoin_wallet.dart deleted file mode 100644 index 0a69b23b..00000000 --- a/lib/core/bitcoin_wallet.dart +++ /dev/null @@ -1,149 +0,0 @@ -import 'dart:convert'; -import 'dart:typed_data'; -import 'package:cake_wallet/core/bitcoin_transaction_history.dart'; -import 'package:cake_wallet/core/transaction_history.dart'; -import 'package:mobx/mobx.dart'; -import 'package:bip39/bip39.dart' as bip39; -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:cake_wallet/bitcoin/file.dart'; -import 'package:cake_wallet/src/domain/common/pathForWallet.dart'; -import 'package:cake_wallet/src/domain/common/wallet_type.dart'; -import 'package:cake_wallet/bitcoin/electrum.dart'; -import 'package:cake_wallet/bitcoin/bitcoin_balance.dart'; -import 'package:cake_wallet/src/domain/common/node.dart'; -import 'wallet_base.dart'; - -part 'bitcoin_wallet.g.dart'; - -/* TODO: Save balance to a wallet file. - Load balance from the wallet file in `init` method. -*/ - -class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet; - -abstract class BitcoinWalletBase extends WalletBase with Store { - static Future load( - {@required String name, @required String password}) async { - final walletDirPath = - await pathForWalletDir(name: name, type: WalletType.bitcoin); - final walletPath = '$walletDirPath/$name'; - final walletJSONRaw = await read(path: walletPath, password: password); - final jsoned = json.decode(walletJSONRaw) as Map; - final mnemonic = jsoned['mnemonic'] as String; - final accountIndex = - (jsoned['account_index'] == "null" || jsoned['account_index'] == null) - ? 0 - : int.parse(jsoned['account_index'] as String); - - return BitcoinWalletBase.build( - mnemonic: mnemonic, - password: password, - name: name, - accountIndex: accountIndex); - } - - factory BitcoinWalletBase.build( - {@required String mnemonic, - @required String password, - @required String name, - @required String dirPath, - int accountIndex = 0}) { - final walletPath = '$dirPath/$name'; - final eclient = ElectrumClient(); - final history = BitcoinTransactionHistory( - eclient: eclient, dirPath: dirPath, password: password); - - return BitcoinWallet._internal( - eclient: eclient, - path: walletPath, - mnemonic: mnemonic, - password: password, - accountIndex: accountIndex, - transactionHistory: history); - } - - BitcoinWalletBase._internal( - {@required this.eclient, - @required this.path, - @required String password, - int accountIndex = 0, - this.transactionHistory, - this.mnemonic}) { - hd = bitcoin.HDWallet.fromSeed(bip39.mnemonicToSeed(mnemonic), - network: bitcoin.bitcoin); - _password = password; - _accountIndex = accountIndex; - } - - final BitcoinTransactionHistory transactionHistory; - final String path; - bitcoin.HDWallet hd; - final ElectrumClient eclient; - final String mnemonic; - int _accountIndex; - String _password; - - @override - String get name => path.split('/').last ?? ''; - - @override - String get filename => hd.address; - - String get xpub => hd.base58; - - List getAddresses() => _accountIndex == 0 - ? [address] - : List.generate( - _accountIndex, (i) => _getAddress(hd: hd, index: i)); - - Future init() async { - await transactionHistory.init(); - } - - Future newAddress() async { - _accountIndex += 1; - final address = _getAddress(hd: hd, index: _accountIndex); - await save(); - - return address; - } - - @override - Future startSync() async {} - - @override - Future connectToNode({@required Node node}) async {} - - @override - Future createTransaction(Object credentials) async {} - - @override - Future save() async => await write( - path: path, - password: _password, - obj: {'mnemonic': mnemonic, 'account_index': _accountIndex.toString()}); - - String _getAddress({bitcoin.HDWallet hd, int index}) => bitcoin - .P2PKH( - data: PaymentData( - pubkey: Uint8List.fromList(hd.derive(index).pubKey.codeUnits))) - .data - .address; - - Future> _fetchBalances() async { - final balances = await Future.wait( - getAddresses().map((address) => eclient.getBalance(address: address))); - final balance = balances.fold({}, (Map acc, val) { - acc['confirmed'] = - (val['confirmed'] as int ?? 0) + (acc['confirmed'] ?? 0); - acc['unconfirmed'] = - (val['unconfirmed'] as int ?? 0) + (acc['unconfirmed'] ?? 0); - - return acc; - }); - - return balance; - } -} diff --git a/lib/core/bitcoin_wallet_list_service.dart b/lib/core/bitcoin_wallet_list_service.dart deleted file mode 100644 index 5bd19e9a..00000000 --- a/lib/core/bitcoin_wallet_list_service.dart +++ /dev/null @@ -1,103 +0,0 @@ -import 'dart:io'; -import 'package:bip39/bip39.dart' as bip39; -import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart'; -import 'package:cake_wallet/core/wallet_credentials.dart'; -import 'package:cake_wallet/core/wallet_list_service.dart'; -import 'package:cake_wallet/core/bitcoin_wallet.dart'; -import 'package:cake_wallet/src/domain/common/pathForWallet.dart'; -import 'package:cake_wallet/src/domain/common/wallet.dart'; -import 'package:cake_wallet/src/domain/common/wallet_description.dart'; -import 'package:cake_wallet/src/domain/common/wallet_type.dart'; -import 'package:cake_wallet/src/domain/common/wallets_manager.dart'; -/* -* -* BitcoinRestoreWalletFromSeedCredentials -* -* */ - -class BitcoinNewWalletCredentials extends WalletCredentials {} - -/* -* -* BitcoinRestoreWalletFromSeedCredentials -* -* */ - -class BitcoinRestoreWalletFromSeedCredentials extends WalletCredentials { - const BitcoinRestoreWalletFromSeedCredentials( - {String name, String password, this.mnemonic}) - : super(name: name, password: password); - - final String mnemonic; -} - -/* -* -* BitcoinRestoreWalletFromWIFCredentials -* -* */ - -class BitcoinRestoreWalletFromWIFCredentials extends WalletCredentials { - const BitcoinRestoreWalletFromWIFCredentials( - {String name, String password, this.wif}) - : super(name: name, password: password); - - final String wif; -} - -/* -* -* BitcoinWalletListService -* -* */ - -class BitcoinWalletListService extends WalletListService< - BitcoinNewWalletCredentials, - BitcoinRestoreWalletFromSeedCredentials, - BitcoinRestoreWalletFromWIFCredentials> { - @override - Future create(BitcoinNewWalletCredentials credentials) async { - final wallet = await BitcoinWalletBase.build( - mnemonic: bip39.generateMnemonic(), - password: credentials.password, - name: credentials.name); - await wallet.save(); - - return wallet; - } - - @override - Future isWalletExit(String name) async => - File(await pathForWallet(name: name, type: WalletType.bitcoin)) - .existsSync(); - - @override - Future openWallet(String name, String password) async { - // TODO: implement openWallet - throw UnimplementedError(); - } - - Future remove(String wallet) { - // TODO: implement remove - throw UnimplementedError(); - } - - @override - Future restoreFromKeys( - BitcoinRestoreWalletFromWIFCredentials credentials) async { - // TODO: implement restoreFromKeys - throw UnimplementedError(); - } - - @override - Future restoreFromSeed( - BitcoinRestoreWalletFromSeedCredentials credentials) async { - final wallet = await BitcoinWalletBase.build( - name: credentials.name, - password: credentials.password, - mnemonic: credentials.mnemonic); - await wallet.save(); - - return wallet; - } -} diff --git a/lib/core/generate_wallet_password.dart b/lib/core/generate_wallet_password.dart new file mode 100644 index 00000000..ea5b80d2 --- /dev/null +++ b/lib/core/generate_wallet_password.dart @@ -0,0 +1,12 @@ +import 'package:uuid/uuid.dart'; +import 'package:cake_wallet/bitcoin/key.dart'; +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; + +String generateWalletPassword(WalletType type) { + switch (type) { + case WalletType.bitcoin: + return generateKey(); + default: + return Uuid().v4(); + } +} diff --git a/lib/core/mnemonic_length.dart b/lib/core/mnemonic_length.dart new file mode 100644 index 00000000..84100d8a --- /dev/null +++ b/lib/core/mnemonic_length.dart @@ -0,0 +1,17 @@ +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; + +const bitcoinMnemonicLength = 12; +const moneroMnemonicLength = 25; + +int mnemonicLength(WalletType type) { + // TODO: need to have only one place for get(set) mnemonic string lenth; + + switch (type) { + case WalletType.monero: + return moneroMnemonicLength; + case WalletType.bitcoin: + return bitcoinMnemonicLength; + default: + return 0; + } +} \ No newline at end of file diff --git a/lib/core/seed_validator.dart b/lib/core/seed_validator.dart new file mode 100644 index 00000000..54b1ffea --- /dev/null +++ b/lib/core/seed_validator.dart @@ -0,0 +1,73 @@ +import 'package:bip39/src/wordlists/english.dart' as bitcoin_english; +import 'package:cake_wallet/core/validator.dart'; +import 'package:cake_wallet/src/domain/common/mnemonic_item.dart'; +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; +import 'package:cake_wallet/src/domain/monero/mnemonics/chinese_simplified.dart'; +import 'package:cake_wallet/src/domain/monero/mnemonics/dutch.dart'; +import 'package:cake_wallet/src/domain/monero/mnemonics/english.dart'; +import 'package:cake_wallet/src/domain/monero/mnemonics/german.dart'; +import 'package:cake_wallet/src/domain/monero/mnemonics/japanese.dart'; +import 'package:cake_wallet/src/domain/monero/mnemonics/portuguese.dart'; +import 'package:cake_wallet/src/domain/monero/mnemonics/russian.dart'; +import 'package:cake_wallet/src/domain/monero/mnemonics/spanish.dart'; + +class SeedValidator extends Validator { + SeedValidator({this.type, this.language}) + : _words = getWordList(type: type, language: language); + + final WalletType type; + final String language; + final List _words; + + static List getWordList({WalletType type, String language}) { + switch (type) { + case WalletType.bitcoin: + return getBitcoinWordList(language); + case WalletType.monero: + return getMoneroWordList(language); + default: + return []; + } + } + + static List getMoneroWordList(String language) { + // FIXME: Unnamed constants; Need to be sure that string are in same case; + + switch (language) { + case 'English': + return EnglishMnemonics.words; + break; + case 'Chinese (simplified)': + return ChineseSimplifiedMnemonics.words; + break; + case 'Dutch': + return DutchMnemonics.words; + break; + case 'German': + return GermanMnemonics.words; + break; + case 'Japanese': + return JapaneseMnemonics.words; + break; + case 'Portuguese': + return PortugueseMnemonics.words; + break; + case 'Russian': + return RussianMnemonics.words; + break; + case 'Spanish': + return SpanishMnemonics.words; + break; + default: + return EnglishMnemonics.words; + } + } + + static List getBitcoinWordList(String language) { + assert(language.toLowerCase() == 'english'); + return bitcoin_english.WORDLIST; + } + + @override + bool isValid(MnemonicItem value) => _words.contains(value.text); +} diff --git a/lib/core/setup_pin_code_state.dart b/lib/core/setup_pin_code_state.dart deleted file mode 100644 index 1d254c97..00000000 --- a/lib/core/setup_pin_code_state.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:flutter/foundation.dart'; - -abstract class SetupPinCodeState {} - -class InitialSetupPinCodeState extends SetupPinCodeState {} - -class SetupPinCodeInProgress extends SetupPinCodeState {} - -class SetupPinCodeFinishedSuccessfully extends SetupPinCodeState {} - -class SetupPinCodeFinishedFailure extends SetupPinCodeState { - SetupPinCodeFinishedFailure({@required this.error}); - - final String error; -} \ No newline at end of file diff --git a/lib/core/transaction_history.dart b/lib/core/transaction_history.dart index 7a9e6c53..78711905 100644 --- a/lib/core/transaction_history.dart +++ b/lib/core/transaction_history.dart @@ -1,10 +1,11 @@ import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/src/domain/common/transaction_info.dart'; -abstract class TranasctionHistoryBase { - TranasctionHistoryBase() : _isUpdating = false; +abstract class TransactionHistoryBase { + TransactionHistoryBase() : _isUpdating = false; @observable - List transactions; + ObservableList transactions; bool _isUpdating; @@ -15,7 +16,7 @@ abstract class TranasctionHistoryBase { try { _isUpdating = false; - transactions = await fetchTransactions(); + transactions.addAll(await fetchTransactions()); _isUpdating = true; } catch (e) { _isUpdating = false; diff --git a/lib/core/validator.dart b/lib/core/validator.dart index e69de29b..826100ab 100644 --- a/lib/core/validator.dart +++ b/lib/core/validator.dart @@ -0,0 +1,39 @@ +import 'package:flutter/foundation.dart'; + +abstract class Validator { + Validator({@required this.errorMessage}); + + final String errorMessage; + + bool isValid(T value); + + String call(T value) => !isValid(value) ? errorMessage : null; +} + +class TextValidator extends Validator { + TextValidator( + {this.minLength, this.maxLength, this.pattern, String errorMessage}) + : super(errorMessage: errorMessage); + + final int minLength; + final int maxLength; + String pattern; + + @override + bool isValid(String value) { + if (value == null || value.isEmpty) { + return true; + } + + return value.length > minLength && + (maxLength > 0 ? (value.length <= maxLength) : true) && + (pattern != null ? match(value) : true); + } + + bool match(String value) => RegExp(pattern).hasMatch(value); +} + +class WalletNameValidator extends TextValidator { + WalletNameValidator() + : super(minLength: 1, maxLength: 15, pattern: '^[a-zA-Z0-9_]\$'); +} diff --git a/lib/core/wallet_base.dart b/lib/core/wallet_base.dart index d1c14c70..6ee4d257 100644 --- a/lib/core/wallet_base.dart +++ b/lib/core/wallet_base.dart @@ -1,20 +1,24 @@ import 'package:flutter/foundation.dart'; -import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/core/transaction_history.dart'; import 'package:cake_wallet/src/domain/common/node.dart'; +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; abstract class WalletBase { - String get name; + WalletType type; - String get filename; + String get name; - @observable String address; - @observable BalaceType balance; + TransactionHistoryBase transactionHistory; + Future connectToNode({@required Node node}); + Future startSync(); + Future createTransaction(Object credentials); + Future save(); -} \ No newline at end of file +} diff --git a/lib/core/wallet_creation_service.dart b/lib/core/wallet_creation_service.dart index 129b6ad1..c020ce26 100644 --- a/lib/core/wallet_creation_service.dart +++ b/lib/core/wallet_creation_service.dart @@ -1,34 +1,47 @@ -import 'package:cake_wallet/core/wallet_creation_state.dart'; import 'package:flutter/foundation.dart'; -import 'package:mobx/mobx.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:cake_wallet/core/generate_wallet_password.dart'; +import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/core/wallet_credentials.dart'; -import 'package:cake_wallet/core/bitcoin_wallet_list_service.dart'; -import 'package:cake_wallet/core/monero_wallet_list_service.dart'; -import 'package:cake_wallet/core/wallet_list_service.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_wallet_service.dart'; +import 'package:cake_wallet/monero/monero_wallet_service.dart'; +import 'package:cake_wallet/core/wallet_service.dart'; import 'package:cake_wallet/src/domain/common/wallet_type.dart'; +import 'package:cake_wallet/src/domain/common/secret_store_key.dart'; +import 'package:cake_wallet/src/domain/common/encrypt.dart'; -part 'wallet_creation_service.g.dart'; - -class WalletCreationService = WalletCreationServiceBase - with _$WalletCreationService; - -abstract class WalletCreationServiceBase with Store { - @observable - WalletCreationState state; +class WalletCreationService { + WalletCreationService( + {WalletType initialType, + this.appStore, + this.secureStorage, + this.sharedPreferences}) + : type = initialType { + if (type != null) { + changeWalletType(type: type); + } + } WalletType type; + final AppStore appStore; + final FlutterSecureStorage secureStorage; + final SharedPreferences sharedPreferences; - WalletListService _service; +// final WalletService walletService; +// final Box walletInfoSource; + + WalletService _service; void changeWalletType({@required WalletType type}) { this.type = type; switch (type) { case WalletType.monero: - _service = MoneroWalletListService(); + _service = MoneroWalletService(); break; case WalletType.bitcoin: - _service = BitcoinWalletListService(); + _service = BitcoinWalletService(); break; default: break; @@ -36,32 +49,45 @@ abstract class WalletCreationServiceBase with Store { } Future create(WalletCredentials credentials) async { - try { - state = WalletCreating(); - await _service.create(credentials); - state = WalletCreatedSuccessfully(); - } catch (e) { - state = WalletCreationFailure(error: e.toString()); - } + final password = generateWalletPassword(type); + credentials.password = password; + await saveWalletPassword(password: password, walletName: credentials.name); + final wallet = await _service.create(credentials); + appStore.wallet = wallet; + appStore.authenticationStore.allowed(); } Future restoreFromKeys(WalletCredentials credentials) async { - try { - state = WalletCreating(); - await _service.restoreFromKeys(credentials); - state = WalletCreatedSuccessfully(); - } catch (e) { - state = WalletCreationFailure(error: e.toString()); - } + final password = generateWalletPassword(type); + credentials.password = password; + await saveWalletPassword(password: password, walletName: credentials.name); + final wallet = await _service.restoreFromKeys(credentials); + appStore.wallet = wallet; + appStore.authenticationStore.allowed(); } Future restoreFromSeed(WalletCredentials credentials) async { - try { - state = WalletCreating(); - await _service.restoreFromSeed(credentials); - state = WalletCreatedSuccessfully(); - } catch (e) { - state = WalletCreationFailure(error: e.toString()); - } + final password = generateWalletPassword(type); + credentials.password = password; + await saveWalletPassword(password: password, walletName: credentials.name); + final wallet = await _service.restoreFromSeed(credentials); + appStore.wallet = wallet; + appStore.authenticationStore.allowed(); + } + + Future getWalletPassword({String walletName}) async { + final key = generateStoreKeyFor( + key: SecretStoreKey.moneroWalletPassword, walletName: walletName); + final encodedPassword = await secureStorage.read(key: key); + + return decodeWalletPassword(password: encodedPassword); + } + + Future saveWalletPassword({String walletName, String password}) async { + final key = generateStoreKeyFor( + key: SecretStoreKey.moneroWalletPassword, walletName: walletName); + final encodedPassword = encodeWalletPassword(password: password); + + await secureStorage.write(key: key, value: encodedPassword); } } diff --git a/lib/core/wallet_credentials.dart b/lib/core/wallet_credentials.dart index e43acf9e..ee7b1ef5 100644 --- a/lib/core/wallet_credentials.dart +++ b/lib/core/wallet_credentials.dart @@ -1,6 +1,6 @@ abstract class WalletCredentials { - const WalletCredentials({this.name, this.password}); + WalletCredentials({this.name, this.password}); final String name; - final String password; + String password; } \ No newline at end of file diff --git a/lib/core/wallet_list_service.dart b/lib/core/wallet_list_service.dart deleted file mode 100644 index 7015545a..00000000 --- a/lib/core/wallet_list_service.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:cake_wallet/core/wallet_credentials.dart'; - -abstract class WalletListService { - Future create(N credentials); - - Future restoreFromSeed(RFS credentials); - - Future restoreFromKeys(RFK credentials); - - Future openWallet(String name, String password); - - Future isWalletExit(String name); - - Future remove(String wallet); -} diff --git a/lib/core/wallet_service.dart b/lib/core/wallet_service.dart new file mode 100644 index 00000000..f26fc10c --- /dev/null +++ b/lib/core/wallet_service.dart @@ -0,0 +1,17 @@ +import 'package:cake_wallet/core/wallet_base.dart'; +import 'package:cake_wallet/core/wallet_credentials.dart'; + +abstract class WalletService { + Future create(N credentials); + + Future restoreFromSeed(RFS credentials); + + Future restoreFromKeys(RFK credentials); + + Future openWallet(String name, String password); + + Future isWalletExit(String name); + + Future remove(String wallet); +} diff --git a/lib/di.dart b/lib/di.dart new file mode 100644 index 00000000..237b211b --- /dev/null +++ b/lib/di.dart @@ -0,0 +1,105 @@ +import 'package:cake_wallet/core/auth_service.dart'; +import 'package:cake_wallet/src/screens/auth/auth_page.dart'; +import 'package:cake_wallet/src/screens/dashboard/dashboard_page.dart'; +import 'package:cake_wallet/src/screens/receive/receive_page.dart'; +import 'package:cake_wallet/src/screens/subaddress/address_edit_or_create_page.dart'; +import 'package:cake_wallet/view_model/address_list/address_edit_or_create_view_model.dart'; +import 'package:cake_wallet/view_model/auth_view_model.dart'; +import 'package:cake_wallet/view_model/dashboard_view_model.dart'; +import 'package:cake_wallet/view_model/address_list/address_list_view_model.dart'; +import 'package:get_it/get_it.dart'; +import 'package:http/http.dart'; +import 'package:mobx/mobx.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:cake_wallet/view_model/wallet_restoration_from_seed_vm.dart'; +import 'package:cake_wallet/core/wallet_base.dart'; +import 'package:cake_wallet/core/wallet_creation_service.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; +import 'package:cake_wallet/view_model/wallet_new_vm.dart'; +import 'package:cake_wallet/store/authentication_store.dart'; + +final getIt = GetIt.instance; + +ReactionDisposer _onCurrentWalletChangeReaction; + +void setup() { + getIt.registerSingleton(AuthenticationStore()); + getIt.registerSingleton( + AppStore(authenticationStore: getIt.get())); + getIt.registerSingleton(FlutterSecureStorage()); + getIt.registerSingletonAsync( + () => SharedPreferences.getInstance()); + getIt.registerFactoryParam( + (type, _) => WalletCreationService( + initialType: type, + appStore: getIt.get(), + secureStorage: getIt.get(), + sharedPreferences: getIt.get())); + + getIt.registerFactoryParam((type, _) => + WalletNewVM(getIt.get(param1: type), type: type)); + + getIt + .registerFactoryParam((args, _) { + final type = args.first as WalletType; + final language = args[1] as String; + final mnemonic = args[2] as String; + + return WalletRestorationFromSeedVM( + getIt.get(param1: type), + type: type, + language: language, + seed: mnemonic); + }); + + getIt.registerFactory( + () => AddressListViewModel(wallet: getIt.get().wallet)); + + getIt.registerFactory( + () => DashboardViewModel(appStore: getIt.get())); + + getIt.registerFactory(() => AuthService( + secureStorage: getIt.get(), + sharedPreferences: getIt.get())); + + getIt.registerFactory(() => AuthViewModel( + authService: getIt.get(), + sharedPreferences: getIt.get())); + + getIt.registerFactory(() => AuthPage( + authViewModel: getIt.get(), + onAuthenticationFinished: (isAuthenticated, __) { + if (isAuthenticated) { + getIt.get().allowed(); + } + }, + closable: false)); + + getIt.registerFactory(() => DashboardPage( + walletViewModel: getIt.get(), + )); + + getIt.registerFactory(() => + ReceivePage(addressListViewModel: getIt.get())); + + getIt.registerFactoryParam( + (dynamic item, _) => AddressEditOrCreateViewModel( + wallet: getIt.get().wallet, item: item)); + + getIt.registerFactoryParam( + (dynamic item, _) => AddressEditOrCreatePage( + addressEditOrCreateViewModel: + getIt.get(param1: item))); + + final appStore = getIt.get(); + + _onCurrentWalletChangeReaction ??= + reaction((_) => appStore.wallet, (WalletBase wallet) async { + print('Wallet name ${wallet.name}'); + await getIt + .get() + .setString('current_wallet_name', wallet.name); + }); +} diff --git a/lib/main.dart b/lib/main.dart index 9f0dd47a..706c1f91 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,13 @@ -import 'package:cake_wallet/core/app_service.dart'; +import 'package:cake_wallet/reactions/bootstrap.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'; +import 'package:cake_wallet/monero/monero_wallet_service.dart'; import 'package:cake_wallet/core/wallet_creation_service.dart'; +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/view_model/wallet_new_vm.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:get_it/get_it.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; @@ -27,7 +33,8 @@ import 'package:cake_wallet/src/stores/wallet/wallet_store.dart'; import 'package:cake_wallet/src/stores/send_template/send_template_store.dart'; import 'package:cake_wallet/src/stores/exchange_template/exchange_template_store.dart'; import 'package:cake_wallet/src/screens/root/root.dart'; -import 'package:cake_wallet/src/stores/authentication/authentication_store.dart'; + +//import 'package:cake_wallet/src/stores/authentication/authentication_store.dart'; import 'package:cake_wallet/src/stores/settings/settings_store.dart'; import 'package:cake_wallet/src/stores/price/price_store.dart'; import 'package:cake_wallet/src/domain/services/user_service.dart'; @@ -47,6 +54,8 @@ import 'package:cake_wallet/src/stores/seed_language/seed_language_store.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + setup(); + final appDir = await getApplicationDocumentsDirectory(); Hive.init(appDir.path); Hive.registerAdapter(ContactAdapter()); @@ -87,13 +96,13 @@ void main() async { sharedPreferences: sharedPreferences); final userService = UserService( sharedPreferences: sharedPreferences, secureStorage: secureStorage); - final authenticationStore = AuthenticationStore(userService: userService); +// final authenticationStore = AuthenticationStore(userService: userService); await initialSetup( sharedPreferences: sharedPreferences, walletListService: walletListService, nodes: nodes, - authStore: authenticationStore, +// authStore: authenticationStore, initialMigrationVersion: 2); final settingsStore = await SettingsStoreBase.load( @@ -119,8 +128,7 @@ void main() async { final walletCreationService = WalletCreationService(); final authService = AuthService(); - final appStore = AppService( - walletCreationService: walletCreationService, authService: authService); + setReactions( settingsStore: settingsStore, @@ -128,7 +136,7 @@ void main() async { syncStore: syncStore, walletStore: walletStore, walletService: walletService, - authenticationStore: authenticationStore, +// authenticationStore: authenticationStore, loginStore: loginStore); runApp(MultiProvider(providers: [ @@ -141,7 +149,7 @@ void main() async { Provider(create: (_) => walletStore), Provider(create: (_) => syncStore), Provider(create: (_) => balanceStore), - Provider(create: (_) => authenticationStore), +// Provider(create: (_) => authenticationStore), Provider(create: (_) => contacts), Provider(create: (_) => nodes), Provider(create: (_) => transactionDescriptions), @@ -149,7 +157,7 @@ void main() async { Provider(create: (_) => seedLanguageStore), Provider(create: (_) => sendTemplateStore), Provider(create: (_) => exchangeTemplateStore), - Provider(create: (_) => appStore), +// Provider(create: (_) => appStore), Provider(create: (_) => walletCreationService), Provider(create: (_) => authService) ], child: CakeWalletApp())); @@ -159,7 +167,7 @@ Future initialSetup( {WalletListService walletListService, SharedPreferences sharedPreferences, Box nodes, - AuthenticationStore authStore, +// AuthenticationStore authStore, int initialMigrationVersion = 1, WalletType initialWalletType = WalletType.bitcoin}) async { await walletListService.changeWalletManger(walletType: initialWalletType); @@ -167,7 +175,12 @@ Future initialSetup( version: initialMigrationVersion, sharedPreferences: sharedPreferences, nodes: nodes); - await authStore.started(); +// await authStore.started(); + await bootstrap(); +// final authenticationStore = getIt.get(); + // FIXME +// authenticationStore.state = AuthenticationState.denied; + monero_wallet.onStartup(); } @@ -241,6 +254,8 @@ class MaterialAppWithTheme extends StatelessWidget { nodes: nodes, trades: trades, transactionDescriptions: transactionDescriptions), - home: Root()); + home: Root( + authenticationStore: getIt.get(), + )); } } diff --git a/lib/core/monero_balance.dart b/lib/monero/monero_balance.dart similarity index 100% rename from lib/core/monero_balance.dart rename to lib/monero/monero_balance.dart diff --git a/lib/monero/monero_subaddress_list.dart b/lib/monero/monero_subaddress_list.dart new file mode 100644 index 00000000..286a2670 --- /dev/null +++ b/lib/monero/monero_subaddress_list.dart @@ -0,0 +1,76 @@ +import 'package:flutter/services.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cw_monero/subaddress_list.dart' as subaddress_list; +import 'package:cake_wallet/src/domain/monero/subaddress.dart'; + +part 'monero_subaddress_list.g.dart'; + +class MoneroSubaddressList = MoneroSubaddressListBase + with _$MoneroSubaddressList; + +abstract class MoneroSubaddressListBase with Store { + MoneroSubaddressListBase() { + _isRefreshing = false; + _isUpdating = false; + subaddresses = ObservableList(); + } + + @observable + ObservableList subaddresses; + + bool _isRefreshing; + bool _isUpdating; + + void update({int accountIndex}) { + if (_isUpdating) { + return; + } + + try { + _isUpdating = true; + refresh(accountIndex: accountIndex); + subaddresses.clear(); + subaddresses.addAll(getAll()); + _isUpdating = false; + } catch (e) { + _isUpdating = false; + rethrow; + } + } + + List getAll() { + return subaddress_list + .getAllSubaddresses() + .map((subaddressRow) => Subaddress.fromRow(subaddressRow)) + .toList(); + } + + Future addSubaddress({int accountIndex, String label}) async { + await subaddress_list.addSubaddress( + accountIndex: accountIndex, label: label); + update(accountIndex: accountIndex); + } + + Future setLabelSubaddress( + {int accountIndex, int addressIndex, String label}) async { + await subaddress_list.setLabelForSubaddress( + accountIndex: accountIndex, addressIndex: addressIndex, label: label); + update(accountIndex: accountIndex); + } + + void refresh({int accountIndex}) { + if (_isRefreshing) { + return; + } + + try { + _isRefreshing = true; + subaddress_list.refreshSubaddresses(accountIndex: accountIndex); + _isRefreshing = false; + } on PlatformException catch (e) { + _isRefreshing = false; + print(e); + rethrow; + } + } +} diff --git a/lib/core/monero_transaction_history.dart b/lib/monero/monero_transaction_history.dart similarity index 72% rename from lib/core/monero_transaction_history.dart rename to lib/monero/monero_transaction_history.dart index 03b48324..7eeeef42 100644 --- a/lib/core/monero_transaction_history.dart +++ b/lib/monero/monero_transaction_history.dart @@ -8,7 +8,7 @@ import 'package:cake_wallet/src/domain/monero/monero_transaction_info.dart'; part 'monero_transaction_history.g.dart'; -List _getAllTransactions(dynamic _) => +List _getAllTransactions(dynamic _) => monero_transaction_history .getAllTransations() .map((row) => MoneroTransactionInfo.fromRow(row)) @@ -18,9 +18,13 @@ class MoneroTransactionHistory = MoneroTransactionHistoryBase with _$MoneroTransactionHistory; abstract class MoneroTransactionHistoryBase - extends TranasctionHistoryBase with Store { + extends TransactionHistoryBase with Store { + MoneroTransactionHistoryBase() { + transactions = ObservableList(); + } + @override - Future> fetchTransactions() async { + Future> fetchTransactions() async { monero_transaction_history.refreshTransactions(); return _getAllTransactions(null); } diff --git a/lib/core/monero_wallet.dart b/lib/monero/monero_wallet.dart similarity index 81% rename from lib/core/monero_wallet.dart rename to lib/monero/monero_wallet.dart index 1b3fcd5a..30fb0091 100644 --- a/lib/core/monero_wallet.dart +++ b/lib/monero/monero_wallet.dart @@ -1,35 +1,36 @@ -import 'package:cake_wallet/core/monero_balance.dart'; -import 'package:cake_wallet/core/monero_transaction_history.dart'; +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; +import 'package:flutter/foundation.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/monero/monero_balance.dart'; +import 'package:cake_wallet/monero/monero_transaction_history.dart'; +import 'package:cake_wallet/monero/monero_subaddress_list.dart'; +import 'package:cake_wallet/core/wallet_base.dart'; +import 'package:cake_wallet/core/transaction_history.dart'; import 'package:cake_wallet/src/domain/common/sync_status.dart'; import 'package:cake_wallet/src/domain/monero/account.dart'; import 'package:cake_wallet/src/domain/monero/account_list.dart'; import 'package:cake_wallet/src/domain/monero/subaddress.dart'; -import 'package:cake_wallet/src/domain/monero/subaddress_list.dart'; import 'package:cw_monero/wallet.dart'; -import 'package:flutter/foundation.dart'; -import 'package:mobx/mobx.dart'; import 'package:cake_wallet/src/domain/common/node.dart'; import 'package:cw_monero/wallet.dart' as monero_wallet; -import 'wallet_base.dart'; part 'monero_wallet.g.dart'; class MoneroWallet = MoneroWalletBase with _$MoneroWallet; abstract class MoneroWalletBase extends WalletBase with Store { - MoneroWalletBase({String filename, this.isRecovery = false}) { - transactionHistory = MoneroTransactionHistory(); + MoneroWalletBase({String filename, this.isRecovery = false}) + : transactionHistory = MoneroTransactionHistory() { _filename = filename; accountList = AccountList(); - subaddressList = SubaddressList(); + subaddressList = MoneroSubaddressList(); balance = MoneroBalance( fullBalance: monero_wallet.getFullBalance(accountIndex: 0), unlockedBalance: monero_wallet.getFullBalance(accountIndex: 0)); } - MoneroTransactionHistory transactionHistory; - SubaddressList subaddressList; - AccountList accountList; + @override + final MoneroTransactionHistory transactionHistory; @observable Account account; @@ -41,30 +42,40 @@ abstract class MoneroWalletBase extends WalletBase with Store { SyncStatus syncStatus; @override - String get name => filename.split('/').last; + String get name => _filename.split('/').last; @override - String get filename => _filename; + final type = WalletType.monero; - String _filename; + @override + @observable + String address; bool isRecovery; - SyncListner _listner; + MoneroSubaddressList subaddressList; + + AccountList accountList; + + String _filename; + + SyncListner _listener; - void init() { + Future init() async { + await accountList.update(); account = accountList.getAll().first; - subaddressList.refresh(accountIndex: account.id ?? 0); + subaddressList.update(accountIndex: account.id ?? 0); subaddress = subaddressList.getAll().first; balance = MoneroBalance( fullBalance: monero_wallet.getFullBalance(accountIndex: account.id), unlockedBalance: monero_wallet.getFullBalance(accountIndex: account.id)); + address = subaddress.address; _setListeners(); } void close() { - _listner?.stop(); + _listener?.stop(); } @override @@ -133,8 +144,8 @@ abstract class MoneroWalletBase extends WalletBase with Store { Future isConnected() async => monero_wallet.isConnected(); void _setListeners() { - _listner?.stop(); - _listner = monero_wallet.setListeners( + _listener?.stop(); + _listener = monero_wallet.setListeners( _onNewBlock, _onNeedToRefresh, _onNewTransaction); } diff --git a/lib/core/monero_wallet_list_service.dart b/lib/monero/monero_wallet_service.dart similarity index 78% rename from lib/core/monero_wallet_list_service.dart rename to lib/monero/monero_wallet_service.dart index c87cd75a..1c8d3357 100644 --- a/lib/core/monero_wallet_list_service.dart +++ b/lib/monero/monero_wallet_service.dart @@ -1,21 +1,20 @@ -import 'package:cake_wallet/core/monero_wallet.dart'; +import 'package:cake_wallet/monero/monero_wallet.dart'; import 'package:cake_wallet/core/wallet_credentials.dart'; -import 'package:cake_wallet/core/wallet_list_service.dart'; +import 'package:cake_wallet/core/wallet_service.dart'; import 'package:cake_wallet/src/domain/common/pathForWallet.dart'; import 'package:cake_wallet/src/domain/common/wallet_type.dart'; import 'package:cw_monero/wallet_manager.dart' as monero_wallet_manager; import 'package:cw_monero/wallet.dart' as monero_wallet; class MoneroNewWalletCredentials extends WalletCredentials { - const MoneroNewWalletCredentials( - {String name, String password, this.language}) + MoneroNewWalletCredentials({String name, String password, this.language}) : super(name: name, password: password); final String language; } class MoneroRestoreWalletFromSeedCredentials extends WalletCredentials { - const MoneroRestoreWalletFromSeedCredentials( + MoneroRestoreWalletFromSeedCredentials( {String name, String password, this.mnemonic, this.height}) : super(name: name, password: password); @@ -24,7 +23,7 @@ class MoneroRestoreWalletFromSeedCredentials extends WalletCredentials { } class MoneroRestoreWalletFromKeysCredentials extends WalletCredentials { - const MoneroRestoreWalletFromKeysCredentials( + MoneroRestoreWalletFromKeysCredentials( {String name, String password, this.language, @@ -41,12 +40,12 @@ class MoneroRestoreWalletFromKeysCredentials extends WalletCredentials { final int height; } -class MoneroWalletListService extends WalletListService< +class MoneroWalletService extends WalletService< MoneroNewWalletCredentials, MoneroRestoreWalletFromSeedCredentials, MoneroRestoreWalletFromKeysCredentials> { @override - Future create(MoneroNewWalletCredentials credentials) async { + Future create(MoneroNewWalletCredentials credentials) async { try { final path = await pathForWallet(name: credentials.name, type: WalletType.monero); @@ -56,7 +55,10 @@ class MoneroWalletListService extends WalletListService< password: credentials.password, language: credentials.language); - return MoneroWallet(filename: monero_wallet.getFilename())..init(); + final wallet = MoneroWallet(filename: monero_wallet.getFilename()); + await wallet.init(); + + return wallet; } catch (e) { // TODO: Implement Exception fop wallet list service. print('MoneroWalletsManager Error: $e'); @@ -77,7 +79,7 @@ class MoneroWalletListService extends WalletListService< } @override - Future openWallet(String name, String password) async { + Future openWallet(String name, String password) async { try { final path = await pathForWallet(name: name, type: WalletType.monero); monero_wallet_manager.openWallet(path: path, password: password); @@ -86,7 +88,10 @@ class MoneroWalletListService extends WalletListService< // final walletInfo = walletInfoSource.values // .firstWhere((info) => info.id == id, orElse: () => null); - return MoneroWallet(filename: monero_wallet.getFilename())..init(); + final wallet = MoneroWallet(filename: monero_wallet.getFilename()); + await wallet.init(); + + return wallet; } catch (e) { // TODO: Implement Exception fop wallet list service. print('MoneroWalletsManager Error: $e'); @@ -100,7 +105,7 @@ class MoneroWalletListService extends WalletListService< } @override - Future restoreFromKeys( + Future restoreFromKeys( MoneroRestoreWalletFromKeysCredentials credentials) async { try { final path = @@ -115,7 +120,10 @@ class MoneroWalletListService extends WalletListService< viewKey: credentials.viewKey, spendKey: credentials.spendKey); - return MoneroWallet(filename: monero_wallet.getFilename())..init(); + final wallet = MoneroWallet(filename: monero_wallet.getFilename()); + await wallet.init(); + + return wallet; } catch (e) { // TODO: Implement Exception fop wallet list service. print('MoneroWalletsManager Error: $e'); @@ -124,7 +132,7 @@ class MoneroWalletListService extends WalletListService< } @override - Future restoreFromSeed( + Future restoreFromSeed( MoneroRestoreWalletFromSeedCredentials credentials) async { try { final path = @@ -136,7 +144,10 @@ class MoneroWalletListService extends WalletListService< seed: credentials.mnemonic, restoreHeight: credentials.height); - return MoneroWallet(filename: monero_wallet.getFilename())..init(); + final wallet = MoneroWallet(filename: monero_wallet.getFilename()); + await wallet.init(); + + return wallet; } catch (e) { // TODO: Implement Exception fop wallet list service. print('MoneroWalletsManager Error: $e'); diff --git a/lib/palette.dart b/lib/palette.dart index 766d9fe7..8d06a823 100644 --- a/lib/palette.dart +++ b/lib/palette.dart @@ -12,6 +12,7 @@ class Palette { static const Color blue = Color.fromRGBO(88, 143, 252, 1.0); static const Color darkLavender = Color.fromRGBO(225, 238, 250, 1.0); static const Color nightBlue = Color.fromRGBO(46, 57, 96, 1.0); + static const Color eee = Color.fromRGBO(236, 239, 245, 1.0); } class PaletteDark { diff --git a/lib/reactions/bootstrap.dart b/lib/reactions/bootstrap.dart new file mode 100644 index 00000000..36612d70 --- /dev/null +++ b/lib/reactions/bootstrap.dart @@ -0,0 +1,66 @@ +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/di.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_wallet_service.dart'; +import 'package:cake_wallet/monero/monero_wallet_service.dart'; +import 'package:cake_wallet/core/wallet_service.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/store/authentication_store.dart'; +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; +import 'package:cake_wallet/src/domain/common/secret_store_key.dart'; +import 'package:cake_wallet/src/domain/common/encrypt.dart'; + +// FIXME: move me +Future getWalletPassword({String walletName}) async { + final secureStorage = getIt.get(); + final key = generateStoreKeyFor( + key: SecretStoreKey.moneroWalletPassword, walletName: walletName); + final encodedPassword = await secureStorage.read(key: key); + + return decodeWalletPassword(password: encodedPassword); +} + +// FIXME: move me +Future loadCurrentWallet() async { + final appStore = getIt.get(); + final name = getIt.get().getString('current_wallet_name'); + final type = WalletType.monero; // FIXME + final password = await getWalletPassword(walletName: name); + + WalletService _service; + switch (type) { + case WalletType.monero: + _service = MoneroWalletService(); + break; + case WalletType.bitcoin: + _service = BitcoinWalletService(); + break; + default: + break; + } + + final wallet = await _service.openWallet(name, password); + appStore.wallet = wallet; +} + +ReactionDisposer _initialAuthReaction; + +Future bootstrap() async { + final authenticationStore = getIt.get(); + + if (authenticationStore.state == AuthenticationState.uninitialized) { + authenticationStore.state = + getIt.get().getString('current_wallet_name') == null + ? AuthenticationState.denied + : AuthenticationState.installed; + } + + _initialAuthReaction ??= autorun((_) async { + final state = authenticationStore.state; + + if (state == AuthenticationState.installed) { + await loadCurrentWallet(); + } + }); +} diff --git a/lib/router.dart b/lib/router.dart index 0a42d7e4..cc261500 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -1,3 +1,5 @@ +import 'package:cake_wallet/view_model/wallet_new_vm.dart'; +import 'package:cake_wallet/view_model/wallet_restoration_from_seed_vm.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; @@ -6,7 +8,7 @@ import 'package:provider/provider.dart'; import 'package:hive/hive.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/generated/i18n.dart'; - +import 'di.dart'; // MARK: Import domains import 'package:cake_wallet/src/domain/common/contact.dart'; @@ -21,7 +23,7 @@ import 'package:cake_wallet/src/domain/common/node.dart'; import 'package:cake_wallet/src/domain/monero/transaction_description.dart'; import 'package:cake_wallet/src/domain/exchange/trade.dart'; import 'package:cake_wallet/src/domain/monero/account.dart'; -import 'package:cake_wallet/src/domain/common/mnemotic_item.dart'; +import 'package:cake_wallet/src/domain/common/mnemonic_item.dart'; import 'package:cake_wallet/src/domain/common/transaction_info.dart'; import 'package:cake_wallet/src/domain/monero/subaddress.dart'; import 'package:cake_wallet/src/domain/common/wallet_type.dart'; @@ -56,7 +58,7 @@ import 'package:cake_wallet/src/screens/auth/auth_page.dart'; import 'package:cake_wallet/src/screens/nodes/new_node_page.dart'; import 'package:cake_wallet/src/screens/nodes/nodes_list_page.dart'; import 'package:cake_wallet/src/screens/receive/receive_page.dart'; -import 'package:cake_wallet/src/screens/subaddress/new_subaddress_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/src/screens/new_wallet/new_wallet_page.dart'; import 'package:cake_wallet/src/screens/setup_pin_code/setup_pin_code.dart'; @@ -126,24 +128,18 @@ class Router { Navigator.pushNamed(context, Routes.newWalletType)))); case Routes.newWalletType: - return CupertinoPageRoute(builder: (_) => NewWalletTypePage()); + return CupertinoPageRoute( + builder: (_) => NewWalletTypePage( + onTypeSelected: (context, type) => Navigator.of(context) + .pushNamed(Routes.newWallet, arguments: type), + )); case Routes.newWallet: final type = settings.arguments as WalletType; - walletListService.changeWalletManger(walletType: type); + final walletNewVM = getIt.get(param1: type); return CupertinoPageRoute( - builder: - (_) => - ProxyProvider( - update: (_, authStore, __) => WalletCreationStore( - authStore: authStore, - sharedPreferences: sharedPreferences, - walletListService: walletListService), - child: NewWalletPage( - walletsService: walletListService, - walletService: walletService, - sharedPreferences: sharedPreferences))); + builder: (_) => NewWalletPage(walletNewVM)); case Routes.setupPin: Function(BuildContext, String) callback; @@ -163,6 +159,13 @@ class Router { callback == null ? null : callback(context, pin))), fullscreenDialog: true); + case Routes.restoreWalletType: + return CupertinoPageRoute( + builder: (_) => NewWalletTypePage( + onTypeSelected: (context, type) => Navigator.of(context) + .pushNamed(Routes.restoreWalletOptions, arguments: type), + )); + case Routes.restoreOptions: final type = settings.arguments as WalletType; walletListService.changeWalletManger(walletType: type); @@ -175,7 +178,28 @@ class Router { walletListService.changeWalletManger(walletType: type); return CupertinoPageRoute( - builder: (_) => RestoreWalletOptionsPage(type: type)); + builder: (_) => RestoreWalletOptionsPage( + type: type, + onRestoreFromSeed: (context) { + final route = type == WalletType.monero + ? Routes.seedLanguage + : Routes.restoreWalletFromSeed; + final args = type == WalletType.monero + ? [type, Routes.restoreWalletFromSeed] + : [type]; + + Navigator.of(context).pushNamed(route, arguments: args); + }, + onRestoreFromKeys: (context) { + final route = type == WalletType.monero + ? Routes.seedLanguage + : Routes.restoreWalletFromKeys; + final args = type == WalletType.monero + ? [type, Routes.restoreWalletFromSeed] + : [type]; + + Navigator.of(context).pushNamed(route, arguments: args); + })); case Routes.restoreWalletOptionsFromWelcome: return CupertinoPageRoute( @@ -186,7 +210,7 @@ class Router { sharedPreferences: sharedPreferences)), child: SetupPinCodePage( onPinCodeSetup: (context, _) => Navigator.pushNamed( - context, Routes.restoreWalletOptions)))); + context, Routes.restoreWalletType)))); case Routes.seed: return MaterialPageRoute( @@ -196,8 +220,11 @@ class Router { callback: settings.arguments as void Function())); case Routes.restoreWalletFromSeed: - final type = settings.arguments as WalletType; - walletListService.changeWalletManger(walletType: type); + final args = settings.arguments as List; + final type = args.first as WalletType; + final language = type == WalletType.monero + ? args[1] as String + : 'English'; // FIXME: Unnamed constant; English default and only one language for bitcoin. return CupertinoPageRoute( builder: (_) => @@ -207,11 +234,15 @@ class Router { sharedPreferences: sharedPreferences, walletListService: walletListService), child: RestoreWalletFromSeedPage( - walletsService: walletListService, - walletService: walletService, - sharedPreferences: sharedPreferences))); + type: type, language: language))); case Routes.restoreWalletFromKeys: + final args = settings.arguments as List; + final type = args.first as WalletType; + final language = type == WalletType.monero + ? args[1] as String + : 'English'; // FIXME: Unnamed constant; English default and only one language for bitcoin. + return CupertinoPageRoute( builder: (_) => ProxyProvider( @@ -256,22 +287,16 @@ class Router { case Routes.sendTemplate: return CupertinoPageRoute( - builder: (_) => Provider( - create: (_) => SendStore( - walletService: walletService, - priceStore: priceStore, - transactionDescriptions: transactionDescriptions), - child: SendTemplatePage()) - ); + builder: (_) => Provider( + create: (_) => SendStore( + walletService: walletService, + priceStore: priceStore, + transactionDescriptions: transactionDescriptions), + child: SendTemplatePage())); case Routes.receive: return CupertinoPageRoute( - fullscreenDialog: true, - builder: (_) => MultiProvider(providers: [ - Provider( - create: (_) => - SubaddressListStore(walletService: walletService)) - ], child: ReceivePage())); + fullscreenDialog: true, builder: (_) => getIt.get()); case Routes.transactionDetails: return CupertinoPageRoute( @@ -281,10 +306,8 @@ class Router { case Routes.newSubaddress: return CupertinoPageRoute( - builder: (_) => Provider( - create: (_) => - SubadrressCreationStore(walletService: walletService), - child: NewSubaddressPage())); + builder: (_) => + getIt.get(param1: settings.arguments)); case Routes.disclaimer: return CupertinoPageRoute(builder: (_) => DisclaimerPage()); @@ -294,7 +317,15 @@ class Router { builder: (_) => DisclaimerPage(isReadOnly: true)); case Routes.seedLanguage: - return CupertinoPageRoute(builder: (_) => SeedLanguage()); + final args = settings.arguments as List; + final type = args.first as WalletType; + final redirectRoute = args[1] as String; + + return CupertinoPageRoute(builder: (_) { + return SeedLanguage( + onConfirm: (context, lang) => Navigator.of(context) + .popAndPushNamed(redirectRoute, arguments: [type, lang])); + }); case Routes.walletList: return MaterialPageRoute( @@ -306,17 +337,18 @@ class Router { child: WalletListPage())); case Routes.auth: - return MaterialPageRoute( - fullscreenDialog: true, - builder: (_) => Provider( - create: (_) => AuthStore( - sharedPreferences: sharedPreferences, - userService: userService, - walletService: walletService), - child: AuthPage( - onAuthenticationFinished: - settings.arguments as OnAuthenticationFinished), - )); + return null; +// return MaterialPageRoute( +// fullscreenDialog: true, +// builder: (_) => Provider( +// create: (_) => AuthStore( +// sharedPreferences: sharedPreferences, +// userService: userService, +// walletService: walletService), +// child: AuthPage( +// onAuthenticationFinished: +// settings.arguments as OnAuthenticationFinished), +// )); case Routes.unlock: return MaterialPageRoute( @@ -455,15 +487,13 @@ class Router { ], child: SubaddressListPage())); case Routes.restoreWalletFromSeedDetails: + final args = settings.arguments as List; + final walletRestorationFromSeedVM = + getIt.get(param1: args); + return CupertinoPageRoute( - builder: (_) => - ProxyProvider( - update: (_, authStore, __) => WalletRestorationStore( - authStore: authStore, - sharedPreferences: sharedPreferences, - walletListService: walletListService, - seed: settings.arguments as List), - child: RestoreWalletFromSeedDetailsPage())); + builder: (_) => RestoreWalletFromSeedDetailsPage( + walletRestorationFromSeedVM: walletRestorationFromSeedVM)); case Routes.exchange: return MaterialPageRoute( @@ -487,22 +517,24 @@ class Router { case Routes.exchangeTemplate: return MaterialPageRoute( - builder: (_) => Provider(create: (_) { - final xmrtoprovider = XMRTOExchangeProvider(); - - return ExchangeStore( - initialProvider: xmrtoprovider, - initialDepositCurrency: CryptoCurrency.xmr, - initialReceiveCurrency: CryptoCurrency.btc, - trades: trades, - providerList: [ - xmrtoprovider, - ChangeNowExchangeProvider(), - MorphTokenExchangeProvider(trades: trades) - ], - walletStore: walletStore); - }, child: ExchangeTemplatePage(),) - ); + builder: (_) => Provider( + create: (_) { + final xmrtoprovider = XMRTOExchangeProvider(); + + return ExchangeStore( + initialProvider: xmrtoprovider, + initialDepositCurrency: CryptoCurrency.xmr, + initialReceiveCurrency: CryptoCurrency.btc, + trades: trades, + providerList: [ + xmrtoprovider, + ChangeNowExchangeProvider(), + MorphTokenExchangeProvider(trades: trades) + ], + walletStore: walletStore); + }, + child: ExchangeTemplatePage(), + )); case Routes.settings: return MaterialPageRoute( diff --git a/lib/routes.dart b/lib/routes.dart index a84b7e28..79c086d3 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -46,4 +46,5 @@ class Routes { static const newWalletType = '/new_wallet_type'; static const sendTemplate = '/send_template'; static const exchangeTemplate = '/exchange_template'; + static const restoreWalletType = '/restore_wallet_type'; } \ No newline at end of file diff --git a/lib/src/domain/common/balance.dart b/lib/src/domain/common/balance.dart index 2f53e248..e8564c5a 100644 --- a/lib/src/domain/common/balance.dart +++ b/lib/src/domain/common/balance.dart @@ -1 +1,3 @@ -abstract class Balance {} \ No newline at end of file +abstract class Balance { + const Balance(); +} diff --git a/lib/src/domain/common/mnemonic_item.dart b/lib/src/domain/common/mnemonic_item.dart new file mode 100644 index 00000000..0d22f79f --- /dev/null +++ b/lib/src/domain/common/mnemonic_item.dart @@ -0,0 +1,11 @@ +class MnemonicItem { + MnemonicItem({String text}) : _text = text; + + String get text => _text; + String _text; + + void changeText(String text) => _text = text; + + @override + String toString() => text; +} diff --git a/lib/src/domain/common/mnemotic_item.dart b/lib/src/domain/common/mnemotic_item.dart deleted file mode 100644 index 6f7690b4..00000000 --- a/lib/src/domain/common/mnemotic_item.dart +++ /dev/null @@ -1,17 +0,0 @@ -class MnemoticItem { - MnemoticItem({String text, this.dic}) : _text = text; - - String get text => _text; - final List dic; - - String _text; - - bool isCorrect() => dic.contains(text); - - void changeText(String text) { - _text = text; - } - - @override - String toString() => text; -} diff --git a/lib/src/domain/services/wallet_list_service.dart b/lib/src/domain/services/wallet_list_service.dart index 6d05c932..28cb2b0a 100644 --- a/lib/src/domain/services/wallet_list_service.dart +++ b/lib/src/domain/services/wallet_list_service.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'package:cake_wallet/bitcoin/bitcoin_wallet.manager.dart'; import 'package:cake_wallet/bitcoin/key.dart'; import 'package:cake_wallet/src/domain/common/wallet_info.dart'; import 'package:flutter/foundation.dart'; @@ -119,7 +118,7 @@ class WalletListService { MoneroWalletsManager(walletInfoSource: walletInfoSource); break; case WalletType.bitcoin: - walletsManager = BitcoinWalletManager(); +// walletsManager = BitcoinWalletManager(); break; case WalletType.none: walletsManager = null; diff --git a/lib/src/reactions/set_reactions.dart b/lib/src/reactions/set_reactions.dart index 2351bd0b..a8cd25dc 100644 --- a/lib/src/reactions/set_reactions.dart +++ b/lib/src/reactions/set_reactions.dart @@ -35,10 +35,10 @@ void setReactions( settingsStore: settingsStore, priceStore: priceStore); autorun((_) async { - if (authenticationStore.state == AuthenticationState.allowed) { - await loginStore.loadCurrentWallet(); - authenticationStore.state = AuthenticationState.readyToLogin; - } +// if (authenticationStore.state == AuthenticationState.allowed) { +// await loginStore.loadCurrentWallet(); +// authenticationStore.state = AuthenticationState.readyToLogin; +// } }); } diff --git a/lib/src/screens/auth/auth_page.dart b/lib/src/screens/auth/auth_page.dart index 0483af63..346e153e 100644 --- a/lib/src/screens/auth/auth_page.dart +++ b/lib/src/screens/auth/auth_page.dart @@ -1,10 +1,9 @@ import 'package:mobx/mobx.dart'; -import 'package:provider/provider.dart'; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/stores/auth/auth_state.dart'; -import 'package:cake_wallet/src/stores/auth/auth_store.dart'; +import 'package:cake_wallet/view_model/auth_state.dart'; +import 'package:cake_wallet/view_model/auth_view_model.dart'; import 'package:cake_wallet/src/screens/pin_code/pin_code.dart'; import 'package:cake_wallet/src/stores/settings/settings_store.dart'; import 'package:cake_wallet/src/domain/common/biometric_auth.dart'; @@ -12,8 +11,12 @@ import 'package:cake_wallet/src/domain/common/biometric_auth.dart'; typedef OnAuthenticationFinished = void Function(bool, AuthPageState); class AuthPage extends StatefulWidget { - AuthPage({this.onAuthenticationFinished, this.closable = true}); + AuthPage( + {this.onAuthenticationFinished, + this.authViewModel, + this.closable = true}); + final AuthViewModel authViewModel; final OnAuthenticationFinished onAuthenticationFinished; final bool closable; @@ -25,40 +28,13 @@ class AuthPageState extends State { final _key = GlobalKey(); final _pinCodeKey = GlobalKey(); final _backArrowImageDarkTheme = - Image.asset('assets/images/back_arrow_dark_theme.png'); - - void changeProcessText(String text) { - _key.currentState.showSnackBar( - SnackBar(content: Text(text), backgroundColor: Colors.green)); - } - - void close() => Navigator.of(_key.currentContext).pop(); + Image.asset('assets/images/back_arrow_dark_theme.png'); + ReactionDisposer _reaction; @override - Widget build(BuildContext context) { - final authStore = Provider.of(context); - final settingsStore = Provider.of(context); - - if (settingsStore.allowBiometricalAuthentication) { - WidgetsBinding.instance.addPostFrameCallback((_) { - final biometricAuth = BiometricAuth(); - biometricAuth.isAuthenticated().then( - (isAuth) { - if (isAuth) { - authStore.biometricAuth(); - _key.currentState.showSnackBar( - SnackBar( - content: Text(S.of(context).authenticated), - backgroundColor: Colors.green, - ), - ); - } - } - ); - }); - } - - reaction((_) => authStore.state, (AuthState state) { + void initState() { + _reaction ??= + reaction((_) => widget.authViewModel.state, (AuthState state) { if (state is AuthenticatedSuccessfully) { WidgetsBinding.instance.addPostFrameCallback((_) { if (widget.onAuthenticationFinished != null) { @@ -119,32 +95,69 @@ class AuthPageState extends State { }); } }); + super.initState(); + } + + @override + void dispose() { + _reaction.reaction.dispose(); + super.dispose(); + } + + void changeProcessText(String text) => _key.currentState.showSnackBar( + SnackBar(content: Text(text), backgroundColor: Colors.green)); + + void close() => Navigator.of(_key.currentContext).pop(); + + @override + Widget build(BuildContext context) { +// final authStore = Provider.of(context); +// final settingsStore = Provider.of(context); + +// if (settingsStore.allowBiometricalAuthentication) { +// WidgetsBinding.instance.addPostFrameCallback((_) { +// final biometricAuth = BiometricAuth(); +// biometricAuth.isAuthenticated().then( +// (isAuth) { +// if (isAuth) { +// authStore.biometricAuth(); +// _key.currentState.showSnackBar( +// SnackBar( +// content: Text(S.of(context).authenticated), +// backgroundColor: Colors.green, +// ), +// ); +// } +// } +// ); +// }); +// } return Scaffold( key: _key, appBar: CupertinoNavigationBar( leading: widget.closable - ? SizedBox( - height: 37, - width: 20, - child: ButtonTheme( - minWidth: double.minPositive, - child: FlatButton( - highlightColor: Colors.transparent, - splashColor: Colors.transparent, - padding: EdgeInsets.all(0), - onPressed: () => Navigator.of(context).pop(), - child: _backArrowImageDarkTheme), - ), - ) - : Container(), + ? SizedBox( + height: 37, + width: 20, + child: ButtonTheme( + minWidth: double.minPositive, + child: FlatButton( + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + padding: EdgeInsets.all(0), + onPressed: () => Navigator.of(context).pop(), + child: _backArrowImageDarkTheme), + ), + ) + : Container(), backgroundColor: Theme.of(context).backgroundColor, border: null, ), resizeToAvoidBottomPadding: false, body: PinCode( - (pin, _) => authStore.auth( - password: pin.fold('', (ac, val) => ac + '$val')), + (pin, _) => widget.authViewModel + .auth(password: pin.fold('', (ac, val) => ac + '$val')), false, _pinCodeKey)); } diff --git a/lib/src/screens/auth/create_login_page.dart b/lib/src/screens/auth/create_login_page.dart index eee2774a..1c817d65 100644 --- a/lib/src/screens/auth/create_login_page.dart +++ b/lib/src/screens/auth/create_login_page.dart @@ -14,15 +14,16 @@ Widget createLoginPage( @required WalletService walletService, @required WalletListService walletListService, @required AuthenticationStore authenticationStore}) => - Provider( - create: (_) => AuthStore( - sharedPreferences: sharedPreferences, - userService: userService, - walletService: walletService), - child: AuthPage( - onAuthenticationFinished: (isAuthenticated, state) { - if (isAuthenticated) { - authenticationStore.loggedIn(); - } - }, - closable: false)); + null; +// Provider( +// create: (_) => AuthStore( +// sharedPreferences: sharedPreferences, +// userService: userService, +// walletService: walletService), +// child: AuthPage( +// onAuthenticationFinished: (isAuthenticated, state) { +// if (isAuthenticated) { +// authenticationStore.loggedIn(); +// } +// }, +// closable: false)); diff --git a/lib/src/screens/auth/create_unlock_page.dart b/lib/src/screens/auth/create_unlock_page.dart index ddde813c..7a962821 100644 --- a/lib/src/screens/auth/create_unlock_page.dart +++ b/lib/src/screens/auth/create_unlock_page.dart @@ -11,13 +11,14 @@ Widget createUnlockPage( @required UserService userService, @required WalletService walletService, @required Function(bool, AuthPageState) onAuthenticationFinished}) => - WillPopScope( - onWillPop: () async => false, - child: Provider( - create: (_) => AuthStore( - sharedPreferences: sharedPreferences, - userService: userService, - walletService: walletService), - child: AuthPage( - onAuthenticationFinished: onAuthenticationFinished, - closable: false))); \ No newline at end of file + null; +// WillPopScope( +// onWillPop: () async => false, +// child: Provider( +// create: (_) => AuthStore( +// sharedPreferences: sharedPreferences, +// userService: userService, +// walletService: walletService), +// child: AuthPage( +// onAuthenticationFinished: onAuthenticationFinished, +// closable: false))); \ No newline at end of file diff --git a/lib/src/screens/base_page.dart b/lib/src/screens/base_page.dart index ac3b9729..4db32ab5 100644 --- a/lib/src/screens/base_page.dart +++ b/lib/src/screens/base_page.dart @@ -10,12 +10,19 @@ enum AppBarStyle { regular, withShadow } abstract class BasePage extends StatelessWidget { String get title => null; + bool get isModalBackButton => false; + Color get backgroundLightColor => Colors.white; + Color get backgroundDarkColor => PaletteDark.darkNightBlue; + bool get resizeToAvoidBottomPadding => true; + AppBarStyle get appBarStyle => AppBarStyle.regular; + Widget Function(BuildContext, Widget) get rootWrapper => null; + final _backArrowImage = Image.asset('assets/images/back_arrow.png'); final _backArrowImageDarkTheme = Image.asset('assets/images/back_arrow_dark_theme.png'); @@ -83,9 +90,8 @@ abstract class BasePage extends StatelessWidget { leading: leading(context), middle: middle(context), trailing: trailing(context), - backgroundColor: _isDarkTheme - ? backgroundDarkColor - : backgroundLightColor); + backgroundColor: + _isDarkTheme ? backgroundDarkColor : backgroundLightColor); case AppBarStyle.withShadow: return NavBar.withShadow( @@ -93,9 +99,8 @@ abstract class BasePage extends StatelessWidget { leading: leading(context), middle: middle(context), trailing: trailing(context), - backgroundColor: _isDarkTheme - ? backgroundDarkColor - : backgroundLightColor); + backgroundColor: + _isDarkTheme ? backgroundDarkColor : backgroundLightColor); default: return NavBar( @@ -103,9 +108,8 @@ abstract class BasePage extends StatelessWidget { leading: leading(context), middle: middle(context), trailing: trailing(context), - backgroundColor: _isDarkTheme - ? backgroundDarkColor - : backgroundLightColor); + backgroundColor: + _isDarkTheme ? backgroundDarkColor : backgroundLightColor); } } @@ -116,13 +120,14 @@ abstract class BasePage extends StatelessWidget { final _themeChanger = Provider.of(context); final _isDarkTheme = _themeChanger.getTheme() == Themes.darkTheme; - return Scaffold( - backgroundColor: _isDarkTheme - ? backgroundDarkColor - : backgroundLightColor, + final root = Scaffold( + backgroundColor: + _isDarkTheme ? backgroundDarkColor : backgroundLightColor, resizeToAvoidBottomPadding: resizeToAvoidBottomPadding, appBar: appBar(context), body: SafeArea(child: body(context)), floatingActionButton: floatingActionButton(context)); + + return rootWrapper?.call(context, root) ?? root; } } diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart index 86cff539..b587116f 100644 --- a/lib/src/screens/dashboard/dashboard_page.dart +++ b/lib/src/screens/dashboard/dashboard_page.dart @@ -1,28 +1,36 @@ import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; +import 'package:cake_wallet/view_model/dashboard_view_model.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/wallet_card.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/trade_history_panel.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/menu_widget.dart'; class DashboardPage extends StatelessWidget { + DashboardPage({@required this.walletViewModel}); + + final DashboardViewModel walletViewModel; final _bodyKey = GlobalKey(); @override - Widget build(BuildContext context) => DashboardPageBody(key: _bodyKey); + Widget build(BuildContext context) => + DashboardPageBody(key: _bodyKey, walletViewModel: walletViewModel); } class DashboardPageBody extends StatefulWidget { - DashboardPageBody({Key key}) : super(key: key); + DashboardPageBody({Key key, @required this.walletViewModel}) + : super(key: key); + + final DashboardViewModel walletViewModel; @override DashboardPageBodyState createState() => DashboardPageBodyState(); } class DashboardPageBodyState extends State { - @override Widget build(BuildContext context) { - final menuButton = Image.asset('assets/images/header.png', + final menuButton = Image.asset( + 'assets/images/header.png', color: Theme.of(context).primaryTextTheme.title.color, ); @@ -30,15 +38,10 @@ class DashboardPageBodyState extends State { child: Scaffold( body: Container( decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Theme.of(context).scaffoldBackgroundColor, - Theme.of(context).primaryColor - ], - begin: Alignment.centerLeft, - end: Alignment.centerRight - ) - ), + gradient: LinearGradient(colors: [ + Theme.of(context).scaffoldBackgroundColor, + Theme.of(context).primaryColor + ], begin: Alignment.centerLeft, end: Alignment.centerRight)), child: Column( children: [ Container( @@ -55,22 +58,17 @@ class DashboardPageBodyState extends State { padding: EdgeInsets.all(0), onPressed: () async { await showDialog( - builder: (_) => MenuWidget(), - context: context - ); + builder: (_) => MenuWidget(), context: context); }, child: menuButton), ), ), ), Padding( - padding: EdgeInsets.only(left: 20, top: 20), - child: WalletCard(), - ), - SizedBox( - height: 28, - ), - Expanded(child: TradeHistoryPanel()) + padding: EdgeInsets.only(left: 20, top: 20), + child: WalletCard(walletVM: widget.walletViewModel)), + SizedBox(height: 28), + Expanded(child: TradeHistoryPanel(dashboardViewModel: widget.walletViewModel)) ], ), ), diff --git a/lib/src/screens/dashboard/widgets/button_header.dart b/lib/src/screens/dashboard/widgets/button_header.dart index b620e9e1..ed7cc8f4 100644 --- a/lib/src/screens/dashboard/widgets/button_header.dart +++ b/lib/src/screens/dashboard/widgets/button_header.dart @@ -17,7 +17,7 @@ class ButtonHeader extends SliverPersistentHeaderDelegate { @override Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { - final actionListStore = Provider.of(context); +// final actionListStore = Provider.of(context); final historyPanelWidth = MediaQuery.of(context).size.width; final _themeChanger = Provider.of(context); @@ -97,44 +97,44 @@ class ButtonHeader extends SliverPersistentHeaderDelegate { style: TextStyle( fontWeight: FontWeight.bold, color: Theme.of(context).primaryTextTheme.caption.color))), - PopupMenuItem( - value: 0, - child: Observer( - builder: (_) => Row( - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, - children: [ - Text(S.of(context).incoming), - Checkbox( - value: actionListStore - .transactionFilterStore - .displayIncoming, - onChanged: (value) => - actionListStore - .transactionFilterStore - .toggleIncoming(), - ) - ]))), - PopupMenuItem( - value: 1, - child: Observer( - builder: (_) => Row( - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, - children: [ - Text(S.of(context).outgoing), - Checkbox( - value: actionListStore - .transactionFilterStore - .displayOutgoing, - onChanged: (value) => - actionListStore - .transactionFilterStore - .toggleOutgoing(), - ) - ]))), +// PopupMenuItem( +// value: 0, +// child: Observer( +// builder: (_) => Row( +// mainAxisAlignment: +// MainAxisAlignment +// .spaceBetween, +// children: [ +// Text(S.of(context).incoming), +// Checkbox( +// value: actionListStore +// .transactionFilterStore +// .displayIncoming, +// onChanged: (value) => +// actionListStore +// .transactionFilterStore +// .toggleIncoming(), +// ) +// ]))), +// PopupMenuItem( +// value: 1, +// child: Observer( +// builder: (_) => Row( +// mainAxisAlignment: +// MainAxisAlignment +// .spaceBetween, +// children: [ +// Text(S.of(context).outgoing), +// Checkbox( +// value: actionListStore +// .transactionFilterStore +// .displayOutgoing, +// onChanged: (value) => +// actionListStore +// .transactionFilterStore +// .toggleOutgoing(), +// ) +// ]))), PopupMenuItem( value: 2, child: @@ -156,17 +156,17 @@ class ButtonHeader extends SliverPersistentHeaderDelegate { .spaceBetween, children: [ Text('XMR.TO'), - Checkbox( - value: actionListStore - .tradeFilterStore - .displayXMRTO, - onChanged: (value) => - actionListStore - .tradeFilterStore - .toggleDisplayExchange( - ExchangeProviderDescription - .xmrto), - ) +// Checkbox( +// value: actionListStore +// .tradeFilterStore +// .displayXMRTO, +// onChanged: (value) => +// actionListStore +// .tradeFilterStore +// .toggleDisplayExchange( +// ExchangeProviderDescription +// .xmrto), +// ) ]))), PopupMenuItem( value: 4, @@ -177,17 +177,17 @@ class ButtonHeader extends SliverPersistentHeaderDelegate { .spaceBetween, children: [ Text('Change.NOW'), - Checkbox( - value: actionListStore - .tradeFilterStore - .displayChangeNow, - onChanged: (value) => - actionListStore - .tradeFilterStore - .toggleDisplayExchange( - ExchangeProviderDescription - .changeNow), - ) +// Checkbox( +// value: actionListStore +// .tradeFilterStore +// .displayChangeNow, +// onChanged: (value) => +// actionListStore +// .tradeFilterStore +// .toggleDisplayExchange( +// ExchangeProviderDescription +// .changeNow), +// ) ]))), PopupMenuItem( value: 5, @@ -198,17 +198,17 @@ class ButtonHeader extends SliverPersistentHeaderDelegate { .spaceBetween, children: [ Text('MorphToken'), - Checkbox( - value: actionListStore - .tradeFilterStore - .displayMorphToken, - onChanged: (value) => - actionListStore - .tradeFilterStore - .toggleDisplayExchange( - ExchangeProviderDescription - .morphToken), - ) +// Checkbox( +// value: actionListStore +// .tradeFilterStore +// .displayMorphToken, +// onChanged: (value) => +// actionListStore +// .tradeFilterStore +// .toggleDisplayExchange( +// ExchangeProviderDescription +// .morphToken), +// ) ]))) ], child: filterButton, @@ -225,10 +225,10 @@ class ButtonHeader extends SliverPersistentHeaderDelegate { .add(Duration(days: 1))); if (picked != null && picked.length == 2) { - actionListStore.transactionFilterStore - .changeStartDate(picked.first); - actionListStore.transactionFilterStore - .changeEndDate(picked.last); +// actionListStore.transactionFilterStore +// .changeStartDate(picked.first); +// actionListStore.transactionFilterStore +// .changeEndDate(picked.last); } } }, diff --git a/lib/src/screens/dashboard/widgets/trade_history_panel.dart b/lib/src/screens/dashboard/widgets/trade_history_panel.dart index 8d47fb3a..2aca26c5 100644 --- a/lib/src/screens/dashboard/widgets/trade_history_panel.dart +++ b/lib/src/screens/dashboard/widgets/trade_history_panel.dart @@ -1,21 +1,26 @@ -import 'package:cake_wallet/src/domain/common/balance_display_mode.dart'; -import 'package:cake_wallet/src/stores/action_list/action_list_store.dart'; -import 'package:cake_wallet/src/stores/action_list/date_section_item.dart'; -import 'package:cake_wallet/src/stores/action_list/trade_list_item.dart'; -import 'package:cake_wallet/src/stores/action_list/transaction_list_item.dart'; -import 'package:cake_wallet/src/stores/settings/settings_store.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/view_model/dashboard_view_model.dart'; +import 'package:cake_wallet/src/domain/common/balance_display_mode.dart'; +import 'package:cake_wallet/src/stores/action_list/action_list_store.dart'; +import 'package:cake_wallet/src/stores/action_list/date_section_item.dart'; +import 'package:cake_wallet/src/stores/action_list/trade_list_item.dart'; +import 'package:cake_wallet/src/stores/action_list/transaction_list_item.dart'; +import 'package:cake_wallet/src/stores/settings/settings_store.dart'; import 'date_section_raw.dart'; import 'trade_row.dart'; import 'transaction_raw.dart'; import 'button_header.dart'; class TradeHistoryPanel extends StatefulWidget { + TradeHistoryPanel({this.dashboardViewModel}); + + DashboardViewModel dashboardViewModel; + @override TradeHistoryPanelState createState() => TradeHistoryPanelState(); } @@ -44,119 +49,127 @@ class TradeHistoryPanelState extends State { @override Widget build(BuildContext context) { - final actionListStore = Provider.of(context); - final settingsStore = Provider.of(context); - final transactionDateFormat = DateFormat("HH:mm"); +// final actionListStore = Provider.of(context); +// final settingsStore = Provider.of(context); + final transactionDateFormat = DateFormat('HH:mm'); return Container( - height: MediaQuery.of(context).size.height, - width: MediaQuery.of(context).size.width, - alignment: Alignment.bottomCenter, - child: AnimatedContainer( + height: MediaQuery.of(context).size.height, width: MediaQuery.of(context).size.width, - height: panelHeight, - duration: Duration(milliseconds: 1000), - curve: Curves.fastOutSlowIn, - child: ClipRRect( - borderRadius: BorderRadius.only(topLeft: Radius.circular(20), topRight: Radius.circular(20)), - child: CustomScrollView( - slivers: [ - SliverPersistentHeader( - delegate: ButtonHeader(), - pinned: true, - floating: false, - ), - Observer( - key: _listObserverKey, - builder: (_) { - final items = actionListStore.items == null - ? [] - : actionListStore.items; - final itemsCount = items.length + 1; - final symbol = settingsStore.fiatCurrency.toString(); - double freeSpaceHeight = MediaQuery.of(context).size.height - 496; - - return SliverList( - key: _listKey, - delegate: SliverChildBuilderDelegate( - (context, index) { - - if (index == itemsCount - 1) { - freeSpaceHeight = freeSpaceHeight >= 0 ? freeSpaceHeight : 0; + alignment: Alignment.bottomCenter, + child: AnimatedContainer( + width: MediaQuery.of(context).size.width, + height: panelHeight, + duration: Duration(milliseconds: 1000), + curve: Curves.fastOutSlowIn, + child: ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20)), + child: CustomScrollView( + slivers: [ + SliverPersistentHeader( + delegate: ButtonHeader(), + pinned: true, + floating: false, + ), + Observer( + key: _listObserverKey, + builder: (_) { +// final items = actionListStore.items == null +// ? [] +// : actionListStore.items; + final items = widget.dashboardViewModel.transactions; + final itemsCount = items.length + 1; + final symbol = + '\$'; // settingsStore.fiatCurrency.toString(); + var freeSpaceHeight = + MediaQuery.of(context).size.height - 496; + + return SliverList( + key: _listKey, + delegate: + SliverChildBuilderDelegate((context, index) { + if (index == itemsCount - 1) { + freeSpaceHeight = freeSpaceHeight >= 0 + ? freeSpaceHeight + : 0; + + return Container( + height: freeSpaceHeight, + width: MediaQuery.of(context).size.width, + color: Theme.of(context).backgroundColor); + } + + final item = items[index]; + + if (item is DateSectionItem) { + freeSpaceHeight -= 38; + return DateSectionRaw(date: item.date); + } + + if (item is TransactionListItem) { + freeSpaceHeight -= 62; + final transaction = item.transaction; + final savedDisplayMode = + BalanceDisplayMode.all; + //settingsStore +// .balanceDisplayMode; + final formattedAmount = savedDisplayMode == + BalanceDisplayMode.hiddenBalance + ? '---' + : transaction.amountFormatted(); + final formattedFiatAmount = + savedDisplayMode == + BalanceDisplayMode.hiddenBalance + ? '---' + : transaction + .fiatAmount(); // symbol ??? + + return TransactionRow( + onTap: () => Navigator.of(context) + .pushNamed(Routes.transactionDetails, + arguments: transaction), + direction: transaction.direction, + formattedDate: transactionDateFormat + .format(transaction.date), + formattedAmount: formattedAmount, + formattedFiatAmount: formattedFiatAmount, + isPending: transaction.isPending); + } + + if (item is TradeListItem) { + freeSpaceHeight -= 62; + final trade = item.trade; + final savedDisplayMode = + BalanceDisplayMode.all; + //settingsStore + // .balanceDisplayMode; + final formattedAmount = trade.amount != null + ? savedDisplayMode == + BalanceDisplayMode.hiddenBalance + ? '---' + : trade.amountFormatted() + : trade.amount; + + return TradeRow( + onTap: () => Navigator.of(context) + .pushNamed(Routes.tradeDetails, + arguments: trade), + provider: trade.provider, + from: trade.from, + to: trade.to, + createdAtFormattedDate: + transactionDateFormat + .format(trade.createdAt), + formattedAmount: formattedAmount); + } return Container( - height: freeSpaceHeight, - width: MediaQuery.of(context).size.width, - color: Theme.of(context).backgroundColor, - ); - } - - final item = items[index]; - - if (item is DateSectionItem) { - freeSpaceHeight -= 38; - return DateSectionRaw(date: item.date); - } - - if (item is TransactionListItem) { - freeSpaceHeight -= 62; - final transaction = item.transaction; - final savedDisplayMode = settingsStore.balanceDisplayMode; - final formattedAmount = - savedDisplayMode == BalanceDisplayMode.hiddenBalance - ? '---' - : transaction.amountFormatted(); - final formattedFiatAmount = - savedDisplayMode == BalanceDisplayMode.hiddenBalance - ? '---' - : transaction.fiatAmount(); // symbol ??? - - return TransactionRow( - onTap: () => Navigator.of(context).pushNamed( - Routes.transactionDetails, - arguments: transaction), - direction: transaction.direction, - formattedDate: - transactionDateFormat.format(transaction.date), - formattedAmount: formattedAmount, - formattedFiatAmount: formattedFiatAmount, - isPending: transaction.isPending); - } - - if (item is TradeListItem) { - freeSpaceHeight -= 62; - final trade = item.trade; - final savedDisplayMode = settingsStore.balanceDisplayMode; - final formattedAmount = trade.amount != null - ? savedDisplayMode == BalanceDisplayMode.hiddenBalance - ? '---' - : trade.amountFormatted() - : trade.amount; - - return TradeRow( - onTap: () => Navigator.of(context) - .pushNamed(Routes.tradeDetails, arguments: trade), - provider: trade.provider, - from: trade.from, - to: trade.to, - createdAtFormattedDate: - transactionDateFormat.format(trade.createdAt), - formattedAmount: formattedAmount); - } - - return Container( - color: Theme.of(context).backgroundColor - ); - }, - - childCount: itemsCount - ) - ); - }) - ], - ), - ) - ), - ); + color: Theme.of(context).backgroundColor); + }, childCount: itemsCount)); + }) + ], + )))); //, } -} \ No newline at end of file +} diff --git a/lib/src/screens/dashboard/widgets/wallet_card.dart b/lib/src/screens/dashboard/widgets/wallet_card.dart index 424b4b10..8af70065 100644 --- a/lib/src/screens/dashboard/widgets/wallet_card.dart +++ b/lib/src/screens/dashboard/widgets/wallet_card.dart @@ -1,21 +1,27 @@ import 'dart:async'; import 'package:cake_wallet/palette.dart'; -import 'package:cake_wallet/src/domain/common/balance_display_mode.dart'; +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:cake_wallet/src/domain/common/balance_display_mode.dart'; import 'package:cake_wallet/src/stores/balance/balance_store.dart'; import 'package:cake_wallet/src/stores/settings/settings_store.dart'; import 'package:cake_wallet/src/stores/sync/sync_store.dart'; import 'package:cake_wallet/src/stores/wallet/wallet_store.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/domain/common/sync_status.dart'; import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart'; import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/view_model/dashboard_view_model.dart'; class WalletCard extends StatefulWidget { + WalletCard({this.walletVM}); + + DashboardViewModel walletVM; + @override WalletCardState createState() => WalletCardState(); } @@ -50,14 +56,12 @@ class WalletCardState extends State { cardWidth = screenWidth; opacity = 1; }); - Timer(Duration(milliseconds: 500), () => - setState(() => isDraw = true) - ); + Timer(Duration(milliseconds: 500), () => setState(() => isDraw = true)); } @override Widget build(BuildContext context) { - final List colorsSync = [ + final colorsSync = [ Theme.of(context).cardTheme.color, Theme.of(context).hoverColor ]; @@ -67,77 +71,67 @@ class WalletCardState extends State { height: cardHeight, alignment: Alignment.centerRight, child: AnimatedContainer( - alignment: Alignment.centerLeft, - width: cardWidth, - height: cardHeight, - duration: Duration(milliseconds: 500), - curve: Curves.fastOutSlowIn, - padding: EdgeInsets.only( - top: 1, - left: 1, - bottom: 1 - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.only(topLeft: Radius.circular(10), bottomLeft: Radius.circular(10)), - color: Theme.of(context).focusColor, - boxShadow: [ - BoxShadow( - color: PaletteDark.darkNightBlue.withOpacity(0.5), - blurRadius: 8, - offset: Offset(5, 5)) - ] - ), - child: ClipRRect( - borderRadius: BorderRadius.only(topLeft: Radius.circular(10), bottomLeft: Radius.circular(10)), - child: Container( - width: cardWidth, - height: cardHeight, - color: Theme.of(context).cardColor, - child: InkWell( - onTap: () => setState(() => isFrontSide = !isFrontSide), - child: isFrontSide - ? frontSide(colorsSync) - : backSide(colorsSync) + alignment: Alignment.centerLeft, + width: cardWidth, + height: cardHeight, + duration: Duration(milliseconds: 500), + curve: Curves.fastOutSlowIn, + padding: EdgeInsets.only(top: 1, left: 1, bottom: 1), + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10), + bottomLeft: Radius.circular(10)), + color: Theme.of(context).focusColor, + boxShadow: [ + BoxShadow( + color: PaletteDark.darkNightBlue.withOpacity(0.5), + blurRadius: 8, + offset: Offset(5, 5)) + ]), + child: ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10), bottomLeft: Radius.circular(10)), + child: Container( + width: cardWidth, + height: cardHeight, + color: Theme.of(context).cardColor, + child: InkWell( + onTap: () => setState(() => isFrontSide = !isFrontSide), + child: isFrontSide + ? frontSide(colorsSync) + : backSide(colorsSync)), ), - ), - ) - ), + )), ); } Widget frontSide(List colorsSync) { - final syncStore = Provider.of(context); - final walletStore = Provider.of(context); +// final syncStore = Provider.of(context); +// final walletStore = Provider.of(context); final settingsStore = Provider.of(context); - final balanceStore = Provider.of(context); - final triangleButton = Image.asset('assets/images/triangle.png', +// final balanceStore = Provider.of(context); + final triangleButton = Image.asset( + 'assets/images/triangle.png', color: Theme.of(context).primaryTextTheme.title.color, ); return Observer( key: _syncingObserverKey, builder: (_) { - final status = syncStore.status; + final status = widget.walletVM.status; final statusText = status.title(); - final progress = syncStore.status.progress(); + final progress = status.progress(); final indicatorWidth = progress * cardWidth; - - String shortAddress = walletStore.subaddress.address; - shortAddress = shortAddress.replaceRange(4, shortAddress.length - 4, '...'); - + final shortAddress = widget.walletVM.address + .replaceRange(4, widget.walletVM.address.length - 4, '...'); var descriptionText = ''; if (status is SyncingSyncStatus) { - descriptionText = S - .of(context) - .Blocks_remaining( - syncStore.status.toString()); + descriptionText = S.of(context).Blocks_remaining(status.toString()); } if (status is FailedSyncStatus) { - descriptionText = S - .of(context) - .please_try_to_connect_to_another_node; + descriptionText = S.of(context).please_try_to_connect_to_another_node; } return Container( @@ -149,183 +143,206 @@ class WalletCardState extends State { height: cardHeight, width: indicatorWidth, decoration: BoxDecoration( - borderRadius: BorderRadius.only(topLeft: Radius.circular(10), bottomLeft: Radius.circular(10)), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10), + bottomLeft: Radius.circular(10)), gradient: LinearGradient( colors: colorsSync, begin: Alignment.topCenter, - end: Alignment.bottomCenter - ) - ), + end: Alignment.bottomCenter)), ), progress != 1 - ? Positioned( - left: indicatorWidth, - top: 0, - bottom: 0, - child: Container( - width: 1, - height: cardHeight, - color: Theme.of(context).focusColor, - ) - ) - : Offstage(), - isDraw ? Positioned( - left: 20, - right: 20, - top: 30, - bottom: 30, - child: Container( - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, + ? Positioned( + left: indicatorWidth, + top: 0, + bottom: 0, + child: Container( + width: 1, + height: cardHeight, + color: Theme.of(context).focusColor, + )) + : Offstage(), + isDraw + ? Positioned( + left: 20, + right: 20, + top: 30, + bottom: 30, + child: Container( + child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Column( + Row( crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - InkWell( - onTap: () {}, - child: Row( - children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + InkWell( + onTap: () {}, + child: Row( + children: [ + Text( + widget.walletVM.name, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Theme.of(context) + .primaryTextTheme + .title + .color), + ), + SizedBox(width: 10), + triangleButton + ], + ), + ), + SizedBox(height: 5), + if (widget.walletVM.subname?.isNotEmpty ?? false) Text( - walletStore.name, + widget.walletVM.subname, style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Theme.of(context).primaryTextTheme.title.color - ), - ), - SizedBox(width: 10), - triangleButton - ], - ), - ), - SizedBox( - height: 5, + fontSize: 12, + color: Theme.of(context) + .primaryTextTheme + .caption + .color), + ) + ], ), - Text( - walletStore.account.label, - style: TextStyle( - fontSize: 12, - color: Theme.of(context).primaryTextTheme.caption.color + Container( + width: 98, + height: 32, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Theme.of(context) + .accentTextTheme + .subtitle + .backgroundColor, + borderRadius: BorderRadius.all( + Radius.circular(16))), + child: Text( + shortAddress, + style: TextStyle( + fontSize: 12, + color: Theme.of(context) + .primaryTextTheme + .caption + .color), ), ) ], ), - Container( - width: 98, - height: 32, - alignment: Alignment.center, - decoration: BoxDecoration( - color: Theme.of(context).accentTextTheme.subtitle.backgroundColor, - borderRadius: BorderRadius.all(Radius.circular(16)) - ), - child: Text( - shortAddress, - style: TextStyle( - fontSize: 12, - color: Theme.of(context).primaryTextTheme.caption.color - ), - ), - ) - ], - ), - status is SyncedSyncStatus - ? Observer( - key: _balanceObserverKey, - builder: (_) { - final balanceDisplayMode = settingsStore.balanceDisplayMode; - final symbol = settingsStore - .fiatCurrency - .toString(); - var balance = '---'; - var fiatBalance = '---'; + status is SyncedSyncStatus + ? Observer( + key: _balanceObserverKey, + builder: (_) { + final balanceDisplayMode = + BalanceDisplayMode.fullBalance; +// settingsStore.balanceDisplayMode; + final symbol = + settingsStore.fiatCurrency.toString(); + var balance = '---'; + var fiatBalance = '---'; - if (balanceDisplayMode == - BalanceDisplayMode.availableBalance) { - balance = - balanceStore.unlockedBalance ?? - '0.0'; - fiatBalance = - '$symbol ${balanceStore.fiatUnlockedBalance}'; - } + if (balanceDisplayMode == + BalanceDisplayMode.availableBalance) { + balance = widget.walletVM.balance + .unlockedBalance ?? + '0.0'; + fiatBalance = '\$ 123.43'; +// '$symbol ${balanceStore.fiatUnlockedBalance}'; + } - if (balanceDisplayMode == - BalanceDisplayMode.fullBalance) { - balance = - balanceStore.fullBalance ?? '0.0'; - fiatBalance = - '$symbol ${balanceStore.fiatFullBalance}'; - } + if (balanceDisplayMode == + BalanceDisplayMode.fullBalance) { + balance = widget.walletVM.balance + .totalBalance ?? + '0.0'; + fiatBalance = '\$ 123.43'; +// '$symbol ${balanceStore.fiatFullBalance}'; + } - return Row( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, + return Row( + crossAxisAlignment: + CrossAxisAlignment.end, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + balanceDisplayMode.toString(), + style: TextStyle( + fontSize: 12, + color: Theme.of(context) + .primaryTextTheme + .caption + .color), + ), + SizedBox(height: 5), + Text( + balance, + style: TextStyle( + fontSize: 28, + color: Theme.of(context) + .primaryTextTheme + .title + .color), + ) + ], + ), + Text( + fiatBalance, + style: TextStyle( + fontSize: 14, + color: Theme.of(context) + .primaryTextTheme + .title + .color), + ) + ], + ); + }) + : Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, children: [ - Text( - balanceDisplayMode.toString(), - style: TextStyle( - fontSize: 12, - color: Theme.of(context).primaryTextTheme.caption.color - ), - ), - SizedBox(height: 5), - Text( - balance, - style: TextStyle( - fontSize: 28, - color: Theme.of(context).primaryTextTheme.title.color - ), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + statusText, + style: TextStyle( + fontSize: 12, + color: Theme.of(context) + .primaryTextTheme + .caption + .color), + ), + SizedBox(height: 5), + Text( + descriptionText, + style: TextStyle( + fontSize: 14, + color: Theme.of(context) + .primaryTextTheme + .title + .color), + ) + ], ) ], - ), - Text( - fiatBalance, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).primaryTextTheme.title.color - ), ) - ], - ); - } - ) - : Row( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - statusText, - style: TextStyle( - fontSize: 12, - color: Theme.of(context).primaryTextTheme.caption.color - ), - ), - SizedBox(height: 5), - Text( - descriptionText, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).primaryTextTheme.title.color - ), - ) - ], - ) ], - ) - ], - ), - ) - ) - : Offstage() + ), + )) + : Offstage() ], ), ); @@ -334,149 +351,157 @@ class WalletCardState extends State { } Widget backSide(List colorsSync) { - final walletStore = Provider.of(context); - final rightArrow = Image.asset('assets/images/right_arrow.png', + final rightArrow = Image.asset( + 'assets/images/right_arrow.png', color: Theme.of(context).primaryTextTheme.title.color, ); - double messageBoxHeight = 0; - double messageBoxWidth = cardWidth - 10; + var messageBoxHeight = 0.0; + var messageBoxWidth = cardWidth - 10; return Observer( - key: _addressObserverKey, - builder: (_) { - return Container( - width: cardWidth, - height: cardHeight, - alignment: Alignment.topCenter, - child: Stack( - alignment: Alignment.topRight, - children: [ - Container( - width: cardWidth, - height: cardHeight, - padding: EdgeInsets.only(left: 20, right: 20, top: 30, bottom: 30), - decoration: BoxDecoration( - borderRadius: BorderRadius.only(topLeft: Radius.circular(10), bottomLeft: Radius.circular(10)), - gradient: LinearGradient( - colors: colorsSync, - begin: Alignment.topCenter, - end: Alignment.bottomCenter - ) - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Expanded( - child: Container( + key: _addressObserverKey, + builder: (_) { + return Container( + width: cardWidth, + height: cardHeight, + alignment: Alignment.topCenter, + child: Stack( + alignment: Alignment.topRight, + children: [ + Container( + width: cardWidth, + height: cardHeight, + padding: + EdgeInsets.only(left: 20, right: 20, top: 30, bottom: 30), + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10), + bottomLeft: Radius.circular(10)), + gradient: LinearGradient( + colors: colorsSync, + begin: Alignment.topCenter, + end: Alignment.bottomCenter)), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Expanded( + child: Container( height: 90, child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( S.current.card_address, style: TextStyle( fontSize: 12, - color: Theme.of(context).primaryTextTheme.caption.color - ), + color: Theme.of(context) + .primaryTextTheme + .caption + .color), ), + SizedBox(height: 10), GestureDetector( onTap: () { Clipboard.setData(ClipboardData( - text: walletStore.subaddress.address)); - _addressObserverKey.currentState.setState(() { + text: widget.walletVM.address)); + _addressObserverKey.currentState + .setState(() { messageBoxHeight = 20; messageBoxWidth = cardWidth; }); Timer(Duration(milliseconds: 1000), () { try { - _addressObserverKey.currentState.setState(() { + _addressObserverKey.currentState + .setState(() { messageBoxHeight = 0; messageBoxWidth = cardWidth - 10; }); - } catch(e) { + } catch (e) { print('${e.toString()}'); } }); }, child: Text( - walletStore.subaddress.address, + widget.walletVM.address, style: TextStyle( fontSize: 14, - color: Theme.of(context).primaryTextTheme.title.color - ), + color: Theme.of(context) + .primaryTextTheme + .title + .color), ), ) ], ), + )), + SizedBox(width: 10), + Container( + width: 90, + height: 90, + child: QrImage( + data: widget.walletVM.address, + backgroundColor: Colors.transparent, + foregroundColor: Theme.of(context) + .primaryTextTheme + .caption + .color), ) - ), - SizedBox(width: 10), - Container( - width: 90, - height: 90, - child: QrImage( - data: walletStore.subaddress.address, - backgroundColor: Colors.transparent, - foregroundColor: Theme.of(context).primaryTextTheme.caption.color - ), - ) - ], - ), - Container( - height: 44, - padding: EdgeInsets.only(left: 20, right: 20), - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(22)), - color: Theme.of(context).primaryTextTheme.overline.color + ], ), - child: InkWell( - onTap: () => Navigator.of(context, - rootNavigator: true) - .pushNamed(Routes.receive), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - S.of(context).accounts_subaddresses, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).primaryTextTheme.title.color + Container( + height: 44, + padding: EdgeInsets.only(left: 20, right: 20), + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(22)), + color: Theme.of(context) + .primaryTextTheme + .overline + .color), + child: InkWell( + onTap: () => + Navigator.of(context, rootNavigator: true) + .pushNamed(Routes.receive), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + S.of(context).accounts_subaddresses, + style: TextStyle( + fontSize: 14, + color: Theme.of(context) + .primaryTextTheme + .title + .color), ), - ), - rightArrow - ], + rightArrow + ], + ), ), - ), - ) - ], - ), - ), - AnimatedContainer( - width: messageBoxWidth, - height: messageBoxHeight, - alignment: Alignment.center, - duration: Duration(milliseconds: 500), - curve: Curves.fastOutSlowIn, - decoration: BoxDecoration( - borderRadius: BorderRadius.only(topLeft: Radius.circular(10)), - color: Colors.green - ), - child: Text( - S.of(context).copied_to_clipboard, - style: TextStyle( - fontSize: 10, - color: Colors.white + ) + ], ), ), - ) - ], - ), - ); - } - ); + AnimatedContainer( + width: messageBoxWidth, + height: messageBoxHeight, + alignment: Alignment.center, + duration: Duration(milliseconds: 500), + curve: Curves.fastOutSlowIn, + decoration: BoxDecoration( + borderRadius: + BorderRadius.only(topLeft: Radius.circular(10)), + color: Colors.green), + child: Text( + S.of(context).copied_to_clipboard, + style: TextStyle(fontSize: 10, color: Colors.white), + ), + ) + ], + ), + ); + }); } -} \ No newline at end of file +} diff --git a/lib/src/screens/new_wallet/new_wallet_page.dart b/lib/src/screens/new_wallet/new_wallet_page.dart index b3b18123..1eaf95b4 100644 --- a/lib/src/screens/new_wallet/new_wallet_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_page.dart @@ -1,85 +1,54 @@ -import 'package:cake_wallet/core/monero_wallet_list_service.dart'; -import 'package:cake_wallet/core/wallet_creation_service.dart'; -import 'package:cake_wallet/core/wallet_credentials.dart'; -import 'package:cake_wallet/src/domain/common/wallet_type.dart'; import 'package:mobx/mobx.dart'; -import 'package:provider/provider.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/stores/wallet_creation/wallet_creation_store.dart'; -import 'package:cake_wallet/src/stores/wallet_creation/wallet_creation_state.dart'; -import 'package:cake_wallet/src/domain/services/wallet_list_service.dart'; -import 'package:cake_wallet/src/domain/services/wallet_service.dart'; +import 'package:cake_wallet/core/validator.dart'; +import 'package:cake_wallet/src/widgets/seed_language_selector.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; -import 'package:cake_wallet/src/stores/seed_language/seed_language_store.dart'; -import 'package:cake_wallet/src/screens/new_wallet/widgets/select_button.dart'; import 'package:cake_wallet/src/screens/seed_language/widgets/seed_language_picker.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/view_model/wallet_creation_state.dart'; +import 'package:cake_wallet/view_model/wallet_new_vm.dart'; class NewWalletPage extends BasePage { - NewWalletPage( - {@required this.walletsService, - @required this.walletService, - @required this.sharedPreferences}); + NewWalletPage(this._walletNewVM); - final WalletListService walletsService; - final WalletService walletService; - final SharedPreferences sharedPreferences; + final WalletNewVM _walletNewVM; @override String get title => S.current.new_wallet; @override - Widget body(BuildContext context) => WalletNameForm(); + Widget body(BuildContext context) => WalletNameForm(_walletNewVM); } class WalletNameForm extends StatefulWidget { + WalletNameForm(this._walletNewVM); + + final WalletNewVM _walletNewVM; + @override - _WalletNameFormState createState() => _WalletNameFormState(); + _WalletNameFormState createState() => _WalletNameFormState(_walletNewVM); } class _WalletNameFormState extends State { + _WalletNameFormState(this._walletNewVM); + static const aspectRatioImage = 1.22; - final _formKey = GlobalKey(); - final nameController = TextEditingController(); final walletNameImage = Image.asset('assets/images/wallet_name.png'); + final _formKey = GlobalKey(); + final _languageSelectorKey = GlobalKey(); + ReactionDisposer _stateReaction; + final WalletNewVM _walletNewVM; @override - void dispose() { - nameController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final walletCreationStore = Provider.of(context); - final walletCreationService = Provider.of(context); - - // FIXME: Does seed language store is really needed ??? - - final seedLanguageStore = Provider.of(context); - - final seedLocales = [ - S.current.seed_language_english, - S.current.seed_language_chinese, - S.current.seed_language_dutch, - S.current.seed_language_german, - S.current.seed_language_japanese, - S.current.seed_language_portuguese, - S.current.seed_language_russian, - S.current.seed_language_spanish - ]; - - nameController.addListener(() => - walletCreationStore.setDisabledStatus(!nameController.text.isNotEmpty)); - - reaction((_) => walletCreationStore.state, (WalletCreationState state) { + void initState() { + _stateReaction ??= + reaction((_) => _walletNewVM.state, (WalletCreationState state) { if (state is WalletCreatedSuccessfully) { Navigator.of(context).popUntil((route) => route.isFirst); } @@ -98,7 +67,11 @@ class _WalletNameFormState extends State { }); } }); + super.initState(); + } + @override + Widget build(BuildContext context) { return Container( padding: EdgeInsets.only(top: 24), child: ScrollableWithBottomSection( @@ -116,94 +89,76 @@ class _WalletNameFormState extends State { child: Form( key: _formKey, child: TextFormField( - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 20.0, - fontWeight: FontWeight.w600, - color: Theme.of(context).primaryTextTheme.title.color), - controller: nameController, - decoration: InputDecoration( - hintStyle: TextStyle( - fontSize: 16.0, - color: Theme.of(context) - .primaryTextTheme - .caption - .color), - hintText: S.of(context).wallet_name, - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).dividerColor, - width: 1.0)), - enabledBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).dividerColor, - width: 1.0))), - validator: (value) { - walletCreationStore.validateWalletName(value); - return walletCreationStore.errorMessage; - }, - )), + onChanged: (value) => _walletNewVM.name = value, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 20.0, + fontWeight: FontWeight.w600, + color: + Theme.of(context).primaryTextTheme.title.color), + decoration: InputDecoration( + hintStyle: TextStyle( + fontSize: 16.0, + color: Theme.of(context) + .primaryTextTheme + .caption + .color), + hintText: S.of(context).wallet_name, + 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: WalletNameValidator())), ), - Padding( - padding: EdgeInsets.only(top: 40), - child: Text( - S.of(context).seed_language_choose, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.w600, - color: Theme.of(context).primaryTextTheme.title.color), + if (_walletNewVM.hasLanguageSelector) ...[ + Padding( + padding: EdgeInsets.only(top: 40), + child: Text( + S.of(context).seed_language_choose, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.w600, + color: Theme.of(context).primaryTextTheme.title.color), + ), ), - ), - Padding( - padding: EdgeInsets.only(top: 24), - child: Observer( - builder: (_) => SelectButton( - image: null, - text: seedLocales[seedLanguages - .indexOf(seedLanguageStore.selectedSeedLanguage)], - color: Theme.of(context) - .accentTextTheme - .title - .backgroundColor, - textColor: Theme.of(context).primaryTextTheme.title.color, - onTap: () async => await showDialog( - context: context, - builder: (BuildContext context) => - SeedLanguagePicker()))), - ) + Padding( + padding: EdgeInsets.only(top: 24), + child: SeedLanguageSelector( + key: _languageSelectorKey, + initialSelected: defaultSeedLanguage), + ) + ] ]), bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), bottomSection: Observer( builder: (context) { return LoadingPrimaryButton( - onPressed: () => _confirmForm(walletCreationService, - seedLanguageStore.selectedSeedLanguage), + onPressed: _confirmForm, text: S.of(context).continue_text, color: Colors.green, textColor: Colors.white, - isLoading: walletCreationStore.state is WalletIsCreating, - isDisabled: walletCreationStore.isDisabledStatus, + isLoading: _walletNewVM.state is WalletCreatedSuccessfully, + isDisabled: _walletNewVM.name.isEmpty, ); }, )), ); } - void _confirmForm( - WalletCreationService walletCreationService, String language) { + void _confirmForm() { if (!_formKey.currentState.validate()) { return; } - WalletCredentials credentials; - - if (walletCreationService.type == WalletType.monero) { - credentials = MoneroNewWalletCredentials( - name: nameController.text, language: language); - } - - walletCreationService.create(credentials); + _walletNewVM.create( + options: _walletNewVM.hasLanguageSelector + ? _languageSelectorKey.currentState.selected + : null); } } diff --git a/lib/src/screens/new_wallet/new_wallet_type_page.dart b/lib/src/screens/new_wallet/new_wallet_type_page.dart index 044b52e6..9dfcfbf3 100644 --- a/lib/src/screens/new_wallet/new_wallet_type_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_type_page.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:cake_wallet/generated/i18n.dart'; @@ -8,14 +9,23 @@ import 'package:cake_wallet/src/screens/new_wallet/widgets/select_button.dart'; import 'package:cake_wallet/routes.dart'; class NewWalletTypePage extends BasePage { + NewWalletTypePage({this.onTypeSelected}); + + final void Function(BuildContext, WalletType) onTypeSelected; + @override String get title => S.current.new_wallet; @override - Widget body(BuildContext context) => WalletTypeForm(); + Widget body(BuildContext context) => + WalletTypeForm(onTypeSelected: onTypeSelected); } class WalletTypeForm extends StatefulWidget { + WalletTypeForm({this.onTypeSelected}); + + final void Function(BuildContext, WalletType) onTypeSelected; + @override WalletTypeFormState createState() => WalletTypeFormState(); } @@ -23,35 +33,19 @@ class WalletTypeForm extends StatefulWidget { class WalletTypeFormState extends State { static const aspectRatioImage = 1.22; - final moneroIcon = Image.asset('assets/images/monero.png', height: 24, width: 24); - final bitcoinIcon = Image.asset('assets/images/bitcoin.png', height: 24, width: 24); + final moneroIcon = + Image.asset('assets/images/monero.png', height: 24, width: 24); + final bitcoinIcon = + Image.asset('assets/images/bitcoin.png', height: 24, width: 24); final walletTypeImage = Image.asset('assets/images/wallet_type.png'); - bool isDisabledButton; - bool isMoneroSelected; - bool isBitcoinSelected; - - Color moneroBackgroundColor = Colors.transparent; - Color moneroTextColor = Colors.transparent; - Color bitcoinBackgroundColor = Colors.transparent; - Color bitcoinTextColor = Colors.transparent; + WalletType selected; + List types; @override void initState() { - isDisabledButton = true; - isMoneroSelected = false; - isBitcoinSelected = false; - + types = [WalletType.bitcoin, WalletType.monero]; super.initState(); - WidgetsBinding.instance.addPostFrameCallback(afterLayout); - } - - void afterLayout(dynamic _) { - moneroBackgroundColor = Theme.of(context).accentTextTheme.title.backgroundColor; - moneroTextColor = Theme.of(context).primaryTextTheme.title.color; - bitcoinBackgroundColor = Theme.of(context).accentTextTheme.title.backgroundColor; - bitcoinTextColor = Theme.of(context).primaryTextTheme.title.color; - setState(() {}); } @override @@ -75,71 +69,52 @@ class WalletTypeFormState extends State { S.of(context).choose_wallet_currency, textAlign: TextAlign.center, style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Theme.of(context).primaryTextTheme.title.color - ), + fontSize: 16, + fontWeight: FontWeight.w600, + color: Theme.of(context).primaryTextTheme.title.color), ), ), - Padding( - padding: EdgeInsets.only(top: 24), - child: SelectButton( - image: bitcoinIcon, - text: 'Bitcoin', - color: bitcoinBackgroundColor, - textColor: bitcoinTextColor, - onTap: () {}), - ), - Padding( - padding: EdgeInsets.only(top: 20), - child: SelectButton( - image: moneroIcon, - text: 'Monero', - color: moneroBackgroundColor, - textColor: moneroTextColor, - onTap: () => onSelectMoneroButton(context)), - ) + ...types.map((type) => Padding( + padding: EdgeInsets.only(top: 24), + child: SelectButton( + image: _iconFor(type), + text: walletTypeToString(type), + color: _backgroundColorFor(selected == type), + textColor: _textColorFor(selected == type), + onTap: () => setState(() => selected = type)), + )) ], ), - bottomSectionPadding: EdgeInsets.only( - left: 24, - right: 24, - bottom: 24 - ), + bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), bottomSection: PrimaryButton( - onPressed: () => Navigator.of(context).pushNamed(Routes.newWallet), + onPressed: () => widget.onTypeSelected(context, selected), text: S.of(context).seed_language_next, color: Colors.green, textColor: Colors.white, - isDisabled: isDisabledButton, + isDisabled: selected == null, ), ), ); } - void onSelectMoneroButton(BuildContext context) { - isMoneroSelected = true; - isBitcoinSelected = false; - isDisabledButton = false; - - moneroBackgroundColor = Theme.of(context).accentTextTheme.title.decorationColor; - moneroTextColor = Theme.of(context).primaryTextTheme.title.backgroundColor; - bitcoinBackgroundColor = Theme.of(context).accentTextTheme.title.backgroundColor; - bitcoinTextColor = Theme.of(context).primaryTextTheme.title.color; - - setState(() {}); + // FIXME: Move color selection inside ui element; add isSelected to buttons. + + Color _backgroundColorFor(bool isSelected) => isSelected + ? Theme.of(context).accentTextTheme.title.decorationColor + : Theme.of(context).accentTextTheme.title.backgroundColor; + + Color _textColorFor(bool isSelected) => isSelected + ? Theme.of(context).primaryTextTheme.title.backgroundColor + : Theme.of(context).primaryTextTheme.title.color; + + Image _iconFor(WalletType type) { + switch (type) { + case WalletType.monero: + return moneroIcon; + case WalletType.bitcoin: + return bitcoinIcon; + default: + return null; + } } - - void onSelectBitcoinButton(BuildContext context) { - isMoneroSelected = false; - isBitcoinSelected = true; - isDisabledButton = false; - - moneroBackgroundColor = Theme.of(context).accentTextTheme.title.backgroundColor; - moneroTextColor = Theme.of(context).primaryTextTheme.title.color; - bitcoinBackgroundColor = moneroBackgroundColor = Theme.of(context).accentTextTheme.title.decorationColor; - bitcoinTextColor = Theme.of(context).primaryTextTheme.title.backgroundColor; - - setState(() {}); - } -} \ No newline at end of file +} diff --git a/lib/src/screens/receive/receive_page.dart b/lib/src/screens/receive/receive_page.dart index 2479404e..e4e58826 100644 --- a/lib/src/screens/receive/receive_page.dart +++ b/lib/src/screens/receive/receive_page.dart @@ -1,3 +1,5 @@ +import 'package:cake_wallet/palette.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; @@ -16,347 +18,256 @@ import 'package:cake_wallet/src/screens/receive/widgets/header_tile.dart'; import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; import 'package:cake_wallet/themes.dart'; import 'package:cake_wallet/theme_changer.dart'; +import 'package:cake_wallet/core/amount_validator.dart'; +import 'package:cake_wallet/src/screens/receive/widgets/address_cell.dart'; +import 'package:cake_wallet/view_model/address_list/account_list_header.dart'; +import 'package:cake_wallet/view_model/address_list/address_list_header.dart'; +import 'package:cake_wallet/view_model/address_list/address_list_item.dart'; +import 'package:cake_wallet/view_model/address_list/address_list_view_model.dart'; -class ReceivePage extends StatefulWidget { - @override - ReceivePageState createState() => ReceivePageState(); -} +class ReceivePage extends BasePage { + ReceivePage({this.addressListViewModel}) + : amountController = TextEditingController(), + _formKey = GlobalKey() { + amountController.addListener(() => addressListViewModel.amount = + _formKey.currentState.validate() ? amountController.text : ''); + } -class ReceivePageState extends State { - final amountController = TextEditingController(); - final _formKey = GlobalKey(); - final _backArrowImage = Image.asset('assets/images/back_arrow.png'); - final _backArrowImageDarkTheme = - Image.asset('assets/images/back_arrow_dark_theme.png'); + final AddressListViewModel addressListViewModel; + final TextEditingController amountController; + final GlobalKey _formKey; @override - void dispose() { - amountController.dispose(); - super.dispose(); - } + Color get backgroundLightColor => Colors.transparent; @override - Widget build(BuildContext context) { - final walletStore = Provider.of(context); - final subaddressListStore = Provider.of(context); - final accountListStore = Provider.of(context); + Color get backgroundDarkColor => Colors.transparent; - final shareImage = Image.asset('assets/images/share.png', - color: Theme.of(context).primaryTextTheme.title.color, - ); - final copyImage = Image.asset('assets/images/copy_content.png', - color: Theme.of(context).primaryTextTheme.title.color, - ); + @override + Widget Function(BuildContext, Widget) get rootWrapper => + (BuildContext context, Widget scaffold) => Container( + decoration: BoxDecoration( + gradient: LinearGradient(colors: [ + Theme.of(context).scaffoldBackgroundColor, + Theme.of(context).primaryColor + ], begin: Alignment.topLeft, end: Alignment.bottomRight)), + child: scaffold); - final currentColor = Theme.of(context).accentTextTheme.subtitle.decorationColor; - final notCurrentColor = Theme.of(context).backgroundColor; + @override + Widget middle(BuildContext context) => Text( + S.of(context).receive, + style: TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.bold, + color: Theme.of(context).primaryTextTheme.title.color), + ); - final currentTextColor = Colors.blue; - final notCurrentTextColor = Theme.of(context).primaryTextTheme.caption.color; + @override + Widget trailing(BuildContext context) { + final shareImage = Image.asset('assets/images/share.png', + color: Theme.of(context).primaryTextTheme.title.color); - final _themeChanger = Provider.of(context); - Image _backButton; + return SizedBox( + height: 20.0, + width: 14.0, + child: ButtonTheme( + minWidth: double.minPositive, + child: FlatButton( + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + padding: EdgeInsets.all(0), + onPressed: () => Share.text(S.current.share_address, + addressListViewModel.address.address, 'text/plain'), + child: shareImage), + ), + ); + } - if (_themeChanger.getTheme() == Themes.darkTheme) { - _backButton = _backArrowImageDarkTheme; - } else { - _backButton = _backArrowImage; - } + @override + Widget build(BuildContext context) { + return super.build(context); + } - amountController.addListener(() { - if (_formKey.currentState.validate()) { - walletStore.onChangedAmountValue(amountController.text); - } else { - walletStore.onChangedAmountValue(''); - } - }); + @override + Widget body(BuildContext context) { + final copyImage = Image.asset('assets/images/copy_content.png', + color: Theme.of(context).primaryTextTheme.title.color); - return Scaffold( - resizeToAvoidBottomPadding: false, - body: Container( - height: MediaQuery.of(context).size.height, - width: MediaQuery.of(context).size.width, - padding: EdgeInsets.only(top: 24), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Theme.of(context).scaffoldBackgroundColor, - Theme.of(context).primaryColor - ], - begin: Alignment.centerLeft, - end: Alignment.centerRight - ) + return SingleChildScrollView( + child: Column( + children: [ + SizedBox(height: 25), + Row(children: [ + Spacer(flex: 4), + Observer( + builder: (_) => Flexible( + flex: 6, + child: Center( + child: AspectRatio( + aspectRatio: 1.0, + child: QrImage( + data: addressListViewModel.uri.toString(), + backgroundColor: Colors.transparent, + foregroundColor: Theme.of(context) + .primaryTextTheme + .display4 + .color, + ))))), + Spacer(flex: 4) + ]), + Padding( + padding: EdgeInsets.fromLTRB(24, 40, 24, 0), + child: Row( + children: [ + Expanded( + child: Form( + key: _formKey, + child: BaseTextFormField( + controller: amountController, + keyboardType: + TextInputType.numberWithOptions(decimal: true), + inputFormatters: [ + BlacklistingTextInputFormatter( + RegExp('[\\-|\\ |\\,]')) + ], + textAlign: TextAlign.center, + hintText: S.of(context).receive_amount, + borderColor: Theme.of(context) + .primaryTextTheme + .headline5 + .color + .withOpacity(0.4), + validator: AmountValidator(), + autovalidate: true, + placeholderTextStyle: TextStyle( + color: Theme.of(context) + .primaryTextTheme + .headline5 + .color, + fontSize: 20, + fontWeight: FontWeight.w600)))) + ], + ), ), - child: Column( - children: [ - Container( - padding: EdgeInsets.only( - top: 10, - bottom: 20, - left: 5, - right: 10 - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox( - height: 44, - width: 44, - child: ButtonTheme( - minWidth: double.minPositive, - child: FlatButton( - highlightColor: Colors.transparent, - splashColor: Colors.transparent, - padding: EdgeInsets.all(0), - onPressed: () => Navigator.of(context).pop(), - child: _backButton), - ), - ), - Text( - S.of(context).receive, - style: TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.bold, - color: Theme.of(context).primaryTextTheme.title.color), - ), - SizedBox( - height: 44.0, - width: 44.0, - child: ButtonTheme( - minWidth: double.minPositive, - child: FlatButton( - highlightColor: Colors.transparent, - splashColor: Colors.transparent, - padding: EdgeInsets.all(0), - onPressed: () => Share.text( - S.current.share_address, walletStore.subaddress.address, 'text/plain'), - child: shareImage), - ), - ) - ], - ), - ), - Expanded( - child: SingleChildScrollView( - child: Column( - children: [ - Observer(builder: (_) { - return Row( - children: [ - Spacer( - flex: 1, - ), - Flexible( - flex: 2, - child: Center( - child: AspectRatio( - aspectRatio: 1.0, - child: QrImage( - data: walletStore.subaddress.address + - walletStore.amountValue, - backgroundColor: Colors.transparent, - foregroundColor: Theme.of(context).primaryTextTheme.display4.color, - ), - ), - )), - Spacer( - flex: 1, - ) - ], - ); - }), - Padding( - padding: EdgeInsets.all(24), - child: Row( - children: [ - Expanded( - child: Form( - key: _formKey, - child: BaseTextFormField( - controller: amountController, - keyboardType: TextInputType.numberWithOptions(decimal: true), - inputFormatters: [ - BlacklistingTextInputFormatter( - RegExp('[\\-|\\ |\\,]')) - ], - textAlign: TextAlign.center, - hintText: S.of(context).receive_amount, - borderColor: Theme.of(context).primaryTextTheme.caption.color, - validator: (value) { - walletStore.validateAmount(value); - return walletStore.errorMessage; - }, - autovalidate: true, - ) - ) - ) - ], - ), - ), - Padding( - padding: EdgeInsets.only(left: 24, right: 24, bottom: 24), - child: Builder( - builder: (context) => Observer( - builder: (context) => GestureDetector( - onTap: () { - Clipboard.setData(ClipboardData( - text: walletStore.subaddress.address)); - Scaffold.of(context).showSnackBar(SnackBar( - content: Text( - S.of(context).copied_to_clipboard, - style: TextStyle(color: Colors.white), - ), - backgroundColor: Colors.green, - duration: Duration(milliseconds: 500), - )); - }, - child: Container( - height: 48, - padding: EdgeInsets.only(left: 24, right: 24), - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(24)), - color: Theme.of(context).primaryTextTheme.overline.color - ), - child: Row( - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - child: Text( - walletStore.subaddress.address, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: Theme.of(context).primaryTextTheme.title.color - ), - ), - ), - Padding( - padding: EdgeInsets.only(left: 12), - child: copyImage, - ) - ], + Padding( + padding: EdgeInsets.only(left: 24, right: 24, bottom: 40, top: 40), + child: Builder( + builder: (context) => Observer( + builder: (context) => GestureDetector( + onTap: () { + Clipboard.setData(ClipboardData( + text: addressListViewModel.address.address)); + Scaffold.of(context).showSnackBar(SnackBar( + content: Text( + S.of(context).copied_to_clipboard, + style: TextStyle(color: Colors.white), + ), + backgroundColor: Colors.green, + duration: Duration(milliseconds: 500), + )); + }, + child: Container( + height: 48, + padding: EdgeInsets.only(left: 24, right: 24), + decoration: BoxDecoration( + borderRadius: + BorderRadius.all(Radius.circular(24)), + color: Theme.of(context) + .primaryTextTheme + .overline + .color), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: Text( + addressListViewModel.address.address, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Theme.of(context) + .primaryTextTheme + .title + .color), ), ), - ) - ) - ), - ), - Observer( - builder: (_) => ListView.separated( - separatorBuilder: (context, index) => Divider( - height: 1, - color: Theme.of(context).dividerColor, - ), - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: subaddressListStore.subaddresses.length + 2, - itemBuilder: (context, index) { - - if (index == 0) { - return ClipRRect( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(24), - topRight: Radius.circular(24) - ), - child: HeaderTile( - onTap: () async { - await showDialog( - context: context, - builder: (BuildContext context) { - return AccountListPage(accountListStore: accountListStore); - } - ); - }, - title: walletStore.account.label, - icon: Icon( - Icons.arrow_forward_ios, - size: 14, - color: Theme.of(context).primaryTextTheme.title.color, - ) - ), - ); - } - - if (index == 1) { - return HeaderTile( - onTap: () => Navigator.of(context) - .pushNamed(Routes.newSubaddress), - title: S.of(context).subaddresses, - icon: Icon( - Icons.add, - size: 20, - color: Theme.of(context).primaryTextTheme.title.color, - ) - ); - } - - index -= 2; + Padding( + padding: EdgeInsets.only(left: 12), + child: copyImage, + ) + ], + ), + ), + ))), + ), + Observer( + builder: (_) => ListView.separated( + separatorBuilder: (context, _) => + Divider(height: 1, color: Theme.of(context).dividerColor), + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: addressListViewModel.items.length, + itemBuilder: (context, index) { + final item = addressListViewModel.items[index]; + Widget cell = Container(); - return Observer( - builder: (_) { - final subaddress = subaddressListStore.subaddresses[index]; - final isCurrent = - walletStore.subaddress.address == subaddress.address; + if (item is AccountListHeader) { + cell = HeaderTile( + onTap: () async { + await showDialog( + context: context, + builder: (BuildContext context) { +// return AccountListPage( +// accountListStore: +// accountListStore); + }); + }, + title: addressListViewModel.accountLabel, + icon: Icon( + Icons.arrow_forward_ios, + size: 14, + color: + Theme.of(context).primaryTextTheme.title.color, + )); + } - final label = subaddress.label.isNotEmpty - ? subaddress.label - : subaddress.address; + if (item is AddressListHeader) { + cell = HeaderTile( + onTap: () => Navigator.of(context) + .pushNamed(Routes.newSubaddress), + title: S.of(context).subaddresses, + icon: Icon( + Icons.add, + size: 20, + color: + Theme.of(context).primaryTextTheme.title.color, + )); + } - final content = InkWell( - onTap: () => walletStore.setSubaddress(subaddress), - child: Container( - color: isCurrent ? currentColor : notCurrentColor, - padding: EdgeInsets.only( - left: 24, - right: 24, - top: 28, - bottom: 28 - ), - child: Text( - label, - style: TextStyle( - fontSize: subaddress.label.isNotEmpty - ? 18 : 10, - fontWeight: FontWeight.bold, - color: isCurrent - ? currentTextColor - : notCurrentTextColor, - ), - ), - ), - ); + if (item is AddressListItem) { + cell = Observer( + builder: (_) => AddressCell.fromItem(item, + isCurrent: item.address == + addressListViewModel.address.address, + onTap: (_) => + addressListViewModel.address = item, + onEdit: () => Navigator.of(context) + .pushNamed(Routes.newSubaddress, arguments: item))); + } - return isCurrent - ? content - : Slidable( - key: Key(subaddress.address), - actionPane: SlidableDrawerActionPane(), - child: content, - secondaryActions: [ - IconSlideAction( - caption: S.of(context).edit, - color: Theme.of(context).primaryTextTheme.overline.color, - icon: Icons.edit, - onTap: () => Navigator.of(context) - .pushNamed(Routes.newSubaddress, arguments: subaddress), - ) - ] - ); - } - ); - } - ) - ), - ], - ), - ) - ) - ], - ) + return index != 0 + ? cell + : ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(24), + topRight: Radius.circular(24)), + child: cell, + ); + })), + ], ), ); } -} \ No newline at end of file +} diff --git a/lib/src/screens/receive/widgets/address_cell.dart b/lib/src/screens/receive/widgets/address_cell.dart new file mode 100644 index 00000000..dfe1cf66 --- /dev/null +++ b/lib/src/screens/receive/widgets/address_cell.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/view_model/address_list/address_list_item.dart'; + +class AddressCell extends StatelessWidget { + factory AddressCell.fromItem(AddressListItem item, + {@required bool isCurrent, + Function(String) onTap, + Function() onEdit}) => + AddressCell( + address: item.address, + name: item.name, + isCurrent: isCurrent, + onTap: onTap, + onEdit: onEdit); + + AddressCell( + {@required this.address, + @required this.name, + @required this.isCurrent, + this.onTap, + this.onEdit}); + + final String address; + final String name; + final bool isCurrent; + final Function(String) onTap; + final Function() onEdit; + + String get label => name ?? address; + + @override + Widget build(BuildContext context) { + const currentTextColor = Colors.blue; // FIXME: Why it's defined here ? + final currentColor = + Theme.of(context).accentTextTheme.subtitle.decorationColor; + final notCurrentColor = Theme.of(context).backgroundColor; + final notCurrentTextColor = + Theme.of(context).primaryTextTheme.caption.color; + final Widget cell = InkWell( + onTap: () => onTap(address), + child: Container( + color: isCurrent ? currentColor : notCurrentColor, + padding: EdgeInsets.only(left: 24, right: 24, top: 28, bottom: 28), + child: Text( + name ?? address, + style: TextStyle( + fontSize: name?.isNotEmpty ?? false ? 18 : 10, + fontWeight: FontWeight.bold, + color: isCurrent ? currentTextColor : notCurrentTextColor, + ), + ), + )); + + return isCurrent + ? cell + : Slidable( + key: Key(address), + actionPane: SlidableDrawerActionPane(), + child: cell, + secondaryActions: [ + IconSlideAction( + caption: S.of(context).edit, + color: Theme.of(context).primaryTextTheme.overline.color, + icon: Icons.edit, + onTap: () => onEdit?.call()) + ]); + } +} diff --git a/lib/src/screens/restore/restore_wallet_from_seed_details.dart b/lib/src/screens/restore/restore_wallet_from_seed_details.dart index 2347b34b..5a83a8b3 100644 --- a/lib/src/screens/restore/restore_wallet_from_seed_details.dart +++ b/lib/src/screens/restore/restore_wallet_from_seed_details.dart @@ -1,26 +1,35 @@ import 'package:mobx/mobx.dart'; -import 'package:provider/provider.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/stores/wallet_restoration/wallet_restoration_store.dart'; -import 'package:cake_wallet/src/stores/wallet_restoration/wallet_restoration_state.dart'; +import 'package:cake_wallet/core/validator.dart'; +import 'package:cake_wallet/view_model/wallet_creation_state.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/widgets/blockchain_height_widget.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/view_model/wallet_restoration_from_seed_vm.dart'; class RestoreWalletFromSeedDetailsPage extends BasePage { + RestoreWalletFromSeedDetailsPage( + {@required this.walletRestorationFromSeedVM}); + + final WalletRestorationFromSeedVM walletRestorationFromSeedVM; + @override String get title => S.current.restore_wallet_restore_description; @override - Widget body(BuildContext context) => RestoreFromSeedDetailsForm(); + Widget body(BuildContext context) => RestoreFromSeedDetailsForm( + walletRestorationFromSeedVM: walletRestorationFromSeedVM); } class RestoreFromSeedDetailsForm extends StatefulWidget { + RestoreFromSeedDetailsForm({@required this.walletRestorationFromSeedVM}); + + final WalletRestorationFromSeedVM walletRestorationFromSeedVM; + @override _RestoreFromSeedDetailsFormState createState() => _RestoreFromSeedDetailsFormState(); @@ -31,31 +40,17 @@ class _RestoreFromSeedDetailsFormState final _formKey = GlobalKey(); final _blockchainHeightKey = GlobalKey(); final _nameController = TextEditingController(); + ReactionDisposer _stateReaction; @override - void dispose() { - _nameController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final walletRestorationStore = Provider.of(context); - - _nameController.addListener(() { - if (_nameController.text.isNotEmpty) { - walletRestorationStore.setDisabledState(false); - } else { - walletRestorationStore.setDisabledState(true); - } - }); - - reaction((_) => walletRestorationStore.state, (WalletRestorationState state) { - if (state is WalletRestoredSuccessfully) { + void initState() { + _stateReaction = reaction((_) => widget.walletRestorationFromSeedVM.state, + (WalletCreationState state) { + if (state is WalletCreatedSuccessfully) { Navigator.of(context).popUntil((route) => route.isFirst); } - if (state is WalletRestorationFailure) { + if (state is WalletCreationFailure) { WidgetsBinding.instance.addPostFrameCallback((_) { showDialog( context: context, @@ -64,73 +59,87 @@ class _RestoreFromSeedDetailsFormState alertTitle: S.current.restore_title_from_seed, alertContent: state.error, buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop() - ); + buttonAction: () => Navigator.of(context).pop()); }); }); } }); + _nameController.addListener( + () => widget.walletRestorationFromSeedVM.name = _nameController.text); + super.initState(); + } + + @override + void dispose() { + _nameController.dispose(); + _stateReaction.reaction.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { return Container( padding: EdgeInsets.only(left: 24, right: 24), child: ScrollableWithBottomSection( contentPadding: EdgeInsets.only(bottom: 24.0), content: Form( key: _formKey, - child: Column( + child: Column(children: [ + Row( children: [ - Row( - children: [ - Flexible( - child: Container( - padding: EdgeInsets.only(top: 20.0), - child: TextFormField( - style: TextStyle( - fontSize: 16.0, - color: Theme.of(context).primaryTextTheme.title.color - ), - controller: _nameController, - decoration: InputDecoration( - hintStyle: TextStyle( - color: Theme.of(context).primaryTextTheme.caption.color, - fontSize: 16 - ), - hintText: S.of(context).restore_wallet_name, - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).dividerColor, - width: 1.0)), - enabledBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).dividerColor, - width: 1.0))), - validator: (value) { - walletRestorationStore - .validateWalletName(value); - return walletRestorationStore.errorMessage; - }, - ), - )) - ], - ), - BlockchainHeightWidget(key: _blockchainHeightKey), - ]), + Flexible( + child: Container( + padding: EdgeInsets.only(top: 20.0), + child: TextFormField( + style: TextStyle( + fontSize: 16.0, + color: Theme.of(context).primaryTextTheme.title.color), + controller: _nameController, + decoration: InputDecoration( + hintStyle: TextStyle( + color: Theme.of(context) + .primaryTextTheme + .caption + .color, + fontSize: 16), + hintText: S.of(context).restore_wallet_name, + 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: WalletNameValidator(), + ), + )) + ], + ), + if (widget.walletRestorationFromSeedVM.hasRestorationHeight) + BlockchainHeightWidget( + key: _blockchainHeightKey, + onHeightChange: (height) { + widget.walletRestorationFromSeedVM.height = height; + print(height); + }), + ]), ), bottomSectionPadding: EdgeInsets.only(bottom: 24), bottomSection: Observer(builder: (_) { return LoadingPrimaryButton( onPressed: () { if (_formKey.currentState.validate()) { - walletRestorationStore.restoreFromSeed( - name: _nameController.text, - restoreHeight: _blockchainHeightKey.currentState.height); + widget.walletRestorationFromSeedVM.create(); } }, - isLoading: walletRestorationStore.state is WalletIsRestoring, + isLoading: + widget.walletRestorationFromSeedVM.state is WalletCreating, text: S.of(context).restore_recover, color: Colors.green, textColor: Colors.white, - isDisabled: walletRestorationStore.disabledState, + isDisabled: _nameController.text.isNotEmpty, ); }), ), diff --git a/lib/src/screens/restore/restore_wallet_from_seed_page.dart b/lib/src/screens/restore/restore_wallet_from_seed_page.dart index 74820d50..5c46b32e 100644 --- a/lib/src/screens/restore/restore_wallet_from_seed_page.dart +++ b/lib/src/screens/restore/restore_wallet_from_seed_page.dart @@ -1,27 +1,19 @@ -import 'package:provider/provider.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/domain/services/wallet_list_service.dart'; -import 'package:cake_wallet/src/domain/services/wallet_service.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/stores/wallet_restoration/wallet_restoration_store.dart'; import 'package:cake_wallet/src/widgets/seed_widget.dart'; -import 'package:cake_wallet/src/stores/seed_language/seed_language_store.dart'; +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; +import 'package:cake_wallet/core/seed_validator.dart'; import 'package:cake_wallet/palette.dart'; +import 'package:cake_wallet/core/mnemonic_length.dart'; class RestoreWalletFromSeedPage extends BasePage { - RestoreWalletFromSeedPage( - {@required this.walletsService, - @required this.walletService, - @required this.sharedPreferences}); + RestoreWalletFromSeedPage({@required this.type, @required this.language}); - final WalletListService walletsService; - final WalletService walletService; - final SharedPreferences sharedPreferences; + final WalletType type; + final String language; final formKey = GlobalKey<_RestoreFromSeedFormState>(); @override @@ -34,11 +26,14 @@ class RestoreWalletFromSeedPage extends BasePage { Color get backgroundDarkColor => PaletteDark.lightNightBlue; @override - Widget body(BuildContext context) => RestoreFromSeedForm(key: formKey); + Widget body(BuildContext context) => + RestoreFromSeedForm(key: formKey, type: type, language: language); } class RestoreFromSeedForm extends StatefulWidget { - RestoreFromSeedForm({Key key}) : super(key: key); + RestoreFromSeedForm({Key key, this.type, this.language}) : super(key: key); + final WalletType type; + final String language; @override _RestoreFromSeedFormState createState() => _RestoreFromSeedFormState(); @@ -46,13 +41,11 @@ class RestoreFromSeedForm extends StatefulWidget { class _RestoreFromSeedFormState extends State { final _seedKey = GlobalKey(); - void clear() => _seedKey.currentState.clear(); + + String mnemonic() => _seedKey.currentState.items.map((e) => e.text).join(' '); @override Widget build(BuildContext context) { - final walletRestorationStore = Provider.of(context); - final seedLanguageStore = Provider.of(context); - return GestureDetector( onTap: () => SystemChannels.textInput.invokeMethod('TextInput.hide'), @@ -60,11 +53,13 @@ class _RestoreFromSeedFormState extends State { color: Theme.of(context).backgroundColor, child: SeedWidget( key: _seedKey, - onMnemoticChange: (seed) => walletRestorationStore.setSeed(seed), + maxLength: mnemonicLength(widget.type), + onMnemonicChange: (seed) => null, onFinish: () => Navigator.of(context).pushNamed( Routes.restoreWalletFromSeedDetails, - arguments: _seedKey.currentState.items), - seedLanguage: seedLanguageStore.selectedSeedLanguage, + arguments: [widget.type, widget.language, mnemonic()]), + validator: + SeedValidator(type: widget.type, language: widget.language), ), ), ); diff --git a/lib/src/screens/restore/restore_wallet_options_page.dart b/lib/src/screens/restore/restore_wallet_options_page.dart index 784f84ae..bb805e7c 100644 --- a/lib/src/screens/restore/restore_wallet_options_page.dart +++ b/lib/src/screens/restore/restore_wallet_options_page.dart @@ -1,19 +1,21 @@ import 'package:flutter/material.dart'; -import 'package:flutter/cupertino.dart'; +import 'package:provider/provider.dart'; import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/palette.dart'; import 'package:cake_wallet/src/screens/restore/widgets/restore_button.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/domain/common/wallet_type.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/stores/seed_language/seed_language_store.dart'; -import 'package:provider/provider.dart'; class RestoreWalletOptionsPage extends BasePage { - RestoreWalletOptionsPage({@required this.type}); + RestoreWalletOptionsPage( + {@required this.type, + @required this.onRestoreFromSeed, + @required this.onRestoreFromKeys}); - static const _aspectRatioImage = 2.086; final WalletType type; + final Function(BuildContext context) onRestoreFromSeed; + final Function(BuildContext context) onRestoreFromKeys; @override String get title => S.current.restore_seed_keys_restore; @@ -23,8 +25,6 @@ class RestoreWalletOptionsPage extends BasePage { @override Widget body(BuildContext context) { - final seedLanguageStore = Provider.of(context); - return Container( width: double.infinity, height: double.infinity, @@ -33,28 +33,56 @@ class RestoreWalletOptionsPage extends BasePage { child: Column( children: [ RestoreButton( - onPressed: () { - seedLanguageStore - .setCurrentRoute(Routes.restoreWalletFromSeed); - Navigator.pushNamed(context, Routes.seedLanguage); - }, + onPressed: () => onRestoreFromSeed(context), image: imageSeed, title: S.of(context).restore_title_from_seed, - description: S.of(context).restore_description_from_seed), + description: _fromSeedDescription(context)), Padding( padding: EdgeInsets.only(top: 24), child: RestoreButton( - onPressed: () { - seedLanguageStore - .setCurrentRoute(Routes.restoreWalletFromKeys); - Navigator.pushNamed(context, Routes.seedLanguage); - }, + onPressed: () => onRestoreFromKeys(context), image: imageKeys, - title: S.of(context).restore_title_from_keys, - description: S.of(context).restore_description_from_keys), + title: _fromKeyTitle(context), + description: _fromKeyDescription(context)), ) ], ), )); } + + String _fromSeedDescription(BuildContext context) { + switch (type) { + case WalletType.monero: + return S.of(context).restore_description_from_seed; + case WalletType.bitcoin: + // TODO: Add transaction for bitcoin description. + return 'Restore your wallet from 12 word combination code'; + default: + return ''; + } + } + + String _fromKeyDescription(BuildContext context) { + switch (type) { + case WalletType.monero: + return S.of(context).restore_description_from_keys; + case WalletType.bitcoin: + // TODO: Add transaction for bitcoin description. + return 'Restore your wallet from generated WIF string from your private keys'; + default: + return ''; + } + } + + String _fromKeyTitle(BuildContext context) { + switch (type) { + case WalletType.monero: + return S.of(context).restore_title_from_keys; + case WalletType.bitcoin: + // TODO: Add transaction for bitcoin description. + return 'Restore from WIF'; + default: + return ''; + } + } } diff --git a/lib/src/screens/root/root.dart b/lib/src/screens/root/root.dart index 917ff347..48d6eadd 100644 --- a/lib/src/screens/root/root.dart +++ b/lib/src/screens/root/root.dart @@ -1,10 +1,15 @@ +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/src/screens/dashboard/dashboard_page.dart'; +import 'package:cake_wallet/store/app_store.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:hive/hive.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/src/stores/authentication/authentication_store.dart'; +import 'package:cake_wallet/store/authentication_store.dart'; + +//import 'package:cake_wallet/src/stores/authentication/authentication_store.dart'; import 'package:cake_wallet/src/stores/price/price_store.dart'; import 'package:cake_wallet/src/stores/settings/settings_store.dart'; import 'package:cake_wallet/src/stores/wallet/wallet_store.dart'; @@ -21,7 +26,10 @@ import 'package:cake_wallet/src/screens/auth/auth_page.dart'; import 'package:cake_wallet/src/screens/welcome/create_welcome_page.dart'; class Root extends StatefulWidget { - Root({Key key}) : super(key: key); + Root({Key key, this.authenticationStore, this.appStore}) : super(key: key); + + final AuthenticationStore authenticationStore; + final AppStore appStore; @override RootState createState() => RootState(); @@ -30,7 +38,6 @@ class Root extends StatefulWidget { class RootState extends State with WidgetsBindingObserver { bool _isInactive; bool _postFrameCallback; - AuthenticationStore _authenticationStore; @override void initState() { @@ -48,12 +55,12 @@ class RootState extends State with WidgetsBindingObserver { return; } - if (!_isInactive && - _authenticationStore.state == - AuthenticationState.authenticated || - _authenticationStore.state == AuthenticationState.active) { - setState(() => _isInactive = true); - } +// if (!_isInactive && +// widget.authenticationStore.state == +// AuthenticationState.authenticated || +// widget.authenticationStore.state == AuthenticationState.active) { +// setState(() => _isInactive = true); +// } break; default: @@ -63,18 +70,18 @@ class RootState extends State with WidgetsBindingObserver { @override Widget build(BuildContext context) { - _authenticationStore = Provider.of(context); - final sharedPreferences = Provider.of(context); - final walletListService = Provider.of(context); - final walletService = Provider.of(context); - final userService = Provider.of(context); - final priceStore = Provider.of(context); - final authenticationStore = Provider.of(context); - final trades = Provider.of>(context); - final transactionDescriptions = - Provider.of>(context); - final walletStore = Provider.of(context); - final settingsStore = Provider.of(context); +// _authenticationStore = Provider.of(context); +// final sharedPreferences = Provider.of(context); +// final walletListService = Provider.of(context); +// final walletService = Provider.of(context); +// final userService = Provider.of(context); +// final priceStore = Provider.of(context); +// final authenticationStore = Provider.of(context); +// final trades = Provider.of>(context); +// final transactionDescriptions = +// Provider.of>(context); +// final walletStore = Provider.of(context); +// final settingsStore = Provider.of(context); if (_isInactive && !_postFrameCallback) { _postFrameCallback = true; @@ -96,38 +103,51 @@ class RootState extends State with WidgetsBindingObserver { } return Observer(builder: (_) { - final state = _authenticationStore.state; + final state = widget.authenticationStore.state; + print(state); if (state == AuthenticationState.denied) { return createWelcomePage(); } - if (state == AuthenticationState.readyToLogin) { - return createLoginPage( - sharedPreferences: sharedPreferences, - userService: userService, - walletService: walletService, - walletListService: walletListService, - authenticationStore: authenticationStore); + if (state == AuthenticationState.installed) { + return getIt.get(); } - if (state == AuthenticationState.authenticated || - state == AuthenticationState.restored) { - return createDashboardPage( - walletService: walletService, - priceStore: priceStore, - trades: trades, - transactionDescriptions: transactionDescriptions, - walletStore: walletStore, - settingsStore: settingsStore); + if (state == AuthenticationState.allowed) { + return getIt.get(); } - if (state == AuthenticationState.created) { - return createSeedPage( - settingsStore: settingsStore, - walletService: walletService, - callback: () => - _authenticationStore.state = AuthenticationState.authenticated); - } +// if (state == AuthenticationState.denied) { +// return createWelcomePage(); +// } + +// if (state == AuthenticationState.readyToLogin) { +// return createLoginPage( +// sharedPreferences: sharedPreferences, +// userService: userService, +// walletService: walletService, +// walletListService: walletListService, +// authenticationStore: authenticationStore); +// } + +// if (state == AuthenticationState.authenticated || +// state == AuthenticationState.restored) { +// return createDashboardPage( +// walletService: walletService, +// priceStore: priceStore, +// trades: trades, +// transactionDescriptions: transactionDescriptions, +// walletStore: walletStore, +// settingsStore: settingsStore); +// } + +// if (state == AuthenticationState.created) { +// return createSeedPage( +// settingsStore: settingsStore, +// walletService: walletService, +// callback: () => +// _authenticationStore.state = AuthenticationState.authenticated); +// } return Container(color: Colors.white); }); diff --git a/lib/src/screens/seed_language/seed_language_page.dart b/lib/src/screens/seed_language/seed_language_page.dart index c17109b9..48b2fd5b 100644 --- a/lib/src/screens/seed_language/seed_language_page.dart +++ b/lib/src/screens/seed_language/seed_language_page.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/src/widgets/seed_language_selector.dart'; import 'package:provider/provider.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter/material.dart'; @@ -11,79 +12,68 @@ import 'package:cake_wallet/src/screens/new_wallet/widgets/select_button.dart'; import 'package:cake_wallet/src/screens/seed_language/widgets/seed_language_picker.dart'; class SeedLanguage extends BasePage { + SeedLanguage({this.onConfirm}); + + final Function(BuildContext, String) onConfirm; + @override - Widget body(BuildContext context) => SeedLanguageForm(); + Widget body(BuildContext context) => SeedLanguageForm(onConfirm: onConfirm); } class SeedLanguageForm extends StatefulWidget { + SeedLanguageForm({this.onConfirm}); + + final Function(BuildContext, String) onConfirm; + @override SeedLanguageFormState createState() => SeedLanguageFormState(); } class SeedLanguageFormState extends State { static const aspectRatioImage = 1.22; + final walletNameImage = Image.asset('assets/images/wallet_name.png'); + final _languageSelectorKey = GlobalKey(); @override Widget build(BuildContext context) { - final seedLanguageStore = Provider.of(context); - - final List seedLocales = [ - S.current.seed_language_english, - S.current.seed_language_chinese, - S.current.seed_language_dutch, - S.current.seed_language_german, - S.current.seed_language_japanese, - S.current.seed_language_portuguese, - S.current.seed_language_russian, - S.current.seed_language_spanish - ]; - return Container( padding: EdgeInsets.only(top: 24), child: ScrollableWithBottomSection( contentPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), - content: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - padding: EdgeInsets.only(left: 12, right: 12), - child: AspectRatio( - aspectRatio: aspectRatioImage, - child: FittedBox(child: walletNameImage, fit: BoxFit.fill)), - ), - Padding(padding: EdgeInsets.only(top: 40), - child: Text( - S.of(context).seed_language_choose, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.w600, - color: Theme.of(context).primaryTextTheme.title.color - ), - ), - ), - Padding(padding: EdgeInsets.only(top: 24), - child: Observer( - builder: (_) => SelectButton( - image: null, - text: seedLocales[seedLanguages.indexOf(seedLanguageStore.selectedSeedLanguage)], - color: Theme.of(context).accentTextTheme.title.backgroundColor, - textColor: Theme.of(context).primaryTextTheme.title.color, - onTap: () async => await showDialog( - context: context, - builder: (BuildContext context) => SeedLanguagePicker() - ) - ) - ), - ) - ]), - bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), + content: + Column(crossAxisAlignment: CrossAxisAlignment.center, children: [ + Padding( + padding: EdgeInsets.only(left: 12, right: 12), + child: AspectRatio( + aspectRatio: aspectRatioImage, + child: FittedBox(child: walletNameImage, fit: BoxFit.fill)), + ), + Padding( + padding: EdgeInsets.only(top: 40), + child: Text( + S.of(context).seed_language_choose, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.w600, + color: Theme.of(context).primaryTextTheme.title.color), + ), + ), + Padding( + padding: EdgeInsets.only(top: 24), + child: SeedLanguageSelector( + key: _languageSelectorKey, + initialSelected: defaultSeedLanguage), + ) + ]), + bottomSectionPadding: + EdgeInsets.only(left: 24, right: 24, bottom: 24), bottomSection: Observer( builder: (context) { return PrimaryButton( - onPressed: () => - Navigator.of(context).popAndPushNamed(seedLanguageStore.currentRoute), + onPressed: () => widget + .onConfirm(context, _languageSelectorKey.currentState.selected), text: S.of(context).seed_language_next, color: Colors.green, textColor: Colors.white); diff --git a/lib/src/screens/seed_language/widgets/seed_language_picker.dart b/lib/src/screens/seed_language/widgets/seed_language_picker.dart index a08a4a32..cc476b29 100644 --- a/lib/src/screens/seed_language/widgets/seed_language_picker.dart +++ b/lib/src/screens/seed_language/widgets/seed_language_picker.dart @@ -17,7 +17,7 @@ List flagImages = [ Image.asset('assets/images/spain.png'), ]; -List languageCodes = [ +const List languageCodes = [ 'Eng', 'Chi', 'Ned', @@ -28,19 +28,39 @@ List languageCodes = [ 'Esp', ]; -enum Places {topLeft, topRight, bottomLeft, bottomRight, inside} +const defaultSeedLanguage = 'English'; + +const List seedLanguages = [ + defaultSeedLanguage, + 'Chinese (simplified)', + 'Dutch', + 'German', + 'Japanese', + 'Portuguese', + 'Russian', + 'Spanish' +]; + +enum Places { topLeft, topRight, bottomLeft, bottomRight, inside } class SeedLanguagePicker extends StatefulWidget { + SeedLanguagePicker({Key key, this.selected = defaultSeedLanguage}) + : super(key: key); + + final String selected; + @override - SeedLanguagePickerState createState() => SeedLanguagePickerState(); + SeedLanguagePickerState createState() => + SeedLanguagePickerState(selected: selected); } class SeedLanguagePickerState extends State { + SeedLanguagePickerState({this.selected}); + + String selected; @override Widget build(BuildContext context) { - final seedLanguageStore = Provider.of(context); - return GestureDetector( onTap: () => Navigator.of(context).pop(), child: Container( @@ -48,7 +68,8 @@ class SeedLanguagePickerState extends State { child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 3.0, sigmaY: 3.0), child: Container( - decoration: BoxDecoration(color: PaletteDark.darkNightBlue.withOpacity(0.75)), + decoration: BoxDecoration( + color: PaletteDark.darkNightBlue.withOpacity(0.75)), child: Center( child: Column( mainAxisSize: MainAxisSize.min, @@ -62,8 +83,7 @@ class SeedLanguagePickerState extends State { fontSize: 18, fontWeight: FontWeight.bold, decoration: TextDecoration.none, - color: Colors.white - ), + color: Colors.white), ), ), Padding( @@ -74,9 +94,8 @@ class SeedLanguagePickerState extends State { height: 300, width: 300, decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(14)), - color: Theme.of(context).dividerColor - ), + borderRadius: BorderRadius.all(Radius.circular(14)), + color: Theme.of(context).dividerColor), child: GridView.count( shrinkWrap: true, crossAxisCount: 3, @@ -85,71 +104,64 @@ class SeedLanguagePickerState extends State { crossAxisSpacing: 1, mainAxisSpacing: 1, children: List.generate(9, (index) { - if (index == 8) { - return gridTile( - isCurrent: false, - place: Places.bottomRight, - image: null, - text: '', - onTap: null); - + isCurrent: false, + place: Places.bottomRight, + image: null, + text: '', + onTap: null); } else { - final code = languageCodes[index]; final flag = flagImages[index]; - final isCurrent = index == seedLanguages.indexOf(seedLanguageStore.selectedSeedLanguage); + final isCurrent = + index == seedLanguages.indexOf(selected); if (index == 0) { return gridTile( - isCurrent: isCurrent, - place: Places.topLeft, - image: flag, - text: code, - onTap: () { - seedLanguageStore.setSelectedSeedLanguage(seedLanguages[index]); - Navigator.of(context).pop(); - } - ); + isCurrent: isCurrent, + place: Places.topLeft, + image: flag, + text: code, + onTap: () { + selected = seedLanguages[index]; + Navigator.of(context).pop(selected); + }); } if (index == 2) { return gridTile( - isCurrent: isCurrent, - place: Places.topRight, - image: flag, - text: code, - onTap: () { - seedLanguageStore.setSelectedSeedLanguage(seedLanguages[index]); - Navigator.of(context).pop(); - } - ); + isCurrent: isCurrent, + place: Places.topRight, + image: flag, + text: code, + onTap: () { + selected = seedLanguages[index]; + Navigator.of(context).pop(selected); + }); } if (index == 6) { return gridTile( + isCurrent: isCurrent, + place: Places.bottomLeft, + image: flag, + text: code, + onTap: () { + selected = seedLanguages[index]; + Navigator.of(context).pop(selected); + }); + } + + return gridTile( isCurrent: isCurrent, - place: Places.bottomLeft, + place: Places.inside, image: flag, text: code, onTap: () { - seedLanguageStore.setSelectedSeedLanguage(seedLanguages[index]); - Navigator.of(context).pop(); - } - ); - } - - return gridTile( - isCurrent: isCurrent, - place: Places.inside, - image: flag, - text: code, - onTap: () { - seedLanguageStore.setSelectedSeedLanguage(seedLanguages[index]); - Navigator.of(context).pop(); - } - ); + selected = seedLanguages[index]; + Navigator.of(context).pop(selected); + }); } }), ), @@ -165,13 +177,12 @@ class SeedLanguagePickerState extends State { ); } - Widget gridTile({ - @required bool isCurrent, - @required Places place, - @required Image image, - @required String text, - @required VoidCallback onTap}) { - + Widget gridTile( + {@required bool isCurrent, + @required Places place, + @required Image image, + @required String text, + @required VoidCallback onTap}) { BorderRadius borderRadius; final color = isCurrent ? Theme.of(context).accentTextTheme.subtitle.decorationColor @@ -199,40 +210,33 @@ class SeedLanguagePickerState extends State { } return GestureDetector( - onTap: onTap, - child: Container( - padding: EdgeInsets.all(10), - decoration: BoxDecoration( - borderRadius: borderRadius, - color: color - ), - child: Center( - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - image != null - ? image - : Offstage(), - Padding( - padding: image != null - ? EdgeInsets.only(left: 10) - : EdgeInsets.only(left: 0), - child: Text( - text, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - decoration: TextDecoration.none, - color: textColor + onTap: onTap, + child: Container( + padding: EdgeInsets.all(10), + decoration: BoxDecoration(borderRadius: borderRadius, color: color), + child: Center( + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + image != null ? image : Offstage(), + Padding( + padding: image != null + ? EdgeInsets.only(left: 10) + : EdgeInsets.only(left: 0), + child: Text( + text, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + decoration: TextDecoration.none, + color: textColor), ), - ), - ) - ], + ) + ], + ), ), - ), - ) - ); + )); } -} \ No newline at end of file +} diff --git a/lib/src/screens/subaddress/address_edit_or_create_page.dart b/lib/src/screens/subaddress/address_edit_or_create_page.dart new file mode 100644 index 00000000..e242ddce --- /dev/null +++ b/lib/src/screens/subaddress/address_edit_or_create_page.dart @@ -0,0 +1,75 @@ +import 'package:mobx/mobx.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/view_model/address_list/address_edit_or_create_view_model.dart'; +import 'package:cake_wallet/core/AddressLabelValidator.dart'; +import 'package:cake_wallet/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'; + +class AddressEditOrCreatePage extends BasePage { + AddressEditOrCreatePage({@required this.addressEditOrCreateViewModel}) + : _formKey = GlobalKey(), + _labelController = TextEditingController(), + super() { + _labelController.addListener( + () => addressEditOrCreateViewModel.label = _labelController.text); + _labelController.text = addressEditOrCreateViewModel.label; + print(_labelController.text); + print(addressEditOrCreateViewModel.label); + } + + final AddressEditOrCreateViewModel addressEditOrCreateViewModel; + final GlobalKey _formKey; + final TextEditingController _labelController; + + @override + String get title => S.current.new_subaddress_title; + + @override + Widget body(BuildContext context) { + reaction((_) => addressEditOrCreateViewModel.state, + (AddressEditOrCreateState state) { + if (state is AddressSavedSuccessfully) { + WidgetsBinding.instance + .addPostFrameCallback((_) => Navigator.of(context).pop()); + } + }); + + return Form( + key: _formKey, + child: Container( + padding: EdgeInsets.all(24.0), + child: Column( + children: [ + Expanded( + child: Center( + child: BaseTextFormField( + controller: _labelController, + hintText: S.of(context).new_subaddress_label_name, + validator: AddressLabelValidator()))), + Observer( + builder: (_) => LoadingPrimaryButton( + onPressed: () async { + if (_formKey.currentState.validate()) { + await addressEditOrCreateViewModel.save(); + } + }, + text: addressEditOrCreateViewModel.isEdit + ? S.of(context).rename + : S.of(context).new_subaddress_create, + color: Colors.green, + textColor: Colors.white, + isLoading: + addressEditOrCreateViewModel.state is AddressIsSaving, + isDisabled: + addressEditOrCreateViewModel.label?.isEmpty ?? true, + ), + ) + ], + ), + )); + } +} \ No newline at end of file diff --git a/lib/src/screens/subaddress/new_subaddress_page.dart b/lib/src/screens/subaddress/new_subaddress_page.dart deleted file mode 100644 index 8309ff4a..00000000 --- a/lib/src/screens/subaddress/new_subaddress_page.dart +++ /dev/null @@ -1,130 +0,0 @@ -import 'package:cake_wallet/src/domain/monero/subaddress.dart'; -import 'package:mobx/mobx.dart'; -import 'package:provider/provider.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/stores/subaddress_creation/subaddress_creation_state.dart'; -import 'package:cake_wallet/src/stores/subaddress_creation/subaddress_creation_store.dart'; -import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; - -class NewSubaddressPage extends BasePage { - NewSubaddressPage({this.subaddress}); - - final Subaddress subaddress; - - @override - String get title => S.current.new_subaddress_title; - - @override - Widget body(BuildContext context) => NewSubaddressForm(subaddress); - - @override - Widget build(BuildContext context) { - final subaddressCreationStore = - Provider.of(context); - - reaction((_) => subaddressCreationStore.state, (SubaddressCreationState state) { - if (state is SubaddressCreatedSuccessfully) { - WidgetsBinding.instance - .addPostFrameCallback((_) => Navigator.of(context).pop()); - } - }); - - return super.build(context); - } -} - -class NewSubaddressForm extends StatefulWidget { - NewSubaddressForm(this.subaddress); - - final Subaddress subaddress; - - @override - NewSubaddressFormState createState() => NewSubaddressFormState(subaddress); -} - -class NewSubaddressFormState extends State { - NewSubaddressFormState(this.subaddress); - - final _formKey = GlobalKey(); - final _labelController = TextEditingController(); - final Subaddress subaddress; - - @override - void initState() { - if (subaddress != null) _labelController.text = subaddress.label; - super.initState(); - } - - @override - void dispose() { - _labelController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final subaddressCreationStore = - Provider.of(context); - - _labelController.addListener(() { - if (_labelController.text.isNotEmpty) { - subaddressCreationStore.setDisabledStatus(false); - } else { - subaddressCreationStore.setDisabledStatus(true); - } - }); - - return Form( - key: _formKey, - child: Container( - padding: EdgeInsets.all(24.0), - child: Column( - children: [ - Expanded( - child: Center( - child: BaseTextFormField( - controller: _labelController, - hintText: S.of(context).new_subaddress_label_name, - validator: (value) { - subaddressCreationStore.validateSubaddressName(value); - return subaddressCreationStore.errorMessage; - } - ) - ) - ), - Observer( - builder: (_) => LoadingPrimaryButton( - onPressed: () async { - if (_formKey.currentState.validate()) { - if (subaddress != null) { - await subaddressCreationStore.setLabel( - addressIndex: subaddress.id, - label: _labelController.text - ); - } else { - await subaddressCreationStore.add( - label: _labelController.text); - } - } - }, - text: subaddress != null - ? S.of(context).rename - : S.of(context).new_subaddress_create, - color: Colors.green, - textColor: Colors.white, - isLoading: - subaddressCreationStore.state is SubaddressIsCreating, - isDisabled: subaddressCreationStore.isDisabledStatus, - ), - ) - ], - ), - ) - ); - } -} diff --git a/lib/src/screens/subaddress/subaddress_list_page.dart b/lib/src/screens/subaddress/subaddress_list_page.dart index 3a34867d..0327aa93 100644 --- a/lib/src/screens/subaddress/subaddress_list_page.dart +++ b/lib/src/screens/subaddress/subaddress_list_page.dart @@ -32,9 +32,7 @@ class SubaddressListPage extends BasePage { child: Observer( builder: (_) => ListView.separated( separatorBuilder: (_, __) => Divider( - color: Theme.of(context).dividerTheme.color, - height: 1.0, - ), + color: Theme.of(context).dividerTheme.color, height: 1.0), itemCount: subaddressListStore.subaddresses == null ? 0 : subaddressListStore.subaddresses.length, @@ -42,9 +40,7 @@ class SubaddressListPage extends BasePage { final subaddress = subaddressListStore.subaddresses[index]; final isCurrent = walletStore.subaddress.address == subaddress.address; - final label = subaddress.label != null - ? subaddress.label - : subaddress.address; + final label = subaddress.label ?? subaddress.address; return InkWell( onTap: () => Navigator.of(context).pop(subaddress), diff --git a/lib/src/stores/auth/auth_store.dart b/lib/src/stores/auth/auth_store.dart index 91912d7d..8844cd0f 100644 --- a/lib/src/stores/auth/auth_store.dart +++ b/lib/src/stores/auth/auth_store.dart @@ -1,99 +1,99 @@ -import 'dart:async'; -import 'package:flutter/foundation.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:mobx/mobx.dart'; -import 'package:cake_wallet/src/domain/services/user_service.dart'; -import 'package:cake_wallet/src/domain/services/wallet_service.dart'; -import 'package:cake_wallet/src/stores/auth/auth_state.dart'; -import 'package:cake_wallet/generated/i18n.dart'; - -part 'auth_store.g.dart'; - -class AuthStore = AuthStoreBase with _$AuthStore; - -abstract class AuthStoreBase with Store { - AuthStoreBase( - {@required this.userService, - @required this.walletService, - @required this.sharedPreferences}) { - state = AuthenticationStateInitial(); - _failureCounter = 0; - } - - static const maxFailedLogins = 3; - static const banTimeout = 180; // 3 mins - final banTimeoutKey = S.current.auth_store_ban_timeout; - - final UserService userService; - final WalletService walletService; - - final SharedPreferences sharedPreferences; - - @observable - AuthState state; - - @observable - int _failureCounter; - - @action - Future auth({String password}) async { - state = AuthenticationStateInitial(); - final _banDuration = banDuration(); - - if (_banDuration != null) { - state = AuthenticationBanned( - error: S.current.auth_store_banned_for + '${_banDuration.inMinutes}' + S.current.auth_store_banned_minutes); - return; - } - - state = AuthenticationInProgress(); - final isAuth = await userService.authenticate(password); - - if (isAuth) { - state = AuthenticatedSuccessfully(); - _failureCounter = 0; - } else { - _failureCounter += 1; - - if (_failureCounter >= maxFailedLogins) { - final banDuration = await ban(); - state = AuthenticationBanned( - error: S.current.auth_store_banned_for + '${banDuration.inMinutes}' + S.current.auth_store_banned_minutes); - return; - } - - state = AuthenticationFailure(error: S.current.auth_store_incorrect_password); - } - } - - Duration banDuration() { - final unbanTimestamp = sharedPreferences.getInt(banTimeoutKey); - - if (unbanTimestamp == null) { - return null; - } - - final unbanTime = DateTime.fromMillisecondsSinceEpoch(unbanTimestamp); - final now = DateTime.now(); - - if (now.isAfter(unbanTime)) { - return null; - } - - return Duration(milliseconds: unbanTimestamp - now.millisecondsSinceEpoch); - } - - Future ban() async { - final multiplier = _failureCounter - maxFailedLogins + 1; - final timeout = (multiplier * banTimeout) * 1000; - final unbanTimestamp = DateTime.now().millisecondsSinceEpoch + timeout; - await sharedPreferences.setInt(banTimeoutKey, unbanTimestamp); - - return Duration(milliseconds: timeout); - } - - @action - void biometricAuth() { - state = AuthenticatedSuccessfully(); - } -} +//import 'dart:async'; +//import 'package:flutter/foundation.dart'; +//import 'package:shared_preferences/shared_preferences.dart'; +//import 'package:mobx/mobx.dart'; +//import 'package:cake_wallet/src/domain/services/user_service.dart'; +//import 'package:cake_wallet/src/domain/services/wallet_service.dart'; +//import 'package:cake_wallet/view_model/auth_state.dart'; +//import 'package:cake_wallet/generated/i18n.dart'; +// +//part 'auth_store.g.dart'; +// +//class AuthStore = AuthStoreBase with _$AuthStore; +// +//abstract class AuthStoreBase with Store { +// AuthStoreBase( +// {@required this.userService, +// @required this.walletService, +// @required this.sharedPreferences}) { +// state = AuthenticationStateInitial(); +// _failureCounter = 0; +// } +// +// static const maxFailedLogins = 3; +// static const banTimeout = 180; // 3 mins +// final banTimeoutKey = S.current.auth_store_ban_timeout; +// +// final UserService userService; +// final WalletService walletService; +// +// final SharedPreferences sharedPreferences; +// +// @observable +// AuthState state; +// +// @observable +// int _failureCounter; +// +// @action +// Future auth({String password}) async { +// state = AuthenticationStateInitial(); +// final _banDuration = banDuration(); +// +// if (_banDuration != null) { +// state = AuthenticationBanned( +// error: S.current.auth_store_banned_for + '${_banDuration.inMinutes}' + S.current.auth_store_banned_minutes); +// return; +// } +// +// state = AuthenticationInProgress(); +// final isAuth = await userService.authenticate(password); +// +// if (isAuth) { +// state = AuthenticatedSuccessfully(); +// _failureCounter = 0; +// } else { +// _failureCounter += 1; +// +// if (_failureCounter >= maxFailedLogins) { +// final banDuration = await ban(); +// state = AuthenticationBanned( +// error: S.current.auth_store_banned_for + '${banDuration.inMinutes}' + S.current.auth_store_banned_minutes); +// return; +// } +// +// state = AuthenticationFailure(error: S.current.auth_store_incorrect_password); +// } +// } +// +// Duration banDuration() { +// final unbanTimestamp = sharedPreferences.getInt(banTimeoutKey); +// +// if (unbanTimestamp == null) { +// return null; +// } +// +// final unbanTime = DateTime.fromMillisecondsSinceEpoch(unbanTimestamp); +// final now = DateTime.now(); +// +// if (now.isAfter(unbanTime)) { +// return null; +// } +// +// return Duration(milliseconds: unbanTimestamp - now.millisecondsSinceEpoch); +// } +// +// Future ban() async { +// final multiplier = _failureCounter - maxFailedLogins + 1; +// final timeout = (multiplier * banTimeout) * 1000; +// final unbanTimestamp = DateTime.now().millisecondsSinceEpoch + timeout; +// await sharedPreferences.setInt(banTimeoutKey, unbanTimestamp); +// +// return Duration(milliseconds: timeout); +// } +// +// @action +// void biometricAuth() { +// state = AuthenticatedSuccessfully(); +// } +//} diff --git a/lib/src/stores/authentication/authentication_store.dart b/lib/src/stores/authentication/authentication_store.dart index f9e69f86..d03a52ba 100644 --- a/lib/src/stores/authentication/authentication_store.dart +++ b/lib/src/stores/authentication/authentication_store.dart @@ -30,8 +30,8 @@ abstract class AuthenticationStoreBase with Store { @observable AuthenticationState state; - @observable - String errorMessage; +// @observable +// String errorMessage; Future started() async { final canAuth = await userService.canAuthenticate(); diff --git a/lib/src/stores/wallet_restoration/wallet_restoration_store.dart b/lib/src/stores/wallet_restoration/wallet_restoration_store.dart index 3f8351f5..4d1379c9 100644 --- a/lib/src/stores/wallet_restoration/wallet_restoration_store.dart +++ b/lib/src/stores/wallet_restoration/wallet_restoration_store.dart @@ -2,7 +2,7 @@ import 'package:mobx/mobx.dart'; import 'package:flutter/foundation.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:cake_wallet/src/domain/services/wallet_list_service.dart'; -import 'package:cake_wallet/src/domain/common/mnemotic_item.dart'; +import 'package:cake_wallet/src/domain/common/mnemonic_item.dart'; import 'package:cake_wallet/src/stores/wallet_restoration/wallet_restoration_state.dart'; import 'package:cake_wallet/src/stores/authentication/authentication_store.dart'; import 'package:cake_wallet/src/domain/common/crypto_currency.dart'; @@ -37,7 +37,7 @@ abstract class WalleRestorationStoreBase with Store { bool isValid; @observable - List seed; + List seed; @observable bool disabledState; @@ -79,35 +79,35 @@ abstract class WalleRestorationStoreBase with Store { } @action - void setSeed(List seed) { + void setSeed(List seed) { this.seed = seed; } - @action - void validateSeed(List seed) { - final _seed = seed != null ? seed : this.seed; - bool isValid = _seed != null ? _seed.length == 25 : false; - - if (!isValid) { - errorMessage = S.current.wallet_restoration_store_incorrect_seed_length; - this.isValid = isValid; - return; - } - - for (final item in _seed) { - if (!item.isCorrect()) { - isValid = false; - break; - } - } - - if (isValid) { - errorMessage = null; - } - - this.isValid = isValid; - return; - } +// @action +// void validateSeed(List seed) { +// final _seed = seed != null ? seed : this.seed; +// bool isValid = _seed != null ? _seed.length == 25 : false; +// +// if (!isValid) { +// errorMessage = S.current.wallet_restoration_store_incorrect_seed_length; +// this.isValid = isValid; +// return; +// } +// +// for (final item in _seed) { +// if (!item.isCorrect()) { +// isValid = false; +// break; +// } +// } +// +// if (isValid) { +// errorMessage = null; +// } +// +// this.isValid = isValid; +// return; +// } String _seedText() { return seed.fold('', (acc, item) => acc + ' ' + item.toString()); diff --git a/lib/src/widgets/base_text_form_field.dart b/lib/src/widgets/base_text_form_field.dart index ff06ce4c..50e07f7b 100644 --- a/lib/src/widgets/base_text_form_field.dart +++ b/lib/src/widgets/base_text_form_field.dart @@ -2,24 +2,24 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; class BaseTextFormField extends StatelessWidget { - BaseTextFormField({ - this.controller, - this.keyboardType = TextInputType.text, - this.textInputAction = TextInputAction.done, - this.textAlign = TextAlign.start, - this.autovalidate = false, - this.hintText = '', - this.maxLines = 1, - this.inputFormatters, - this.textColor, - this.hintColor, - this.borderColor, - this.prefix, - this.suffix, - this.suffixIcon, - this.enabled = true, - this.validator - }); + BaseTextFormField( + {this.controller, + this.keyboardType = TextInputType.text, + this.textInputAction = TextInputAction.done, + this.textAlign = TextAlign.start, + this.autovalidate = false, + this.hintText = '', + this.maxLines = 1, + this.inputFormatters, + this.textColor, + this.hintColor, + this.borderColor, + this.prefix, + this.suffix, + this.suffixIcon, + this.enabled = true, + this.validator, + this.placeholderTextStyle}); final TextEditingController controller; final TextInputType keyboardType; @@ -37,6 +37,7 @@ class BaseTextFormField extends StatelessWidget { final Widget suffixIcon; final bool enabled; final FormFieldValidator validator; + final TextStyle placeholderTextStyle; @override Widget build(BuildContext context) { @@ -50,32 +51,27 @@ class BaseTextFormField extends StatelessWidget { inputFormatters: inputFormatters, enabled: enabled, style: TextStyle( - fontSize: 16.0, - color: textColor ?? Theme.of(context).primaryTextTheme.title.color - ), + fontSize: 16.0, + color: textColor ?? Theme.of(context).primaryTextTheme.title.color), decoration: InputDecoration( - prefix: prefix, - suffix: suffix, - suffixIcon: suffixIcon, - hintStyle: TextStyle( - color: hintColor ?? Theme.of(context).primaryTextTheme.caption.color, - fontSize: 16 - ), - hintText: hintText, - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: borderColor ?? Theme.of(context).dividerColor, - width: 1.0 - ) - ), - enabledBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: borderColor ?? Theme.of(context).dividerColor, - width: 1.0 - ) - ) - ), + prefix: prefix, + suffix: suffix, + suffixIcon: suffixIcon, + hintStyle: placeholderTextStyle ?? + TextStyle( + color: hintColor ?? + Theme.of(context).primaryTextTheme.caption.color, + fontSize: 16), + hintText: hintText, + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: borderColor ?? Theme.of(context).dividerColor, + width: 1.0)), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: borderColor ?? Theme.of(context).dividerColor, + width: 1.0))), validator: validator, ); } -} \ No newline at end of file +} diff --git a/lib/src/widgets/blockchain_height_widget.dart b/lib/src/widgets/blockchain_height_widget.dart index f342bb03..9e66d407 100644 --- a/lib/src/widgets/blockchain_height_widget.dart +++ b/lib/src/widgets/blockchain_height_widget.dart @@ -4,7 +4,10 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/domain/monero/get_height_by_date.dart'; class BlockchainHeightWidget extends StatefulWidget { - BlockchainHeightWidget({GlobalKey key}) : super(key: key); + BlockchainHeightWidget({GlobalKey key, this.onHeightChange}) + : super(key: key); + + final Function(int) onHeightChange; @override State createState() => BlockchainHeightState(); @@ -13,15 +16,23 @@ class BlockchainHeightWidget extends StatefulWidget { class BlockchainHeightState extends State { final dateController = TextEditingController(); final restoreHeightController = TextEditingController(); + int get height => _height; int _height = 0; @override void initState() { - restoreHeightController.addListener(() => _height = - restoreHeightController.text != null + restoreHeightController.addListener(() { + try { + _changeHeight(restoreHeightController.text != null && + restoreHeightController.text.isNotEmpty ? int.parse(restoreHeightController.text) : 0); + } catch (_) { + _changeHeight(0); + } + }); + super.initState(); } @@ -38,21 +49,18 @@ class BlockchainHeightState extends State { child: TextFormField( style: TextStyle( fontSize: 16.0, - color: Theme.of(context).primaryTextTheme.title.color - ), + color: Theme.of(context).primaryTextTheme.title.color), controller: restoreHeightController, keyboardType: TextInputType.numberWithOptions( signed: false, decimal: false), decoration: InputDecoration( hintStyle: TextStyle( color: Theme.of(context).primaryTextTheme.caption.color, - fontSize: 16 - ), + fontSize: 16), hintText: S.of(context).widgets_restore_from_blockheight, focusedBorder: UnderlineInputBorder( borderSide: BorderSide( - color: Theme.of(context).dividerColor, - width: 1.0)), + color: Theme.of(context).dividerColor, width: 1.0)), enabledBorder: UnderlineInputBorder( borderSide: BorderSide( color: Theme.of(context).dividerColor, @@ -81,13 +89,14 @@ class BlockchainHeightState extends State { child: TextFormField( style: TextStyle( fontSize: 16.0, - color: Theme.of(context).primaryTextTheme.title.color - ), + color: Theme.of(context).primaryTextTheme.title.color), decoration: InputDecoration( hintStyle: TextStyle( - color: Theme.of(context).primaryTextTheme.caption.color, - fontSize: 16 - ), + color: Theme.of(context) + .primaryTextTheme + .caption + .color, + fontSize: 16), hintText: S.of(context).widgets_restore_from_date, focusedBorder: UnderlineInputBorder( borderSide: BorderSide( @@ -113,7 +122,7 @@ class BlockchainHeightState extends State { Future _selectDate(BuildContext context) async { final now = DateTime.now(); - final DateTime date = await showDatePicker( + final date = await showDatePicker( context: context, initialDate: now.subtract(Duration(days: 1)), firstDate: DateTime(2014, DateTime.april), @@ -125,8 +134,13 @@ class BlockchainHeightState extends State { setState(() { dateController.text = DateFormat('yyyy-MM-dd').format(date); restoreHeightController.text = '$height'; - _height = height; + _changeHeight(height); }); } } + + void _changeHeight(int height) { + _height = height; + widget.onHeightChange?.call(height); + } } diff --git a/lib/src/widgets/seed_language_selector.dart b/lib/src/widgets/seed_language_selector.dart index e69de29b..f66cadb8 100644 --- a/lib/src/widgets/seed_language_selector.dart +++ b/lib/src/widgets/seed_language_selector.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/new_wallet/widgets/select_button.dart'; +import 'package:cake_wallet/src/screens/seed_language/widgets/seed_language_picker.dart'; + +class SeedLanguageSelector extends StatefulWidget { + SeedLanguageSelector({Key key, this.initialSelected}) : super(key: key); + + final String initialSelected; + + @override + SeedLanguageSelectorState createState() => + SeedLanguageSelectorState(selected: initialSelected); +} + +class SeedLanguageSelectorState extends State { + SeedLanguageSelectorState({this.selected}); + + final seedLocales = [ + S.current.seed_language_english, + S.current.seed_language_chinese, + S.current.seed_language_dutch, + S.current.seed_language_german, + S.current.seed_language_japanese, + S.current.seed_language_portuguese, + S.current.seed_language_russian, + S.current.seed_language_spanish + ]; + String selected; + final _pickerKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return SelectButton( + image: null, + text: seedLocales[seedLanguages.indexOf(selected)], + color: Theme.of(context).accentTextTheme.title.backgroundColor, + textColor: Theme.of(context).primaryTextTheme.title.color, + onTap: () async { + final selected = await showDialog( + context: context, + builder: (BuildContext context) => + SeedLanguagePicker(key: _pickerKey, selected: this.selected)); + setState(() => this.selected = selected); + }); + } +} diff --git a/lib/src/widgets/seed_widget.dart b/lib/src/widgets/seed_widget.dart index 0c145b37..c9b91855 100644 --- a/lib/src/widgets/seed_widget.dart +++ b/lib/src/widgets/seed_widget.dart @@ -1,94 +1,64 @@ -import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:cake_wallet/palette.dart'; -import 'package:cake_wallet/src/domain/monero/mnemonics/english.dart'; -import 'package:cake_wallet/src/domain/monero/mnemonics/chinese_simplified.dart'; -import 'package:cake_wallet/src/domain/monero/mnemonics/dutch.dart'; -import 'package:cake_wallet/src/domain/monero/mnemonics/german.dart'; -import 'package:cake_wallet/src/domain/monero/mnemonics/japanese.dart'; -import 'package:cake_wallet/src/domain/monero/mnemonics/portuguese.dart'; -import 'package:cake_wallet/src/domain/monero/mnemonics/russian.dart'; -import 'package:cake_wallet/src/domain/monero/mnemonics/spanish.dart'; -import 'package:cake_wallet/src/domain/common/mnemotic_item.dart'; +import 'package:cake_wallet/core/seed_validator.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/src/domain/common/mnemonic_item.dart'; import 'package:cake_wallet/generated/i18n.dart'; class SeedWidget extends StatefulWidget { - SeedWidget({Key key, this.onMnemoticChange, this.onFinish, this.seedLanguage}) : super(key: key) { - switch (seedLanguage) { - case 'English': - words = EnglishMnemonics.words; - break; - case 'Chinese (simplified)': - words = ChineseSimplifiedMnemonics.words; - break; - case 'Dutch': - words = DutchMnemonics.words; - break; - case 'German': - words = GermanMnemonics.words; - break; - case 'Japanese': - words = JapaneseMnemonics.words; - break; - case 'Portuguese': - words = PortugueseMnemonics.words; - break; - case 'Russian': - words = RussianMnemonics.words; - break; - case 'Spanish': - words = SpanishMnemonics.words; - break; - default: - words = EnglishMnemonics.words; - } - } - - final Function(List) onMnemoticChange; + SeedWidget( + {Key key, + this.maxLength, + this.onMnemonicChange, + this.onFinish, + this.validator}) + : super(key: key); + + final int maxLength; + final Function(List) onMnemonicChange; final Function() onFinish; - final String seedLanguage; - List words; + final SeedValidator validator; @override - SeedWidgetState createState() => SeedWidgetState(); + SeedWidgetState createState() => SeedWidgetState(maxLength: maxLength); } class SeedWidgetState extends State { - static const maxLength = 25; + SeedWidgetState({this.maxLength}); - List items = []; + List items = []; + final int maxLength; final _seedController = TextEditingController(); final _seedTextFieldKey = GlobalKey(); - MnemoticItem selectedItem; + MnemonicItem selectedItem; bool isValid; String errorMessage; - List currentMnemotics; - bool isCurrentMnemoticValid; + List currentMnemonics; + bool isCurrentMnemonicValid; String _errorMessage; @override void initState() { super.initState(); isValid = false; - isCurrentMnemoticValid = false; + isCurrentMnemonicValid = false; _seedController - .addListener(() => changeCurrentMnemotic(_seedController.text)); + .addListener(() => changeCurrentMnemonic(_seedController.text)); } - void addMnemotic(String text) { - setState(() => items.add(MnemoticItem( - text: text.trim().toLowerCase(), dic: widget.words))); + void addMnemonic(String text) { + setState(() => items.add(MnemonicItem(text: text.trim().toLowerCase()))); _seedController.text = ''; - if (widget.onMnemoticChange != null) { - widget.onMnemoticChange(items); + if (widget.onMnemonicChange != null) { + widget.onMnemonicChange(items); } } - void mnemoticFromText(String text) { + void mnemonicFromText(String text) { final splitted = text.split(' '); if (splitted.length >= 2) { @@ -98,18 +68,18 @@ class SeedWidgetState extends State { } if (selectedItem != null) { - editTextOfSelectedMnemotic(text); + editTextOfSelectedMnemonic(text); } else { - addMnemotic(text); + addMnemonic(text); } } } } - void selectMnemotic(MnemoticItem item) { + void selectMnemonic(MnemonicItem item) { setState(() { selectedItem = item; - currentMnemotics = [item]; + currentMnemonics = [item]; _seedController ..text = item.text @@ -117,23 +87,23 @@ class SeedWidgetState extends State { }); } - void onMnemoticTap(MnemoticItem item) { + void onMnemonicTap(MnemonicItem item) { if (selectedItem == item) { setState(() => selectedItem = null); _seedController.text = ''; return; } - selectMnemotic(item); + selectMnemonic(item); } - void editTextOfSelectedMnemotic(String text) { + void editTextOfSelectedMnemonic(String text) { setState(() => selectedItem.changeText(text)); selectedItem = null; _seedController.text = ''; - if (widget.onMnemoticChange != null) { - widget.onMnemoticChange(items); + if (widget.onMnemonicChange != null) { + widget.onMnemonicChange(items); } } @@ -143,83 +113,77 @@ class SeedWidgetState extends State { selectedItem = null; _seedController.text = ''; - if (widget.onMnemoticChange != null) { - widget.onMnemoticChange(items); + if (widget.onMnemonicChange != null) { + widget.onMnemonicChange(items); } }); } - void invalidate() { - setState(() => isValid = false); - } + void invalidate() => setState(() => isValid = false); - void validated() { - setState(() => isValid = true); - } + void validated() => setState(() => isValid = true); - void setErrorMessage(String errorMessage) { - setState(() => this.errorMessage = errorMessage); - } + void setErrorMessage(String errorMessage) => + setState(() => this.errorMessage = errorMessage); void replaceText(String text) { setState(() => items = []); - mnemoticFromText(text); + mnemonicFromText(text); } - void changeCurrentMnemotic(String text) { + void changeCurrentMnemonic(String text) { setState(() { final trimmedText = text.trim(); final splitted = trimmedText.split(' '); _errorMessage = null; if (text == null) { - currentMnemotics = []; - isCurrentMnemoticValid = false; + currentMnemonics = []; + isCurrentMnemonicValid = false; return; } - currentMnemotics = splitted - .map((text) => MnemoticItem(text: text, dic: widget.words)) - .toList(); + currentMnemonics = + splitted.map((text) => MnemonicItem(text: text)).toList(); - bool isValid = true; + var isValid = true; - for (final word in currentMnemotics) { - isValid = word.isCorrect(); + for (final word in currentMnemonics) { + isValid = widget.validator.isValid(word); if (!isValid) { break; } } - isCurrentMnemoticValid = isValid; + isCurrentMnemonicValid = isValid; }); } - void saveCurrentMnemoticToItems() { + void saveCurrentMnemonicToItems() { setState(() { if (selectedItem != null) { - selectedItem.changeText(currentMnemotics.first.text.trim()); + selectedItem.changeText(currentMnemonics.first.text.trim()); selectedItem = null; } else { - items.addAll(currentMnemotics); + items.addAll(currentMnemonics); } - currentMnemotics = []; + currentMnemonics = []; _seedController.text = ''; }); } void showErrorIfExist() { setState(() => _errorMessage = - !isCurrentMnemoticValid ? S.current.incorrect_seed : null); + !isCurrentMnemonicValid ? S.current.incorrect_seed : null); } bool isSeedValid() { bool isValid; for (final item in items) { - isValid = item.isCorrect(); + isValid = widget.validator.isValid(item); if (!isValid) { break; @@ -234,192 +198,207 @@ class SeedWidgetState extends State { return Container( child: Column(children: [ Flexible( - fit: FlexFit.tight, - flex: 1, - child: Container( - width: double.infinity, - height: double.infinity, - padding: EdgeInsets.all(24), - decoration: BoxDecoration( - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(24), - bottomRight: Radius.circular(24) + fit: FlexFit.tight, + flex: 1, + child: Container( + width: double.infinity, + height: double.infinity, + padding: EdgeInsets.all(24), + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(24), + bottomRight: Radius.circular(24)), + color: Theme.of(context).accentTextTheme.title.backgroundColor), + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + S.of(context).restore_active_seed, + style: TextStyle( + fontSize: 14, + color: + Theme.of(context).primaryTextTheme.caption.color), ), - color: Theme.of(context).accentTextTheme.title.backgroundColor - ), - child: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - S.of(context).restore_active_seed, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).primaryTextTheme.caption.color - ), - ), - Padding( - padding: EdgeInsets.only(top: 5), - child: Wrap( - children: items.map((item) { - final isValid = item.isCorrect(); - final isSelected = selectedItem == item; - - return InkWell( - onTap: () => onMnemoticTap(item), - child: Container( - decoration: BoxDecoration( - color: isValid ? Colors.transparent : Palette.red), - margin: EdgeInsets.only(right: 7, bottom: 8), - child: Text( - item.toString(), - style: TextStyle( - color: isValid - ? Theme.of(context).primaryTextTheme.title.color - : Theme.of(context).primaryTextTheme.caption.color, - fontSize: 16, - fontWeight: - isSelected ? FontWeight.w900 : FontWeight.w400, - decoration: isSelected - ? TextDecoration.underline - : TextDecoration.none), - )), - ); - }).toList(),) - ) - ], - ), + Padding( + padding: EdgeInsets.only(top: 5), + child: Wrap( + children: items.map((item) { + final isValid = widget.validator.isValid(item); + final isSelected = selectedItem == item; + + return InkWell( + onTap: () => onMnemonicTap(item), + child: Container( + decoration: BoxDecoration( + color: isValid + ? Colors.transparent + : Palette.red), + margin: EdgeInsets.only(right: 7, bottom: 8), + child: Text( + item.toString(), + style: TextStyle( + color: isValid + ? Theme.of(context) + .primaryTextTheme + .title + .color + : Theme.of(context) + .primaryTextTheme + .caption + .color, + fontSize: 16, + fontWeight: isSelected + ? FontWeight.w900 + : FontWeight.w400, + decoration: isSelected + ? TextDecoration.underline + : TextDecoration.none), + )), + ); + }).toList(), + )) + ], ), ), + ), ), Flexible( fit: FlexFit.tight, flex: 2, child: Padding( - padding: EdgeInsets.only(left: 24, top: 48, right: 24, bottom: 24), + padding: + EdgeInsets.only(left: 24, top: 48, right: 24, bottom: 24), child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - S.of(context).restore_new_seed, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Theme.of(context).primaryTextTheme.title.color - ), - ), - Padding( - padding: EdgeInsets.only(top: 24), - child: TextFormField( - key: _seedTextFieldKey, - onFieldSubmitted: (text) => isCurrentMnemoticValid - ? saveCurrentMnemoticToItems() - : null, - style: TextStyle( - fontSize: 16.0, - color: Theme.of(context).primaryTextTheme.title.color + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + S.of(context).restore_new_seed, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: + Theme.of(context).primaryTextTheme.title.color), ), - controller: _seedController, - textInputAction: TextInputAction.done, - decoration: InputDecoration( - suffixIcon: GestureDetector( - behavior: HitTestBehavior.opaque, - child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: 145), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Text( - '${items.length}/${SeedWidgetState.maxLength}', - style: TextStyle( - color: Theme.of(context).primaryTextTheme.caption.color, - fontSize: 14)), - SizedBox(width: 10), - InkWell( - onTap: () async => - Clipboard.getData('text/plain').then( + Padding( + padding: EdgeInsets.only(top: 24), + child: TextFormField( + key: _seedTextFieldKey, + onFieldSubmitted: (text) => isCurrentMnemonicValid + ? saveCurrentMnemonicToItems() + : null, + style: TextStyle( + fontSize: 16.0, + color: + Theme.of(context).primaryTextTheme.title.color), + controller: _seedController, + textInputAction: TextInputAction.done, + decoration: InputDecoration( + suffixIcon: GestureDetector( + behavior: HitTestBehavior.opaque, + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: 145), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text('${items.length}/$maxLength', + style: TextStyle( + color: Theme.of(context) + .primaryTextTheme + .caption + .color, + fontSize: 14)), + SizedBox(width: 10), + InkWell( + onTap: () async => + Clipboard.getData('text/plain').then( (clipboard) => - replaceText(clipboard.text)), - child: Container( - height: 35, - padding: EdgeInsets.all(7), - decoration: BoxDecoration( - color: - Theme.of(context).accentTextTheme.title.backgroundColor, - borderRadius: - BorderRadius.circular(10.0)), - child: Text( - S.of(context).paste, - style: TextStyle( - color: Theme.of(context).primaryTextTheme.title.color - ), - )), - ) - ], + replaceText(clipboard.text)), + child: Container( + height: 35, + padding: EdgeInsets.all(7), + decoration: BoxDecoration( + color: Theme.of(context) + .accentTextTheme + .title + .backgroundColor, + borderRadius: + BorderRadius.circular(10.0)), + child: Text( + S.of(context).paste, + style: TextStyle( + color: Theme.of(context) + .primaryTextTheme + .title + .color), + )), + ) + ], + ), + ), ), - ), - ), - hintStyle: - TextStyle( - color: Theme.of(context).primaryTextTheme.caption.color, - fontSize: 16 - ), - hintText: S.of(context).restore_from_seed_placeholder, - errorText: _errorMessage, - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).dividerColor, width: 1.0)), - enabledBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).dividerColor, - width: 1.0))), - enableInteractiveSelection: false, - ), - ) - ]), - ) - ), + hintStyle: TextStyle( + color: Theme.of(context) + .primaryTextTheme + .caption + .color, + fontSize: 16), + hintText: + S.of(context).restore_from_seed_placeholder, + errorText: _errorMessage, + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).dividerColor, + width: 1.0)), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).dividerColor, + width: 1.0))), + enableInteractiveSelection: false, + ), + ) + ]), + )), Padding( padding: EdgeInsets.only(left: 24, right: 24, bottom: 24), child: Row( children: [ Flexible( - child: Padding( - padding: EdgeInsets.only(right: 8), - child: PrimaryButton( - onPressed: clear, - text: S.of(context).clear, - color: Colors.red, - textColor: Colors.white, - isDisabled: items.isEmpty, - ), - ) - ), + child: Padding( + padding: EdgeInsets.only(right: 8), + child: PrimaryButton( + onPressed: clear, + text: S.of(context).clear, + color: Colors.red, + textColor: Colors.white, + isDisabled: items.isEmpty, + ), + )), Flexible( child: Padding( padding: EdgeInsets.only(left: 8), child: (selectedItem == null && items.length == maxLength) ? PrimaryButton( - text: S.of(context).restore_next, - isDisabled: !isSeedValid(), - onPressed: () => widget.onFinish != null - ? widget.onFinish() - : null, - color: Colors.green, - textColor: Colors.white) + text: S.of(context).restore_next, + isDisabled: !isSeedValid(), + onPressed: () => widget.onFinish != null + ? widget.onFinish() + : null, + color: Colors.green, + textColor: Colors.white) : PrimaryButton( - text: selectedItem != null - ? S.of(context).save - : S.of(context).add_new_word, - onPressed: () => isCurrentMnemoticValid - ? saveCurrentMnemoticToItems() - : null, - onDisabledPressed: () => showErrorIfExist(), - isDisabled: !isCurrentMnemoticValid, - color: Colors.green, - textColor: Colors.white), + text: selectedItem != null + ? S.of(context).save + : S.of(context).add_new_word, + onPressed: () => isCurrentMnemonicValid + ? saveCurrentMnemonicToItems() + : null, + onDisabledPressed: () => showErrorIfExist(), + isDisabled: !isCurrentMnemonicValid, + color: Colors.green, + textColor: Colors.white), ), ) ], diff --git a/lib/store/app_store.dart b/lib/store/app_store.dart new file mode 100644 index 00000000..9a41be31 --- /dev/null +++ b/lib/store/app_store.dart @@ -0,0 +1,16 @@ +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/core/wallet_base.dart'; +import 'package:cake_wallet/store/authentication_store.dart'; + +part 'app_store.g.dart'; + +class AppStore = AppStoreBase with _$AppStore; + +abstract class AppStoreBase with Store { + AppStoreBase({this.authenticationStore}); + + AuthenticationStore authenticationStore; + + @observable + WalletBase wallet; +} diff --git a/lib/store/authentication_store.dart b/lib/store/authentication_store.dart new file mode 100644 index 00000000..626ca2a9 --- /dev/null +++ b/lib/store/authentication_store.dart @@ -0,0 +1,23 @@ +import 'package:mobx/mobx.dart'; + +part 'authentication_store.g.dart'; + +class AuthenticationStore = AuthenticationStoreBase with _$AuthenticationStore; + +enum AuthenticationState { uninitialized, installed, allowed, denied } + +abstract class AuthenticationStoreBase with Store { + AuthenticationStoreBase() : state = AuthenticationState.uninitialized; + + @observable + AuthenticationState state; + + @action + void installed() => state = AuthenticationState.installed; + + @action + void allowed() => state = AuthenticationState.allowed; + + @action + void denied() => state = AuthenticationState.denied; +} diff --git a/lib/store/wallet_list_store.dart b/lib/store/wallet_list_store.dart new file mode 100644 index 00000000..e256c6ae --- /dev/null +++ b/lib/store/wallet_list_store.dart @@ -0,0 +1,10 @@ +import 'package:mobx/mobx.dart'; + +part 'wallet_list_store.g.dart'; + +class WalletListStore = WalletListStoreBase with _$WalletListStore; + +abstract class WalletListStoreBase with Store { + @observable + Object state; +} \ No newline at end of file diff --git a/lib/themes.dart b/lib/themes.dart index 71776a51..26a62956 100644 --- a/lib/themes.dart +++ b/lib/themes.dart @@ -44,9 +44,10 @@ class Themes { ), display4: TextStyle( color: Palette.oceanBlue // QR code - ) + ), +// headline1: TextStyle(color: Palette.nightBlue) ), - dividerColor: Palette.periwinkle, + dividerColor: Palette.eee, accentTextTheme: TextTheme( title: TextStyle( color: Palette.darkLavender, // top panel @@ -112,7 +113,8 @@ class Themes { ), display4: TextStyle( color: PaletteDark.gray // QR code - ) + ), +// headline5: TextStyle(color: PaletteDark.gray) ), dividerColor: PaletteDark.distantBlue, accentTextTheme: TextTheme( diff --git a/lib/utils/list_item.dart b/lib/utils/list_item.dart new file mode 100644 index 00000000..2c64fd4d --- /dev/null +++ b/lib/utils/list_item.dart @@ -0,0 +1,3 @@ +abstract class ListItem { + const ListItem(); +} \ No newline at end of file diff --git a/lib/utils/list_section.dart b/lib/utils/list_section.dart new file mode 100644 index 00000000..2e50e0f7 --- /dev/null +++ b/lib/utils/list_section.dart @@ -0,0 +1,5 @@ +class ListSection { + const ListSection({this.items}); + + final List items; +} \ No newline at end of file diff --git a/lib/view_model/address_list/account_list_header.dart b/lib/view_model/address_list/account_list_header.dart new file mode 100644 index 00000000..46e86ef8 --- /dev/null +++ b/lib/view_model/address_list/account_list_header.dart @@ -0,0 +1,3 @@ +import 'package:cake_wallet/utils/list_item.dart'; + +class AccountListHeader extends ListItem {} \ No newline at end of file diff --git a/lib/view_model/address_list/address_edit_or_create_view_model.dart b/lib/view_model/address_list/address_edit_or_create_view_model.dart new file mode 100644 index 00000000..25e2095b --- /dev/null +++ b/lib/view_model/address_list/address_edit_or_create_view_model.dart @@ -0,0 +1,93 @@ +import 'package:flutter/foundation.dart'; +import 'package:mobx/mobx.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'; + +part 'address_edit_or_create_view_model.g.dart'; + +class AddressEditOrCreateViewModel = AddressEditOrCreateViewModelBase + with _$AddressEditOrCreateViewModel; + +abstract class AddressEditOrCreateState {} + +class AddressEditOrCreateStateInitial extends AddressEditOrCreateState {} + +class AddressIsSaving extends AddressEditOrCreateState {} + +class AddressSavedSuccessfully extends AddressEditOrCreateState {} + +class AddressEditOrCreateStateFailure extends AddressEditOrCreateState { + AddressEditOrCreateStateFailure({this.error}); + + String error; +} + +abstract class AddressEditOrCreateViewModelBase with Store { + AddressEditOrCreateViewModelBase({@required WalletBase wallet, dynamic item}) + : isEdit = item != null, + state = AddressEditOrCreateStateInitial(), + label = item?.name as String, + _item = item, + _wallet = wallet; + + dynamic _item; + + @observable + AddressEditOrCreateState state; + + @observable + String label; + + bool isEdit; + + final WalletBase _wallet; + + Future save() async { + final wallet = _wallet; + + try { + state = AddressIsSaving(); + + if (isEdit) { + await _update(); + } else { + await _createNew(); + } + + state = AddressSavedSuccessfully(); + } catch (e) { + state = AddressEditOrCreateStateFailure(error: e.toString()); + } + } + + Future _createNew() async { + final wallet = _wallet; + + if (wallet is BitcoinWallet) { + await wallet.generateNewAddress(label: label); + } + + if (wallet is MoneroWallet) { + await wallet.subaddressList + .addSubaddress(accountIndex: wallet.account.id, label: label); + await wallet.save(); + } + } + + Future _update() async { + final wallet = _wallet; + + if (wallet is BitcoinWallet) { + await wallet.updateAddress(_item.address as String, label: label); + } + + if (wallet is MoneroWallet) { + await wallet.subaddressList.setLabelSubaddress( + accountIndex: wallet.account.id, + addressIndex: _item.id as int, + label: label); + await wallet.save(); + } + } +} diff --git a/lib/view_model/address_list/address_list_header.dart b/lib/view_model/address_list/address_list_header.dart new file mode 100644 index 00000000..f52a7715 --- /dev/null +++ b/lib/view_model/address_list/address_list_header.dart @@ -0,0 +1,3 @@ +import 'package:cake_wallet/utils/list_item.dart'; + +class AddressListHeader extends ListItem {} \ No newline at end of file diff --git a/lib/view_model/address_list/address_list_item.dart b/lib/view_model/address_list/address_list_item.dart new file mode 100644 index 00000000..a7c232c5 --- /dev/null +++ b/lib/view_model/address_list/address_list_item.dart @@ -0,0 +1,14 @@ +import 'package:flutter/foundation.dart'; +import 'package:cake_wallet/utils/list_item.dart'; + +class AddressListItem extends ListItem { + const AddressListItem({@required this.address, this.name, this.id}) + : super(); + + final int id; + final String address; + final String name; + + @override + String toString() => name ?? address; +} \ No newline at end of file diff --git a/lib/view_model/address_list/address_list_view_model.dart b/lib/view_model/address_list/address_list_view_model.dart new file mode 100644 index 00000000..32dbeb7a --- /dev/null +++ b/lib/view_model/address_list/address_list_view_model.dart @@ -0,0 +1,135 @@ +import 'package:flutter/foundation.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart'; +import 'package:cake_wallet/monero/monero_wallet.dart'; +import 'package:cake_wallet/core/wallet_base.dart'; +import 'package:cake_wallet/utils/list_item.dart'; +import 'package:cake_wallet/view_model/address_list/account_list_header.dart'; +import 'package:cake_wallet/view_model/address_list/address_list_header.dart'; +import 'package:cake_wallet/view_model/address_list/address_list_item.dart'; + +part 'address_list_view_model.g.dart'; + +class AddressListViewModel = AddressListViewModelBase + with _$AddressListViewModel; + +abstract class PaymentURI { + PaymentURI({this.amount, this.address}); + + final String amount; + final String address; +} + +class MoneroURI extends PaymentURI { + MoneroURI({String amount, String address}) + : super(amount: amount, address: address); + + @override + String toString() { + var base = 'monero:' + address; + + if (amount?.isNotEmpty ?? false) { + base += '?tx_amount=$amount'; + } + + return base; + } +} + +class BitcoinURI extends PaymentURI { + BitcoinURI({String amount, String address}) + : super(amount: amount, address: address); + + @override + String toString() { + var base = 'bitcoin:' + address; + + if (amount?.isNotEmpty ?? false) { + base += '?amount=$amount'; + } + + return base; + } +} + +abstract class AddressListViewModelBase with Store { + AddressListViewModelBase({@required WalletBase wallet}) { + hasAccounts = _wallet is MoneroWallet; + _wallet = wallet; + _init(); + } + + @observable + String amount; + + @computed + AddressListItem get address => AddressListItem(address: _wallet.address); + + @computed + PaymentURI get uri { + if (_wallet is MoneroWallet) { + return MoneroURI(amount: amount, address: address.address); + } + + if (_wallet is BitcoinWallet) { + return BitcoinURI(amount: amount, address: address.address); + } + + return null; + } + + @computed + ObservableList get items => + ObservableList()..addAll(_baseItems)..addAll(addressList); + + @computed + ObservableList get addressList { + final wallet = _wallet; + final addressList = ObservableList(); + + if (wallet is MoneroWallet) { + addressList.addAll(wallet.subaddressList.subaddresses.map((subaddress) => + AddressListItem( + id: subaddress.id, + name: subaddress.label, + address: subaddress.address))); + } + + if (wallet is BitcoinWallet) { + final bitcoinAddresses = wallet.addresses.map( + (addr) => AddressListItem(name: addr.label, address: addr.address)); + addressList.addAll(bitcoinAddresses); + } + + return addressList; + } + + set address(AddressListItem address) => _wallet.address = address.address; + + bool hasAccounts; + + WalletBase _wallet; + + List _baseItems; + + @computed + String get accountLabel { + final wallet = _wallet; + + if (wallet is MoneroWallet) { + return wallet.account.label; + } + + return null; + } + + void _init() { + _baseItems = []; + + if (_wallet is MoneroWallet) { + _baseItems.add(AccountListHeader()); + } + + _baseItems.add(AddressListHeader()); + } +} diff --git a/lib/view_model/auth_state.dart b/lib/view_model/auth_state.dart new file mode 100644 index 00000000..01e4c257 --- /dev/null +++ b/lib/view_model/auth_state.dart @@ -0,0 +1,20 @@ +abstract class AuthState {} + +class AuthenticationStateInitial extends AuthState {} + +class AuthenticationInProgress extends AuthState {} + +class AuthenticatedSuccessfully extends AuthState {} + +class AuthenticationFailure extends AuthState { + AuthenticationFailure({this.error}); + + final String error; +} + +class AuthenticationBanned extends AuthState { + AuthenticationBanned({this.error}); + + final String error; +} + diff --git a/lib/view_model/auth_view_model.dart b/lib/view_model/auth_view_model.dart new file mode 100644 index 00000000..c135ddb5 --- /dev/null +++ b/lib/view_model/auth_view_model.dart @@ -0,0 +1,97 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/view_model/auth_state.dart'; +import 'package:cake_wallet/core/auth_service.dart'; +import 'package:cake_wallet/generated/i18n.dart'; + +part 'auth_view_model.g.dart'; + +class AuthViewModel = AuthViewModelBase with _$AuthViewModel; + +abstract class AuthViewModelBase with Store { + AuthViewModelBase( + {@required this.authService, @required this.sharedPreferences}) { + state = AuthenticationStateInitial(); + _failureCounter = 0; + } + + static const maxFailedLogins = 3; + static const banTimeout = 180; // 3 mins + final banTimeoutKey = S.current.auth_store_ban_timeout; + + final AuthService authService; + final SharedPreferences sharedPreferences; + + @observable + AuthState state; + + @observable + int _failureCounter; + + @action + Future auth({String password}) async { + state = AuthenticationStateInitial(); + final _banDuration = banDuration(); + + if (_banDuration != null) { + state = AuthenticationBanned( + error: S.current.auth_store_banned_for + + '${_banDuration.inMinutes}' + + S.current.auth_store_banned_minutes); + return; + } + + state = AuthenticationInProgress(); + final isAuth = await authService.authenticate(password); + + if (isAuth) { + state = AuthenticatedSuccessfully(); + _failureCounter = 0; + } else { + _failureCounter += 1; + + if (_failureCounter >= maxFailedLogins) { + final banDuration = await ban(); + state = AuthenticationBanned( + error: S.current.auth_store_banned_for + + '${banDuration.inMinutes}' + + S.current.auth_store_banned_minutes); + return; + } + + state = + AuthenticationFailure(error: S.current.auth_store_incorrect_password); + } + } + + Duration banDuration() { + final unbanTimestamp = sharedPreferences.getInt(banTimeoutKey); + + if (unbanTimestamp == null) { + return null; + } + + final unbanTime = DateTime.fromMillisecondsSinceEpoch(unbanTimestamp); + final now = DateTime.now(); + + if (now.isAfter(unbanTime)) { + return null; + } + + return Duration(milliseconds: unbanTimestamp - now.millisecondsSinceEpoch); + } + + Future ban() async { + final multiplier = _failureCounter - maxFailedLogins + 1; + final timeout = (multiplier * banTimeout) * 1000; + final unbanTimestamp = DateTime.now().millisecondsSinceEpoch + timeout; + await sharedPreferences.setInt(banTimeoutKey, unbanTimestamp); + + return Duration(milliseconds: timeout); + } + + @action + void biometricAuth() => state = AuthenticatedSuccessfully(); +} diff --git a/lib/view_model/dashboard_view_model.dart b/lib/view_model/dashboard_view_model.dart new file mode 100644 index 00000000..086b4ff2 --- /dev/null +++ b/lib/view_model/dashboard_view_model.dart @@ -0,0 +1,68 @@ +import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart'; +import 'package:cake_wallet/src/domain/common/transaction_direction.dart'; +import 'package:cake_wallet/src/domain/common/transaction_info.dart'; +import 'package:cake_wallet/src/stores/action_list/transaction_list_item.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/core/wallet_base.dart'; +import 'package:cake_wallet/src/domain/common/sync_status.dart'; +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; +import 'package:cake_wallet/store/app_store.dart'; + +part 'dashboard_view_model.g.dart'; + +class DashboardViewModel = DashboardViewModelBase with _$DashboardViewModel; + +class WalletBalace { + WalletBalace({this.unlockedBalance, this.totalBalance}); + + final String unlockedBalance; + final String totalBalance; +} + +abstract class DashboardViewModelBase with Store { + DashboardViewModelBase({this.appStore}) { + name = appStore.wallet?.name; + balance = WalletBalace(unlockedBalance: '0.001', totalBalance: '0.005'); + status = SyncedSyncStatus(); + type = WalletType.bitcoin; + wallet ??= appStore.wallet; + _reaction = reaction((_) => appStore.wallet, _onWalletChange); + transactions = ObservableList.of(wallet.transactionHistory.transactions + .map((transaction) => TransactionListItem(transaction: transaction))); + } + + @observable + WalletType type; + + @observable + String name; + + @computed + String get address => wallet.address; + + @observable + WalletBalace balance; + + @observable + SyncStatus status; + + @observable + ObservableList transactions; + + @observable + String subname; + + WalletBase wallet; + + AppStore appStore; + + ReactionDisposer _reaction; + + void _onWalletChange(WalletBase wallet) { + name = wallet.name; + transactions.clear(); + transactions.addAll(wallet.transactionHistory.transactions + .map((transaction) => TransactionListItem(transaction: transaction))); + balance = WalletBalace(unlockedBalance: '0.001', totalBalance: '0.005'); + } +} diff --git a/lib/view_model/wallet_creation_state.dart b/lib/view_model/wallet_creation_state.dart new file mode 100644 index 00000000..0d0d8ab8 --- /dev/null +++ b/lib/view_model/wallet_creation_state.dart @@ -0,0 +1,15 @@ +import 'package:flutter/foundation.dart'; + +abstract class WalletCreationState {} + +class InitialWalletCreationState extends WalletCreationState {} + +class WalletCreating extends WalletCreationState {} + +class WalletCreatedSuccessfully extends WalletCreationState {} + +class WalletCreationFailure extends WalletCreationState { + WalletCreationFailure({@required this.error}); + + final String error; +} diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart new file mode 100644 index 00000000..6a77de6f --- /dev/null +++ b/lib/view_model/wallet_creation_vm.dart @@ -0,0 +1,40 @@ +import 'package:flutter/foundation.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/core/wallet_credentials.dart'; +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; +import 'package:cake_wallet/view_model/wallet_creation_state.dart'; + +part 'wallet_creation_vm.g.dart'; + +class WalletCreationVM = WalletCreationVMBase with _$WalletCreationVM; + +abstract class WalletCreationVMBase with Store { + WalletCreationVMBase({@required this.type}) { + state = InitialWalletCreationState(); + name = ''; + } + + @observable + String name; + + @observable + WalletCreationState state; + + WalletType type; + + Future create({dynamic options}) async { + try { + state = WalletCreating(); + await process(getCredentials(options)); + state = WalletCreatedSuccessfully(); + } catch (e) { + state = WalletCreationFailure(error: e.toString()); + } + } + + WalletCredentials getCredentials(dynamic options) => + throw UnimplementedError(); + + Future process(WalletCredentials credentials) => + throw UnimplementedError(); +} diff --git a/lib/view_model/wallet_new_vm.dart b/lib/view_model/wallet_new_vm.dart new file mode 100644 index 00000000..1e0bba29 --- /dev/null +++ b/lib/view_model/wallet_new_vm.dart @@ -0,0 +1,42 @@ +import 'package:flutter/foundation.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/monero/monero_wallet_service.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_wallet_creation_credentials.dart'; +import 'package:cake_wallet/core/wallet_creation_service.dart'; +import 'package:cake_wallet/core/wallet_credentials.dart'; +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; +import 'package:cake_wallet/view_model/wallet_creation_vm.dart'; + +part 'wallet_new_vm.g.dart'; + +class WalletNewVM = WalletNewVMBase with _$WalletNewVM; + +abstract class WalletNewVMBase extends WalletCreationVM with Store { + WalletNewVMBase(this._walletCreationService, {@required WalletType type}) + : selectedMnemonicLanguage = '', + super(type: type); + + @observable + String selectedMnemonicLanguage; + + bool get hasLanguageSelector => type == WalletType.monero; + + final WalletCreationService _walletCreationService; + + @override + WalletCredentials getCredentials(dynamic options) { + switch (type) { + case WalletType.monero: + return MoneroNewWalletCredentials( + name: name, language: options as String); + case WalletType.bitcoin: + return BitcoinNewWalletCredentials(name: name); + default: + return null; + } + } + + @override + Future process(WalletCredentials credentials) async => + _walletCreationService.create(credentials); +} diff --git a/lib/view_model/wallet_restoration_from_seed_vm.dart b/lib/view_model/wallet_restoration_from_seed_vm.dart new file mode 100644 index 00000000..107f0ece --- /dev/null +++ b/lib/view_model/wallet_restoration_from_seed_vm.dart @@ -0,0 +1,52 @@ +import 'package:flutter/foundation.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/monero/monero_wallet_service.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_wallet_creation_credentials.dart'; +import 'package:cake_wallet/core/generate_wallet_password.dart'; +import 'package:cake_wallet/core/wallet_creation_service.dart'; +import 'package:cake_wallet/core/wallet_credentials.dart'; +import 'package:cake_wallet/src/domain/common/wallet_type.dart'; +import 'package:cake_wallet/view_model/wallet_creation_vm.dart'; + +part 'wallet_restoration_from_seed_vm.g.dart'; + +class WalletRestorationFromSeedVM = WalletRestorationFromSeedVMBase + with _$WalletRestorationFromSeedVM; + +abstract class WalletRestorationFromSeedVMBase extends WalletCreationVM + with Store { + WalletRestorationFromSeedVMBase(this._walletCreationService, + {@required WalletType type, @required this.language, this.seed}) + : super(type: type); + + @observable + String seed; + + @observable + int height; + + bool get hasRestorationHeight => type == WalletType.monero; + + final String language; + final WalletCreationService _walletCreationService; + + @override + WalletCredentials getCredentials(dynamic options) { + final password = generateWalletPassword(type); + + switch (type) { + case WalletType.monero: + return MoneroRestoreWalletFromSeedCredentials( + name: name, height: height, mnemonic: seed, password: password); + case WalletType.bitcoin: + return BitcoinRestoreWalletFromSeedCredentials( + name: name, mnemonic: seed, password: password); + default: + return null; + } + } + + @override + Future process(WalletCredentials credentials) async => + _walletCreationService.restoreFromSeed(credentials); +} diff --git a/pubspec.lock b/pubspec.lock index 91463b49..8e2af0a3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -371,6 +371,13 @@ packages: 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" glob: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 42426cc2..4d2bcf46 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -58,6 +58,7 @@ dependencies: password: ^1.0.0 basic_utils: ^1.0.8 bitcoin_flutter: ^2.0.0 + get_it: ^4.0.2 dev_dependencies: flutter_test: