LCW-1191-ios-node-qr-issue (#2568)

* refactor: improve `NodeCreateOrEditPage` and `NodeForm` structure and state management

* fix: eliminate `widget.nodeViewModel` references in constructor
This commit is contained in:
Konstantin Ullrich
2025-10-08 16:59:57 +03:00
committed by GitHub
parent 9a7230fc73
commit 82dfd6afa2
2 changed files with 250 additions and 314 deletions
@@ -1,91 +1,45 @@
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/nodes/widgets/node_form.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.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/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart';
class NodeCreateOrEditPage extends BasePage {
NodeCreateOrEditPage(
{required this.nodeCreateOrEditViewModel, this.editingNode, this.isSelected, this.type})
: _formKey = GlobalKey<FormState>(),
_addressController = TextEditingController(),
_pathController = TextEditingController(),
_portController = TextEditingController(),
_loginController = TextEditingController(),
_passwordController = TextEditingController() {
reaction((_) => nodeCreateOrEditViewModel.address, (String address) {
if (address != _addressController.text) {
_addressController.text = address;
}
});
reaction((_) => nodeCreateOrEditViewModel.port, (String port) {
if (port != _portController.text) {
_portController.text = port;
}
});
if (nodeCreateOrEditViewModel.hasAuthCredentials) {
reaction((_) => nodeCreateOrEditViewModel.login, (String login) {
if (login != _loginController.text) {
_loginController.text = login;
}
});
reaction((_) => nodeCreateOrEditViewModel.password, (String password) {
if (password != _passwordController.text) {
_passwordController.text = password;
}
});
}
_addressController
.addListener(() => nodeCreateOrEditViewModel.address = _addressController.text);
_pathController.addListener(() => nodeCreateOrEditViewModel.path = _pathController.text);
_portController.addListener(() => nodeCreateOrEditViewModel.port = _portController.text);
_loginController.addListener(() => nodeCreateOrEditViewModel.login = _loginController.text);
_passwordController
.addListener(() => nodeCreateOrEditViewModel.password = _passwordController.text);
}
NodeCreateOrEditPage({
required this.nodeCreateOrEditViewModel,
this.editingNode,
this.isSelected,
this.type,
}) : _formKey = GlobalKey<FormState>();
final GlobalKey<FormState> _formKey;
final TextEditingController _addressController;
final TextEditingController _pathController;
final TextEditingController _portController;
final TextEditingController _loginController;
final TextEditingController _passwordController;
final NodeCreateOrEditViewModel nodeCreateOrEditViewModel;
final Node? editingNode;
final bool? isSelected;
final WalletType? type;
@override
String get title => editingNode != null ? S.current.edit_node : S.current.node_new;
@override
Widget trailing(BuildContext context) => IconButton(
onPressed: () async {
await nodeCreateOrEditViewModel.scanQRCodeForNewNode(context);
},
onPressed: () => nodeCreateOrEditViewModel.scanQRCodeForNewNode(context),
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
hoverColor: Colors.transparent,
icon: Image.asset(
'assets/images/qr_code_icon.png',
),
icon: Image.asset('assets/images/qr_code_icon.png'),
);
final NodeCreateOrEditViewModel nodeCreateOrEditViewModel;
final Node? editingNode;
final bool? isSelected;
final WalletType? type;
@override
Widget body(BuildContext context) {
reaction(
@@ -93,116 +47,110 @@ class NodeCreateOrEditPage extends BasePage {
(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(),
),
);
},
(_) => showPopUp<void>(
context: context,
builder: (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(),
);
},
);
},
(_) => showPopUp<void>(
context: context,
builder: (context) => 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(
contentPadding: EdgeInsets.only(bottom: 24.0, top: 8),
content: NodeForm(
formKey: _formKey,
nodeViewModel: nodeCreateOrEditViewModel,
editingNode: editingNode,
type: type,
padding: const EdgeInsets.only(left: 24, right: 24),
child: ScrollableWithBottomSection(
contentPadding: const EdgeInsets.only(bottom: 24.0, top: 8),
content: NodeForm(
formKey: _formKey,
nodeViewModel: nodeCreateOrEditViewModel,
editingNode: editingNode,
type: type,
),
bottomSectionPadding: const EdgeInsets.only(bottom: 24),
bottomSection: Observer(
builder: (_) => Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Flexible(
child: Container(
padding: const EdgeInsets.only(right: 8.0),
child: LoadingPrimaryButton(
onPressed: () async {
final confirmed = await showPopUp<bool>(
context: context,
builder: (context) => AlertWithTwoActions(
alertTitle: S.of(context).remove_node,
alertContent: S.of(context).remove_node_message,
rightButtonText: S.of(context).remove,
leftButtonText: S.of(context).cancel,
actionRightButton: () => Navigator.pop(context, true),
actionLeftButton: () => Navigator.pop(context, false),
),
) ??
false;
if (confirmed) {
await editingNode!.delete();
Navigator.of(context).pop();
}
},
text: S.of(context).delete,
isDisabled: editingNode == null ||
!nodeCreateOrEditViewModel.isReady ||
(isSelected ?? false),
color: Theme.of(context).colorScheme.errorContainer,
textColor: Theme.of(context).colorScheme.onErrorContainer,
),
),
),
Flexible(
child: Container(
padding: const EdgeInsets.only(left: 8.0),
child: PrimaryButton(
onPressed: () async {
if (_formKey.currentState != null && !_formKey.currentState!.validate()) {
return;
}
await nodeCreateOrEditViewModel.save(
editingNode: editingNode, saveAsCurrent: isSelected ?? false);
if (context.mounted) {
Navigator.of(context).pop();
}
},
text: S.of(context).save,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
isDisabled: (!nodeCreateOrEditViewModel.isReady) ||
(nodeCreateOrEditViewModel.connectionState is IsExecutingState),
),
),
),
],
),
bottomSectionPadding: EdgeInsets.only(bottom: 24),
bottomSection: Observer(
builder: (_) => Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Flexible(
child: Container(
padding: EdgeInsets.only(right: 8.0),
child: LoadingPrimaryButton(
onPressed: () async {
final confirmed = await showPopUp<bool>(
context: context,
builder: (BuildContext context) {
return AlertWithTwoActions(
alertTitle: S.of(context).remove_node,
alertContent: S.of(context).remove_node_message,
rightButtonText: S.of(context).remove,
leftButtonText: S.of(context).cancel,
actionRightButton: () => Navigator.pop(context, true),
actionLeftButton: () => Navigator.pop(context, false),
);
},
) ??
false;
if (confirmed) {
await editingNode!.delete();
Navigator.of(context).pop();
}
},
text: S.of(context).delete,
isDisabled: editingNode == null ||
!nodeCreateOrEditViewModel.isReady ||
(isSelected ?? false),
color: Theme.of(context).colorScheme.errorContainer,
textColor: Theme.of(context).colorScheme.onErrorContainer,
),
),
),
Flexible(
child: Container(
padding: EdgeInsets.only(left: 8.0),
child: PrimaryButton(
onPressed: () async {
if (_formKey.currentState != null &&
!_formKey.currentState!.validate()) {
return;
}
await nodeCreateOrEditViewModel.save(
editingNode: editingNode, saveAsCurrent: isSelected ?? false);
if (context.mounted) {
Navigator.of(context).pop();
}
},
text: S.of(context).save,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
isDisabled: (!nodeCreateOrEditViewModel.isReady) ||
(nodeCreateOrEditViewModel.connectionState is IsExecutingState),
),
)),
],
)),
));
),
),
);
}
}
+140 -152
View File
@@ -1,6 +1,7 @@
import 'package:cake_wallet/core/node_address_validator.dart';
import 'package:cake_wallet/core/node_port_validator.dart';
import 'package:cake_wallet/core/socks_proxy_node_address_validator.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
import 'package:cake_wallet/src/widgets/standard_checkbox.dart';
import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart';
@@ -8,15 +9,30 @@ import 'package:cw_core/node.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:mobx/mobx.dart';
class NodeForm extends StatelessWidget {
class NodeForm extends StatefulWidget {
NodeForm({
required this.nodeViewModel,
required this.formKey,
this.editingNode,
this.type,
});
final NodeCreateOrEditViewModel nodeViewModel;
final GlobalKey<FormState> formKey;
final Node? editingNode;
final WalletType? type;
@override
State<StatefulWidget> createState() =>
_NodeFormState(nodeViewModel: nodeViewModel, editingNode: this.editingNode);
}
class _NodeFormState extends State<NodeForm> {
_NodeFormState({
required this.nodeViewModel,
Node? editingNode,
}) : _addressController = TextEditingController(text: editingNode?.uri.host.toString()),
_pathController = TextEditingController(text: editingNode?.path.toString()),
_portController = TextEditingController(
@@ -28,28 +44,25 @@ class NodeForm extends StatelessWidget {
_socksAddressController = TextEditingController(text: editingNode?.socksProxyAddress) {
if (editingNode != null) {
nodeViewModel
..setAddress((editingNode!.uri.host.toString()))
..setPath((editingNode!.path.toString()))
..setPort((editingNode!.uri.hasPort ? editingNode!.uri.port.toString() : ''))
..setPassword((editingNode!.password ?? ''))
..setLogin((editingNode!.login ?? ''))
..setSSL((editingNode!.isSSL))
..setTrusted((editingNode!.trusted))
..setIsEnabledForAutoSwitching((editingNode!.isEnabledForAutoSwitching))
..setSocksProxy((editingNode!.useSocksProxy))
..setSocksProxyAddress((editingNode!.socksProxyAddress ?? ''));
..setAddress(editingNode.uri.host.toString())
..setPath(editingNode.path.toString())
..setPort(editingNode.uri.hasPort ? editingNode.uri.port.toString() : '')
..setPassword(editingNode.password ?? '')
..setLogin(editingNode.login ?? '')
..setSSL(editingNode.isSSL)
..setTrusted(editingNode.trusted)
..setIsEnabledForAutoSwitching(editingNode.isEnabledForAutoSwitching)
..setSocksProxy(editingNode.useSocksProxy)
..setSocksProxyAddress(editingNode.socksProxyAddress ?? '');
}
if (nodeViewModel.hasAuthCredentials) {
reaction((_) => nodeViewModel.login, (String login) {
if (login != _loginController.text) {
_loginController.text = login;
}
if (login != _loginController.text) _loginController.text = login;
});
reaction((_) => nodeViewModel.password, (String password) {
if (password != _passwordController.text) {
_passwordController.text = password;
}
if (password != _passwordController.text) _passwordController.text = password;
});
}
reaction((_) => nodeViewModel.address, (String address) {
@@ -59,15 +72,11 @@ class NodeForm extends StatelessWidget {
});
reaction((_) => nodeViewModel.port, (String port) {
if (port != _portController.text) {
_portController.text = port;
}
if (port != _portController.text) _portController.text = port;
});
reaction((_) => nodeViewModel.path, (String path) {
if (path != _pathController.text) {
_pathController.text = path;
}
if (path != _pathController.text) _pathController.text = path;
});
_addressController.addListener(() => nodeViewModel.address = _addressController.text);
@@ -80,9 +89,6 @@ class NodeForm extends StatelessWidget {
}
final NodeCreateOrEditViewModel nodeViewModel;
final GlobalKey<FormState> formKey;
final Node? editingNode;
final WalletType? type;
final TextEditingController _addressController;
final TextEditingController _pathController;
@@ -92,54 +98,46 @@ class NodeForm extends StatelessWidget {
final TextEditingController _socksAddressController;
@override
Widget build(BuildContext context) {
return Form(
key: formKey,
child: Column(
children: <Widget>[
Row(
children: <Widget>[
Expanded(
child: BaseTextFormField(
controller: _addressController,
hintText: S.of(context).node_address,
validator: type == WalletType.decred
? NodeAddressValidatorDecredBlankException()
: NodeAddressValidator(),
),
)
],
),
SizedBox(height: 10.0),
Widget build(BuildContext context) => Form(
key: widget.formKey,
child: Column(children: <Widget>[
Row(children: <Widget>[
Expanded(
child: BaseTextFormField(
controller: _addressController,
hintText: S.of(context).node_address,
validator: widget.type == WalletType.decred
? NodeAddressValidatorDecredBlankException()
: NodeAddressValidator(),
),
)
]),
const SizedBox(height: 10),
if (nodeViewModel.hasPathSupport) ...[
Row(
children: <Widget>[
Expanded(
child: BaseTextFormField(
controller: _pathController,
hintText: "/path",
validator: NodePathValidator(),
),
)
],
),
SizedBox(height: 10.0),
],
Row(
children: <Widget>[
Row(children: <Widget>[
Expanded(
child: BaseTextFormField(
controller: _portController,
hintText: S.of(context).node_port,
keyboardType: TextInputType.numberWithOptions(signed: false, decimal: false),
validator: NodePortValidator(),
controller: _pathController,
hintText: "/path",
validator: NodePathValidator(),
),
)
],
),
SizedBox(height: 10.0),
]),
const SizedBox(height: 10),
],
Row(children: <Widget>[
Expanded(
child: BaseTextFormField(
controller: _portController,
hintText: S.of(context).node_port,
keyboardType: TextInputType.numberWithOptions(signed: false, decimal: false),
validator: NodePortValidator(),
),
)
]),
const SizedBox(height: 10),
Padding(
padding: EdgeInsets.only(top: 20),
padding: const EdgeInsets.only(top: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
@@ -156,9 +154,9 @@ class NodeForm extends StatelessWidget {
],
),
),
SizedBox(height: 10.0),
const SizedBox(height: 10),
Padding(
padding: EdgeInsets.only(top: 20),
padding: const EdgeInsets.only(top: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
@@ -175,31 +173,27 @@ class NodeForm extends StatelessWidget {
],
),
),
SizedBox(height: 10.0),
const SizedBox(height: 10),
if (nodeViewModel.hasAuthCredentials) ...[
Row(
children: <Widget>[
Expanded(
child: BaseTextFormField(
controller: _loginController,
hintText: S.of(context).login,
),
)
],
),
SizedBox(height: 10.0),
Row(
children: <Widget>[
Expanded(
child: BaseTextFormField(
controller: _passwordController,
hintText: S.of(context).password,
),
)
],
),
Row(children: <Widget>[
Expanded(
child: BaseTextFormField(
controller: _loginController,
hintText: S.of(context).login,
),
)
]),
const SizedBox(height: 10),
Row(children: <Widget>[
Expanded(
child: BaseTextFormField(
controller: _passwordController,
hintText: S.of(context).password,
),
)
]),
Padding(
padding: EdgeInsets.only(top: 20),
padding: const EdgeInsets.only(top: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
@@ -217,66 +211,60 @@ class NodeForm extends StatelessWidget {
),
),
Observer(
builder: (_) => Column(
builder: (_) => Column(children: [
if (nodeViewModel.usesEmbeddedProxy) ...[
Padding(
padding: const EdgeInsets.only(top: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
if (nodeViewModel.usesEmbeddedProxy) ...[
Padding(
padding: EdgeInsets.only(top: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
StandardCheckbox(
value: nodeViewModel.usesEmbeddedProxy,
gradientBackground: false,
borderColor: Theme.of(context).dividerColor,
iconColor: Theme.of(context).colorScheme.primary,
onChanged: null,
caption: 'Embedded Tor SOCKS Proxy',
),
],
),
),
],
Padding(
padding: EdgeInsets.only(top: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
StandardCheckbox(
value: nodeViewModel.useSocksProxy,
borderColor: Theme.of(context).colorScheme.surfaceContainerHighest,
iconColor: Theme.of(context).colorScheme.primary,
onChanged: (value) {
if (!value) {
_socksAddressController.text = '';
}
nodeViewModel.useSocksProxy = value;
},
caption: 'SOCKS Proxy',
),
],
),
StandardCheckbox(
value: nodeViewModel.usesEmbeddedProxy,
gradientBackground: false,
borderColor: Theme.of(context).dividerColor,
iconColor: Theme.of(context).colorScheme.primary,
onChanged: null,
caption: 'Embedded Tor SOCKS Proxy',
),
if (nodeViewModel.useSocksProxy) ...[
SizedBox(height: 10.0),
Row(
children: <Widget>[
Expanded(
child: BaseTextFormField(
controller: _socksAddressController,
hintText: '[<ip>:]<port>',
validator: SocksProxyNodeAddressValidator(),
))
],
),
]
],
)),
),
),
],
Padding(
padding: const EdgeInsets.only(top: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
StandardCheckbox(
value: nodeViewModel.useSocksProxy,
borderColor: Theme.of(context).colorScheme.surfaceContainerHighest,
iconColor: Theme.of(context).colorScheme.primary,
onChanged: (value) {
if (!value) _socksAddressController.text = '';
nodeViewModel.useSocksProxy = value;
},
caption: 'SOCKS Proxy',
),
],
),
),
if (nodeViewModel.useSocksProxy) ...[
const SizedBox(height: 10),
Row(children: <Widget>[
Expanded(
child: BaseTextFormField(
controller: _socksAddressController,
hintText: '[<ip>:]<port>',
validator: SocksProxyNodeAddressValidator(),
),
)
]),
]
]),
),
]
],
),
);
}
]),
);
}