This commit is contained in:
M 2020-06-20 10:10:00 +03:00
parent 719842964b
commit 81cee186db
94 changed files with 3786 additions and 3001 deletions

2
.gitignore vendored
View file

@ -88,3 +88,5 @@ android/key.properties
**/tool/.secrets-prod.json **/tool/.secrets-prod.json
**/lib/.secrets.g.dart **/lib/.secrets.g.dart
vendor/

6
.gitmodules vendored Normal file
View file

@ -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

View file

@ -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});
}

View file

@ -1,14 +1,35 @@
import 'dart:convert';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart'; import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart';
import 'package:cake_wallet/src/domain/common/balance.dart'; import 'package:cake_wallet/src/domain/common/balance.dart';
class BitcoinBalance extends Balance { 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 confirmed;
final int unconfirmed; final int unconfirmed;
int get total => confirmed + unconfirmed; int get total => confirmed + unconfirmed;
String get confirmedFormatted => bitcoinAmountToString(amount: confirmed); String get confirmedFormatted => bitcoinAmountToString(amount: confirmed);
String get unconfirmedFormatted => bitcoinAmountToString(amount: unconfirmed); String get unconfirmedFormatted => bitcoinAmountToString(amount: unconfirmed);
String get totalFormatted => bitcoinAmountToString(amount: total); String get totalFormatted => bitcoinAmountToString(amount: total);
String toJSON() =>
json.encode({'confirmed': confirmed, 'unconfirmed': unconfirmed});
} }

View file

@ -1,61 +1,78 @@
import 'dart:convert'; import 'dart:convert';
import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart';
import 'package:flutter/foundation.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/bitcoin_transaction_info.dart';
import 'package:cake_wallet/bitcoin/electrum.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/src/domain/common/transaction_info.dart';
import 'package:cake_wallet/bitcoin/file.dart'; import 'package:cake_wallet/src/domain/common/transaction_direction.dart';
class BitcoinTransactionHistory extends TransactionHistory { part 'bitcoin_transaction_history.g.dart';
BitcoinTransactionHistory(
{@required this.eclient, // TODO: Think about another transaction store for bitcoin transaction history..
@required this.path,
@required String password, const _transactionsHistoryFileName = 'transactions.json';
@required this.wallet})
: _transactions = BehaviorSubject<List<TransactionInfo>>.seeded([]), class BitcoinTransactionHistory = BitcoinTransactionHistoryBase
with _$BitcoinTransactionHistory;
abstract class BitcoinTransactionHistoryBase
extends TransactionHistoryBase<BitcoinTransactionInfo> with Store {
BitcoinTransactionHistoryBase(
{this.eclient, String dirPath, @required String password})
: path = '$dirPath/$_transactionsHistoryFileName',
_password = password, _password = password,
_height = 0; _height = 0;
final BitcoinWallet wallet; BitcoinWallet wallet;
final ElectrumClient eclient; final ElectrumClient eclient;
final String path; final String path;
final String _password; final String _password;
int _height; int _height;
@override
Observable<List<TransactionInfo>> get transactions => _transactions.stream;
List<TransactionInfo> get transactionsAll => _transactions.value;
final BehaviorSubject<List<TransactionInfo>> _transactions;
bool _isUpdating = false;
Future<void> init() async { Future<void> init() async {
// TODO: throw exeption if wallet is null;
final info = await _read(); final info = await _read();
_height = (info['height'] as int) ?? _height; _height = (info['height'] as int) ?? _height;
_transactions.value = info['transactions'] as List<TransactionInfo>; // FIXME: remove hardcoded value
transactions = ObservableList.of([
BitcoinTransactionInfo(
id: 'test',
height: 12,
amount: 12,
direction: TransactionDirection.incoming,
date: DateTime.now(),
isPending: false)
]);
} }
@override @override
Future<List<TransactionInfo>> getAll() async => _transactions.value; Future update() async {
await super.update();
_updateHeight();
}
@override @override
Future update() async { Future<List<BitcoinTransactionInfo>> fetchTransactions() async {
if (_isUpdating) { final addresses = wallet.addresses;
return; final histories =
} 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))
.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);
try { return historiesWithDetails
_isUpdating = true; .map((info) => BitcoinTransactionInfo.fromHexAndHeader(
final newTransasctions = await fetchTransactions(); info['raw'] as String, info['header'] as Map<String, Object>,
_transactions.value = _transactions.value + newTransasctions; addresses: addresses.map((record) => record.address).toList()))
_updateHeight(); .toList();
await save();
_isUpdating = false;
} catch (e) {
_isUpdating = false;
rethrow;
}
} }
Future<Map<String, Object>> fetchTransactionInfo( Future<Map<String, Object>> fetchTransactionInfo(
@ -69,51 +86,20 @@ class BitcoinTransactionHistory extends TransactionHistory {
return {'raw': raw, 'header': header}; return {'raw': raw, 'header': header};
} }
Future<List<BitcoinTransactionInfo>> 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<String, Object>,
addresses: addresses))
.toList();
}
Future<void> add(List<BitcoinTransactionInfo> transactions) async { Future<void> add(List<BitcoinTransactionInfo> transactions) async {
final txs = await getAll() this.transactions.addAll(transactions);
..addAll(transactions); await save();
await writeData(
path: path,
password: _password,
data: json
.encode(txs.map((tx) => (tx as BitcoinTransactionInfo).toJson())));
} }
Future<void> addOne(BitcoinTransactionInfo tx) async { Future<void> addOne(BitcoinTransactionInfo tx) async {
final txs = await getAll() transactions.add(tx);
..add(tx); await save();
await writeData(
path: path,
password: _password,
data: json
.encode(txs.map((tx) => (tx as BitcoinTransactionInfo).toJson())));
} }
Future<void> save() async => writeData( Future<void> save() async => writeData(
path: path, path: path,
password: _password, password: _password,
data: json data: json.encode({'height': _height, 'transactions': transactions}));
.encode({'height': _height, 'transactions': _transactions.value}));
Future<Map<String, Object>> _read() async { Future<Map<String, Object>> _read() async {
try { try {
@ -133,13 +119,13 @@ class BitcoinTransactionHistory extends TransactionHistory {
return {'transactions': transactions, 'height': height}; return {'transactions': transactions, 'height': height};
} catch (_) { } catch (_) {
return {'transactions': List<TransactionInfo>(), 'height': 0}; return {'transactions': <TransactionInfo>[], 'height': 0};
} }
} }
void _updateHeight() { void _updateHeight() {
final int newHeight = _transactions.value final newHeight = transactions.fold(
.fold(0, (acc, val) => val.height > acc ? val.height : acc); 0, (int acc, val) => val.height > acc ? val.height : acc);
_height = newHeight > _height ? newHeight : _height; _height = newHeight > _height ? newHeight : _height;
} }
} }

View file

@ -66,7 +66,7 @@ class BitcoinTransactionInfo extends TransactionInfo {
String amountFormatted() => bitcoinAmountToString(amount: amount); String amountFormatted() => bitcoinAmountToString(amount: amount);
@override @override
String fiatAmount() => ''; String fiatAmount() => '\$ 24.5';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final m = Map<String, dynamic>(); final m = Map<String, dynamic>();

View file

@ -1,251 +1,184 @@
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart'; import 'dart:convert';
import 'package:cake_wallet/bitcoin/bitcoin_balance.dart'; import 'package:mobx/mobx.dart';
import 'package:cake_wallet/src/domain/common/sync_status.dart';
import 'package:flutter/foundation.dart';
import 'package:rxdart/rxdart.dart';
import 'package:bip39/bip39.dart' as bip39; 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/bitcoin_flutter.dart' as bitcoin;
import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData; 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/file.dart';
import 'package:cake_wallet/bitcoin/electrum.dart'; import 'package:cake_wallet/bitcoin/electrum.dart';
import 'package:cake_wallet/bitcoin/bitcoin_transaction_history.dart'; import 'package:cake_wallet/bitcoin/bitcoin_balance.dart';
import 'package:cake_wallet/src/domain/common/pathForWallet.dart';
import 'package:cake_wallet/src/domain/common/node.dart'; import 'package:cake_wallet/src/domain/common/node.dart';
import 'package:cake_wallet/src/domain/common/pending_transaction.dart'; import 'package:cake_wallet/core/wallet_base.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';
class BitcoinWallet extends Wallet { part 'bitcoin_wallet.g.dart';
BitcoinWallet(
{@required this.hdwallet,
@required this.eclient,
@required this.path,
@required String password,
int accountIndex = 0,
this.mnemonic})
: _accountIndex = accountIndex,
_password = password,
_syncStatus = BehaviorSubject<SyncStatus>(),
_onBalanceChange = BehaviorSubject<BitcoinBalance>(),
_onAddressChange = BehaviorSubject<String>(),
_onNameChange = BehaviorSubject<String>();
@override /* TODO: Save balance to a wallet file.
Observable<BitcoinBalance> get onBalanceChange => _onBalanceChange.stream; Load balance from the wallet file in `init` method.
*/
@override class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet;
Observable<SyncStatus> get syncStatus => _syncStatus.stream;
@override abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
String get name => path.split('/').last ?? ''; static BitcoinWallet fromJSON(
@override {@required String password,
String get address => hdwallet.address; @required String name,
String get xpub => hdwallet.base58; @required String dirPath,
String jsonSource}) {
final String path; final data = json.decode(jsonSource) as Map;
final bitcoin.HDWallet hdwallet; final mnemonic = data['mnemonic'] as String;
final ElectrumClient eclient;
final String mnemonic;
BitcoinTransactionHistory history;
final BehaviorSubject<SyncStatus> _syncStatus;
final BehaviorSubject<BitcoinBalance> _onBalanceChange;
final BehaviorSubject<String> _onAddressChange;
final BehaviorSubject<String> _onNameChange;
BehaviorSubject<Object> _addressUpdatesSubject;
StreamSubscription<Object> _addressUpdatesSubscription;
final String _password;
int _accountIndex;
static Future<BitcoinWallet> 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<String, Object>;
final mnemonic = jsoned['mnemonic'] as String;
final accountIndex = final accountIndex =
(jsoned['account_index'] == "null" || jsoned['account_index'] == null) (data['account_index'] == "null" || data['account_index'] == null)
? 0 ? 0
: int.parse(jsoned['account_index'] as String); : int.parse(data['account_index'] as String);
final _addresses = data['addresses'] as List;
final addresses = <BitcoinAddressRecord>[];
final balance = BitcoinBalance.fromJSON(data['balance'] as String) ??
BitcoinBalance(confirmed: 0, unconfirmed: 0);
return await build( _addresses?.forEach((Object el) {
if (el is String) {
addresses.add(BitcoinAddressRecord.fromJSON(el));
}
});
return BitcoinWalletBase.build(
dirPath: dirPath,
mnemonic: mnemonic, mnemonic: mnemonic,
password: password, password: password,
name: name, name: name,
accountIndex: accountIndex); accountIndex: accountIndex,
initialAddresses: addresses,
initialBalance: balance);
} }
static Future<BitcoinWallet> build( static BitcoinWallet build(
{@required String mnemonic, {@required String mnemonic,
@required String password, @required String password,
@required String name, @required String name,
int accountIndex = 0}) async { @required String dirPath,
final hd = bitcoin.HDWallet.fromSeed(bip39.mnemonicToSeed(mnemonic), List<BitcoinAddressRecord> initialAddresses,
network: bitcoin.bitcoin); BitcoinBalance initialBalance,
final walletDirPath = int accountIndex = 0}) {
await pathForWalletDir(name: name, type: WalletType.bitcoin); final walletPath = '$dirPath/$name';
final walletPath = '$walletDirPath/$name';
final historyPath = '$walletDirPath/transactions.json';
final eclient = ElectrumClient(); final eclient = ElectrumClient();
final wallet = BitcoinWallet( final history = BitcoinTransactionHistory(
hdwallet: hd, eclient: eclient, dirPath: dirPath, password: password);
return BitcoinWallet._internal(
eclient: eclient, eclient: eclient,
path: walletPath, path: walletPath,
name: name,
mnemonic: mnemonic, mnemonic: mnemonic,
password: password, password: password,
accountIndex: accountIndex); accountIndex: accountIndex,
final history = BitcoinTransactionHistory( initialAddresses: initialAddresses,
eclient: eclient, initialBalance: initialBalance,
path: historyPath, transactionHistory: history);
password: password,
wallet: wallet);
wallet.history = history;
await history.init();
await wallet.updateInfo();
return wallet;
} }
List<String> getAddresses() => _accountIndex == 0 BitcoinWalletBase._internal(
? [address] {@required this.eclient,
: List<String>.generate( @required this.path,
_accountIndex, (i) => _getAddress(hd: hdwallet, index: i)); @required String password,
@required this.name,
List<BitcoinAddressRecord> initialAddresses,
int accountIndex = 0,
this.transactionHistory,
this.mnemonic,
BitcoinBalance initialBalance}) {
balance = initialBalance ?? BitcoinBalance(confirmed: 0, unconfirmed: 0);
hd = bitcoin.HDWallet.fromSeed(bip39.mnemonicToSeed(mnemonic),
network: bitcoin.bitcoin);
addresses = initialAddresses != null
? ObservableList<BitcoinAddressRecord>.of(initialAddresses)
: ObservableList<BitcoinAddressRecord>();
Future<String> newAddress() async { if (addresses.isEmpty) {
addresses.add(BitcoinAddressRecord(hd.address));
}
address = addresses.first.address;
_password = password;
_accountIndex = accountIndex;
}
@override
final BitcoinTransactionHistory transactionHistory;
final String path;
bitcoin.HDWallet hd;
final ElectrumClient eclient;
final String mnemonic;
int _accountIndex;
String _password;
@override
String name;
@override
@observable
String address;
@override
@observable
BitcoinBalance balance;
@override
final type = WalletType.bitcoin;
ObservableList<BitcoinAddressRecord> addresses;
String get xpub => hd.base58;
Future<void> init() async {
await transactionHistory.init();
}
Future<BitcoinAddressRecord> generateNewAddress({String label}) async {
_accountIndex += 1; _accountIndex += 1;
final address = _getAddress(hd: hdwallet, index: _accountIndex); final address = BitcoinAddressRecord(
_getAddress(hd: hd, index: _accountIndex),
label: label);
addresses.add(address);
await save(); await save();
return address; return address;
} }
@override Future<void> updateAddress(String address, {String label}) async {
Future close() async { for (final addr in addresses) {
await _addressUpdatesSubscription?.cancel(); if (addr.address == address) {
} addr.label = label;
await save();
@override break;
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();
} }
} }
@override @override
Future<PendingTransaction> createTransaction( Future<void> startSync() async {}
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;
}
@override @override
Future<String> getAddress() async => address; Future<void> connectToNode({@required Node node}) async {}
@override @override
Future<int> getCurrentHeight() async => 0; Future<void> createTransaction(Object credentials) async {}
@override @override
Future<String> getFilename() async => path.split('/').last ?? ''; Future<void> save() async =>
await write(path: path, password: _password, data: toJSON());
@override String toJSON() => json.encode({
Future<String> getFullBalance() async => 'mnemonic': mnemonic,
bitcoinAmountToString(amount: _onBalanceChange.value.total); 'account_index': _accountIndex.toString(),
'addresses': addresses.map((addr) => addr.toJSON()).toList(),
@override 'balance': balance?.toJSON()
TransactionHistory getHistory() => history; });
@override
Future<Map<String, String>> getKeys() async =>
{'publicKey': hdwallet.pubKey, 'privateKey': hdwallet.privKey};
@override
Future<String> getName() async => path.split('/').last ?? '';
@override
Future<int> getNodeHeight() async => 0;
@override
Future<String> getSeed() async => mnemonic;
@override
WalletType getType() => WalletType.bitcoin;
@override
Future<String> getUnlockedBalance() async =>
bitcoinAmountToString(amount: _onBalanceChange.value.total);
@override
Future<bool> isConnected() async => eclient.isConnected;
@override
Observable<String> get onAddressChange => _onAddressChange.stream;
@override
Observable<String> get onNameChange => _onNameChange.stream;
@override
Future rescan({int restoreHeight = 0}) {
// TODO: implement rescan
return null;
}
@override
Future startSync() async {
_addressUpdatesSubject = eclient.addressUpdate(address: address);
_addressUpdatesSubscription =
_addressUpdatesSubject.listen((obj) => print('new obj: $obj'));
_onBalanceChange.value = await fetchBalance();
getHistory().update();
}
@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<BitcoinBalance> fetchBalance() async {
final balance = await _fetchBalances();
return BitcoinBalance(
confirmed: balance['confirmed'], unconfirmed: balance['unconfirmed']);
}
Future<void> save() async => await write(
path: path,
password: _password,
obj: {'mnemonic': mnemonic, 'account_index': _accountIndex.toString()});
String _getAddress({bitcoin.HDWallet hd, int index}) => bitcoin String _getAddress({bitcoin.HDWallet hd, int index}) => bitcoin
.P2PKH( .P2PKH(
@ -256,9 +189,8 @@ class BitcoinWallet extends Wallet {
Future<Map<String, int>> _fetchBalances() async { Future<Map<String, int>> _fetchBalances() async {
final balances = await Future.wait( final balances = await Future.wait(
getAddresses().map((address) => eclient.getBalance(address: address))); addresses.map((record) => eclient.getBalance(address: record.address)));
final balance = final balance = balances.fold(<String, int>{}, (Map<String, int> acc, val) {
balances.fold(Map<String, int>(), (Map<String, int> acc, val) {
acc['confirmed'] = acc['confirmed'] =
(val['confirmed'] as int ?? 0) + (acc['confirmed'] ?? 0); (val['confirmed'] as int ?? 0) + (acc['confirmed'] ?? 0);
acc['unconfirmed'] = acc['unconfirmed'] =

View file

@ -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<Wallet> 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<bool> isWalletExit(String name) async =>
File(await pathForWallet(name: name, type: WalletType.bitcoin))
.existsSync();
@override
Future<Wallet> 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<Wallet> restoreFromKeys(String name, String password, String language,
int restoreHeight, String address, String viewKey, String spendKey) {
// TODO: implement restoreFromKeys
return null;
}
@override
Future<Wallet> 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;
}
}

View file

@ -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;
}

View file

@ -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<BitcoinWallet> 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<bool> isWalletExit(String name) async =>
File(await pathForWallet(name: name, type: WalletType.bitcoin))
.existsSync();
@override
Future<BitcoinWallet> 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<void> remove(String wallet) {
// TODO: implement remove
throw UnimplementedError();
}
@override
Future<BitcoinWallet> restoreFromKeys(
BitcoinRestoreWalletFromWIFCredentials credentials) async {
// TODO: implement restoreFromKeys
throw UnimplementedError();
}
@override
Future<BitcoinWallet> 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;
}
}

View file

@ -7,12 +7,11 @@ import 'package:flutter/foundation.dart';
Future<void> write( Future<void> write(
{@required String path, {@required String path,
@required String password, @required String password,
@required Map<String, String> obj}) async { @required String data}) async {
final jsoned = json.encode(obj);
final keys = extractKeys(password); final keys = extractKeys(password);
final key = encrypt.Key.fromBase64(keys.first); final key = encrypt.Key.fromBase64(keys.first);
final iv = encrypt.IV.fromBase64(keys.last); 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); final f = File(path);
f.writeAsStringSync(encrypted); f.writeAsStringSync(encrypted);
} }

View file

@ -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);
}

View file

@ -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 '';
}
}
}

View file

@ -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;
}

View file

@ -1,21 +1,41 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:mobx/mobx.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 { Future setPassword(String password) async {
@observable final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword);
SetupPinCodeState setupPinCodeState; final encodedPassword = encodedPinCode(pin: password);
await secureStorage.write(key: key, value: encodedPassword);
Future<void> setupPinCode({@required String pin}) async {}
Future<bool> authenticate({@required String pin}) async {
return false;
} }
void resetSetupPinCodeState() => Future<bool> canAuthenticate() async {
setupPinCodeState = InitialSetupPinCodeState(); final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword);
final walletName = sharedPreferences.getString('current_wallet_name') ?? '';
var password = '';
try {
password = await secureStorage.read(key: key);
} catch (e) {
print(e);
}
return walletName.isNotEmpty && password.isNotEmpty;
}
Future<bool> 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;
}
} }

View file

@ -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<BitcoinTransactionInfo> 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<void> init() async {
// TODO: throw exeption if wallet is null;
final info = await _read();
_height = (info['height'] as int) ?? _height;
transactions = info['transactions'] as List<BitcoinTransactionInfo>;
}
@override
Future update() async {
await super.update();
_updateHeight();
}
@override
Future<List<BitcoinTransactionInfo>> 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<String, Object>,
addresses: addresses))
.toList();
}
Future<Map<String, Object>> fetchTransactionInfo(
{@required String hash, @required int height}) async {
final rawFetching = eclient.getTransactionRaw(hash: hash);
final headerFetching = eclient.getHeader(height: height);
final result = await Future.wait([rawFetching, headerFetching]);
final raw = result.first as String;
final header = result[1] as Map<String, Object>;
return {'raw': raw, 'header': header};
}
Future<void> add(List<BitcoinTransactionInfo> transactions) async {
this.transactions.addAll(transactions);
await save();
}
Future<void> addOne(BitcoinTransactionInfo tx) async {
transactions.add(tx);
await save();
}
Future<void> save() async => writeData(
path: path,
password: _password,
data: json.encode({'height': _height, 'transactions': transactions}));
Future<Map<String, Object>> _read() async {
try {
final content = await read(path: path, password: _password);
final jsoned = json.decode(content) as Map<String, Object>;
final height = jsoned['height'] as int;
final transactions = (jsoned['transactions'] as List<dynamic>)
.map((dynamic row) {
if (row is Map<String, Object>) {
return BitcoinTransactionInfo.fromJson(row);
}
return null;
})
.where((el) => el != null)
.toList();
return {'transactions': transactions, 'height': height};
} catch (_) {
return {'transactions': <TransactionInfo>[], 'height': 0};
}
}
void _updateHeight() {
final newHeight = transactions.fold(
0, (int acc, val) => val.height > acc ? val.height : acc);
_height = newHeight > _height ? newHeight : _height;
}
}

View file

@ -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<BitcoinBalance> with Store {
static Future<BitcoinWalletBase> 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<String, Object>;
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<String> getAddresses() => _accountIndex == 0
? [address]
: List<String>.generate(
_accountIndex, (i) => _getAddress(hd: hd, index: i));
Future<void> init() async {
await transactionHistory.init();
}
Future<String> newAddress() async {
_accountIndex += 1;
final address = _getAddress(hd: hd, index: _accountIndex);
await save();
return address;
}
@override
Future<void> startSync() async {}
@override
Future<void> connectToNode({@required Node node}) async {}
@override
Future<void> createTransaction(Object credentials) async {}
@override
Future<void> 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<Map<String, int>> _fetchBalances() async {
final balances = await Future.wait(
getAddresses().map((address) => eclient.getBalance(address: address)));
final balance = balances.fold(<String, int>{}, (Map<String, int> 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;
}
}

View file

@ -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<void> 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<bool> isWalletExit(String name) async =>
File(await pathForWallet(name: name, type: WalletType.bitcoin))
.existsSync();
@override
Future<void> openWallet(String name, String password) async {
// TODO: implement openWallet
throw UnimplementedError();
}
Future<void> remove(String wallet) {
// TODO: implement remove
throw UnimplementedError();
}
@override
Future<void> restoreFromKeys(
BitcoinRestoreWalletFromWIFCredentials credentials) async {
// TODO: implement restoreFromKeys
throw UnimplementedError();
}
@override
Future<void> restoreFromSeed(
BitcoinRestoreWalletFromSeedCredentials credentials) async {
final wallet = await BitcoinWalletBase.build(
name: credentials.name,
password: credentials.password,
mnemonic: credentials.mnemonic);
await wallet.save();
return wallet;
}
}

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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<MnemonicItem> {
SeedValidator({this.type, this.language})
: _words = getWordList(type: type, language: language);
final WalletType type;
final String language;
final List<String> _words;
static List<String> getWordList({WalletType type, String language}) {
switch (type) {
case WalletType.bitcoin:
return getBitcoinWordList(language);
case WalletType.monero:
return getMoneroWordList(language);
default:
return [];
}
}
static List<String> 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<String> getBitcoinWordList(String language) {
assert(language.toLowerCase() == 'english');
return bitcoin_english.WORDLIST;
}
@override
bool isValid(MnemonicItem value) => _words.contains(value.text);
}

View file

@ -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;
}

View file

@ -1,10 +1,11 @@
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:cake_wallet/src/domain/common/transaction_info.dart';
abstract class TranasctionHistoryBase<TransactionType> { abstract class TransactionHistoryBase<TransactionType extends TransactionInfo> {
TranasctionHistoryBase() : _isUpdating = false; TransactionHistoryBase() : _isUpdating = false;
@observable @observable
List<TransactionType> transactions; ObservableList<TransactionType> transactions;
bool _isUpdating; bool _isUpdating;
@ -15,7 +16,7 @@ abstract class TranasctionHistoryBase<TransactionType> {
try { try {
_isUpdating = false; _isUpdating = false;
transactions = await fetchTransactions(); transactions.addAll(await fetchTransactions());
_isUpdating = true; _isUpdating = true;
} catch (e) { } catch (e) {
_isUpdating = false; _isUpdating = false;

View file

@ -0,0 +1,39 @@
import 'package:flutter/foundation.dart';
abstract class Validator<T> {
Validator({@required this.errorMessage});
final String errorMessage;
bool isValid(T value);
String call(T value) => !isValid(value) ? errorMessage : null;
}
class TextValidator extends Validator<String> {
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_]\$');
}

View file

@ -1,20 +1,24 @@
import 'package:flutter/foundation.dart'; 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/node.dart';
import 'package:cake_wallet/src/domain/common/wallet_type.dart';
abstract class WalletBase<BalaceType> { abstract class WalletBase<BalaceType> {
WalletType type;
String get name; String get name;
String get filename;
@observable
String address; String address;
@observable
BalaceType balance; BalaceType balance;
TransactionHistoryBase transactionHistory;
Future<void> connectToNode({@required Node node}); Future<void> connectToNode({@required Node node});
Future<void> startSync(); Future<void> startSync();
Future<void> createTransaction(Object credentials); Future<void> createTransaction(Object credentials);
Future<void> save(); Future<void> save();
} }

View file

@ -1,34 +1,47 @@
import 'package:cake_wallet/core/wallet_creation_state.dart';
import 'package:flutter/foundation.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/wallet_credentials.dart';
import 'package:cake_wallet/core/bitcoin_wallet_list_service.dart'; import 'package:cake_wallet/bitcoin/bitcoin_wallet_service.dart';
import 'package:cake_wallet/core/monero_wallet_list_service.dart'; import 'package:cake_wallet/monero/monero_wallet_service.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/wallet_type.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 {
WalletCreationService(
class WalletCreationService = WalletCreationServiceBase {WalletType initialType,
with _$WalletCreationService; this.appStore,
this.secureStorage,
abstract class WalletCreationServiceBase with Store { this.sharedPreferences})
@observable : type = initialType {
WalletCreationState state; if (type != null) {
changeWalletType(type: type);
}
}
WalletType type; WalletType type;
final AppStore appStore;
final FlutterSecureStorage secureStorage;
final SharedPreferences sharedPreferences;
WalletListService _service; // final WalletService walletService;
// final Box<WalletInfo> walletInfoSource;
WalletService _service;
void changeWalletType({@required WalletType type}) { void changeWalletType({@required WalletType type}) {
this.type = type; this.type = type;
switch (type) { switch (type) {
case WalletType.monero: case WalletType.monero:
_service = MoneroWalletListService(); _service = MoneroWalletService();
break; break;
case WalletType.bitcoin: case WalletType.bitcoin:
_service = BitcoinWalletListService(); _service = BitcoinWalletService();
break; break;
default: default:
break; break;
@ -36,32 +49,45 @@ abstract class WalletCreationServiceBase with Store {
} }
Future<void> create(WalletCredentials credentials) async { Future<void> create(WalletCredentials credentials) async {
try { final password = generateWalletPassword(type);
state = WalletCreating(); credentials.password = password;
await _service.create(credentials); await saveWalletPassword(password: password, walletName: credentials.name);
state = WalletCreatedSuccessfully(); final wallet = await _service.create(credentials);
} catch (e) { appStore.wallet = wallet;
state = WalletCreationFailure(error: e.toString()); appStore.authenticationStore.allowed();
}
} }
Future<void> restoreFromKeys(WalletCredentials credentials) async { Future<void> restoreFromKeys(WalletCredentials credentials) async {
try { final password = generateWalletPassword(type);
state = WalletCreating(); credentials.password = password;
await _service.restoreFromKeys(credentials); await saveWalletPassword(password: password, walletName: credentials.name);
state = WalletCreatedSuccessfully(); final wallet = await _service.restoreFromKeys(credentials);
} catch (e) { appStore.wallet = wallet;
state = WalletCreationFailure(error: e.toString()); appStore.authenticationStore.allowed();
}
} }
Future<void> restoreFromSeed(WalletCredentials credentials) async { Future<void> restoreFromSeed(WalletCredentials credentials) async {
try { final password = generateWalletPassword(type);
state = WalletCreating(); credentials.password = password;
await _service.restoreFromSeed(credentials); await saveWalletPassword(password: password, walletName: credentials.name);
state = WalletCreatedSuccessfully(); final wallet = await _service.restoreFromSeed(credentials);
} catch (e) { appStore.wallet = wallet;
state = WalletCreationFailure(error: e.toString()); appStore.authenticationStore.allowed();
} }
Future<String> getWalletPassword({String walletName}) async {
final key = generateStoreKeyFor(
key: SecretStoreKey.moneroWalletPassword, walletName: walletName);
final encodedPassword = await secureStorage.read(key: key);
return decodeWalletPassword(password: encodedPassword);
}
Future<void> 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);
} }
} }

View file

@ -1,6 +1,6 @@
abstract class WalletCredentials { abstract class WalletCredentials {
const WalletCredentials({this.name, this.password}); WalletCredentials({this.name, this.password});
final String name; final String name;
final String password; String password;
} }

View file

@ -1,16 +0,0 @@
import 'package:cake_wallet/core/wallet_credentials.dart';
abstract class WalletListService<N extends WalletCredentials,
RFS extends WalletCredentials, RFK extends WalletCredentials> {
Future<void> create(N credentials);
Future<void> restoreFromSeed(RFS credentials);
Future<void> restoreFromKeys(RFK credentials);
Future<void> openWallet(String name, String password);
Future<bool> isWalletExit(String name);
Future<void> remove(String wallet);
}

View file

@ -0,0 +1,17 @@
import 'package:cake_wallet/core/wallet_base.dart';
import 'package:cake_wallet/core/wallet_credentials.dart';
abstract class WalletService<N extends WalletCredentials,
RFS extends WalletCredentials, RFK extends WalletCredentials> {
Future<WalletBase> create(N credentials);
Future<WalletBase> restoreFromSeed(RFS credentials);
Future<WalletBase> restoreFromKeys(RFK credentials);
Future<WalletBase> openWallet(String name, String password);
Future<bool> isWalletExit(String name);
Future<void> remove(String wallet);
}

105
lib/di.dart Normal file
View file

@ -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>(
AppStore(authenticationStore: getIt.get<AuthenticationStore>()));
getIt.registerSingleton<FlutterSecureStorage>(FlutterSecureStorage());
getIt.registerSingletonAsync<SharedPreferences>(
() => SharedPreferences.getInstance());
getIt.registerFactoryParam<WalletCreationService, WalletType, void>(
(type, _) => WalletCreationService(
initialType: type,
appStore: getIt.get<AppStore>(),
secureStorage: getIt.get<FlutterSecureStorage>(),
sharedPreferences: getIt.get<SharedPreferences>()));
getIt.registerFactoryParam<WalletNewVM, WalletType, void>((type, _) =>
WalletNewVM(getIt.get<WalletCreationService>(param1: type), type: type));
getIt
.registerFactoryParam<WalletRestorationFromSeedVM, List, void>((args, _) {
final type = args.first as WalletType;
final language = args[1] as String;
final mnemonic = args[2] as String;
return WalletRestorationFromSeedVM(
getIt.get<WalletCreationService>(param1: type),
type: type,
language: language,
seed: mnemonic);
});
getIt.registerFactory<AddressListViewModel>(
() => AddressListViewModel(wallet: getIt.get<AppStore>().wallet));
getIt.registerFactory(
() => DashboardViewModel(appStore: getIt.get<AppStore>()));
getIt.registerFactory<AuthService>(() => AuthService(
secureStorage: getIt.get<FlutterSecureStorage>(),
sharedPreferences: getIt.get<SharedPreferences>()));
getIt.registerFactory<AuthViewModel>(() => AuthViewModel(
authService: getIt.get<AuthService>(),
sharedPreferences: getIt.get<SharedPreferences>()));
getIt.registerFactory<AuthPage>(() => AuthPage(
authViewModel: getIt.get<AuthViewModel>(),
onAuthenticationFinished: (isAuthenticated, __) {
if (isAuthenticated) {
getIt.get<AuthenticationStore>().allowed();
}
},
closable: false));
getIt.registerFactory<DashboardPage>(() => DashboardPage(
walletViewModel: getIt.get<DashboardViewModel>(),
));
getIt.registerFactory<ReceivePage>(() =>
ReceivePage(addressListViewModel: getIt.get<AddressListViewModel>()));
getIt.registerFactoryParam<AddressEditOrCreateViewModel, dynamic, void>(
(dynamic item, _) => AddressEditOrCreateViewModel(
wallet: getIt.get<AppStore>().wallet, item: item));
getIt.registerFactoryParam<AddressEditOrCreatePage, dynamic, void>(
(dynamic item, _) => AddressEditOrCreatePage(
addressEditOrCreateViewModel:
getIt.get<AddressEditOrCreateViewModel>(param1: item)));
final appStore = getIt.get<AppStore>();
_onCurrentWalletChangeReaction ??=
reaction((_) => appStore.wallet, (WalletBase wallet) async {
print('Wallet name ${wallet.name}');
await getIt
.get<SharedPreferences>()
.setString('current_wallet_name', wallet.name);
});
}

View file

@ -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/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/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:flutter_localizations/flutter_localizations.dart';
import 'package:get_it/get_it.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.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/send_template/send_template_store.dart';
import 'package:cake_wallet/src/stores/exchange_template/exchange_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/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/settings/settings_store.dart';
import 'package:cake_wallet/src/stores/price/price_store.dart'; import 'package:cake_wallet/src/stores/price/price_store.dart';
import 'package:cake_wallet/src/domain/services/user_service.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 { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
setup();
final appDir = await getApplicationDocumentsDirectory(); final appDir = await getApplicationDocumentsDirectory();
Hive.init(appDir.path); Hive.init(appDir.path);
Hive.registerAdapter(ContactAdapter()); Hive.registerAdapter(ContactAdapter());
@ -87,13 +96,13 @@ void main() async {
sharedPreferences: sharedPreferences); sharedPreferences: sharedPreferences);
final userService = UserService( final userService = UserService(
sharedPreferences: sharedPreferences, secureStorage: secureStorage); sharedPreferences: sharedPreferences, secureStorage: secureStorage);
final authenticationStore = AuthenticationStore(userService: userService); // final authenticationStore = AuthenticationStore(userService: userService);
await initialSetup( await initialSetup(
sharedPreferences: sharedPreferences, sharedPreferences: sharedPreferences,
walletListService: walletListService, walletListService: walletListService,
nodes: nodes, nodes: nodes,
authStore: authenticationStore, // authStore: authenticationStore,
initialMigrationVersion: 2); initialMigrationVersion: 2);
final settingsStore = await SettingsStoreBase.load( final settingsStore = await SettingsStoreBase.load(
@ -119,8 +128,7 @@ void main() async {
final walletCreationService = WalletCreationService(); final walletCreationService = WalletCreationService();
final authService = AuthService(); final authService = AuthService();
final appStore = AppService(
walletCreationService: walletCreationService, authService: authService);
setReactions( setReactions(
settingsStore: settingsStore, settingsStore: settingsStore,
@ -128,7 +136,7 @@ void main() async {
syncStore: syncStore, syncStore: syncStore,
walletStore: walletStore, walletStore: walletStore,
walletService: walletService, walletService: walletService,
authenticationStore: authenticationStore, // authenticationStore: authenticationStore,
loginStore: loginStore); loginStore: loginStore);
runApp(MultiProvider(providers: [ runApp(MultiProvider(providers: [
@ -141,7 +149,7 @@ void main() async {
Provider(create: (_) => walletStore), Provider(create: (_) => walletStore),
Provider(create: (_) => syncStore), Provider(create: (_) => syncStore),
Provider(create: (_) => balanceStore), Provider(create: (_) => balanceStore),
Provider(create: (_) => authenticationStore), // Provider(create: (_) => authenticationStore),
Provider(create: (_) => contacts), Provider(create: (_) => contacts),
Provider(create: (_) => nodes), Provider(create: (_) => nodes),
Provider(create: (_) => transactionDescriptions), Provider(create: (_) => transactionDescriptions),
@ -149,7 +157,7 @@ void main() async {
Provider(create: (_) => seedLanguageStore), Provider(create: (_) => seedLanguageStore),
Provider(create: (_) => sendTemplateStore), Provider(create: (_) => sendTemplateStore),
Provider(create: (_) => exchangeTemplateStore), Provider(create: (_) => exchangeTemplateStore),
Provider(create: (_) => appStore), // Provider(create: (_) => appStore),
Provider(create: (_) => walletCreationService), Provider(create: (_) => walletCreationService),
Provider(create: (_) => authService) Provider(create: (_) => authService)
], child: CakeWalletApp())); ], child: CakeWalletApp()));
@ -159,7 +167,7 @@ Future<void> initialSetup(
{WalletListService walletListService, {WalletListService walletListService,
SharedPreferences sharedPreferences, SharedPreferences sharedPreferences,
Box<Node> nodes, Box<Node> nodes,
AuthenticationStore authStore, // AuthenticationStore authStore,
int initialMigrationVersion = 1, int initialMigrationVersion = 1,
WalletType initialWalletType = WalletType.bitcoin}) async { WalletType initialWalletType = WalletType.bitcoin}) async {
await walletListService.changeWalletManger(walletType: initialWalletType); await walletListService.changeWalletManger(walletType: initialWalletType);
@ -167,7 +175,12 @@ Future<void> initialSetup(
version: initialMigrationVersion, version: initialMigrationVersion,
sharedPreferences: sharedPreferences, sharedPreferences: sharedPreferences,
nodes: nodes); nodes: nodes);
await authStore.started(); // await authStore.started();
await bootstrap();
// final authenticationStore = getIt.get<AuthenticationStore>();
// FIXME
// authenticationStore.state = AuthenticationState.denied;
monero_wallet.onStartup(); monero_wallet.onStartup();
} }
@ -241,6 +254,8 @@ class MaterialAppWithTheme extends StatelessWidget {
nodes: nodes, nodes: nodes,
trades: trades, trades: trades,
transactionDescriptions: transactionDescriptions), transactionDescriptions: transactionDescriptions),
home: Root()); home: Root(
authenticationStore: getIt.get<AuthenticationStore>(),
));
} }
} }

View file

@ -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<Subaddress>();
}
@observable
ObservableList<Subaddress> 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<Subaddress> 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;
}
}
}

View file

@ -8,7 +8,7 @@ import 'package:cake_wallet/src/domain/monero/monero_transaction_info.dart';
part 'monero_transaction_history.g.dart'; part 'monero_transaction_history.g.dart';
List<TransactionInfo> _getAllTransactions(dynamic _) => List<MoneroTransactionInfo> _getAllTransactions(dynamic _) =>
monero_transaction_history monero_transaction_history
.getAllTransations() .getAllTransations()
.map((row) => MoneroTransactionInfo.fromRow(row)) .map((row) => MoneroTransactionInfo.fromRow(row))
@ -18,9 +18,13 @@ class MoneroTransactionHistory = MoneroTransactionHistoryBase
with _$MoneroTransactionHistory; with _$MoneroTransactionHistory;
abstract class MoneroTransactionHistoryBase abstract class MoneroTransactionHistoryBase
extends TranasctionHistoryBase<TransactionInfo> with Store { extends TransactionHistoryBase<MoneroTransactionInfo> with Store {
MoneroTransactionHistoryBase() {
transactions = ObservableList<MoneroTransactionInfo>();
}
@override @override
Future<List<TransactionInfo>> fetchTransactions() async { Future<List<MoneroTransactionInfo>> fetchTransactions() async {
monero_transaction_history.refreshTransactions(); monero_transaction_history.refreshTransactions();
return _getAllTransactions(null); return _getAllTransactions(null);
} }

View file

@ -1,35 +1,36 @@
import 'package:cake_wallet/core/monero_balance.dart'; import 'package:cake_wallet/src/domain/common/wallet_type.dart';
import 'package:cake_wallet/core/monero_transaction_history.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/common/sync_status.dart';
import 'package:cake_wallet/src/domain/monero/account.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/account_list.dart';
import 'package:cake_wallet/src/domain/monero/subaddress.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: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:cake_wallet/src/domain/common/node.dart';
import 'package:cw_monero/wallet.dart' as monero_wallet; import 'package:cw_monero/wallet.dart' as monero_wallet;
import 'wallet_base.dart';
part 'monero_wallet.g.dart'; part 'monero_wallet.g.dart';
class MoneroWallet = MoneroWalletBase with _$MoneroWallet; class MoneroWallet = MoneroWalletBase with _$MoneroWallet;
abstract class MoneroWalletBase extends WalletBase<MoneroBalance> with Store { abstract class MoneroWalletBase extends WalletBase<MoneroBalance> with Store {
MoneroWalletBase({String filename, this.isRecovery = false}) { MoneroWalletBase({String filename, this.isRecovery = false})
transactionHistory = MoneroTransactionHistory(); : transactionHistory = MoneroTransactionHistory() {
_filename = filename; _filename = filename;
accountList = AccountList(); accountList = AccountList();
subaddressList = SubaddressList(); subaddressList = MoneroSubaddressList();
balance = MoneroBalance( balance = MoneroBalance(
fullBalance: monero_wallet.getFullBalance(accountIndex: 0), fullBalance: monero_wallet.getFullBalance(accountIndex: 0),
unlockedBalance: monero_wallet.getFullBalance(accountIndex: 0)); unlockedBalance: monero_wallet.getFullBalance(accountIndex: 0));
} }
MoneroTransactionHistory transactionHistory; @override
SubaddressList subaddressList; final MoneroTransactionHistory transactionHistory;
AccountList accountList;
@observable @observable
Account account; Account account;
@ -41,30 +42,40 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance> with Store {
SyncStatus syncStatus; SyncStatus syncStatus;
@override @override
String get name => filename.split('/').last; String get name => _filename.split('/').last;
@override @override
String get filename => _filename; final type = WalletType.monero;
String _filename; @override
@observable
String address;
bool isRecovery; bool isRecovery;
SyncListner _listner; MoneroSubaddressList subaddressList;
void init() { AccountList accountList;
String _filename;
SyncListner _listener;
Future<void> init() async {
await accountList.update();
account = accountList.getAll().first; account = accountList.getAll().first;
subaddressList.refresh(accountIndex: account.id ?? 0); subaddressList.update(accountIndex: account.id ?? 0);
subaddress = subaddressList.getAll().first; subaddress = subaddressList.getAll().first;
balance = MoneroBalance( balance = MoneroBalance(
fullBalance: monero_wallet.getFullBalance(accountIndex: account.id), fullBalance: monero_wallet.getFullBalance(accountIndex: account.id),
unlockedBalance: unlockedBalance:
monero_wallet.getFullBalance(accountIndex: account.id)); monero_wallet.getFullBalance(accountIndex: account.id));
address = subaddress.address;
_setListeners(); _setListeners();
} }
void close() { void close() {
_listner?.stop(); _listener?.stop();
} }
@override @override
@ -133,8 +144,8 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance> with Store {
Future<bool> isConnected() async => monero_wallet.isConnected(); Future<bool> isConnected() async => monero_wallet.isConnected();
void _setListeners() { void _setListeners() {
_listner?.stop(); _listener?.stop();
_listner = monero_wallet.setListeners( _listener = monero_wallet.setListeners(
_onNewBlock, _onNeedToRefresh, _onNewTransaction); _onNewBlock, _onNeedToRefresh, _onNewTransaction);
} }

View file

@ -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_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/pathForWallet.dart';
import 'package:cake_wallet/src/domain/common/wallet_type.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_manager.dart' as monero_wallet_manager;
import 'package:cw_monero/wallet.dart' as monero_wallet; import 'package:cw_monero/wallet.dart' as monero_wallet;
class MoneroNewWalletCredentials extends WalletCredentials { class MoneroNewWalletCredentials extends WalletCredentials {
const MoneroNewWalletCredentials( MoneroNewWalletCredentials({String name, String password, this.language})
{String name, String password, this.language})
: super(name: name, password: password); : super(name: name, password: password);
final String language; final String language;
} }
class MoneroRestoreWalletFromSeedCredentials extends WalletCredentials { class MoneroRestoreWalletFromSeedCredentials extends WalletCredentials {
const MoneroRestoreWalletFromSeedCredentials( MoneroRestoreWalletFromSeedCredentials(
{String name, String password, this.mnemonic, this.height}) {String name, String password, this.mnemonic, this.height})
: super(name: name, password: password); : super(name: name, password: password);
@ -24,7 +23,7 @@ class MoneroRestoreWalletFromSeedCredentials extends WalletCredentials {
} }
class MoneroRestoreWalletFromKeysCredentials extends WalletCredentials { class MoneroRestoreWalletFromKeysCredentials extends WalletCredentials {
const MoneroRestoreWalletFromKeysCredentials( MoneroRestoreWalletFromKeysCredentials(
{String name, {String name,
String password, String password,
this.language, this.language,
@ -41,12 +40,12 @@ class MoneroRestoreWalletFromKeysCredentials extends WalletCredentials {
final int height; final int height;
} }
class MoneroWalletListService extends WalletListService< class MoneroWalletService extends WalletService<
MoneroNewWalletCredentials, MoneroNewWalletCredentials,
MoneroRestoreWalletFromSeedCredentials, MoneroRestoreWalletFromSeedCredentials,
MoneroRestoreWalletFromKeysCredentials> { MoneroRestoreWalletFromKeysCredentials> {
@override @override
Future<void> create(MoneroNewWalletCredentials credentials) async { Future<MoneroWallet> create(MoneroNewWalletCredentials credentials) async {
try { try {
final path = final path =
await pathForWallet(name: credentials.name, type: WalletType.monero); await pathForWallet(name: credentials.name, type: WalletType.monero);
@ -56,7 +55,10 @@ class MoneroWalletListService extends WalletListService<
password: credentials.password, password: credentials.password,
language: credentials.language); language: credentials.language);
return MoneroWallet(filename: monero_wallet.getFilename())..init(); final wallet = MoneroWallet(filename: monero_wallet.getFilename());
await wallet.init();
return wallet;
} catch (e) { } catch (e) {
// TODO: Implement Exception fop wallet list service. // TODO: Implement Exception fop wallet list service.
print('MoneroWalletsManager Error: $e'); print('MoneroWalletsManager Error: $e');
@ -77,7 +79,7 @@ class MoneroWalletListService extends WalletListService<
} }
@override @override
Future<void> openWallet(String name, String password) async { Future<MoneroWallet> openWallet(String name, String password) async {
try { try {
final path = await pathForWallet(name: name, type: WalletType.monero); final path = await pathForWallet(name: name, type: WalletType.monero);
monero_wallet_manager.openWallet(path: path, password: password); monero_wallet_manager.openWallet(path: path, password: password);
@ -86,7 +88,10 @@ class MoneroWalletListService extends WalletListService<
// final walletInfo = walletInfoSource.values // final walletInfo = walletInfoSource.values
// .firstWhere((info) => info.id == id, orElse: () => null); // .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) { } catch (e) {
// TODO: Implement Exception fop wallet list service. // TODO: Implement Exception fop wallet list service.
print('MoneroWalletsManager Error: $e'); print('MoneroWalletsManager Error: $e');
@ -100,7 +105,7 @@ class MoneroWalletListService extends WalletListService<
} }
@override @override
Future<void> restoreFromKeys( Future<MoneroWallet> restoreFromKeys(
MoneroRestoreWalletFromKeysCredentials credentials) async { MoneroRestoreWalletFromKeysCredentials credentials) async {
try { try {
final path = final path =
@ -115,7 +120,10 @@ class MoneroWalletListService extends WalletListService<
viewKey: credentials.viewKey, viewKey: credentials.viewKey,
spendKey: credentials.spendKey); spendKey: credentials.spendKey);
return MoneroWallet(filename: monero_wallet.getFilename())..init(); final wallet = MoneroWallet(filename: monero_wallet.getFilename());
await wallet.init();
return wallet;
} catch (e) { } catch (e) {
// TODO: Implement Exception fop wallet list service. // TODO: Implement Exception fop wallet list service.
print('MoneroWalletsManager Error: $e'); print('MoneroWalletsManager Error: $e');
@ -124,7 +132,7 @@ class MoneroWalletListService extends WalletListService<
} }
@override @override
Future<void> restoreFromSeed( Future<MoneroWallet> restoreFromSeed(
MoneroRestoreWalletFromSeedCredentials credentials) async { MoneroRestoreWalletFromSeedCredentials credentials) async {
try { try {
final path = final path =
@ -136,7 +144,10 @@ class MoneroWalletListService extends WalletListService<
seed: credentials.mnemonic, seed: credentials.mnemonic,
restoreHeight: credentials.height); restoreHeight: credentials.height);
return MoneroWallet(filename: monero_wallet.getFilename())..init(); final wallet = MoneroWallet(filename: monero_wallet.getFilename());
await wallet.init();
return wallet;
} catch (e) { } catch (e) {
// TODO: Implement Exception fop wallet list service. // TODO: Implement Exception fop wallet list service.
print('MoneroWalletsManager Error: $e'); print('MoneroWalletsManager Error: $e');

View file

@ -12,6 +12,7 @@ class Palette {
static const Color blue = Color.fromRGBO(88, 143, 252, 1.0); static const Color blue = Color.fromRGBO(88, 143, 252, 1.0);
static const Color darkLavender = Color.fromRGBO(225, 238, 250, 1.0); static const Color darkLavender = Color.fromRGBO(225, 238, 250, 1.0);
static const Color nightBlue = Color.fromRGBO(46, 57, 96, 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 { class PaletteDark {

View file

@ -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<String> getWalletPassword({String walletName}) async {
final secureStorage = getIt.get<FlutterSecureStorage>();
final key = generateStoreKeyFor(
key: SecretStoreKey.moneroWalletPassword, walletName: walletName);
final encodedPassword = await secureStorage.read(key: key);
return decodeWalletPassword(password: encodedPassword);
}
// FIXME: move me
Future<void> loadCurrentWallet() async {
final appStore = getIt.get<AppStore>();
final name = getIt.get<SharedPreferences>().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<void> bootstrap() async {
final authenticationStore = getIt.get<AuthenticationStore>();
if (authenticationStore.state == AuthenticationState.uninitialized) {
authenticationStore.state =
getIt.get<SharedPreferences>().getString('current_wallet_name') == null
? AuthenticationState.denied
: AuthenticationState.installed;
}
_initialAuthReaction ??= autorun((_) async {
final state = authenticationStore.state;
if (state == AuthenticationState.installed) {
await loadCurrentWallet();
}
});
}

View file

@ -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/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.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:hive/hive.dart';
import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'di.dart';
// MARK: Import domains // MARK: Import domains
import 'package:cake_wallet/src/domain/common/contact.dart'; 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/monero/transaction_description.dart';
import 'package:cake_wallet/src/domain/exchange/trade.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/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/common/transaction_info.dart';
import 'package:cake_wallet/src/domain/monero/subaddress.dart'; import 'package:cake_wallet/src/domain/monero/subaddress.dart';
import 'package:cake_wallet/src/domain/common/wallet_type.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/new_node_page.dart';
import 'package:cake_wallet/src/screens/nodes/nodes_list_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/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/wallet_list/wallet_list_page.dart';
import 'package:cake_wallet/src/screens/new_wallet/new_wallet_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'; import 'package:cake_wallet/src/screens/setup_pin_code/setup_pin_code.dart';
@ -126,24 +128,18 @@ class Router {
Navigator.pushNamed(context, Routes.newWalletType)))); Navigator.pushNamed(context, Routes.newWalletType))));
case Routes.newWalletType: case Routes.newWalletType:
return CupertinoPageRoute<void>(builder: (_) => NewWalletTypePage()); return CupertinoPageRoute<void>(
builder: (_) => NewWalletTypePage(
onTypeSelected: (context, type) => Navigator.of(context)
.pushNamed(Routes.newWallet, arguments: type),
));
case Routes.newWallet: case Routes.newWallet:
final type = settings.arguments as WalletType; final type = settings.arguments as WalletType;
walletListService.changeWalletManger(walletType: type); final walletNewVM = getIt.get<WalletNewVM>(param1: type);
return CupertinoPageRoute<void>( return CupertinoPageRoute<void>(
builder: builder: (_) => NewWalletPage(walletNewVM));
(_) =>
ProxyProvider<AuthenticationStore, WalletCreationStore>(
update: (_, authStore, __) => WalletCreationStore(
authStore: authStore,
sharedPreferences: sharedPreferences,
walletListService: walletListService),
child: NewWalletPage(
walletsService: walletListService,
walletService: walletService,
sharedPreferences: sharedPreferences)));
case Routes.setupPin: case Routes.setupPin:
Function(BuildContext, String) callback; Function(BuildContext, String) callback;
@ -163,6 +159,13 @@ class Router {
callback == null ? null : callback(context, pin))), callback == null ? null : callback(context, pin))),
fullscreenDialog: true); fullscreenDialog: true);
case Routes.restoreWalletType:
return CupertinoPageRoute<void>(
builder: (_) => NewWalletTypePage(
onTypeSelected: (context, type) => Navigator.of(context)
.pushNamed(Routes.restoreWalletOptions, arguments: type),
));
case Routes.restoreOptions: case Routes.restoreOptions:
final type = settings.arguments as WalletType; final type = settings.arguments as WalletType;
walletListService.changeWalletManger(walletType: type); walletListService.changeWalletManger(walletType: type);
@ -175,7 +178,28 @@ class Router {
walletListService.changeWalletManger(walletType: type); walletListService.changeWalletManger(walletType: type);
return CupertinoPageRoute<void>( return CupertinoPageRoute<void>(
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: case Routes.restoreWalletOptionsFromWelcome:
return CupertinoPageRoute<void>( return CupertinoPageRoute<void>(
@ -186,7 +210,7 @@ class Router {
sharedPreferences: sharedPreferences)), sharedPreferences: sharedPreferences)),
child: SetupPinCodePage( child: SetupPinCodePage(
onPinCodeSetup: (context, _) => Navigator.pushNamed( onPinCodeSetup: (context, _) => Navigator.pushNamed(
context, Routes.restoreWalletOptions)))); context, Routes.restoreWalletType))));
case Routes.seed: case Routes.seed:
return MaterialPageRoute<void>( return MaterialPageRoute<void>(
@ -196,8 +220,11 @@ class Router {
callback: settings.arguments as void Function())); callback: settings.arguments as void Function()));
case Routes.restoreWalletFromSeed: case Routes.restoreWalletFromSeed:
final type = settings.arguments as WalletType; final args = settings.arguments as List<dynamic>;
walletListService.changeWalletManger(walletType: type); 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<void>( return CupertinoPageRoute<void>(
builder: (_) => builder: (_) =>
@ -207,11 +234,15 @@ class Router {
sharedPreferences: sharedPreferences, sharedPreferences: sharedPreferences,
walletListService: walletListService), walletListService: walletListService),
child: RestoreWalletFromSeedPage( child: RestoreWalletFromSeedPage(
walletsService: walletListService, type: type, language: language)));
walletService: walletService,
sharedPreferences: sharedPreferences)));
case Routes.restoreWalletFromKeys: case Routes.restoreWalletFromKeys:
final args = settings.arguments as List<dynamic>;
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<void>( return CupertinoPageRoute<void>(
builder: (_) => builder: (_) =>
ProxyProvider<AuthenticationStore, WalletRestorationStore>( ProxyProvider<AuthenticationStore, WalletRestorationStore>(
@ -256,22 +287,16 @@ class Router {
case Routes.sendTemplate: case Routes.sendTemplate:
return CupertinoPageRoute<void>( return CupertinoPageRoute<void>(
builder: (_) => Provider( builder: (_) => Provider(
create: (_) => SendStore( create: (_) => SendStore(
walletService: walletService, walletService: walletService,
priceStore: priceStore, priceStore: priceStore,
transactionDescriptions: transactionDescriptions), transactionDescriptions: transactionDescriptions),
child: SendTemplatePage()) child: SendTemplatePage()));
);
case Routes.receive: case Routes.receive:
return CupertinoPageRoute<void>( return CupertinoPageRoute<void>(
fullscreenDialog: true, fullscreenDialog: true, builder: (_) => getIt.get<ReceivePage>());
builder: (_) => MultiProvider(providers: [
Provider(
create: (_) =>
SubaddressListStore(walletService: walletService))
], child: ReceivePage()));
case Routes.transactionDetails: case Routes.transactionDetails:
return CupertinoPageRoute<void>( return CupertinoPageRoute<void>(
@ -281,10 +306,8 @@ class Router {
case Routes.newSubaddress: case Routes.newSubaddress:
return CupertinoPageRoute<void>( return CupertinoPageRoute<void>(
builder: (_) => Provider( builder: (_) =>
create: (_) => getIt.get<AddressEditOrCreatePage>(param1: settings.arguments));
SubadrressCreationStore(walletService: walletService),
child: NewSubaddressPage()));
case Routes.disclaimer: case Routes.disclaimer:
return CupertinoPageRoute<void>(builder: (_) => DisclaimerPage()); return CupertinoPageRoute<void>(builder: (_) => DisclaimerPage());
@ -294,7 +317,15 @@ class Router {
builder: (_) => DisclaimerPage(isReadOnly: true)); builder: (_) => DisclaimerPage(isReadOnly: true));
case Routes.seedLanguage: case Routes.seedLanguage:
return CupertinoPageRoute<void>(builder: (_) => SeedLanguage()); final args = settings.arguments as List<dynamic>;
final type = args.first as WalletType;
final redirectRoute = args[1] as String;
return CupertinoPageRoute<void>(builder: (_) {
return SeedLanguage(
onConfirm: (context, lang) => Navigator.of(context)
.popAndPushNamed(redirectRoute, arguments: [type, lang]));
});
case Routes.walletList: case Routes.walletList:
return MaterialPageRoute<void>( return MaterialPageRoute<void>(
@ -306,17 +337,18 @@ class Router {
child: WalletListPage())); child: WalletListPage()));
case Routes.auth: case Routes.auth:
return MaterialPageRoute<void>( return null;
fullscreenDialog: true, // return MaterialPageRoute<void>(
builder: (_) => Provider( // fullscreenDialog: true,
create: (_) => AuthStore( // builder: (_) => Provider(
sharedPreferences: sharedPreferences, // create: (_) => AuthStore(
userService: userService, // sharedPreferences: sharedPreferences,
walletService: walletService), // userService: userService,
child: AuthPage( // walletService: walletService),
onAuthenticationFinished: // child: AuthPage(
settings.arguments as OnAuthenticationFinished), // onAuthenticationFinished:
)); // settings.arguments as OnAuthenticationFinished),
// ));
case Routes.unlock: case Routes.unlock:
return MaterialPageRoute<void>( return MaterialPageRoute<void>(
@ -455,15 +487,13 @@ class Router {
], child: SubaddressListPage())); ], child: SubaddressListPage()));
case Routes.restoreWalletFromSeedDetails: case Routes.restoreWalletFromSeedDetails:
final args = settings.arguments as List;
final walletRestorationFromSeedVM =
getIt.get<WalletRestorationFromSeedVM>(param1: args);
return CupertinoPageRoute<void>( return CupertinoPageRoute<void>(
builder: (_) => builder: (_) => RestoreWalletFromSeedDetailsPage(
ProxyProvider<AuthenticationStore, WalletRestorationStore>( walletRestorationFromSeedVM: walletRestorationFromSeedVM));
update: (_, authStore, __) => WalletRestorationStore(
authStore: authStore,
sharedPreferences: sharedPreferences,
walletListService: walletListService,
seed: settings.arguments as List<MnemoticItem>),
child: RestoreWalletFromSeedDetailsPage()));
case Routes.exchange: case Routes.exchange:
return MaterialPageRoute<void>( return MaterialPageRoute<void>(
@ -487,22 +517,24 @@ class Router {
case Routes.exchangeTemplate: case Routes.exchangeTemplate:
return MaterialPageRoute<void>( return MaterialPageRoute<void>(
builder: (_) => Provider(create: (_) { builder: (_) => Provider(
final xmrtoprovider = XMRTOExchangeProvider(); create: (_) {
final xmrtoprovider = XMRTOExchangeProvider();
return ExchangeStore( return ExchangeStore(
initialProvider: xmrtoprovider, initialProvider: xmrtoprovider,
initialDepositCurrency: CryptoCurrency.xmr, initialDepositCurrency: CryptoCurrency.xmr,
initialReceiveCurrency: CryptoCurrency.btc, initialReceiveCurrency: CryptoCurrency.btc,
trades: trades, trades: trades,
providerList: [ providerList: [
xmrtoprovider, xmrtoprovider,
ChangeNowExchangeProvider(), ChangeNowExchangeProvider(),
MorphTokenExchangeProvider(trades: trades) MorphTokenExchangeProvider(trades: trades)
], ],
walletStore: walletStore); walletStore: walletStore);
}, child: ExchangeTemplatePage(),) },
); child: ExchangeTemplatePage(),
));
case Routes.settings: case Routes.settings:
return MaterialPageRoute<void>( return MaterialPageRoute<void>(

View file

@ -46,4 +46,5 @@ class Routes {
static const newWalletType = '/new_wallet_type'; static const newWalletType = '/new_wallet_type';
static const sendTemplate = '/send_template'; static const sendTemplate = '/send_template';
static const exchangeTemplate = '/exchange_template'; static const exchangeTemplate = '/exchange_template';
static const restoreWalletType = '/restore_wallet_type';
} }

View file

@ -1 +1,3 @@
abstract class Balance {} abstract class Balance {
const Balance();
}

View file

@ -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;
}

View file

@ -1,17 +0,0 @@
class MnemoticItem {
MnemoticItem({String text, this.dic}) : _text = text;
String get text => _text;
final List<String> dic;
String _text;
bool isCorrect() => dic.contains(text);
void changeText(String text) {
_text = text;
}
@override
String toString() => text;
}

View file

@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'package:cake_wallet/bitcoin/bitcoin_wallet.manager.dart';
import 'package:cake_wallet/bitcoin/key.dart'; import 'package:cake_wallet/bitcoin/key.dart';
import 'package:cake_wallet/src/domain/common/wallet_info.dart'; import 'package:cake_wallet/src/domain/common/wallet_info.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@ -119,7 +118,7 @@ class WalletListService {
MoneroWalletsManager(walletInfoSource: walletInfoSource); MoneroWalletsManager(walletInfoSource: walletInfoSource);
break; break;
case WalletType.bitcoin: case WalletType.bitcoin:
walletsManager = BitcoinWalletManager(); // walletsManager = BitcoinWalletManager();
break; break;
case WalletType.none: case WalletType.none:
walletsManager = null; walletsManager = null;

View file

@ -35,10 +35,10 @@ void setReactions(
settingsStore: settingsStore, settingsStore: settingsStore,
priceStore: priceStore); priceStore: priceStore);
autorun((_) async { autorun((_) async {
if (authenticationStore.state == AuthenticationState.allowed) { // if (authenticationStore.state == AuthenticationState.allowed) {
await loginStore.loadCurrentWallet(); // await loginStore.loadCurrentWallet();
authenticationStore.state = AuthenticationState.readyToLogin; // authenticationStore.state = AuthenticationState.readyToLogin;
} // }
}); });
} }

View file

@ -1,10 +1,9 @@
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:provider/provider.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/stores/auth/auth_state.dart'; import 'package:cake_wallet/view_model/auth_state.dart';
import 'package:cake_wallet/src/stores/auth/auth_store.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/screens/pin_code/pin_code.dart';
import 'package:cake_wallet/src/stores/settings/settings_store.dart'; import 'package:cake_wallet/src/stores/settings/settings_store.dart';
import 'package:cake_wallet/src/domain/common/biometric_auth.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); typedef OnAuthenticationFinished = void Function(bool, AuthPageState);
class AuthPage extends StatefulWidget { 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 OnAuthenticationFinished onAuthenticationFinished;
final bool closable; final bool closable;
@ -25,40 +28,13 @@ class AuthPageState extends State<AuthPage> {
final _key = GlobalKey<ScaffoldState>(); final _key = GlobalKey<ScaffoldState>();
final _pinCodeKey = GlobalKey<PinCodeState>(); final _pinCodeKey = GlobalKey<PinCodeState>();
final _backArrowImageDarkTheme = final _backArrowImageDarkTheme =
Image.asset('assets/images/back_arrow_dark_theme.png'); Image.asset('assets/images/back_arrow_dark_theme.png');
ReactionDisposer _reaction;
void changeProcessText(String text) {
_key.currentState.showSnackBar(
SnackBar(content: Text(text), backgroundColor: Colors.green));
}
void close() => Navigator.of(_key.currentContext).pop();
@override @override
Widget build(BuildContext context) { void initState() {
final authStore = Provider.of<AuthStore>(context); _reaction ??=
final settingsStore = Provider.of<SettingsStore>(context); reaction((_) => widget.authViewModel.state, (AuthState state) {
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) {
if (state is AuthenticatedSuccessfully) { if (state is AuthenticatedSuccessfully) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (widget.onAuthenticationFinished != null) { if (widget.onAuthenticationFinished != null) {
@ -119,32 +95,69 @@ class AuthPageState extends State<AuthPage> {
}); });
} }
}); });
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<AuthStore>(context);
// final settingsStore = Provider.of<SettingsStore>(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( return Scaffold(
key: _key, key: _key,
appBar: CupertinoNavigationBar( appBar: CupertinoNavigationBar(
leading: widget.closable leading: widget.closable
? SizedBox( ? SizedBox(
height: 37, height: 37,
width: 20, width: 20,
child: ButtonTheme( child: ButtonTheme(
minWidth: double.minPositive, minWidth: double.minPositive,
child: FlatButton( child: FlatButton(
highlightColor: Colors.transparent, highlightColor: Colors.transparent,
splashColor: Colors.transparent, splashColor: Colors.transparent,
padding: EdgeInsets.all(0), padding: EdgeInsets.all(0),
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
child: _backArrowImageDarkTheme), child: _backArrowImageDarkTheme),
), ),
) )
: Container(), : Container(),
backgroundColor: Theme.of(context).backgroundColor, backgroundColor: Theme.of(context).backgroundColor,
border: null, border: null,
), ),
resizeToAvoidBottomPadding: false, resizeToAvoidBottomPadding: false,
body: PinCode( body: PinCode(
(pin, _) => authStore.auth( (pin, _) => widget.authViewModel
password: pin.fold('', (ac, val) => ac + '$val')), .auth(password: pin.fold('', (ac, val) => ac + '$val')),
false, false,
_pinCodeKey)); _pinCodeKey));
} }

View file

@ -14,15 +14,16 @@ Widget createLoginPage(
@required WalletService walletService, @required WalletService walletService,
@required WalletListService walletListService, @required WalletListService walletListService,
@required AuthenticationStore authenticationStore}) => @required AuthenticationStore authenticationStore}) =>
Provider( null;
create: (_) => AuthStore( // Provider(
sharedPreferences: sharedPreferences, // create: (_) => AuthStore(
userService: userService, // sharedPreferences: sharedPreferences,
walletService: walletService), // userService: userService,
child: AuthPage( // walletService: walletService),
onAuthenticationFinished: (isAuthenticated, state) { // child: AuthPage(
if (isAuthenticated) { // onAuthenticationFinished: (isAuthenticated, state) {
authenticationStore.loggedIn(); // if (isAuthenticated) {
} // authenticationStore.loggedIn();
}, // }
closable: false)); // },
// closable: false));

View file

@ -11,13 +11,14 @@ Widget createUnlockPage(
@required UserService userService, @required UserService userService,
@required WalletService walletService, @required WalletService walletService,
@required Function(bool, AuthPageState) onAuthenticationFinished}) => @required Function(bool, AuthPageState) onAuthenticationFinished}) =>
WillPopScope( null;
onWillPop: () async => false, // WillPopScope(
child: Provider( // onWillPop: () async => false,
create: (_) => AuthStore( // child: Provider(
sharedPreferences: sharedPreferences, // create: (_) => AuthStore(
userService: userService, // sharedPreferences: sharedPreferences,
walletService: walletService), // userService: userService,
child: AuthPage( // walletService: walletService),
onAuthenticationFinished: onAuthenticationFinished, // child: AuthPage(
closable: false))); // onAuthenticationFinished: onAuthenticationFinished,
// closable: false)));

View file

@ -10,12 +10,19 @@ enum AppBarStyle { regular, withShadow }
abstract class BasePage extends StatelessWidget { abstract class BasePage extends StatelessWidget {
String get title => null; String get title => null;
bool get isModalBackButton => false; bool get isModalBackButton => false;
Color get backgroundLightColor => Colors.white; Color get backgroundLightColor => Colors.white;
Color get backgroundDarkColor => PaletteDark.darkNightBlue; Color get backgroundDarkColor => PaletteDark.darkNightBlue;
bool get resizeToAvoidBottomPadding => true; bool get resizeToAvoidBottomPadding => true;
AppBarStyle get appBarStyle => AppBarStyle.regular; AppBarStyle get appBarStyle => AppBarStyle.regular;
Widget Function(BuildContext, Widget) get rootWrapper => null;
final _backArrowImage = Image.asset('assets/images/back_arrow.png'); final _backArrowImage = Image.asset('assets/images/back_arrow.png');
final _backArrowImageDarkTheme = final _backArrowImageDarkTheme =
Image.asset('assets/images/back_arrow_dark_theme.png'); Image.asset('assets/images/back_arrow_dark_theme.png');
@ -83,9 +90,8 @@ abstract class BasePage extends StatelessWidget {
leading: leading(context), leading: leading(context),
middle: middle(context), middle: middle(context),
trailing: trailing(context), trailing: trailing(context),
backgroundColor: _isDarkTheme backgroundColor:
? backgroundDarkColor _isDarkTheme ? backgroundDarkColor : backgroundLightColor);
: backgroundLightColor);
case AppBarStyle.withShadow: case AppBarStyle.withShadow:
return NavBar.withShadow( return NavBar.withShadow(
@ -93,9 +99,8 @@ abstract class BasePage extends StatelessWidget {
leading: leading(context), leading: leading(context),
middle: middle(context), middle: middle(context),
trailing: trailing(context), trailing: trailing(context),
backgroundColor: _isDarkTheme backgroundColor:
? backgroundDarkColor _isDarkTheme ? backgroundDarkColor : backgroundLightColor);
: backgroundLightColor);
default: default:
return NavBar( return NavBar(
@ -103,9 +108,8 @@ abstract class BasePage extends StatelessWidget {
leading: leading(context), leading: leading(context),
middle: middle(context), middle: middle(context),
trailing: trailing(context), trailing: trailing(context),
backgroundColor: _isDarkTheme backgroundColor:
? backgroundDarkColor _isDarkTheme ? backgroundDarkColor : backgroundLightColor);
: backgroundLightColor);
} }
} }
@ -116,13 +120,14 @@ abstract class BasePage extends StatelessWidget {
final _themeChanger = Provider.of<ThemeChanger>(context); final _themeChanger = Provider.of<ThemeChanger>(context);
final _isDarkTheme = _themeChanger.getTheme() == Themes.darkTheme; final _isDarkTheme = _themeChanger.getTheme() == Themes.darkTheme;
return Scaffold( final root = Scaffold(
backgroundColor: _isDarkTheme backgroundColor:
? backgroundDarkColor _isDarkTheme ? backgroundDarkColor : backgroundLightColor,
: backgroundLightColor,
resizeToAvoidBottomPadding: resizeToAvoidBottomPadding, resizeToAvoidBottomPadding: resizeToAvoidBottomPadding,
appBar: appBar(context), appBar: appBar(context),
body: SafeArea(child: body(context)), body: SafeArea(child: body(context)),
floatingActionButton: floatingActionButton(context)); floatingActionButton: floatingActionButton(context));
return rootWrapper?.call(context, root) ?? root;
} }
} }

View file

@ -1,28 +1,36 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/cupertino.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/wallet_card.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/trade_history_panel.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/trade_history_panel.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/menu_widget.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/menu_widget.dart';
class DashboardPage extends StatelessWidget { class DashboardPage extends StatelessWidget {
DashboardPage({@required this.walletViewModel});
final DashboardViewModel walletViewModel;
final _bodyKey = GlobalKey(); final _bodyKey = GlobalKey();
@override @override
Widget build(BuildContext context) => DashboardPageBody(key: _bodyKey); Widget build(BuildContext context) =>
DashboardPageBody(key: _bodyKey, walletViewModel: walletViewModel);
} }
class DashboardPageBody extends StatefulWidget { class DashboardPageBody extends StatefulWidget {
DashboardPageBody({Key key}) : super(key: key); DashboardPageBody({Key key, @required this.walletViewModel})
: super(key: key);
final DashboardViewModel walletViewModel;
@override @override
DashboardPageBodyState createState() => DashboardPageBodyState(); DashboardPageBodyState createState() => DashboardPageBodyState();
} }
class DashboardPageBodyState extends State<DashboardPageBody> { class DashboardPageBodyState extends State<DashboardPageBody> {
@override @override
Widget build(BuildContext context) { 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, color: Theme.of(context).primaryTextTheme.title.color,
); );
@ -30,15 +38,10 @@ class DashboardPageBodyState extends State<DashboardPageBody> {
child: Scaffold( child: Scaffold(
body: Container( body: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(colors: [
colors: [ Theme.of(context).scaffoldBackgroundColor,
Theme.of(context).scaffoldBackgroundColor, Theme.of(context).primaryColor
Theme.of(context).primaryColor ], begin: Alignment.centerLeft, end: Alignment.centerRight)),
],
begin: Alignment.centerLeft,
end: Alignment.centerRight
)
),
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
Container( Container(
@ -55,22 +58,17 @@ class DashboardPageBodyState extends State<DashboardPageBody> {
padding: EdgeInsets.all(0), padding: EdgeInsets.all(0),
onPressed: () async { onPressed: () async {
await showDialog<void>( await showDialog<void>(
builder: (_) => MenuWidget(), builder: (_) => MenuWidget(), context: context);
context: context
);
}, },
child: menuButton), child: menuButton),
), ),
), ),
), ),
Padding( Padding(
padding: EdgeInsets.only(left: 20, top: 20), padding: EdgeInsets.only(left: 20, top: 20),
child: WalletCard(), child: WalletCard(walletVM: widget.walletViewModel)),
), SizedBox(height: 28),
SizedBox( Expanded(child: TradeHistoryPanel(dashboardViewModel: widget.walletViewModel))
height: 28,
),
Expanded(child: TradeHistoryPanel())
], ],
), ),
), ),

View file

@ -17,7 +17,7 @@ class ButtonHeader extends SliverPersistentHeaderDelegate {
@override @override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
final actionListStore = Provider.of<ActionListStore>(context); // final actionListStore = Provider.of<ActionListStore>(context);
final historyPanelWidth = MediaQuery.of(context).size.width; final historyPanelWidth = MediaQuery.of(context).size.width;
final _themeChanger = Provider.of<ThemeChanger>(context); final _themeChanger = Provider.of<ThemeChanger>(context);
@ -97,44 +97,44 @@ class ButtonHeader extends SliverPersistentHeaderDelegate {
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Theme.of(context).primaryTextTheme.caption.color))), color: Theme.of(context).primaryTextTheme.caption.color))),
PopupMenuItem( // PopupMenuItem(
value: 0, // value: 0,
child: Observer( // child: Observer(
builder: (_) => Row( // builder: (_) => Row(
mainAxisAlignment: // mainAxisAlignment:
MainAxisAlignment // MainAxisAlignment
.spaceBetween, // .spaceBetween,
children: [ // children: [
Text(S.of(context).incoming), // Text(S.of(context).incoming),
Checkbox( // Checkbox(
value: actionListStore // value: actionListStore
.transactionFilterStore // .transactionFilterStore
.displayIncoming, // .displayIncoming,
onChanged: (value) => // onChanged: (value) =>
actionListStore // actionListStore
.transactionFilterStore // .transactionFilterStore
.toggleIncoming(), // .toggleIncoming(),
) // )
]))), // ]))),
PopupMenuItem( // PopupMenuItem(
value: 1, // value: 1,
child: Observer( // child: Observer(
builder: (_) => Row( // builder: (_) => Row(
mainAxisAlignment: // mainAxisAlignment:
MainAxisAlignment // MainAxisAlignment
.spaceBetween, // .spaceBetween,
children: [ // children: [
Text(S.of(context).outgoing), // Text(S.of(context).outgoing),
Checkbox( // Checkbox(
value: actionListStore // value: actionListStore
.transactionFilterStore // .transactionFilterStore
.displayOutgoing, // .displayOutgoing,
onChanged: (value) => // onChanged: (value) =>
actionListStore // actionListStore
.transactionFilterStore // .transactionFilterStore
.toggleOutgoing(), // .toggleOutgoing(),
) // )
]))), // ]))),
PopupMenuItem( PopupMenuItem(
value: 2, value: 2,
child: child:
@ -156,17 +156,17 @@ class ButtonHeader extends SliverPersistentHeaderDelegate {
.spaceBetween, .spaceBetween,
children: [ children: [
Text('XMR.TO'), Text('XMR.TO'),
Checkbox( // Checkbox(
value: actionListStore // value: actionListStore
.tradeFilterStore // .tradeFilterStore
.displayXMRTO, // .displayXMRTO,
onChanged: (value) => // onChanged: (value) =>
actionListStore // actionListStore
.tradeFilterStore // .tradeFilterStore
.toggleDisplayExchange( // .toggleDisplayExchange(
ExchangeProviderDescription // ExchangeProviderDescription
.xmrto), // .xmrto),
) // )
]))), ]))),
PopupMenuItem( PopupMenuItem(
value: 4, value: 4,
@ -177,17 +177,17 @@ class ButtonHeader extends SliverPersistentHeaderDelegate {
.spaceBetween, .spaceBetween,
children: [ children: [
Text('Change.NOW'), Text('Change.NOW'),
Checkbox( // Checkbox(
value: actionListStore // value: actionListStore
.tradeFilterStore // .tradeFilterStore
.displayChangeNow, // .displayChangeNow,
onChanged: (value) => // onChanged: (value) =>
actionListStore // actionListStore
.tradeFilterStore // .tradeFilterStore
.toggleDisplayExchange( // .toggleDisplayExchange(
ExchangeProviderDescription // ExchangeProviderDescription
.changeNow), // .changeNow),
) // )
]))), ]))),
PopupMenuItem( PopupMenuItem(
value: 5, value: 5,
@ -198,17 +198,17 @@ class ButtonHeader extends SliverPersistentHeaderDelegate {
.spaceBetween, .spaceBetween,
children: [ children: [
Text('MorphToken'), Text('MorphToken'),
Checkbox( // Checkbox(
value: actionListStore // value: actionListStore
.tradeFilterStore // .tradeFilterStore
.displayMorphToken, // .displayMorphToken,
onChanged: (value) => // onChanged: (value) =>
actionListStore // actionListStore
.tradeFilterStore // .tradeFilterStore
.toggleDisplayExchange( // .toggleDisplayExchange(
ExchangeProviderDescription // ExchangeProviderDescription
.morphToken), // .morphToken),
) // )
]))) ])))
], ],
child: filterButton, child: filterButton,
@ -225,10 +225,10 @@ class ButtonHeader extends SliverPersistentHeaderDelegate {
.add(Duration(days: 1))); .add(Duration(days: 1)));
if (picked != null && picked.length == 2) { if (picked != null && picked.length == 2) {
actionListStore.transactionFilterStore // actionListStore.transactionFilterStore
.changeStartDate(picked.first); // .changeStartDate(picked.first);
actionListStore.transactionFilterStore // actionListStore.transactionFilterStore
.changeEndDate(picked.last); // .changeEndDate(picked.last);
} }
} }
}, },

View file

@ -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/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:cake_wallet/routes.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 'date_section_raw.dart';
import 'trade_row.dart'; import 'trade_row.dart';
import 'transaction_raw.dart'; import 'transaction_raw.dart';
import 'button_header.dart'; import 'button_header.dart';
class TradeHistoryPanel extends StatefulWidget { class TradeHistoryPanel extends StatefulWidget {
TradeHistoryPanel({this.dashboardViewModel});
DashboardViewModel dashboardViewModel;
@override @override
TradeHistoryPanelState createState() => TradeHistoryPanelState(); TradeHistoryPanelState createState() => TradeHistoryPanelState();
} }
@ -44,119 +49,127 @@ class TradeHistoryPanelState extends State<TradeHistoryPanel> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final actionListStore = Provider.of<ActionListStore>(context); // final actionListStore = Provider.of<ActionListStore>(context);
final settingsStore = Provider.of<SettingsStore>(context); // final settingsStore = Provider.of<SettingsStore>(context);
final transactionDateFormat = DateFormat("HH:mm"); final transactionDateFormat = DateFormat('HH:mm');
return Container( return Container(
height: MediaQuery.of(context).size.height, height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
alignment: Alignment.bottomCenter,
child: AnimatedContainer(
width: MediaQuery.of(context).size.width, width: MediaQuery.of(context).size.width,
height: panelHeight, alignment: Alignment.bottomCenter,
duration: Duration(milliseconds: 1000), child: AnimatedContainer(
curve: Curves.fastOutSlowIn, width: MediaQuery.of(context).size.width,
child: ClipRRect( height: panelHeight,
borderRadius: BorderRadius.only(topLeft: Radius.circular(20), topRight: Radius.circular(20)), duration: Duration(milliseconds: 1000),
child: CustomScrollView( curve: Curves.fastOutSlowIn,
slivers: <Widget>[ child: ClipRRect(
SliverPersistentHeader( borderRadius: BorderRadius.only(
delegate: ButtonHeader(), topLeft: Radius.circular(20),
pinned: true, topRight: Radius.circular(20)),
floating: false, child: CustomScrollView(
), slivers: <Widget>[
Observer( SliverPersistentHeader(
key: _listObserverKey, delegate: ButtonHeader(),
builder: (_) { pinned: true,
final items = actionListStore.items == null floating: false,
? <String>[] ),
: actionListStore.items; Observer(
final itemsCount = items.length + 1; key: _listObserverKey,
final symbol = settingsStore.fiatCurrency.toString(); builder: (_) {
double freeSpaceHeight = MediaQuery.of(context).size.height - 496; // final items = actionListStore.items == null
// ? <String>[]
// : 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( return SliverList(
key: _listKey, key: _listKey,
delegate: SliverChildBuilderDelegate( delegate:
(context, index) { SliverChildBuilderDelegate((context, index) {
if (index == itemsCount - 1) {
freeSpaceHeight = freeSpaceHeight >= 0
? freeSpaceHeight
: 0;
if (index == itemsCount - 1) { return Container(
freeSpaceHeight = freeSpaceHeight >= 0 ? freeSpaceHeight : 0; 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( return Container(
height: freeSpaceHeight, color: Theme.of(context).backgroundColor);
width: MediaQuery.of(context).size.width, }, childCount: itemsCount));
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
)
);
})
],
),
)
),
);
} }
} }

View file

@ -1,21 +1,27 @@
import 'dart:async'; import 'dart:async';
import 'package:cake_wallet/palette.dart'; 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:flutter/services.dart';
import 'package:provider/provider.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/balance/balance_store.dart';
import 'package:cake_wallet/src/stores/settings/settings_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/sync/sync_store.dart';
import 'package:cake_wallet/src/stores/wallet/wallet_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/generated/i18n.dart';
import 'package:cake_wallet/src/domain/common/sync_status.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/src/screens/receive/widgets/qr_image.dart';
import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/view_model/dashboard_view_model.dart';
class WalletCard extends StatefulWidget { class WalletCard extends StatefulWidget {
WalletCard({this.walletVM});
DashboardViewModel walletVM;
@override @override
WalletCardState createState() => WalletCardState(); WalletCardState createState() => WalletCardState();
} }
@ -50,14 +56,12 @@ class WalletCardState extends State<WalletCard> {
cardWidth = screenWidth; cardWidth = screenWidth;
opacity = 1; opacity = 1;
}); });
Timer(Duration(milliseconds: 500), () => Timer(Duration(milliseconds: 500), () => setState(() => isDraw = true));
setState(() => isDraw = true)
);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final List<Color> colorsSync = [ final colorsSync = [
Theme.of(context).cardTheme.color, Theme.of(context).cardTheme.color,
Theme.of(context).hoverColor Theme.of(context).hoverColor
]; ];
@ -67,77 +71,67 @@ class WalletCardState extends State<WalletCard> {
height: cardHeight, height: cardHeight,
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: AnimatedContainer( child: AnimatedContainer(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
width: cardWidth, width: cardWidth,
height: cardHeight, height: cardHeight,
duration: Duration(milliseconds: 500), duration: Duration(milliseconds: 500),
curve: Curves.fastOutSlowIn, curve: Curves.fastOutSlowIn,
padding: EdgeInsets.only( padding: EdgeInsets.only(top: 1, left: 1, bottom: 1),
top: 1, decoration: BoxDecoration(
left: 1, borderRadius: BorderRadius.only(
bottom: 1 topLeft: Radius.circular(10),
), bottomLeft: Radius.circular(10)),
decoration: BoxDecoration( color: Theme.of(context).focusColor,
borderRadius: BorderRadius.only(topLeft: Radius.circular(10), bottomLeft: Radius.circular(10)), boxShadow: [
color: Theme.of(context).focusColor, BoxShadow(
boxShadow: [ color: PaletteDark.darkNightBlue.withOpacity(0.5),
BoxShadow( blurRadius: 8,
color: PaletteDark.darkNightBlue.withOpacity(0.5), offset: Offset(5, 5))
blurRadius: 8, ]),
offset: Offset(5, 5)) child: ClipRRect(
] borderRadius: BorderRadius.only(
), topLeft: Radius.circular(10), bottomLeft: Radius.circular(10)),
child: ClipRRect( child: Container(
borderRadius: BorderRadius.only(topLeft: Radius.circular(10), bottomLeft: Radius.circular(10)), width: cardWidth,
child: Container( height: cardHeight,
width: cardWidth, color: Theme.of(context).cardColor,
height: cardHeight, child: InkWell(
color: Theme.of(context).cardColor, onTap: () => setState(() => isFrontSide = !isFrontSide),
child: InkWell( child: isFrontSide
onTap: () => setState(() => isFrontSide = !isFrontSide), ? frontSide(colorsSync)
child: isFrontSide : backSide(colorsSync)),
? frontSide(colorsSync)
: backSide(colorsSync)
), ),
), )),
)
),
); );
} }
Widget frontSide(List<Color> colorsSync) { Widget frontSide(List<Color> colorsSync) {
final syncStore = Provider.of<SyncStore>(context); // final syncStore = Provider.of<SyncStore>(context);
final walletStore = Provider.of<WalletStore>(context); // final walletStore = Provider.of<WalletStore>(context);
final settingsStore = Provider.of<SettingsStore>(context); final settingsStore = Provider.of<SettingsStore>(context);
final balanceStore = Provider.of<BalanceStore>(context); // final balanceStore = Provider.of<BalanceStore>(context);
final triangleButton = Image.asset('assets/images/triangle.png', final triangleButton = Image.asset(
'assets/images/triangle.png',
color: Theme.of(context).primaryTextTheme.title.color, color: Theme.of(context).primaryTextTheme.title.color,
); );
return Observer( return Observer(
key: _syncingObserverKey, key: _syncingObserverKey,
builder: (_) { builder: (_) {
final status = syncStore.status; final status = widget.walletVM.status;
final statusText = status.title(); final statusText = status.title();
final progress = syncStore.status.progress(); final progress = status.progress();
final indicatorWidth = progress * cardWidth; final indicatorWidth = progress * cardWidth;
final shortAddress = widget.walletVM.address
String shortAddress = walletStore.subaddress.address; .replaceRange(4, widget.walletVM.address.length - 4, '...');
shortAddress = shortAddress.replaceRange(4, shortAddress.length - 4, '...');
var descriptionText = ''; var descriptionText = '';
if (status is SyncingSyncStatus) { if (status is SyncingSyncStatus) {
descriptionText = S descriptionText = S.of(context).Blocks_remaining(status.toString());
.of(context)
.Blocks_remaining(
syncStore.status.toString());
} }
if (status is FailedSyncStatus) { if (status is FailedSyncStatus) {
descriptionText = S descriptionText = S.of(context).please_try_to_connect_to_another_node;
.of(context)
.please_try_to_connect_to_another_node;
} }
return Container( return Container(
@ -149,183 +143,206 @@ class WalletCardState extends State<WalletCard> {
height: cardHeight, height: cardHeight,
width: indicatorWidth, width: indicatorWidth,
decoration: BoxDecoration( 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( gradient: LinearGradient(
colors: colorsSync, colors: colorsSync,
begin: Alignment.topCenter, begin: Alignment.topCenter,
end: Alignment.bottomCenter end: Alignment.bottomCenter)),
)
),
), ),
progress != 1 progress != 1
? Positioned( ? Positioned(
left: indicatorWidth, left: indicatorWidth,
top: 0, top: 0,
bottom: 0, bottom: 0,
child: Container( child: Container(
width: 1, width: 1,
height: cardHeight, height: cardHeight,
color: Theme.of(context).focusColor, color: Theme.of(context).focusColor,
) ))
) : Offstage(),
: Offstage(), isDraw
isDraw ? Positioned( ? Positioned(
left: 20, left: 20,
right: 20, right: 20,
top: 30, top: 30,
bottom: 30, bottom: 30,
child: Container( child: Container(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[ children: <Widget>[
Column( Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[ children: <Widget>[
InkWell( Column(
onTap: () {}, crossAxisAlignment: CrossAxisAlignment.start,
child: Row( children: <Widget>[
children: <Widget>[ InkWell(
Text( onTap: () {},
walletStore.name, child: Row(
style: TextStyle( children: <Widget>[
fontSize: 20, Text(
fontWeight: FontWeight.bold, widget.walletVM.name,
color: Theme.of(context).primaryTextTheme.title.color style: TextStyle(
), fontSize: 20,
fontWeight: FontWeight.bold,
color: Theme.of(context)
.primaryTextTheme
.title
.color),
),
SizedBox(width: 10),
triangleButton
],
), ),
SizedBox(width: 10), ),
triangleButton SizedBox(height: 5),
], if (widget.walletVM.subname?.isNotEmpty ?? false)
), Text(
widget.walletVM.subname,
style: TextStyle(
fontSize: 12,
color: Theme.of(context)
.primaryTextTheme
.caption
.color),
)
],
), ),
SizedBox( Container(
height: 5, width: 98,
), height: 32,
Text( alignment: Alignment.center,
walletStore.account.label, decoration: BoxDecoration(
style: TextStyle( color: Theme.of(context)
fontSize: 12, .accentTextTheme
color: Theme.of(context).primaryTextTheme.caption.color .subtitle
.backgroundColor,
borderRadius: BorderRadius.all(
Radius.circular(16))),
child: Text(
shortAddress,
style: TextStyle(
fontSize: 12,
color: Theme.of(context)
.primaryTextTheme
.caption
.color),
), ),
) )
], ],
), ),
Container( status is SyncedSyncStatus
width: 98, ? Observer(
height: 32, key: _balanceObserverKey,
alignment: Alignment.center, builder: (_) {
decoration: BoxDecoration( final balanceDisplayMode =
color: Theme.of(context).accentTextTheme.subtitle.backgroundColor, BalanceDisplayMode.fullBalance;
borderRadius: BorderRadius.all(Radius.circular(16)) // settingsStore.balanceDisplayMode;
), final symbol =
child: Text( settingsStore.fiatCurrency.toString();
shortAddress, var balance = '---';
style: TextStyle( var fiatBalance = '---';
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 = '---';
if (balanceDisplayMode == if (balanceDisplayMode ==
BalanceDisplayMode.availableBalance) { BalanceDisplayMode.availableBalance) {
balance = balance = widget.walletVM.balance
balanceStore.unlockedBalance ?? .unlockedBalance ??
'0.0'; '0.0';
fiatBalance = fiatBalance = '\$ 123.43';
'$symbol ${balanceStore.fiatUnlockedBalance}'; // '$symbol ${balanceStore.fiatUnlockedBalance}';
} }
if (balanceDisplayMode == if (balanceDisplayMode ==
BalanceDisplayMode.fullBalance) { BalanceDisplayMode.fullBalance) {
balance = balance = widget.walletVM.balance
balanceStore.fullBalance ?? '0.0'; .totalBalance ??
fiatBalance = '0.0';
'$symbol ${balanceStore.fiatFullBalance}'; fiatBalance = '\$ 123.43';
} // '$symbol ${balanceStore.fiatFullBalance}';
}
return Row( return Row(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment:
mainAxisAlignment: MainAxisAlignment.spaceBetween, CrossAxisAlignment.end,
children: <Widget>[ mainAxisAlignment:
Column( MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: <Widget>[
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: <Widget>[ children: <Widget>[
Text( Column(
balanceDisplayMode.toString(), crossAxisAlignment:
style: TextStyle( CrossAxisAlignment.start,
fontSize: 12, children: <Widget>[
color: Theme.of(context).primaryTextTheme.caption.color Text(
), statusText,
), style: TextStyle(
SizedBox(height: 5), fontSize: 12,
Text( color: Theme.of(context)
balance, .primaryTextTheme
style: TextStyle( .caption
fontSize: 28, .color),
color: Theme.of(context).primaryTextTheme.title.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: <Widget>[
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
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<WalletCard> {
} }
Widget backSide(List<Color> colorsSync) { Widget backSide(List<Color> colorsSync) {
final walletStore = Provider.of<WalletStore>(context); final rightArrow = Image.asset(
final rightArrow = Image.asset('assets/images/right_arrow.png', 'assets/images/right_arrow.png',
color: Theme.of(context).primaryTextTheme.title.color, color: Theme.of(context).primaryTextTheme.title.color,
); );
double messageBoxHeight = 0; var messageBoxHeight = 0.0;
double messageBoxWidth = cardWidth - 10; var messageBoxWidth = cardWidth - 10;
return Observer( return Observer(
key: _addressObserverKey, key: _addressObserverKey,
builder: (_) { builder: (_) {
return Container( return Container(
width: cardWidth, width: cardWidth,
height: cardHeight, height: cardHeight,
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
child: Stack( child: Stack(
alignment: Alignment.topRight, alignment: Alignment.topRight,
children: <Widget>[ children: <Widget>[
Container( Container(
width: cardWidth, width: cardWidth,
height: cardHeight, height: cardHeight,
padding: EdgeInsets.only(left: 20, right: 20, top: 30, bottom: 30), padding:
decoration: BoxDecoration( EdgeInsets.only(left: 20, right: 20, top: 30, bottom: 30),
borderRadius: BorderRadius.only(topLeft: Radius.circular(10), bottomLeft: Radius.circular(10)), decoration: BoxDecoration(
gradient: LinearGradient( borderRadius: BorderRadius.only(
colors: colorsSync, topLeft: Radius.circular(10),
begin: Alignment.topCenter, bottomLeft: Radius.circular(10)),
end: Alignment.bottomCenter gradient: LinearGradient(
) colors: colorsSync,
), begin: Alignment.topCenter,
child: Column( end: Alignment.bottomCenter)),
mainAxisAlignment: MainAxisAlignment.spaceBetween, child: Column(
children: <Widget>[ mainAxisAlignment: MainAxisAlignment.spaceBetween,
Row( children: <Widget>[
children: <Widget>[ Row(
Expanded( children: <Widget>[
child: Container( Expanded(
child: Container(
height: 90, height: 90,
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Text( Text(
S.current.card_address, S.current.card_address,
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: Theme.of(context).primaryTextTheme.caption.color color: Theme.of(context)
), .primaryTextTheme
.caption
.color),
), ),
SizedBox(height: 10),
GestureDetector( GestureDetector(
onTap: () { onTap: () {
Clipboard.setData(ClipboardData( Clipboard.setData(ClipboardData(
text: walletStore.subaddress.address)); text: widget.walletVM.address));
_addressObserverKey.currentState.setState(() { _addressObserverKey.currentState
.setState(() {
messageBoxHeight = 20; messageBoxHeight = 20;
messageBoxWidth = cardWidth; messageBoxWidth = cardWidth;
}); });
Timer(Duration(milliseconds: 1000), () { Timer(Duration(milliseconds: 1000), () {
try { try {
_addressObserverKey.currentState.setState(() { _addressObserverKey.currentState
.setState(() {
messageBoxHeight = 0; messageBoxHeight = 0;
messageBoxWidth = cardWidth - 10; messageBoxWidth = cardWidth - 10;
}); });
} catch(e) { } catch (e) {
print('${e.toString()}'); print('${e.toString()}');
} }
}); });
}, },
child: Text( child: Text(
walletStore.subaddress.address, widget.walletVM.address,
style: TextStyle( style: TextStyle(
fontSize: 14, 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( Container(
onTap: () => Navigator.of(context, height: 44,
rootNavigator: true) padding: EdgeInsets.only(left: 20, right: 20),
.pushNamed(Routes.receive), alignment: Alignment.center,
child: Row( decoration: BoxDecoration(
mainAxisAlignment: MainAxisAlignment.spaceBetween, borderRadius: BorderRadius.all(Radius.circular(22)),
children: <Widget>[ color: Theme.of(context)
Text( .primaryTextTheme
S.of(context).accounts_subaddresses, .overline
style: TextStyle( .color),
fontSize: 14, child: InkWell(
color: Theme.of(context).primaryTextTheme.title.color onTap: () =>
Navigator.of(context, rootNavigator: true)
.pushNamed(Routes.receive),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
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),
),
)
],
),
);
});
} }
} }

View file

@ -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: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_mobx/flutter_mobx.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/stores/wallet_creation/wallet_creation_store.dart'; import 'package:cake_wallet/core/validator.dart';
import 'package:cake_wallet/src/stores/wallet_creation/wallet_creation_state.dart'; import 'package:cake_wallet/src/widgets/seed_language_selector.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/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/primary_button.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/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/screens/seed_language/widgets/seed_language_picker.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.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 { class NewWalletPage extends BasePage {
NewWalletPage( NewWalletPage(this._walletNewVM);
{@required this.walletsService,
@required this.walletService,
@required this.sharedPreferences});
final WalletListService walletsService; final WalletNewVM _walletNewVM;
final WalletService walletService;
final SharedPreferences sharedPreferences;
@override @override
String get title => S.current.new_wallet; String get title => S.current.new_wallet;
@override @override
Widget body(BuildContext context) => WalletNameForm(); Widget body(BuildContext context) => WalletNameForm(_walletNewVM);
} }
class WalletNameForm extends StatefulWidget { class WalletNameForm extends StatefulWidget {
WalletNameForm(this._walletNewVM);
final WalletNewVM _walletNewVM;
@override @override
_WalletNameFormState createState() => _WalletNameFormState(); _WalletNameFormState createState() => _WalletNameFormState(_walletNewVM);
} }
class _WalletNameFormState extends State<WalletNameForm> { class _WalletNameFormState extends State<WalletNameForm> {
_WalletNameFormState(this._walletNewVM);
static const aspectRatioImage = 1.22; static const aspectRatioImage = 1.22;
final _formKey = GlobalKey<FormState>();
final nameController = TextEditingController();
final walletNameImage = Image.asset('assets/images/wallet_name.png'); final walletNameImage = Image.asset('assets/images/wallet_name.png');
final _formKey = GlobalKey<FormState>();
final _languageSelectorKey = GlobalKey<SeedLanguageSelectorState>();
ReactionDisposer _stateReaction;
final WalletNewVM _walletNewVM;
@override @override
void dispose() { void initState() {
nameController.dispose(); _stateReaction ??=
super.dispose(); reaction((_) => _walletNewVM.state, (WalletCreationState state) {
}
@override
Widget build(BuildContext context) {
final walletCreationStore = Provider.of<WalletCreationStore>(context);
final walletCreationService = Provider.of<WalletCreationService>(context);
// FIXME: Does seed language store is really needed ???
final seedLanguageStore = Provider.of<SeedLanguageStore>(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) {
if (state is WalletCreatedSuccessfully) { if (state is WalletCreatedSuccessfully) {
Navigator.of(context).popUntil((route) => route.isFirst); Navigator.of(context).popUntil((route) => route.isFirst);
} }
@ -98,7 +67,11 @@ class _WalletNameFormState extends State<WalletNameForm> {
}); });
} }
}); });
super.initState();
}
@override
Widget build(BuildContext context) {
return Container( return Container(
padding: EdgeInsets.only(top: 24), padding: EdgeInsets.only(top: 24),
child: ScrollableWithBottomSection( child: ScrollableWithBottomSection(
@ -116,94 +89,76 @@ class _WalletNameFormState extends State<WalletNameForm> {
child: Form( child: Form(
key: _formKey, key: _formKey,
child: TextFormField( child: TextFormField(
textAlign: TextAlign.center, onChanged: (value) => _walletNewVM.name = value,
style: TextStyle( textAlign: TextAlign.center,
fontSize: 20.0, style: TextStyle(
fontWeight: FontWeight.w600, fontSize: 20.0,
color: Theme.of(context).primaryTextTheme.title.color), fontWeight: FontWeight.w600,
controller: nameController, color:
decoration: InputDecoration( Theme.of(context).primaryTextTheme.title.color),
hintStyle: TextStyle( decoration: InputDecoration(
fontSize: 16.0, hintStyle: TextStyle(
color: Theme.of(context) fontSize: 16.0,
.primaryTextTheme color: Theme.of(context)
.caption .primaryTextTheme
.color), .caption
hintText: S.of(context).wallet_name, .color),
focusedBorder: UnderlineInputBorder( hintText: S.of(context).wallet_name,
borderSide: BorderSide( focusedBorder: UnderlineInputBorder(
color: Theme.of(context).dividerColor, borderSide: BorderSide(
width: 1.0)), color: Theme.of(context).dividerColor,
enabledBorder: UnderlineInputBorder( width: 1.0)),
borderSide: BorderSide( enabledBorder: UnderlineInputBorder(
color: Theme.of(context).dividerColor, borderSide: BorderSide(
width: 1.0))), color: Theme.of(context).dividerColor,
validator: (value) { width: 1.0))),
walletCreationStore.validateWalletName(value); validator: WalletNameValidator())),
return walletCreationStore.errorMessage;
},
)),
), ),
Padding( if (_walletNewVM.hasLanguageSelector) ...[
padding: EdgeInsets.only(top: 40), Padding(
child: Text( padding: EdgeInsets.only(top: 40),
S.of(context).seed_language_choose, child: Text(
textAlign: TextAlign.center, S.of(context).seed_language_choose,
style: TextStyle( textAlign: TextAlign.center,
fontSize: 16.0, style: TextStyle(
fontWeight: FontWeight.w600, fontSize: 16.0,
color: Theme.of(context).primaryTextTheme.title.color), fontWeight: FontWeight.w600,
color: Theme.of(context).primaryTextTheme.title.color),
),
), ),
), Padding(
Padding( padding: EdgeInsets.only(top: 24),
padding: EdgeInsets.only(top: 24), child: SeedLanguageSelector(
child: Observer( key: _languageSelectorKey,
builder: (_) => SelectButton( initialSelected: defaultSeedLanguage),
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: bottomSectionPadding:
EdgeInsets.only(left: 24, right: 24, bottom: 24), EdgeInsets.only(left: 24, right: 24, bottom: 24),
bottomSection: Observer( bottomSection: Observer(
builder: (context) { builder: (context) {
return LoadingPrimaryButton( return LoadingPrimaryButton(
onPressed: () => _confirmForm(walletCreationService, onPressed: _confirmForm,
seedLanguageStore.selectedSeedLanguage),
text: S.of(context).continue_text, text: S.of(context).continue_text,
color: Colors.green, color: Colors.green,
textColor: Colors.white, textColor: Colors.white,
isLoading: walletCreationStore.state is WalletIsCreating, isLoading: _walletNewVM.state is WalletCreatedSuccessfully,
isDisabled: walletCreationStore.isDisabledStatus, isDisabled: _walletNewVM.name.isEmpty,
); );
}, },
)), )),
); );
} }
void _confirmForm( void _confirmForm() {
WalletCreationService walletCreationService, String language) {
if (!_formKey.currentState.validate()) { if (!_formKey.currentState.validate()) {
return; return;
} }
WalletCredentials credentials; _walletNewVM.create(
options: _walletNewVM.hasLanguageSelector
if (walletCreationService.type == WalletType.monero) { ? _languageSelectorKey.currentState.selected
credentials = MoneroNewWalletCredentials( : null);
name: nameController.text, language: language);
}
walletCreationService.create(credentials);
} }
} }

View file

@ -1,3 +1,4 @@
import 'package:cake_wallet/src/domain/common/wallet_type.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:cake_wallet/generated/i18n.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'; import 'package:cake_wallet/routes.dart';
class NewWalletTypePage extends BasePage { class NewWalletTypePage extends BasePage {
NewWalletTypePage({this.onTypeSelected});
final void Function(BuildContext, WalletType) onTypeSelected;
@override @override
String get title => S.current.new_wallet; String get title => S.current.new_wallet;
@override @override
Widget body(BuildContext context) => WalletTypeForm(); Widget body(BuildContext context) =>
WalletTypeForm(onTypeSelected: onTypeSelected);
} }
class WalletTypeForm extends StatefulWidget { class WalletTypeForm extends StatefulWidget {
WalletTypeForm({this.onTypeSelected});
final void Function(BuildContext, WalletType) onTypeSelected;
@override @override
WalletTypeFormState createState() => WalletTypeFormState(); WalletTypeFormState createState() => WalletTypeFormState();
} }
@ -23,35 +33,19 @@ class WalletTypeForm extends StatefulWidget {
class WalletTypeFormState extends State<WalletTypeForm> { class WalletTypeFormState extends State<WalletTypeForm> {
static const aspectRatioImage = 1.22; static const aspectRatioImage = 1.22;
final moneroIcon = Image.asset('assets/images/monero.png', height: 24, width: 24); final moneroIcon =
final bitcoinIcon = Image.asset('assets/images/bitcoin.png', height: 24, width: 24); 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'); final walletTypeImage = Image.asset('assets/images/wallet_type.png');
bool isDisabledButton; WalletType selected;
bool isMoneroSelected; List<WalletType> types;
bool isBitcoinSelected;
Color moneroBackgroundColor = Colors.transparent;
Color moneroTextColor = Colors.transparent;
Color bitcoinBackgroundColor = Colors.transparent;
Color bitcoinTextColor = Colors.transparent;
@override @override
void initState() { void initState() {
isDisabledButton = true; types = [WalletType.bitcoin, WalletType.monero];
isMoneroSelected = false;
isBitcoinSelected = false;
super.initState(); 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 @override
@ -75,71 +69,52 @@ class WalletTypeFormState extends State<WalletTypeForm> {
S.of(context).choose_wallet_currency, S.of(context).choose_wallet_currency,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Theme.of(context).primaryTextTheme.title.color color: Theme.of(context).primaryTextTheme.title.color),
),
), ),
), ),
Padding( ...types.map((type) => Padding(
padding: EdgeInsets.only(top: 24), padding: EdgeInsets.only(top: 24),
child: SelectButton( child: SelectButton(
image: bitcoinIcon, image: _iconFor(type),
text: 'Bitcoin', text: walletTypeToString(type),
color: bitcoinBackgroundColor, color: _backgroundColorFor(selected == type),
textColor: bitcoinTextColor, textColor: _textColorFor(selected == type),
onTap: () {}), onTap: () => setState(() => selected = type)),
), ))
Padding(
padding: EdgeInsets.only(top: 20),
child: SelectButton(
image: moneroIcon,
text: 'Monero',
color: moneroBackgroundColor,
textColor: moneroTextColor,
onTap: () => onSelectMoneroButton(context)),
)
], ],
), ),
bottomSectionPadding: EdgeInsets.only( bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
left: 24,
right: 24,
bottom: 24
),
bottomSection: PrimaryButton( bottomSection: PrimaryButton(
onPressed: () => Navigator.of(context).pushNamed(Routes.newWallet), onPressed: () => widget.onTypeSelected(context, selected),
text: S.of(context).seed_language_next, text: S.of(context).seed_language_next,
color: Colors.green, color: Colors.green,
textColor: Colors.white, textColor: Colors.white,
isDisabled: isDisabledButton, isDisabled: selected == null,
), ),
), ),
); );
} }
void onSelectMoneroButton(BuildContext context) { // FIXME: Move color selection inside ui element; add isSelected to buttons.
isMoneroSelected = true;
isBitcoinSelected = false;
isDisabledButton = false;
moneroBackgroundColor = Theme.of(context).accentTextTheme.title.decorationColor; Color _backgroundColorFor(bool isSelected) => isSelected
moneroTextColor = Theme.of(context).primaryTextTheme.title.backgroundColor; ? Theme.of(context).accentTextTheme.title.decorationColor
bitcoinBackgroundColor = Theme.of(context).accentTextTheme.title.backgroundColor; : Theme.of(context).accentTextTheme.title.backgroundColor;
bitcoinTextColor = Theme.of(context).primaryTextTheme.title.color;
setState(() {}); Color _textColorFor(bool isSelected) => isSelected
} ? Theme.of(context).primaryTextTheme.title.backgroundColor
: Theme.of(context).primaryTextTheme.title.color;
void onSelectBitcoinButton(BuildContext context) { Image _iconFor(WalletType type) {
isMoneroSelected = false; switch (type) {
isBitcoinSelected = true; case WalletType.monero:
isDisabledButton = false; return moneroIcon;
case WalletType.bitcoin:
moneroBackgroundColor = Theme.of(context).accentTextTheme.title.backgroundColor; return bitcoinIcon;
moneroTextColor = Theme.of(context).primaryTextTheme.title.color; default:
bitcoinBackgroundColor = moneroBackgroundColor = Theme.of(context).accentTextTheme.title.decorationColor; return null;
bitcoinTextColor = Theme.of(context).primaryTextTheme.title.backgroundColor; }
setState(() {});
} }
} }

View file

@ -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/material.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -16,346 +18,255 @@ 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/src/widgets/base_text_form_field.dart';
import 'package:cake_wallet/themes.dart'; import 'package:cake_wallet/themes.dart';
import 'package:cake_wallet/theme_changer.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 { class ReceivePage extends BasePage {
@override ReceivePage({this.addressListViewModel})
ReceivePageState createState() => ReceivePageState(); : amountController = TextEditingController(),
} _formKey = GlobalKey<FormState>() {
amountController.addListener(() => addressListViewModel.amount =
_formKey.currentState.validate() ? amountController.text : '');
}
class ReceivePageState extends State<ReceivePage> { final AddressListViewModel addressListViewModel;
final amountController = TextEditingController(); final TextEditingController amountController;
final _formKey = GlobalKey<FormState>(); final GlobalKey<FormState> _formKey;
final _backArrowImage = Image.asset('assets/images/back_arrow.png');
final _backArrowImageDarkTheme =
Image.asset('assets/images/back_arrow_dark_theme.png');
@override @override
void dispose() { Color get backgroundLightColor => Colors.transparent;
amountController.dispose();
super.dispose(); @override
Color get backgroundDarkColor => Colors.transparent;
@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);
@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),
);
@override
Widget trailing(BuildContext context) {
final shareImage = Image.asset('assets/images/share.png',
color: Theme.of(context).primaryTextTheme.title.color);
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),
),
);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final walletStore = Provider.of<WalletStore>(context); return super.build(context);
final subaddressListStore = Provider.of<SubaddressListStore>(context); }
final accountListStore = Provider.of<AccountListStore>(context);
final shareImage = Image.asset('assets/images/share.png', @override
color: Theme.of(context).primaryTextTheme.title.color, Widget body(BuildContext context) {
);
final copyImage = Image.asset('assets/images/copy_content.png', final copyImage = Image.asset('assets/images/copy_content.png',
color: Theme.of(context).primaryTextTheme.title.color, color: Theme.of(context).primaryTextTheme.title.color);
);
final currentColor = Theme.of(context).accentTextTheme.subtitle.decorationColor; return SingleChildScrollView(
final notCurrentColor = Theme.of(context).backgroundColor; child: Column(
children: <Widget>[
final currentTextColor = Colors.blue; SizedBox(height: 25),
final notCurrentTextColor = Theme.of(context).primaryTextTheme.caption.color; Row(children: <Widget>[
Spacer(flex: 4),
final _themeChanger = Provider.of<ThemeChanger>(context); Observer(
Image _backButton; builder: (_) => Flexible(
flex: 6,
if (_themeChanger.getTheme() == Themes.darkTheme) { child: Center(
_backButton = _backArrowImageDarkTheme; child: AspectRatio(
} else { aspectRatio: 1.0,
_backButton = _backArrowImage; child: QrImage(
} data: addressListViewModel.uri.toString(),
backgroundColor: Colors.transparent,
amountController.addListener(() { foregroundColor: Theme.of(context)
if (_formKey.currentState.validate()) { .primaryTextTheme
walletStore.onChangedAmountValue(amountController.text); .display4
} else { .color,
walletStore.onChangedAmountValue(''); ))))),
} Spacer(flex: 4)
}); ]),
Padding(
return Scaffold( padding: EdgeInsets.fromLTRB(24, 40, 24, 0),
resizeToAvoidBottomPadding: false, child: Row(
body: Container( children: <Widget>[
height: MediaQuery.of(context).size.height, Expanded(
width: MediaQuery.of(context).size.width, child: Form(
padding: EdgeInsets.only(top: 24), key: _formKey,
decoration: BoxDecoration( child: BaseTextFormField(
gradient: LinearGradient( controller: amountController,
colors: [ keyboardType:
Theme.of(context).scaffoldBackgroundColor, TextInputType.numberWithOptions(decimal: true),
Theme.of(context).primaryColor inputFormatters: [
], BlacklistingTextInputFormatter(
begin: Alignment.centerLeft, RegExp('[\\-|\\ |\\,]'))
end: Alignment.centerRight ],
) 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( Padding(
children: <Widget>[ padding: EdgeInsets.only(left: 24, right: 24, bottom: 40, top: 40),
Container( child: Builder(
padding: EdgeInsets.only( builder: (context) => Observer(
top: 10, builder: (context) => GestureDetector(
bottom: 20, onTap: () {
left: 5, Clipboard.setData(ClipboardData(
right: 10 text: addressListViewModel.address.address));
), Scaffold.of(context).showSnackBar(SnackBar(
child: Row( content: Text(
mainAxisAlignment: MainAxisAlignment.spaceBetween, S.of(context).copied_to_clipboard,
crossAxisAlignment: CrossAxisAlignment.center, style: TextStyle(color: Colors.white),
children: <Widget>[ ),
SizedBox( backgroundColor: Colors.green,
height: 44, duration: Duration(milliseconds: 500),
width: 44, ));
child: ButtonTheme( },
minWidth: double.minPositive, child: Container(
child: FlatButton( height: 48,
highlightColor: Colors.transparent, padding: EdgeInsets.only(left: 24, right: 24),
splashColor: Colors.transparent, decoration: BoxDecoration(
padding: EdgeInsets.all(0), borderRadius:
onPressed: () => Navigator.of(context).pop(), BorderRadius.all(Radius.circular(24)),
child: _backButton), color: Theme.of(context)
), .primaryTextTheme
), .overline
Text( .color),
S.of(context).receive, child: Row(
style: TextStyle( mainAxisSize: MainAxisSize.max,
fontSize: 18.0, children: <Widget>[
fontWeight: FontWeight.bold, Expanded(
color: Theme.of(context).primaryTextTheme.title.color), child: Text(
), addressListViewModel.address.address,
SizedBox( maxLines: 1,
height: 44.0, overflow: TextOverflow.ellipsis,
width: 44.0, style: TextStyle(
child: ButtonTheme( fontSize: 18,
minWidth: double.minPositive, fontWeight: FontWeight.w600,
child: FlatButton( color: Theme.of(context)
highlightColor: Colors.transparent, .primaryTextTheme
splashColor: Colors.transparent, .title
padding: EdgeInsets.all(0), .color),
onPressed: () => Share.text(
S.current.share_address, walletStore.subaddress.address, 'text/plain'),
child: shareImage),
),
)
],
),
),
Expanded(
child: SingleChildScrollView(
child: Column(
children: <Widget>[
Observer(builder: (_) {
return Row(
children: <Widget>[
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: <Widget>[
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: <Widget>[
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: 12),
), child: copyImage,
), )
Observer( ],
builder: (_) => ListView.separated( ),
separatorBuilder: (context, index) => Divider( ),
height: 1, ))),
color: Theme.of(context).dividerColor, ),
), Observer(
shrinkWrap: true, builder: (_) => ListView.separated(
physics: NeverScrollableScrollPhysics(), separatorBuilder: (context, _) =>
itemCount: subaddressListStore.subaddresses.length + 2, Divider(height: 1, color: Theme.of(context).dividerColor),
itemBuilder: (context, index) { shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: addressListViewModel.items.length,
itemBuilder: (context, index) {
final item = addressListViewModel.items[index];
Widget cell = Container();
if (index == 0) { if (item is AccountListHeader) {
return ClipRRect( cell = HeaderTile(
borderRadius: BorderRadius.only( onTap: () async {
topLeft: Radius.circular(24), await showDialog<void>(
topRight: Radius.circular(24) context: context,
), builder: (BuildContext context) {
child: HeaderTile( // return AccountListPage(
onTap: () async { // accountListStore:
await showDialog<void>( // accountListStore);
context: context, });
builder: (BuildContext context) { },
return AccountListPage(accountListStore: accountListStore); title: addressListViewModel.accountLabel,
} icon: Icon(
); Icons.arrow_forward_ios,
}, size: 14,
title: walletStore.account.label, color:
icon: Icon( Theme.of(context).primaryTextTheme.title.color,
Icons.arrow_forward_ios, ));
size: 14, }
color: Theme.of(context).primaryTextTheme.title.color,
)
),
);
}
if (index == 1) { if (item is AddressListHeader) {
return HeaderTile( cell = HeaderTile(
onTap: () => Navigator.of(context) onTap: () => Navigator.of(context)
.pushNamed(Routes.newSubaddress), .pushNamed(Routes.newSubaddress),
title: S.of(context).subaddresses, title: S.of(context).subaddresses,
icon: Icon( icon: Icon(
Icons.add, Icons.add,
size: 20, size: 20,
color: Theme.of(context).primaryTextTheme.title.color, color:
) Theme.of(context).primaryTextTheme.title.color,
); ));
} }
index -= 2; 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 Observer( return index != 0
builder: (_) { ? cell
final subaddress = subaddressListStore.subaddresses[index]; : ClipRRect(
final isCurrent = borderRadius: BorderRadius.only(
walletStore.subaddress.address == subaddress.address; topLeft: Radius.circular(24),
topRight: Radius.circular(24)),
final label = subaddress.label.isNotEmpty child: cell,
? subaddress.label );
: subaddress.address; })),
],
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,
),
),
),
);
return isCurrent
? content
: Slidable(
key: Key(subaddress.address),
actionPane: SlidableDrawerActionPane(),
child: content,
secondaryActions: <Widget>[
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),
)
]
);
}
);
}
)
),
],
),
)
)
],
)
), ),
); );
} }

View file

@ -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: <Widget>[
IconSlideAction(
caption: S.of(context).edit,
color: Theme.of(context).primaryTextTheme.overline.color,
icon: Icons.edit,
onTap: () => onEdit?.call())
]);
}
}

View file

@ -1,26 +1,35 @@
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:provider/provider.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/stores/wallet_restoration/wallet_restoration_store.dart'; import 'package:cake_wallet/core/validator.dart';
import 'package:cake_wallet/src/stores/wallet_restoration/wallet_restoration_state.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/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/blockchain_height_widget.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/scollable_with_bottom_section.dart';
import 'package:cake_wallet/src/widgets/primary_button.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/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/view_model/wallet_restoration_from_seed_vm.dart';
class RestoreWalletFromSeedDetailsPage extends BasePage { class RestoreWalletFromSeedDetailsPage extends BasePage {
RestoreWalletFromSeedDetailsPage(
{@required this.walletRestorationFromSeedVM});
final WalletRestorationFromSeedVM walletRestorationFromSeedVM;
@override @override
String get title => S.current.restore_wallet_restore_description; String get title => S.current.restore_wallet_restore_description;
@override @override
Widget body(BuildContext context) => RestoreFromSeedDetailsForm(); Widget body(BuildContext context) => RestoreFromSeedDetailsForm(
walletRestorationFromSeedVM: walletRestorationFromSeedVM);
} }
class RestoreFromSeedDetailsForm extends StatefulWidget { class RestoreFromSeedDetailsForm extends StatefulWidget {
RestoreFromSeedDetailsForm({@required this.walletRestorationFromSeedVM});
final WalletRestorationFromSeedVM walletRestorationFromSeedVM;
@override @override
_RestoreFromSeedDetailsFormState createState() => _RestoreFromSeedDetailsFormState createState() =>
_RestoreFromSeedDetailsFormState(); _RestoreFromSeedDetailsFormState();
@ -31,31 +40,17 @@ class _RestoreFromSeedDetailsFormState
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
final _blockchainHeightKey = GlobalKey<BlockchainHeightState>(); final _blockchainHeightKey = GlobalKey<BlockchainHeightState>();
final _nameController = TextEditingController(); final _nameController = TextEditingController();
ReactionDisposer _stateReaction;
@override @override
void dispose() { void initState() {
_nameController.dispose(); _stateReaction = reaction((_) => widget.walletRestorationFromSeedVM.state,
super.dispose(); (WalletCreationState state) {
} if (state is WalletCreatedSuccessfully) {
@override
Widget build(BuildContext context) {
final walletRestorationStore = Provider.of<WalletRestorationStore>(context);
_nameController.addListener(() {
if (_nameController.text.isNotEmpty) {
walletRestorationStore.setDisabledState(false);
} else {
walletRestorationStore.setDisabledState(true);
}
});
reaction((_) => walletRestorationStore.state, (WalletRestorationState state) {
if (state is WalletRestoredSuccessfully) {
Navigator.of(context).popUntil((route) => route.isFirst); Navigator.of(context).popUntil((route) => route.isFirst);
} }
if (state is WalletRestorationFailure) { if (state is WalletCreationFailure) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
showDialog<void>( showDialog<void>(
context: context, context: context,
@ -64,73 +59,87 @@ class _RestoreFromSeedDetailsFormState
alertTitle: S.current.restore_title_from_seed, alertTitle: S.current.restore_title_from_seed,
alertContent: state.error, alertContent: state.error,
buttonText: S.of(context).ok, 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( return Container(
padding: EdgeInsets.only(left: 24, right: 24), padding: EdgeInsets.only(left: 24, right: 24),
child: ScrollableWithBottomSection( child: ScrollableWithBottomSection(
contentPadding: EdgeInsets.only(bottom: 24.0), contentPadding: EdgeInsets.only(bottom: 24.0),
content: Form( content: Form(
key: _formKey, key: _formKey,
child: Column( child: Column(children: <Widget>[
Row(
children: <Widget>[ children: <Widget>[
Row( Flexible(
children: <Widget>[ child: Container(
Flexible( padding: EdgeInsets.only(top: 20.0),
child: Container( child: TextFormField(
padding: EdgeInsets.only(top: 20.0), style: TextStyle(
child: TextFormField( fontSize: 16.0,
style: TextStyle( color: Theme.of(context).primaryTextTheme.title.color),
fontSize: 16.0, controller: _nameController,
color: Theme.of(context).primaryTextTheme.title.color decoration: InputDecoration(
), hintStyle: TextStyle(
controller: _nameController, color: Theme.of(context)
decoration: InputDecoration( .primaryTextTheme
hintStyle: TextStyle( .caption
color: Theme.of(context).primaryTextTheme.caption.color, .color,
fontSize: 16 fontSize: 16),
), hintText: S.of(context).restore_wallet_name,
hintText: S.of(context).restore_wallet_name, focusedBorder: UnderlineInputBorder(
focusedBorder: UnderlineInputBorder( borderSide: BorderSide(
borderSide: BorderSide( color: Theme.of(context).dividerColor,
color: Theme.of(context).dividerColor, width: 1.0)),
width: 1.0)), enabledBorder: UnderlineInputBorder(
enabledBorder: UnderlineInputBorder( borderSide: BorderSide(
borderSide: BorderSide( color: Theme.of(context).dividerColor,
color: Theme.of(context).dividerColor, width: 1.0))),
width: 1.0))), validator: WalletNameValidator(),
validator: (value) { ),
walletRestorationStore ))
.validateWalletName(value); ],
return walletRestorationStore.errorMessage; ),
}, if (widget.walletRestorationFromSeedVM.hasRestorationHeight)
), BlockchainHeightWidget(
)) key: _blockchainHeightKey,
], onHeightChange: (height) {
), widget.walletRestorationFromSeedVM.height = height;
BlockchainHeightWidget(key: _blockchainHeightKey), print(height);
]), }),
]),
), ),
bottomSectionPadding: EdgeInsets.only(bottom: 24), bottomSectionPadding: EdgeInsets.only(bottom: 24),
bottomSection: Observer(builder: (_) { bottomSection: Observer(builder: (_) {
return LoadingPrimaryButton( return LoadingPrimaryButton(
onPressed: () { onPressed: () {
if (_formKey.currentState.validate()) { if (_formKey.currentState.validate()) {
walletRestorationStore.restoreFromSeed( widget.walletRestorationFromSeedVM.create();
name: _nameController.text,
restoreHeight: _blockchainHeightKey.currentState.height);
} }
}, },
isLoading: walletRestorationStore.state is WalletIsRestoring, isLoading:
widget.walletRestorationFromSeedVM.state is WalletCreating,
text: S.of(context).restore_recover, text: S.of(context).restore_recover,
color: Colors.green, color: Colors.green,
textColor: Colors.white, textColor: Colors.white,
isDisabled: walletRestorationStore.disabledState, isDisabled: _nameController.text.isNotEmpty,
); );
}), }),
), ),

View file

@ -1,27 +1,19 @@
import 'package:provider/provider.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/generated/i18n.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/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/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/palette.dart';
import 'package:cake_wallet/core/mnemonic_length.dart';
class RestoreWalletFromSeedPage extends BasePage { class RestoreWalletFromSeedPage extends BasePage {
RestoreWalletFromSeedPage( RestoreWalletFromSeedPage({@required this.type, @required this.language});
{@required this.walletsService,
@required this.walletService,
@required this.sharedPreferences});
final WalletListService walletsService; final WalletType type;
final WalletService walletService; final String language;
final SharedPreferences sharedPreferences;
final formKey = GlobalKey<_RestoreFromSeedFormState>(); final formKey = GlobalKey<_RestoreFromSeedFormState>();
@override @override
@ -34,11 +26,14 @@ class RestoreWalletFromSeedPage extends BasePage {
Color get backgroundDarkColor => PaletteDark.lightNightBlue; Color get backgroundDarkColor => PaletteDark.lightNightBlue;
@override @override
Widget body(BuildContext context) => RestoreFromSeedForm(key: formKey); Widget body(BuildContext context) =>
RestoreFromSeedForm(key: formKey, type: type, language: language);
} }
class RestoreFromSeedForm extends StatefulWidget { 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 @override
_RestoreFromSeedFormState createState() => _RestoreFromSeedFormState(); _RestoreFromSeedFormState createState() => _RestoreFromSeedFormState();
@ -46,13 +41,11 @@ class RestoreFromSeedForm extends StatefulWidget {
class _RestoreFromSeedFormState extends State<RestoreFromSeedForm> { class _RestoreFromSeedFormState extends State<RestoreFromSeedForm> {
final _seedKey = GlobalKey<SeedWidgetState>(); final _seedKey = GlobalKey<SeedWidgetState>();
void clear() => _seedKey.currentState.clear();
String mnemonic() => _seedKey.currentState.items.map((e) => e.text).join(' ');
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final walletRestorationStore = Provider.of<WalletRestorationStore>(context);
final seedLanguageStore = Provider.of<SeedLanguageStore>(context);
return GestureDetector( return GestureDetector(
onTap: () => onTap: () =>
SystemChannels.textInput.invokeMethod<void>('TextInput.hide'), SystemChannels.textInput.invokeMethod<void>('TextInput.hide'),
@ -60,11 +53,13 @@ class _RestoreFromSeedFormState extends State<RestoreFromSeedForm> {
color: Theme.of(context).backgroundColor, color: Theme.of(context).backgroundColor,
child: SeedWidget( child: SeedWidget(
key: _seedKey, key: _seedKey,
onMnemoticChange: (seed) => walletRestorationStore.setSeed(seed), maxLength: mnemonicLength(widget.type),
onMnemonicChange: (seed) => null,
onFinish: () => Navigator.of(context).pushNamed( onFinish: () => Navigator.of(context).pushNamed(
Routes.restoreWalletFromSeedDetails, Routes.restoreWalletFromSeedDetails,
arguments: _seedKey.currentState.items), arguments: [widget.type, widget.language, mnemonic()]),
seedLanguage: seedLanguageStore.selectedSeedLanguage, validator:
SeedValidator(type: widget.type, language: widget.language),
), ),
), ),
); );

View file

@ -1,19 +1,21 @@
import 'package:flutter/material.dart'; 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/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/restore/widgets/restore_button.dart';
import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/domain/common/wallet_type.dart'; import 'package:cake_wallet/src/domain/common/wallet_type.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/stores/seed_language/seed_language_store.dart'; import 'package:cake_wallet/src/stores/seed_language/seed_language_store.dart';
import 'package:provider/provider.dart';
class RestoreWalletOptionsPage extends BasePage { 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 WalletType type;
final Function(BuildContext context) onRestoreFromSeed;
final Function(BuildContext context) onRestoreFromKeys;
@override @override
String get title => S.current.restore_seed_keys_restore; String get title => S.current.restore_seed_keys_restore;
@ -23,8 +25,6 @@ class RestoreWalletOptionsPage extends BasePage {
@override @override
Widget body(BuildContext context) { Widget body(BuildContext context) {
final seedLanguageStore = Provider.of<SeedLanguageStore>(context);
return Container( return Container(
width: double.infinity, width: double.infinity,
height: double.infinity, height: double.infinity,
@ -33,28 +33,56 @@ class RestoreWalletOptionsPage extends BasePage {
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
RestoreButton( RestoreButton(
onPressed: () { onPressed: () => onRestoreFromSeed(context),
seedLanguageStore
.setCurrentRoute(Routes.restoreWalletFromSeed);
Navigator.pushNamed(context, Routes.seedLanguage);
},
image: imageSeed, image: imageSeed,
title: S.of(context).restore_title_from_seed, title: S.of(context).restore_title_from_seed,
description: S.of(context).restore_description_from_seed), description: _fromSeedDescription(context)),
Padding( Padding(
padding: EdgeInsets.only(top: 24), padding: EdgeInsets.only(top: 24),
child: RestoreButton( child: RestoreButton(
onPressed: () { onPressed: () => onRestoreFromKeys(context),
seedLanguageStore
.setCurrentRoute(Routes.restoreWalletFromKeys);
Navigator.pushNamed(context, Routes.seedLanguage);
},
image: imageKeys, image: imageKeys,
title: S.of(context).restore_title_from_keys, title: _fromKeyTitle(context),
description: S.of(context).restore_description_from_keys), 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 '';
}
}
} }

View file

@ -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/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:cake_wallet/routes.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/price/price_store.dart';
import 'package:cake_wallet/src/stores/settings/settings_store.dart'; import 'package:cake_wallet/src/stores/settings/settings_store.dart';
import 'package:cake_wallet/src/stores/wallet/wallet_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'; import 'package:cake_wallet/src/screens/welcome/create_welcome_page.dart';
class Root extends StatefulWidget { 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 @override
RootState createState() => RootState(); RootState createState() => RootState();
@ -30,7 +38,6 @@ class Root extends StatefulWidget {
class RootState extends State<Root> with WidgetsBindingObserver { class RootState extends State<Root> with WidgetsBindingObserver {
bool _isInactive; bool _isInactive;
bool _postFrameCallback; bool _postFrameCallback;
AuthenticationStore _authenticationStore;
@override @override
void initState() { void initState() {
@ -48,12 +55,12 @@ class RootState extends State<Root> with WidgetsBindingObserver {
return; return;
} }
if (!_isInactive && // if (!_isInactive &&
_authenticationStore.state == // widget.authenticationStore.state ==
AuthenticationState.authenticated || // AuthenticationState.authenticated ||
_authenticationStore.state == AuthenticationState.active) { // widget.authenticationStore.state == AuthenticationState.active) {
setState(() => _isInactive = true); // setState(() => _isInactive = true);
} // }
break; break;
default: default:
@ -63,18 +70,18 @@ class RootState extends State<Root> with WidgetsBindingObserver {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
_authenticationStore = Provider.of<AuthenticationStore>(context); // _authenticationStore = Provider.of<AuthenticationStore>(context);
final sharedPreferences = Provider.of<SharedPreferences>(context); // final sharedPreferences = Provider.of<SharedPreferences>(context);
final walletListService = Provider.of<WalletListService>(context); // final walletListService = Provider.of<WalletListService>(context);
final walletService = Provider.of<WalletService>(context); // final walletService = Provider.of<WalletService>(context);
final userService = Provider.of<UserService>(context); // final userService = Provider.of<UserService>(context);
final priceStore = Provider.of<PriceStore>(context); // final priceStore = Provider.of<PriceStore>(context);
final authenticationStore = Provider.of<AuthenticationStore>(context); // final authenticationStore = Provider.of<AuthenticationStore>(context);
final trades = Provider.of<Box<Trade>>(context); // final trades = Provider.of<Box<Trade>>(context);
final transactionDescriptions = // final transactionDescriptions =
Provider.of<Box<TransactionDescription>>(context); // Provider.of<Box<TransactionDescription>>(context);
final walletStore = Provider.of<WalletStore>(context); // final walletStore = Provider.of<WalletStore>(context);
final settingsStore = Provider.of<SettingsStore>(context); // final settingsStore = Provider.of<SettingsStore>(context);
if (_isInactive && !_postFrameCallback) { if (_isInactive && !_postFrameCallback) {
_postFrameCallback = true; _postFrameCallback = true;
@ -96,38 +103,51 @@ class RootState extends State<Root> with WidgetsBindingObserver {
} }
return Observer(builder: (_) { return Observer(builder: (_) {
final state = _authenticationStore.state; final state = widget.authenticationStore.state;
print(state);
if (state == AuthenticationState.denied) { if (state == AuthenticationState.denied) {
return createWelcomePage(); return createWelcomePage();
} }
if (state == AuthenticationState.readyToLogin) { if (state == AuthenticationState.installed) {
return createLoginPage( return getIt.get<AuthPage>();
sharedPreferences: sharedPreferences,
userService: userService,
walletService: walletService,
walletListService: walletListService,
authenticationStore: authenticationStore);
} }
if (state == AuthenticationState.authenticated || if (state == AuthenticationState.allowed) {
state == AuthenticationState.restored) { return getIt.get<DashboardPage>();
return createDashboardPage(
walletService: walletService,
priceStore: priceStore,
trades: trades,
transactionDescriptions: transactionDescriptions,
walletStore: walletStore,
settingsStore: settingsStore);
} }
if (state == AuthenticationState.created) { // if (state == AuthenticationState.denied) {
return createSeedPage( // return createWelcomePage();
settingsStore: settingsStore, // }
walletService: walletService,
callback: () => // if (state == AuthenticationState.readyToLogin) {
_authenticationStore.state = AuthenticationState.authenticated); // 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); return Container(color: Colors.white);
}); });

View file

@ -1,3 +1,4 @@
import 'package:cake_wallet/src/widgets/seed_language_selector.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:flutter/material.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'; import 'package:cake_wallet/src/screens/seed_language/widgets/seed_language_picker.dart';
class SeedLanguage extends BasePage { class SeedLanguage extends BasePage {
SeedLanguage({this.onConfirm});
final Function(BuildContext, String) onConfirm;
@override @override
Widget body(BuildContext context) => SeedLanguageForm(); Widget body(BuildContext context) => SeedLanguageForm(onConfirm: onConfirm);
} }
class SeedLanguageForm extends StatefulWidget { class SeedLanguageForm extends StatefulWidget {
SeedLanguageForm({this.onConfirm});
final Function(BuildContext, String) onConfirm;
@override @override
SeedLanguageFormState createState() => SeedLanguageFormState(); SeedLanguageFormState createState() => SeedLanguageFormState();
} }
class SeedLanguageFormState extends State<SeedLanguageForm> { class SeedLanguageFormState extends State<SeedLanguageForm> {
static const aspectRatioImage = 1.22; static const aspectRatioImage = 1.22;
final walletNameImage = Image.asset('assets/images/wallet_name.png'); final walletNameImage = Image.asset('assets/images/wallet_name.png');
final _languageSelectorKey = GlobalKey<SeedLanguageSelectorState>();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final seedLanguageStore = Provider.of<SeedLanguageStore>(context);
final List<String> 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( return Container(
padding: EdgeInsets.only(top: 24), padding: EdgeInsets.only(top: 24),
child: ScrollableWithBottomSection( child: ScrollableWithBottomSection(
contentPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), contentPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
content: Column( content:
crossAxisAlignment: CrossAxisAlignment.center, Column(crossAxisAlignment: CrossAxisAlignment.center, children: [
children: [ Padding(
Padding( padding: EdgeInsets.only(left: 12, right: 12),
padding: EdgeInsets.only(left: 12, right: 12), child: AspectRatio(
child: AspectRatio( aspectRatio: aspectRatioImage,
aspectRatio: aspectRatioImage, child: FittedBox(child: walletNameImage, fit: BoxFit.fill)),
child: FittedBox(child: walletNameImage, fit: BoxFit.fill)), ),
), Padding(
Padding(padding: EdgeInsets.only(top: 40), padding: EdgeInsets.only(top: 40),
child: Text( child: Text(
S.of(context).seed_language_choose, S.of(context).seed_language_choose,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 16.0, fontSize: 16.0,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Theme.of(context).primaryTextTheme.title.color color: Theme.of(context).primaryTextTheme.title.color),
), ),
), ),
), Padding(
Padding(padding: EdgeInsets.only(top: 24), padding: EdgeInsets.only(top: 24),
child: Observer( child: SeedLanguageSelector(
builder: (_) => SelectButton( key: _languageSelectorKey,
image: null, initialSelected: defaultSeedLanguage),
text: seedLocales[seedLanguages.indexOf(seedLanguageStore.selectedSeedLanguage)], )
color: Theme.of(context).accentTextTheme.title.backgroundColor, ]),
textColor: Theme.of(context).primaryTextTheme.title.color, bottomSectionPadding:
onTap: () async => await showDialog( EdgeInsets.only(left: 24, right: 24, bottom: 24),
context: context,
builder: (BuildContext context) => SeedLanguagePicker()
)
)
),
)
]),
bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
bottomSection: Observer( bottomSection: Observer(
builder: (context) { builder: (context) {
return PrimaryButton( return PrimaryButton(
onPressed: () => onPressed: () => widget
Navigator.of(context).popAndPushNamed(seedLanguageStore.currentRoute), .onConfirm(context, _languageSelectorKey.currentState.selected),
text: S.of(context).seed_language_next, text: S.of(context).seed_language_next,
color: Colors.green, color: Colors.green,
textColor: Colors.white); textColor: Colors.white);

View file

@ -17,7 +17,7 @@ List<Image> flagImages = [
Image.asset('assets/images/spain.png'), Image.asset('assets/images/spain.png'),
]; ];
List<String> languageCodes = [ const List<String> languageCodes = [
'Eng', 'Eng',
'Chi', 'Chi',
'Ned', 'Ned',
@ -28,19 +28,39 @@ List<String> languageCodes = [
'Esp', 'Esp',
]; ];
enum Places {topLeft, topRight, bottomLeft, bottomRight, inside} const defaultSeedLanguage = 'English';
const List<String> seedLanguages = [
defaultSeedLanguage,
'Chinese (simplified)',
'Dutch',
'German',
'Japanese',
'Portuguese',
'Russian',
'Spanish'
];
enum Places { topLeft, topRight, bottomLeft, bottomRight, inside }
class SeedLanguagePicker extends StatefulWidget { class SeedLanguagePicker extends StatefulWidget {
SeedLanguagePicker({Key key, this.selected = defaultSeedLanguage})
: super(key: key);
final String selected;
@override @override
SeedLanguagePickerState createState() => SeedLanguagePickerState(); SeedLanguagePickerState createState() =>
SeedLanguagePickerState(selected: selected);
} }
class SeedLanguagePickerState extends State<SeedLanguagePicker> { class SeedLanguagePickerState extends State<SeedLanguagePicker> {
SeedLanguagePickerState({this.selected});
String selected;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final seedLanguageStore = Provider.of<SeedLanguageStore>(context);
return GestureDetector( return GestureDetector(
onTap: () => Navigator.of(context).pop(), onTap: () => Navigator.of(context).pop(),
child: Container( child: Container(
@ -48,7 +68,8 @@ class SeedLanguagePickerState extends State<SeedLanguagePicker> {
child: BackdropFilter( child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 3.0, sigmaY: 3.0), filter: ImageFilter.blur(sigmaX: 3.0, sigmaY: 3.0),
child: Container( child: Container(
decoration: BoxDecoration(color: PaletteDark.darkNightBlue.withOpacity(0.75)), decoration: BoxDecoration(
color: PaletteDark.darkNightBlue.withOpacity(0.75)),
child: Center( child: Center(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -62,8 +83,7 @@ class SeedLanguagePickerState extends State<SeedLanguagePicker> {
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
decoration: TextDecoration.none, decoration: TextDecoration.none,
color: Colors.white color: Colors.white),
),
), ),
), ),
Padding( Padding(
@ -74,9 +94,8 @@ class SeedLanguagePickerState extends State<SeedLanguagePicker> {
height: 300, height: 300,
width: 300, width: 300,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(14)), borderRadius: BorderRadius.all(Radius.circular(14)),
color: Theme.of(context).dividerColor color: Theme.of(context).dividerColor),
),
child: GridView.count( child: GridView.count(
shrinkWrap: true, shrinkWrap: true,
crossAxisCount: 3, crossAxisCount: 3,
@ -85,71 +104,64 @@ class SeedLanguagePickerState extends State<SeedLanguagePicker> {
crossAxisSpacing: 1, crossAxisSpacing: 1,
mainAxisSpacing: 1, mainAxisSpacing: 1,
children: List.generate(9, (index) { children: List.generate(9, (index) {
if (index == 8) { if (index == 8) {
return gridTile( return gridTile(
isCurrent: false, isCurrent: false,
place: Places.bottomRight, place: Places.bottomRight,
image: null, image: null,
text: '', text: '',
onTap: null); onTap: null);
} else { } else {
final code = languageCodes[index]; final code = languageCodes[index];
final flag = flagImages[index]; final flag = flagImages[index];
final isCurrent = index == seedLanguages.indexOf(seedLanguageStore.selectedSeedLanguage); final isCurrent =
index == seedLanguages.indexOf(selected);
if (index == 0) { if (index == 0) {
return gridTile( return gridTile(
isCurrent: isCurrent, isCurrent: isCurrent,
place: Places.topLeft, place: Places.topLeft,
image: flag, image: flag,
text: code, text: code,
onTap: () { onTap: () {
seedLanguageStore.setSelectedSeedLanguage(seedLanguages[index]); selected = seedLanguages[index];
Navigator.of(context).pop(); Navigator.of(context).pop(selected);
} });
);
} }
if (index == 2) { if (index == 2) {
return gridTile( return gridTile(
isCurrent: isCurrent, isCurrent: isCurrent,
place: Places.topRight, place: Places.topRight,
image: flag, image: flag,
text: code, text: code,
onTap: () { onTap: () {
seedLanguageStore.setSelectedSeedLanguage(seedLanguages[index]); selected = seedLanguages[index];
Navigator.of(context).pop(); Navigator.of(context).pop(selected);
} });
);
} }
if (index == 6) { if (index == 6) {
return gridTile( return gridTile(
isCurrent: isCurrent, isCurrent: isCurrent,
place: Places.bottomLeft, place: Places.bottomLeft,
image: flag, image: flag,
text: code, text: code,
onTap: () { onTap: () {
seedLanguageStore.setSelectedSeedLanguage(seedLanguages[index]); selected = seedLanguages[index];
Navigator.of(context).pop(); Navigator.of(context).pop(selected);
} });
);
} }
return gridTile( return gridTile(
isCurrent: isCurrent, isCurrent: isCurrent,
place: Places.inside, place: Places.inside,
image: flag, image: flag,
text: code, text: code,
onTap: () { onTap: () {
seedLanguageStore.setSelectedSeedLanguage(seedLanguages[index]); selected = seedLanguages[index];
Navigator.of(context).pop(); Navigator.of(context).pop(selected);
} });
);
} }
}), }),
), ),
@ -165,13 +177,12 @@ class SeedLanguagePickerState extends State<SeedLanguagePicker> {
); );
} }
Widget gridTile({ Widget gridTile(
@required bool isCurrent, {@required bool isCurrent,
@required Places place, @required Places place,
@required Image image, @required Image image,
@required String text, @required String text,
@required VoidCallback onTap}) { @required VoidCallback onTap}) {
BorderRadius borderRadius; BorderRadius borderRadius;
final color = isCurrent final color = isCurrent
? Theme.of(context).accentTextTheme.subtitle.decorationColor ? Theme.of(context).accentTextTheme.subtitle.decorationColor
@ -199,40 +210,33 @@ class SeedLanguagePickerState extends State<SeedLanguagePicker> {
} }
return GestureDetector( return GestureDetector(
onTap: onTap, onTap: onTap,
child: Container( child: Container(
padding: EdgeInsets.all(10), padding: EdgeInsets.all(10),
decoration: BoxDecoration( decoration: BoxDecoration(borderRadius: borderRadius, color: color),
borderRadius: borderRadius, child: Center(
color: color child: Row(
), mainAxisSize: MainAxisSize.min,
child: Center( mainAxisAlignment: MainAxisAlignment.center,
child: Row( crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min, children: <Widget>[
mainAxisAlignment: MainAxisAlignment.center, image != null ? image : Offstage(),
crossAxisAlignment: CrossAxisAlignment.center, Padding(
children: <Widget>[ padding: image != null
image != null ? EdgeInsets.only(left: 10)
? image : EdgeInsets.only(left: 0),
: Offstage(), child: Text(
Padding( text,
padding: image != null style: TextStyle(
? EdgeInsets.only(left: 10) fontSize: 18,
: EdgeInsets.only(left: 0), fontWeight: FontWeight.bold,
child: Text( decoration: TextDecoration.none,
text, color: textColor),
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
decoration: TextDecoration.none,
color: textColor
), ),
), )
) ],
], ),
), ),
), ));
)
);
} }
} }

View file

@ -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<FormState>(),
_labelController = TextEditingController(),
super() {
_labelController.addListener(
() => addressEditOrCreateViewModel.label = _labelController.text);
_labelController.text = addressEditOrCreateViewModel.label;
print(_labelController.text);
print(addressEditOrCreateViewModel.label);
}
final AddressEditOrCreateViewModel addressEditOrCreateViewModel;
final GlobalKey<FormState> _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: <Widget>[
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,
),
)
],
),
));
}
}

View file

@ -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<SubadrressCreationStore>(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<NewSubaddressForm> {
NewSubaddressFormState(this.subaddress);
final _formKey = GlobalKey<FormState>();
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<SubadrressCreationStore>(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: <Widget>[
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,
),
)
],
),
)
);
}
}

View file

@ -32,9 +32,7 @@ class SubaddressListPage extends BasePage {
child: Observer( child: Observer(
builder: (_) => ListView.separated( builder: (_) => ListView.separated(
separatorBuilder: (_, __) => Divider( separatorBuilder: (_, __) => Divider(
color: Theme.of(context).dividerTheme.color, color: Theme.of(context).dividerTheme.color, height: 1.0),
height: 1.0,
),
itemCount: subaddressListStore.subaddresses == null itemCount: subaddressListStore.subaddresses == null
? 0 ? 0
: subaddressListStore.subaddresses.length, : subaddressListStore.subaddresses.length,
@ -42,9 +40,7 @@ class SubaddressListPage extends BasePage {
final subaddress = subaddressListStore.subaddresses[index]; final subaddress = subaddressListStore.subaddresses[index];
final isCurrent = final isCurrent =
walletStore.subaddress.address == subaddress.address; walletStore.subaddress.address == subaddress.address;
final label = subaddress.label != null final label = subaddress.label ?? subaddress.address;
? subaddress.label
: subaddress.address;
return InkWell( return InkWell(
onTap: () => Navigator.of(context).pop(subaddress), onTap: () => Navigator.of(context).pop(subaddress),

View file

@ -1,99 +1,99 @@
import 'dart:async'; //import 'dart:async';
import 'package:flutter/foundation.dart'; //import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart'; //import 'package:shared_preferences/shared_preferences.dart';
import 'package:mobx/mobx.dart'; //import 'package:mobx/mobx.dart';
import 'package:cake_wallet/src/domain/services/user_service.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/domain/services/wallet_service.dart';
import 'package:cake_wallet/src/stores/auth/auth_state.dart'; //import 'package:cake_wallet/view_model/auth_state.dart';
import 'package:cake_wallet/generated/i18n.dart'; //import 'package:cake_wallet/generated/i18n.dart';
//
part 'auth_store.g.dart'; //part 'auth_store.g.dart';
//
class AuthStore = AuthStoreBase with _$AuthStore; //class AuthStore = AuthStoreBase with _$AuthStore;
//
abstract class AuthStoreBase with Store { //abstract class AuthStoreBase with Store {
AuthStoreBase( // AuthStoreBase(
{@required this.userService, // {@required this.userService,
@required this.walletService, // @required this.walletService,
@required this.sharedPreferences}) { // @required this.sharedPreferences}) {
state = AuthenticationStateInitial(); // state = AuthenticationStateInitial();
_failureCounter = 0; // _failureCounter = 0;
} // }
//
static const maxFailedLogins = 3; // static const maxFailedLogins = 3;
static const banTimeout = 180; // 3 mins // static const banTimeout = 180; // 3 mins
final banTimeoutKey = S.current.auth_store_ban_timeout; // final banTimeoutKey = S.current.auth_store_ban_timeout;
//
final UserService userService; // final UserService userService;
final WalletService walletService; // final WalletService walletService;
//
final SharedPreferences sharedPreferences; // final SharedPreferences sharedPreferences;
//
@observable // @observable
AuthState state; // AuthState state;
//
@observable // @observable
int _failureCounter; // int _failureCounter;
//
@action // @action
Future auth({String password}) async { // Future auth({String password}) async {
state = AuthenticationStateInitial(); // state = AuthenticationStateInitial();
final _banDuration = banDuration(); // final _banDuration = banDuration();
//
if (_banDuration != null) { // if (_banDuration != null) {
state = AuthenticationBanned( // state = AuthenticationBanned(
error: S.current.auth_store_banned_for + '${_banDuration.inMinutes}' + S.current.auth_store_banned_minutes); // error: S.current.auth_store_banned_for + '${_banDuration.inMinutes}' + S.current.auth_store_banned_minutes);
return; // return;
} // }
//
state = AuthenticationInProgress(); // state = AuthenticationInProgress();
final isAuth = await userService.authenticate(password); // final isAuth = await userService.authenticate(password);
//
if (isAuth) { // if (isAuth) {
state = AuthenticatedSuccessfully(); // state = AuthenticatedSuccessfully();
_failureCounter = 0; // _failureCounter = 0;
} else { // } else {
_failureCounter += 1; // _failureCounter += 1;
//
if (_failureCounter >= maxFailedLogins) { // if (_failureCounter >= maxFailedLogins) {
final banDuration = await ban(); // final banDuration = await ban();
state = AuthenticationBanned( // state = AuthenticationBanned(
error: S.current.auth_store_banned_for + '${banDuration.inMinutes}' + S.current.auth_store_banned_minutes); // error: S.current.auth_store_banned_for + '${banDuration.inMinutes}' + S.current.auth_store_banned_minutes);
return; // return;
} // }
//
state = AuthenticationFailure(error: S.current.auth_store_incorrect_password); // state = AuthenticationFailure(error: S.current.auth_store_incorrect_password);
} // }
} // }
//
Duration banDuration() { // Duration banDuration() {
final unbanTimestamp = sharedPreferences.getInt(banTimeoutKey); // final unbanTimestamp = sharedPreferences.getInt(banTimeoutKey);
//
if (unbanTimestamp == null) { // if (unbanTimestamp == null) {
return null; // return null;
} // }
//
final unbanTime = DateTime.fromMillisecondsSinceEpoch(unbanTimestamp); // final unbanTime = DateTime.fromMillisecondsSinceEpoch(unbanTimestamp);
final now = DateTime.now(); // final now = DateTime.now();
//
if (now.isAfter(unbanTime)) { // if (now.isAfter(unbanTime)) {
return null; // return null;
} // }
//
return Duration(milliseconds: unbanTimestamp - now.millisecondsSinceEpoch); // return Duration(milliseconds: unbanTimestamp - now.millisecondsSinceEpoch);
} // }
//
Future<Duration> ban() async { // Future<Duration> ban() async {
final multiplier = _failureCounter - maxFailedLogins + 1; // final multiplier = _failureCounter - maxFailedLogins + 1;
final timeout = (multiplier * banTimeout) * 1000; // final timeout = (multiplier * banTimeout) * 1000;
final unbanTimestamp = DateTime.now().millisecondsSinceEpoch + timeout; // final unbanTimestamp = DateTime.now().millisecondsSinceEpoch + timeout;
await sharedPreferences.setInt(banTimeoutKey, unbanTimestamp); // await sharedPreferences.setInt(banTimeoutKey, unbanTimestamp);
//
return Duration(milliseconds: timeout); // return Duration(milliseconds: timeout);
} // }
//
@action // @action
void biometricAuth() { // void biometricAuth() {
state = AuthenticatedSuccessfully(); // state = AuthenticatedSuccessfully();
} // }
} //}

View file

@ -30,8 +30,8 @@ abstract class AuthenticationStoreBase with Store {
@observable @observable
AuthenticationState state; AuthenticationState state;
@observable // @observable
String errorMessage; // String errorMessage;
Future started() async { Future started() async {
final canAuth = await userService.canAuthenticate(); final canAuth = await userService.canAuthenticate();

View file

@ -2,7 +2,7 @@ import 'package:mobx/mobx.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.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/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/wallet_restoration/wallet_restoration_state.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/domain/common/crypto_currency.dart'; import 'package:cake_wallet/src/domain/common/crypto_currency.dart';
@ -37,7 +37,7 @@ abstract class WalleRestorationStoreBase with Store {
bool isValid; bool isValid;
@observable @observable
List<MnemoticItem> seed; List<MnemonicItem> seed;
@observable @observable
bool disabledState; bool disabledState;
@ -79,35 +79,35 @@ abstract class WalleRestorationStoreBase with Store {
} }
@action @action
void setSeed(List<MnemoticItem> seed) { void setSeed(List<MnemonicItem> seed) {
this.seed = seed; this.seed = seed;
} }
@action // @action
void validateSeed(List<MnemoticItem> seed) { // void validateSeed(List<MnemonicItem> seed) {
final _seed = seed != null ? seed : this.seed; // final _seed = seed != null ? seed : this.seed;
bool isValid = _seed != null ? _seed.length == 25 : false; // bool isValid = _seed != null ? _seed.length == 25 : false;
//
if (!isValid) { // if (!isValid) {
errorMessage = S.current.wallet_restoration_store_incorrect_seed_length; // errorMessage = S.current.wallet_restoration_store_incorrect_seed_length;
this.isValid = isValid; // this.isValid = isValid;
return; // return;
} // }
//
for (final item in _seed) { // for (final item in _seed) {
if (!item.isCorrect()) { // if (!item.isCorrect()) {
isValid = false; // isValid = false;
break; // break;
} // }
} // }
//
if (isValid) { // if (isValid) {
errorMessage = null; // errorMessage = null;
} // }
//
this.isValid = isValid; // this.isValid = isValid;
return; // return;
} // }
String _seedText() { String _seedText() {
return seed.fold('', (acc, item) => acc + ' ' + item.toString()); return seed.fold('', (acc, item) => acc + ' ' + item.toString());

View file

@ -2,24 +2,24 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
class BaseTextFormField extends StatelessWidget { class BaseTextFormField extends StatelessWidget {
BaseTextFormField({ BaseTextFormField(
this.controller, {this.controller,
this.keyboardType = TextInputType.text, this.keyboardType = TextInputType.text,
this.textInputAction = TextInputAction.done, this.textInputAction = TextInputAction.done,
this.textAlign = TextAlign.start, this.textAlign = TextAlign.start,
this.autovalidate = false, this.autovalidate = false,
this.hintText = '', this.hintText = '',
this.maxLines = 1, this.maxLines = 1,
this.inputFormatters, this.inputFormatters,
this.textColor, this.textColor,
this.hintColor, this.hintColor,
this.borderColor, this.borderColor,
this.prefix, this.prefix,
this.suffix, this.suffix,
this.suffixIcon, this.suffixIcon,
this.enabled = true, this.enabled = true,
this.validator this.validator,
}); this.placeholderTextStyle});
final TextEditingController controller; final TextEditingController controller;
final TextInputType keyboardType; final TextInputType keyboardType;
@ -37,6 +37,7 @@ class BaseTextFormField extends StatelessWidget {
final Widget suffixIcon; final Widget suffixIcon;
final bool enabled; final bool enabled;
final FormFieldValidator<String> validator; final FormFieldValidator<String> validator;
final TextStyle placeholderTextStyle;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -50,31 +51,26 @@ class BaseTextFormField extends StatelessWidget {
inputFormatters: inputFormatters, inputFormatters: inputFormatters,
enabled: enabled, enabled: enabled,
style: TextStyle( style: TextStyle(
fontSize: 16.0, fontSize: 16.0,
color: textColor ?? Theme.of(context).primaryTextTheme.title.color color: textColor ?? Theme.of(context).primaryTextTheme.title.color),
),
decoration: InputDecoration( decoration: InputDecoration(
prefix: prefix, prefix: prefix,
suffix: suffix, suffix: suffix,
suffixIcon: suffixIcon, suffixIcon: suffixIcon,
hintStyle: TextStyle( hintStyle: placeholderTextStyle ??
color: hintColor ?? Theme.of(context).primaryTextTheme.caption.color, TextStyle(
fontSize: 16 color: hintColor ??
), Theme.of(context).primaryTextTheme.caption.color,
hintText: hintText, fontSize: 16),
focusedBorder: UnderlineInputBorder( hintText: hintText,
borderSide: BorderSide( focusedBorder: UnderlineInputBorder(
color: borderColor ?? Theme.of(context).dividerColor, borderSide: BorderSide(
width: 1.0 color: borderColor ?? Theme.of(context).dividerColor,
) width: 1.0)),
), enabledBorder: UnderlineInputBorder(
enabledBorder: UnderlineInputBorder( borderSide: BorderSide(
borderSide: BorderSide( color: borderColor ?? Theme.of(context).dividerColor,
color: borderColor ?? Theme.of(context).dividerColor, width: 1.0))),
width: 1.0
)
)
),
validator: validator, validator: validator,
); );
} }

View file

@ -4,7 +4,10 @@ import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/domain/monero/get_height_by_date.dart'; import 'package:cake_wallet/src/domain/monero/get_height_by_date.dart';
class BlockchainHeightWidget extends StatefulWidget { class BlockchainHeightWidget extends StatefulWidget {
BlockchainHeightWidget({GlobalKey key}) : super(key: key); BlockchainHeightWidget({GlobalKey key, this.onHeightChange})
: super(key: key);
final Function(int) onHeightChange;
@override @override
State<StatefulWidget> createState() => BlockchainHeightState(); State<StatefulWidget> createState() => BlockchainHeightState();
@ -13,15 +16,23 @@ class BlockchainHeightWidget extends StatefulWidget {
class BlockchainHeightState extends State<BlockchainHeightWidget> { class BlockchainHeightState extends State<BlockchainHeightWidget> {
final dateController = TextEditingController(); final dateController = TextEditingController();
final restoreHeightController = TextEditingController(); final restoreHeightController = TextEditingController();
int get height => _height; int get height => _height;
int _height = 0; int _height = 0;
@override @override
void initState() { void initState() {
restoreHeightController.addListener(() => _height = restoreHeightController.addListener(() {
restoreHeightController.text != null try {
_changeHeight(restoreHeightController.text != null &&
restoreHeightController.text.isNotEmpty
? int.parse(restoreHeightController.text) ? int.parse(restoreHeightController.text)
: 0); : 0);
} catch (_) {
_changeHeight(0);
}
});
super.initState(); super.initState();
} }
@ -38,21 +49,18 @@ class BlockchainHeightState extends State<BlockchainHeightWidget> {
child: TextFormField( child: TextFormField(
style: TextStyle( style: TextStyle(
fontSize: 16.0, fontSize: 16.0,
color: Theme.of(context).primaryTextTheme.title.color color: Theme.of(context).primaryTextTheme.title.color),
),
controller: restoreHeightController, controller: restoreHeightController,
keyboardType: TextInputType.numberWithOptions( keyboardType: TextInputType.numberWithOptions(
signed: false, decimal: false), signed: false, decimal: false),
decoration: InputDecoration( decoration: InputDecoration(
hintStyle: TextStyle( hintStyle: TextStyle(
color: Theme.of(context).primaryTextTheme.caption.color, color: Theme.of(context).primaryTextTheme.caption.color,
fontSize: 16 fontSize: 16),
),
hintText: S.of(context).widgets_restore_from_blockheight, hintText: S.of(context).widgets_restore_from_blockheight,
focusedBorder: UnderlineInputBorder( focusedBorder: UnderlineInputBorder(
borderSide: BorderSide( borderSide: BorderSide(
color: Theme.of(context).dividerColor, color: Theme.of(context).dividerColor, width: 1.0)),
width: 1.0)),
enabledBorder: UnderlineInputBorder( enabledBorder: UnderlineInputBorder(
borderSide: BorderSide( borderSide: BorderSide(
color: Theme.of(context).dividerColor, color: Theme.of(context).dividerColor,
@ -81,13 +89,14 @@ class BlockchainHeightState extends State<BlockchainHeightWidget> {
child: TextFormField( child: TextFormField(
style: TextStyle( style: TextStyle(
fontSize: 16.0, fontSize: 16.0,
color: Theme.of(context).primaryTextTheme.title.color color: Theme.of(context).primaryTextTheme.title.color),
),
decoration: InputDecoration( decoration: InputDecoration(
hintStyle: TextStyle( hintStyle: TextStyle(
color: Theme.of(context).primaryTextTheme.caption.color, color: Theme.of(context)
fontSize: 16 .primaryTextTheme
), .caption
.color,
fontSize: 16),
hintText: S.of(context).widgets_restore_from_date, hintText: S.of(context).widgets_restore_from_date,
focusedBorder: UnderlineInputBorder( focusedBorder: UnderlineInputBorder(
borderSide: BorderSide( borderSide: BorderSide(
@ -113,7 +122,7 @@ class BlockchainHeightState extends State<BlockchainHeightWidget> {
Future _selectDate(BuildContext context) async { Future _selectDate(BuildContext context) async {
final now = DateTime.now(); final now = DateTime.now();
final DateTime date = await showDatePicker( final date = await showDatePicker(
context: context, context: context,
initialDate: now.subtract(Duration(days: 1)), initialDate: now.subtract(Duration(days: 1)),
firstDate: DateTime(2014, DateTime.april), firstDate: DateTime(2014, DateTime.april),
@ -125,8 +134,13 @@ class BlockchainHeightState extends State<BlockchainHeightWidget> {
setState(() { setState(() {
dateController.text = DateFormat('yyyy-MM-dd').format(date); dateController.text = DateFormat('yyyy-MM-dd').format(date);
restoreHeightController.text = '$height'; restoreHeightController.text = '$height';
_height = height; _changeHeight(height);
}); });
} }
} }
void _changeHeight(int height) {
_height = height;
widget.onHeightChange?.call(height);
}
} }

View file

@ -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<SeedLanguageSelector> {
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<SeedLanguagePickerState>();
@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<String>(
context: context,
builder: (BuildContext context) =>
SeedLanguagePicker(key: _pickerKey, selected: this.selected));
setState(() => this.selected = selected);
});
}
}

View file

@ -1,94 +1,64 @@
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:cake_wallet/palette.dart'; import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/src/domain/monero/mnemonics/english.dart'; import 'package:cake_wallet/core/seed_validator.dart';
import 'package:cake_wallet/src/domain/monero/mnemonics/chinese_simplified.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/domain/monero/mnemonics/dutch.dart'; import 'package:cake_wallet/src/domain/common/mnemonic_item.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/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
class SeedWidget extends StatefulWidget { class SeedWidget extends StatefulWidget {
SeedWidget({Key key, this.onMnemoticChange, this.onFinish, this.seedLanguage}) : super(key: key) { SeedWidget(
switch (seedLanguage) { {Key key,
case 'English': this.maxLength,
words = EnglishMnemonics.words; this.onMnemonicChange,
break; this.onFinish,
case 'Chinese (simplified)': this.validator})
words = ChineseSimplifiedMnemonics.words; : super(key: key);
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<MnemoticItem>) onMnemoticChange; final int maxLength;
final Function(List<MnemonicItem>) onMnemonicChange;
final Function() onFinish; final Function() onFinish;
final String seedLanguage; final SeedValidator validator;
List<String> words;
@override @override
SeedWidgetState createState() => SeedWidgetState(); SeedWidgetState createState() => SeedWidgetState(maxLength: maxLength);
} }
class SeedWidgetState extends State<SeedWidget> { class SeedWidgetState extends State<SeedWidget> {
static const maxLength = 25; SeedWidgetState({this.maxLength});
List<MnemoticItem> items = <MnemoticItem>[]; List<MnemonicItem> items = <MnemonicItem>[];
final int maxLength;
final _seedController = TextEditingController(); final _seedController = TextEditingController();
final _seedTextFieldKey = GlobalKey(); final _seedTextFieldKey = GlobalKey();
MnemoticItem selectedItem; MnemonicItem selectedItem;
bool isValid; bool isValid;
String errorMessage; String errorMessage;
List<MnemoticItem> currentMnemotics; List<MnemonicItem> currentMnemonics;
bool isCurrentMnemoticValid; bool isCurrentMnemonicValid;
String _errorMessage; String _errorMessage;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
isValid = false; isValid = false;
isCurrentMnemoticValid = false; isCurrentMnemonicValid = false;
_seedController _seedController
.addListener(() => changeCurrentMnemotic(_seedController.text)); .addListener(() => changeCurrentMnemonic(_seedController.text));
} }
void addMnemotic(String text) { void addMnemonic(String text) {
setState(() => items.add(MnemoticItem( setState(() => items.add(MnemonicItem(text: text.trim().toLowerCase())));
text: text.trim().toLowerCase(), dic: widget.words)));
_seedController.text = ''; _seedController.text = '';
if (widget.onMnemoticChange != null) { if (widget.onMnemonicChange != null) {
widget.onMnemoticChange(items); widget.onMnemonicChange(items);
} }
} }
void mnemoticFromText(String text) { void mnemonicFromText(String text) {
final splitted = text.split(' '); final splitted = text.split(' ');
if (splitted.length >= 2) { if (splitted.length >= 2) {
@ -98,18 +68,18 @@ class SeedWidgetState extends State<SeedWidget> {
} }
if (selectedItem != null) { if (selectedItem != null) {
editTextOfSelectedMnemotic(text); editTextOfSelectedMnemonic(text);
} else { } else {
addMnemotic(text); addMnemonic(text);
} }
} }
} }
} }
void selectMnemotic(MnemoticItem item) { void selectMnemonic(MnemonicItem item) {
setState(() { setState(() {
selectedItem = item; selectedItem = item;
currentMnemotics = [item]; currentMnemonics = [item];
_seedController _seedController
..text = item.text ..text = item.text
@ -117,23 +87,23 @@ class SeedWidgetState extends State<SeedWidget> {
}); });
} }
void onMnemoticTap(MnemoticItem item) { void onMnemonicTap(MnemonicItem item) {
if (selectedItem == item) { if (selectedItem == item) {
setState(() => selectedItem = null); setState(() => selectedItem = null);
_seedController.text = ''; _seedController.text = '';
return; return;
} }
selectMnemotic(item); selectMnemonic(item);
} }
void editTextOfSelectedMnemotic(String text) { void editTextOfSelectedMnemonic(String text) {
setState(() => selectedItem.changeText(text)); setState(() => selectedItem.changeText(text));
selectedItem = null; selectedItem = null;
_seedController.text = ''; _seedController.text = '';
if (widget.onMnemoticChange != null) { if (widget.onMnemonicChange != null) {
widget.onMnemoticChange(items); widget.onMnemonicChange(items);
} }
} }
@ -143,83 +113,77 @@ class SeedWidgetState extends State<SeedWidget> {
selectedItem = null; selectedItem = null;
_seedController.text = ''; _seedController.text = '';
if (widget.onMnemoticChange != null) { if (widget.onMnemonicChange != null) {
widget.onMnemoticChange(items); widget.onMnemonicChange(items);
} }
}); });
} }
void invalidate() { void invalidate() => setState(() => isValid = false);
setState(() => isValid = false);
}
void validated() { void validated() => setState(() => isValid = true);
setState(() => isValid = true);
}
void setErrorMessage(String errorMessage) { void setErrorMessage(String errorMessage) =>
setState(() => this.errorMessage = errorMessage); setState(() => this.errorMessage = errorMessage);
}
void replaceText(String text) { void replaceText(String text) {
setState(() => items = []); setState(() => items = []);
mnemoticFromText(text); mnemonicFromText(text);
} }
void changeCurrentMnemotic(String text) { void changeCurrentMnemonic(String text) {
setState(() { setState(() {
final trimmedText = text.trim(); final trimmedText = text.trim();
final splitted = trimmedText.split(' '); final splitted = trimmedText.split(' ');
_errorMessage = null; _errorMessage = null;
if (text == null) { if (text == null) {
currentMnemotics = []; currentMnemonics = [];
isCurrentMnemoticValid = false; isCurrentMnemonicValid = false;
return; return;
} }
currentMnemotics = splitted currentMnemonics =
.map((text) => MnemoticItem(text: text, dic: widget.words)) splitted.map((text) => MnemonicItem(text: text)).toList();
.toList();
bool isValid = true; var isValid = true;
for (final word in currentMnemotics) { for (final word in currentMnemonics) {
isValid = word.isCorrect(); isValid = widget.validator.isValid(word);
if (!isValid) { if (!isValid) {
break; break;
} }
} }
isCurrentMnemoticValid = isValid; isCurrentMnemonicValid = isValid;
}); });
} }
void saveCurrentMnemoticToItems() { void saveCurrentMnemonicToItems() {
setState(() { setState(() {
if (selectedItem != null) { if (selectedItem != null) {
selectedItem.changeText(currentMnemotics.first.text.trim()); selectedItem.changeText(currentMnemonics.first.text.trim());
selectedItem = null; selectedItem = null;
} else { } else {
items.addAll(currentMnemotics); items.addAll(currentMnemonics);
} }
currentMnemotics = []; currentMnemonics = [];
_seedController.text = ''; _seedController.text = '';
}); });
} }
void showErrorIfExist() { void showErrorIfExist() {
setState(() => _errorMessage = setState(() => _errorMessage =
!isCurrentMnemoticValid ? S.current.incorrect_seed : null); !isCurrentMnemonicValid ? S.current.incorrect_seed : null);
} }
bool isSeedValid() { bool isSeedValid() {
bool isValid; bool isValid;
for (final item in items) { for (final item in items) {
isValid = item.isCorrect(); isValid = widget.validator.isValid(item);
if (!isValid) { if (!isValid) {
break; break;
@ -234,192 +198,207 @@ class SeedWidgetState extends State<SeedWidget> {
return Container( return Container(
child: Column(children: [ child: Column(children: [
Flexible( Flexible(
fit: FlexFit.tight, fit: FlexFit.tight,
flex: 1, flex: 1,
child: Container( child: Container(
width: double.infinity, width: double.infinity,
height: double.infinity, height: double.infinity,
padding: EdgeInsets.all(24), padding: EdgeInsets.all(24),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(24), bottomLeft: Radius.circular(24),
bottomRight: 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: <Widget>[
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 Padding(
), padding: EdgeInsets.only(top: 5),
child: SingleChildScrollView( child: Wrap(
child: Column( children: items.map((item) {
mainAxisAlignment: MainAxisAlignment.start, final isValid = widget.validator.isValid(item);
crossAxisAlignment: CrossAxisAlignment.start, final isSelected = selectedItem == item;
children: <Widget>[
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( return InkWell(
onTap: () => onMnemoticTap(item), onTap: () => onMnemonicTap(item),
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: isValid ? Colors.transparent : Palette.red), color: isValid
margin: EdgeInsets.only(right: 7, bottom: 8), ? Colors.transparent
child: Text( : Palette.red),
item.toString(), margin: EdgeInsets.only(right: 7, bottom: 8),
style: TextStyle( child: Text(
color: isValid item.toString(),
? Theme.of(context).primaryTextTheme.title.color style: TextStyle(
: Theme.of(context).primaryTextTheme.caption.color, color: isValid
fontSize: 16, ? Theme.of(context)
fontWeight: .primaryTextTheme
isSelected ? FontWeight.w900 : FontWeight.w400, .title
decoration: isSelected .color
? TextDecoration.underline : Theme.of(context)
: TextDecoration.none), .primaryTextTheme
)), .caption
); .color,
}).toList(),) fontSize: 16,
) fontWeight: isSelected
], ? FontWeight.w900
), : FontWeight.w400,
decoration: isSelected
? TextDecoration.underline
: TextDecoration.none),
)),
);
}).toList(),
))
],
), ),
), ),
),
), ),
Flexible( Flexible(
fit: FlexFit.tight, fit: FlexFit.tight,
flex: 2, flex: 2,
child: Padding( 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( child: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[ children: <Widget>[
Text( Text(
S.of(context).restore_new_seed, S.of(context).restore_new_seed,
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Theme.of(context).primaryTextTheme.title.color 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
), ),
controller: _seedController, Padding(
textInputAction: TextInputAction.done, padding: EdgeInsets.only(top: 24),
decoration: InputDecoration( child: TextFormField(
suffixIcon: GestureDetector( key: _seedTextFieldKey,
behavior: HitTestBehavior.opaque, onFieldSubmitted: (text) => isCurrentMnemonicValid
child: ConstrainedBox( ? saveCurrentMnemonicToItems()
constraints: BoxConstraints(maxWidth: 145), : null,
child: Row( style: TextStyle(
mainAxisAlignment: MainAxisAlignment.end, fontSize: 16.0,
children: <Widget>[ color:
Text( Theme.of(context).primaryTextTheme.title.color),
'${items.length}/${SeedWidgetState.maxLength}', controller: _seedController,
style: TextStyle( textInputAction: TextInputAction.done,
color: Theme.of(context).primaryTextTheme.caption.color, decoration: InputDecoration(
fontSize: 14)), suffixIcon: GestureDetector(
SizedBox(width: 10), behavior: HitTestBehavior.opaque,
InkWell( child: ConstrainedBox(
onTap: () async => constraints: BoxConstraints(maxWidth: 145),
Clipboard.getData('text/plain').then( child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
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) => (clipboard) =>
replaceText(clipboard.text)), replaceText(clipboard.text)),
child: Container( child: Container(
height: 35, height: 35,
padding: EdgeInsets.all(7), padding: EdgeInsets.all(7),
decoration: BoxDecoration( decoration: BoxDecoration(
color: color: Theme.of(context)
Theme.of(context).accentTextTheme.title.backgroundColor, .accentTextTheme
borderRadius: .title
BorderRadius.circular(10.0)), .backgroundColor,
child: Text( borderRadius:
S.of(context).paste, BorderRadius.circular(10.0)),
style: TextStyle( child: Text(
color: Theme.of(context).primaryTextTheme.title.color S.of(context).paste,
), style: TextStyle(
)), color: Theme.of(context)
) .primaryTextTheme
], .title
.color),
)),
)
],
),
),
), ),
), hintStyle: TextStyle(
), color: Theme.of(context)
hintStyle: .primaryTextTheme
TextStyle( .caption
color: Theme.of(context).primaryTextTheme.caption.color, .color,
fontSize: 16 fontSize: 16),
), hintText:
hintText: S.of(context).restore_from_seed_placeholder, S.of(context).restore_from_seed_placeholder,
errorText: _errorMessage, errorText: _errorMessage,
focusedBorder: UnderlineInputBorder( focusedBorder: UnderlineInputBorder(
borderSide: BorderSide( borderSide: BorderSide(
color: Theme.of(context).dividerColor, width: 1.0)), color: Theme.of(context).dividerColor,
enabledBorder: UnderlineInputBorder( width: 1.0)),
borderSide: BorderSide( enabledBorder: UnderlineInputBorder(
color: Theme.of(context).dividerColor, borderSide: BorderSide(
width: 1.0))), color: Theme.of(context).dividerColor,
enableInteractiveSelection: false, width: 1.0))),
), enableInteractiveSelection: false,
) ),
]), )
) ]),
), )),
Padding( Padding(
padding: EdgeInsets.only(left: 24, right: 24, bottom: 24), padding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
child: Row( child: Row(
children: <Widget>[ children: <Widget>[
Flexible( Flexible(
child: Padding( child: Padding(
padding: EdgeInsets.only(right: 8), padding: EdgeInsets.only(right: 8),
child: PrimaryButton( child: PrimaryButton(
onPressed: clear, onPressed: clear,
text: S.of(context).clear, text: S.of(context).clear,
color: Colors.red, color: Colors.red,
textColor: Colors.white, textColor: Colors.white,
isDisabled: items.isEmpty, isDisabled: items.isEmpty,
), ),
) )),
),
Flexible( Flexible(
child: Padding( child: Padding(
padding: EdgeInsets.only(left: 8), padding: EdgeInsets.only(left: 8),
child: (selectedItem == null && items.length == maxLength) child: (selectedItem == null && items.length == maxLength)
? PrimaryButton( ? PrimaryButton(
text: S.of(context).restore_next, text: S.of(context).restore_next,
isDisabled: !isSeedValid(), isDisabled: !isSeedValid(),
onPressed: () => widget.onFinish != null onPressed: () => widget.onFinish != null
? widget.onFinish() ? widget.onFinish()
: null, : null,
color: Colors.green, color: Colors.green,
textColor: Colors.white) textColor: Colors.white)
: PrimaryButton( : PrimaryButton(
text: selectedItem != null text: selectedItem != null
? S.of(context).save ? S.of(context).save
: S.of(context).add_new_word, : S.of(context).add_new_word,
onPressed: () => isCurrentMnemoticValid onPressed: () => isCurrentMnemonicValid
? saveCurrentMnemoticToItems() ? saveCurrentMnemonicToItems()
: null, : null,
onDisabledPressed: () => showErrorIfExist(), onDisabledPressed: () => showErrorIfExist(),
isDisabled: !isCurrentMnemoticValid, isDisabled: !isCurrentMnemonicValid,
color: Colors.green, color: Colors.green,
textColor: Colors.white), textColor: Colors.white),
), ),
) )
], ],

16
lib/store/app_store.dart Normal file
View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -44,9 +44,10 @@ class Themes {
), ),
display4: TextStyle( display4: TextStyle(
color: Palette.oceanBlue // QR code color: Palette.oceanBlue // QR code
) ),
// headline1: TextStyle(color: Palette.nightBlue)
), ),
dividerColor: Palette.periwinkle, dividerColor: Palette.eee,
accentTextTheme: TextTheme( accentTextTheme: TextTheme(
title: TextStyle( title: TextStyle(
color: Palette.darkLavender, // top panel color: Palette.darkLavender, // top panel
@ -112,7 +113,8 @@ class Themes {
), ),
display4: TextStyle( display4: TextStyle(
color: PaletteDark.gray // QR code color: PaletteDark.gray // QR code
) ),
// headline5: TextStyle(color: PaletteDark.gray)
), ),
dividerColor: PaletteDark.distantBlue, dividerColor: PaletteDark.distantBlue,
accentTextTheme: TextTheme( accentTextTheme: TextTheme(

3
lib/utils/list_item.dart Normal file
View file

@ -0,0 +1,3 @@
abstract class ListItem {
const ListItem();
}

View file

@ -0,0 +1,5 @@
class ListSection<Item> {
const ListSection({this.items});
final List<Item> items;
}

View file

@ -0,0 +1,3 @@
import 'package:cake_wallet/utils/list_item.dart';
class AccountListHeader extends ListItem {}

View file

@ -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<void> 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<void> _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<void> _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();
}
}
}

View file

@ -0,0 +1,3 @@
import 'package:cake_wallet/utils/list_item.dart';
class AddressListHeader extends ListItem {}

View file

@ -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;
}

View file

@ -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<ListItem> get items =>
ObservableList<ListItem>()..addAll(_baseItems)..addAll(addressList);
@computed
ObservableList<ListItem> get addressList {
final wallet = _wallet;
final addressList = ObservableList<ListItem>();
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<ListItem> _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());
}
}

View file

@ -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;
}

View file

@ -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<void> 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<Duration> 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();
}

View file

@ -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<Object> 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');
}
}

View file

@ -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;
}

View file

@ -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<void> 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<void> process(WalletCredentials credentials) =>
throw UnimplementedError();
}

View file

@ -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<void> process(WalletCredentials credentials) async =>
_walletCreationService.create(credentials);
}

View file

@ -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<void> process(WalletCredentials credentials) async =>
_walletCreationService.restoreFromSeed(credentials);
}

View file

@ -371,6 +371,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.1.19" 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: glob:
dependency: transitive dependency: transitive
description: description:

View file

@ -58,6 +58,7 @@ dependencies:
password: ^1.0.0 password: ^1.0.0
basic_utils: ^1.0.8 basic_utils: ^1.0.8
bitcoin_flutter: ^2.0.0 bitcoin_flutter: ^2.0.0
get_it: ^4.0.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: