mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-20 06:08:34 +02:00
Merged 4.0.9
This commit is contained in:
@@ -60,7 +60,7 @@ class AddressValidator extends TextValidator {
|
||||
case CryptoCurrency.bnb:
|
||||
return [42];
|
||||
case CryptoCurrency.btc:
|
||||
return [33, 34, 42];
|
||||
return [33, 34, 42, 62];
|
||||
case CryptoCurrency.dai:
|
||||
return [42];
|
||||
case CryptoCurrency.dash:
|
||||
|
||||
@@ -4,7 +4,11 @@ class InitialExecutionState extends ExecutionState {}
|
||||
|
||||
class IsExecutingState extends ExecutionState {}
|
||||
|
||||
class ExecutedSuccessfullyState extends ExecutionState {}
|
||||
class ExecutedSuccessfullyState extends ExecutionState {
|
||||
ExecutedSuccessfullyState({this.payload});
|
||||
|
||||
final dynamic payload;
|
||||
}
|
||||
|
||||
class FailureState extends ExecutionState {
|
||||
FailureState(this.error);
|
||||
|
||||
@@ -7,7 +7,7 @@ class MoneroLabelValidator extends TextValidator {
|
||||
MoneroLabelValidator({@required CryptoCurrency type})
|
||||
: super(
|
||||
errorMessage: S.current.error_text_account_name,
|
||||
pattern: '^[a-zA-Z0-9_]{1,15}\$',
|
||||
pattern: '^[a-zA-Z0-9_ ]{1,15}\$',
|
||||
minLength: 1,
|
||||
maxLength: 15);
|
||||
}
|
||||
|
||||
+18
-2
@@ -22,6 +22,7 @@ import 'package:cake_wallet/src/screens/nodes/nodes_list_page.dart';
|
||||
import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart';
|
||||
import 'package:cake_wallet/src/screens/rescan/rescan_page.dart';
|
||||
import 'package:cake_wallet/src/screens/restore/wallet_restore_page.dart';
|
||||
import 'package:cake_wallet/src/screens/seed/pre_seed_page.dart';
|
||||
import 'package:cake_wallet/src/screens/seed/wallet_seed_page.dart';
|
||||
import 'package:cake_wallet/src/screens/send/send_template_page.dart';
|
||||
import 'package:cake_wallet/src/screens/settings/change_language.dart';
|
||||
@@ -49,6 +50,7 @@ import 'package:cake_wallet/store/wallet_list_store.dart';
|
||||
import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/exchange/exchange_trade_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart';
|
||||
import 'package:cake_wallet/view_model/node_list/node_list_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/rescan_view_model.dart';
|
||||
@@ -274,7 +276,7 @@ Future setup(
|
||||
getIt.registerFactory(() => MoneroAccountListPage(
|
||||
accountListViewModel: getIt.get<MoneroAccountListViewModel>()));
|
||||
|
||||
getIt.registerFactory(() {
|
||||
/*getIt.registerFactory(() {
|
||||
final wallet = getIt.get<AppStore>().wallet;
|
||||
|
||||
if (wallet is MoneroWallet) {
|
||||
@@ -287,7 +289,19 @@ Future setup(
|
||||
|
||||
getIt.registerFactory(() => MoneroAccountEditOrCreatePage(
|
||||
moneroAccountCreationViewModel:
|
||||
getIt.get<MoneroAccountEditOrCreateViewModel>()));
|
||||
getIt.get<MoneroAccountEditOrCreateViewModel>()));*/
|
||||
|
||||
getIt.registerFactoryParam<MoneroAccountEditOrCreateViewModel,
|
||||
AccountListItem, void>(
|
||||
(AccountListItem account, _) => MoneroAccountEditOrCreateViewModel(
|
||||
(getIt.get<AppStore>().wallet as MoneroWallet).accountList,
|
||||
accountListItem: account));
|
||||
|
||||
getIt.registerFactoryParam<MoneroAccountEditOrCreatePage, AccountListItem,
|
||||
void>(
|
||||
(AccountListItem account, _) => MoneroAccountEditOrCreatePage(
|
||||
moneroAccountCreationViewModel:
|
||||
getIt.get<MoneroAccountEditOrCreateViewModel>(param1: account)));
|
||||
|
||||
getIt.registerFactory(() {
|
||||
final appStore = getIt.get<AppStore>();
|
||||
@@ -411,4 +425,6 @@ Future setup(
|
||||
void Function(BuildContext, WalletType), bool>(
|
||||
(para1, param2) => NewWalletTypePage(getIt.get<WalletNewVM>(),
|
||||
onTypeSelected: para1, isNewWallet: param2));
|
||||
|
||||
getIt.registerFactory(() => PreSeedPage());
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ class Node extends HiveObject with Keyable {
|
||||
{@required this.uri,
|
||||
@required WalletType type,
|
||||
this.login,
|
||||
this.password}) {
|
||||
this.password,
|
||||
this.useSSL}) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@@ -22,7 +23,8 @@ class Node extends HiveObject with Keyable {
|
||||
: uri = map['uri'] as String ?? '',
|
||||
login = map['login'] as String,
|
||||
password = map['password'] as String,
|
||||
typeRaw = map['typeRaw'] as int;
|
||||
typeRaw = map['typeRaw'] as int,
|
||||
useSSL = map['useSSL'] as bool;
|
||||
|
||||
static const boxName = 'Nodes';
|
||||
|
||||
@@ -38,6 +40,11 @@ class Node extends HiveObject with Keyable {
|
||||
@HiveField(3)
|
||||
int typeRaw;
|
||||
|
||||
@HiveField(4)
|
||||
bool useSSL;
|
||||
|
||||
bool get isSSL => useSSL ?? false;
|
||||
|
||||
@override
|
||||
dynamic get keyIndex {
|
||||
_keyIndex ??= key;
|
||||
|
||||
@@ -14,7 +14,8 @@ class TradeIsCreatedSuccessfully extends ExchangeTradeState {
|
||||
}
|
||||
|
||||
class TradeIsCreatedFailure extends ExchangeTradeState {
|
||||
TradeIsCreatedFailure({@required this.error});
|
||||
TradeIsCreatedFailure({@required this.title, @required this.error});
|
||||
|
||||
final String title;
|
||||
final String error;
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import 'package:cake_wallet/exchange/xmrto/xmrto_trade_request.dart';
|
||||
import 'package:cake_wallet/exchange/trade_not_created_exeption.dart';
|
||||
import 'package:cake_wallet/exchange/exchange_provider_description.dart';
|
||||
import 'package:cake_wallet/exchange/trade_not_found_exeption.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
|
||||
class XMRTOExchangeProvider extends ExchangeProvider {
|
||||
XMRTOExchangeProvider()
|
||||
@@ -90,12 +91,25 @@ class XMRTOExchangeProvider extends ExchangeProvider {
|
||||
Future<Trade> createTrade({TradeRequest request}) async {
|
||||
final _request = request as XMRTOTradeRequest;
|
||||
final url = originalApiUri + _orderCreateUriSuffix;
|
||||
final _amount = _request.isBTCRequest
|
||||
? _request.receiveAmount
|
||||
: _request.amount;
|
||||
|
||||
final _amountCurrency = _request.isBTCRequest
|
||||
? _request.to.toString()
|
||||
: _request.from.toString();
|
||||
|
||||
final pattern = '^([0-9]+([.\,][0-9]{0,8})?|[.\,][0-9]{1,8})\$';
|
||||
final isValid = RegExp(pattern).hasMatch(_amount);
|
||||
|
||||
if (!isValid) {
|
||||
throw TradeNotCreatedException(description,
|
||||
description: S.current.xmr_to_error_description);
|
||||
}
|
||||
|
||||
final body = {
|
||||
'amount':
|
||||
_request.isBTCRequest ? _request.receiveAmount : _request.amount,
|
||||
'amount_currency': _request.isBTCRequest
|
||||
? _request.to.toString()
|
||||
: _request.from.toString(),
|
||||
'amount': _amount,
|
||||
'amount_currency': _amountCurrency,
|
||||
'btc_dest_address': _request.address
|
||||
};
|
||||
final response = await post(url,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -40,6 +40,10 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance> with Store {
|
||||
fullBalance: monero_wallet.getFullBalance(accountIndex: 0),
|
||||
unlockedBalance: monero_wallet.getFullBalance(accountIndex: 0));
|
||||
_onAccountChangeReaction = reaction((_) => account, (Account account) {
|
||||
balance = MoneroBalance(
|
||||
fullBalance: monero_wallet.getFullBalance(accountIndex: account.id),
|
||||
unlockedBalance:
|
||||
monero_wallet.getUnlockedBalance(accountIndex: account.id));
|
||||
subaddressList.update(accountIndex: account.id);
|
||||
subaddress = subaddressList.subaddresses.first;
|
||||
address = subaddress.address;
|
||||
@@ -94,7 +98,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance> with Store {
|
||||
bool _isSavingAfterNewTransaction;
|
||||
|
||||
Future<void> init() async {
|
||||
await accountList.update();
|
||||
accountList.update();
|
||||
account = accountList.accounts.first;
|
||||
subaddressList.update(accountIndex: account.id ?? 0);
|
||||
subaddress = subaddressList.getAll().first;
|
||||
@@ -147,8 +151,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance> with Store {
|
||||
address: node.uri,
|
||||
login: node.login,
|
||||
password: node.password,
|
||||
useSSL: false,
|
||||
// FIXME: hardcoded value
|
||||
useSSL: node.isSSL,
|
||||
isLightWallet: false); // FIXME: hardcoded value
|
||||
syncStatus = ConnectedSyncStatus();
|
||||
} catch (e) {
|
||||
@@ -256,6 +259,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance> with Store {
|
||||
monero_wallet.rescanBlockchainAsync();
|
||||
await startSync();
|
||||
_askForUpdateBalance();
|
||||
accountList.update();
|
||||
await _askForUpdateTransactionHistory();
|
||||
await save();
|
||||
await walletInfo.save();
|
||||
@@ -363,17 +367,20 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance> with Store {
|
||||
|
||||
void _onNewBlock(int height, int blocksLeft, double ptc) async {
|
||||
if (walletInfo.isRecovery) {
|
||||
_askForUpdateTransactionHistory();
|
||||
await _askForUpdateTransactionHistory();
|
||||
_askForUpdateBalance();
|
||||
accountList.update();
|
||||
}
|
||||
|
||||
if (blocksLeft < 100) {
|
||||
await _askForUpdateTransactionHistory();
|
||||
_askForUpdateBalance();
|
||||
accountList.update();
|
||||
syncStatus = SyncedSyncStatus();
|
||||
await _afterSyncSave();
|
||||
|
||||
if (walletInfo.isRecovery) {
|
||||
setAsRecovered();
|
||||
await setAsRecovered();
|
||||
}
|
||||
} else {
|
||||
syncStatus = SyncingSyncStatus(blocksLeft, ptc);
|
||||
|
||||
+9
-2
@@ -2,7 +2,9 @@ import 'package:cake_wallet/entities/contact_record.dart';
|
||||
import 'package:cake_wallet/entities/transaction_description.dart';
|
||||
import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart';
|
||||
import 'package:cake_wallet/src/screens/restore/wallet_restore_page.dart';
|
||||
import 'package:cake_wallet/src/screens/seed/pre_seed_page.dart';
|
||||
import 'package:cake_wallet/store/settings_store.dart';
|
||||
import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cake_wallet/routes.dart';
|
||||
@@ -70,7 +72,7 @@ Route<dynamic> createRoute(RouteSettings settings) {
|
||||
return CupertinoPageRoute<void>(
|
||||
builder: (_) => getIt.get<NewWalletTypePage>(
|
||||
param1: (BuildContext context, WalletType _) =>
|
||||
Navigator.of(context).pushNamed(Routes.seed, arguments: true),
|
||||
Navigator.of(context).pushNamed(Routes.preSeed, arguments: true),
|
||||
param2: true));
|
||||
|
||||
case Routes.newWallet:
|
||||
@@ -245,7 +247,8 @@ Route<dynamic> createRoute(RouteSettings settings) {
|
||||
|
||||
case Routes.accountCreation:
|
||||
return CupertinoPageRoute<String>(
|
||||
builder: (_) => getIt.get<MoneroAccountEditOrCreatePage>());
|
||||
builder: (_) => getIt.get<MoneroAccountEditOrCreatePage>(
|
||||
param1: settings.arguments as AccountListItem));
|
||||
|
||||
case Routes.addressBook:
|
||||
return MaterialPageRoute<void>(
|
||||
@@ -306,6 +309,10 @@ Route<dynamic> createRoute(RouteSettings settings) {
|
||||
return MaterialPageRoute<void>(
|
||||
builder: (_) => getIt.get<LanguageListPage>());
|
||||
|
||||
case Routes.preSeed:
|
||||
return MaterialPageRoute<void>(
|
||||
builder: (_) => getIt.get<PreSeedPage>());
|
||||
|
||||
default:
|
||||
return MaterialPageRoute<void>(
|
||||
builder: (_) => Scaffold(
|
||||
|
||||
@@ -47,4 +47,5 @@ class Routes {
|
||||
static const exchangeTemplate = '/exchange_template';
|
||||
static const restoreWalletType = '/restore_wallet_type';
|
||||
static const restoreWallet = '/restore_wallet';
|
||||
static const preSeed = '/pre_seed';
|
||||
}
|
||||
@@ -39,7 +39,7 @@ class AddressPage extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
S.of(context).addresses,
|
||||
S.of(context).accounts_subaddresses,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:cake_wallet/entities/wallet_type.dart';
|
||||
import 'package:cake_wallet/src/screens/dashboard/wallet_menu.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
|
||||
// FIXME: terrible design.
|
||||
|
||||
@@ -147,7 +148,7 @@ class MenuWidgetState extends State<MenuWidget> {
|
||||
),
|
||||
if (widget.dashboardViewModel.subname !=
|
||||
null)
|
||||
Text(
|
||||
Observer(builder: (_) => Text(
|
||||
widget.dashboardViewModel.subname,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
@@ -156,7 +157,7 @@ class MenuWidgetState extends State<MenuWidget> {
|
||||
.decorationColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 12),
|
||||
)
|
||||
))
|
||||
],
|
||||
),
|
||||
))
|
||||
|
||||
@@ -547,7 +547,7 @@ class ExchangePage extends BasePage {
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertWithOneAction(
|
||||
alertTitle: S.of(context).error,
|
||||
alertTitle: S.of(context).provider_error(state.title),
|
||||
alertContent: state.error,
|
||||
buttonText: S.of(context).ok,
|
||||
buttonAction: () => Navigator.of(context).pop());
|
||||
@@ -555,6 +555,7 @@ class ExchangePage extends BasePage {
|
||||
});
|
||||
}
|
||||
if (state is TradeIsCreatedSuccessfully) {
|
||||
exchangeViewModel.reset();
|
||||
Navigator.of(context).pushNamed(Routes.exchangeConfirm);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -103,7 +103,12 @@ class MoneroAccountListPage extends StatelessWidget {
|
||||
accountListViewModel
|
||||
.select(account);
|
||||
Navigator.of(context).pop();
|
||||
});
|
||||
},
|
||||
onEdit: () async =>
|
||||
await Navigator.of(context)
|
||||
.pushNamed(
|
||||
Routes.accountCreation,
|
||||
arguments: account));
|
||||
},
|
||||
),
|
||||
isAlwaysShowScrollThumb
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
|
||||
class AccountTile extends StatelessWidget {
|
||||
AccountTile({
|
||||
@required this.isCurrent,
|
||||
@required this.accountName,
|
||||
@required this.onTap
|
||||
@required this.onTap,
|
||||
@required this.onEdit
|
||||
});
|
||||
|
||||
final bool isCurrent;
|
||||
final String accountName;
|
||||
final VoidCallback onTap;
|
||||
final Function() onTap;
|
||||
final Function() onEdit;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -20,7 +24,7 @@ class AccountTile extends StatelessWidget {
|
||||
? Theme.of(context).textTheme.subtitle.color
|
||||
: Theme.of(context).textTheme.display4.color;
|
||||
|
||||
return GestureDetector(
|
||||
final Widget cell = GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
height: 77,
|
||||
@@ -39,5 +43,17 @@ class AccountTile extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return isCurrent ? cell : Slidable(
|
||||
key: Key(accountName),
|
||||
child: cell,
|
||||
actionPane: SlidableDrawerActionPane(),
|
||||
secondaryActions: <Widget>[
|
||||
IconSlideAction(
|
||||
caption: S.of(context).edit,
|
||||
color: Colors.blue,
|
||||
icon: Icons.edit,
|
||||
onTap: () => onEdit?.call())
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
import 'package:cake_wallet/core/execution_state.dart';
|
||||
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
|
||||
import 'package:cake_wallet/src/widgets/standard_checkbox.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
@@ -67,6 +71,39 @@ class NodeCreateOrEditPage extends BasePage {
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) {
|
||||
|
||||
reaction((_) => nodeCreateOrEditViewModel.connectionState,
|
||||
(ExecutionState state) {
|
||||
if (state is ExecutedSuccessfullyState) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) =>
|
||||
AlertWithOneAction(
|
||||
alertTitle: S.of(context).new_node_testing,
|
||||
alertContent: state.payload as bool
|
||||
? S.of(context).node_connection_successful
|
||||
: S.of(context).node_connection_failed,
|
||||
buttonText: S.of(context).ok,
|
||||
buttonAction: () => Navigator.of(context).pop()));
|
||||
});
|
||||
}
|
||||
|
||||
if (state is FailureState) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertWithOneAction(
|
||||
alertTitle: S.of(context).error,
|
||||
alertContent: state.error,
|
||||
buttonText: S.of(context).ok,
|
||||
buttonAction: () => Navigator.of(context).pop());
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.only(left: 24, right: 24),
|
||||
child: ScrollableWithBottomSection(
|
||||
@@ -122,7 +159,22 @@ class NodeCreateOrEditPage extends BasePage {
|
||||
)
|
||||
)
|
||||
],
|
||||
)
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 20),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Observer(
|
||||
builder: (_) => StandardCheckbox(
|
||||
value: nodeCreateOrEditViewModel.useSSL,
|
||||
onChanged: (value) =>
|
||||
nodeCreateOrEditViewModel.useSSL = value,
|
||||
caption: S.of(context).use_ssl,
|
||||
))
|
||||
],
|
||||
))
|
||||
]
|
||||
],
|
||||
)),
|
||||
@@ -133,9 +185,18 @@ class NodeCreateOrEditPage extends BasePage {
|
||||
Flexible(
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(right: 8.0),
|
||||
child: PrimaryButton(
|
||||
onPressed: () => nodeCreateOrEditViewModel.reset(),
|
||||
text: S.of(context).reset,
|
||||
child: LoadingPrimaryButton(
|
||||
onPressed: () async {
|
||||
if (!_formKey.currentState.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
await nodeCreateOrEditViewModel.connect();
|
||||
},
|
||||
isLoading: nodeCreateOrEditViewModel
|
||||
.connectionState is IsExecutingState,
|
||||
text: S.of(context).node_test,
|
||||
isDisabled: !nodeCreateOrEditViewModel.isReady,
|
||||
color: Colors.orange,
|
||||
textColor: Colors.white),
|
||||
)),
|
||||
@@ -154,7 +215,9 @@ class NodeCreateOrEditPage extends BasePage {
|
||||
text: S.of(context).save,
|
||||
color: Theme.of(context).accentTextTheme.body2.color,
|
||||
textColor: Colors.white,
|
||||
isDisabled: !nodeCreateOrEditViewModel.isReady,
|
||||
isDisabled: (!nodeCreateOrEditViewModel.isReady)||
|
||||
(nodeCreateOrEditViewModel
|
||||
.connectionState is IsExecutingState),
|
||||
),
|
||||
)),
|
||||
],
|
||||
|
||||
@@ -99,9 +99,9 @@ class NodeListPage extends BasePage {
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
// FIXME: Add translation.
|
||||
return AlertWithTwoActions(
|
||||
alertTitle: 'Change current node',
|
||||
alertTitle: S.of(context)
|
||||
.change_current_node_title,
|
||||
alertContent: S
|
||||
.of(context)
|
||||
.change_current_node(node.uri),
|
||||
|
||||
@@ -117,7 +117,7 @@ class ReceivePage extends BasePage {
|
||||
Icons.arrow_forward_ios,
|
||||
size: 14,
|
||||
color:
|
||||
Theme.of(context).textTheme.display1.color,
|
||||
Theme.of(context).textTheme.display1.color,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ class ReceivePage extends BasePage {
|
||||
Icons.add,
|
||||
size: 20,
|
||||
color:
|
||||
Theme.of(context).textTheme.display1.color,
|
||||
Theme.of(context).textTheme.display1.color,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -140,13 +140,13 @@ class ReceivePage extends BasePage {
|
||||
addressListViewModel.address.address;
|
||||
final backgroundColor = isCurrent
|
||||
? Theme.of(context)
|
||||
.textTheme
|
||||
.display3
|
||||
.decorationColor
|
||||
.textTheme
|
||||
.display3
|
||||
.decorationColor
|
||||
: Theme.of(context)
|
||||
.textTheme
|
||||
.display2
|
||||
.decorationColor;
|
||||
.textTheme
|
||||
.display2
|
||||
.decorationColor;
|
||||
final textColor = isCurrent
|
||||
? Theme.of(context).textTheme.display3.color
|
||||
: Theme.of(context).textTheme.display2.color;
|
||||
@@ -155,8 +155,7 @@ class ReceivePage extends BasePage {
|
||||
isCurrent: isCurrent,
|
||||
backgroundColor: backgroundColor,
|
||||
textColor: textColor,
|
||||
onTap: (_) =>
|
||||
addressListViewModel.setAddress(item),
|
||||
onTap: (_) => addressListViewModel.setAddress(item),
|
||||
onEdit: () => Navigator.of(context).pushNamed(
|
||||
Routes.newSubaddress,
|
||||
arguments: item));
|
||||
@@ -166,11 +165,11 @@ class ReceivePage extends BasePage {
|
||||
return index != 0
|
||||
? cell
|
||||
: ClipRRect(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(30),
|
||||
topRight: Radius.circular(30)),
|
||||
child: cell,
|
||||
);
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(30),
|
||||
topRight: Radius.circular(30)),
|
||||
child: cell,
|
||||
);
|
||||
})),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -14,6 +14,7 @@ class AddressCell extends StatelessWidget {
|
||||
address: item.address,
|
||||
name: item.name,
|
||||
isCurrent: isCurrent,
|
||||
isPrimary: item.isPrimary,
|
||||
backgroundColor: backgroundColor,
|
||||
textColor: textColor,
|
||||
onTap: onTap,
|
||||
@@ -23,6 +24,7 @@ class AddressCell extends StatelessWidget {
|
||||
{@required this.address,
|
||||
@required this.name,
|
||||
@required this.isCurrent,
|
||||
@required this.isPrimary,
|
||||
@required this.backgroundColor,
|
||||
@required this.textColor,
|
||||
this.onTap,
|
||||
@@ -31,6 +33,7 @@ class AddressCell extends StatelessWidget {
|
||||
final String address;
|
||||
final String name;
|
||||
final bool isCurrent;
|
||||
final bool isPrimary;
|
||||
final Color backgroundColor;
|
||||
final Color textColor;
|
||||
final Function(String) onTap;
|
||||
@@ -56,7 +59,7 @@ class AddressCell extends StatelessWidget {
|
||||
),
|
||||
));
|
||||
|
||||
return isCurrent
|
||||
return (isCurrent || isPrimary)
|
||||
? cell
|
||||
: Slidable(
|
||||
key: Key(address),
|
||||
|
||||
@@ -52,19 +52,9 @@ class QRWidget extends StatelessWidget {
|
||||
))))),
|
||||
Spacer(flex: 3)
|
||||
]),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 20),
|
||||
child: Text(
|
||||
S.of(context).scan_qr_code,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).indicatorColor),
|
||||
),
|
||||
),
|
||||
isAmountFieldShow
|
||||
? Padding(
|
||||
padding: EdgeInsets.only(top: 40),
|
||||
padding: EdgeInsets.only(top: 60),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import 'package:cake_wallet/di.dart';
|
||||
import 'package:cake_wallet/routes.dart';
|
||||
import 'package:cake_wallet/store/settings_store.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.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';
|
||||
|
||||
class PreSeedPage extends BasePage {
|
||||
static final imageLight = Image.asset('assets/images/pre_seed_light.png');
|
||||
static final imageDark = Image.asset('assets/images/pre_seed_dark.png');
|
||||
|
||||
@override
|
||||
Widget leading(BuildContext context) => null;
|
||||
|
||||
@override
|
||||
String get title => S.current.pre_seed_title;
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) {
|
||||
final image =
|
||||
getIt.get<SettingsStore>().isDarkTheme ? imageDark : imageLight;
|
||||
|
||||
return WillPopScope(
|
||||
onWillPop: () async => false,
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(24),
|
||||
child: Column(
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 2,
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: FittedBox(child: image, fit: BoxFit.contain))),
|
||||
Flexible(
|
||||
flex: 3,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 70, left: 16, right: 16),
|
||||
child: Text(
|
||||
S.of(context).pre_seed_description,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.normal,
|
||||
color: Theme.of(context)
|
||||
.primaryTextTheme
|
||||
.caption
|
||||
.color),
|
||||
),
|
||||
),
|
||||
PrimaryButton(
|
||||
onPressed: () => Navigator.of(context)
|
||||
.popAndPushNamed(Routes.seed, arguments: true),
|
||||
text: S.of(context).pre_seed_button_text,
|
||||
color: Theme.of(context).accentTextTheme.body2.color,
|
||||
textColor: Colors.white)
|
||||
],
|
||||
))
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -86,7 +86,7 @@ class WalletSeedPage extends BasePage {
|
||||
final image =
|
||||
getIt.get<SettingsStore>().isDarkTheme ? imageDark : imageLight;
|
||||
|
||||
return Container(
|
||||
return WillPopScope(onWillPop: () async => false, child: Container(
|
||||
padding: EdgeInsets.all(24),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
@@ -194,6 +194,6 @@ class WalletSeedPage extends BasePage {
|
||||
],
|
||||
))
|
||||
],
|
||||
));
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,7 +243,7 @@ class WalletListBodyState extends State<WalletListBody> {
|
||||
changeProcessText(S.of(context).creating_new_wallet);
|
||||
await widget.walletListViewModel.walletNewVM.create(options: 'English'); // FIXME: Unnamed constant
|
||||
hideProgressText();
|
||||
await Navigator.of(context).pushNamed(Routes.seed, arguments: true);
|
||||
await Navigator.of(context).pushNamed(Routes.preSeed);
|
||||
} catch(e) {
|
||||
changeProcessText(S.of(context).creating_new_wallet_error(e.toString()));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class StandardCheckbox extends StatefulWidget {
|
||||
StandardCheckbox({
|
||||
@required this.value,
|
||||
this.caption = '',
|
||||
@required this.onChanged});
|
||||
|
||||
final bool value;
|
||||
final String caption;
|
||||
final Function(bool) onChanged;
|
||||
|
||||
@override
|
||||
StandardCheckboxState createState() =>
|
||||
StandardCheckboxState(value, caption, onChanged);
|
||||
}
|
||||
|
||||
class StandardCheckboxState extends State<StandardCheckbox> {
|
||||
StandardCheckboxState(this.value, this.caption, this.onChanged);
|
||||
|
||||
bool value;
|
||||
String caption;
|
||||
Function(bool) onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
value = !value;
|
||||
onChanged(value);
|
||||
setState(() {});
|
||||
},
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
height: 24.0,
|
||||
width: 24.0,
|
||||
margin: EdgeInsets.only(
|
||||
right: 10.0,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context)
|
||||
.primaryTextTheme
|
||||
.caption
|
||||
.color,
|
||||
width: 1.0),
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(8.0)),
|
||||
color: Theme.of(context).backgroundColor),
|
||||
child: value
|
||||
? Icon(
|
||||
Icons.check,
|
||||
color: Colors.blue,
|
||||
size: 20.0,
|
||||
)
|
||||
: Offstage(),
|
||||
),
|
||||
Text(
|
||||
caption,
|
||||
style: TextStyle(
|
||||
fontSize: 16.0,
|
||||
color: Theme.of(context)
|
||||
.primaryTextTheme
|
||||
.title
|
||||
.color),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import 'package:mobx/mobx.dart';
|
||||
import 'package:cake_wallet/exchange/exchange_provider_description.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart';
|
||||
|
||||
part 'trade_filter_store.g.dart';
|
||||
part'trade_filter_store.g.dart';
|
||||
|
||||
class TradeFilterStore = TradeFilterStoreBase with _$TradeFilterStore;
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart';
|
||||
import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart';
|
||||
import 'package:cake_wallet/entities/transaction_history.dart';
|
||||
import 'package:cake_wallet/monero/account.dart';
|
||||
import 'package:cake_wallet/monero/monero_balance.dart';
|
||||
import 'package:cake_wallet/monero/monero_transaction_history.dart';
|
||||
import 'package:cake_wallet/monero/monero_transaction_info.dart';
|
||||
import 'package:cake_wallet/monero/monero_wallet.dart';
|
||||
import 'package:cake_wallet/entities/balance_display_mode.dart';
|
||||
import 'package:cake_wallet/entities/crypto_currency.dart';
|
||||
@@ -74,28 +79,53 @@ abstract class DashboardViewModelBase with Store {
|
||||
wallet ??= appStore.wallet;
|
||||
type = wallet.type;
|
||||
|
||||
transactions = ObservableList.of(wallet
|
||||
.transactionHistory.transactions.values
|
||||
.map((transaction) => TransactionListItem(
|
||||
transaction: transaction,
|
||||
balanceViewModel: balanceViewModel,
|
||||
settingsStore: appStore.settingsStore)));
|
||||
|
||||
_reaction = reaction((_) => appStore.wallet, _onWalletChange);
|
||||
// FIXME: fixme
|
||||
|
||||
final _wallet = wallet;
|
||||
|
||||
if (_wallet is MoneroWallet) {
|
||||
subname = _wallet.account?.label;
|
||||
|
||||
_onMoneroAccountChangeReaction = reaction((_) => _wallet.account,
|
||||
(Account account) => _onMoneroAccountChange(_wallet));
|
||||
|
||||
_onMoneroBalanceChangeReaction = reaction((_) => _wallet.balance,
|
||||
(MoneroBalance balance) => _onMoneroTransactionsUpdate(_wallet));
|
||||
|
||||
final _accountTransactions = _wallet
|
||||
.transactionHistory.transactions.values
|
||||
.where((tx) => tx.accountIndex == _wallet.account.id)
|
||||
.toList();
|
||||
|
||||
transactions = ObservableList.of(_accountTransactions.map((transaction) =>
|
||||
TransactionListItem(
|
||||
transaction: transaction,
|
||||
balanceViewModel: balanceViewModel,
|
||||
settingsStore: appStore.settingsStore)));
|
||||
} else {
|
||||
transactions = ObservableList.of(wallet
|
||||
.transactionHistory.transactions.values
|
||||
.map((transaction) => TransactionListItem(
|
||||
transaction: transaction,
|
||||
balanceViewModel: balanceViewModel,
|
||||
settingsStore: appStore.settingsStore)));
|
||||
}
|
||||
|
||||
connectMapToListWithTransform(
|
||||
appStore.wallet.transactionHistory.transactions,
|
||||
transactions,
|
||||
(TransactionInfo val) => TransactionListItem(
|
||||
transaction: val,
|
||||
balanceViewModel: balanceViewModel,
|
||||
settingsStore: appStore.settingsStore));
|
||||
settingsStore: appStore.settingsStore),
|
||||
filter: (TransactionInfo tx) {
|
||||
final wallet = _wallet;
|
||||
if (tx is MoneroTransactionInfo && wallet is MoneroWallet) {
|
||||
return tx.accountIndex == wallet.account.id;
|
||||
}
|
||||
|
||||
final _wallet = wallet;
|
||||
|
||||
if (_wallet is MoneroWallet) {
|
||||
subname = _wallet.account?.label;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@observable
|
||||
@@ -170,6 +200,10 @@ abstract class DashboardViewModelBase with Store {
|
||||
|
||||
ReactionDisposer _reaction;
|
||||
|
||||
ReactionDisposer _onMoneroAccountChangeReaction;
|
||||
|
||||
ReactionDisposer _onMoneroBalanceChangeReaction;
|
||||
|
||||
Future<void> reconnect() async {
|
||||
final node = appStore.settingsStore.getCurrentNode(wallet.type);
|
||||
await wallet.connectToNode(node: node);
|
||||
@@ -180,9 +214,47 @@ abstract class DashboardViewModelBase with Store {
|
||||
this.wallet = wallet;
|
||||
type = wallet.type;
|
||||
name = wallet.name;
|
||||
|
||||
if (wallet is MoneroWallet) {
|
||||
subname = wallet.account?.label;
|
||||
|
||||
_onMoneroAccountChangeReaction?.reaction?.dispose();
|
||||
_onMoneroBalanceChangeReaction?.reaction?.dispose();
|
||||
|
||||
_onMoneroAccountChangeReaction = reaction((_) => wallet.account,
|
||||
(Account account) => _onMoneroAccountChange(wallet));
|
||||
|
||||
_onMoneroBalanceChangeReaction = reaction((_) => wallet.balance,
|
||||
(MoneroBalance balance) => _onMoneroTransactionsUpdate(wallet));
|
||||
|
||||
_onMoneroTransactionsUpdate(wallet);
|
||||
} else {
|
||||
transactions.clear();
|
||||
|
||||
transactions.addAll(wallet.transactionHistory.transactions.values.map(
|
||||
(transaction) => TransactionListItem(
|
||||
transaction: transaction,
|
||||
balanceViewModel: balanceViewModel,
|
||||
settingsStore: appStore.settingsStore)));
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
void _onMoneroAccountChange(MoneroWallet wallet) {
|
||||
subname = wallet.account?.label;
|
||||
_onMoneroTransactionsUpdate(wallet);
|
||||
}
|
||||
|
||||
@action
|
||||
void _onMoneroTransactionsUpdate(MoneroWallet wallet) {
|
||||
transactions.clear();
|
||||
transactions.addAll(wallet.transactionHistory.transactions.values.map(
|
||||
(transaction) => TransactionListItem(
|
||||
|
||||
final _accountTransactions = wallet.transactionHistory.transactions.values
|
||||
.where((tx) => tx.accountIndex == wallet.account.id)
|
||||
.toList();
|
||||
|
||||
transactions.addAll(_accountTransactions.map((transaction) =>
|
||||
TransactionListItem(
|
||||
transaction: transaction,
|
||||
balanceViewModel: balanceViewModel,
|
||||
settingsStore: appStore.settingsStore)));
|
||||
|
||||
@@ -174,7 +174,7 @@ abstract class ExchangeViewModelBase with Store {
|
||||
return;
|
||||
}
|
||||
|
||||
final _amount = double.parse(amount.replaceAll(',', '.'));
|
||||
final _amount = double.parse(amount.replaceAll(',', '.')) ?? 0;
|
||||
provider
|
||||
.calculateAmount(
|
||||
from: depositCurrency,
|
||||
@@ -247,10 +247,12 @@ abstract class ExchangeViewModelBase with Store {
|
||||
if (limitsState is LimitsLoadedSuccessfully && amount != null) {
|
||||
if (double.parse(amount) < limits.min) {
|
||||
tradeState = TradeIsCreatedFailure(
|
||||
title: provider.title,
|
||||
error: S.current.error_text_minimal_limit('${provider.description}',
|
||||
'${limits.min}', currency.toString()));
|
||||
} else if (limits.max != null && double.parse(amount) > limits.max) {
|
||||
tradeState = TradeIsCreatedFailure(
|
||||
title: provider.title,
|
||||
error: S.current.error_text_maximum_limit('${provider.description}',
|
||||
'${limits.max}', currency.toString()));
|
||||
} else {
|
||||
@@ -262,11 +264,14 @@ abstract class ExchangeViewModelBase with Store {
|
||||
await trades.add(trade);
|
||||
tradeState = TradeIsCreatedSuccessfully(trade: trade);
|
||||
} catch (e) {
|
||||
tradeState = TradeIsCreatedFailure(error: e.toString());
|
||||
tradeState = TradeIsCreatedFailure(
|
||||
title: provider.title,
|
||||
error: e.toString());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tradeState = TradeIsCreatedFailure(
|
||||
title: provider.title,
|
||||
error: S.current
|
||||
.error_text_limits_loading_failed('${provider.description}'));
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ abstract class MoneroAccountEditOrCreateViewModelBase with Store {
|
||||
{AccountListItem accountListItem})
|
||||
: state = InitialExecutionState(),
|
||||
isEdit = accountListItem != null,
|
||||
label = accountListItem?.label??'',
|
||||
_accountListItem = accountListItem;
|
||||
|
||||
final bool isEdit;
|
||||
|
||||
@@ -12,7 +12,9 @@ class NodeCreateOrEditViewModel = NodeCreateOrEditViewModelBase
|
||||
|
||||
abstract class NodeCreateOrEditViewModelBase with Store {
|
||||
NodeCreateOrEditViewModelBase(this._nodeSource, this._wallet)
|
||||
: state = InitialExecutionState();
|
||||
: state = InitialExecutionState(),
|
||||
connectionState = InitialExecutionState(),
|
||||
useSSL = false;
|
||||
|
||||
@observable
|
||||
ExecutionState state;
|
||||
@@ -29,6 +31,12 @@ abstract class NodeCreateOrEditViewModelBase with Store {
|
||||
@observable
|
||||
String password;
|
||||
|
||||
@observable
|
||||
ExecutionState connectionState;
|
||||
|
||||
@observable
|
||||
bool useSSL;
|
||||
|
||||
@computed
|
||||
bool get isReady =>
|
||||
(address?.isNotEmpty ?? false) && (port?.isNotEmpty ?? false);
|
||||
@@ -54,6 +62,7 @@ abstract class NodeCreateOrEditViewModelBase with Store {
|
||||
port = '';
|
||||
login = '';
|
||||
password = '';
|
||||
useSSL = false;
|
||||
}
|
||||
|
||||
@action
|
||||
@@ -61,11 +70,25 @@ abstract class NodeCreateOrEditViewModelBase with Store {
|
||||
try {
|
||||
state = IsExecutingState();
|
||||
final node =
|
||||
Node(uri: uri, type: _wallet.type, login: login, password: password);
|
||||
Node(uri: uri, type: _wallet.type, login: login, password: password,
|
||||
useSSL: useSSL);
|
||||
await _nodeSource.add(node);
|
||||
state = ExecutedSuccessfullyState();
|
||||
} catch (e) {
|
||||
state = FailureState(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> connect() async {
|
||||
try {
|
||||
connectionState = IsExecutingState();
|
||||
final node =
|
||||
Node(uri: uri, type: _wallet.type, login: login, password: password);
|
||||
final isAlive = await node.requestNode();
|
||||
connectionState = ExecutedSuccessfullyState(payload: isAlive);
|
||||
} catch (e) {
|
||||
connectionState = FailureState(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,11 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:cake_wallet/utils/list_item.dart';
|
||||
|
||||
class WalletAddressListItem extends ListItem {
|
||||
const WalletAddressListItem({@required this.address, this.name, this.id})
|
||||
: super();
|
||||
const WalletAddressListItem({@required this.address, @required this.isPrimary,
|
||||
this.name, this.id}) : super();
|
||||
|
||||
final int id;
|
||||
final bool isPrimary;
|
||||
final String address;
|
||||
final String name;
|
||||
|
||||
|
||||
@@ -97,16 +97,28 @@ abstract class WalletAddressListViewModelBase with Store {
|
||||
final addressList = ObservableList<ListItem>();
|
||||
|
||||
if (wallet is MoneroWallet) {
|
||||
addressList.addAll(wallet.subaddressList.subaddresses.map((subaddress) =>
|
||||
WalletAddressListItem(
|
||||
id: subaddress.id,
|
||||
name: subaddress.label,
|
||||
address: subaddress.address)));
|
||||
final primaryAddress = wallet.subaddressList.subaddresses.first;
|
||||
addressList.addAll(wallet.subaddressList.subaddresses.map((subaddress) {
|
||||
final isPrimary = subaddress == primaryAddress;
|
||||
|
||||
return WalletAddressListItem(
|
||||
id: subaddress.id,
|
||||
isPrimary: isPrimary,
|
||||
name: subaddress.label,
|
||||
address: subaddress.address);
|
||||
}));
|
||||
}
|
||||
|
||||
if (wallet is BitcoinWallet) {
|
||||
final bitcoinAddresses = wallet.addresses.map((addr) =>
|
||||
WalletAddressListItem(name: addr.label, address: addr.address));
|
||||
final primaryAddress = wallet.addresses.first;
|
||||
final bitcoinAddresses = wallet.addresses.map((addr) {
|
||||
final isPrimary = addr == primaryAddress;
|
||||
|
||||
return WalletAddressListItem(
|
||||
isPrimary: isPrimary,
|
||||
name: addr.label,
|
||||
address: addr.address);
|
||||
});
|
||||
addressList.addAll(bitcoinAddresses);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user