Added the project

This commit is contained in:
M
2020-01-04 21:31:52 +02:00
parent 2803db18b2
commit 69f4c127ab
417 changed files with 33436 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
abstract class Balance {}
@@ -0,0 +1,46 @@
import 'package:flutter/foundation.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/domain/common/enumerable_item.dart';
class BalanceDisplayMode extends EnumerableItem<int> with Serializable<int> {
static const all = [
BalanceDisplayMode.fullBalance,
BalanceDisplayMode.availableBalance,
BalanceDisplayMode.hiddenBalance
];
static const fullBalance = BalanceDisplayMode(raw: 0, title: 'Full Balance');
static const availableBalance =
BalanceDisplayMode(raw: 1, title: 'Available Balance');
static const hiddenBalance =
BalanceDisplayMode(raw: 2, title: 'Hidden Balance');
static BalanceDisplayMode deserialize({int raw}) {
switch (raw) {
case 0:
return fullBalance;
case 1:
return availableBalance;
case 2:
return hiddenBalance;
default:
return null;
}
}
const BalanceDisplayMode({@required String title, @required int raw})
: super(title: title, raw: raw);
@override
String toString() {
switch (this) {
case BalanceDisplayMode.fullBalance:
return S.current.xmr_full_balance;
case BalanceDisplayMode.availableBalance:
return S.current.xmr_available_balance;
case BalanceDisplayMode.hiddenBalance:
return S.current.xmr_hidden;
default:
return '';
}
}
}
@@ -0,0 +1,25 @@
import 'package:cake_wallet/src/domain/common/transaction_priority.dart';
double calculateEstimatedFee({TransactionPriority priority}) {
if (priority == TransactionPriority.slow) {
return 0.00002459;
}
if (priority == TransactionPriority.regular) {
return 0.00012305;
}
if (priority == TransactionPriority.medium) {
return 0.00024503;
}
if (priority == TransactionPriority.fast) {
return 0.00061453;
}
if (priority == TransactionPriority.fastest) {
return 0.0260216;
}
return 0;
}
@@ -0,0 +1,14 @@
String calculateFiatAmount({double price, String cryptoAmount}) {
if (price == null || cryptoAmount == null) {
return '0.00';
}
final _amount = double.parse(cryptoAmount);
final result = price * _amount;
if (result == 0.0) {
return '0.00';
}
return result > 0.01 ? result.toStringAsFixed(2) : '< 0.01';
}
@@ -0,0 +1,13 @@
String calculateFiatAmountRaw({double price, double cryptoAmount}) {
if (price == null) {
return '0.00';
}
final result = price * cryptoAmount;
if (result == 0.0) {
return '0.00';
}
return result > 0.01 ? result.toStringAsFixed(2) : '< 0.01';
}
+30
View File
@@ -0,0 +1,30 @@
import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:cake_wallet/src/domain/common/crypto_currency.dart';
part 'contact.g.dart';
@HiveType()
class Contact extends HiveObject {
static const boxName = 'Contacts';
@HiveField(0)
String name;
@HiveField(1)
String address;
@HiveField(2)
int raw;
CryptoCurrency get type => CryptoCurrency.deserialize(raw: raw);
Contact(
{@required this.name,
@required this.address,
CryptoCurrency type})
: raw = type?.raw;
updateCryptoCurrency({@required CryptoCurrency currency}) =>
raw = currency.raw;
}
@@ -0,0 +1,66 @@
import 'package:cake_wallet/src/domain/common/enumerable_item.dart';
import 'package:hive/hive.dart';
part 'crypto_currency.g.dart';
@HiveType()
class CryptoCurrency extends EnumerableItem<int> with Serializable<int> {
static const all = [
CryptoCurrency.xmr,
CryptoCurrency.btc,
CryptoCurrency.eth,
CryptoCurrency.ltc,
CryptoCurrency.bch,
CryptoCurrency.dash
];
static const xmr = CryptoCurrency(title: 'XMR', raw: 0);
static const btc = CryptoCurrency(title: 'BTC', raw: 1);
static const eth = CryptoCurrency(title: 'ETH', raw: 2);
static const ltc = CryptoCurrency(title: 'LTC', raw: 3);
static const bch = CryptoCurrency(title: 'BCH', raw: 4);
static const dash = CryptoCurrency(title: 'DASH', raw: 5);
static CryptoCurrency deserialize({int raw}) {
switch (raw) {
case 0:
return xmr;
case 1:
return btc;
case 2:
return eth;
case 3:
return ltc;
case 4:
return bch;
case 5:
return dash;
default:
return null;
}
}
static CryptoCurrency fromString(String raw) {
switch (raw.toLowerCase()) {
case 'xmr':
return xmr;
case 'btc':
return btc;
case 'eth':
return eth;
case 'ltc':
return ltc;
case 'bch':
return bch;
case 'dash':
return dash;
default:
return null;
}
}
const CryptoCurrency({final String title, final int raw})
: super(title: title, raw: raw);
@override
String toString() => title;
}
@@ -0,0 +1,10 @@
import 'package:cake_wallet/src/domain/common/crypto_currency.dart';
String cryptoToString(CryptoCurrency crypto) {
switch (crypto) {
case CryptoCurrency.xmr:
return 'XMR';
default:
return '';
}
}
@@ -0,0 +1,46 @@
import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:cake_wallet/src/domain/common/node.dart';
import 'package:cake_wallet/src/domain/common/balance_display_mode.dart';
import 'package:cake_wallet/src/domain/common/fiat_currency.dart';
import 'package:cake_wallet/src/domain/common/node_list.dart';
import 'package:cake_wallet/src/domain/common/transaction_priority.dart';
Future defaultSettingsMigration(
{@required int version,
@required SharedPreferences sharedPreferences,
@required Box<Node> nodes}) async {
int currentVersion =
sharedPreferences.getInt('current_default_settings_migration_version') ??
0;
if (currentVersion >= version) {
return;
}
try {
switch (version) {
case 1:
sharedPreferences.setString(
'current_fiat_currency', FiatCurrency.usd.toString());
sharedPreferences.setInt(
'current_fee_priority', TransactionPriority.standart.raw);
sharedPreferences.setInt('current_balance_display_mode',
BalanceDisplayMode.availableBalance.raw);
sharedPreferences.setInt(
'current_default_settings_migration_version', 1);
sharedPreferences.setBool('save_recipient_address', false);
await resetToDefault(nodes);
sharedPreferences.setInt('current_node_id', 0);
break;
default:
break;
}
} catch (e) {
print('Migration error: ${e.toString()}');
}
sharedPreferences.setInt(
'current_default_settings_migration_version', version);
}
+102
View File
@@ -0,0 +1,102 @@
import 'dart:convert';
import 'package:dio/dio.dart' as Dio;
import 'package:crypto/crypto.dart' as crypto;
import 'dart:math' as math;
class DigestRequest {
final md5 = crypto.md5;
String generateCnonce() {
final rnd = math.Random.secure();
var values = List<int>.generate(32, (i) => rnd.nextInt(256));
return base64Url.encode(values).substring(0, 8);
}
String generateHA1({String realm, String username, String password}) {
final ha1CredentialsData =
Utf8Encoder().convert('$username:$realm:$password');
final ha1 = md5.convert(ha1CredentialsData).toString();
return ha1;
}
String generateHA2({String method, String uri}) {
final ha2Data = Utf8Encoder().convert('$method:$uri');
final ha2 = md5.convert(ha2Data).toString();
return ha2;
}
String generateResponseString(
{String ha1,
String ha2,
String nonce,
String nonceCount,
String cnonce,
String qop}) {
final responseData =
Utf8Encoder().convert('$ha1:$nonce:$nonceCount:$cnonce:$qop:$ha2');
final response = md5.convert(responseData).toString();
return response;
}
Map<String, String> parsetAuthorizationHeader({String source}) {
final authHeaderParts =
source.substring(7).split(',').map((item) => item.trim());
var authenticate = Map<String, String>();
for (final part in authHeaderParts) {
final kv = part.split('=');
authenticate[kv[0]] =
kv.getRange(1, kv.length).join('=').replaceAll('"', '');
}
return authenticate;
}
Future<Dio.Response> request(
{String uri, String login, String password}) async {
final path = '/json_rpc';
final method = 'POST';
final url = Uri.http(uri, path);
final dio = Dio.Dio();
Map<String, String> headers = {'Content-type': 'application/json'};
String body =
json.encode({"jsonrpc": "2.0", "id": "0", "method": "get_info"});
var credentialsResponse = await dio.post(url.toString(),
options: Dio.Options(headers: headers, validateStatus: (_) => true));
var resHeaeders = credentialsResponse.headers;
final authenticate =
parsetAuthorizationHeader(source: resHeaeders['www-authenticate'].first);
final qop = authenticate['qop'];
final algorithm = 'MD5';
final realm = 'monero-rpc';
final nonce = authenticate['nonce'];
final cnonce = generateCnonce();
var nonceCount = '00000001';
final ha1 = generateHA1(realm: realm, username: login, password: password);
final ha2 = generateHA2(method: method, uri: path);
final response = generateResponseString(
ha1: ha1,
ha2: ha2,
nonce: nonce,
nonceCount: nonceCount,
cnonce: cnonce,
qop: qop);
final authorizationHeaders = {
'Content-type': 'application/json',
'Authorization':
'Digest username="$login",realm="$realm",nonce="$nonce",uri="$path",algorithm="$algorithm",qop=$qop,nc=$nonceCount,cnonce="$cnonce",response="$response"'
};
final res = await dio.post(url.toString(),
options: Dio.Options(headers: authorizationHeaders), data: body);
return res;
}
}
+54
View File
@@ -0,0 +1,54 @@
import 'package:encrypt/encrypt.dart';
import 'package:password/password.dart';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
String encrypt({String source, String key, int keyLength = 16}) {
final _key = Key.fromUtf8(key);
final iv = IV.fromLength(keyLength);
final encrypter = Encrypter(AES(_key));
final encrypted = encrypter.encrypt(source, iv: iv);
return encrypted.base64;
}
String decrypt({String source, String key, int keyLength = 16}) {
final _key = Key.fromUtf8(key);
final iv = IV.fromLength(keyLength);
final encrypter = Encrypter(AES(_key));
final decrypted = encrypter.decrypt64(source, iv: iv);
return decrypted;
}
String hash({String source}) {
final algorithm = PBKDF2();
final hash = Password.hash(source, algorithm);
return hash;
}
String encodedPinCode({String pin}) {
final source = '${secrets.salt}$pin';
return encrypt(source: source, key: secrets.key);
}
String decodedPinCode({String pin}) {
final decrypted = decrypt(source: pin, key: secrets.key);
return decrypted.substring(secrets.key.length, decrypted.length);
}
String encodeWalletPassword({String password}) {
final source = password;
final _key = secrets.shortKey + secrets.walletSalt;
return encrypt(source: source, key: _key);
}
String decodeWalletPassword({String password}) {
final source = password;
final _key = secrets.shortKey + secrets.walletSalt;
return decrypt(source: source, key: _key);
}
@@ -0,0 +1,16 @@
import 'package:flutter/foundation.dart';
abstract class EnumerableItem<T> {
final T raw;
final String title;
const EnumerableItem({@required this.title, @required this.raw});
@override
String toString() => title;
}
mixin Serializable<T> on EnumerableItem<T> {
static Serializable deserialize<T>({T raw}) => null;
T serialize() => raw;
}
+37
View File
@@ -0,0 +1,37 @@
import 'dart:convert';
import 'package:http/http.dart';
import 'package:cake_wallet/src/domain/common/crypto_currency.dart';
import 'package:cake_wallet/src/domain/common/fiat_currency.dart';
import 'package:cake_wallet/src/domain/common/currency_formatter.dart';
const fiatApiAuthority = 'fiat-api.cakewallet.com';
const fiatApiPath = '/v1/rates';
Future<double> fetchPriceFor({CryptoCurrency crypto, FiatCurrency fiat}) async {
double price = 0.0;
try {
final fiatStringified = fiat.toString();
final uri =
Uri.https(fiatApiAuthority, fiatApiPath, {'convert': fiatStringified});
final response = await get(uri.toString());
if (response.statusCode != 200) {
return 0.0;
}
final responseJSON = json.decode(response.body);
final data = responseJSON['data'];
for (final item in data) {
if (item['symbol'] == cryptoToString(crypto)) {
price = item['quote'][fiatStringified]['price'];
break;
}
}
return price;
} catch (e) {
return price;
}
}
+80
View File
@@ -0,0 +1,80 @@
import 'package:cake_wallet/src/domain/common/enumerable_item.dart';
class FiatCurrency extends EnumerableItem<String> with Serializable<String> {
static const all = [
FiatCurrency.aud,
FiatCurrency.bgn,
FiatCurrency.brl,
FiatCurrency.cad,
FiatCurrency.chf,
FiatCurrency.cny,
FiatCurrency.czk,
FiatCurrency.eur,
FiatCurrency.dkk,
FiatCurrency.gbp,
FiatCurrency.hkd,
FiatCurrency.hrk,
FiatCurrency.huf,
FiatCurrency.idr,
FiatCurrency.ils,
FiatCurrency.inr,
FiatCurrency.isk,
FiatCurrency.jpy,
FiatCurrency.krw,
FiatCurrency.mxn,
FiatCurrency.myr,
FiatCurrency.nok,
FiatCurrency.nzd,
FiatCurrency.php,
FiatCurrency.pln,
FiatCurrency.ron,
FiatCurrency.rub,
FiatCurrency.sek,
FiatCurrency.sgd,
FiatCurrency.thb,
FiatCurrency.usd,
FiatCurrency.zar,
FiatCurrency.vef
];
static const aud = FiatCurrency(symbol: 'AUD');
static const bgn = FiatCurrency(symbol: 'BGN');
static const brl = FiatCurrency(symbol: 'BRL');
static const cad = FiatCurrency(symbol: 'CAD');
static const chf = FiatCurrency(symbol: 'CHF');
static const cny = FiatCurrency(symbol: 'CNY');
static const czk = FiatCurrency(symbol: 'CZK');
static const eur = FiatCurrency(symbol: 'EUR');
static const dkk = FiatCurrency(symbol: 'DKK');
static const gbp = FiatCurrency(symbol: 'GBP');
static const hkd = FiatCurrency(symbol: 'HKD');
static const hrk = FiatCurrency(symbol: 'HRK');
static const huf = FiatCurrency(symbol: 'HUF');
static const idr = FiatCurrency(symbol: 'IDR');
static const ils = FiatCurrency(symbol: 'ILS');
static const inr = FiatCurrency(symbol: 'INR');
static const isk = FiatCurrency(symbol: 'ISK');
static const jpy = FiatCurrency(symbol: 'JPY');
static const krw = FiatCurrency(symbol: 'KRW');
static const mxn = FiatCurrency(symbol: 'MXN');
static const myr = FiatCurrency(symbol: 'MYR');
static const nok = FiatCurrency(symbol: 'NOK');
static const nzd = FiatCurrency(symbol: 'NZD');
static const php = FiatCurrency(symbol: 'PHP');
static const pln = FiatCurrency(symbol: 'PLN');
static const ron = FiatCurrency(symbol: 'RON');
static const rub = FiatCurrency(symbol: 'RUB');
static const sek = FiatCurrency(symbol: 'SEK');
static const sgd = FiatCurrency(symbol: 'SGD');
static const thb = FiatCurrency(symbol: 'THB');
static const usd = FiatCurrency(symbol: 'USD');
static const zar = FiatCurrency(symbol: 'ZAR');
static const vef = FiatCurrency(symbol: 'VEF');
const FiatCurrency({String symbol}) : super(title: symbol, raw: symbol);
operator ==(o) => o is FiatCurrency && o.raw == raw;
@override
int get hashCode => raw.hashCode ^ title.hashCode;
}
@@ -0,0 +1,23 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hive/hive.dart';
Future<List<int>> getEncryptionKey(
{String forKey, FlutterSecureStorage secureStorage}) async {
final stringifiedKey =
await secureStorage.read(key: 'transactionDescriptionsBoxKey');
List<int> key;
if (stringifiedKey == null) {
key = Hive.generateSecureKey();
final keyStringified = key.join(',');
await secureStorage.write(
key: 'transactionDescriptionsBoxKey', value: keyStringified);
} else {
key = stringifiedKey
.split(',')
.map((i) => int.parse(i))
.toList();
}
return key;
}
+16
View File
@@ -0,0 +1,16 @@
import 'package:flutter/material.dart';
class Language with ChangeNotifier {
String _currentLanguage;
Language(this._currentLanguage);
getCurrentLanguage() => _currentLanguage;
setCurrentLanguage(String language){
_currentLanguage = language;
notifyListeners();
}
}
+19
View File
@@ -0,0 +1,19 @@
class MnemoticItem {
String get text => _text;
final List<String> dic;
String _text;
MnemoticItem({String text, this.dic}) {
_text = text;
}
bool isCorrect() => dic.contains(text);
void changeText(String text) {
_text = text;
}
@override
String toString() => text;
}
+49
View File
@@ -0,0 +1,49 @@
import 'package:flutter/foundation.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:hive/hive.dart';
import 'package:cake_wallet/src/domain/common/digest_request.dart';
part 'node.g.dart';
@HiveType()
class Node extends HiveObject {
static const boxName = 'Nodes';
@HiveField(0)
String uri;
@HiveField(1)
String login;
@HiveField(2)
String password;
Node({@required this.uri, this.login, this.password});
Node.fromMap(Map map)
: uri = map['uri'] ?? '',
login = map['login'],
password = map['password'];
Future<bool> requestNode(String uri, {String login, String password}) async {
var resBody;
if (login != null && password != null) {
final digestRequest = DigestRequest();
var response = await digestRequest.request(
uri: uri, login: login, password: password);
resBody = response.data;
} else {
final url = Uri.http(uri, '/json_rpc');
Map<String, String> headers = {'Content-type': 'application/json'};
String body =
json.encode({"jsonrpc": "2.0", "id": "0", "method": "get_info"});
var response =
await http.post(url.toString(), headers: headers, body: body);
resBody = json.decode(response.body);
}
return !resBody["result"]["offline"];
}
}
+23
View File
@@ -0,0 +1,23 @@
import 'package:flutter/services.dart';
import 'package:hive/hive.dart';
import "package:yaml/yaml.dart";
import 'package:cake_wallet/src/domain/common/node.dart';
Future<List<Node>> loadDefaultNodes() async {
String nodesRaw = await rootBundle.loadString('assets/node_list.yml');
List nodes = loadYaml(nodesRaw);
return nodes.map((raw) => Node.fromMap(raw)).toList();
}
Future resetToDefault(Box<Node> nodeSource) async {
final nodes = await loadDefaultNodes();
await nodeSource.clear();
Map<int, Node> enteties = {};
for (var i = 0; i < nodes.length; i++) {
enteties[i] = nodes[i];
}
await nodeSource.putAll(enteties);
}
@@ -0,0 +1,3 @@
bool parseBoolFromString(String string) {
return string.toString() == 'true';
}
@@ -0,0 +1,25 @@
import 'package:flutter/foundation.dart';
import 'package:cw_monero/transaction_history.dart' as transactionHistory;
import 'package:cw_monero/structs/pending_transaction.dart';
import 'package:cake_wallet/src/domain/monero/monero_amount_format.dart';
class PendingTransaction {
final String amount;
final String fee;
final String hash;
int _pointerAddress;
PendingTransaction(
{@required this.amount, @required this.fee, @required this.hash});
PendingTransaction.fromTransactionDescription(
PendingTransactionDescription transactionDescription)
: amount = moneroAmountToString(amount: transactionDescription.amount),
fee = moneroAmountToString(amount: transactionDescription.fee),
hash = transactionDescription.hash,
_pointerAddress = transactionDescription.pointerAddress;
Future commit() async => transactionHistory
.commitTransactionFromPointerAddress(address: _pointerAddress);
}
+15
View File
@@ -0,0 +1,15 @@
import 'package:barcode_scan/barcode_scan.dart';
var isQrScannerShown = false;
Future<String> presentQRScanner() async {
isQrScannerShown = true;
try {
final result = await BarcodeScanner.scan();
isQrScannerShown = false;
return result;
} catch (e) {
isQrScannerShown = false;
throw e;
}
}
@@ -0,0 +1,27 @@
enum SecretStoreKey { moneroWalletPassword, pinCodePassword }
const moneroWalletPassword = "MONERO_WALLET_PASSWORD";
const pinCodePassword = "PIN_CODE_PASSWORD";
String generateStoreKeyFor({SecretStoreKey key, String walletName = "",}) {
var _key = "";
switch (key) {
case SecretStoreKey.moneroWalletPassword:
{
_key = moneroWalletPassword + "_" + walletName.toUpperCase();
}
break;
case SecretStoreKey.pinCodePassword:
{
_key = pinCodePassword;
}
break;
default:
{}
}
return _key;
}
+66
View File
@@ -0,0 +1,66 @@
import 'package:cake_wallet/generated/i18n.dart';
abstract class SyncStatus {
const SyncStatus();
double progress();
String title();
}
class SyncingSyncStatus extends SyncStatus {
final int height;
final int blockchainHeight;
final int refreshHeight;
SyncingSyncStatus(this.height, this.blockchainHeight, this.refreshHeight);
double progress() {
final line = blockchainHeight - refreshHeight;
final diff = line - (blockchainHeight - height);
return diff <= 0 ? 0.0 : diff / line;
}
String title() => S.current.sync_status_syncronizing;
@override
String toString() => '${blockchainHeight - height}';
}
class SyncedSyncStatus extends SyncStatus {
double progress() => 1.0;
String title() => S.current.sync_status_syncronized;
}
class NotConnectedSyncStatus extends SyncStatus {
const NotConnectedSyncStatus();
double progress() => 0.0;
String title() => S.current.sync_status_not_connected;
}
class StartingSyncStatus extends SyncStatus {
double progress() => 0.0;
String title() => S.current.sync_status_starting_sync;
}
class FailedSyncStatus extends SyncStatus {
double progress() => 1.0;
String title() => S.current.sync_status_failed_connect;
}
class ConnectingSyncStatus extends SyncStatus {
double progress() => 0.0;
String title() => S.current.sync_status_connecting;
}
class ConnectedSyncStatus extends SyncStatus {
double progress() => 0.0;
String title() => S.current.sync_status_connected;
}
@@ -0,0 +1 @@
abstract class TransactionCreationCredentials {}
@@ -0,0 +1,17 @@
enum TransactionDirection { incoming, outgoing }
TransactionDirection parseTransactionDirectionFromInt(int raw) {
switch (raw) {
case 0: return TransactionDirection.incoming;
case 1: return TransactionDirection.outgoing;
default: return null;
}
}
TransactionDirection parseTransactionDirectionFromNumber(String raw) {
switch (raw) {
case "0": return TransactionDirection.incoming;
case "1": return TransactionDirection.outgoing;
default: return null;
}
}
@@ -0,0 +1,10 @@
import 'package:rxdart/rxdart.dart';
import 'package:cake_wallet/src/domain/common/transaction_info.dart';
abstract class TransactionHistory {
Observable<List<TransactionInfo>> transactions;
Future<List<TransactionInfo>> getAll();
Future<int> count();
Future refresh();
Future update();
}
@@ -0,0 +1,46 @@
import 'package:cake_wallet/src/domain/monero/monero_amount_format.dart';
import 'package:cw_monero/structs/transaction_info_row.dart';
import 'package:cake_wallet/src/domain/common/parseBoolFromString.dart';
import 'package:cake_wallet/src/domain/common/transaction_direction.dart';
class TransactionInfo {
final String id;
final int height;
final TransactionDirection direction;
final DateTime date;
final int accountIndex;
final bool isPending;
final int amount;
String recipientAddress;
String _fiatAmount;
TransactionInfo.fromMap(Map map)
: id = map['hash'] ?? '',
height = map['height'] ?? '',
direction = parseTransactionDirectionFromNumber(map['direction']) ??
TransactionDirection.incoming,
date = DateTime.fromMillisecondsSinceEpoch(
(int.parse(map['timestamp']) ?? 0) * 1000),
isPending = parseBoolFromString(map['isPending']),
amount = map['amount'],
accountIndex = int.parse(map['accountIndex']);
TransactionInfo.fromRow(TransactionInfoRow row)
: id = row.getHash(),
height = row.blockHeight,
direction = parseTransactionDirectionFromInt(row.direction) ??
TransactionDirection.incoming,
date = DateTime.fromMillisecondsSinceEpoch(row.getDatetime() * 1000),
isPending = row.isPending != 0,
amount = row.getAmount(),
accountIndex = row.subaddrAccount;
TransactionInfo(this.id, this.height, this.direction, this.date,
this.isPending, this.amount, this.accountIndex);
String amountFormatted() => '${moneroAmountToString(amount: amount)} XMR';
String fiatAmount() => _fiatAmount ?? '';
changeFiatAmount(String amount) => _fiatAmount = amount;
}
@@ -0,0 +1,56 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/domain/common/enumerable_item.dart';
class TransactionPriority extends EnumerableItem<int> with Serializable<int> {
static const all = [
TransactionPriority.slow,
TransactionPriority.regular,
TransactionPriority.medium,
TransactionPriority.fast,
TransactionPriority.fastest
];
static const slow = TransactionPriority(title: 'Slow', raw: 0);
static const regular = TransactionPriority(title: 'Regular', raw: 1);
static const medium = TransactionPriority(title: 'Medium', raw: 2);
static const fast = TransactionPriority(title: 'Fast', raw: 3);
static const fastest = TransactionPriority(title: 'Fastest', raw: 4);
static const standart = slow;
static TransactionPriority deserialize({int raw}) {
switch (raw) {
case 0:
return slow;
case 1:
return regular;
case 2:
return medium;
case 3:
return fast;
case 4:
return fastest;
default:
return null;
}
}
const TransactionPriority({String title, int raw})
: super(title: title, raw: raw);
@override
String toString() {
switch (this) {
case TransactionPriority.slow:
return S.current.transaction_priority_slow;
case TransactionPriority.regular:
return S.current.transaction_priority_regular;
case TransactionPriority.medium:
return S.current.transaction_priority_medium;
case TransactionPriority.fast:
return S.current.transaction_priority_fast;
case TransactionPriority.fastest:
return S.current.transaction_priority_fastest;
default:
return '';
}
}
}
+61
View File
@@ -0,0 +1,61 @@
import 'package:rxdart/rxdart.dart';
import 'package:cake_wallet/src/domain/common/sync_status.dart';
import 'package:cake_wallet/src/domain/common/transaction_history.dart';
import 'package:cake_wallet/src/domain/common/wallet_type.dart';
import 'package:cake_wallet/src/domain/common/transaction_creation_credentials.dart';
import 'package:cake_wallet/src/domain/common/pending_transaction.dart';
import 'package:cake_wallet/src/domain/common/balance.dart';
import 'package:cake_wallet/src/domain/common/node.dart';
abstract class Wallet {
WalletType getType();
WalletType walletType;
Observable<Balance> onBalanceChange;
Observable<SyncStatus> syncStatus;
Observable<String> get onNameChange;
Observable<String> get onAddressChange;
String get name;
String get address;
Future updateInfo();
Future<String> getFilename();
Future<String> getName();
Future<String> getAddress();
Future<String> getSeed();
Future<Map<String, String>> getKeys();
Future<String> getFullBalance();
Future<String> getUnlockedBalance();
Future<int> getCurrentHeight();
Future<int> getNodeHeight();
Future<bool> isConnected();
Future close();
TransactionHistory getHistory();
Future connectToNode({Node node, bool useSSL = false, bool isLightWallet = false});
Future startSync();
Future<PendingTransaction> createTransaction(
TransactionCreationCredentials credentials);
Future rescan({int restoreHeight = 0});
}
@@ -0,0 +1,8 @@
import 'package:cake_wallet/src/domain/common/wallet_type.dart';
class WalletDescription {
final String name;
final WalletType type;
WalletDescription({this.name, this.type});
}
+27
View File
@@ -0,0 +1,27 @@
import 'package:cake_wallet/src/domain/common/wallet_type.dart';
import 'package:hive/hive.dart';
part 'wallet_info.g.dart';
@HiveType()
class WalletInfo extends HiveObject {
static const boxName = 'WalletInfo';
@HiveField(0)
String id;
@HiveField(1)
String name;
@HiveField(2)
WalletType type;
@HiveField(3)
bool isRecovery;
@HiveField(4)
int restoreHeight;
WalletInfo(
{this.id, this.name, this.type, this.isRecovery, this.restoreHeight});
}
+39
View File
@@ -0,0 +1,39 @@
import 'package:hive/hive.dart';
part 'wallet_type.g.dart';
@HiveType()
enum WalletType {
@HiveField(0)
monero,
@HiveField(1)
none
}
int serializeToInt(WalletType type) {
switch (type) {
case WalletType.monero:
return 0;
default:
return -1;
}
}
WalletType deserializeToInt(int raw) {
switch (raw) {
case 0:
return WalletType.monero;
default:
return null;
}
}
String walletTypeToString(WalletType type) {
switch (type) {
case WalletType.monero:
return 'Monero';
default:
return '';
}
}
@@ -0,0 +1,18 @@
import 'package:cake_wallet/src/domain/common/wallet.dart';
import 'package:cake_wallet/src/domain/common/wallet_description.dart';
abstract class WalletsManager {
Future<Wallet> create(String name, String password);
Future<Wallet> restoreFromSeed(
String name, String password, String seed, int restoreHeight);
Future<Wallet> restoreFromKeys(String name, String password,
int restoreHeight, String address, String viewKey, String spendKey);
Future<Wallet> openWallet(String name, String password);
Future<bool> isWalletExit(String name);
Future remove(WalletDescription wallet);
}
@@ -0,0 +1,139 @@
import 'dart:convert';
import 'package:cake_wallet/src/domain/exchange/trade_not_found_exeption.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cake_wallet/src/domain/common/crypto_currency.dart';
import 'package:cake_wallet/src/domain/exchange/exchange_pair.dart';
import 'package:cake_wallet/src/domain/exchange/exchange_provider.dart';
import 'package:cake_wallet/src/domain/exchange/limits.dart';
import 'package:cake_wallet/src/domain/exchange/trade.dart';
import 'package:cake_wallet/src/domain/exchange/trade_request.dart';
import 'package:cake_wallet/src/domain/exchange/trade_state.dart';
import 'package:cake_wallet/src/domain/exchange/changenow/changenow_request.dart';
import 'package:cake_wallet/src/domain/exchange/exchange_provider_description.dart';
import 'package:cake_wallet/src/domain/exchange/trade_not_created_exeption.dart';
class ChangeNowExchangeProvider extends ExchangeProvider {
static const apiUri = 'https://changenow.io/api/v1';
static const apiKey = secrets.change_now_api_key;
static const _exchangeAmountUriSufix = '/exchange-amount/';
static const _transactionsUriSufix = '/transactions/';
static const _minAmountUriSufix = '/min-amount/';
String get title => 'ChangeNOW';
ExchangeProviderDescription get description =>
ExchangeProviderDescription.changeNow;
ChangeNowExchangeProvider() {
pairList = CryptoCurrency.all
.map((i) {
return CryptoCurrency.all.map((k) {
if (i == CryptoCurrency.btc && k == CryptoCurrency.xmr) {
return ExchangePair(from: i, to: k, reverse: false);
}
if (i == CryptoCurrency.xmr && k == CryptoCurrency.btc) {
return null;
}
return ExchangePair(from: i, to: k, reverse: true);
}).where((c) => c != null);
})
.expand((i) => i)
.toList();
}
Future<Limits> fetchLimits({CryptoCurrency from, CryptoCurrency to}) async {
final symbol = from.toString() + '_' + to.toString();
final url = apiUri + _minAmountUriSufix + symbol;
final response = await get(url);
final responseJSON = json.decode(response.body);
final double min = responseJSON['minAmount'];
return Limits(min: min, max: null);
}
Future<Trade> createTrade({TradeRequest request}) async {
const url = apiUri + _transactionsUriSufix + apiKey;
final _request = request as ChangeNowRequest;
final body = {
'from': _request.from.toString(),
'to': _request.to.toString(),
'address': _request.address,
'amount': _request.amount,
'refundAddress': _request.refundAddress
};
final response = await post(url,
headers: {'Content-Type': 'application/json'}, body: json.encode(body));
if (response.statusCode != 200) {
if (response.statusCode == 400) {
final responseJSON = json.decode(response.body);
final error = responseJSON['message'];
throw TradeNotCreatedException(description, description: error);
}
throw TradeNotCreatedException(description);
}
final responseJSON = json.decode(response.body);
return Trade(
id: responseJSON['id'],
from: _request.from,
to: _request.to,
provider: description,
inputAddress: responseJSON['payinAddress'],
refundAddress: responseJSON['refundAddress'],
extraId: responseJSON["payinExtraId"],
createdAt: DateTime.now(),
amount: _request.amount,
state: TradeState.created);
}
Future<Trade> findTradeById({@required String id}) async {
final url = apiUri + _transactionsUriSufix + id + '/' + apiKey;
final response = await get(url);
if (response.statusCode != 200) {
if (response.statusCode == 400) {
final responseJSON = json.decode(response.body);
final error = responseJSON['message'];
throw TradeNotFoundException(id,
provider: description, description: error);
}
throw TradeNotFoundException(id, provider: description);
}
final responseJSON = json.decode(response.body);
return Trade(
id: id,
from: CryptoCurrency.fromString(responseJSON['fromCurrency']),
to: CryptoCurrency.fromString(responseJSON['toCurrency']),
provider: description,
inputAddress: responseJSON['payinAddress'],
amount: responseJSON['expectedSendAmount'].toString(),
state: TradeState.deserialize(raw: responseJSON['status']),
extraId: responseJSON['payinExtraId'],
outputTransaction: responseJSON['payoutHash']);
}
Future<double> calculateAmount(
{CryptoCurrency from, CryptoCurrency to, double amount}) async {
final url = apiUri +
_exchangeAmountUriSufix +
amount.toString() +
'/' +
from.toString() +
'_' +
to.toString();
final response = await get(url);
final responseJSON = json.decode(response.body);
return responseJSON['estimatedAmount'];
}
}
@@ -0,0 +1,18 @@
import 'package:flutter/foundation.dart';
import 'package:cake_wallet/src/domain/common/crypto_currency.dart';
import 'package:cake_wallet/src/domain/exchange/trade_request.dart';
class ChangeNowRequest extends TradeRequest {
CryptoCurrency from;
CryptoCurrency to;
String address;
String amount;
String refundAddress;
ChangeNowRequest(
{@required this.from,
@required this.to,
@required this.address,
@required this.amount,
@required this.refundAddress});
}
@@ -0,0 +1,9 @@
import 'package:cake_wallet/src/domain/common/crypto_currency.dart';
class ExchangePair {
final CryptoCurrency from;
final CryptoCurrency to;
final bool reverse;
ExchangePair({this.from, this.to, this.reverse = true});
}
@@ -0,0 +1,22 @@
import 'package:flutter/foundation.dart';
import 'package:cake_wallet/src/domain/common/crypto_currency.dart';
import 'package:cake_wallet/src/domain/exchange/trade_request.dart';
import 'package:cake_wallet/src/domain/exchange/exchange_pair.dart';
import 'package:cake_wallet/src/domain/exchange/limits.dart';
import 'package:cake_wallet/src/domain/exchange/trade.dart';
import 'package:cake_wallet/src/domain/exchange/exchange_provider_description.dart';
abstract class ExchangeProvider {
String get title;
List<ExchangePair> pairList;
ExchangeProviderDescription description;
@override
String toString() => title;
Future<Limits> fetchLimits({CryptoCurrency from, CryptoCurrency to});
Future<Trade> createTrade({TradeRequest request});
Future<Trade> findTradeById({@required String id});
Future<double> calculateAmount(
{CryptoCurrency from, CryptoCurrency to, double amount});
}
@@ -0,0 +1,24 @@
import 'package:cake_wallet/src/domain/common/enumerable_item.dart';
class ExchangeProviderDescription extends EnumerableItem<int>
with Serializable<int> {
static const xmrto = ExchangeProviderDescription(title: 'XMR.TO', raw: 0);
static const changeNow =
ExchangeProviderDescription(title: 'ChangeNOW', raw: 1);
static ExchangeProviderDescription deserialize({int raw}) {
switch (raw) {
case 0:
return xmrto;
case 1:
return changeNow;
default:
return null;
}
}
final String title;
const ExchangeProviderDescription({this.title, int raw})
: super(title: title, raw: raw);
}
+6
View File
@@ -0,0 +1,6 @@
class Limits {
final double min;
final double max;
const Limits({this.min, this.max});
}
+103
View File
@@ -0,0 +1,103 @@
import 'package:hive/hive.dart';
import 'package:cake_wallet/src/domain/common/crypto_currency.dart';
import 'package:cake_wallet/src/domain/exchange/exchange_provider_description.dart';
import 'package:cake_wallet/src/domain/exchange/trade_state.dart';
part 'trade.g.dart';
@HiveType()
class Trade extends HiveObject {
static const boxName = 'Trades';
@HiveField(0)
String id;
@HiveField(1)
int providerRaw;
ExchangeProviderDescription get provider =>
ExchangeProviderDescription.deserialize(raw: providerRaw);
@HiveField(2)
int fromRaw;
CryptoCurrency get from => CryptoCurrency.deserialize(raw: fromRaw);
@HiveField(3)
int toRaw;
CryptoCurrency get to => CryptoCurrency.deserialize(raw: toRaw);
@HiveField(4)
String stateRaw;
TradeState get state => TradeState.deserialize(raw: stateRaw);
@HiveField(5)
DateTime createdAt;
@HiveField(6)
DateTime expiredAt;
@HiveField(7)
String amount;
@HiveField(8)
String inputAddress;
@HiveField(9)
String extraId;
@HiveField(10)
String outputTransaction;
@HiveField(11)
String refundAddress;
@HiveField(12)
String walletId;
static Trade fromMap(Map map) {
return Trade(
id: map['id'],
provider: ExchangeProviderDescription.deserialize(raw: map['provider']),
from: CryptoCurrency.deserialize(raw: map['input']),
to: CryptoCurrency.deserialize(raw: map['output']),
createdAt: map['date'] != null
? DateTime.fromMillisecondsSinceEpoch(map['date'])
: null,
amount: map['amount'],
walletId: map['wallet_id']);
}
Trade(
{this.id,
ExchangeProviderDescription provider,
CryptoCurrency from,
CryptoCurrency to,
TradeState state,
this.createdAt,
this.expiredAt,
this.amount,
this.inputAddress,
this.extraId,
this.outputTransaction,
this.refundAddress,
this.walletId})
: providerRaw = provider?.raw,
fromRaw = from?.raw,
toRaw = to?.raw,
stateRaw = state?.raw;
Map<String, dynamic> toMap() {
return {
'id': id,
'provider': provider.serialize(),
'input': from.serialize(),
'output': to.serialize(),
'date': createdAt != null ? createdAt.millisecondsSinceEpoch : null,
'amount': amount,
'wallet_id': walletId
};
}
}
@@ -0,0 +1,19 @@
import 'package:cake_wallet/src/domain/exchange/exchange_provider_description.dart';
import 'package:cake_wallet/generated/i18n.dart';
class TradeNotCreatedException implements Exception {
ExchangeProviderDescription provider;
String description;
TradeNotCreatedException(this.provider, {this.description = ''});
@override
String toString() {
var text = provider != null
? S.current.trade_for_not_created(provider.title)
: S.current.trade_not_created;
text += ' $description';
return text;
}
}
@@ -0,0 +1,20 @@
import 'package:cake_wallet/src/domain/exchange/exchange_provider_description.dart';
import 'package:cake_wallet/generated/i18n.dart';
class TradeNotFoundException implements Exception {
String tradeId;
ExchangeProviderDescription provider;
String description;
TradeNotFoundException(this.tradeId, {this.provider, this.description = ''});
@override
String toString() {
var text = tradeId != null && provider != null
? S.current.trade_id_not_found(tradeId, provider.title)
: S.current.trade_not_found;
text += ' $description';
return text;
}
}
@@ -0,0 +1 @@
abstract class TradeRequest {}
+68
View File
@@ -0,0 +1,68 @@
import 'package:flutter/foundation.dart';
import 'package:cake_wallet/src/domain/common/enumerable_item.dart';
class TradeState extends EnumerableItem<String> with Serializable<String> {
static const pending = TradeState(raw: 'pending', title: 'Pending');
static const confirming = TradeState(raw: 'confirming', title: 'Confirming');
static const trading = TradeState(raw: 'trading', title: 'Trading');
static const traded = TradeState(raw: 'traded', title: 'Traded');
static const complete = TradeState(raw: 'complete', title: 'Complete');
static const toBeCreated =
TradeState(raw: 'TO_BE_CREATED', title: 'To be created');
static const unpaid = TradeState(raw: 'UNPAID', title: 'Unpaid');
static const underpaid = TradeState(raw: 'UNDERPAID', title: 'Underpaid');
static const paidUnconfirmed =
TradeState(raw: 'PAID_UNCONFIRMED', title: 'Paid unconfirmed');
static const paid = TradeState(raw: 'PAID', title: 'Paid');
static const btcSent = TradeState(raw: 'BTC_SENT', title: 'Btc sent');
static const timeout = TradeState(raw: 'TIMED_OUT', title: 'Timeout');
static const notFound = TradeState(raw: 'NOT_FOUND', title: 'Not found');
static const created = TradeState(raw: 'created', title: 'Created');
static const finished = TradeState(raw: 'finished', title: 'Finished');
static const waiting = TradeState(raw: 'waiting', title: 'Waiting');
static TradeState deserialize({String raw}) {
switch (raw) {
case 'pending':
return pending;
case 'confirming':
return confirming;
case 'trading':
return trading;
case 'traded':
return traded;
case 'complete':
return complete;
case 'TO_BE_CREATED':
return toBeCreated;
case 'UNPAID':
return unpaid;
case 'UNDERPAID':
return underpaid;
case 'PAID_UNCONFIRMED':
return paidUnconfirmed;
case 'PAID':
return paid;
case 'BTC_SENT':
return btcSent;
case 'TIMED_OUT':
return timeout;
case 'created':
return created;
case 'finished':
return finished;
case 'waiting':
return waiting;
default:
return null;
}
}
const TradeState({@required String raw, @required String title})
: super(raw: raw, title: title);
operator ==(o) => o is TradeState && o.raw == raw;
@override
int get hashCode => raw.hashCode ^ title.hashCode;
}
@@ -0,0 +1,170 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart';
import 'package:cake_wallet/src/domain/common/crypto_currency.dart';
import 'package:cake_wallet/src/domain/exchange/exchange_pair.dart';
import 'package:cake_wallet/src/domain/exchange/exchange_provider.dart';
import 'package:cake_wallet/src/domain/exchange/limits.dart';
import 'package:cake_wallet/src/domain/exchange/trade.dart';
import 'package:cake_wallet/src/domain/exchange/trade_request.dart';
import 'package:cake_wallet/src/domain/exchange/trade_state.dart';
import 'package:cake_wallet/src/domain/exchange/xmrto/xmrto_trade_request.dart';
import 'package:cake_wallet/src/domain/exchange/trade_not_created_exeption.dart';
import 'package:cake_wallet/src/domain/exchange/exchange_provider_description.dart';
import 'package:cake_wallet/src/domain/exchange/trade_not_found_exeption.dart';
class XMRTOExchangeProvider extends ExchangeProvider {
static const userAgent = 'CakeWallet/XMR iOS';
static const originalApiUri = 'https://xmr.to/api/v2/xmr2btc';
static const proxyApiUri = 'https://xmrproxy.net/api/v2/xmr2btc';
static const _orderParameterUriSufix = '/order_parameter_query';
static const _orderStatusUriSufix = '/order_status_query/';
static const _orderCreateUriSufix = '/order_create/';
static String _apiUri = '';
static Future<String> getApiUri() async {
if (_apiUri != null && _apiUri.isNotEmpty) {
return _apiUri;
}
const url = originalApiUri + _orderParameterUriSufix;
final response =
await get(url, headers: {'Content-Type': 'application/json'});
_apiUri = response.statusCode == 403 ? proxyApiUri : originalApiUri;
return _apiUri;
}
String get title => 'XMR.TO';
ExchangeProviderDescription get description =>
ExchangeProviderDescription.xmrto;
List<ExchangePair> pairList = [
ExchangePair(
from: CryptoCurrency.xmr, to: CryptoCurrency.btc, reverse: false)
];
double _rate = 0;
Future<Limits> fetchLimits({CryptoCurrency from, CryptoCurrency to}) async {
final url = await getApiUri() + _orderParameterUriSufix;
final response = await get(url);
if (response.statusCode != 200) {
return Limits(min: 0, max: 0);
}
final responseJSON = json.decode(response.body);
final double min = responseJSON['lower_limit'];
final double max = responseJSON['upper_limit'];
return Limits(min: min, max: max);
}
Future<Trade> createTrade({TradeRequest request}) async {
final _request = request as XMRTOTradeRequest;
final url = await getApiUri() + _orderCreateUriSufix;
final body = {
'btc_amount': _request.amount,
'btc_dest_address': _request.address
};
final response = await post(url,
headers: {'Content-Type': 'application/json'}, body: json.encode(body));
if (response.statusCode != 201) {
if (response.statusCode == 400) {
final responseJSON = json.decode(response.body);
throw TradeNotCreatedException(description,
description: responseJSON['error_msg']);
}
throw TradeNotCreatedException(description);
}
final responseJSON = json.decode(response.body);
final uuid = responseJSON["uuid"];
return Trade(
id: uuid,
provider: description,
from: _request.from,
to: _request.to,
state: TradeState.created,
amount: _request.amount,
createdAt: DateTime.now());
}
Future<Trade> findTradeById({@required String id}) async {
const headers = {
'Content-Type': 'application/json',
'User-Agent': userAgent
};
final url = await getApiUri() + _orderStatusUriSufix;
final body = {'uuid': id};
final response = await post(url, headers: headers, body: json.encode(body));
if (response.statusCode != 200) {
if (response.statusCode == 400) {
final responseJSON = json.decode(response.body);
final error = responseJSON['error_msg'];
throw TradeNotFoundException(id,
provider: description, description: error);
}
throw TradeNotFoundException(id, provider: description);
}
final responseJSON = json.decode(response.body);
final address = responseJSON['xmr_receiving_integrated_address'];
final paymentId = responseJSON['xmr_required_payment_id_short'];
final amount = responseJSON['xmr_amount_total'].toString();
final stateRaw = responseJSON['state'];
final expiredAtRaw = responseJSON['expires_at'];
final expiredAt = DateTime.parse(expiredAtRaw).toLocal();
final outputTransaction = responseJSON['btc_transaction_id'];
final state = TradeState.deserialize(raw: stateRaw);
return Trade(
id: id,
provider: description,
from: CryptoCurrency.xmr,
to: CryptoCurrency.btc,
inputAddress: address,
extraId: paymentId,
expiredAt: expiredAt,
amount: amount,
state: state,
outputTransaction: outputTransaction);
}
Future<double> calculateAmount(
{CryptoCurrency from, CryptoCurrency to, double amount}) async {
if (from != CryptoCurrency.xmr && to != CryptoCurrency.btc) {
return 0;
}
if (_rate == null || _rate == 0) {
_rate = await _fetchRates();
}
final double result = _rate * amount;
return double.parse(result.toStringAsFixed(12));
}
Future<double> _fetchRates() async {
try {
final url = await getApiUri() + _orderParameterUriSufix;
final response =
await get(url, headers: {'Content-Type': 'application/json'});
final responseJSON = json.decode(response.body);
double btcprice = responseJSON['price'];
double price = 1 / btcprice;
return price;
} catch (e) {
print(e.toString());
return 0.0;
}
}
}
@@ -0,0 +1,18 @@
import 'package:flutter/foundation.dart';
import 'package:cake_wallet/src/domain/common/crypto_currency.dart';
import 'package:cake_wallet/src/domain/exchange/trade_request.dart';
class XMRTOTradeRequest extends TradeRequest {
final CryptoCurrency from;
final CryptoCurrency to;
final String amount;
final String address;
final String refundAddress;
XMRTOTradeRequest(
{@required this.from,
@required this.to,
@required this.amount,
@required this.address,
@required this.refundAddress});
}
+16
View File
@@ -0,0 +1,16 @@
import 'package:cw_monero/structs/account_row.dart';
class Account {
final int id;
final String label;
Account({this.id, this.label});
Account.fromMap(Map map)
: this.id = map['id'] == null ? 0 : int.parse(map['id']),
this.label = map['label'] ?? '';
Account.fromRow(AccountRow row)
: this.id = row.getId(),
this.label = row.getLabel();
}
+67
View File
@@ -0,0 +1,67 @@
import 'package:rxdart/rxdart.dart';
import 'package:cw_monero/account_list.dart' as accountListAPI;
import 'package:cake_wallet/src/domain/monero/account.dart';
class AccountList {
get accounts => _accounts.stream;
BehaviorSubject<List<Account>> _accounts;
bool _isRefreshing;
bool _isUpdating;
AccountList() {
_isRefreshing = false;
_isUpdating = false;
_accounts = BehaviorSubject<List<Account>>();
}
Future update() async {
if (_isUpdating) {
return;
}
try {
_isUpdating = true;
await refresh();
final accounts = getAll();
_accounts.add(accounts);
_isUpdating = false;
} catch (e) {
_isUpdating = false;
throw e;
}
}
List<Account> getAll() {
return accountListAPI
.getAllAccount()
.map((accountRow) => Account.fromRow(accountRow))
.toList();
}
Future addAccount({String label}) async {
await accountListAPI.addAccount(label: label);
await update();
}
Future setLabelSubaddress({int accountIndex, String label}) async {
await accountListAPI.setLabelForAccount(accountIndex: accountIndex, label: label);
await update();
}
refresh() {
if (_isRefreshing) {
return;
}
try {
_isRefreshing = true;
accountListAPI.refreshAccounts();
_isRefreshing = false;
} catch (e) {
_isRefreshing = false;
print(e);
throw e;
}
}
}
@@ -0,0 +1,101 @@
import 'package:intl/intl.dart';
final dateFormat = DateFormat('yyyy-MM');
final dates = {
"2014-5": 18844,
"2014-6": 65406,
"2014-7": 108882,
"2014-8": 153594,
"2014-9": 198072,
"2014-10": 241088,
"2014-11": 285305,
"2014-12": 328069,
"2015-1": 372369,
"2015-2": 416505,
"2015-3": 456631,
"2015-4": 501084,
"2015-5": 543973,
"2015-6": 588326,
"2015-7": 631187,
"2015-8": 675484,
"2015-9": 719725,
"2015-10": 762463,
"2015-11": 806528,
"2015-12": 849041,
"2016-1": 892866,
"2016-2": 936736,
"2016-3": 977691,
"2016-4": 1015848,
"2016-5": 1037417,
"2016-6": 1059651,
"2016-7": 1081269,
"2016-8": 1103630,
"2016-9": 1125983,
"2016-10": 1147617,
"2016-11": 1169779,
"2016-12": 1191402,
"2017-1": 1213861,
"2017-2": 1236197,
"2017-3": 1256358,
"2017-4": 1278622,
"2017-5": 1300239,
"2017-6": 1322564,
"2017-7": 1344225,
"2017-8": 1366664,
"2017-9": 1389113,
"2017-10": 1410738,
"2017-11": 1433039,
"2017-12": 1454639,
"2018-1": 1477201,
"2018-2": 1499599,
"2018-3": 1519796,
"2018-4": 1542067,
"2018-5": 1562861,
"2018-6": 1585135,
"2018-7": 1606715,
"2018-8": 1629017,
"2018-9": 1651347,
"2018-10": 1673031,
"2018-11": 1695128,
"2018-12": 1716687,
"2019-1": 1738923,
"2019-2": 1761435,
"2019-3": 1781681,
"2019-4": 1803081,
"2019-5": 1824671,
"2019-6": 1847005,
"2019-7": 1868590,
"2019-8": 1888590,
"2019-9": 1898590,
};
int getHeigthByDate({DateTime date}) {
final raw = '${date.year}' + '-' + '${date.month}';
var endHeight = dates[raw] ?? 0;
int preLastYear = date.year;
int preLastMonth = date.month - 1;
if (endHeight <= 0) {
endHeight = dates.values.toList()[dates.length - 1];
final preLastDate = dateFormat.parse(dates.keys.elementAt(dates.keys.length - 2));
preLastYear = preLastDate.year;
preLastMonth = preLastDate.month;
} else {
preLastYear = date.year;
preLastMonth = date.month - 1;
}
if (preLastMonth <= 0) {
preLastMonth = 12;
preLastYear -= 1;
}
var startRaw = '$preLastYear' + '-' + '$preLastMonth';
var startHeight = dates[startRaw];
var diff = endHeight - startHeight;
var heightPerDay = diff / 30;
var daysHeight = date.day * heightPerDay.round();
var height = endHeight + daysHeight;
return height;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,12 @@
import 'package:intl/intl.dart';
const moneroAmountLength = 12;
const moneroAmountDivider = 1000000000000;
final moneroAmountFormat = NumberFormat()
..maximumFractionDigits = moneroAmountLength
..minimumFractionDigits = 1;
String moneroAmountToString({int amount}) =>
moneroAmountFormat.format(amount / moneroAmountDivider);
double moneroAmountToDouble({int amount}) => amount / moneroAmountDivider;
@@ -0,0 +1,9 @@
import 'package:flutter/foundation.dart';
import 'package:cake_wallet/src/domain/common/balance.dart';
class MoneroBalance extends Balance {
final String fullBalance;
final String unlockedBalance;
MoneroBalance({@required this.fullBalance, @required this.unlockedBalance});
}
@@ -0,0 +1,13 @@
import 'package:cake_wallet/src/domain/common/transaction_creation_credentials.dart';
import 'package:cake_wallet/src/domain/common/transaction_priority.dart';
class MoneroTransactionCreationCredentials
extends TransactionCreationCredentials {
final String address;
final String paymentId;
final String amount;
final TransactionPriority priority;
MoneroTransactionCreationCredentials(
{this.address, this.paymentId, this.priority, this.amount});
}
@@ -0,0 +1,65 @@
import 'dart:core';
import 'package:flutter/services.dart';
import 'package:rxdart/rxdart.dart';
import 'package:cw_monero/transaction_history.dart' as moneroTransactionHistory;
import 'package:cake_wallet/src/domain/common/transaction_history.dart';
import 'package:cake_wallet/src/domain/common/transaction_info.dart';
List<TransactionInfo> _getAllTransactions(_) => moneroTransactionHistory
.getAllTransations()
.map((row) => TransactionInfo.fromRow(row))
.toList();
class MoneroTransactionHistory extends TransactionHistory {
get transactions => _transactions.stream;
BehaviorSubject<List<TransactionInfo>> _transactions;
bool _isUpdating = false;
bool _isRefreshing = false;
bool _needToCheckForRefresh = false;
MoneroTransactionHistory()
: _transactions = BehaviorSubject<List<TransactionInfo>>.seeded([]);
Future update() async {
if (_isUpdating) {
return;
}
try {
_isUpdating = true;
await refresh();
_transactions.value = await getAll(force: true);
_isUpdating = false;
if (!_needToCheckForRefresh) {
_needToCheckForRefresh = true;
}
} catch (e) {
_isUpdating = false;
print(e);
throw e;
}
}
Future<List<TransactionInfo>> getAll({bool force = false}) async =>
_getAllTransactions(null);
Future<int> count() async => moneroTransactionHistory.countOfTransactions();
Future refresh() async {
if (_isRefreshing) {
return;
}
try {
_isRefreshing = true;
moneroTransactionHistory.refreshTransactions();
_isRefreshing = false;
} on PlatformException catch (e) {
_isRefreshing = false;
print(e);
throw e;
}
}
}
+396
View File
@@ -0,0 +1,396 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:hive/hive.dart';
import 'package:rxdart/rxdart.dart';
import 'package:cw_monero/wallet.dart' as moneroWallet;
import 'package:cw_monero/transaction_history.dart' as transactionHistory;
import 'package:cake_wallet/src/domain/common/wallet_info.dart';
import 'package:cake_wallet/src/domain/common/wallet.dart';
import 'package:cake_wallet/src/domain/common/sync_status.dart';
import 'package:cake_wallet/src/domain/common/transaction_history.dart';
import 'package:cake_wallet/src/domain/common/transaction_creation_credentials.dart';
import 'package:cake_wallet/src/domain/common/pending_transaction.dart';
import 'package:cake_wallet/src/domain/common/wallet_type.dart';
import 'package:cake_wallet/src/domain/common/node.dart';
import 'package:cake_wallet/src/domain/monero/monero_amount_format.dart';
import 'package:cake_wallet/src/domain/monero/account.dart';
import 'package:cake_wallet/src/domain/monero/account_list.dart';
import 'package:cake_wallet/src/domain/monero/subaddress_list.dart';
import 'package:cake_wallet/src/domain/monero/monero_transaction_creation_credentials.dart';
import 'package:cake_wallet/src/domain/monero/monero_transaction_history.dart';
import 'package:cake_wallet/src/domain/monero/subaddress.dart';
import 'package:cake_wallet/src/domain/common/balance.dart';
import 'package:cake_wallet/src/domain/monero/monero_balance.dart';
const monero_block_size = 1000;
class MoneroWallet extends Wallet {
static Future<MoneroWallet> createdWallet(
{Box<WalletInfo> walletInfoSource,
String name,
bool isRecovery = false,
int restoreHeight = 0}) async {
const type = WalletType.monero;
final id = walletTypeToString(type).toLowerCase() + '_' + name;
final walletInfo = WalletInfo(
id: id,
name: name,
type: type,
isRecovery: isRecovery,
restoreHeight: restoreHeight);
await walletInfoSource.add(walletInfo);
return await configured(
walletInfo: walletInfo, walletInfoSource: walletInfoSource);
}
static Future<MoneroWallet> load(
Box<WalletInfo> walletInfoSource, String name, WalletType type) async {
final id = walletTypeToString(type).toLowerCase() + '_' + name;
final walletInfo =
walletInfoSource.values.firstWhere((info) => info.id == id, orElse: () => null);
return await configured(
walletInfoSource: walletInfoSource, walletInfo: walletInfo);
}
static Future<MoneroWallet> configured(
{@required Box<WalletInfo> walletInfoSource,
@required WalletInfo walletInfo}) async {
final wallet = MoneroWallet(
walletInfoSource: walletInfoSource, walletInfo: walletInfo);
if (walletInfo.isRecovery) {
await wallet.setRecoveringFromSeed();
if (walletInfo.restoreHeight != null) {
await wallet.setRefreshFromBlockHeight(
height: walletInfo.restoreHeight);
}
}
return wallet;
}
WalletType getType() => WalletType.monero;
bool get isRecovery => walletInfo.isRecovery;
Observable<SyncStatus> get syncStatus => _syncStatus.stream;
Observable<Balance> get onBalanceChange => _onBalanceChange.stream;
Observable<Account> get onAccountChange => _account.stream;
Observable<String> get onNameChange => _name.stream;
Observable<String> get onAddressChange => _address.stream;
Observable<Subaddress> get subaddress => _subaddress.stream;
Account get account => _account.value;
String get address => _address.value;
String get name => _name.value;
Box<WalletInfo> walletInfoSource;
WalletInfo walletInfo;
BehaviorSubject<Account> _account;
BehaviorSubject<MoneroBalance> _onBalanceChange;
BehaviorSubject<SyncStatus> _syncStatus;
BehaviorSubject<String> _name;
BehaviorSubject<String> _address;
BehaviorSubject<Subaddress> _subaddress;
int _cachedBlockchainHeight;
bool _isSaving;
int _lastSaveTime;
int _lastRefreshTime;
int _refreshHeight;
int _lastSyncHeight;
TransactionHistory _cachedTransactionHistory;
SubaddressList _cachedSubaddressList;
AccountList _cachedAccountList;
MoneroWallet(
{this.walletInfoSource, this.walletInfo}) {
_cachedBlockchainHeight = 0;
_isSaving = false;
_lastSaveTime = 0;
_lastRefreshTime = 0;
_refreshHeight = 0;
_lastSyncHeight = 0;
_name = BehaviorSubject<String>();
_address = BehaviorSubject<String>();
_syncStatus = BehaviorSubject<SyncStatus>();
_onBalanceChange = BehaviorSubject<MoneroBalance>();
_account = BehaviorSubject<Account>()..add(Account(id: 0));
_subaddress = BehaviorSubject<Subaddress>();
setListeners();
}
Future updateInfo() async {
_name.value = await getName();
final acccountList = getAccountList();
await acccountList.refresh();
_account.value = acccountList.getAll().first;
final subaddressList = getSubaddress();
await subaddressList.refresh(
accountIndex: _account.value != null ? _account.value.id : 0);
final subaddresses = subaddressList.getAll();
_subaddress.value = subaddresses.first;
_address.value = await getAddress();
}
Future<String> getFilename() async => moneroWallet.getFilename();
Future<String> getName() async => getFilename()
.then((filename) => filename.split('/'))
.then((splitted) => splitted.last);
Future<String> getAddress() async => moneroWallet.getAddress(
accountIndex: _account.value.id, addressIndex: _subaddress.value.id);
Future<String> getSeed() async => moneroWallet.getSeed();
Future<String> getFullBalance() async => moneroAmountToString(
amount: moneroWallet.getFullBalance(accountIndex: _account.value.id));
Future<String> getUnlockedBalance() async => moneroAmountToString(
amount: moneroWallet.getUnlockedBalance(accountIndex: _account.value.id));
Future<int> getCurrentHeight() async => moneroWallet.getCurrentHeight();
Future<int> getNodeHeight() async => moneroWallet.getNodeHeight();
Future<Map<String, String>> getKeys() async => {
'publicViewKey': moneroWallet.getPublicViewKey(),
'privateViewKey': moneroWallet.getSecretViewKey(),
'publicSpendKey': moneroWallet.getPublicSpendKey(),
'privateSpendKey': moneroWallet.getSecretSpendKey()
};
Future close() async {
moneroWallet.closeListeners();
moneroWallet.closeCurrentWallet();
}
Future connectToNode(
{Node node, bool useSSL = false, bool isLightWallet = false}) async {
try {
_syncStatus.value = ConnectingSyncStatus();
await moneroWallet.setupNode(
address: node.uri,
login: node.login,
password: node.password,
useSSL: useSSL,
isLightWallet: isLightWallet);
_syncStatus.value = ConnectedSyncStatus();
} catch (e) {
_syncStatus.value = FailedSyncStatus();
print(e);
}
}
Future startSync() async {
try {
_syncStatus.value = StartingSyncStatus();
moneroWallet.startRefresh();
} on PlatformException catch (e) {
_syncStatus.value = FailedSyncStatus();
print(e);
throw e;
}
}
TransactionHistory getHistory() {
if (_cachedTransactionHistory == null) {
_cachedTransactionHistory = MoneroTransactionHistory();
}
return _cachedTransactionHistory;
}
SubaddressList getSubaddress() {
if (_cachedSubaddressList == null) {
_cachedSubaddressList = SubaddressList();
}
return _cachedSubaddressList;
}
AccountList getAccountList() {
if (_cachedAccountList == null) {
_cachedAccountList = AccountList();
}
return _cachedAccountList;
}
Future askForSave() async {
final diff = DateTime.now().millisecondsSinceEpoch - _lastSaveTime;
if (_lastSaveTime != 0 && diff < 120000) {
return;
}
await store();
}
Future<int> getNodeHeightOrUpdate(int baseHeight) async {
if (_cachedBlockchainHeight < baseHeight) {
_cachedBlockchainHeight = await getNodeHeight();
}
return _cachedBlockchainHeight;
}
Future<PendingTransaction> createTransaction(
TransactionCreationCredentials credentials) async {
MoneroTransactionCreationCredentials _credentials =
credentials as MoneroTransactionCreationCredentials;
final transactionDescription = await transactionHistory.createTransaction(
address: _credentials.address,
paymentId: _credentials.paymentId,
amount: _credentials.amount,
priorityRaw: _credentials.priority.serialize(),
accountIndex: _account.value.id);
return PendingTransaction.fromTransactionDescription(
transactionDescription);
}
setRecoveringFromSeed() =>
moneroWallet.setRecoveringFromSeed(isRecovery: true);
setRefreshFromBlockHeight({int height}) =>
moneroWallet.setRefreshFromBlockHeight(height: height);
Future<bool> isConnected() async => moneroWallet.isConnected();
Future setAsRecovered() async {
walletInfo.isRecovery = false;
await walletInfo.save();
}
Future askForUpdateBalance() async {
final fullBalance = await getFullBalance();
final unlockedBalance = await getUnlockedBalance();
final needToChange = _onBalanceChange.value != null
? _onBalanceChange.value.fullBalance != fullBalance ||
_onBalanceChange.value.unlockedBalance != unlockedBalance
: true;
if (!needToChange) {
return;
}
_onBalanceChange.add(MoneroBalance(
fullBalance: fullBalance, unlockedBalance: unlockedBalance));
}
Future askForUpdateTransactionHistory() async => await getHistory().update();
Future rescan({int restoreHeight = 0}) async {
_syncStatus.value = StartingSyncStatus();
setRefreshFromBlockHeight(height: restoreHeight);
moneroWallet.rescanBlockchainAsync();
_syncStatus.value = StartingSyncStatus();
}
changeCurrentSubaddress(Subaddress subaddress) =>
_subaddress.value = subaddress;
changeAccount(Account account) {
_account.add(account);
getSubaddress()
.refresh(accountIndex: account.id)
.then((_) => getSubaddress().getAll())
.then((subaddresses) => _subaddress.value = subaddresses[0]);
}
Future store() async {
if (_isSaving) {
return;
}
try {
_isSaving = true;
moneroWallet.store();
_isSaving = false;
} on PlatformException catch (e) {
print(e);
_isSaving = false;
throw e;
}
}
setListeners() => moneroWallet.setListeners(
_onNewBlock, _onNeedToRefresh, _onNewTransaction);
Future _onNewBlock(int height) async {
try {
final nodeHeight = await getNodeHeightOrUpdate(height);
if (isRecovery && _refreshHeight <= 0) {
_refreshHeight = height;
}
if (isRecovery &&
(_lastSyncHeight == 0 ||
(height - _lastSyncHeight) > monero_block_size)) {
_lastSyncHeight = height;
askForUpdateBalance();
askForUpdateTransactionHistory();
}
if (height > 0 && ((nodeHeight - height) < monero_block_size)) {
_syncStatus.add(SyncedSyncStatus());
} else {
_syncStatus.add(SyncingSyncStatus(height, nodeHeight, _refreshHeight));
}
} catch (e) {
print(e);
}
}
Future _onNeedToRefresh() async {
try {
final currentHeight = await getCurrentHeight();
final nodeHeight = await getNodeHeightOrUpdate(currentHeight);
// no blocks - maybe we're not connected to the node ?
if (currentHeight <= 1 || nodeHeight == 0) {
return;
}
if (_syncStatus.value is FailedSyncStatus) {
return;
}
askForUpdateBalance();
_syncStatus.add(SyncedSyncStatus());
if (isRecovery) {
askForUpdateTransactionHistory();
}
if (isRecovery && (nodeHeight - currentHeight < monero_block_size)) {
await setAsRecovered();
}
final now = DateTime.now().millisecondsSinceEpoch;
final diff = now - _lastRefreshTime;
if (diff >= 0 && diff < 60000) {
return;
}
await store();
_lastRefreshTime = now;
} catch (e) {
print(e);
}
}
Future _onNewTransaction() async {
await askForUpdateBalance();
await askForUpdateTransactionHistory();
}
}
@@ -0,0 +1,160 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:path_provider/path_provider.dart';
import 'package:cw_monero/wallet_manager.dart' as moneroWalletManager;
import 'package:cake_wallet/src/domain/common/wallet_info.dart';
import 'package:cake_wallet/src/domain/common/wallet_type.dart';
import 'package:cake_wallet/src/domain/common/wallets_manager.dart';
import 'package:cake_wallet/src/domain/common/wallet.dart';
import 'package:cake_wallet/src/domain/monero/monero_wallet.dart';
import 'package:cake_wallet/src/domain/common/wallet_description.dart';
Future<String> pathForWallet({String name}) async {
final directory = await getApplicationDocumentsDirectory();
final pathDir = directory.path + '/$name';
final dir = Directory(pathDir);
if (!await dir.exists()) {
await dir.create();
}
return pathDir + '/$name';
}
class MoneroWalletsManager extends WalletsManager {
static const type = WalletType.monero;
Box<WalletInfo> walletInfoSource;
MoneroWalletsManager({@required this.walletInfoSource});
Future<Wallet> create(String name, String password) async {
try {
const isRecovery = false;
final path = await pathForWallet(name: name);
await moneroWalletManager.createWallet(path: path, password: password);
final wallet = await MoneroWallet.createdWallet(
walletInfoSource: walletInfoSource, name: name, isRecovery: isRecovery)
..updateInfo();
return wallet;
} catch (e) {
print('MoneroWalletsManager Error: $e');
throw e;
}
}
Future<Wallet> restoreFromSeed(
String name, String password, String seed, int restoreHeight) async {
try {
const isRecovery = true;
final path = await pathForWallet(name: name);
await moneroWalletManager.restoreFromSeed(
path: path,
password: password,
seed: seed,
restoreHeight: restoreHeight);
return await MoneroWallet.createdWallet(
walletInfoSource: walletInfoSource,
name: name,
isRecovery: isRecovery,
restoreHeight: restoreHeight)
..updateInfo();
} catch (e) {
print('MoneroWalletsManager Error: $e');
throw e;
}
}
Future<Wallet> restoreFromKeys(
String name,
String password,
int restoreHeight,
String address,
String viewKey,
String spendKey) async {
try {
const isRecovery = true;
final path = await pathForWallet(name: name);
await moneroWalletManager.restoreFromKeys(
path: path,
password: password,
restoreHeight: restoreHeight,
address: address,
viewKey: viewKey,
spendKey: spendKey);
final wallet = await MoneroWallet.createdWallet(
walletInfoSource: walletInfoSource,
name: name,
isRecovery: isRecovery,
restoreHeight: restoreHeight)
..updateInfo();
return wallet;
} catch (e) {
print('MoneroWalletsManager Error: $e');
throw e;
}
}
Future<Wallet> openWallet(String name, String password) async {
try {
final path = await pathForWallet(name: name);
await moneroWalletManager.openWallet(path: path, password: password);
final wallet = await MoneroWallet.load(walletInfoSource, name, type)
..updateInfo();
return wallet;
} catch (e) {
print('MoneroWalletsManager Error: $e');
throw e;
}
}
Future<bool> isWalletExit(String name) async {
try {
final path = await pathForWallet(name: name);
return moneroWalletManager.isWalletExist(path: path);
} catch (e) {
print('MoneroWalletsManager Error: $e');
throw e;
}
}
Future remove(WalletDescription wallet) async {
final dir = await getApplicationDocumentsDirectory();
final root = dir.path.replaceAll('app_flutter', 'files');
final walletFilePath = root + '/cw_monero/' + wallet.name;
final keyPath = walletFilePath + '.keys';
final addressFilePath = walletFilePath + '.address.txt';
final walletFile = File(walletFilePath);
final keyFile = File(keyPath);
final addressFile = File(addressFilePath);
if (await walletFile.exists()) {
await walletFile.delete();
}
if (await keyFile.exists()) {
await keyFile.delete();
}
if (await addressFile.exists()) {
await addressFile.delete();
}
final id =
walletTypeToString(wallet.type).toLowerCase() + '_' + wallet.name;
final info = walletInfoSource.values.firstWhere((info) => info.id == id, orElse: () => null);
await info?.delete();
}
}
+19
View File
@@ -0,0 +1,19 @@
import 'package:cw_monero/structs/subaddress_row.dart';
class Subaddress {
final int id;
final String address;
final String label;
Subaddress({this.id, this.address, this.label});
Subaddress.fromMap(Map map)
: this.id = map['id'] == null ? 0 : int.parse(map['id']),
this.address = map['address'] ?? '',
this.label = map['label'] ?? '';
Subaddress.fromRow(SubaddressRow row)
: this.id = row.getId(),
this.address = row.getAddress(),
this.label = row.getLabel();
}
@@ -0,0 +1,70 @@
import 'package:flutter/services.dart';
import 'package:rxdart/rxdart.dart';
import 'package:cw_monero/subaddress_list.dart' as subaddressListAPI;
import 'package:cake_wallet/src/domain/monero/subaddress.dart';
class SubaddressList {
get subaddresses => _subaddress.stream;
BehaviorSubject<List<Subaddress>> _subaddress;
bool _isRefreshing;
bool _isUpdating;
SubaddressList() {
_isRefreshing = false;
_isUpdating = false;
_subaddress = BehaviorSubject<List<Subaddress>>();
}
Future update({int accountIndex}) async {
if (_isUpdating) {
return;
}
try {
_isUpdating = true;
await refresh(accountIndex: accountIndex);
final subaddresses = getAll();
_subaddress.add(subaddresses);
_isUpdating = false;
} catch (e) {
_isUpdating = false;
throw e;
}
}
List<Subaddress> getAll() {
return subaddressListAPI
.getAllSubaddresses()
.map((subaddressRow) => Subaddress.fromRow(subaddressRow))
.toList();
}
Future addSubaddress({int accountIndex, String label}) async {
await subaddressListAPI.addSubaddress(accountIndex: accountIndex, label: label);
await update(accountIndex: accountIndex);
}
Future setLabelSubaddress(
{int accountIndex, int addressIndex, String label}) async {
await subaddressListAPI.setLabelForSubaddress(
accountIndex: accountIndex, addressIndex: addressIndex, label: label);
await update();
}
Future refresh({int accountIndex}) async {
if (_isRefreshing) {
return;
}
try {
_isRefreshing = true;
subaddressListAPI.refreshSubaddresses(accountIndex: accountIndex);
_isRefreshing = false;
} on PlatformException catch (e) {
_isRefreshing = false;
print(e);
throw e;
}
}
}
@@ -0,0 +1,16 @@
import 'package:hive/hive.dart';
part 'transaction_description.g.dart';
@HiveType()
class TransactionDescription extends HiveObject {
static const boxName = 'TransactionDescriptions';
@HiveField(0)
String id;
@HiveField(1)
String recipientAddress;
TransactionDescription({this.id, this.recipientAddress});
}
+46
View File
@@ -0,0 +1,46 @@
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';
class UserService {
final FlutterSecureStorage secureStorage;
final SharedPreferences sharedPreferences;
UserService({this.sharedPreferences, this.secureStorage});
Future setPassword(String password) async {
final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword);
try {
final encodedPassord = encodedPinCode(pin: password);
await secureStorage.write(key: key, value: encodedPassord);
} catch (e) {
print(e);
}
}
Future<bool> canAuthenticate() async {
final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword);
final sharedPreferences = await SharedPreferences.getInstance();
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;
}
}
@@ -0,0 +1,145 @@
import 'dart:async';
import 'package:cake_wallet/src/domain/common/wallet_info.dart';
import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:uuid/uuid.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:cake_wallet/src/domain/common/encrypt.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/wallets_manager.dart';
import 'package:cake_wallet/src/domain/common/secret_store_key.dart';
import 'package:cake_wallet/src/domain/common/wallet_type.dart';
import 'package:cake_wallet/src/domain/monero/monero_wallets_manager.dart';
import 'package:cake_wallet/src/domain/services/wallet_service.dart';
class WalletIsExistException implements Exception {
String name;
WalletIsExistException(this.name);
@override
String toString() => "Wallet with name $name is already exist!";
}
class WalletListService {
final FlutterSecureStorage secureStorage;
final WalletService walletService;
final Box<WalletInfo> walletInfoSource;
final SharedPreferences sharedPreferences;
WalletsManager walletsManager;
WalletListService(
{this.secureStorage,
this.walletInfoSource,
this.walletsManager,
@required this.walletService,
@required this.sharedPreferences});
Future<List<WalletDescription>> getAll() async => walletInfoSource.values
.map((info) => WalletDescription(name: info.name, type: info.type))
.toList();
Future create(String name) async {
if (await walletsManager.isWalletExit(name)) {
throw WalletIsExistException(name);
}
if (walletService.currentWallet != null) {
await walletService.close();
}
final password = Uuid().v4();
await saveWalletPassword(password: password, walletName: name);
final wallet = await walletsManager.create(name, password);
await onWalletChange(wallet);
}
Future restoreFromSeed(String name, String seed, int restoreHeight) async {
if (await walletsManager.isWalletExit(name)) {
throw WalletIsExistException(name);
}
if (walletService.currentWallet != null) {
await walletService.close();
}
final password = Uuid().v4();
await saveWalletPassword(password: password, walletName: name);
final wallet = await walletsManager.restoreFromSeed(
name, password, seed, restoreHeight);
await onWalletChange(wallet);
}
Future restoreFromKeys(String name, int restoreHeight, String address,
String viewKey, String spendKey) async {
if (await walletsManager.isWalletExit(name)) {
throw WalletIsExistException(name);
}
if (walletService.currentWallet != null) {
await walletService.close();
}
final password = Uuid().v4();
await saveWalletPassword(password: password, walletName: name);
final wallet = await walletsManager.restoreFromKeys(
name, password, restoreHeight, address, viewKey, spendKey);
await onWalletChange(wallet);
}
Future openWallet(String name) async {
if (walletService.currentWallet != null) {
await walletService.close();
}
final password = await getWalletPassword(walletName: name);
final wallet = await walletsManager.openWallet(name, password);
await onWalletChange(wallet);
}
Future changeWalletManger({WalletType walletType}) async {
switch (walletType) {
case WalletType.monero:
walletsManager =
MoneroWalletsManager(walletInfoSource: walletInfoSource);
break;
case WalletType.none:
walletsManager = null;
break;
}
}
Future onWalletChange(Wallet wallet) async {
walletService.currentWallet = wallet;
final walletName = await wallet.getName();
await sharedPreferences.setString('current_wallet_name', walletName);
}
Future remove(WalletDescription wallet) async =>
await walletsManager.remove(wallet);
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 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);
}
}
@@ -0,0 +1,98 @@
import 'package:rxdart/rxdart.dart';
import 'package:cake_wallet/src/domain/common/balance.dart';
import 'package:cake_wallet/src/domain/common/wallet_description.dart';
import 'package:cake_wallet/src/domain/common/wallet.dart';
import 'package:cake_wallet/src/domain/common/transaction_history.dart';
import 'package:cake_wallet/src/domain/common/wallet_type.dart';
import 'package:cake_wallet/src/domain/common/sync_status.dart';
import 'package:cake_wallet/src/domain/common/transaction_creation_credentials.dart';
import 'package:cake_wallet/src/domain/common/pending_transaction.dart';
import 'package:cake_wallet/src/domain/common/node.dart';
class WalletService extends Wallet {
Observable<Wallet> get onWalletChange => _onWalletChanged.stream;
Observable<Balance> get onBalanceChange => _onBalanceChange.stream;
Observable<SyncStatus> get syncStatus => _syncStatus.stream;
Observable<String> get onAddressChange => _currentWallet.onAddressChange;
Observable<String> get onNameChange => _currentWallet.onNameChange;
String get address => _currentWallet.address;
String get name => _currentWallet.name;
SyncStatus get syncStatusValue => _syncStatus.value;
WalletType get walletType => _currentWallet.walletType;
get currentWallet => _currentWallet;
set currentWallet(Wallet wallet) {
_currentWallet = wallet;
if (wallet == null) {
return;
}
_currentWallet.onBalanceChange
.listen((wallet) => _onBalanceChange.add(wallet));
_currentWallet.syncStatus.listen((status) => _syncStatus.add(status));
_onWalletChanged.add(wallet);
final type = wallet.getType();
wallet.getName().then(
(name) => description = WalletDescription(name: name, type: type));
}
BehaviorSubject<Wallet> _onWalletChanged;
BehaviorSubject<Balance> _onBalanceChange;
BehaviorSubject<SyncStatus> _syncStatus;
Wallet _currentWallet;
WalletService() {
_currentWallet = null;
walletType = WalletType.none;
_syncStatus = BehaviorSubject<SyncStatus>();
_onBalanceChange = BehaviorSubject<Balance>();
_onWalletChanged = BehaviorSubject<Wallet>();
}
WalletDescription description;
WalletType getType() => WalletType.monero;
Future<String> getFilename() => _currentWallet.getFilename();
Future<String> getName() => _currentWallet.getName();
Future<String> getAddress() => _currentWallet.getAddress();
Future<String> getSeed() => _currentWallet.getSeed();
Future<Map<String, String>> getKeys() => _currentWallet.getKeys();
Future<String> getFullBalance() => _currentWallet.getFullBalance();
Future<String> getUnlockedBalance() => _currentWallet.getUnlockedBalance();
Future<int> getCurrentHeight() => _currentWallet.getCurrentHeight();
Future<int> getNodeHeight() => _currentWallet.getNodeHeight();
Future<bool> isConnected() => _currentWallet.isConnected();
Future close() => _currentWallet.close();
Future connectToNode({Node node, bool useSSL = false, bool isLightWallet = false}) =>
_currentWallet.connectToNode(
node: node,
useSSL: useSSL,
isLightWallet: isLightWallet);
Future startSync() => _currentWallet.startSync();
TransactionHistory getHistory() => _currentWallet.getHistory();
Future<PendingTransaction> createTransaction(
TransactionCreationCredentials credentials) =>
_currentWallet.createTransaction(credentials);
Future updateInfo() async => _currentWallet.updateInfo();
Future rescan({int restoreHeight = 0}) async => _currentWallet.rescan(restoreHeight: restoreHeight);
}
+86
View File
@@ -0,0 +1,86 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/src/domain/common/sync_status.dart';
import 'package:cake_wallet/src/domain/services/wallet_service.dart';
import 'package:cake_wallet/src/start_updating_price.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/settings/settings_store.dart';
import 'package:cake_wallet/src/stores/price/price_store.dart';
import 'package:cake_wallet/src/stores/authentication/authentication_store.dart';
import 'package:cake_wallet/src/stores/login/login_store.dart';
setReactions(
{@required SettingsStore settingsStore,
@required PriceStore priceStore,
@required SyncStore syncStore,
@required WalletStore walletStore,
@required WalletService walletService,
@required AuthenticationStore authenticationStore,
@required LoginStore loginStore}) {
connectToNode(settingsStore: settingsStore, walletStore: walletStore);
onSyncStatusChange(
syncStore: syncStore,
walletStore: walletStore,
settingsStore: settingsStore);
onCurrentWalletChange(
walletStore: walletStore,
settingsStore: settingsStore,
priceStore: priceStore);
autorun((_) async {
if (authenticationStore.state == AuthenticationState.allowed) {
await loginStore.loadCurrentWallet();
authenticationStore.state = AuthenticationState.readyToLogin;
}
});
}
connectToNode({SettingsStore settingsStore, WalletStore walletStore}) =>
reaction((_) => settingsStore.node,
(node) async => await walletStore.connectToNode(node: node));
onSyncStatusChange(
{SyncStore syncStore,
WalletStore walletStore,
SettingsStore settingsStore}) =>
reaction((_) => syncStore.status, (status) async {
if (status is ConnectedSyncStatus) {
await walletStore.startSync();
}
// Reconnect to the node if the app is not started sync after 30 seconds
if (status is StartingSyncStatus) {
await startReconnectionObserver(
syncStore: syncStore, walletStore: walletStore);
}
});
Timer _reconnectionTimer;
startReconnectionObserver({SyncStore syncStore, WalletStore walletStore}) {
if (_reconnectionTimer != null) {
_reconnectionTimer.cancel();
}
_reconnectionTimer = Timer.periodic(Duration(minutes: 1), (_) async {
try {
final isConnected = await walletStore.isConnected();
if (!isConnected) {
await walletStore.reconnect();
}
} catch (e) {
print(e);
}
});
}
onCurrentWalletChange(
{WalletStore walletStore,
SettingsStore settingsStore,
PriceStore priceStore}) =>
reaction((_) => walletStore.name, (_) {
walletStore.connectToNode(node: settingsStore.node);
startUpdatingPrice(settingsStore: settingsStore, priceStore: priceStore);
});
@@ -0,0 +1,120 @@
import 'package:provider/provider.dart';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/stores/account_list/account_list_store.dart';
import 'package:cake_wallet/src/stores/wallet/wallet_store.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
class AccountListPage extends BasePage {
String get title => S.current.accounts;
@override
Widget trailing(BuildContext context) {
final accountListStore = Provider.of<AccountListStore>(context);
return Container(
width: 28.0,
height: 28.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).selectedRowColor),
child: Stack(
alignment: Alignment.center,
children: <Widget>[
Icon(Icons.add, color: Palette.violet, size: 22.0),
ButtonTheme(
minWidth: 28.0,
height: 28.0,
child: FlatButton(
shape: CircleBorder(),
onPressed: () async {
await Navigator.of(context)
.pushNamed(Routes.accountCreation);
await accountListStore.updateAccountList();
},
child: Offstage()),
)
],
));
}
@override
Widget body(BuildContext context) {
final accountListStore = Provider.of<AccountListStore>(context);
final walletStore = Provider.of<WalletStore>(context);
final currentColor = Theme.of(context).selectedRowColor;
final notCurrentColor = Theme.of(context).backgroundColor;
return Container(
padding: EdgeInsets.only(top: 10, bottom: 20),
child: Observer(
builder: (_) {
final accounts = accountListStore.accounts;
return ListView.builder(
itemCount: accounts == null
? 0
: accounts.length,
itemBuilder: (BuildContext context, int index) {
final account = accounts[index];
return Observer(builder: (_) {
final isCurrent = walletStore.account.id == account.id;
return Slidable(
key: Key(account.id.toString()),
actionPane: SlidableDrawerActionPane(),
child: Container(
color: isCurrent ? currentColor : notCurrentColor,
child: Column(
children: <Widget>[
ListTile(
title: Text(
account.label,
style: TextStyle(
fontSize: 16.0,
color: Theme.of(context).primaryTextTheme.headline.color),
),
onTap: () {
if (isCurrent) {
return;
}
walletStore.setAccount(account);
Navigator.of(context).pop();
},
),
Divider(
color: Theme.of(context).dividerTheme.color,
height: 1.0,
)
],
),
),
secondaryActions: <Widget>[
IconSlideAction(
caption: S.of(context).edit,
color: Colors.blue,
icon: Icons.edit,
onTap: () async {
await Navigator.of(context)
.pushNamed(Routes.accountCreation, arguments: account);
// await accountListStore.updateAccountList().then((_) {
// if (isCurrent) walletStore.setAccount(accountListStore.accounts[index]);
// });
},
)
],
);
});
});
}
),
);
}
}
+110
View File
@@ -0,0 +1,110 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:provider/provider.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/stores/account_list/account_list_store.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/domain/monero/account.dart';
import 'package:cake_wallet/palette.dart';
class AccountPage extends BasePage {
String get title => 'Account';
final Account account;
AccountPage({this.account});
@override
Widget body(BuildContext context) => AccountForm(account);
}
class AccountForm extends StatefulWidget {
final Account account;
AccountForm(this.account);
@override
createState() => AccountFormState();
}
class AccountFormState extends State<AccountForm> {
final _formKey = GlobalKey<FormState>();
final _textController = TextEditingController();
@override
void initState() {
if (widget.account != null) _textController.text = widget.account.label;
super.initState();
}
@override
void dispose() {
_textController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final accountListStore = Provider.of<AccountListStore>(context);
return Form(
key: _formKey,
child: Container(
padding: EdgeInsets.all(20.0),
child: Column(
children: <Widget>[
Expanded(
child: Center(
child: TextFormField(
decoration: InputDecoration(
hintStyle: TextStyle(color: Theme.of(context).hintColor),
hintText: S.of(context).account,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Palette.cakeGreen, width: 2.0)),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).focusColor, width: 1.0))),
controller: _textController,
validator: (value) {
accountListStore.validateAccountName(value);
return accountListStore.errorMessage;
},
),
)),
Observer(
builder: (_) => LoadingPrimaryButton(
onPressed: () async {
if (!_formKey.currentState.validate()) {
return;
}
if (widget.account != null) {
await accountListStore.renameAccount(
index: widget.account.id,
label: _textController.text);
} else {
await accountListStore.addAccount(
label: _textController.text);
}
Navigator.pop(context, _textController.text);
},
text:
widget.account != null ? 'Rename' : S.of(context).add,
color: Theme.of(context)
.primaryTextTheme
.button
.backgroundColor,
borderColor: Theme.of(context)
.primaryTextTheme
.button
.decorationColor,
isLoading: accountListStore.isAccountCreating,
))
],
),
),
);
}
}
@@ -0,0 +1,261 @@
import 'package:provider/provider.dart';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/domain/common/crypto_currency.dart';
import 'package:cake_wallet/src/stores/address_book/address_book_store.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
class AddressBookPage extends BasePage {
bool get isModalBackButton => true;
String get title => S.current.address_book;
AppBarStyle get appBarStyle => AppBarStyle.withShadow;
final bool isEditable;
AddressBookPage({this.isEditable = true});
@override
Widget trailing(BuildContext context) {
if (!isEditable) {
return null;
}
final addressBookStore = Provider.of<AddressBookStore>(context);
return Container(
width: 28.0,
height: 28.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).selectedRowColor),
child: Stack(
alignment: Alignment.center,
children: <Widget>[
Icon(Icons.add, color: Palette.violet, size: 22.0),
ButtonTheme(
minWidth: 28.0,
height: 28.0,
child: FlatButton(
shape: CircleBorder(),
onPressed: () async {
await Navigator.of(context)
.pushNamed(Routes.addressBookAddContact);
await addressBookStore.updateContactList();
},
child: Offstage()),
)
],
));
}
@override
Widget body(BuildContext context) {
final addressBookStore = Provider.of<AddressBookStore>(context);
return Container(
padding: EdgeInsets.only(top: 20.0, bottom: 20.0),
child: Observer(
builder: (_) => ListView.separated(
separatorBuilder: (_, __) => Divider(
color: Theme.of(context).dividerTheme.color,
height: 1.0,
),
itemCount: addressBookStore.contactList == null
? 0
: addressBookStore.contactList.length,
itemBuilder: (BuildContext context, int index) {
final contact = addressBookStore.contactList[index];
final content = ListTile(
onTap: () async {
if (!isEditable) {
Navigator.of(context).pop(contact);
return;
}
bool isCopied = await showNameAndAddressDialog(context, contact.name, contact.address);
if (isCopied) {
Clipboard.setData(ClipboardData(text: contact.address));
Scaffold.of(context).showSnackBar(
SnackBar(
content:
Text('Copied to Clipboard'),
backgroundColor: Colors.green,
duration:
Duration(milliseconds: 1500),
),
);
}
},
leading: Container(
height: 25.0,
width: 48.0,
alignment: Alignment.center,
decoration: BoxDecoration(
color: _getCurrencyBackgroundColor(contact.type),
borderRadius: BorderRadius.circular(6.0),
),
child: Text(
contact.type.toString(),
style: TextStyle(
fontSize: 11.0,
color: _getCurrencyTextColor(contact.type),
),
),
),
title: Text(
contact.name,
style: TextStyle(
fontSize: 16.0,
color: Theme.of(context).primaryTextTheme.title.color),
),
);
return !isEditable ? content
: Slidable(
key: Key('1'),// Key(contact.id.toString()),
actionPane: SlidableDrawerActionPane(),
child: content,
secondaryActions: <Widget>[
IconSlideAction(
caption: 'Edit',
color: Colors.blue,
icon: Icons.edit,
onTap: () async {
await Navigator.of(context)
.pushNamed(Routes.addressBookAddContact, arguments: contact);
await addressBookStore.updateContactList();
},
),
IconSlideAction(
caption: 'Delete',
color: Colors.red,
icon: CupertinoIcons.delete,
onTap: () async {
await showAlertDialog(context).then((isDelete) async{
if (isDelete != null && isDelete) {
await addressBookStore.delete(contact: contact);
await addressBookStore.updateContactList();
}
});
},
),
],
dismissal: SlidableDismissal(
child: SlidableDrawerDismissal(),
onDismissed: (actionType) async {
await addressBookStore.delete(contact: contact);
await addressBookStore.updateContactList();
},
onWillDismiss: (actionType) async {
return await showAlertDialog(context);
},
),
);
}),
));
}
Color _getCurrencyBackgroundColor(CryptoCurrency currency) {
Color color;
switch (currency) {
case CryptoCurrency.xmr:
color = Palette.cakeGreenWithOpacity;
break;
case CryptoCurrency.btc:
color = Colors.orange;
break;
case CryptoCurrency.eth:
color = Colors.black;
break;
case CryptoCurrency.ltc:
color = Colors.blue[200];
break;
case CryptoCurrency.bch:
color = Colors.orangeAccent;
break;
case CryptoCurrency.dash:
color = Colors.blue;
break;
default:
color = Colors.white;
}
return color;
}
Color _getCurrencyTextColor(CryptoCurrency currency) {
Color color;
switch (currency) {
case CryptoCurrency.xmr:
color = Palette.cakeGreen;
break;
case CryptoCurrency.ltc:
color = Palette.lightBlue;
break;
default:
color = Colors.white;
}
return color;
}
Future<bool> showAlertDialog(BuildContext context) async {
return await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text(
'Remove contact',
textAlign: TextAlign.center,
),
content: const Text(
'Are you sure that you want to remove selected contact?',
textAlign: TextAlign.center,
),
actions: <Widget>[
FlatButton(
onPressed: () =>
Navigator.pop(context, false),
child: const Text('Cancel')),
FlatButton(
onPressed: () =>
Navigator.pop(context, true),
child: const Text('Remove')),
],
);
});
}
showNameAndAddressDialog(BuildContext context, String name, String address) async {
return await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
name,
textAlign: TextAlign.center,
style: TextStyle(fontWeight: FontWeight.bold),
),
content: Text(
address,
textAlign: TextAlign.center,
),
actions: <Widget>[
FlatButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text('Cancel')),
FlatButton(
onPressed: () => Navigator.of(context).pop(true),
child: Text('Copy'))
],
);
}
);
}
}
@@ -0,0 +1,254 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/domain/common/crypto_currency.dart';
import 'package:cake_wallet/src/domain/common/contact.dart';
import 'package:cake_wallet/src/stores/address_book/address_book_store.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/address_text_field.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/palette.dart';
class ContactPage extends BasePage {
String get title => S.current.contact;
final Contact contact;
ContactPage({this.contact});
@override
Widget body(BuildContext context) => ContactForm(contact);
}
class ContactForm extends StatefulWidget {
final Contact contact;
ContactForm(this.contact);
@override
createState() => ContactFormState();
}
class ContactFormState extends State<ContactForm> {
final _formKey = GlobalKey<FormState>();
final _contactNameController = TextEditingController();
final _currencyTypeController = TextEditingController();
final _addressController = TextEditingController();
CryptoCurrency _selectectCrypto = CryptoCurrency.xmr;
@override
void initState() {
super.initState();
if (widget.contact == null) {
_currencyTypeController.text = _selectectCrypto.toString();
} else {
_selectectCrypto = widget.contact.type;
_contactNameController.text = widget.contact.name;
_currencyTypeController.text = _selectectCrypto.toString();
_addressController.text = widget.contact.address;
}
}
@override
void dispose() {
_contactNameController.dispose();
_currencyTypeController.dispose();
_addressController.dispose();
super.dispose();
}
_setCurrencyType(BuildContext context) async {
String currencyType = CryptoCurrency.all[0].toString();
CryptoCurrency selectedCurrency = CryptoCurrency.all[0];
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(S.of(context).please_select),
backgroundColor: Theme.of(context).backgroundColor,
content: Container(
height: 150.0,
child: CupertinoPicker(
backgroundColor: Theme.of(context).backgroundColor,
itemExtent: 45.0,
onSelectedItemChanged: (int index) {
selectedCurrency = CryptoCurrency.all[index];
currencyType = CryptoCurrency.all[index].toString();
},
children:
List.generate(CryptoCurrency.all.length, (int index) {
return Center(
child: Text(
CryptoCurrency.all[index].toString(),
style: TextStyle(
color: Theme.of(context)
.primaryTextTheme
.caption
.color),
),
);
})),
),
actions: <Widget>[
FlatButton(
onPressed: () {
Navigator.pop(context);
},
child: Text(S.of(context).cancel)),
FlatButton(
onPressed: () {
_selectectCrypto = selectedCurrency;
_currencyTypeController.text = currencyType;
Navigator.of(context).pop();
},
child: Text(S.of(context).ok))
],
);
});
}
@override
Widget build(BuildContext context) {
final addressBookStore = Provider.of<AddressBookStore>(context);
return ScrollableWithBottomSection(
content: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
TextFormField(
style: TextStyle(
fontSize: 14.0,
color: Theme.of(context).primaryTextTheme.headline.color),
decoration: InputDecoration(
hintStyle: TextStyle(color: Theme.of(context).hintColor),
hintText: S.of(context).contact_name,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Palette.cakeGreen, width: 2.0)),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).focusColor, width: 1.0))),
controller: _contactNameController,
validator: (value) {
addressBookStore.validateContactName(value);
return addressBookStore.errorMessage;
},
),
SizedBox(height: 14.0),
Container(
child: InkWell(
onTap: () => _setCurrencyType(context),
child: IgnorePointer(
child: TextFormField(
style: TextStyle(
fontSize: 14.0,
color: Theme.of(context)
.primaryTextTheme
.headline
.color),
decoration: InputDecoration(
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Palette.cakeGreen,
width: 2.0)),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).focusColor,
width: 1.0))),
controller: _currencyTypeController,
),
),
),
),
SizedBox(height: 14.0),
AddressTextField(
controller: _addressController,
options: [AddressTextFieldOption.qrCode],
validator: (value) {
addressBookStore.validateAddress(value,
cryptoCurrency: _selectectCrypto);
return addressBookStore.errorMessage;
},
)
],
),
),
bottomSection: Row(
children: <Widget>[
Expanded(
child: PrimaryButton(
onPressed: () {
setState(() {
_selectectCrypto = CryptoCurrency.xmr;
_contactNameController.text = '';
_currencyTypeController.text =
_selectectCrypto.toString();
_addressController.text = '';
});
},
text: S.of(context).reset,
color:
Theme.of(context).accentTextTheme.button.backgroundColor,
borderColor:
Theme.of(context).accentTextTheme.button.decorationColor),
),
SizedBox(width: 20),
Expanded(
child: PrimaryButton(
onPressed: () async {
if (!_formKey.currentState.validate()) {
return;
}
try {
if (widget.contact == null) {
final newContact = Contact(
name: _contactNameController.text,
address: _addressController.text,
type: _selectectCrypto);
await addressBookStore.add(contact: newContact);
} else {
widget.contact.name = _contactNameController.text;
widget.contact.address = _addressController.text;
widget.contact.updateCryptoCurrency(currency: _selectectCrypto);
await addressBookStore.update(contact: widget.contact);
}
Navigator.pop(context);
} catch (e) {
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
e.toString(),
textAlign: TextAlign.center,
),
actions: <Widget>[
FlatButton(
onPressed: () =>
Navigator.of(context).pop(),
child: Text(S.of(context).ok))
],
);
});
}
},
text: S.of(context).save,
color: Theme.of(context)
.primaryTextTheme
.button
.backgroundColor,
borderColor: Theme.of(context)
.primaryTextTheme
.button
.decorationColor))
],
));
}
}
+95
View File
@@ -0,0 +1,95 @@
import 'package:mobx/mobx.dart';
import 'package:provider/provider.dart';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/stores/auth/auth_state.dart';
import 'package:cake_wallet/src/stores/auth/auth_store.dart';
import 'package:cake_wallet/src/screens/pin_code/pin_code.dart';
class AuthPage extends StatefulWidget {
final Function(bool, AuthPageState) onAuthenticationFinished;
final bool closable;
AuthPage({this.onAuthenticationFinished, this.closable = true});
@override
AuthPageState createState() => AuthPageState();
}
class AuthPageState extends State<AuthPage> {
final _key = GlobalKey<ScaffoldState>();
final _pinCodeKey = GlobalKey<PinCodeState>();
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);
reaction((_) => authStore.state, (state) {
if (state is AuthenticatedSuccessfully) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (widget.onAuthenticationFinished != null) {
widget.onAuthenticationFinished(true, this);
} else {
_key.currentState.showSnackBar(
SnackBar(
content: Text(S.of(context).authenticated),
backgroundColor: Colors.green,
),
);
}
});
}
if (state is AuthenticationInProgress) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_key.currentState.showSnackBar(
SnackBar(
content: Text(S.of(context).authentication),
backgroundColor: Colors.green,
),
);
});
}
if (state is AuthenticationFailure || state is AuthenticationBanned) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_pinCodeKey.currentState.clear();
_key.currentState.hideCurrentSnackBar();
_key.currentState.showSnackBar(
SnackBar(
content: Text(S.of(context).failed_authentication(state.error)),
backgroundColor: Colors.red,
),
);
if (widget.onAuthenticationFinished != null) {
widget.onAuthenticationFinished(false, this);
}
});
}
});
return Scaffold(
key: _key,
appBar: CupertinoNavigationBar(
leading: widget.closable ? CloseButton() : Container(),
backgroundColor: Theme.of(context).backgroundColor,
border: null,
),
resizeToAvoidBottomPadding: false,
body: PinCode(
(pin, _) => authStore.auth(
password: pin.fold('', (ac, val) => ac + '$val')),
false,
_pinCodeKey));
}
}
@@ -0,0 +1,28 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:cake_wallet/src/domain/services/user_service.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/auth/auth_page.dart';
import 'package:cake_wallet/src/stores/auth/auth_store.dart';
import 'package:cake_wallet/src/stores/authentication/authentication_store.dart';
Widget createLoginPage(
{@required SharedPreferences sharedPreferences,
@required UserService userService,
@required WalletService walletService,
@required WalletListService walletListService,
@required AuthenticationStore authenticationStore}) =>
Provider(
create: (_) => AuthStore(
sharedPreferences: sharedPreferences,
userService: userService,
walletService: walletService),
child: AuthPage(
onAuthenticationFinished: (isAuthenticated, state) {
if (isAuthenticated) {
authenticationStore.loggedIn();
}
},
closable: false));
@@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.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/screens/auth/auth_page.dart';
import 'package:cake_wallet/src/stores/auth/auth_store.dart';
Widget createUnlockPage(
{@required SharedPreferences sharedPreferences,
@required UserService userService,
@required WalletService walletService,
@required Function(bool, AuthPageState) onAuthenticationFinished}) =>
WillPopScope(
onWillPop: () async => false,
child: Provider(
create: (_) => AuthStore(
sharedPreferences: sharedPreferences,
userService: userService,
walletService: walletService),
child: AuthPage(
onAuthenticationFinished: onAuthenticationFinished,
closable: false)));
+125
View File
@@ -0,0 +1,125 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/src/widgets/nav_bar.dart';
import 'package:provider/provider.dart';
import 'package:cake_wallet/themes.dart';
import 'package:cake_wallet/theme_changer.dart';
enum AppBarStyle { regular, withShadow }
abstract class BasePage extends StatelessWidget {
String get title => null;
bool get isModalBackButton => false;
Color get backgroundColor => Colors.white;
bool get resizeToAvoidBottomPadding => true;
AppBarStyle get appBarStyle => AppBarStyle.regular;
final _backArrowImage = Image.asset('assets/images/back_arrow.png');
final _backArrowImageDarkTheme =
Image.asset('assets/images/back_arrow_dark_theme.png');
final _closeButtonImage = Image.asset('assets/images/close_button.png');
final _closeButtonImageDarkTheme =
Image.asset('assets/images/close_button_dark_theme.png');
void onClose(BuildContext context) => Navigator.of(context).pop();
Widget leading(BuildContext context) {
if (ModalRoute.of(context).isFirst) {
return null;
}
ThemeChanger _themeChanger = Provider.of<ThemeChanger>(context);
Image _closeButton, _backButton;
if (_themeChanger.getTheme() == Themes.darkTheme) {
_backButton = _backArrowImageDarkTheme;
_closeButton = _closeButtonImageDarkTheme;
} else {
_backButton = _backArrowImage;
_closeButton = _closeButtonImage;
}
return SizedBox(
height: 37,
width: isModalBackButton ? 37 : 20,
child: ButtonTheme(
minWidth: double.minPositive,
child: FlatButton(
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
padding: EdgeInsets.all(0),
onPressed: () => onClose(context),
child: isModalBackButton ? _closeButton : _backButton),
),
);
}
Widget middle(BuildContext context) {
return title == null
? null
: Text(
title,
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.w600,
color: Theme.of(context).primaryTextTheme.title.color),
);
}
Widget trailing(BuildContext context) => null;
Widget floatingActionButton(BuildContext context) => null;
Widget appBar(BuildContext context) {
final _themeChanger = Provider.of<ThemeChanger>(context);
final _isDarkTheme = _themeChanger.getTheme() == Themes.darkTheme;
switch (appBarStyle) {
case AppBarStyle.regular:
return NavBar(
context: context,
leading: leading(context),
middle: middle(context),
trailing: trailing(context),
backgroundColor: _isDarkTheme
? Theme.of(context).backgroundColor
: backgroundColor);
case AppBarStyle.withShadow:
return NavBar.withShadow(
context: context,
leading: leading(context),
middle: middle(context),
trailing: trailing(context),
backgroundColor: _isDarkTheme
? Theme.of(context).backgroundColor
: backgroundColor);
default:
return NavBar(
context: context,
leading: leading(context),
middle: middle(context),
trailing: trailing(context),
backgroundColor: _isDarkTheme
? Theme.of(context).backgroundColor
: backgroundColor);
}
}
Widget body(BuildContext context);
@override
Widget build(BuildContext context) {
final _themeChanger = Provider.of<ThemeChanger>(context);
final _isDarkTheme = _themeChanger.getTheme() == Themes.darkTheme;
return Scaffold(
backgroundColor:
_isDarkTheme ? Theme.of(context).backgroundColor : backgroundColor,
resizeToAvoidBottomPadding: resizeToAvoidBottomPadding,
appBar: appBar(context),
body: SafeArea(child: body(context)),
floatingActionButton: floatingActionButton(context));
}
}
@@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:provider/provider.dart';
import 'package:cake_wallet/src/domain/exchange/trade.dart';
import 'package:cake_wallet/src/domain/monero/transaction_description.dart';
import 'package:cake_wallet/src/domain/services/wallet_service.dart';
import 'package:cake_wallet/src/screens/dashboard/dashboard_page.dart';
import 'package:cake_wallet/src/stores/action_list/action_list_store.dart';
import 'package:cake_wallet/src/stores/action_list/trade_filter_store.dart';
import 'package:cake_wallet/src/stores/action_list/transaction_filter_store.dart';
import 'package:cake_wallet/src/stores/price/price_store.dart';
import 'package:cake_wallet/src/stores/settings/settings_store.dart';
import 'package:cake_wallet/src/stores/wallet/wallet_store.dart';
Widget createDashboardPage(
{@required WalletService walletService,
@required PriceStore priceStore,
@required Box<TransactionDescription> transactionDescriptions,
@required SettingsStore settingsStore,
@required Box<Trade> trades,
@required WalletStore walletStore}) =>
Provider(
create: (_) => ActionListStore(
walletService: walletService,
settingsStore: settingsStore,
priceStore: priceStore,
tradesSource: trades,
transactionFilterStore: TransactionFilterStore(),
tradeFilterStore: TradeFilterStore(walletStore: walletStore),
transactionDescriptions: transactionDescriptions),
child: DashboardPage());
@@ -0,0 +1,584 @@
import 'package:provider/provider.dart';
import 'package:intl/intl.dart';
import 'package:date_range_picker/date_range_picker.dart' as DateRagePicker;
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/domain/common/balance_display_mode.dart';
import 'package:cake_wallet/src/domain/common/sync_status.dart';
import 'package:cake_wallet/src/domain/exchange/exchange_provider_description.dart';
import 'package:cake_wallet/src/stores/action_list/action_list_store.dart';
import 'package:cake_wallet/src/stores/balance/balance_store.dart';
import 'package:cake_wallet/src/stores/sync/sync_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/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/screens/base_page.dart';
import 'package:cake_wallet/src/screens/dashboard/date_section_raw.dart';
import 'package:cake_wallet/src/screens/dashboard/trade_row.dart';
import 'package:cake_wallet/src/screens/dashboard/transaction_raw.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/screens/dashboard/wallet_menu.dart';
import 'package:cake_wallet/src/widgets/picker.dart';
class DashboardPage extends BasePage {
final _bodyKey = GlobalKey();
@override
Widget leading(BuildContext context) {
return SizedBox(
width: 30,
child: FlatButton(
padding: EdgeInsets.all(0),
onPressed: () => _presentWalletMenu(context),
child: Image.asset('assets/images/more.png',
color: Theme.of(context).primaryTextTheme.caption.color,
width: 30)));
}
@override
Widget middle(BuildContext context) {
final walletStore = Provider.of<WalletStore>(context);
return Observer(builder: (_) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
walletStore.name,
style: TextStyle(
color: Theme.of(context).primaryTextTheme.title.color),
),
SizedBox(height: 5),
Text(
walletStore.account != null ? '${walletStore.account.label}' : '',
style: TextStyle(
fontWeight: FontWeight.w400,
fontSize: 10,
color: Theme.of(context).primaryTextTheme.title.color),
),
]);
});
}
@override
Widget trailing(BuildContext context) {
return SizedBox(
width: 20,
child: FlatButton(
padding: EdgeInsets.all(0),
onPressed: () => Navigator.of(context).pushNamed(Routes.settings),
child: Image.asset('assets/images/settings_icon.png',
color: Colors.grey, height: 20)),
);
}
@override
Widget body(BuildContext context) => DashboardPageBody(key: _bodyKey);
@override
Widget floatingActionButton(BuildContext context) => FloatingActionButton(
child: Image.asset('assets/images/exchange_icon.png',
color: Colors.white, height: 26, width: 22),
backgroundColor: Palette.floatingActionButton,
onPressed: () async {
final actionListStore = Provider.of<ActionListStore>(context);
await Navigator.of(context, rootNavigator: true)
.pushNamed(Routes.exchange);
actionListStore.updateTradeList();
});
void _presentWalletMenu(BuildContext bodyContext) {
final walletMenu = WalletMenu(bodyContext);
showDialog(
builder: (_) => Picker(
items: walletMenu.items,
selectedAtIndex: -1,
title: S.of(bodyContext).wallet_menu,
pickerHeight: 510,
onItemSelected: (item) =>
walletMenu.action(walletMenu.items.indexOf(item))),
context: bodyContext);
}
}
class DashboardPageBody extends StatefulWidget {
DashboardPageBody({Key key}) : super(key: key);
@override
DashboardPageBodyState createState() => DashboardPageBodyState();
}
class DashboardPageBodyState extends State<DashboardPageBody> {
static final transactionDateFormat = DateFormat("MMM d, yyyy HH:mm");
final _connectionStatusObserverKey = GlobalKey();
final _balanceObserverKey = GlobalKey();
final _balanceTitleObserverKey = GlobalKey();
final _syncingObserverKey = GlobalKey();
final _listObserverKey = GlobalKey();
final _listKey = GlobalKey();
@override
Widget build(BuildContext context) {
final balanceStore = Provider.of<BalanceStore>(context);
final actionListStore = Provider.of<ActionListStore>(context);
final syncStore = Provider.of<SyncStore>(context);
final settingsStore = Provider.of<SettingsStore>(context);
return Observer(
key: _listObserverKey,
builder: (_) {
final items =
actionListStore.items == null ? [] : actionListStore.items;
final itemsCount = items.length + 2;
return ListView.builder(
key: _listKey,
padding: EdgeInsets.only(bottom: 15),
itemCount: itemsCount,
itemBuilder: (context, index) {
if (index == 0) {
return Container(
margin: EdgeInsets.only(bottom: 20),
decoration: BoxDecoration(
color: Theme.of(context).backgroundColor,
boxShadow: [
BoxShadow(
color: Palette.shadowGreyWithOpacity,
blurRadius: 10,
offset: Offset(0, 12))
]),
child: Column(
children: <Widget>[
Observer(
key: _syncingObserverKey,
builder: (_) {
final status = syncStore.status;
final statusText = status.title();
final progress = syncStore.status.progress();
final isFialure = status is FailedSyncStatus;
var descriptionText = '';
if (status is SyncingSyncStatus) {
descriptionText = S
.of(context)
.Blocks_remaining(
syncStore.status.toString());
}
if (status is FailedSyncStatus) {
descriptionText = S
.of(context)
.please_try_to_connect_to_another_node;
}
return Container(
child: Column(
children: [
SizedBox(
height: 3,
child: LinearProgressIndicator(
backgroundColor: Palette.separator,
valueColor:
AlwaysStoppedAnimation<Color>(
Palette.cakeGreen),
value: progress,
),
),
SizedBox(height: 10),
Text(statusText,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: isFialure
? Palette.failure
: Palette.cakeGreen)),
Text(descriptionText,
style: TextStyle(
fontSize: 11,
color: Palette.wildDarkBlue,
height: 2.0))
],
),
);
}),
GestureDetector(
onTapUp: (_) => balanceStore.isReversing = false,
onTapDown: (_) => balanceStore.isReversing = true,
child: Container(
padding: EdgeInsets.only(top: 40, bottom: 40),
color: Colors.transparent,
child: Column(
children: <Widget>[
Container(width: double.infinity),
Observer(
key: _balanceTitleObserverKey,
builder: (_) {
final savedDisplayMode =
settingsStore.balanceDisplayMode;
final displayMode =
balanceStore.isReversing
? (savedDisplayMode ==
BalanceDisplayMode
.availableBalance
? BalanceDisplayMode
.fullBalance
: BalanceDisplayMode
.availableBalance)
: savedDisplayMode;
var title = displayMode.toString();
return Text(title,
style: TextStyle(
color: Palette.violet,
fontSize: 16));
}),
Observer(
key: _connectionStatusObserverKey,
builder: (_) {
final savedDisplayMode =
settingsStore.balanceDisplayMode;
var balance = '---';
final displayMode =
balanceStore.isReversing
? (savedDisplayMode ==
BalanceDisplayMode
.availableBalance
? BalanceDisplayMode
.fullBalance
: BalanceDisplayMode
.availableBalance)
: savedDisplayMode;
if (displayMode ==
BalanceDisplayMode.availableBalance) {
balance =
balanceStore.unlockedBalance ??
'0.0';
}
if (displayMode ==
BalanceDisplayMode.fullBalance) {
balance =
balanceStore.fullBalance ?? '0.0';
}
return Text(
balance,
style: TextStyle(
color: Theme.of(context)
.primaryTextTheme
.caption
.color,
fontSize: 42),
);
}),
Padding(
padding: EdgeInsets.only(top: 7),
child: Observer(
key: _balanceObserverKey,
builder: (_) {
final savedDisplayMode =
settingsStore.balanceDisplayMode;
final displayMode =
balanceStore.isReversing
? (savedDisplayMode ==
BalanceDisplayMode
.availableBalance
? BalanceDisplayMode
.fullBalance
: BalanceDisplayMode
.availableBalance)
: savedDisplayMode;
final symbol = settingsStore
.fiatCurrency
.toString();
var balance = '---';
if (displayMode ==
BalanceDisplayMode
.availableBalance) {
balance =
'${balanceStore.fiatUnlockedBalance} $symbol';
}
if (displayMode ==
BalanceDisplayMode.fullBalance) {
balance =
'${balanceStore.fiatFullBalance} $symbol';
}
return Text(balance,
style: TextStyle(
color: Palette.wildDarkBlue,
fontSize: 16));
}))
],
),
),
),
Padding(
padding: const EdgeInsets.only(
left: 35, right: 35, bottom: 30),
child: Container(
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: <Widget>[
Expanded(
child: PrimaryImageButton(
image: Image.asset(
'assets/images/send_icon.png',
height: 25,
width: 25),
text: S.of(context).send,
onPressed: () => Navigator.of(context,
rootNavigator: true)
.pushNamed(Routes.send),
color: Theme.of(context)
.primaryTextTheme
.button
.backgroundColor,
borderColor: Theme.of(context)
.primaryTextTheme
.button
.decorationColor,
)),
SizedBox(width: 10),
Expanded(
child: PrimaryImageButton(
image: Image.asset(
'assets/images/receive_icon.png',
height: 25,
width: 25),
text: S.of(context).receive,
onPressed: () => Navigator.of(context,
rootNavigator: true)
.pushNamed(Routes.receive),
color: Theme.of(context)
.accentTextTheme
.caption
.backgroundColor,
borderColor: Theme.of(context)
.accentTextTheme
.caption
.decorationColor,
))
],
),
)),
],
),
);
}
if (index == 1 && actionListStore.totalCount > 0) {
return Padding(
padding: EdgeInsets.only(right: 20, top: 10, bottom: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
PopupMenuButton<int>(
itemBuilder: (context) => [
PopupMenuItem(
enabled: false,
value: -1,
child: Text(S.of(context).transactions,
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.black))),
PopupMenuItem(
value: 0,
child: Observer(
builder: (_) => Row(
mainAxisAlignment:
MainAxisAlignment
.spaceBetween,
children: [
Text(S.of(context).incoming),
Checkbox(
value: actionListStore
.transactionFilterStore
.displayIncoming,
onChanged: (value) =>
actionListStore
.transactionFilterStore
.toggleIncoming(),
)
]))),
PopupMenuItem(
value: 1,
child: Observer(
builder: (_) => Row(
mainAxisAlignment:
MainAxisAlignment
.spaceBetween,
children: [
Text(S.of(context).outgoing),
Checkbox(
value: actionListStore
.transactionFilterStore
.displayOutgoing,
onChanged: (value) =>
actionListStore
.transactionFilterStore
.toggleOutgoing(),
)
]))),
PopupMenuItem(
value: 2,
child:
Text(S.of(context).transactions_by_date)),
PopupMenuDivider(),
PopupMenuItem(
enabled: false,
value: -1,
child: Text(S.of(context).trades,
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.black))),
PopupMenuItem(
value: 3,
child: Observer(
builder: (_) => Row(
mainAxisAlignment:
MainAxisAlignment
.spaceBetween,
children: [
Text('XMR.TO'),
Checkbox(
value: actionListStore
.tradeFilterStore
.displayXMRTO,
onChanged: (value) =>
actionListStore
.tradeFilterStore
.toggleDisplayExchange(
ExchangeProviderDescription
.xmrto),
)
]))),
PopupMenuItem(
value: 4,
child: Observer(
builder: (_) => Row(
mainAxisAlignment:
MainAxisAlignment
.spaceBetween,
children: [
Text('Change.NOW'),
Checkbox(
value: actionListStore
.tradeFilterStore
.displayChangeNow,
onChanged: (value) =>
actionListStore
.tradeFilterStore
.toggleDisplayExchange(
ExchangeProviderDescription
.changeNow),
)
])))
],
child: Text(S.of(context).filters,
style: TextStyle(
fontSize: 16.0,
color: Theme.of(context)
.primaryTextTheme
.subtitle
.color)),
onSelected: (item) async {
if (item == 2) {
final List<DateTime> picked =
await DateRagePicker.showDatePicker(
context: context,
initialFirstDate: DateTime.now()
.subtract(Duration(days: 1)),
initialLastDate: (DateTime.now()),
firstDate: DateTime(2015),
lastDate: DateTime.now()
.add(Duration(days: 1)));
if (picked != null && picked.length == 2) {
actionListStore.transactionFilterStore
.changeStartDate(picked.first);
actionListStore.transactionFilterStore
.changeEndDate(picked.last);
}
}
},
)
]),
);
}
index -= 2;
if (index < 0 || index >= items.length) {
return Container();
}
final item = items[index];
if (item is DateSectionItem) {
return DateSectionRaw(date: item.date);
}
if (item is TransactionListItem) {
final transaction = item.transaction;
final savedDisplayMode = settingsStore.balanceDisplayMode;
final formattedAmount =
savedDisplayMode == BalanceDisplayMode.hiddenBalance
? '---'
: transaction.amountFormatted();
final formattedFiatAmount =
savedDisplayMode == BalanceDisplayMode.hiddenBalance
? '---'
: transaction.fiatAmount();
return TransactionRow(
onTap: () => Navigator.of(context).pushNamed(
Routes.transactionDetails,
arguments: transaction),
direction: transaction.direction,
formattedDate:
transactionDateFormat.format(transaction.date),
formattedAmount: formattedAmount,
formattedFiatAmount: formattedFiatAmount,
isPending: transaction.isPending);
}
if (item is TradeListItem) {
final trade = item.trade;
final savedDisplayMode = settingsStore.balanceDisplayMode;
final formattedAmount = trade.amount != null
? savedDisplayMode == BalanceDisplayMode.hiddenBalance
? '---'
: trade.amount
: trade.amount;
return TradeRow(
onTap: () => Navigator.of(context)
.pushNamed(Routes.tradeDetails, arguments: trade),
provider: trade.provider,
from: trade.from,
to: trade.to,
createdAtFormattedDate:
DateFormat("dd.MM.yyyy, H:m").format(trade.createdAt),
formattedAmount: formattedAmount);
}
return Container();
});
});
}
}
@@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:cake_wallet/palette.dart';
import 'package:intl/intl.dart';
import 'package:cake_wallet/generated/i18n.dart';
class DateSectionRaw extends StatelessWidget {
static final dateSectionDateFormat = DateFormat("d MMM");
static final nowDate = DateTime.now();
final DateTime date;
DateSectionRaw({this.date});
@override
Widget build(BuildContext context) {
final diffDays = date.difference(nowDate).inDays;
final isToday = nowDate.day == date.day && nowDate.month == date.month && nowDate.year == date.year;
var title = "";
if (isToday) {
title = S.of(context).today;
} else if (diffDays == 0) {
title = S.of(context).yesterday;
} else if (diffDays > -7 && diffDays < 0) {
final dateFormat = DateFormat("EEEE");
title = dateFormat.format(date);
} else {
title = dateSectionDateFormat.format(date);
}
return Padding(
padding: const EdgeInsets.only(top: 10, bottom: 10),
child: Center(
child: Text(title,
style: TextStyle(fontSize: 16, color: Palette.wildDarkBlue))),
);
}
}
+87
View File
@@ -0,0 +1,87 @@
import 'package:flutter/material.dart';
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/src/domain/common/crypto_currency.dart';
import 'package:cake_wallet/src/domain/exchange/exchange_provider_description.dart';
class TradeRow extends StatelessWidget {
final VoidCallback onTap;
final ExchangeProviderDescription provider;
final CryptoCurrency from;
final CryptoCurrency to;
final String createdAtFormattedDate;
final String formattedAmount;
TradeRow(
{this.provider,
this.from,
this.to,
this.createdAtFormattedDate,
this.formattedAmount,
@required this.onTap});
@override
Widget build(BuildContext context) {
final amountCrypto = provider == ExchangeProviderDescription.xmrto
? to.toString()
: from.toString();
return InkWell(
onTap: onTap,
child: Container(
padding: EdgeInsets.only(top: 14, bottom: 14, left: 20, right: 20),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: PaletteDark.darkGrey,
width: 0.5,
style: BorderStyle.solid))),
child: Row(children: <Widget>[
_getPoweredImage(provider),
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 10, right: 10),
child: Column(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text('${from.toString()} → ${to.toString()}',
style: TextStyle(
fontSize: 16,
color: Theme.of(context).primaryTextTheme.subhead.color)),
formattedAmount != null
? Text(formattedAmount + ' ' + amountCrypto,
style: const TextStyle(
fontSize: 16, color: Palette.purpleBlue))
: Container()
]),
SizedBox(height: 6),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(createdAtFormattedDate,
style: const TextStyle(
fontSize: 13, color: Palette.blueGrey))
]),
],
),
))
]),
));
}
Image _getPoweredImage(ExchangeProviderDescription provider) {
Image image;
switch (provider) {
case ExchangeProviderDescription.xmrto:
image = Image.asset('assets/images/xmr_btc.png');
break;
case ExchangeProviderDescription.changeNow:
image = Image.asset('assets/images/change_now.png');
break;
default:
image = null;
}
return image;
}
}
@@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/src/domain/common/transaction_direction.dart';
import 'package:cake_wallet/generated/i18n.dart';
class TransactionRow extends StatelessWidget {
final VoidCallback onTap;
final TransactionDirection direction;
final String formattedDate;
final String formattedAmount;
final String formattedFiatAmount;
final bool isPending;
TransactionRow(
{this.direction,
this.formattedDate,
this.formattedAmount,
this.formattedFiatAmount,
this.isPending,
@required this.onTap});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Container(
padding: EdgeInsets.only(top: 14, bottom: 14, left: 20, right: 20),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: PaletteDark.darkGrey,
width: 0.5,
style: BorderStyle.solid))),
child: Row(children: <Widget>[
Image.asset(
direction == TransactionDirection.incoming
? 'assets/images/transaction_incoming.png'
: 'assets/images/transaction_outgoing.png',
height: 25,
width: 25),
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 10, right: 10),
child: Column(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
(direction == TransactionDirection.incoming
? S.of(context).received
: S.of(context).sent) +
(isPending ? S.of(context).pending : ''),
style: TextStyle(
fontSize: 16,
color: Theme.of(context).primaryTextTheme.subhead.color)),
Text(formattedAmount,
style: const TextStyle(
fontSize: 16, color: Palette.purpleBlue))
]),
SizedBox(height: 6),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(formattedDate,
style: const TextStyle(
fontSize: 13, color: Palette.blueGrey)),
Text(formattedFiatAmount,
style: const TextStyle(
fontSize: 14, color: Palette.blueGrey))
]),
],
),
))
]),
));
}
}
@@ -0,0 +1,86 @@
import 'package:flutter/material.dart';
import 'package:cake_wallet/routes.dart';
import 'package:provider/provider.dart';
import 'package:cake_wallet/src/stores/wallet/wallet_store.dart';
import 'package:cake_wallet/generated/i18n.dart';
class WalletMenu {
final List<String> items = [
S.current.reconnect,
S.current.rescan,
S.current.wallets,
S.current.show_seed,
S.current.show_keys,
S.current.accounts,
S.current.address_book_menu
];
final BuildContext context;
WalletMenu(this.context);
void action(int index) {
switch (index) {
case 0:
_presentReconnectAlert(context);
break;
case 1:
Navigator.of(context).pushNamed(Routes.rescan);
break;
case 2:
Navigator.of(context).pushNamed(Routes.walletList);
break;
case 3:
Navigator.of(context).pushNamed(Routes.auth,
arguments: (isAuthenticatedSuccessfully, auth) =>
isAuthenticatedSuccessfully
? Navigator.of(auth.context).popAndPushNamed(Routes.seed)
: null);
break;
case 4:
Navigator.of(context).pushNamed(Routes.auth,
arguments: (isAuthenticatedSuccessfully, auth) =>
isAuthenticatedSuccessfully
? Navigator.of(auth.context)
.popAndPushNamed(Routes.showKeys)
: null);
break;
case 5:
Navigator.of(context).pushNamed(Routes.accountList);
break;
case 6:
Navigator.of(context).pushNamed(Routes.addressBook);
break;
default:
break;
}
}
Future _presentReconnectAlert(BuildContext context) async {
final walletStore = Provider.of<WalletStore>(context);
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
S.of(context).reconnection,
textAlign: TextAlign.center,
),
content: Text(S.of(context).reconnect_alert_text),
actions: <Widget>[
FlatButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(S.of(context).cancel)),
FlatButton(
onPressed: () {
walletStore.reconnect();
Navigator.of(context).pop();
},
child: Text(S.of(context).ok))
],
);
});
}
}
@@ -0,0 +1,305 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:cake_wallet/palette.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
class DisclaimerPage extends BasePage {
final bool isReadOnly;
DisclaimerPage({this.isReadOnly = false});
@override
bool get isModalBackButton => false;
@override
String get title => 'Terms of Use';
@override
Widget body(BuildContext context) => DisclaimerPageBody(isReadOnly: true);
}
class DisclaimerPageBody extends StatefulWidget {
final bool isReadOnly;
DisclaimerPageBody({this.isReadOnly = true});
@override
createState() => DisclaimerBodyState(false);
}
class DisclaimerBodyState extends State<DisclaimerPageBody> {
static const xmrto_url = 'https://xmr.to/app_static/html/tos.html';
static const changenow_url = 'https://changenow.io/terms-of-use';
bool _isAccepted;
bool _checked = false;
String _fileText = '';
DisclaimerBodyState(this._isAccepted);
launchUrl(String url) async {
if (await canLaunch(url)) await launch(url);
}
Future getFileLines() async {
_fileText = await rootBundle.loadString('assets/text/Terms_of_Use.txt');
setState(() {});
}
_showAlertDialog(BuildContext context) async {
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
'Terms and conditions',
textAlign: TextAlign.center,
),
content: Text(
'By using this app, you agree to the Terms of Agreement set forth to below',
textAlign: TextAlign.center,
),
actions: <Widget>[
FlatButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('OK')),
],
);
});
}
_afterLayout(_) {
_showAlertDialog(context);
}
@override
void initState() {
super.initState();
getFileLines();
if (_isAccepted) WidgetsBinding.instance.addPostFrameCallback(_afterLayout);
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
SizedBox(height: 10.0),
Expanded(
child: Stack(
children: <Widget>[
SingleChildScrollView(
padding: EdgeInsets.only(left: 25.0, right: 25.0),
child: Column(
children: <Widget>[
!_isAccepted
? Row(
children: <Widget>[
Expanded(
child: Text(
'Terms and conditions',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold),
),
)
],
)
: Offstage(),
!_isAccepted
? SizedBox(
height: 20.0,
)
: Offstage(),
Row(
children: <Widget>[
Expanded(
child: Text(
'Legal Disclaimer\nAnd\nTerms of Use',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12.0, fontWeight: FontWeight.bold),
),
)
],
),
SizedBox(
height: 16.0,
),
Row(
children: <Widget>[
Expanded(
child: Text(
_fileText,
style: TextStyle(fontSize: 12.0),
))
],
),
SizedBox(
height: 16.0,
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Expanded(
child: Text(
'Other Terms and Conditions',
textAlign: TextAlign.left,
style: TextStyle(
fontSize: 14.0, fontWeight: FontWeight.bold),
),
)
],
),
SizedBox(
height: 16.0,
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Expanded(
child: GestureDetector(
onTap: () => launchUrl(xmrto_url),
child: Text(
xmrto_url,
textAlign: TextAlign.left,
style: TextStyle(
color: Colors.blue,
fontSize: 14.0,
fontWeight: FontWeight.normal,
decoration: TextDecoration.underline),
),
))
],
),
SizedBox(
height: 16.0,
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Expanded(
child: GestureDetector(
onTap: () => launchUrl(changenow_url),
child: Text(
changenow_url,
textAlign: TextAlign.left,
style: TextStyle(
color: Colors.blue,
fontSize: 14.0,
fontWeight: FontWeight.normal,
decoration: TextDecoration.underline),
),
))
],
),
SizedBox(
height: 16.0,
)
],
),
),
Container(
alignment: Alignment.bottomCenter,
child: Container(
height: 12.0,
child: ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 0.7, sigmaY: 0.7),
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Theme.of(context).backgroundColor.withOpacity(0.0),
Theme.of(context).backgroundColor,
],
begin: FractionalOffset.topCenter,
end: FractionalOffset.bottomCenter,
),
),
),
),
),
),
)
],
)),
if (!widget.isReadOnly) ...[
!_isAccepted
? Row(
children: <Widget>[
Expanded(
child: Container(
padding: EdgeInsets.only(
left: 25.0, top: 10.0, right: 25.0, bottom: 10.0),
child: InkWell(
onTap: () {
setState(() {
_checked = !_checked;
});
},
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Container(
height: 25.0,
width: 25.0,
margin: EdgeInsets.only(
right: 10.0,
),
decoration: BoxDecoration(
border: Border.all(
color: Palette.lightGrey, width: 1.0),
borderRadius: BorderRadius.all(
Radius.circular(8.0)),
color: Theme.of(context).backgroundColor),
child: _checked
? Icon(
Icons.check,
color: Colors.blue,
size: 20.0,
)
: null,
),
Text(
'I agree to Terms of Use',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14.0),
)
],
),
)),
),
],
)
: Offstage(),
!_isAccepted
? Container(
padding:
EdgeInsets.only(left: 25.0, right: 25.0, bottom: 25.0),
child: PrimaryButton(
onPressed: _checked ? () {} : null,
text: 'Accept',
color: Theme.of(context).primaryTextTheme.button.backgroundColor,
borderColor: Theme.of(context).primaryTextTheme.button.decorationColor,
),
)
: Offstage(),
_isAccepted
? SizedBox(
height: 20.0,
)
: Offstage()
],
],
);
}
}
+486
View File
@@ -0,0 +1,486 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
import 'package:provider/provider.dart';
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/domain/exchange/exchange_provider_description.dart';
import 'package:cake_wallet/src/domain/common/crypto_currency.dart';
import 'package:cake_wallet/src/domain/exchange/xmrto/xmrto_exchange_provider.dart';
import 'package:cake_wallet/src/stores/exchange/exchange_trade_state.dart';
import 'package:cake_wallet/src/stores/exchange/limits_state.dart';
import 'package:cake_wallet/src/stores/wallet/wallet_store.dart';
import 'package:cake_wallet/src/stores/exchange/exchange_store.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/exchange_card.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/picker.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
class ExchangePage extends BasePage {
String get title => S.current.exchange;
@override
bool get isModalBackButton => true;
final Image arrowBottomPurple = Image.asset(
'assets/images/arrow_bottom_purple_icon.png',
height: 8,
);
@override
Widget middle(BuildContext context) {
final exchangeStore = Provider.of<ExchangeStore>(context);
return FlatButton(
onPressed: () => _presentProviderPicker(context),
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(S.of(context).exchange,
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.w400,
color: Theme.of(context).primaryTextTheme.title.color)),
SizedBox(width: 5),
arrowBottomPurple
]),
Observer(
builder: (_) => Text('${exchangeStore.provider.title}',
style: TextStyle(
fontSize: 10.0,
fontWeight: FontWeight.w400,
color:
Theme.of(context).primaryTextTheme.headline.color)))
],
),
);
}
@override
Widget trailing(BuildContext context) {
final exchangeStore = Provider.of<ExchangeStore>(context);
return ButtonTheme(
minWidth: double.minPositive,
child: FlatButton(
padding: EdgeInsets.all(0),
child: Text(
S.of(context).clear,
style: TextStyle(
color: Theme.of(context).accentTextTheme.caption.color,
fontWeight: FontWeight.w500,
fontSize: 16),
),
onPressed: () => exchangeStore.reset()),
);
}
@override
Widget body(BuildContext context) => ExchangeForm();
void _presentProviderPicker(BuildContext context) {
final exchangeStore = Provider.of<ExchangeStore>(context);
final items = exchangeStore.providersForCurrentPair();
final selectedItem = items.indexOf(exchangeStore.provider);
showDialog(
builder: (_) => Picker(
items: items,
selectedAtIndex: selectedItem,
title: S.of(context).change_exchange_provider,
onItemSelected: (provider) =>
exchangeStore.changeProvider(provider: provider)),
context: context);
}
}
class ExchangeForm extends StatefulWidget {
@override
State<StatefulWidget> createState() => ExchangeFormState();
}
class ExchangeFormState extends State<ExchangeForm> {
final depositKey = GlobalKey<ExchangeCardState>();
final receiveKey = GlobalKey<ExchangeCardState>();
final _formKey = GlobalKey<FormState>();
var _isReactionsSet = false;
final Image arrowBottomPurple = Image.asset(
'assets/images/arrow_bottom_purple_icon.png',
height: 8,
);
final Image arrowBottomCakeGreen = Image.asset(
'assets/images/arrow_bottom_cake_green.png',
height: 8,
);
@override
Widget build(BuildContext context) {
final exchangeStore = Provider.of<ExchangeStore>(context);
final walletStore = Provider.of<WalletStore>(context);
final depositWalletName =
exchangeStore.depositCurrency == CryptoCurrency.xmr
? walletStore.name
: null;
final receiveWalletName =
exchangeStore.receiveCurrency == CryptoCurrency.xmr
? walletStore.name
: null;
WidgetsBinding.instance.addPostFrameCallback(
(_) => _setReactions(context, exchangeStore, walletStore));
return Form(
key: _formKey,
child: ScrollableWithBottomSection(
contentPadding: EdgeInsets.only(left: 20, right: 20),
content: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Padding(
padding: EdgeInsets.only(top: 10, bottom: 20),
child: Text(
S.of(context).you_will_send,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
height: 1.1,
color: Theme.of(context).primaryTextTheme.title.color),
),
),
ExchangeCard(
key: depositKey,
initialCurrency: exchangeStore.depositCurrency,
initialWalletName: depositWalletName,
initialAddress:
exchangeStore.depositCurrency == walletStore.type
? walletStore.address
: null,
initialIsAmountEditable:
!(exchangeStore.provider is XMRTOExchangeProvider),
initialIsAddressEditable:
!(exchangeStore.provider is XMRTOExchangeProvider),
isAmountEstimated:
exchangeStore.provider is XMRTOExchangeProvider,
currencies: CryptoCurrency.all,
onCurrencySelected: (currency) =>
exchangeStore.changeDepositCurrency(currency: currency),
imageArrow: arrowBottomPurple,
currencyValueValidator: (value) {
exchangeStore.validateCryptoCurrency(value);
return exchangeStore.errorMessage;
},
addressTextFieldValidator: (value) {
exchangeStore.validateAddress(value,
cryptoCurrency: exchangeStore.depositCurrency);
return exchangeStore.errorMessage;
},
),
SizedBox(height: 35),
Padding(
padding: EdgeInsets.only(bottom: 20),
child: Text(
S.of(context).you_will_get,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
height: 1.1,
color:
Theme.of(context).primaryTextTheme.title.color),
)),
Observer(
builder: (_) => ExchangeCard(
key: receiveKey,
initialCurrency: exchangeStore.receiveCurrency,
initialWalletName: receiveWalletName,
initialAddress:
exchangeStore.receiveCurrency == walletStore.type
? walletStore.address
: null,
initialIsAmountEditable:
exchangeStore.provider is XMRTOExchangeProvider,
initialIsAddressEditable:
exchangeStore.provider is XMRTOExchangeProvider,
isAmountEstimated: !(exchangeStore.provider
is XMRTOExchangeProvider),
currencies: CryptoCurrency.all,
onCurrencySelected: (currency) => exchangeStore
.changeReceiveCurrency(currency: currency),
imageArrow: arrowBottomCakeGreen,
currencyValueValidator: (value) {
exchangeStore.validateCryptoCurrency(value);
return exchangeStore.errorMessage;
},
addressTextFieldValidator: (value) {
exchangeStore.validateAddress(value,
cryptoCurrency: exchangeStore.receiveCurrency);
return exchangeStore.errorMessage;
},
)),
],
),
),
bottomSectionPadding: EdgeInsets.only(top: 35, left: 20, right: 20),
bottomSection: Column(children: <Widget>[
Padding(
padding: EdgeInsets.only(bottom: 15),
child: Observer(builder: (_) {
final description =
exchangeStore.provider is XMRTOExchangeProvider
? S.of(context).amount_is_guaranteed
: S.of(context).amount_is_estimate;
return Center(
child: Text(
description,
style: TextStyle(color: Palette.blueGrey, fontSize: 12),
),
);
}),
),
Observer(
builder: (_) => LoadingPrimaryButton(
text: S.of(context).exchange,
onPressed: () {
if (_formKey.currentState.validate())
exchangeStore.createTrade();
},
color: Theme.of(context)
.primaryTextTheme
.button
.backgroundColor,
borderColor: Theme.of(context)
.primaryTextTheme
.button
.decorationColor,
isLoading: exchangeStore.tradeState is TradeIsCreating,
)),
Observer(builder: (_) {
final title = exchangeStore.provider.description.title;
var imageSrc = '';
switch (exchangeStore.provider.description) {
case ExchangeProviderDescription.xmrto:
imageSrc = 'assets/images/xmr_btc.png';
break;
case ExchangeProviderDescription.changeNow:
imageSrc = 'assets/images/change_now.png';
break;
}
return Padding(
padding: EdgeInsets.only(top: 20, bottom: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Image.asset(imageSrc),
SizedBox(width: 10),
Text(
S.of(context).powered_by(title),
style: TextStyle(fontSize: 14, color: Palette.powered),
)
],
),
);
})
]),
));
}
void _setReactions(
BuildContext context, ExchangeStore store, WalletStore walletStore) {
if (_isReactionsSet) {
return;
}
final depositAddressController = depositKey.currentState.addressController;
final depositAmountController = depositKey.currentState.amountController;
final receiveAddressController = receiveKey.currentState.addressController;
final receiveAmountController = receiveKey.currentState.amountController;
final limitsState = store.limitsState;
if (limitsState is LimitsLoadedSuccessfully) {
final min = limitsState.limits.min != null
? limitsState.limits.min.toString()
: null;
final max = limitsState.limits.max != null
? limitsState.limits.max.toString()
: null;
final key =
store.provider is XMRTOExchangeProvider ? receiveKey : depositKey;
key.currentState.changeLimits(min: min, max: max);
}
_onCurrencyChange(store.receiveCurrency, walletStore, receiveKey);
_onCurrencyChange(store.depositCurrency, walletStore, depositKey);
reaction(
(_) => walletStore.name,
(_) => _onWalletNameChange(
walletStore, store.receiveCurrency, receiveKey));
reaction(
(_) => walletStore.name,
(_) => _onWalletNameChange(
walletStore, store.depositCurrency, depositKey));
reaction((_) => store.receiveCurrency,
(currency) => _onCurrencyChange(currency, walletStore, receiveKey));
reaction((_) => store.depositCurrency,
(currency) => _onCurrencyChange(currency, walletStore, depositKey));
reaction((_) => store.depositAmount, (amount) {
if (depositKey.currentState.amountController.text != amount) {
depositKey.currentState.amountController.text = amount;
}
});
reaction((_) => store.receiveAmount, (amount) {
if (receiveKey.currentState.amountController.text !=
store.receiveAmount) {
receiveKey.currentState.amountController.text = amount;
}
});
reaction((_) => store.provider, (provider) {
final isReversed = provider is XMRTOExchangeProvider;
if (isReversed) {
receiveKey.currentState.isAddressEditable(isEditable: true);
receiveKey.currentState.isAmountEditable(isEditable: true);
depositKey.currentState.isAddressEditable(isEditable: false);
depositKey.currentState.isAmountEditable(isEditable: false);
} else {
receiveKey.currentState.isAddressEditable(isEditable: true);
receiveKey.currentState.isAmountEditable(isEditable: false);
depositKey.currentState.isAddressEditable(isEditable: true);
depositKey.currentState.isAmountEditable(isEditable: true);
}
depositKey.currentState.changeIsAmountEstimated(isReversed);
receiveKey.currentState.changeIsAmountEstimated(!isReversed);
});
reaction((_) => store.tradeState, (state) {
if (state is TradeIsCreatedFailure) {
WidgetsBinding.instance.addPostFrameCallback((_) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(S.of(context).error),
content: Text(state.error),
actions: <Widget>[
FlatButton(
child: Text(S.of(context).ok),
onPressed: () => Navigator.of(context).pop())
],
);
});
});
}
if (state is TradeIsCreatedSuccessfully) {
Navigator.of(context)
.pushNamed(Routes.exchangeConfirm, arguments: state.trade);
}
});
reaction((_) => store.limitsState, (state) {
final isXMRTO = store.provider is XMRTOExchangeProvider;
String min;
String max;
if (state is LimitsLoadedSuccessfully) {
min = state.limits.min != null ? state.limits.min.toString() : null;
max = state.limits.max != null ? state.limits.max.toString() : null;
}
if (state is LimitsLoadedFailure) {
min = '0';
max = '0';
}
if (state is LimitsIsLoading) {
min = '...';
max = '...';
}
final depositMin = isXMRTO ? null : min;
final depositMax = isXMRTO ? null : max;
final receiveMin = isXMRTO ? min : null;
final receiveMax = isXMRTO ? max : null;
depositKey.currentState.changeLimits(min: depositMin, max: depositMax);
receiveKey.currentState.changeLimits(min: receiveMin, max: receiveMax);
});
depositAddressController.addListener(
() => store.depositAddress = depositAddressController.text);
depositAmountController.addListener(() {
if (depositAmountController.text != store.depositAmount) {
store.changeDepositAmount(amount: depositAmountController.text);
}
});
receiveAddressController.addListener(
() => store.receiveAddress = receiveAddressController.text);
receiveAmountController.addListener(() {
if (receiveAmountController.text != store.receiveAmount) {
store.changeReceiveAmount(amount: receiveAmountController.text);
}
});
reaction((_) => walletStore.address, (address) {
if (store.depositCurrency == CryptoCurrency.xmr) {
depositKey.currentState.changeAddress(address: address);
}
if (store.receiveCurrency == CryptoCurrency.xmr) {
receiveKey.currentState.changeAddress(address: address);
}
});
_isReactionsSet = true;
}
void _onCurrencyChange(CryptoCurrency currency, WalletStore walletStore,
GlobalKey<ExchangeCardState> key) {
final isCurrentTypeWallet = currency == walletStore.type;
key.currentState.changeSelectedCurrency(currency);
key.currentState
.changeWalletName(isCurrentTypeWallet ? walletStore.name : null);
key.currentState
.changeAddress(address: isCurrentTypeWallet ? walletStore.address : '');
key.currentState.changeAmount(amount: '');
}
void _onWalletNameChange(WalletStore walletStore, CryptoCurrency currency,
GlobalKey<ExchangeCardState> key) {
final isCurrentTypeWallet = currency == walletStore.type;
if (isCurrentTypeWallet) {
key.currentState.changeWalletName(walletStore.name);
key.currentState.addressController.text = walletStore.address;
} else if (key.currentState.addressController.text == walletStore.address) {
key.currentState.changeWalletName(null);
key.currentState.addressController.text = null;
}
}
}
@@ -0,0 +1,261 @@
import 'package:flutter/services.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/domain/common/crypto_currency.dart';
import 'package:cake_wallet/src/widgets/picker.dart';
import 'package:cake_wallet/src/widgets/address_text_field.dart';
class ExchangeCard extends StatefulWidget {
final List<CryptoCurrency> currencies;
final Function(CryptoCurrency) onCurrencySelected;
final CryptoCurrency initialCurrency;
final String initialWalletName;
final String initialAddress;
final bool initialIsAmountEditable;
final bool initialIsAddressEditable;
final bool isAmountEstimated;
final Image imageArrow;
final FormFieldValidator<String> currencyValueValidator;
final FormFieldValidator<String> addressTextFieldValidator;
ExchangeCard(
{Key key,
this.initialCurrency,
this.initialAddress,
this.initialWalletName,
this.initialIsAmountEditable,
this.initialIsAddressEditable,
this.isAmountEstimated,
this.currencies,
this.onCurrencySelected,
this.imageArrow,
this.currencyValueValidator,
this.addressTextFieldValidator})
: super(key: key);
@override
createState() => ExchangeCardState();
}
class ExchangeCardState extends State<ExchangeCard> {
final addressController = TextEditingController();
final amountController = TextEditingController();
String _min;
String _max;
CryptoCurrency _selectedCurrency;
String _walletName;
bool _isAmountEditable;
bool _isAddressEditable;
bool _isAmountEstimated;
@override
void initState() {
_isAmountEditable = widget.initialIsAmountEditable;
_isAddressEditable = widget.initialIsAddressEditable;
_walletName = widget.initialWalletName;
_selectedCurrency = widget.initialCurrency;
_isAmountEstimated = widget.isAmountEstimated;
addressController.text = widget.initialAddress;
super.initState();
}
void changeLimits({String min, String max}) {
setState(() {
_min = min;
_max = max;
});
}
void changeSelectedCurrency(CryptoCurrency currency) {
setState(() => _selectedCurrency = currency);
}
void changeWalletName(String walletName) {
setState(() => _walletName = walletName);
}
void changeIsAction(bool isActive) {
setState(() => _isAmountEditable = isActive);
}
void isAmountEditable({bool isEditable = true}) {
setState(() => _isAmountEditable = isEditable);
}
void isAddressEditable({bool isEditable = true}) {
setState(() => _isAddressEditable = isEditable);
}
void changeAddress({String address}) {
setState(() => addressController.text = address);
}
void changeAmount({String amount}) {
setState(() => amountController.text = amount);
}
void changeIsAmountEstimated(bool isEstimated) {
setState(() => _isAmountEstimated = isEstimated);
}
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.fromLTRB(22, 15, 22, 30),
width: double.infinity,
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.all(Radius.circular(12))),
child: Column(children: <Widget>[
_isAmountEstimated != null && _isAmountEstimated
? Row(mainAxisAlignment: MainAxisAlignment.end, children: <Widget>[
SizedBox(
height: 30,
child: Container(
padding: EdgeInsets.fromLTRB(10, 5, 10, 5),
decoration: BoxDecoration(
color: Palette.lightGrey,
borderRadius: BorderRadius.circular(10.0)),
child: Text(
S.of(context).estimated,
style: TextStyle(
fontSize: 14,
color: Palette.wildDarkBlue,
fontWeight: FontWeight.bold),
),
),
),
])
: Container(),
Container(
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container(
height: 52,
width: 80,
child: InkWell(
onTap: () => _presentPicker(context),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(_selectedCurrency.toString(),
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 24,
color: Theme.of(context).primaryTextTheme.title.color)),
widget.imageArrow
]),
_walletName != null
? Text(_walletName,
style: TextStyle(
fontSize: 12,
color: Palette.wildDarkBlue))
: SizedBox(),
]),
),
),
SizedBox(width: 10),
Flexible(
child: Column(
children: [
TextFormField(
style: TextStyle(fontSize: 23, height: 1.21),
controller: amountController,
enabled: _isAmountEditable,
textAlign: TextAlign.right,
keyboardType: TextInputType.numberWithOptions(
signed: false, decimal: false),
inputFormatters: [BlacklistingTextInputFormatter(new RegExp('[\\-|\\ |\\,]'))],
decoration: InputDecoration(
hintStyle: TextStyle(
color: Theme.of(context).cardTheme.color,
fontSize: 23,
height: 1.21),
hintText: '0.00000000',
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Palette.cakeGreen,
width: 2.0)),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: _isAmountEditable
? Palette.deepPurple
: Theme.of(context).focusColor,
width: 1.0))),
validator: widget.currencyValueValidator),
SizedBox(height: 5),
SizedBox(
height: 15,
width: double.infinity,
child: Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
_min != null
? Text(
S.of(context).min_value(_min, _selectedCurrency.toString()),
style: TextStyle(
fontSize: 10,
height: 1.2,
color: Theme.of(context).primaryTextTheme.subtitle.color),
)
: SizedBox(),
_min != null ? SizedBox(width: 10) : SizedBox(),
_max != null
? Text(
S.of(context).max_value(_max, _selectedCurrency.toString()),
style: TextStyle(
fontSize: 10,
height: 1.2,
color: Theme.of(context).primaryTextTheme.subtitle.color))
: SizedBox(),
]),
),
),
],
),
),
]),
),
SizedBox(height: 10),
AddressTextField(
controller: addressController,
isActive: _isAddressEditable,
options: _isAddressEditable
? _walletName != null
? [
AddressTextFieldOption.qrCode,
AddressTextFieldOption.addressBook,
AddressTextFieldOption.subaddressList
]
: [
AddressTextFieldOption.qrCode,
AddressTextFieldOption.addressBook,
]
: [],
validator: widget.addressTextFieldValidator,
)
]),
);
}
void _presentPicker(BuildContext context) {
showDialog(
builder: (_) => Picker(
items: widget.currencies,
selectedAtIndex: widget.currencies.indexOf(_selectedCurrency),
title: S.of(context).change_currency,
onItemSelected: (item) => widget.onCurrencySelected != null
? widget.onCurrencySelected(item)
: null),
context: context);
}
}
@@ -0,0 +1,91 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/domain/exchange/trade.dart';
class ExchangeConfirmPage extends BasePage {
String get title => S.current.copy_id;
final Trade trade;
ExchangeConfirmPage({@required this.trade});
@override
Widget body(BuildContext context) {
return Column(
children: <Widget>[
Expanded(
child: Container(
padding: EdgeInsets.only(left: 80.0, right: 80.0),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
S.of(context).exchange_result_write_down_trade_id,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18.0,
color: Theme.of(context)
.primaryTextTheme
.button
.color),
),
SizedBox(
height: 70.0,
),
Text(
S.of(context).trade_id(trade.id),
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18.0,
color: Theme.of(context)
.primaryTextTheme
.button
.color),
),
SizedBox(
height: 70.0,
),
PrimaryButton(
onPressed: () {
Clipboard.setData(ClipboardData(text: trade.id));
Scaffold.of(context).showSnackBar(SnackBar(
content: Text(
S.of(context).copied_to_clipboard,
textAlign: TextAlign.center,
style: TextStyle(color: Colors.white),
),
backgroundColor: Colors.green,
));
},
text: S.of(context).copy_id,
color: Theme.of(context)
.accentTextTheme
.caption
.backgroundColor,
borderColor: Theme.of(context)
.accentTextTheme
.caption
.decorationColor)
],
),
))),
Container(
padding: EdgeInsets.only(left: 20.0, right: 20.0, bottom: 40.0),
child: PrimaryButton(
onPressed: () => Navigator.of(context)
.pushReplacementNamed(Routes.exchangeTrade, arguments: trade),
text: S.of(context).saved_the_trade_id,
color: Theme.of(context).primaryTextTheme.button.backgroundColor,
borderColor:
Theme.of(context).primaryTextTheme.button.decorationColor),
)
],
);
}
}
@@ -0,0 +1,373 @@
import 'package:cake_wallet/src/domain/common/crypto_currency.dart';
import 'package:cake_wallet/src/domain/exchange/exchange_provider_description.dart';
import 'package:mobx/mobx.dart';
import 'package:provider/provider.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/stores/exchange_trade/exchange_trade_store.dart';
import 'package:cake_wallet/src/stores/send/send_store.dart';
import 'package:cake_wallet/src/stores/send/sending_state.dart';
import 'package:cake_wallet/src/stores/wallet/wallet_store.dart';
import 'package:cake_wallet/src/screens/exchange_trade/widgets/copy_button.dart';
import 'package:cake_wallet/src/screens/receive/qr_image.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/exchange_trade/widgets/timer_widget.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
class ExchangeTradePage extends BasePage {
String get title => S.current.exchange;
@override
Widget body(BuildContext context) => ExchangeTradeForm();
}
class ExchangeTradeForm extends StatefulWidget {
@override
createState() => ExchangeTradeState();
}
class ExchangeTradeState extends State<ExchangeTradeForm> {
final fetchingLabel = S.current.fetching;
String get title => S.current.exchange;
bool _effectsInstalled = false;
@override
Widget build(BuildContext context) {
final tradeStore = Provider.of<ExchangeTradeStore>(context);
final sendStore = Provider.of<SendStore>(context);
final walletStore = Provider.of<WalletStore>(context);
_setEffects(context);
return ScrollableWithBottomSection(
contentPadding: EdgeInsets.only(left: 20, right: 20, top: 20),
content: Observer(builder: (_) {
final trade = tradeStore.trade;
final walletName = walletStore.name;
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Text(
S.of(context).id,
style: TextStyle(
height: 2,
fontWeight: FontWeight.bold,
fontSize: 14.0,
color: Theme.of(context)
.primaryTextTheme
.button
.color),
),
Text(
'${trade.id ?? fetchingLabel}',
style: TextStyle(
fontSize: 14.0,
height: 2,
color: Theme.of(context)
.primaryTextTheme
.subtitle
.color),
)
],
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Text(
S.of(context).amount,
style: TextStyle(
height: 2,
fontWeight: FontWeight.bold,
fontSize: 14.0,
color: Theme.of(context)
.primaryTextTheme
.button
.color),
),
Text(
'${trade.amount ?? fetchingLabel}',
style: TextStyle(
fontSize: 14.0,
height: 2,
color: Theme.of(context)
.primaryTextTheme
.subtitle
.color),
)
],
),
trade.extraId != null
? Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Text(
S.of(context).payment_id,
style: TextStyle(
height: 2,
fontWeight: FontWeight.bold,
fontSize: 14.0,
color: Theme.of(context)
.primaryTextTheme
.button
.color),
),
Text(
'${trade.extraId ?? fetchingLabel}',
style: TextStyle(
fontSize: 14.0,
height: 2,
color: Theme.of(context)
.primaryTextTheme
.subtitle
.color),
)
],
)
: Container(),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Text(
S.of(context).status,
style: TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.bold,
color: Theme.of(context)
.primaryTextTheme
.button
.color,
height: 2),
),
Text(
'${trade.state ?? fetchingLabel}',
style: TextStyle(
fontSize: 14.0,
height: 2,
color: Theme.of(context)
.primaryTextTheme
.subtitle
.color),
)
],
),
trade.expiredAt != null
? Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Text(
S.of(context).offer_expires_in,
style: TextStyle(
fontSize: 14.0,
color: Theme.of(context)
.primaryTextTheme
.button
.color),
),
TimerWidget(trade.expiredAt,
color: Theme.of(context)
.primaryTextTheme
.subtitle
.color)
],
)
: Container(),
],
),
SizedBox(width: 10),
Container(
padding: EdgeInsets.all(5),
color: Colors.white,
constraints: BoxConstraints(minWidth: 65, maxWidth: 105),
child: QrImage(
data: trade.inputAddress ?? fetchingLabel,
backgroundColor: Colors.white))
],
),
),
SizedBox(
height: 20.0,
),
Center(
child: Text(
S.of(context).trade_is_powered_by(trade.provider != null
? trade.provider.title
: fetchingLabel),
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryTextTheme.headline.color),
),
),
Container(
padding: EdgeInsets.only(top: 20, bottom: 20),
child: Center(
child: Text(
trade.inputAddress ?? fetchingLabel,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 14.0, color: Palette.lightViolet),
),
),
),
Container(
padding: EdgeInsets.only(left: 50.0, right: 50.0),
child: Row(
children: <Widget>[
Flexible(
child: Container(
padding: EdgeInsets.only(right: 5.0),
child: CopyButton(
onPressed: () => Clipboard.setData(
ClipboardData(text: trade.inputAddress)),
text: S.of(context).copy_address,
color: Theme.of(context)
.accentTextTheme
.button
.backgroundColor,
borderColor: Theme.of(context)
.accentTextTheme
.button
.decorationColor),
)),
Flexible(
child: Container(
padding: EdgeInsets.only(left: 5.0),
child: CopyButton(
onPressed: () =>
Clipboard.setData(ClipboardData(text: trade.id)),
text: S.of(context).copy_id,
color: Theme.of(context)
.accentTextTheme
.button
.backgroundColor,
borderColor: Theme.of(context)
.accentTextTheme
.button
.decorationColor),
))
],
),
),
Container(
padding: EdgeInsets.only(top: 20),
child: Text(
tradeStore.isSendable
? S.of(context).exchange_result_confirm(
trade.amount ?? fetchingLabel,
trade.from.toString(),
walletName)
: S.of(context).exchange_result_description(
trade.amount ?? fetchingLabel, trade.from.toString()),
textAlign: TextAlign.left,
style: TextStyle(
fontSize: 13.0,
color: Theme.of(context).primaryTextTheme.title.color),
),
),
Text(
S.of(context).exchange_result_write_down_ID,
textAlign: TextAlign.left,
style: TextStyle(
fontSize: 13.0,
color: Theme.of(context).accentTextTheme.title.color),
)
],
);
}),
bottomSection: Observer(
builder: (_) => tradeStore.trade.from == CryptoCurrency.xmr &&
!(sendStore.state is TransactionCreatedSuccessfully)
? Container(
padding: EdgeInsets.only(top: 20.0, bottom: 20.0),
child: LoadingPrimaryButton(
isDisabled: tradeStore.trade.inputAddress == null ||
tradeStore.trade.inputAddress.isEmpty,
isLoading: sendStore.state is CreatingTransaction ||
sendStore.state is TransactionCommitted,
onPressed: () => sendStore.createTransaction(
address: tradeStore.trade.inputAddress,
amount: tradeStore.trade.amount),
text: tradeStore.trade.provider ==
ExchangeProviderDescription.xmrto
? S.of(context).confirm
: S.of(context).send_xmr,
color: Theme.of(context)
.primaryTextTheme
.button
.backgroundColor,
borderColor: Theme.of(context)
.primaryTextTheme
.button
.decorationColor),
)
: Offstage()),
);
}
void _setEffects(BuildContext context) {
if (_effectsInstalled) {
return;
}
final sendStore = Provider.of<SendStore>(context);
reaction((_) => sendStore.state, (state) {
if (state is SendingFailed) {
WidgetsBinding.instance.addPostFrameCallback((_) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(S.of(context).error),
content: Text(state.error),
actions: <Widget>[
FlatButton(
child: Text(S.of(context).ok),
onPressed: () => Navigator.of(context).pop())
],
);
});
});
}
if (state is TransactionCreatedSuccessfully) {
WidgetsBinding.instance.addPostFrameCallback((_) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(S.of(context).sending),
content: Text(S.of(context).transaction_sent),
actions: <Widget>[
FlatButton(
child: Text(S.of(context).ok),
onPressed: () => Navigator.of(context).pop())
],
);
});
});
}
});
_effectsInstalled = true;
}
}
@@ -0,0 +1,30 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class CopyButton extends StatelessWidget {
final VoidCallback onPressed;
final Color color;
final Color borderColor;
final String text;
const CopyButton({
@required this.onPressed,
@required this.text,
@required this.color,
@required this.borderColor});
@override
Widget build(BuildContext context) {
return ButtonTheme(
minWidth: double.infinity,
height: 44.0,
child: FlatButton(
onPressed: onPressed,
color: color,
shape: RoundedRectangleBorder(side: BorderSide(color: borderColor), borderRadius: BorderRadius.circular(10.0)),
child: Text(text, style: TextStyle(fontSize: 14.0)),
)
);
}
}
@@ -0,0 +1,66 @@
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:cake_wallet/generated/i18n.dart';
class TimerWidget extends StatefulWidget {
final DateTime expiratedAt;
final Color color;
TimerWidget(this.expiratedAt, {this.color = Colors.black});
@override
createState() => TimerWidgetState();
}
class TimerWidgetState extends State<TimerWidget> {
int _leftSeconds;
int _minutes;
int _seconds;
bool _isExpired;
Timer _timer;
TimerWidgetState();
@override
void initState() {
super.initState();
final start = DateTime.now();
_isExpired = false;
_leftSeconds = widget.expiratedAt.difference(start).inSeconds;
_recalculate();
WidgetsBinding.instance.addPostFrameCallback((_) {
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
if (_isExpired) {
timer.cancel();
}
_leftSeconds--;
_isExpired = _leftSeconds <= 0;
_recalculate();
setState(() {});
});
});
}
@override
void dispose() {
if (_timer != null) _timer.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return _isExpired
? Text(S.of(context).expired, style: TextStyle(fontSize: 14.0, color: Colors.red))
: Text(
S.of(context).time(_minutes.toString(), _seconds.toString()),
style: TextStyle(fontSize: 14.0, color: widget.color),
);
}
void _recalculate() {
_minutes = _leftSeconds ~/ 60;
_seconds = _leftSeconds % 60;
}
}
+90
View File
@@ -0,0 +1,90 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/stores/settings/settings_store.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
class FaqPage extends BasePage {
String get title => S.current.faq;
@override
Widget body(BuildContext context) {
return FutureBuilder(
builder: (context, snapshot) {
var faqItems = json.decode(snapshot.data.toString());
return ListView.separated(
itemBuilder: (BuildContext context, int index) {
final itemTitle = faqItems[index]["question"];
final itemChild = faqItems[index]["answer"];
return ExpansionTile(
title: Text(
itemTitle
),
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Expanded(
child: Container(
padding: EdgeInsets.only(
left: 15.0,
right: 15.0
),
child: Text(
itemChild,
),
)
)
],
)
],
);
},
separatorBuilder: (_, __) => Divider(
color: Theme.of(context).dividerTheme.color,
height: 1.0,
),
itemCount: faqItems == null ? 0 : faqItems.length,
);
},
future: rootBundle.loadString(getFaqPath(context)),
);
}
String getFaqPath(BuildContext context) {
final settingsStore = Provider.of<SettingsStore>(context);
switch (settingsStore.languageCode) {
case 'en':
return 'assets/faq/faq_en.json';
case 'ru':
return 'assets/faq/faq_ru.json';
case 'es':
return 'assets/faq/faq_es.json';
case 'ja':
return 'assets/faq/faq_ja.json';
case 'ko':
return 'assets/faq/faq_ko.json';
case 'hi':
return 'assets/faq/faq_hi.json';
case 'de':
return 'assets/faq/faq_de.json';
case 'zh':
return 'assets/faq/faq_zh.json';
case 'pt':
return 'assets/faq/faq_pt.json';
case 'pl':
return 'assets/faq/faq_pl.json';
case 'nl':
return 'assets/faq/faq_nl.json';
default:
return 'assets/faq/faq_en.json';
}
}
}
+114
View File
@@ -0,0 +1,114 @@
// import 'package:cake_wallet/src/stores/auth/auth_state.dart';
// import 'package:cake_wallet/src/stores/login/login_store.dart';
// import 'package:flutter/material.dart';
// import 'package:flutter/cupertino.dart';
// import 'package:mobx/mobx.dart';
// import 'package:provider/provider.dart';
// import 'package:cake_wallet/src/screens/pin_code/pin_code.dart';
// import 'package:cake_wallet/src/screens/base_page.dart';
// class LoginPage extends BasePage {
// @override
// Widget leading(BuildContext context) => Container();
// @override
// Widget body(BuildContext context) => _LoginPinCode();
// }
// class _LoginPinCode extends PinCode {
// _LoginPinCode([Key key]) : super((_, __) => null, false, key);
// @override
// _LoginPinCodeState createState() => _LoginPinCodeState();
// }
// class _LoginPinCodeState extends PinCodeState<_LoginPinCode> {
// LoginStore _loginStore;
// String title = 'Enter your PIN';
// @override
// Future onPinCodeEntered(PinCodeState state) async {
// final password = pin.fold("", (ac, val) => ac + '$val');
// await _loginStore.authStore.auth(password: password);
// super.onPinCodeEntered(state);
// }
// @override
// Widget build(BuildContext context) {
// _setLoginStore(store: Provider.of<LoginStore>(context));
// return body(context);
// }
// void _setLoginStore({LoginStore store}) {
// if (_loginStore != null) {
// return;
// }
// _loginStore = store;
// reaction((_) => _loginStore.authStore.state, (state) {
// if (state is AuthenticationFailure) {
// WidgetsBinding.instance.addPostFrameCallback((_) {
// clear();
// Scaffold.of(context).showSnackBar(
// SnackBar(
// content: Text(state.error),
// backgroundColor: Colors.red,
// ),
// );
// });
// }
// if (state is AuthenticationBanned) {
// WidgetsBinding.instance.addPostFrameCallback((_) {
// clear();
// Scaffold.of(context).showSnackBar(
// SnackBar(
// content: Text(state.error),
// backgroundColor: Colors.red,
// ),
// );
// });
// }
// if (state is AuthenticationInProgress) {
// WidgetsBinding.instance.addPostFrameCallback((_) {
// Scaffold.of(context).hideCurrentSnackBar();
// Scaffold.of(context).showSnackBar(
// SnackBar(
// content: Text('Authentication'),
// backgroundColor: Colors.green,
// ),
// );
// });
// }
// });
// reaction((_) => _loginStore.state, (state) {
// if (state is LoadedCurrentWalletFailure) {
// WidgetsBinding.instance.addPostFrameCallback((_) {
// clear();
// Scaffold.of(context).showSnackBar(
// SnackBar(
// content: Text(state.errorMessage),
// backgroundColor: Colors.red,
// ),
// );
// });
// }
// if (state is LoadingCurrentWallet) {
// WidgetsBinding.instance.addPostFrameCallback((_) {
// Scaffold.of(context).hideCurrentSnackBar();
// Scaffold.of(context).showSnackBar(
// SnackBar(
// content: Text('Loading your wallet'),
// backgroundColor: Colors.green,
// ),
// );
// });
// }
// });
// }
// }
@@ -0,0 +1,121 @@
import 'package:mobx/mobx.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/stores/wallet_creation/wallet_creation_store.dart';
import 'package:cake_wallet/src/stores/wallet_creation/wallet_creation_state.dart';
import 'package:cake_wallet/src/domain/services/wallet_list_service.dart';
import 'package:cake_wallet/src/domain/services/wallet_service.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/palette.dart';
class NewWalletPage extends BasePage {
final WalletListService walletsService;
final WalletService walletService;
final SharedPreferences sharedPreferences;
String get title => S.current.new_wallet;
NewWalletPage(
{@required this.walletsService,
@required this.walletService,
@required this.sharedPreferences});
@override
Widget body(BuildContext context) => WalletNameForm();
}
class WalletNameForm extends StatefulWidget {
@override
createState() => _WalletNameFormState();
}
class _WalletNameFormState extends State<WalletNameForm> {
final _formKey = GlobalKey<FormState>();
final nameController = TextEditingController();
@override
Widget build(BuildContext context) {
final walletCreationStore = Provider.of<WalletCreationStore>(context);
reaction((_) => walletCreationStore.state, (state) {
if (state is WalletCreatedSuccessfully) {
Navigator.of(context).popUntil((route) => route.isFirst);
}
if (state is WalletCreationFailure) {
WidgetsBinding.instance.addPostFrameCallback((_) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
content: Text(state.error),
actions: <Widget>[
FlatButton(
child: Text(S.of(context).ok),
onPressed: () => Navigator.of(context).pop(),
),
],
);
});
});
}
});
return ScrollableWithBottomSection(
content: Column(children: [
Padding(
padding: EdgeInsets.only(bottom: 10),
child: Image.asset('assets/images/bitmap.png',
height: 224, width: 400),
),
Padding(
padding: EdgeInsets.only(left: 20, right: 20, bottom: 20),
child: Form(
key: _formKey,
child: TextFormField(
style: TextStyle(
fontSize: 24.0,
color: Theme.of(context).accentTextTheme.subtitle.color),
controller: nameController,
decoration: InputDecoration(
hintStyle: TextStyle(
fontSize: 24.0, color: Theme.of(context).hintColor),
hintText: S.of(context).wallet_name,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Palette.cakeGreen, width: 2.0)),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).focusColor,
width: 1.0))),
validator: (value) {
walletCreationStore.validateWalletName(value);
return walletCreationStore.errorMessage;
},
)),
)
]),
bottomSection: Observer(
builder: (context) {
return LoadingPrimaryButton(
onPressed: () {
if (_formKey.currentState.validate()) {
walletCreationStore.create(name: nameController.text);
}
},
text: S.of(context).continue_text,
color: Theme.of(context).primaryTextTheme.button.backgroundColor,
borderColor:
Theme.of(context).primaryTextTheme.button.decorationColor,
isLoading: walletCreationStore.state is WalletIsCreating,
);
},
));
}
}
+215
View File
@@ -0,0 +1,215 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/stores/node_list/node_list_store.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
class NewNodePage extends BasePage {
@override
String get title => S.current.node_new;
@override
Widget body(BuildContext context) => NewNodePageForm();
}
class NewNodePageForm extends StatefulWidget {
@override
createState() => NewNodeFormState();
}
class NewNodeFormState extends State<NewNodePageForm> {
final _formKey = GlobalKey<FormState>();
final _nodeAddressController = TextEditingController();
final _nodePortController = TextEditingController();
final _loginController = TextEditingController();
final _passwordController = TextEditingController();
@override
void dispose() {
_nodeAddressController.dispose();
_nodePortController.dispose();
_loginController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final nodeList = Provider.of<NodeListStore>(context);
return Form(
key: _formKey,
child: Column(
children: <Widget>[
Expanded(
child: Container(
padding: EdgeInsets.only(left: 38.0, right: 38.0, top: 0),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Row(
children: <Widget>[
Expanded(
child: TextFormField(
style: TextStyle(fontSize: 14.0),
decoration: InputDecoration(
hintStyle:
TextStyle(color: Palette.wildDarkBlue),
hintText: S.of(context).node_address,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Palette.cakeGreen, width: 2.0)),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).focusColor,
width: 1.0))),
controller: _nodeAddressController,
validator: (value) {
nodeList.validateNodeAddress(value);
return nodeList.errorMessage;
},
),
)
],
),
SizedBox(height: 10.0),
Row(
children: <Widget>[
Expanded(
child: TextFormField(
style: TextStyle(fontSize: 14.0),
keyboardType: TextInputType.numberWithOptions(
signed: false, decimal: false),
decoration: InputDecoration(
hintStyle:
TextStyle(color: Palette.wildDarkBlue),
hintText: S.of(context).node_port,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Palette.cakeGreen, width: 2.0)),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).focusColor,
width: 1.0))),
controller: _nodePortController,
validator: (value) {
nodeList.validateNodePort(value);
return nodeList.errorMessage;
},
),
)
],
),
SizedBox(height: 10.0),
Row(
children: <Widget>[
Expanded(
child: TextFormField(
style: TextStyle(fontSize: 14.0),
decoration: InputDecoration(
hintStyle:
TextStyle(color: Palette.wildDarkBlue),
hintText: S.of(context).login,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Palette.cakeGreen, width: 2.0)),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).focusColor,
width: 1.0))),
controller: _loginController,
validator: (value) => null,
),
)
],
),
SizedBox(height: 10.0),
Row(
children: <Widget>[
Expanded(
child: TextFormField(
style: TextStyle(fontSize: 14.0),
decoration: InputDecoration(
hintStyle:
TextStyle(color: Palette.wildDarkBlue),
hintText: S.of(context).password,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Palette.cakeGreen, width: 2.0)),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).focusColor,
width: 1.0))),
controller: _passwordController,
validator: (value) => null,
),
)
],
)
],
),
),
)),
Container(
padding: EdgeInsets.only(bottom: 20.0, left: 20.0, right: 20.0),
child: Row(
children: <Widget>[
Flexible(
child: Container(
padding: EdgeInsets.only(right: 8.0),
child: PrimaryButton(
onPressed: () {
_nodeAddressController.text = '';
_nodePortController.text = '';
_loginController.text = '';
_passwordController.text = '';
},
text: S.of(context).reset,
color: Theme.of(context)
.accentTextTheme
.button
.backgroundColor,
borderColor: Theme.of(context)
.accentTextTheme
.button
.decorationColor),
)),
Flexible(
child: Container(
padding: EdgeInsets.only(left: 8.0),
child: PrimaryButton(
onPressed: () async {
if (!_formKey.currentState.validate()) {
return;
}
await nodeList.addNode(
address: _nodeAddressController.text,
port: _nodePortController.text,
login: _loginController.text,
password: _passwordController.text);
Navigator.of(context).pop();
},
text: S.of(context).save,
color: Theme.of(context)
.primaryTextTheme
.button
.backgroundColor,
borderColor: Theme.of(context)
.primaryTextTheme
.button
.decorationColor,
),
)),
],
),
)
],
));
}
}
+19
View File
@@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
import 'package:cake_wallet/palette.dart';
class NodeIndicator extends StatelessWidget {
final color;
NodeIndicator({this.color = Palette.red});
@override
Widget build(BuildContext context) {
return Container(
width: 10.0,
height: 10.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: color),
);
}
}
+241
View File
@@ -0,0 +1,241 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/nodes/node_indicator.dart';
import 'package:cake_wallet/src/stores/node_list/node_list_store.dart';
import 'package:cake_wallet/src/stores/settings/settings_store.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
class NodeListPage extends BasePage {
NodeListPage();
String get title => S.current.nodes;
@override
Widget trailing(context) {
final nodeList = Provider.of<NodeListStore>(context);
return Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ButtonTheme(
minWidth: double.minPositive,
child: FlatButton(
onPressed: () async {
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
S.of(context).node_reset_settings_title,
textAlign: TextAlign.center,
),
content: Text(
S.of(context).nodes_list_reset_to_default_message,
textAlign: TextAlign.center,
),
actions: <Widget>[
FlatButton(
onPressed: () {
Navigator.pop(context);
},
child: Text(S.of(context).cancel)),
FlatButton(
onPressed: () async {
Navigator.pop(context);
await nodeList.reset();
},
child: Text(S.of(context).reset))
],
);
});
},
child: Text(
S.of(context).reset,
style: TextStyle(
fontSize: 16.0,
color: Theme.of(context).primaryTextTheme.subtitle.color),
)),
),
Container(
width: 28.0,
height: 28.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).selectedRowColor),
child: Stack(
alignment: Alignment.center,
children: <Widget>[
Icon(Icons.add, color: Palette.violet, size: 22.0),
ButtonTheme(
minWidth: 28.0,
height: 28.0,
child: FlatButton(
shape: CircleBorder(),
onPressed: () async {
await Navigator.of(context).pushNamed(Routes.newNode);
nodeList.update();
},
child: Offstage()),
)
],
)),
],
);
}
@override
Widget body(context) => NodeListPageBody();
}
class NodeListPageBody extends StatefulWidget {
@override
createState() => NodeListPageBodyState();
}
class NodeListPageBodyState extends State<NodeListPageBody> {
@override
Widget build(BuildContext context) {
final nodeList = Provider.of<NodeListStore>(context);
final settings = Provider.of<SettingsStore>(context);
final currentColor = Theme.of(context).selectedRowColor;
final notCurrentColor = Theme.of(context).backgroundColor;
return Container(
padding: EdgeInsets.only(bottom: 20.0),
child: Column(
children: <Widget>[
Expanded(child: Observer(builder: (context) {
return ListView.separated(
separatorBuilder: (_, __) => Divider(
color: Theme.of(context).dividerTheme.color, height: 1),
itemCount: nodeList.nodes.length,
itemBuilder: (BuildContext context, int index) {
final node = nodeList.nodes[index];
return Observer(builder: (_) {
final isCurrent = settings.node == null
? false
: node.key == settings.node.key;
final content = Container(
color: isCurrent ? currentColor : notCurrentColor,
child: ListTile(
title: Text(
node.uri,
style: TextStyle(
fontSize: 16.0,
color: Theme.of(context)
.primaryTextTheme
.title
.color),
),
trailing: FutureBuilder(
future: nodeList.isNodeOnline(node),
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.done:
return NodeIndicator(
color: snapshot.data
? Palette.green
: Palette.red);
default:
return NodeIndicator();
}
}),
onTap: () async {
if (!isCurrent) {
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
content: Text(
S
.of(context)
.change_current_node(node.uri),
textAlign: TextAlign.center,
),
actions: <Widget>[
FlatButton(
onPressed: () =>
Navigator.pop(context),
child: Text(S.of(context).cancel)),
FlatButton(
onPressed: () async {
Navigator.of(context).pop();
await settings.setCurrentNode(
node: node);
},
child: Text(S.of(context).change)),
],
);
});
}
},
));
return isCurrent
? content
: Dismissible(
key: Key('${node.key}'),
confirmDismiss: (direction) async {
return await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
S.of(context).remove_node,
textAlign: TextAlign.center,
),
content: Text(
S.of(context).remove_node_message,
textAlign: TextAlign.center,
),
actions: <Widget>[
FlatButton(
onPressed: () =>
Navigator.pop(context, false),
child: Text(S.of(context).cancel)),
FlatButton(
onPressed: () =>
Navigator.pop(context, true),
child: Text(S.of(context).remove)),
],
);
});
},
onDismissed: (direction) async =>
await nodeList.remove(node: node),
direction: DismissDirection.endToStart,
background: Container(
padding: EdgeInsets.only(right: 10.0),
alignment: AlignmentDirectional.centerEnd,
color: Palette.red,
child: Column(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: <Widget>[
const Icon(
CupertinoIcons.delete,
color: Colors.white,
),
Text(
S.of(context).delete,
style: TextStyle(color: Colors.white),
)
],
)),
child: content);
});
});
}))
],
),
);
}
}
+245
View File
@@ -0,0 +1,245 @@
import 'package:provider/provider.dart';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/src/stores/settings/settings_store.dart';
import 'package:cake_wallet/generated/i18n.dart';
abstract class PinCodeWidget extends StatefulWidget {
final Function(List<int> pin, PinCodeState state) onPinCodeEntered;
final bool hasLengthSwitcher;
PinCodeWidget({Key key, this.onPinCodeEntered, this.hasLengthSwitcher})
: super(key: key);
}
class PinCode extends PinCodeWidget {
final Function(List<int> pin, PinCodeState state) onPinCodeEntered;
final bool hasLengthSwitcher;
PinCode(this.onPinCodeEntered, this.hasLengthSwitcher, Key key)
: super(key: key);
@override
PinCodeState createState() => PinCodeState();
}
class PinCodeState<T extends PinCodeWidget> extends State<T> {
static const defaultPinLength = 4;
static const sixPinLength = 6;
static const fourPinLength = 4;
static final deleteIconImage = Image.asset('assets/images/delete_icon.png');
static final backArrowImage = Image.asset('assets/images/back_arrow.png');
GlobalKey _gridViewKey = GlobalKey();
int pinLength = defaultPinLength;
List<int> pin = List<int>.filled(defaultPinLength, null);
String title = S.current.enter_your_pin;
double _aspectRatio = 0;
void setTitle(String title) {
setState(() => this.title = title);
}
void clear() {
setState(() => pin = List<int>.filled(pinLength, null));
}
void onPinCodeEntered(PinCodeState state) {
widget.onPinCodeEntered(state.pin, this);
}
void changePinLength(int length) {
List<int> newPin = List<int>.filled(length, null);
setState(() {
pinLength = length;
pin = newPin;
});
}
setDefaultPinLength() {
final settingsStore = Provider.of<SettingsStore>(context);
pinLength = settingsStore.defaultPinLength;
changePinLength(pinLength);
}
getCurrentAspectRatio() {
final RenderBox renderBox = _gridViewKey.currentContext.findRenderObject();
double cellWidth = renderBox.size.width / 3;
double cellHeight = renderBox.size.height / 4;
if (cellWidth > 0 && cellHeight > 0) _aspectRatio = cellWidth / cellHeight;
setState(() {});
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback(afterLayout);
}
afterLayout(_) {
setDefaultPinLength();
getCurrentAspectRatio();
}
@override
Widget build(BuildContext context) {
return Scaffold(body: body(context));
}
Widget body(BuildContext context) {
return SafeArea(
child: Container(
color: Theme.of(context).backgroundColor,
padding: EdgeInsets.only(left: 40.0, right: 40.0, bottom: 40.0),
child: Column(children: <Widget>[
Spacer(flex: 2),
Text(title,
style: TextStyle(fontSize: 24, color: Palette.wildDarkBlue)),
Spacer(flex: 3),
Container(
width: 180,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(pinLength, (index) {
const size = 10.0;
final isFilled = pin[index] != null;
return Container(
width: size,
height: size,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isFilled ? Palette.deepPurple : Colors.transparent,
border: Border.all(color: Palette.wildDarkBlue),
));
}),
),
),
Spacer(flex: 2),
if (widget.hasLengthSwitcher) ...[
FlatButton(
onPressed: () {
changePinLength(pinLength == PinCodeState.fourPinLength
? PinCodeState.sixPinLength
: PinCodeState.fourPinLength);
},
child: Text(
_changePinLengthText(),
style: TextStyle(fontSize: 16.0, color: Palette.wildDarkBlue),
))
],
Spacer(flex: 1),
Flexible(
flex: 24,
child: Container(
key: _gridViewKey,
child: _aspectRatio > 0
? GridView.count(
shrinkWrap: true,
crossAxisCount: 3,
childAspectRatio: _aspectRatio,
physics: const NeverScrollableScrollPhysics(),
children: List.generate(12, (index) {
const double marginRight = 15;
const double marginLeft = 15;
if (index == 9) {
return Container(
margin: EdgeInsets.only(
left: marginLeft, right: marginRight),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).buttonColor,
),
);
} else if (index == 10) {
index = 0;
} else if (index == 11) {
return Container(
margin: EdgeInsets.only(
left: marginLeft, right: marginRight),
child: FlatButton(
onPressed: () => _pop(),
color: Theme.of(context).buttonColor,
shape: CircleBorder(),
child: deleteIconImage,
),
);
} else {
index++;
}
return Container(
margin: EdgeInsets.only(
left: marginLeft, right: marginRight),
child: FlatButton(
onPressed: () => _push(index),
color: Theme.of(context)
.accentTextTheme
.title
.backgroundColor,
shape: CircleBorder(),
child: Text('$index',
style: TextStyle(
fontSize: 23.0, color: Palette.blueGrey)),
),
);
}),
)
: null))
]),
));
}
void _push(int num) {
if (_pinLength() >= pinLength) {
return;
}
for (var i = 0; i < pin.length; i++) {
if (pin[i] == null) {
setState(() => pin[i] = num);
break;
}
}
if (_pinLength() == pinLength) {
onPinCodeEntered(this);
}
}
void _pop() {
if (_pinLength() == 0) {
return;
}
for (var i = pin.length - 1; i >= 0; i--) {
if (pin[i] != null) {
setState(() => pin[i] = null);
break;
}
}
}
int _pinLength() {
return pin.fold(0, (v, e) {
if (e != null) {
return v + 1;
}
return v;
});
}
String _changePinLengthText() {
return S.current.use +
(pinLength == PinCodeState.fourPinLength
? '${PinCodeState.sixPinLength}'
: '${PinCodeState.fourPinLength}') +
S.current.digit_pin;
}
}
+30
View File
@@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
import 'package:qr/qr.dart';
import 'package:cake_wallet/src/screens/receive/qr_painter.dart';
class QrImage extends StatelessWidget {
QrImage({
@required String data,
this.size = 100.0,
this.backgroundColor,
Color foregroundColor = Colors.black,
int version = 7,
int errorCorrectionLevel = QrErrorCorrectLevel.L,
}) : _painter = new QrPainter(data, foregroundColor, version, errorCorrectionLevel);
final QrPainter _painter;
final Color backgroundColor;
final double size;
@override
Widget build(BuildContext context) {
return new Container(
width: size,
height: size,
color: backgroundColor,
child: CustomPaint(
painter: _painter,
),
);
}
}
+45
View File
@@ -0,0 +1,45 @@
import 'package:flutter/material.dart';
import 'package:qr/qr.dart';
class QrPainter extends CustomPainter {
QrPainter(
String data,
this.color,
this.version,
this.errorCorrectionLevel,
) : this._qr = new QrCode(version, errorCorrectionLevel) {
_p.color = this.color;
_qr.addData(data);
_qr.make();
}
final QrCode _qr;
final _p = new Paint()..style = PaintingStyle.fill;
final int version;
final int errorCorrectionLevel;
final Color color;
@override
void paint(Canvas canvas, Size size) {
final squareSize = size.shortestSide / _qr.moduleCount;
for (int x = 0; x < _qr.moduleCount; x++) {
for (int y = 0; y < _qr.moduleCount; y++) {
if (_qr.isDark(y, x)) {
final squareRect = new Rect.fromLTWH(x * squareSize, y * squareSize, squareSize, squareSize);
canvas.drawRect(squareRect, _p);
}
}
}
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
if (oldDelegate is QrPainter) {
return this.color != oldDelegate.color || this.errorCorrectionLevel != oldDelegate.errorCorrectionLevel ||
this.version != oldDelegate.version;
}
return false;
}
}
+273
View File
@@ -0,0 +1,273 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:esys_flutter_share/esys_flutter_share.dart';
import 'package:provider/provider.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/stores/subaddress_list/subaddress_list_store.dart';
import 'package:cake_wallet/src/stores/wallet/wallet_store.dart';
import 'package:cake_wallet/src/screens/receive/qr_image.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
class ReceivePage extends BasePage {
bool get isModalBackButton => true;
String get title => S.current.receive;
@override
Widget trailing(BuildContext context) {
final walletStore = Provider.of<WalletStore>(context);
return SizedBox(
height: 37.0,
width: 37.0,
child: ButtonTheme(
minWidth: double.minPositive,
child: FlatButton(
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
padding: EdgeInsets.all(0),
onPressed: () => Share.text(
'Share address', walletStore.subaddress.address, 'text/plain'),
child: Icon(Icons.share, size: 30.0,)),
),
);
}
@override
Widget body(BuildContext context) =>
SingleChildScrollView(child: ReceiveBody());
}
class ReceiveBody extends StatefulWidget {
@override
createState() => ReceiveBodyState();
}
class ReceiveBodyState extends State<ReceiveBody> {
final amountController = TextEditingController();
final _formKey = GlobalKey<FormState>();
@override
void dispose() {
amountController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final walletStore = Provider.of<WalletStore>(context);
final subaddressListStore = Provider.of<SubaddressListStore>(context);
final currentColor = Theme.of(context).selectedRowColor;
final notCurrentColor = Theme.of(context).scaffoldBackgroundColor;
amountController.addListener(() {
if (_formKey.currentState.validate()) {
walletStore.onChangedAmountValue(amountController.text);
} else
walletStore.onChangedAmountValue('');
});
return SafeArea(
child: SingleChildScrollView(
child: Column(
children: <Widget>[
Container(
padding: EdgeInsets.all(35.0),
color: Theme.of(context).backgroundColor,
child: Column(
children: <Widget>[
Observer(builder: (_) {
return Row(
children: <Widget>[
Spacer(
flex: 1,
),
Flexible(
flex: 2,
child: AspectRatio(
aspectRatio: 1.0,
child: Container(
padding: EdgeInsets.all(5),
color: Colors.white,
child: QrImage(
data: walletStore.subaddress.address +
walletStore.amountValue,
backgroundColor: Colors.transparent,
),
),
)),
Spacer(
flex: 1,
)
],
);
}),
Observer(builder: (_) {
return Row(
children: <Widget>[
Expanded(
child: Container(
padding: EdgeInsets.all(20.0),
child: Center(
child: 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,
));
},
child: Text(
walletStore.subaddress.address,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.w600,
color: Theme.of(context)
.primaryTextTheme
.title
.color),
),
),
),
))
],
);
}),
Row(
children: <Widget>[
Expanded(
child: Form(
key: _formKey,
child: TextFormField(
keyboardType:
TextInputType.numberWithOptions(decimal: true),
inputFormatters: [
BlacklistingTextInputFormatter(
new RegExp('[\\-|\\ |\\,]'))
],
style: TextStyle(
fontSize: 14.0,
),
decoration: InputDecoration(
hintStyle: TextStyle(
color: Theme.of(context).hintColor),
hintText: S.of(context).amount,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Palette.cakeGreen,
width: 2.0)),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).focusColor,
width: 1.0))),
validator: (value) {
walletStore.validateAmount(value);
return walletStore.errorMessage;
},
autovalidate: true,
controller: amountController,
)))
],
)
],
),
),
Row(
children: <Widget>[
Expanded(
child: Container(
color: Theme.of(context).accentTextTheme.headline.color,
child: Column(
children: <Widget>[
ListTile(
title: Text(
S.of(context).subaddresses,
style: TextStyle(
fontSize: 16.0,
color: Theme.of(context)
.primaryTextTheme
.headline
.color),
),
trailing: Container(
width: 28.0,
height: 28.0,
decoration: BoxDecoration(
color: Theme.of(context).selectedRowColor,
shape: BoxShape.circle),
child: InkWell(
onTap: () => Navigator.of(context)
.pushNamed(Routes.newSubaddress),
borderRadius: BorderRadius.all(Radius.circular(14.0)),
child: Icon(
Icons.add,
color: Palette.violet,
size: 22.0,
),
),
),
),
Divider(
color: Theme.of(context).dividerTheme.color,
height: 1.0,
)
],
),
))
],
),
Observer(builder: (_) {
return ListView.separated(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: subaddressListStore.subaddresses.length,
separatorBuilder: (context, i) {
return Divider(
color: Theme.of(context).dividerTheme.color,
height: 1.0,
);
},
itemBuilder: (context, i) {
return Observer(builder: (_) {
final subaddress = subaddressListStore.subaddresses[i];
final isCurrent =
walletStore.subaddress.address == subaddress.address;
final label = subaddress.label.isNotEmpty
? subaddress.label
: subaddress.address;
return InkWell(
onTap: () => walletStore.setSubaddress(subaddress),
child: Container(
color: isCurrent ? currentColor : notCurrentColor,
child: Column(children: <Widget>[
ListTile(
title: Text(
label,
style: TextStyle(
fontSize: 16.0,
color: Theme.of(context)
.primaryTextTheme
.headline
.color),
),
)
]),
),
);
});
});
})
],
)));
}
}
+41
View File
@@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:provider/provider.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/primary_button.dart';
import 'package:cake_wallet/src/stores/rescan/rescan_wallet_store.dart';
import 'package:cake_wallet/generated/i18n.dart';
class RescanPage extends BasePage {
final blockchainKey = GlobalKey<BlockchainHeightState>();
@override
String get title => S.current.rescan;
@override
Widget body(BuildContext context) {
final rescanWalletStore = Provider.of<RescanWalletStore>(context);
return Padding(
padding: EdgeInsets.only(left: 20, right: 20, bottom: 20),
child:
Column(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
BlockchainHeightWidget(key: blockchainKey),
Observer(
builder: (_) => LoadingPrimaryButton(
isLoading:
rescanWalletStore.state == RescanWalletState.rescaning,
text: S.of(context).rescan,
onPressed: () async {
await rescanWalletStore.rescanCurrentWallet(
restoreHeight: blockchainKey.currentState.height);
Navigator.of(context).pop();
},
color:
Theme.of(context).primaryTextTheme.button.backgroundColor,
borderColor:
Theme.of(context).primaryTextTheme.button.decorationColor))
]),
);
}
}
@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/routes.dart';
import 'package:flutter/cupertino.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/generated/i18n.dart';
class RestoreOptionsPage extends BasePage {
static const _aspectRatioImage = 2.086;
String get title => S.current.restore_restore_wallet;
Color get backgroundColor => Palette.creamyGrey;
final _imageSeedKeys = Image.asset('assets/images/seedKeys.png');
final _imageRestoreSeed = Image.asset('assets/images/restoreSeed.png');
@override
Widget body(BuildContext context) => Container(
padding: EdgeInsets.only(
left: 20.0,
right: 20.0,
),
child: Column(
children: <Widget>[
Flexible(
child: RestoreButton(
onPressed: () {
Navigator.pushNamed(context, Routes.restoreWalletOptionsFromWelcome);
},
image: _imageSeedKeys,
aspectRatioImage: _aspectRatioImage,
titleColor: Palette.lightViolet,
color: Palette.lightViolet,
title: S.of(context).restore_title_from_seed_keys,
description: S.of(context).restore_description_from_seed_keys,
textButton: S.of(context).restore_next,
),
),
Flexible(
child: RestoreButton(
onPressed: () {},
image: _imageRestoreSeed,
aspectRatioImage: _aspectRatioImage,
titleColor: Palette.cakeGreen,
color: Palette.cakeGreen,
title: S.of(context).restore_title_from_backup,
description: S.of(context).restore_description_from_backup,
textButton: S.of(context).restore_next,
))
],
),
);
}
@@ -0,0 +1,225 @@
import 'package:mobx/mobx.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter/services.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/domain/services/wallet_list_service.dart';
import 'package:cake_wallet/src/domain/services/wallet_service.dart';
import 'package:cake_wallet/src/stores/wallet_restoration/wallet_restoration_store.dart';
import 'package:cake_wallet/src/stores/wallet_restoration/wallet_restoration_state.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/primary_button.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/palette.dart';
class RestoreWalletFromKeysPage extends BasePage {
final WalletListService walletsService;
final WalletService walletService;
final SharedPreferences sharedPreferences;
String get title => S.current.restore_title_from_keys;
RestoreWalletFromKeysPage(
{@required this.walletsService,
@required this.sharedPreferences,
@required this.walletService});
@override
Widget body(BuildContext context) => RestoreFromKeysFrom();
}
class RestoreFromKeysFrom extends StatefulWidget {
@override
createState() => _RestoreFromKeysFromState();
}
class _RestoreFromKeysFromState extends State<RestoreFromKeysFrom> {
final _formKey = GlobalKey<FormState>();
final _blockchainHeightKey = GlobalKey<BlockchainHeightState>();
final _nameController = TextEditingController();
final _addressController = TextEditingController();
final _viewKeyController = TextEditingController();
final _spendKeyController = TextEditingController();
@override
Widget build(BuildContext context) {
final walletRestorationStore = Provider.of<WalletRestorationStore>(context);
reaction((_) => walletRestorationStore.state, (state) {
if (state is WalletRestoredSuccessfully) {
Navigator.of(context).popUntil((route) => route.isFirst);
}
if (state is WalletRestorationFailure) {
WidgetsBinding.instance.addPostFrameCallback((_) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
content: Text(state.error),
actions: <Widget>[
FlatButton(
child: Text(S.of(context).ok),
onPressed: () => Navigator.of(context).pop(),
),
],
);
});
});
}
});
return ScrollableWithBottomSection(
contentPadding: EdgeInsets.only(left: 20.0, right: 20.0, bottom: 20.0),
content: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: EdgeInsets.only(left: 13, right: 13),
child: Column(children: <Widget>[
Row(
children: <Widget>[
Flexible(
child: Container(
padding: EdgeInsets.only(top: 20.0),
child: TextFormField(
style: TextStyle(fontSize: 14.0),
controller: _nameController,
decoration: InputDecoration(
hintStyle:
TextStyle(color: Theme.of(context).hintColor),
hintText: S.of(context).restore_wallet_name,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Palette.cakeGreen,
width: 2.0)),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).focusColor,
width: 1.0))),
validator: (value) {
walletRestorationStore.validateWalletName(value);
return walletRestorationStore.errorMessage;
},
),
))
],
),
Row(
children: <Widget>[
Flexible(
child: Container(
padding: EdgeInsets.only(top: 20.0),
child: TextFormField(
style: TextStyle(fontSize: 14.0),
controller: _addressController,
keyboardType: TextInputType.multiline,
maxLines: null,
decoration: InputDecoration(
hintStyle:
TextStyle(color: Theme.of(context).hintColor),
hintText: S.of(context).restore_address,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Palette.cakeGreen,
width: 2.0)),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).focusColor,
width: 1.0))),
validator: (value) {
walletRestorationStore.validateAddress(value);
return walletRestorationStore.errorMessage;
},
),
))
],
),
Row(
children: <Widget>[
Flexible(
child: Container(
padding: EdgeInsets.only(top: 20.0),
child: TextFormField(
style: TextStyle(fontSize: 14.0),
controller: _viewKeyController,
decoration: InputDecoration(
hintStyle:
TextStyle(color: Theme.of(context).hintColor),
hintText: S.of(context).restore_view_key_private,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Palette.cakeGreen,
width: 2.0)),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).focusColor,
width: 1.0))),
validator: (value) {
walletRestorationStore.validateKeys(value);
return walletRestorationStore.errorMessage;
},
),
))
],
),
Row(
children: <Widget>[
Flexible(
child: Container(
padding: EdgeInsets.only(top: 20.0),
child: TextFormField(
style: TextStyle(fontSize: 14.0),
controller: _spendKeyController,
decoration: InputDecoration(
hintStyle:
TextStyle(color: Theme.of(context).hintColor),
hintText: S.of(context).restore_spend_key_private,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Palette.cakeGreen,
width: 2.0)),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).focusColor,
width: 1.0))),
validator: (value) {
walletRestorationStore.validateKeys(value);
return walletRestorationStore.errorMessage;
},
),
))
],
),
BlockchainHeightWidget(key: _blockchainHeightKey),
]),
)
],
),
),
bottomSection: Observer(builder: (_) {
return LoadingPrimaryButton(
onPressed: () {
if (_formKey.currentState.validate()) {
walletRestorationStore.restoreFromKeys(
name: _nameController.text,
address: _addressController.text,
viewKey: _viewKeyController.text,
spendKey: _spendKeyController.text,
restoreHeight: _blockchainHeightKey.currentState.height);
}
},
text: S.of(context).restore_recover,
color: Theme.of(context).primaryTextTheme.button.backgroundColor,
borderColor:
Theme.of(context).primaryTextTheme.button.decorationColor);
}),
);
}
}
@@ -0,0 +1,124 @@
import 'package:mobx/mobx.dart';
import 'package:provider/provider.dart';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/stores/wallet_restoration/wallet_restoration_store.dart';
import 'package:cake_wallet/src/stores/wallet_restoration/wallet_restoration_state.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/blockchain_height_widget.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/palette.dart';
class RestoreWalletFromSeedDetailsPage extends BasePage {
String get title => S.current.restore_wallet_restore_description;
@override
Widget body(BuildContext context) => RestoreFromSeedDetailsForm();
}
class RestoreFromSeedDetailsForm extends StatefulWidget {
@override
createState() => _RestoreFromSeedDetailsFormState();
}
class _RestoreFromSeedDetailsFormState
extends State<RestoreFromSeedDetailsForm> {
final _formKey = GlobalKey<FormState>();
final _blockchainHeightKey = GlobalKey<BlockchainHeightState>();
final _nameController = TextEditingController();
@override
Widget build(BuildContext context) {
final walletRestorationStore = Provider.of<WalletRestorationStore>(context);
reaction((_) => walletRestorationStore.state, (state) {
if (state is WalletRestoredSuccessfully) {
Navigator.of(context).popUntil((route) => route.isFirst);
}
if (state is WalletRestorationFailure) {
WidgetsBinding.instance.addPostFrameCallback((_) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
content: Text(state.error),
actions: <Widget>[
FlatButton(
child: Text(S.of(context).ok),
onPressed: () => Navigator.of(context).pop(),
),
],
);
});
});
}
});
return ScrollableWithBottomSection(
contentPadding: EdgeInsets.only(left: 20.0, right: 20.0, bottom: 20.0),
content: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: EdgeInsets.only(left: 13, right: 13),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
children: <Widget>[
Flexible(
child: Container(
padding: EdgeInsets.only(top: 20.0),
child: TextFormField(
style: TextStyle(fontSize: 14.0),
controller: _nameController,
decoration: InputDecoration(
hintStyle: TextStyle(
color: Theme.of(context).hintColor),
hintText: S.of(context).restore_wallet_name,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Palette.cakeGreen,
width: 2.0)),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).focusColor,
width: 1.0))),
validator: (value) {
walletRestorationStore
.validateWalletName(value);
return walletRestorationStore.errorMessage;
},
),
))
],
),
BlockchainHeightWidget(key: _blockchainHeightKey),
]))
],
),
),
bottomSection: Observer(builder: (_) {
return LoadingPrimaryButton(
onPressed: () {
if (_formKey.currentState.validate()) {
walletRestorationStore.restoreFromSeed(
name: _nameController.text,
restoreHeight: _blockchainHeightKey.currentState.height);
}
},
isLoading: walletRestorationStore.state is WalletIsRestoring,
text: S.of(context).restore_recover,
color: Theme.of(context).primaryTextTheme.button.backgroundColor,
borderColor:
Theme.of(context).primaryTextTheme.button.decorationColor);
}),
);
}
}
@@ -0,0 +1,105 @@
import 'package:mobx/mobx.dart';
import 'package:provider/provider.dart';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/domain/services/wallet_list_service.dart';
import 'package:cake_wallet/src/domain/services/wallet_service.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/stores/wallet_restoration/wallet_restoration_store.dart';
import 'package:cake_wallet/src/widgets/seed_widget.dart';
class RestoreWalletFromSeedPage extends BasePage {
final WalletListService walletsService;
final WalletService walletService;
final SharedPreferences sharedPreferences;
String get title => S.current.restore_title_from_seed;
RestoreWalletFromSeedPage(
{@required this.walletsService,
@required this.walletService,
@required this.sharedPreferences});
@override
Widget body(BuildContext context) => RestoreFromSeedForm();
}
class RestoreFromSeedForm extends StatefulWidget {
@override
createState() => _RestoreFromSeedFormState();
}
class _RestoreFromSeedFormState extends State<RestoreFromSeedForm> {
final _seedKey = GlobalKey<SeedWidgetState>();
bool _reactionSet = false;
@override
Widget build(BuildContext context) {
final walletRestorationStore = Provider.of<WalletRestorationStore>(context);
WidgetsBinding.instance.addPostFrameCallback(
(_) => _setReactions(context, walletRestorationStore));
return GestureDetector(
onTap: () {
SystemChannels.textInput.invokeMethod('TextInput.hide');
},
child: Container(
padding: EdgeInsets.only(left: 20.0, right: 20.0, bottom: 20.0),
child: Column(
children: <Widget>[
Expanded(
child: SeedWidget(
key: _seedKey,
onMnemoticChange: (seed) => walletRestorationStore.setSeed(seed),
)),
Container(
alignment: Alignment.bottomCenter,
child: PrimaryButton(
onPressed: () {
if (!walletRestorationStore.isValid) {
return;
}
Navigator.of(context).pushNamed(
Routes.restoreWalletFromSeedDetails,
arguments: _seedKey.currentState.items);
},
text: S.of(context).restore_next,
color: Theme.of(context)
.primaryTextTheme
.button
.backgroundColor,
borderColor: Theme.of(context)
.primaryTextTheme
.button
.decorationColor))
],
),
),
);
}
void _setReactions(BuildContext context, WalletRestorationStore store) {
if (_reactionSet) {
return;
}
reaction((_) => store.errorMessage, (errorMessage) {
if (errorMessage == null || errorMessage.isEmpty) {
_seedKey.currentState.validated();
} else {
_seedKey.currentState.invalidate();
}
_seedKey.currentState.setErrorMessage(errorMessage);
});
_reactionSet = true;
}
}

Some files were not shown because too many files have changed in this diff Show More