From 433646edc63a3fd7caef4892e4ed5181ef53f171 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 3 Feb 2026 10:58:51 -0600 Subject: [PATCH 01/17] bring back epicboxes in hive boxes --- lib/db/hive/db.dart | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/lib/db/hive/db.dart b/lib/db/hive/db.dart index 3eac4b805..03d0fd303 100644 --- a/lib/db/hive/db.dart +++ b/lib/db/hive/db.dart @@ -11,11 +11,12 @@ import 'dart:isolate'; import 'package:compat/compat.dart' as lib_monero_compat; -import 'package:hive_ce/src/hive_impl.dart'; import 'package:hive_ce/hive.dart' show Box; +import 'package:hive_ce/src/hive_impl.dart'; import 'package:mutex/mutex.dart'; import '../../app_config.dart'; +import '../../models/epicbox_server_model.dart'; import '../../models/exchange/response_objects/trade.dart'; import '../../models/node_model.dart'; import '../../models/notification_model.dart'; @@ -52,6 +53,8 @@ class DB { static const String boxNameDBInfo = "dbInfo"; static const String boxNamePrefs = "prefs"; static const String boxNameOneTimeDialogsShown = "oneTimeDialogsShown"; + static const String boxNameEpicBoxModels = "epicBoxModels"; + static const String boxNamePrimaryEpicBox = "primaryEpicBox"; String _boxNameTxCache({required CryptoCurrency currency}) => "${currency.identifier}_txCache"; @@ -75,6 +78,8 @@ class DB { Box? _boxPrefs; Box? _boxTradeLookup; Box? _boxDBInfo; + late final Box _boxEpicBoxModels; + late final Box _boxPrimaryEpicBoxes; // Box? _boxDesktopData; final Map> _walletBoxes = {}; @@ -115,6 +120,24 @@ class DB { } await hive.openBox(boxNameWalletsToDeleteOnStart); + if (hive.isBoxOpen(boxNameEpicBoxModels)) { + _boxEpicBoxModels = hive.box(boxNameEpicBoxModels); + } else { + _boxEpicBoxModels = await hive.openBox( + boxNameEpicBoxModels, + ); + } + + if (hive.isBoxOpen(boxNamePrimaryEpicBox)) { + _boxPrimaryEpicBoxes = hive.box( + boxNamePrimaryEpicBox, + ); + } else { + _boxPrimaryEpicBoxes = await hive.openBox( + boxNamePrimaryEpicBox, + ); + } + if (hive.isBoxOpen(boxNamePrefs)) { _boxPrefs = hive.box(boxNamePrefs); } else { From cf9861188000d41a3187085ae0139c46fa54b14e Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 3 Feb 2026 11:10:38 -0600 Subject: [PATCH 02/17] feat: register EpicBoxServerModel hive adapter --- lib/main.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/main.dart b/lib/main.dart index dd35ae7b5..0dd2e32c7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -39,6 +39,7 @@ import 'models/exchange/response_objects/trade.dart'; import 'models/models.dart'; import 'models/node_model.dart'; import 'models/notification_model.dart'; +import 'models/epicbox_server_model.dart'; import 'models/trade_wallet_lookup.dart'; import 'pages/campfire_migrate_view.dart'; import 'pages/home_view/home_view.dart'; @@ -155,6 +156,9 @@ void main(List args) async { // node model adapter DB.instance.hive.registerAdapter(NodeModelAdapter()); + // epicbox server model adapter + DB.instance.hive.registerAdapter(EpicBoxServerModelAdapter()); + if (!DB.instance.hive.isAdapterRegistered( lib_monero_compat.WalletInfoAdapter().typeId, )) { From 79fa2bc18d1550e993536ca448c868743f0bd6ec Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 3 Feb 2026 11:11:10 -0600 Subject: [PATCH 03/17] feat: add epicbox server management to node service --- lib/services/node_service.dart | 94 ++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/lib/services/node_service.dart b/lib/services/node_service.dart index c1bc338b3..eeacbc136 100644 --- a/lib/services/node_service.dart +++ b/lib/services/node_service.dart @@ -15,7 +15,9 @@ import 'package:http/http.dart'; import '../app_config.dart'; import '../db/hive/db.dart'; +import '../models/epicbox_server_model.dart'; import '../models/node_model.dart'; +import '../utilities/default_epicboxes.dart'; import '../utilities/default_nodes.dart'; import '../utilities/flutter_secure_storage_interface.dart'; import '../utilities/logger.dart'; @@ -286,6 +288,98 @@ class NodeService extends ChangeNotifier { } } + //============================================================================ + // Epic Box server management + //============================================================================ + + Future updateDefaultEpicBoxes() async { + final primaryEpicBox = getPrimaryEpicBox(); + + for (final defaultEpicBox in DefaultEpicBoxes.all) { + final savedEpicBox = DB.instance.get( + boxName: DB.boxNameEpicBoxModels, + key: defaultEpicBox.id, + ); + if (savedEpicBox == null) { + await DB.instance.put( + boxName: DB.boxNameEpicBoxModels, + key: defaultEpicBox.id, + value: defaultEpicBox, + ); + } else { + await DB.instance.put( + boxName: DB.boxNameEpicBoxModels, + key: savedEpicBox.id, + value: defaultEpicBox.copyWith(enabled: savedEpicBox.enabled), + ); + } + + if (primaryEpicBox != null && primaryEpicBox.id == defaultEpicBox.id) { + await setPrimaryEpicBox( + epicBox: defaultEpicBox.copyWith(enabled: primaryEpicBox.enabled), + ); + } + } + } + + Future setPrimaryEpicBox({ + required EpicBoxServerModel epicBox, + bool shouldNotifyListeners = false, + }) async { + await DB.instance.put( + boxName: DB.boxNamePrimaryEpicBox, + key: 'primary', + value: epicBox, + ); + if (shouldNotifyListeners) { + notifyListeners(); + } + } + + EpicBoxServerModel? getPrimaryEpicBox() { + return DB.instance.get( + boxName: DB.boxNamePrimaryEpicBox, + key: 'primary', + ); + } + + List getEpicBoxes() { + return DB.instance + .values(boxName: DB.boxNameEpicBoxModels) + .toList(); + } + + EpicBoxServerModel? getEpicBoxById({required String id}) { + return DB.instance.get( + boxName: DB.boxNameEpicBoxModels, + key: id, + ); + } + + Future addEpicBox( + EpicBoxServerModel epicBox, + bool shouldNotifyListeners, + ) async { + await DB.instance.put( + boxName: DB.boxNameEpicBoxModels, + key: epicBox.id, + value: epicBox, + ); + if (shouldNotifyListeners) { + notifyListeners(); + } + } + + Future deleteEpicBox(String id, bool shouldNotifyListeners) async { + await DB.instance.delete( + boxName: DB.boxNameEpicBoxModels, + key: id, + ); + if (shouldNotifyListeners) { + notifyListeners(); + } + } + //============================================================================ Future updateCommunityNodes() async { From 8a02f9750004b62d7100b1a6276b4015b2bc90c2 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 3 Feb 2026 11:11:24 -0600 Subject: [PATCH 04/17] feat: add epicbox server connection test utility --- .../test_epicbox_server_connection.dart | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 lib/utilities/test_epicbox_server_connection.dart diff --git a/lib/utilities/test_epicbox_server_connection.dart b/lib/utilities/test_epicbox_server_connection.dart new file mode 100644 index 000000000..bf5faf2e7 --- /dev/null +++ b/lib/utilities/test_epicbox_server_connection.dart @@ -0,0 +1,54 @@ +import 'dart:io'; + +import 'logger.dart'; + +Future _testEpicBoxConnection(String host, int port, bool useSSL) async { + try { + final protocol = useSSL ? 'https' : 'http'; + final client = HttpClient(); + client.connectionTimeout = const Duration(seconds: 5); + + final request = await client.getUrl(Uri.parse('$protocol://$host:$port')); + final response = await request.close(); + final body = await response.transform(const SystemEncoding().decoder).join(); + + client.close(); + + // epicbox servers return an HTML page containing "Epicbox" + return response.statusCode == 200 && body.contains('Epicbox'); + } catch (e) { + Logging.instance.i("_testEpicBoxConnection failed on \"$host:$port\": $e"); + return false; + } +} + +Future testEpicBoxServerConnection( + EpicBoxFormData data, +) async { + if (data.host == null || data.port == null) { + return null; + } + + try { + final useSSL = data.useSSL ?? true; + if (await _testEpicBoxConnection(data.host!, data.port!, useSSL)) { + return data; + } else { + return null; + } + } catch (e, s) { + Logging.instance.w("$e\n$s", error: e, stackTrace: s); + return null; + } +} + +class EpicBoxFormData { + String? name, host; + int? port; + bool? useSSL, isFailover; + + @override + String toString() { + return "{ name: $name, host: $host, port: $port, useSSL: $useSSL }"; + } +} From fd2df8fd5255dfd974c563a78fcbf0f63a2521c2 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 3 Feb 2026 11:14:35 -0600 Subject: [PATCH 05/17] feat: add epicbox server management ui --- lib/main.dart | 1 + .../sub_widgets/wallet_options_button.dart | 56 ++- .../add_edit_epicbox_view.dart | 448 ++++++++++++++++++ .../desktop_manage_epicbox_dialog.dart | 214 +++++++++ lib/services/node_service.dart | 23 +- lib/widgets/epicbox_card.dart | 211 +++++++++ 6 files changed, 941 insertions(+), 12 deletions(-) create mode 100644 lib/pages_desktop_specific/settings/settings_menu/epicbox_settings/add_edit_epicbox_view.dart create mode 100644 lib/pages_desktop_specific/settings/settings_menu/epicbox_settings/desktop_manage_epicbox_dialog.dart create mode 100644 lib/widgets/epicbox_card.dart diff --git a/lib/main.dart b/lib/main.dart index 0dd2e32c7..1d8f99266 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -394,6 +394,7 @@ class _MaterialAppWithThemeState extends ConsumerState unawaited(ref.read(baseCurrenciesProvider).update()); await _nodeService.updateDefaults(); + await _nodeService.updateDefaultEpicBoxes(); await _notificationsService.init( nodeService: _nodeService, tradesService: _tradesService, diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart index 4beb82514..dc7fc8b3a 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart @@ -38,6 +38,7 @@ import '../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart' import '../../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart'; import '../../../addresses/desktop_wallet_addresses_view.dart'; import '../../../password/request_desktop_auth_dialog.dart'; +import '../../../settings/settings_menu/epicbox_settings/desktop_manage_epicbox_dialog.dart'; import 'desktop_delete_wallet_dialog.dart'; enum _WalletOptions { @@ -47,7 +48,8 @@ enum _WalletOptions { showXpub, frostOptions, refreshFromHeight, - showSparkKey; + showSparkKey, + epicBoxSettings; String get prettyName { switch (this) { @@ -65,6 +67,8 @@ enum _WalletOptions { return "Refresh height"; case _WalletOptions.showSparkKey: return "Show Spark View Key"; + case _WalletOptions.epicBoxSettings: + return "Epic Box settings"; } } } @@ -125,6 +129,9 @@ class WalletOptionsButton extends ConsumerWidget { onRefreshHeightPressed: () async { Navigator.of(context).pop(_WalletOptions.refreshFromHeight); }, + onEpicBoxSettingsPressed: () async { + Navigator.of(context).pop(_WalletOptions.epicBoxSettings); + }, walletId: walletId, ); }, @@ -296,6 +303,16 @@ class WalletOptionsButton extends ConsumerWidget { ); } break; + + case _WalletOptions.epicBoxSettings: + unawaited( + showDialog( + context: context, + builder: (context) => + DesktopManageEpicBoxDialog(walletId: walletId), + ), + ); + break; } } }, @@ -327,6 +344,7 @@ class WalletOptionsPopupMenu extends ConsumerWidget { required this.onChangeRepPressed, required this.onFrostMSWalletOptionsPressed, required this.onRefreshHeightPressed, + required this.onEpicBoxSettingsPressed, required this.walletId, }); @@ -336,6 +354,7 @@ class WalletOptionsPopupMenu extends ConsumerWidget { final VoidCallback onChangeRepPressed; final VoidCallback onFrostMSWalletOptionsPressed; final VoidCallback onRefreshHeightPressed; + final VoidCallback onEpicBoxSettingsPressed; final String walletId; @override @@ -515,6 +534,41 @@ class WalletOptionsPopupMenu extends ConsumerWidget { ), ), ), + if (wallet is EpiccashWallet) const SizedBox(height: 8), + if (wallet is EpiccashWallet) + TransparentButton( + onPressed: onEpicBoxSettingsPressed, + child: Padding( + padding: const EdgeInsets.all(8), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SvgPicture.asset( + Assets.svg.node, + width: 20, + height: 20, + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconLeft, + ), + const SizedBox(width: 14), + Expanded( + child: Text( + _WalletOptions.epicBoxSettings.prettyName, + style: + STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: Theme.of( + context, + ).extension()!.textDark, + ), + ), + ), + ], + ), + ), + ), if (xpubEnabled) const SizedBox(height: 8), if (xpubEnabled) TransparentButton( diff --git a/lib/pages_desktop_specific/settings/settings_menu/epicbox_settings/add_edit_epicbox_view.dart b/lib/pages_desktop_specific/settings/settings_menu/epicbox_settings/add_edit_epicbox_view.dart new file mode 100644 index 000000000..ce8aeaff1 --- /dev/null +++ b/lib/pages_desktop_specific/settings/settings_menu/epicbox_settings/add_edit_epicbox_view.dart @@ -0,0 +1,448 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:uuid/uuid.dart'; + +import '../../../../models/epicbox_server_model.dart'; +import '../../../../notifications/show_flush_bar.dart'; +import '../../../../providers/global/node_service_provider.dart'; +import '../../../../utilities/constants.dart'; +import '../../../../utilities/test_epicbox_server_connection.dart'; +import '../../../../utilities/text_styles.dart'; +import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../../../widgets/desktop/desktop_dialog.dart'; +import '../../../../widgets/desktop/primary_button.dart'; +import '../../../../widgets/desktop/secondary_button.dart'; +import '../../../../widgets/icon_widgets/x_icon.dart'; +import '../../../../widgets/stack_text_field.dart'; +import '../../../../widgets/textfield_icon_button.dart'; + +enum AddEditEpicBoxViewType { add, edit } + +class AddEditEpicBoxView extends ConsumerStatefulWidget { + const AddEditEpicBoxView({ + super.key, + required this.viewType, + this.epicBoxId, + required this.onSave, + }); + + final AddEditEpicBoxViewType viewType; + final String? epicBoxId; + final VoidCallback onSave; + + @override + ConsumerState createState() => _AddEditEpicBoxViewState(); +} + +class _AddEditEpicBoxViewState extends ConsumerState { + late final TextEditingController _nameController; + late final TextEditingController _hostController; + late final TextEditingController _portController; + + final _nameFocusNode = FocusNode(); + final _hostFocusNode = FocusNode(); + final _portFocusNode = FocusNode(); + + bool _useSSL = true; + int? port; + + bool get canSave { + return _nameController.text.isNotEmpty && canTestConnection; + } + + bool get canTestConnection { + return _hostController.text.isNotEmpty && + port != null && + port! >= 0 && + port! <= 65535; + } + + Future _testConnection() async { + final data = EpicBoxFormData() + ..name = _nameController.text + ..host = _hostController.text + ..port = port ?? 443 + ..useSSL = _useSSL; + + final result = await testEpicBoxServerConnection(data); + if (!mounted) return; + + if (result != null) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "Connection successful", + context: context, + ), + ); + } else { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Could not connect to server", + context: context, + ), + ); + } + } + + Future _attemptSave() async { + final data = EpicBoxFormData() + ..name = _nameController.text + ..host = _hostController.text + ..port = port ?? 443 + ..useSSL = _useSSL; + + final canConnect = await testEpicBoxServerConnection(data) != null; + + bool shouldSave = canConnect; + + if (!canConnect && mounted) { + await showDialog( + context: context, + useSafeArea: true, + barrierDismissible: true, + builder: (_) => DesktopDialog( + maxWidth: 440, + maxHeight: 300, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: 32), + child: Row( + children: [ + const SizedBox(width: 32), + Text( + "Server currently unreachable", + style: STextStyles.desktopH3(context), + ), + ], + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + top: 16, + bottom: 32, + ), + child: Column( + children: [ + const Spacer(), + Text( + "Would you like to save this server anyways?", + style: STextStyles.desktopTextMedium(context), + ), + const Spacer(flex: 2), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: ButtonHeight.l, + onPressed: () => Navigator.of( + context, + rootNavigator: true, + ).pop(false), + ), + ), + const SizedBox(width: 16), + Expanded( + child: PrimaryButton( + label: "Save", + buttonHeight: ButtonHeight.l, + onPressed: () => Navigator.of( + context, + rootNavigator: true, + ).pop(true), + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ), + ).then((value) { + if (value is bool && value) { + shouldSave = true; + } + }); + } + + if (!shouldSave) return; + + final epicBox = EpicBoxServerModel( + id: widget.epicBoxId ?? const Uuid().v1(), + host: _hostController.text, + port: port ?? 443, + name: _nameController.text, + useSSL: _useSSL, + enabled: true, + isFailover: true, + isDown: false, + ); + + await ref.read(nodeServiceChangeNotifierProvider).addEpicBox(epicBox, true); + widget.onSave(); + + if (mounted) { + Navigator.of(context).pop(); + } + } + + @override + void initState() { + super.initState(); + _nameController = TextEditingController(); + _hostController = TextEditingController(); + _portController = TextEditingController(); + + if (widget.epicBoxId != null) { + final epicBox = ref + .read(nodeServiceChangeNotifierProvider) + .getEpicBoxById(id: widget.epicBoxId!); + if (epicBox != null) { + _nameController.text = epicBox.name; + _hostController.text = epicBox.host; + _portController.text = (epicBox.port ?? 443).toString(); + _useSSL = epicBox.useSSL ?? true; + port = epicBox.port ?? 443; + } + } else { + _portController.text = "443"; + port = 443; + } + } + + @override + void dispose() { + _nameController.dispose(); + _hostController.dispose(); + _portController.dispose(); + _nameFocusNode.dispose(); + _hostFocusNode.dispose(); + _portFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return DesktopDialog( + maxWidth: 580, + maxHeight: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + const SizedBox(width: 8), + const AppBarBackButton(iconSize: 24, size: 40), + Text( + widget.viewType == AddEditEpicBoxViewType.add + ? "Add Epic Box server" + : "Edit Epic Box server", + style: STextStyles.desktopH3(context), + ), + ], + ), + ], + ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + top: 16, + bottom: 32, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: false, + enableSuggestions: false, + controller: _nameController, + focusNode: _nameFocusNode, + style: STextStyles.field(context), + decoration: + standardInputDecoration( + "Server name", + _nameFocusNode, + context, + ).copyWith( + suffixIcon: _nameController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: TextFieldIconButton( + child: const XIcon(), + onTap: () { + _nameController.clear(); + setState(() {}); + }, + ), + ), + ) + : null, + ), + onChanged: (_) => setState(() {}), + ), + ), + const SizedBox(height: 8), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: false, + enableSuggestions: false, + controller: _hostController, + focusNode: _hostFocusNode, + style: STextStyles.field(context), + decoration: + standardInputDecoration( + "Host", + _hostFocusNode, + context, + ).copyWith( + suffixIcon: _hostController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: TextFieldIconButton( + child: const XIcon(), + onTap: () { + _hostController.clear(); + setState(() {}); + }, + ), + ), + ) + : null, + ), + onChanged: (_) => setState(() {}), + ), + ), + const SizedBox(height: 8), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: false, + enableSuggestions: false, + controller: _portController, + focusNode: _portFocusNode, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + keyboardType: TextInputType.number, + style: STextStyles.field(context), + decoration: + standardInputDecoration( + "Port", + _portFocusNode, + context, + ).copyWith( + suffixIcon: _portController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: TextFieldIconButton( + child: const XIcon(), + onTap: () { + _portController.clear(); + setState(() {}); + }, + ), + ), + ) + : null, + ), + onChanged: (value) { + port = int.tryParse(value); + setState(() {}); + }, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + GestureDetector( + onTap: () { + setState(() { + _useSSL = !_useSSL; + }); + }, + child: Container( + color: Colors.transparent, + child: Row( + children: [ + SizedBox( + width: 20, + height: 20, + child: Checkbox( + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + value: _useSSL, + onChanged: (newValue) { + setState(() { + _useSSL = newValue!; + }); + }, + ), + ), + const SizedBox(width: 12), + Text( + "Use SSL", + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + ), + ], + ), + const SizedBox(height: 36), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Test connection", + enabled: canTestConnection, + buttonHeight: ButtonHeight.l, + onPressed: canTestConnection ? _testConnection : null, + ), + ), + const SizedBox(width: 16), + Expanded( + child: PrimaryButton( + label: "Save", + enabled: canSave, + buttonHeight: ButtonHeight.l, + onPressed: canSave ? _attemptSave : null, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/settings/settings_menu/epicbox_settings/desktop_manage_epicbox_dialog.dart b/lib/pages_desktop_specific/settings/settings_menu/epicbox_settings/desktop_manage_epicbox_dialog.dart new file mode 100644 index 000000000..c904c25d8 --- /dev/null +++ b/lib/pages_desktop_specific/settings/settings_menu/epicbox_settings/desktop_manage_epicbox_dialog.dart @@ -0,0 +1,214 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../notifications/show_flush_bar.dart'; +import '../../../../providers/providers.dart'; +import '../../../../themes/stack_colors.dart'; +import '../../../../utilities/assets.dart'; +import '../../../../utilities/test_epicbox_server_connection.dart'; +import '../../../../utilities/text_styles.dart'; +import '../../../../wallets/wallet/impl/epiccash_wallet.dart'; +import '../../../../widgets/custom_buttons/blue_text_button.dart'; +import '../../../../widgets/desktop/desktop_dialog.dart'; +import '../../../../widgets/desktop/desktop_dialog_close_button.dart'; +import '../../../../widgets/epicbox_card.dart'; +import 'add_edit_epicbox_view.dart'; + +class DesktopManageEpicBoxDialog extends ConsumerStatefulWidget { + const DesktopManageEpicBoxDialog({super.key, required this.walletId}); + + final String walletId; + + @override + ConsumerState createState() => + _DesktopManageEpicBoxDialogState(); +} + +class _DesktopManageEpicBoxDialogState + extends ConsumerState { + Future _onConnect(String epicBoxId) async { + final epicBox = ref + .read(nodeServiceChangeNotifierProvider) + .getEpicBoxById(id: epicBoxId); + + if (epicBox == null) return; + + final data = EpicBoxFormData() + ..host = epicBox.host + ..port = epicBox.port ?? 443 + ..useSSL = epicBox.useSSL; + + final canConnect = await testEpicBoxServerConnection(data) != null; + + if (!canConnect && mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + iconAsset: Assets.svg.circleAlert, + message: "Could not connect to server", + context: context, + ), + ); + return; + } + + await ref + .read(nodeServiceChangeNotifierProvider) + .setPrimaryEpicBox(epicBox: epicBox, shouldNotifyListeners: true); + + // update wallet's epicbox config + final wallet = + ref.read(pWallets).getWallet(widget.walletId) as EpiccashWallet; + await wallet.updateEpicboxConfig(epicBox.host, epicBox.port ?? 443); + + if (mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "Connected to ${epicBox.name}", + context: context, + ), + ); + } + } + + void _onEdit(String epicBoxId) { + showDialog( + context: context, + builder: (_) => AddEditEpicBoxView( + viewType: AddEditEpicBoxViewType.edit, + epicBoxId: epicBoxId, + onSave: () {}, + ), + ); + } + + void _onAdd() { + showDialog( + context: context, + builder: (_) => AddEditEpicBoxView( + viewType: AddEditEpicBoxViewType.add, + onSave: () {}, + ), + ); + } + + @override + Widget build(BuildContext context) { + final epicBoxes = ref.watch( + nodeServiceChangeNotifierProvider.select((value) => value.getEpicBoxes()), + ); + final primaryEpicBox = ref.watch( + nodeServiceChangeNotifierProvider.select( + (value) => value.getPrimaryEpicBox(), + ), + ); + + final defaultBoxes = epicBoxes.where((e) => e.isDefault).toList(); + final customBoxes = epicBoxes.where((e) => !e.isDefault).toList(); + + return DesktopDialog( + maxHeight: null, + maxWidth: 580, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Epic Box", style: STextStyles.desktopH3(context)), + const DesktopDialogCloseButton(), + ], + ), + ), + Padding( + padding: const EdgeInsets.only(left: 32, right: 32, top: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Servers", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + CustomTextButton(text: "Add new", onTap: _onAdd), + ], + ), + ), + const SizedBox(height: 12), + Flexible( + child: Padding( + padding: const EdgeInsets.only(left: 20, right: 20, bottom: 20), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (defaultBoxes.isNotEmpty) ...[ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + child: Text( + "Default servers", + style: STextStyles.smallMed12(context).copyWith( + color: Theme.of( + context, + ).extension()!.textDark3, + ), + ), + ), + ...defaultBoxes.map( + (epicBox) => Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: EpicBoxCard( + key: Key("${epicBox.id}_card_key"), + epicBoxId: epicBox.id, + onConnect: () => _onConnect(epicBox.id), + onEdit: () => _onEdit(epicBox.id), + testOnInit: primaryEpicBox?.id == epicBox.id, + ), + ), + ), + ], + if (customBoxes.isNotEmpty) ...[ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + child: Text( + "Custom servers", + style: STextStyles.smallMed12(context).copyWith( + color: Theme.of( + context, + ).extension()!.textDark3, + ), + ), + ), + ...customBoxes.map( + (epicBox) => Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: EpicBoxCard( + key: Key("${epicBox.id}_card_key"), + epicBoxId: epicBox.id, + onConnect: () => _onConnect(epicBox.id), + onEdit: () => _onEdit(epicBox.id), + testOnInit: primaryEpicBox?.id == epicBox.id, + ), + ), + ), + ], + ], + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/services/node_service.dart b/lib/services/node_service.dart index eeacbc136..ad38d8d4b 100644 --- a/lib/services/node_service.dart +++ b/lib/services/node_service.dart @@ -168,15 +168,14 @@ class NodeService extends ChangeNotifier { } List getNodesFor(CryptoCurrency coin) { - final list = - DB.instance - .values(boxName: DB.boxNameNodeModels) - .where( - (e) => - e.coinName == coin.identifier && - !e.id.startsWith(DefaultNodes.defaultNodeIdPrefix), - ) - .toList(); + final list = DB.instance + .values(boxName: DB.boxNameNodeModels) + .where( + (e) => + e.coinName == coin.identifier && + !e.id.startsWith(DefaultNodes.defaultNodeIdPrefix), + ) + .toList(); // add default to end of list list.addAll( @@ -272,8 +271,10 @@ class NodeService extends ChangeNotifier { bool enabled, bool shouldNotifyListeners, ) async { - final model = - DB.instance.get(boxName: DB.boxNameNodeModels, key: id)!; + final model = DB.instance.get( + boxName: DB.boxNameNodeModels, + key: id, + )!; await DB.instance.put( boxName: DB.boxNameNodeModels, key: model.id, diff --git a/lib/widgets/epicbox_card.dart b/lib/widgets/epicbox_card.dart new file mode 100644 index 000000000..13a585b2c --- /dev/null +++ b/lib/widgets/epicbox_card.dart @@ -0,0 +1,211 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; + +import '../providers/global/node_service_provider.dart'; +import '../themes/stack_colors.dart'; +import '../utilities/assets.dart'; +import '../utilities/test_epicbox_server_connection.dart'; +import '../utilities/text_styles.dart'; +import '../utilities/util.dart'; +import 'custom_buttons/blue_text_button.dart'; +import 'expandable.dart'; +import 'rounded_white_container.dart'; + +class EpicBoxCard extends ConsumerStatefulWidget { + const EpicBoxCard({ + super.key, + required this.epicBoxId, + required this.onConnect, + required this.onEdit, + this.testOnInit = false, + }); + + final String epicBoxId; + final VoidCallback onConnect; + final VoidCallback onEdit; + final bool testOnInit; + + @override + ConsumerState createState() => _EpicBoxCardState(); +} + +class _EpicBoxCardState extends ConsumerState { + bool _advancedIsExpanded = false; + bool _testing = false; + bool? _testResult; + + @override + void initState() { + super.initState(); + if (widget.testOnInit) { + WidgetsBinding.instance.addPostFrameCallback((_) => _testConnection()); + } + } + + Future _testConnection() async { + final epicBox = ref + .read(nodeServiceChangeNotifierProvider) + .getEpicBoxById(id: widget.epicBoxId); + if (epicBox == null) return; + + setState(() { + _testing = true; + _testResult = null; + }); + + final data = EpicBoxFormData() + ..host = epicBox.host + ..port = epicBox.port ?? 443 + ..useSSL = epicBox.useSSL; + + final result = await testEpicBoxServerConnection(data) != null; + + if (mounted) { + setState(() { + _testing = false; + _testResult = result; + }); + } + } + + @override + Widget build(BuildContext context) { + final epicBox = ref.watch( + nodeServiceChangeNotifierProvider.select( + (value) => value.getEpicBoxById(id: widget.epicBoxId), + ), + ); + + if (epicBox == null) { + return const SizedBox.shrink(); + } + + final primaryEpicBox = ref.watch( + nodeServiceChangeNotifierProvider.select( + (value) => value.getPrimaryEpicBox(), + ), + ); + + final isPrimary = primaryEpicBox?.id == epicBox.id; + final isDesktop = Util.isDesktop; + + String status; + Color? statusColor; + if (_testing) { + status = "Testing..."; + } else if (_testResult == true) { + status = isPrimary ? "Connected" : "Reachable"; + statusColor = Theme.of(context).extension()!.accentColorGreen; + } else if (_testResult == false) { + status = "Unreachable"; + statusColor = Theme.of(context).extension()!.accentColorRed; + } else { + status = isPrimary ? "Selected" : ""; + if (isPrimary) { + statusColor = Theme.of(context).extension()!.accentColorBlue; + } + } + + return RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + borderColor: isDesktop + ? Theme.of(context).extension()!.background + : null, + child: Expandable( + onExpandChanged: (state) { + setState(() { + _advancedIsExpanded = state == ExpandableState.expanded; + }); + }, + header: Padding( + padding: EdgeInsets.all(isDesktop ? 16 : 12), + child: Row( + children: [ + Container( + width: isDesktop ? 40 : 24, + height: isDesktop ? 40 : 24, + decoration: BoxDecoration( + color: epicBox.isDefault + ? Theme.of( + context, + ).extension()!.buttonBackSecondary + : Theme.of(context) + .extension()! + .infoItemIcons + .withOpacity(0.2), + borderRadius: BorderRadius.circular(100), + ), + child: Center( + child: SvgPicture.asset( + Assets.svg.node, + height: isDesktop ? 18 : 11, + width: isDesktop ? 20 : 14, + color: epicBox.isDefault + ? Theme.of( + context, + ).extension()!.accentColorDark + : Theme.of( + context, + ).extension()!.infoItemIcons, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(epicBox.name, style: STextStyles.titleBold12(context)), + const SizedBox(height: 2), + Text( + "${epicBox.host}:${epicBox.port ?? 443}", + style: STextStyles.label(context), + ), + ], + ), + ), + Text( + status, + style: STextStyles.label(context).copyWith(color: statusColor), + ), + const SizedBox(width: 12), + SvgPicture.asset( + _advancedIsExpanded + ? Assets.svg.chevronUp + : Assets.svg.chevronDown, + width: 12, + height: 6, + color: Theme.of( + context, + ).extension()!.textSubtitle1, + ), + ], + ), + ), + body: Padding( + padding: const EdgeInsets.only(bottom: 24), + child: Row( + children: [ + const SizedBox(width: 66), + CustomTextButton( + text: "Test", + enabled: !_testing, + onTap: _testConnection, + ), + const SizedBox(width: 24), + CustomTextButton( + text: "Connect", + enabled: !isPrimary, + onTap: widget.onConnect, + ), + const SizedBox(width: 48), + if (!epicBox.isDefault) + CustomTextButton(text: "Edit", onTap: widget.onEdit), + ], + ), + ), + ), + ); + } +} From 87f371673bd416aa0d0a6b1e8e42bcd4aea192ba Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 3 Feb 2026 12:28:17 -0600 Subject: [PATCH 06/17] feat: use epicbox.epiccash.com as default epicbox server and format the rest --- lib/utilities/default_epicboxes.dart | 69 ++++++++++++++++------------ 1 file changed, 40 insertions(+), 29 deletions(-) diff --git a/lib/utilities/default_epicboxes.dart b/lib/utilities/default_epicboxes.dart index a2c9b01f0..f655dd9bb 100644 --- a/lib/utilities/default_epicboxes.dart +++ b/lib/utilities/default_epicboxes.dart @@ -16,38 +16,49 @@ abstract class DefaultEpicBoxes { static List get all => [americas, asia, europe]; static List get defaultIds => ['americas', 'asia', 'europe']; + static EpicBoxServerModel get epiccashCom => EpicBoxServerModel( + host: 'epicbox.epiccash.com', + port: 443, + name: 'Official', + id: 'epiccashCom', + useSSL: true, + enabled: true, + isFailover: true, + isDown: false, + ); + static EpicBoxServerModel get americas => EpicBoxServerModel( - host: 'epicbox.stackwallet.com', - port: 443, - name: 'Americas', - id: 'americas', - useSSL: true, - enabled: true, - isFailover: true, - isDown: false, - ); + host: 'epicbox.stackwallet.com', + port: 443, + name: 'Stack Wallet', + id: 'americas', + useSSL: true, + enabled: true, + isFailover: true, + isDown: false, + ); static EpicBoxServerModel get asia => EpicBoxServerModel( - host: 'epicbox.hyperbig.com', - port: 443, - name: 'Asia', - id: 'asia', - useSSL: true, - enabled: true, - isFailover: true, - isDown: false, - ); + host: 'epicbox.hyperbig.com', + port: 443, + name: 'Asia', + id: 'asia', + useSSL: true, + enabled: true, + isFailover: true, + isDown: false, + ); static EpicBoxServerModel get europe => EpicBoxServerModel( - host: 'epicbox.fastepic.eu', - port: 443, - name: 'Europe', - id: 'europe', - useSSL: true, - enabled: true, - isFailover: true, - isDown: false, - ); - - static final defaultEpicBoxServer = americas; + host: 'epicbox.fastepic.eu', + port: 443, + name: 'Europe', + id: 'europe', + useSSL: true, + enabled: true, + isFailover: true, + isDown: false, + ); + + static final defaultEpicBoxServer = epiccashCom; } From fc1a4a66aeb35e9fcb6b1d557bd15540c6b2db51 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 3 Feb 2026 11:52:49 -0600 Subject: [PATCH 07/17] fix: set default epicbox as primary on first launch so the default shows as connected on first look (if it is) --- lib/services/node_service.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/services/node_service.dart b/lib/services/node_service.dart index ad38d8d4b..487e60d11 100644 --- a/lib/services/node_service.dart +++ b/lib/services/node_service.dart @@ -321,6 +321,11 @@ class NodeService extends ChangeNotifier { ); } } + + // set default primary if none exists + if (getPrimaryEpicBox() == null) { + await setPrimaryEpicBox(epicBox: DefaultEpicBoxes.defaultEpicBoxServer); + } } Future setPrimaryEpicBox({ From 18310165365565f396b94e9fe71cb5d474e50d58 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 3 Feb 2026 12:03:09 -0600 Subject: [PATCH 08/17] feat: use stored epicbox config when available --- lib/wallets/wallet/impl/epiccash_wallet.dart | 38 +++++++------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/lib/wallets/wallet/impl/epiccash_wallet.dart b/lib/wallets/wallet/impl/epiccash_wallet.dart index 66d929430..2a6be2f47 100644 --- a/lib/wallets/wallet/impl/epiccash_wallet.dart +++ b/lib/wallets/wallet/impl/epiccash_wallet.dart @@ -148,34 +148,20 @@ class EpiccashWallet extends Bip39Wallet { } Future getEpicBoxConfig() async { - final EpicBoxConfigModel _epicBoxConfig = EpicBoxConfigModel.fromServer( - DefaultEpicBoxes.defaultEpicBoxServer, + // check for user-configured epicbox first + final storedConfig = await secureStorageInterface.read( + key: '${walletId}_epicboxConfig', ); + if (storedConfig != null && storedConfig.isNotEmpty) { + try { + return EpicBoxConfigModel.fromString(storedConfig); + } catch (e) { + Logging.instance.w("Failed to parse stored epicbox config: $e"); + } + } - //Get the default Epicbox server and check if it's conected - // bool isEpicboxConnected = await _testEpicboxServer( - // DefaultEpicBoxes.defaultEpicBoxServer.host, - // DefaultEpicBoxes.defaultEpicBoxServer.port ?? 443); - - // if (isEpicboxConnected) { - //Use default server for as Epicbox config - - // } - // else { - // //Use Europe config - // _epicBoxConfig = EpicBoxConfigModel.fromServer(DefaultEpicBoxes.europe); - // } - // // example of selecting another random server from the default list - // // alternative servers: copy list of all default EB servers but remove the default default - // // List alternativeServers = DefaultEpicBoxes.all; - // // alternativeServers.removeWhere((opt) => opt.name == DefaultEpicBoxes.defaultEpicBoxServer.name); - // // alternativeServers.shuffle(); // randomize which server is used - // // _epicBoxConfig = EpicBoxConfigModel.fromServer(alternativeServers.first); - // - // // TODO test this connection before returning it - // } - - return _epicBoxConfig; + // fall back to default + return EpicBoxConfigModel.fromServer(DefaultEpicBoxes.defaultEpicBoxServer); } Future updateRestoreHeight(int height) async { From e591991200cfe88078b7a4b57d1e830ab4de52c2 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 3 Feb 2026 12:43:05 -0600 Subject: [PATCH 09/17] fix: epic box ui tweaks --- .../settings_menu/epicbox_settings/add_edit_epicbox_view.dart | 2 +- .../epicbox_settings/desktop_manage_epicbox_dialog.dart | 4 ++-- lib/widgets/epicbox_card.dart | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/pages_desktop_specific/settings/settings_menu/epicbox_settings/add_edit_epicbox_view.dart b/lib/pages_desktop_specific/settings/settings_menu/epicbox_settings/add_edit_epicbox_view.dart index ce8aeaff1..2a091b6d5 100644 --- a/lib/pages_desktop_specific/settings/settings_menu/epicbox_settings/add_edit_epicbox_view.dart +++ b/lib/pages_desktop_specific/settings/settings_menu/epicbox_settings/add_edit_epicbox_view.dart @@ -416,7 +416,7 @@ class _AddEditEpicBoxViewState extends ConsumerState { ), ], ), - const SizedBox(height: 36), + const SizedBox(height: 78), Row( children: [ Expanded( diff --git a/lib/pages_desktop_specific/settings/settings_menu/epicbox_settings/desktop_manage_epicbox_dialog.dart b/lib/pages_desktop_specific/settings/settings_menu/epicbox_settings/desktop_manage_epicbox_dialog.dart index c904c25d8..886e564ed 100644 --- a/lib/pages_desktop_specific/settings/settings_menu/epicbox_settings/desktop_manage_epicbox_dialog.dart +++ b/lib/pages_desktop_specific/settings/settings_menu/epicbox_settings/desktop_manage_epicbox_dialog.dart @@ -110,7 +110,7 @@ class _DesktopManageEpicBoxDialogState final customBoxes = epicBoxes.where((e) => !e.isDefault).toList(); return DesktopDialog( - maxHeight: null, + maxHeight: double.infinity, maxWidth: 580, child: Column( mainAxisSize: MainAxisSize.min, @@ -141,7 +141,7 @@ class _DesktopManageEpicBoxDialogState const SizedBox(height: 12), Flexible( child: Padding( - padding: const EdgeInsets.only(left: 20, right: 20, bottom: 20), + padding: const EdgeInsets.only(left: 32, right: 32, bottom: 32), child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/widgets/epicbox_card.dart b/lib/widgets/epicbox_card.dart index 13a585b2c..2ca335a4f 100644 --- a/lib/widgets/epicbox_card.dart +++ b/lib/widgets/epicbox_card.dart @@ -193,7 +193,7 @@ class _EpicBoxCardState extends ConsumerState { enabled: !_testing, onTap: _testConnection, ), - const SizedBox(width: 24), + const SizedBox(width: 48), CustomTextButton( text: "Connect", enabled: !isPrimary, From 57a5524d43b69445d4d08813afce5906c972b77b Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 3 Feb 2026 13:43:03 -0600 Subject: [PATCH 10/17] update epic test connection logging --- lib/networking/http.dart | 4 ++++ .../test_epicbox_server_connection.dart | 17 +++++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/lib/networking/http.dart b/lib/networking/http.dart index 48b5f1c66..4771a10ac 100644 --- a/lib/networking/http.dart +++ b/lib/networking/http.dart @@ -26,8 +26,12 @@ class HTTP { required Uri url, Map? headers, required ({InternetAddress host, int port})? proxyInfo, + Duration? connectionTimeout, }) async { final httpClient = HttpClient(); + if (connectionTimeout != null) { + httpClient.connectionTimeout = connectionTimeout; + } try { if (proxyInfo != null) { SocksTCPClient.assignToHttpClient(httpClient, [ diff --git a/lib/utilities/test_epicbox_server_connection.dart b/lib/utilities/test_epicbox_server_connection.dart index bf5faf2e7..0a2ef90ea 100644 --- a/lib/utilities/test_epicbox_server_connection.dart +++ b/lib/utilities/test_epicbox_server_connection.dart @@ -3,22 +3,31 @@ import 'dart:io'; import 'logger.dart'; Future _testEpicBoxConnection(String host, int port, bool useSSL) async { + final client = HttpClient(); try { final protocol = useSSL ? 'https' : 'http'; - final client = HttpClient(); + client.connectionTimeout = const Duration(seconds: 5); final request = await client.getUrl(Uri.parse('$protocol://$host:$port')); final response = await request.close(); - final body = await response.transform(const SystemEncoding().decoder).join(); + final body = await response + .transform(const SystemEncoding().decoder) + .join(); client.close(); // epicbox servers return an HTML page containing "Epicbox" return response.statusCode == 200 && body.contains('Epicbox'); - } catch (e) { - Logging.instance.i("_testEpicBoxConnection failed on \"$host:$port\": $e"); + } catch (e, s) { + Logging.instance.e( + "_testEpicBoxConnection failed on \"$host:$port\"", + error: e, + stackTrace: s, + ); return false; + } finally { + client.close(force: true); } } From 8a67326f64e0b3197a9c497ca520e981f094dcbf Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 3 Feb 2026 14:43:32 -0600 Subject: [PATCH 11/17] do not say on chain note is optional --- lib/pages/send_view/confirm_transaction_view.dart | 2 +- lib/pages/send_view/send_view.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index 9a7640a86..dfd6c98bd 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -1181,7 +1181,7 @@ class _ConfirmTransactionViewState children: [ if (coin is Epiccash || coin is Mimblewimblecoin) Text( - "On chain Note (optional)", + "On chain Note", style: STextStyles.smallMed12(context), textAlign: TextAlign.left, ), diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index 8761a7234..94b5663c8 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -2353,7 +2353,7 @@ class _SendViewState extends ConsumerState { const SizedBox(height: 12), if (coin is Epiccash) Text( - "On chain Note (optional)", + "On chain Note", style: STextStyles.smallMed12(context), textAlign: TextAlign.left, ), From 4e7ac9e269f68eabf6cc958673b18b9fd0fd6ae7 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 3 Feb 2026 14:48:11 -0600 Subject: [PATCH 12/17] update epic when epic box is changed receiving address --- lib/utilities/default_epicboxes.dart | 29 +------ lib/wallets/wallet/impl/epiccash_wallet.dart | 90 +++++++++++++------- 2 files changed, 61 insertions(+), 58 deletions(-) diff --git a/lib/utilities/default_epicboxes.dart b/lib/utilities/default_epicboxes.dart index f655dd9bb..3ab7f1732 100644 --- a/lib/utilities/default_epicboxes.dart +++ b/lib/utilities/default_epicboxes.dart @@ -13,14 +13,13 @@ import '../models/epicbox_server_model.dart'; abstract class DefaultEpicBoxes { static const String defaultName = "Default"; - static List get all => [americas, asia, europe]; - static List get defaultIds => ['americas', 'asia', 'europe']; + static List get all => [defaultEpicBoxServer, americas]; static EpicBoxServerModel get epiccashCom => EpicBoxServerModel( host: 'epicbox.epiccash.com', port: 443, name: 'Official', - id: 'epiccashCom', + id: 'default_epiccashCom', useSSL: true, enabled: true, isFailover: true, @@ -31,29 +30,7 @@ abstract class DefaultEpicBoxes { host: 'epicbox.stackwallet.com', port: 443, name: 'Stack Wallet', - id: 'americas', - useSSL: true, - enabled: true, - isFailover: true, - isDown: false, - ); - - static EpicBoxServerModel get asia => EpicBoxServerModel( - host: 'epicbox.hyperbig.com', - port: 443, - name: 'Asia', - id: 'asia', - useSSL: true, - enabled: true, - isFailover: true, - isDown: false, - ); - - static EpicBoxServerModel get europe => EpicBoxServerModel( - host: 'epicbox.fastepic.eu', - port: 443, - name: 'Europe', - id: 'europe', + id: 'default_stack', useSSL: true, enabled: true, isFailover: true, diff --git a/lib/wallets/wallet/impl/epiccash_wallet.dart b/lib/wallets/wallet/impl/epiccash_wallet.dart index 2a6be2f47..042138e17 100644 --- a/lib/wallets/wallet/impl/epiccash_wallet.dart +++ b/lib/wallets/wallet/impl/epiccash_wallet.dart @@ -8,6 +8,7 @@ import 'package:mutex/mutex.dart'; import 'package:stack_wallet_backup/generate_password.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; +import '../../../exceptions/main_db/main_db_exception.dart'; import '../../../exceptions/wallet/node_tor_mismatch_config_exception.dart'; import '../../../models/balance.dart'; import '../../../models/epic_slatepack_models.dart'; @@ -120,10 +121,12 @@ class EpiccashWallet extends Bip39Wallet { "epicbox_address_index": 0, }); await secureStorageInterface.write( - key: '${walletId}_epicboxConfig', + key: '${walletId}_epicboxConfigNewNewNew', value: stringConfig, ); libEpic.updateEpicboxConfig(wallet: _wallet!, epicBoxConfig: stringConfig); + + await _generateAndStoreReceivingAddressForIndex(0); // TODO: refresh anything that needs to be refreshed/updated due to epicbox info changed } @@ -150,14 +153,21 @@ class EpiccashWallet extends Bip39Wallet { Future getEpicBoxConfig() async { // check for user-configured epicbox first final storedConfig = await secureStorageInterface.read( - key: '${walletId}_epicboxConfig', + key: '${walletId}_epicboxConfigNewNewNew', ); if (storedConfig != null && storedConfig.isNotEmpty) { try { return EpicBoxConfigModel.fromString(storedConfig); - } catch (e) { - Logging.instance.w("Failed to parse stored epicbox config: $e"); + } catch (e, s) { + Logging.instance.e( + "Failed to parse stored epicbox config $storedConfig." + " Falling back to default.", + error: e, + stackTrace: s, + ); } + } else { + Logging.instance.i("No stored epic box config. Falling back to default."); } // fall back to default @@ -558,7 +568,7 @@ class EpiccashWallet extends Bip39Wallet { return response is String && response.contains("Challenge"); } catch (e, s) { Logging.instance.w( - "_testEpicBoxConnection failed on \"$host:$port\"", + "_testEpicboxServer failed on \"$host:$port\"", error: e, stackTrace: s, ); @@ -605,8 +615,33 @@ class EpiccashWallet extends Bip39Wallet { } } + Future _updateAddressInDB(Address address) async { + try { + final storedAddress = await getCurrentReceivingAddress(); + await mainDB.isar.writeTxn(() async { + if (storedAddress == null) { + await mainDB.isar.addresses.put(address); + } else { + address.id = storedAddress.id; + await storedAddress.transactions.load(); + final txns = storedAddress.transactions.toList(); + await mainDB.isar.addresses.delete(storedAddress.id); + await mainDB.isar.addresses.put(address); + address.transactions.addAll(txns); + await address.transactions.save(); + } + }); + } catch (e) { + throw MainDBException("failed _updateAddressInDB: $address", e); + } + } + /// Only index 0 is currently used in stack wallet. Future
_generateAndStoreReceivingAddressForIndex(int index) async { + if (_wallet == null) { + throw Exception('Wallet not opened. Call open() first.'); + } + // Since only 0 is a valid index in stack wallet at this time, lets just // throw is not zero if (index != 0) { @@ -614,29 +649,10 @@ class EpiccashWallet extends Bip39Wallet { } final epicBoxConfig = await getEpicBoxConfig(); - final address = await thisWalletAddress(index, epicBoxConfig); - - if (info.cachedReceivingAddress != address.value) { - await info.updateReceivingAddress( - newAddress: address.value, - isar: mainDB.isar, - ); - } - return address; - } - - Future
thisWalletAddress( - int index, - EpicBoxConfigModel epicboxConfig, - ) async { - if (_wallet == null) { - throw Exception('Wallet not opened. Call open() first.'); - } - final walletAddress = await libEpic.getAddressInfo( wallet: _wallet!, index: index, - epicboxConfig: epicboxConfig.toString(), + epicboxConfig: epicBoxConfig.toString(), ); Logging.instance.d("WALLET_ADDRESS_IS $walletAddress"); @@ -650,7 +666,14 @@ class EpiccashWallet extends Bip39Wallet { subType: AddressSubType.receiving, publicKey: [], // ?? ); - await mainDB.updateOrPutAddresses([address]); + await _updateAddressInDB(address); + if (info.cachedReceivingAddress != address.value) { + await info.updateReceivingAddress( + newAddress: address.value, + isar: mainDB.isar, + ); + } + return address; } @@ -833,7 +856,7 @@ class EpiccashWallet extends Bip39Wallet { value: password, ); await secureStorageInterface.write( - key: '${walletId}_epicboxConfig', + key: '${walletId}_epicboxConfigNewNewNew', value: epicboxConfig.toString(), ); @@ -890,6 +913,9 @@ class EpiccashWallet extends Bip39Wallet { await updateNode(); + // ensure address is up to date with epic box uri + await _generateAndStoreReceivingAddressForIndex(0); + await _listenToEpicbox(); } catch (e, s) { // do nothing, still allow user into wallet @@ -1100,6 +1126,10 @@ class EpiccashWallet extends Bip39Wallet { epicBoxConfig: epicboxConfig.toString(), ); + await _generateAndStoreReceivingAddressForIndex( + info.epicData?.receivingIndex ?? 0, + ); + await _listenToEpicbox(); highestPercent = 0; @@ -1121,7 +1151,7 @@ class EpiccashWallet extends Bip39Wallet { ); await secureStorageInterface.write( - key: '${walletId}_epicboxConfig', + key: '${walletId}_epicboxConfigNewNewNew', value: epicboxConfig.toString(), ); @@ -1201,10 +1231,6 @@ class EpiccashWallet extends Bip39Wallet { // await epicUpdateCreationHeight(await chainHeight); // } - // this will always be zero???? - final int curAdd = await _getCurrentIndex(); - await _generateAndStoreReceivingAddressForIndex(curAdd); - if (_wallet == null) { throw Exception('Wallet not opened. Call open() first.'); } From b4600e13234769b70877f7f8e34630fe9f3639cd Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 3 Feb 2026 17:04:10 -0600 Subject: [PATCH 13/17] account for on chain note in messages --- .../isar/models/blockchain_data/v2/transaction_v2.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart b/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart index 4bb9e1fa8..927c1fa3b 100644 --- a/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart +++ b/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart @@ -311,7 +311,8 @@ class TransactionV2 { if (isConfirmed(currentChainHeight, minConfirms, minCoinbaseConfirms)) { return "Received"; } else { - if (numberOfMessages == 1) { + if ((onChainNote == null && numberOfMessages == 1) | + (onChainNote != null && numberOfMessages == 2)) { return "Receiving (waiting for sender)"; } else if ((numberOfMessages ?? 0) > 1) { return "Receiving (waiting for confirmations)"; // TODO test if the sender still has to open again after the receiver has 2 messages present, ie. sender->receiver->sender->node (yes) vs. sender->receiver->node (no) @@ -323,7 +324,8 @@ class TransactionV2 { if (isConfirmed(currentChainHeight, minConfirms, minCoinbaseConfirms)) { return "Sent (confirmed)"; } else { - if (numberOfMessages == 1) { + if ((onChainNote == null && numberOfMessages == 1) | + (onChainNote != null && numberOfMessages == 2)) { return "Sending (waiting for receiver)"; } else if ((numberOfMessages ?? 0) > 1) { return "Sending (waiting for confirmations)"; From be51789fb2e037d9d63d23484f61f9cbf384d369 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 3 Feb 2026 17:05:17 -0600 Subject: [PATCH 14/17] fix address order --- lib/wallets/wallet/impl/epiccash_wallet.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/wallets/wallet/impl/epiccash_wallet.dart b/lib/wallets/wallet/impl/epiccash_wallet.dart index 042138e17..3746a4836 100644 --- a/lib/wallets/wallet/impl/epiccash_wallet.dart +++ b/lib/wallets/wallet/impl/epiccash_wallet.dart @@ -1409,7 +1409,7 @@ class EpiccashWallet extends Bip39Wallet { OutputV2 output = OutputV2.isarCantDoRequiredInDefaultConstructor( scriptPubKeyHex: "00", valueStringSats: credit.toString(), - addresses: [if (addressFrom != null) addressFrom], + addresses: [if (addressTo != null) addressTo], walletOwns: true, ); final InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( @@ -1417,7 +1417,7 @@ class EpiccashWallet extends Bip39Wallet { scriptSigAsm: null, sequence: null, outpoint: null, - addresses: [if (addressTo != null) addressTo], + addresses: [if (addressFrom != null) addressFrom], valueStringSats: debit.toString(), witness: null, innerRedeemScriptAsm: null, From d2f99c9881fcebfba7f567c5ac19962afa8a1921 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 3 Feb 2026 17:07:29 -0600 Subject: [PATCH 15/17] clean up txv2 --- .../blockchain_data/v2/transaction_v2.dart | 40 ++----------------- 1 file changed, 4 insertions(+), 36 deletions(-) diff --git a/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart b/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart index 927c1fa3b..721d9a11c 100644 --- a/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart +++ b/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart @@ -271,40 +271,6 @@ class TransactionV2 { return "Restored Funds"; } - if (isCancelled) { - return "Cancelled"; - } else if (type == TransactionType.incoming) { - if (isConfirmed(currentChainHeight, minConfirms, minCoinbaseConfirms)) { - return "Received"; - } else { - if (numberOfMessages == 1) { - return "Receiving (waiting for sender)"; - } else if ((numberOfMessages ?? 0) > 1) { - return "Receiving (waiting for confirmations)"; // TODO test if the sender still has to open again after the receiver has 2 messages present, ie. sender->receiver->sender->node (yes) vs. sender->receiver->node (no) - } else { - return "Receiving ${prettyConfirms()}"; - } - } - } else if (type == TransactionType.outgoing) { - if (isConfirmed(currentChainHeight, minConfirms, minCoinbaseConfirms)) { - return "Sent (confirmed)"; - } else { - if (numberOfMessages == 1) { - return "Sending (waiting for receiver)"; - } else if ((numberOfMessages ?? 0) > 1) { - return "Sending (waiting for confirmations)"; - } else { - return "Sending ${prettyConfirms()}"; - } - } - } - } - - if (isMimblewimblecoinTransaction) { - if (slateId == null) { - return "Restored Funds"; - } - if (isCancelled) { return "Cancelled"; } else if (type == TransactionType.incoming) { @@ -347,7 +313,8 @@ class TransactionV2 { if (isConfirmed(currentChainHeight, minConfirms, minCoinbaseConfirms)) { return "Received"; } else { - if (numberOfMessages == 1) { + if ((onChainNote == null && numberOfMessages == 1) | + (onChainNote != null && numberOfMessages == 2)) { return "Receiving (waiting for sender)"; } else if ((numberOfMessages ?? 0) > 1) { return "Receiving (waiting for confirmations)"; // TODO test if the sender still has to open again after the receiver has 2 messages present, ie. sender->receiver->sender->node (yes) vs. sender->receiver->node (no) @@ -359,7 +326,8 @@ class TransactionV2 { if (isConfirmed(currentChainHeight, minConfirms, minCoinbaseConfirms)) { return "Sent (confirmed)"; } else { - if (numberOfMessages == 1) { + if ((onChainNote == null && numberOfMessages == 1) | + (onChainNote != null && numberOfMessages == 2)) { return "Sending (waiting for receiver)"; } else if ((numberOfMessages ?? 0) > 1) { return "Sending (waiting for confirmations)"; From 03eb58612ed253ea0d9811fa26a0c424690b7191 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 3 Feb 2026 18:10:50 -0600 Subject: [PATCH 16/17] feat: add mobile epicbox server mgmt ui --- .../add_edit_epicbox_mobile_view.dart | 412 ++++++++++++++++++ .../epicbox_settings/manage_epicbox_view.dart | 217 +++++++++ .../wallet_settings_view.dart | 14 + lib/route_generator.dart | 31 ++ lib/widgets/epicbox_card.dart | 11 + 5 files changed, 685 insertions(+) create mode 100644 lib/pages/settings_views/wallet_settings_view/epicbox_settings/add_edit_epicbox_mobile_view.dart create mode 100644 lib/pages/settings_views/wallet_settings_view/epicbox_settings/manage_epicbox_view.dart diff --git a/lib/pages/settings_views/wallet_settings_view/epicbox_settings/add_edit_epicbox_mobile_view.dart b/lib/pages/settings_views/wallet_settings_view/epicbox_settings/add_edit_epicbox_mobile_view.dart new file mode 100644 index 000000000..9208387a5 --- /dev/null +++ b/lib/pages/settings_views/wallet_settings_view/epicbox_settings/add_edit_epicbox_mobile_view.dart @@ -0,0 +1,412 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:uuid/uuid.dart'; + +import '../../../../models/epicbox_server_model.dart'; +import '../../../../notifications/show_flush_bar.dart'; +import '../../../../providers/global/node_service_provider.dart'; +import '../../../../themes/stack_colors.dart'; +import '../../../../utilities/constants.dart'; +import '../../../../utilities/test_epicbox_server_connection.dart'; +import '../../../../utilities/text_styles.dart'; +import '../../../../widgets/background.dart'; +import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../../../widgets/custom_buttons/blue_text_button.dart'; +import '../../../../widgets/icon_widgets/x_icon.dart'; +import '../../../../widgets/stack_text_field.dart'; +import '../../../../widgets/textfield_icon_button.dart'; + +enum AddEditEpicboxMobileViewType { add, edit } + +class AddEditEpicboxMobileView extends ConsumerStatefulWidget { + const AddEditEpicboxMobileView({ + super.key, + required this.viewType, + this.epicBoxId, + }); + + static const routeName = "/addEditEpicboxMobile"; + + final AddEditEpicboxMobileViewType viewType; + final String? epicBoxId; + + @override + ConsumerState createState() => + _AddEditEpicboxMobileViewState(); +} + +class _AddEditEpicboxMobileViewState + extends ConsumerState { + late final TextEditingController _nameController; + late final TextEditingController _hostController; + late final TextEditingController _portController; + + final _nameFocusNode = FocusNode(); + final _hostFocusNode = FocusNode(); + final _portFocusNode = FocusNode(); + + bool _useSSL = true; + int? port; + + bool get canSave { + return _nameController.text.isNotEmpty && canTestConnection; + } + + bool get canTestConnection { + return _hostController.text.isNotEmpty && + port != null && + port! >= 0 && + port! <= 65535; + } + + Future _testConnection() async { + final data = EpicBoxFormData() + ..name = _nameController.text + ..host = _hostController.text + ..port = port ?? 443 + ..useSSL = _useSSL; + + final result = await testEpicBoxServerConnection(data); + if (!mounted) return; + + if (result != null) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "Connection successful", + context: context, + ), + ); + } else { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Could not connect to server", + context: context, + ), + ); + } + } + + Future _attemptSave() async { + final data = EpicBoxFormData() + ..name = _nameController.text + ..host = _hostController.text + ..port = port ?? 443 + ..useSSL = _useSSL; + + final canConnect = await testEpicBoxServerConnection(data) != null; + + bool shouldSave = canConnect; + + if (!canConnect && mounted) { + await showDialog( + context: context, + useSafeArea: true, + barrierDismissible: true, + builder: (context) => AlertDialog( + title: const Text("Server currently unreachable"), + content: const Text("Would you like to save this server anyways?"), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text( + "Cancel", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark, + ), + ), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text( + "Save", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark, + ), + ), + ), + ], + ), + ).then((value) { + if (value == true) { + shouldSave = true; + } + }); + } + + if (!shouldSave) return; + + final epicBox = EpicBoxServerModel( + id: widget.epicBoxId ?? const Uuid().v1(), + host: _hostController.text, + port: port ?? 443, + name: _nameController.text, + useSSL: _useSSL, + enabled: true, + isFailover: true, + isDown: false, + ); + + await ref.read(nodeServiceChangeNotifierProvider).addEpicBox(epicBox, true); + + if (mounted) { + Navigator.of(context).pop(); + } + } + + @override + void initState() { + super.initState(); + _nameController = TextEditingController(); + _hostController = TextEditingController(); + _portController = TextEditingController(); + + if (widget.epicBoxId != null) { + final epicBox = ref + .read(nodeServiceChangeNotifierProvider) + .getEpicBoxById(id: widget.epicBoxId!); + if (epicBox != null) { + _nameController.text = epicBox.name; + _hostController.text = epicBox.host; + _portController.text = (epicBox.port ?? 443).toString(); + _useSSL = epicBox.useSSL ?? true; + port = epicBox.port ?? 443; + } + } else { + _portController.text = "443"; + port = 443; + } + } + + @override + void dispose() { + _nameController.dispose(); + _hostController.dispose(); + _portController.dispose(); + _nameFocusNode.dispose(); + _hostFocusNode.dispose(); + _portFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + widget.viewType == AddEditEpicboxMobileViewType.add + ? "Add Epicbox Server" + : "Edit Epicbox Server", + style: STextStyles.navBarTitle(context), + ), + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: false, + enableSuggestions: false, + controller: _nameController, + focusNode: _nameFocusNode, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Server name", + _nameFocusNode, + context, + ).copyWith( + suffixIcon: _nameController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: TextFieldIconButton( + child: const XIcon(), + onTap: () { + _nameController.clear(); + setState(() {}); + }, + ), + ), + ) + : null, + ), + onChanged: (_) => setState(() {}), + ), + ), + const SizedBox(height: 12), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: false, + enableSuggestions: false, + controller: _hostController, + focusNode: _hostFocusNode, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Host", + _hostFocusNode, + context, + ).copyWith( + suffixIcon: _hostController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: TextFieldIconButton( + child: const XIcon(), + onTap: () { + _hostController.clear(); + setState(() {}); + }, + ), + ), + ) + : null, + ), + onChanged: (_) => setState(() {}), + ), + ), + const SizedBox(height: 12), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: false, + enableSuggestions: false, + controller: _portController, + focusNode: _portFocusNode, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], + keyboardType: TextInputType.number, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Port", + _portFocusNode, + context, + ).copyWith( + suffixIcon: _portController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: TextFieldIconButton( + child: const XIcon(), + onTap: () { + _portController.clear(); + setState(() {}); + }, + ), + ), + ) + : null, + ), + onChanged: (value) { + port = int.tryParse(value); + setState(() {}); + }, + ), + ), + const SizedBox(height: 12), + Row( + children: [ + GestureDetector( + onTap: () { + setState(() { + _useSSL = !_useSSL; + }); + }, + child: Container( + color: Colors.transparent, + child: Row( + children: [ + SizedBox( + width: 20, + height: 20, + child: Checkbox( + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + value: _useSSL, + onChanged: (newValue) { + setState(() { + _useSSL = newValue!; + }); + }, + ), + ), + const SizedBox(width: 12), + Text( + "Use SSL", + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + ), + ], + ), + ], + ), + ), + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: CustomTextButton( + text: "Test connection", + enabled: canTestConnection, + onTap: canTestConnection ? _testConnection : null, + ), + ), + const SizedBox(width: 16), + Expanded( + child: TextButton( + onPressed: canSave ? _attemptSave : null, + style: canSave + ? Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context) + : Theme.of(context) + .extension()! + .getPrimaryDisabledButtonStyle(context), + child: Text( + "Save", + style: STextStyles.button(context), + ), + ), + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/settings_views/wallet_settings_view/epicbox_settings/manage_epicbox_view.dart b/lib/pages/settings_views/wallet_settings_view/epicbox_settings/manage_epicbox_view.dart new file mode 100644 index 000000000..46765b2e6 --- /dev/null +++ b/lib/pages/settings_views/wallet_settings_view/epicbox_settings/manage_epicbox_view.dart @@ -0,0 +1,217 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../notifications/show_flush_bar.dart'; +import '../../../../providers/providers.dart'; +import '../../../../themes/stack_colors.dart'; +import '../../../../utilities/assets.dart'; +import '../../../../utilities/test_epicbox_server_connection.dart'; +import '../../../../utilities/text_styles.dart'; +import '../../../../wallets/wallet/impl/epiccash_wallet.dart'; +import '../../../../widgets/background.dart'; +import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../../../widgets/epicbox_card.dart'; +import 'add_edit_epicbox_mobile_view.dart'; + +class ManageEpicboxView extends ConsumerStatefulWidget { + const ManageEpicboxView({super.key, required this.walletId}); + + static const routeName = "/manageEpicbox"; + + final String walletId; + + @override + ConsumerState createState() => _ManageEpicboxViewState(); +} + +class _ManageEpicboxViewState extends ConsumerState { + Future _onConnect(String epicBoxId) async { + final epicBox = ref + .read(nodeServiceChangeNotifierProvider) + .getEpicBoxById(id: epicBoxId); + + if (epicBox == null) return; + + final data = EpicBoxFormData() + ..host = epicBox.host + ..port = epicBox.port ?? 443 + ..useSSL = epicBox.useSSL; + + final canConnect = await testEpicBoxServerConnection(data) != null; + + if (!canConnect && mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + iconAsset: Assets.svg.circleAlert, + message: "Could not connect to server", + context: context, + ), + ); + return; + } + + await ref + .read(nodeServiceChangeNotifierProvider) + .setPrimaryEpicBox(epicBox: epicBox, shouldNotifyListeners: true); + + // update wallet's epicbox config + final wallet = + ref.read(pWallets).getWallet(widget.walletId) as EpiccashWallet; + await wallet.updateEpicboxConfig(epicBox.host, epicBox.port ?? 443); + + if (mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "Connected to ${epicBox.name}", + context: context, + ), + ); + } + } + + void _onEdit(String epicBoxId) { + Navigator.of(context).pushNamed( + AddEditEpicboxMobileView.routeName, + arguments: ( + viewType: AddEditEpicboxMobileViewType.edit, + epicBoxId: epicBoxId, + ), + ); + } + + void _onAdd() { + Navigator.of(context).pushNamed( + AddEditEpicboxMobileView.routeName, + arguments: ( + viewType: AddEditEpicboxMobileViewType.add, + epicBoxId: null, + ), + ); + } + + @override + Widget build(BuildContext context) { + final epicBoxes = ref.watch( + nodeServiceChangeNotifierProvider.select((value) => value.getEpicBoxes()), + ); + final primaryEpicBox = ref.watch( + nodeServiceChangeNotifierProvider.select( + (value) => value.getPrimaryEpicBox(), + ), + ); + + final defaultBoxes = epicBoxes.where((e) => e.isDefault).toList(); + final customBoxes = epicBoxes.where((e) => !e.isDefault).toList(); + + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Epicbox Servers", + style: STextStyles.navBarTitle(context), + ), + actions: [ + Padding( + padding: const EdgeInsets.only(right: 10), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + icon: SizedBox( + width: 20, + height: 20, + child: Center( + child: Icon( + Icons.add, + color: Theme.of(context) + .extension()! + .topNavIconPrimary, + size: 20, + ), + ), + ), + onPressed: _onAdd, + ), + ), + ), + ], + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (defaultBoxes.isNotEmpty) ...[ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + child: Text( + "Default servers", + style: STextStyles.smallMed12(context).copyWith( + color: Theme.of( + context, + ).extension()!.textDark3, + ), + ), + ), + ...defaultBoxes.map( + (epicBox) => Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: EpicBoxCard( + key: Key("${epicBox.id}_card_key"), + epicBoxId: epicBox.id, + onConnect: () => _onConnect(epicBox.id), + onEdit: () => _onEdit(epicBox.id), + testOnInit: primaryEpicBox?.id == epicBox.id, + ), + ), + ), + ], + if (customBoxes.isNotEmpty) ...[ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + child: Text( + "Custom servers", + style: STextStyles.smallMed12(context).copyWith( + color: Theme.of( + context, + ).extension()!.textDark3, + ), + ), + ), + ...customBoxes.map( + (epicBox) => Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: EpicBoxCard( + key: Key("${epicBox.id}_card_key"), + epicBoxId: epicBox.id, + onConnect: () => _onConnect(epicBox.id), + onEdit: () => _onEdit(epicBox.id), + testOnInit: primaryEpicBox?.id == epicBox.id, + ), + ), + ), + ], + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart index 58ffaad71..4fd2e8f95 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart @@ -55,6 +55,7 @@ import '../../home_view/home_view.dart'; import '../../pinpad_views/lock_screen_view.dart'; import '../global_settings_view/syncing_preferences_views/syncing_preferences_view.dart'; import '../sub_widgets/settings_list_button.dart'; +import 'epicbox_settings/manage_epicbox_view.dart'; import 'frost_ms/frost_ms_options_view.dart'; import 'wallet_backup_views/wallet_backup_view.dart'; import 'wallet_network_settings_view/wallet_network_settings_view.dart'; @@ -409,6 +410,19 @@ class _WalletSettingsViewState extends ConsumerState { ); }, ), + if (wallet is EpiccashWallet) const SizedBox(height: 8), + if (wallet is EpiccashWallet) + SettingsListButton( + iconAssetName: Assets.svg.node, + iconSize: 16, + title: "Epicbox Servers", + onPressed: () { + Navigator.of(context).pushNamed( + ManageEpicboxView.routeName, + arguments: walletId, + ); + }, + ), if (canBackup) const SizedBox(height: 8), if (canBackup) Consumer( diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 21c20a074..fcf57dab9 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -144,6 +144,8 @@ import 'pages/settings_views/global_settings_view/syncing_preferences_views/sync import 'pages/settings_views/global_settings_view/syncing_preferences_views/syncing_preferences_view.dart'; import 'pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart'; import 'pages/settings_views/global_settings_view/tor_settings/tor_settings_view.dart'; +import 'pages/settings_views/wallet_settings_view/epicbox_settings/add_edit_epicbox_mobile_view.dart'; +import 'pages/settings_views/wallet_settings_view/epicbox_settings/manage_epicbox_view.dart'; import 'pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart'; import 'pages/settings_views/wallet_settings_view/frost_ms/frost_participants_view.dart'; import 'pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/complete_reshare_config_view.dart'; @@ -1438,6 +1440,35 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case ManageEpicboxView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => ManageEpicboxView( + walletId: args, + ), + settings: RouteSettings(name: settings.name), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case AddEditEpicboxMobileView.routeName: + if (args + is ({ + AddEditEpicboxMobileViewType viewType, + String? epicBoxId, + })) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => AddEditEpicboxMobileView( + viewType: args.viewType, + epicBoxId: args.epicBoxId, + ), + settings: RouteSettings(name: settings.name), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case WalletBackupView.routeName: if (args is ({String walletId, List mnemonic})) { return getRoute( diff --git a/lib/widgets/epicbox_card.dart b/lib/widgets/epicbox_card.dart index 2ca335a4f..3db6ebb12 100644 --- a/lib/widgets/epicbox_card.dart +++ b/lib/widgets/epicbox_card.dart @@ -43,6 +43,17 @@ class _EpicBoxCardState extends ConsumerState { } } + @override + void didUpdateWidget(EpicBoxCard oldWidget) { + super.didUpdateWidget(oldWidget); + // Auto-test when testOnInit changes from false to true + if (widget.testOnInit && !oldWidget.testOnInit && _testResult == null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) _testConnection(); + }); + } + } + Future _testConnection() async { final epicBox = ref .read(nodeServiceChangeNotifierProvider) From aa20a549a88471c6ecbde6f4daf6caff671bd8cc Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 3 Feb 2026 19:29:54 -0600 Subject: [PATCH 17/17] delete custom epicbox impl and some other minimal working changes --- lib/main.dart | 2 +- .../add_edit_epicbox_mobile_view.dart | 443 ++++++++++-------- .../epicbox_settings/manage_epicbox_view.dart | 32 +- .../add_edit_epicbox_view.dart | 56 ++- .../desktop_manage_epicbox_dialog.dart | 64 ++- lib/route_generator.dart | 6 +- lib/services/node_service.dart | 54 +-- lib/widgets/epicbox_card.dart | 34 +- 8 files changed, 388 insertions(+), 303 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 1d8f99266..ea6880af6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -33,13 +33,13 @@ import 'db/hive/db.dart'; import 'db/isar/main_db.dart'; import 'db/special_migrations.dart'; import 'db/sqlite/firo_cache.dart'; +import 'models/epicbox_server_model.dart'; import 'models/exchange/change_now/exchange_transaction.dart'; import 'models/exchange/change_now/exchange_transaction_status.dart'; import 'models/exchange/response_objects/trade.dart'; import 'models/models.dart'; import 'models/node_model.dart'; import 'models/notification_model.dart'; -import 'models/epicbox_server_model.dart'; import 'models/trade_wallet_lookup.dart'; import 'pages/campfire_migrate_view.dart'; import 'pages/home_view/home_view.dart'; diff --git a/lib/pages/settings_views/wallet_settings_view/epicbox_settings/add_edit_epicbox_mobile_view.dart b/lib/pages/settings_views/wallet_settings_view/epicbox_settings/add_edit_epicbox_mobile_view.dart index 9208387a5..907aaafde 100644 --- a/lib/pages/settings_views/wallet_settings_view/epicbox_settings/add_edit_epicbox_mobile_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/epicbox_settings/add_edit_epicbox_mobile_view.dart @@ -3,18 +3,21 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; import 'package:uuid/uuid.dart'; import '../../../../models/epicbox_server_model.dart'; import '../../../../notifications/show_flush_bar.dart'; import '../../../../providers/global/node_service_provider.dart'; import '../../../../themes/stack_colors.dart'; +import '../../../../utilities/assets.dart'; import '../../../../utilities/constants.dart'; import '../../../../utilities/test_epicbox_server_connection.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../widgets/background.dart'; import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; -import '../../../../widgets/custom_buttons/blue_text_button.dart'; +import '../../../../widgets/desktop/primary_button.dart'; +import '../../../../widgets/desktop/secondary_button.dart'; import '../../../../widgets/icon_widgets/x_icon.dart'; import '../../../../widgets/stack_text_field.dart'; import '../../../../widgets/textfield_icon_button.dart'; @@ -26,12 +29,17 @@ class AddEditEpicboxMobileView extends ConsumerStatefulWidget { super.key, required this.viewType, this.epicBoxId, - }); + required this.routeOnSuccessOrDelete, + }) : assert( + (viewType == .edit && epicBoxId != null) || + viewType == .add && epicBoxId == null, + ); static const routeName = "/addEditEpicboxMobile"; final AddEditEpicboxMobileViewType viewType; final String? epicBoxId; + final String routeOnSuccessOrDelete; @override ConsumerState createState() => @@ -111,14 +119,15 @@ class _AddEditEpicboxMobileViewState title: const Text("Server currently unreachable"), content: const Text("Would you like to save this server anyways?"), actions: [ + // todo both pop until routeOnSuccessOrDelete ? TextButton( onPressed: () => Navigator.of(context).pop(false), child: Text( "Cancel", style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + color: Theme.of( + context, + ).extension()!.accentColorDark, ), ), ), @@ -127,9 +136,9 @@ class _AddEditEpicboxMobileViewState child: Text( "Save", style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + color: Theme.of( + context, + ).extension()!.accentColorDark, ), ), ), @@ -162,6 +171,8 @@ class _AddEditEpicboxMobileViewState } } + late final bool canDelete; + @override void initState() { super.initState(); @@ -169,20 +180,25 @@ class _AddEditEpicboxMobileViewState _hostController = TextEditingController(); _portController = TextEditingController(); - if (widget.epicBoxId != null) { - final epicBox = ref - .read(nodeServiceChangeNotifierProvider) - .getEpicBoxById(id: widget.epicBoxId!); - if (epicBox != null) { + switch (widget.viewType) { + case .add: + _portController.text = "443"; + port = 443; + canDelete = false; + break; + + case .edit: + final epicBox = ref + .read(nodeServiceChangeNotifierProvider) + .getEpicBoxById(id: widget.epicBoxId!)!; + _nameController.text = epicBox.name; _hostController.text = epicBox.host; _portController.text = (epicBox.port ?? 443).toString(); _useSSL = epicBox.useSSL ?? true; port = epicBox.port ?? 443; - } - } else { - _portController.text = "443"; - port = 443; + canDelete = !epicBox.isDefault; + break; } } @@ -214,196 +230,231 @@ class _AddEditEpicboxMobileViewState : "Edit Epicbox Server", style: STextStyles.navBarTitle(context), ), + actions: [ + if (canDelete) + Padding( + padding: const EdgeInsets.only(top: 10, bottom: 10, right: 10), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("deleteNodeAppBarButtonKey"), + size: 36, + shadows: const [], + color: Theme.of( + context, + ).extension()!.background, + icon: SvgPicture.asset( + Assets.svg.trash, + color: Theme.of( + context, + ).extension()!.accentColorDark, + width: 20, + height: 20, + ), + onPressed: () async { + Navigator.popUntil( + context, + ModalRoute.withName(widget.routeOnSuccessOrDelete), + ); + await ref + .read(nodeServiceChangeNotifierProvider) + .deleteEpicBox(widget.epicBoxId!, true); + }, + ), + ), + ), + ], ), - body: Padding( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - Expanded( - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: false, - enableSuggestions: false, - controller: _nameController, - focusNode: _nameFocusNode, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Server name", - _nameFocusNode, - context, - ).copyWith( - suffixIcon: _nameController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: TextFieldIconButton( - child: const XIcon(), - onTap: () { - _nameController.clear(); - setState(() {}); - }, - ), - ), - ) - : null, - ), - onChanged: (_) => setState(() {}), - ), - ), - const SizedBox(height: 12), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: false, - enableSuggestions: false, - controller: _hostController, - focusNode: _hostFocusNode, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Host", - _hostFocusNode, - context, - ).copyWith( - suffixIcon: _hostController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: TextFieldIconButton( - child: const XIcon(), - onTap: () { - _hostController.clear(); - setState(() {}); - }, - ), - ), - ) - : null, + body: SafeArea( + child: LayoutBuilder( + builder: (builderContext, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: constraints.maxHeight), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: false, + enableSuggestions: false, + controller: _nameController, + focusNode: _nameFocusNode, + style: STextStyles.field(context), + decoration: + standardInputDecoration( + "Server name", + _nameFocusNode, + context, + ).copyWith( + suffixIcon: _nameController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only( + right: 0, + ), + child: UnconstrainedBox( + child: TextFieldIconButton( + child: const XIcon(), + onTap: () { + _nameController.clear(); + setState(() {}); + }, + ), + ), + ) + : null, + ), + onChanged: (_) => setState(() {}), + ), ), - onChanged: (_) => setState(() {}), - ), - ), - const SizedBox(height: 12), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: false, - enableSuggestions: false, - controller: _portController, - focusNode: _portFocusNode, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - ], - keyboardType: TextInputType.number, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Port", - _portFocusNode, - context, - ).copyWith( - suffixIcon: _portController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: TextFieldIconButton( - child: const XIcon(), - onTap: () { - _portController.clear(); - setState(() {}); - }, - ), - ), - ) - : null, + const SizedBox(height: 12), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: false, + enableSuggestions: false, + controller: _hostController, + focusNode: _hostFocusNode, + style: STextStyles.field(context), + decoration: + standardInputDecoration( + "Host", + _hostFocusNode, + context, + ).copyWith( + suffixIcon: _hostController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only( + right: 0, + ), + child: UnconstrainedBox( + child: TextFieldIconButton( + child: const XIcon(), + onTap: () { + _hostController.clear(); + setState(() {}); + }, + ), + ), + ) + : null, + ), + onChanged: (_) => setState(() {}), + ), ), - onChanged: (value) { - port = int.tryParse(value); - setState(() {}); - }, - ), - ), - const SizedBox(height: 12), - Row( - children: [ - GestureDetector( - onTap: () { - setState(() { - _useSSL = !_useSSL; - }); - }, - child: Container( - color: Colors.transparent, - child: Row( - children: [ - SizedBox( - width: 20, - height: 20, - child: Checkbox( - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - value: _useSSL, - onChanged: (newValue) { - setState(() { - _useSSL = newValue!; - }); - }, - ), + const SizedBox(height: 12), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: false, + enableSuggestions: false, + controller: _portController, + focusNode: _portFocusNode, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], + keyboardType: TextInputType.number, + style: STextStyles.field(context), + decoration: + standardInputDecoration( + "Port", + _portFocusNode, + context, + ).copyWith( + suffixIcon: _portController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only( + right: 0, + ), + child: UnconstrainedBox( + child: TextFieldIconButton( + child: const XIcon(), + onTap: () { + _portController.clear(); + setState(() {}); + }, + ), + ), + ) + : null, ), - const SizedBox(width: 12), - Text( - "Use SSL", - style: STextStyles.itemSubtitle12(context), + onChanged: (value) { + port = int.tryParse(value); + setState(() {}); + }, + ), + ), + const SizedBox(height: 12), + Row( + children: [ + GestureDetector( + onTap: () { + setState(() { + _useSSL = !_useSSL; + }); + }, + child: Container( + color: Colors.transparent, + child: Row( + children: [ + SizedBox( + width: 20, + height: 20, + child: Checkbox( + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + value: _useSSL, + onChanged: (newValue) { + setState(() { + _useSSL = newValue!; + }); + }, + ), + ), + const SizedBox(width: 12), + Text( + "Use SSL", + style: STextStyles.itemSubtitle12( + context, + ), + ), + ], ), - ], + ), ), - ), + ], + ), + + const Spacer(), + const SizedBox(height: 16), + SecondaryButton( + label: "Test connection", + enabled: canTestConnection, + onPressed: canTestConnection + ? _testConnection + : null, + ), + const SizedBox(height: 16), + PrimaryButton( + label: "Save", + onPressed: canSave ? _attemptSave : null, ), ], ), - ], - ), - ), - ), - const SizedBox(height: 16), - Row( - children: [ - Expanded( - child: CustomTextButton( - text: "Test connection", - enabled: canTestConnection, - onTap: canTestConnection ? _testConnection : null, ), ), - const SizedBox(width: 16), - Expanded( - child: TextButton( - onPressed: canSave ? _attemptSave : null, - style: canSave - ? Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context) - : Theme.of(context) - .extension()! - .getPrimaryDisabledButtonStyle(context), - child: Text( - "Save", - style: STextStyles.button(context), - ), - ), - ), - ], - ), - ], + ), + ); + }, ), ), ), diff --git a/lib/pages/settings_views/wallet_settings_view/epicbox_settings/manage_epicbox_view.dart b/lib/pages/settings_views/wallet_settings_view/epicbox_settings/manage_epicbox_view.dart index 46765b2e6..797ce8e01 100644 --- a/lib/pages/settings_views/wallet_settings_view/epicbox_settings/manage_epicbox_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/epicbox_settings/manage_epicbox_view.dart @@ -7,6 +7,7 @@ import '../../../../notifications/show_flush_bar.dart'; import '../../../../providers/providers.dart'; import '../../../../themes/stack_colors.dart'; import '../../../../utilities/assets.dart'; +import '../../../../utilities/default_epicboxes.dart'; import '../../../../utilities/test_epicbox_server_connection.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../wallets/wallet/impl/epiccash_wallet.dart'; @@ -28,11 +29,11 @@ class ManageEpicboxView extends ConsumerStatefulWidget { class _ManageEpicboxViewState extends ConsumerState { Future _onConnect(String epicBoxId) async { - final epicBox = ref - .read(nodeServiceChangeNotifierProvider) - .getEpicBoxById(id: epicBoxId); - - if (epicBox == null) return; + final epicBox = + ref + .read(nodeServiceChangeNotifierProvider) + .getEpicBoxById(id: epicBoxId) ?? + DefaultEpicBoxes.all.firstWhere((e) => e.id == epicBoxId); final data = EpicBoxFormData() ..host = epicBox.host @@ -79,6 +80,7 @@ class _ManageEpicboxViewState extends ConsumerState { arguments: ( viewType: AddEditEpicboxMobileViewType.edit, epicBoxId: epicBoxId, + routeOnSuccessOrDelete: ManageEpicboxView.routeName, ), ); } @@ -89,6 +91,7 @@ class _ManageEpicboxViewState extends ConsumerState { arguments: ( viewType: AddEditEpicboxMobileViewType.add, epicBoxId: null, + routeOnSuccessOrDelete: ManageEpicboxView.routeName, ), ); } @@ -104,9 +107,6 @@ class _ManageEpicboxViewState extends ConsumerState { ), ); - final defaultBoxes = epicBoxes.where((e) => e.isDefault).toList(); - final customBoxes = epicBoxes.where((e) => !e.isDefault).toList(); - return Background( child: Scaffold( backgroundColor: Theme.of(context).extension()!.background, @@ -132,9 +132,9 @@ class _ManageEpicboxViewState extends ConsumerState { child: Center( child: Icon( Icons.add, - color: Theme.of(context) - .extension()! - .topNavIconPrimary, + color: Theme.of( + context, + ).extension()!.topNavIconPrimary, size: 20, ), ), @@ -151,7 +151,7 @@ class _ManageEpicboxViewState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (defaultBoxes.isNotEmpty) ...[ + ...[ Padding( padding: const EdgeInsets.symmetric( horizontal: 12, @@ -166,20 +166,20 @@ class _ManageEpicboxViewState extends ConsumerState { ), ), ), - ...defaultBoxes.map( + ...DefaultEpicBoxes.all.map( (epicBox) => Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: EpicBoxCard( key: Key("${epicBox.id}_card_key"), epicBoxId: epicBox.id, onConnect: () => _onConnect(epicBox.id), - onEdit: () => _onEdit(epicBox.id), + onEdit: () {}, testOnInit: primaryEpicBox?.id == epicBox.id, ), ), ), ], - if (customBoxes.isNotEmpty) ...[ + if (epicBoxes.isNotEmpty) ...[ Padding( padding: const EdgeInsets.symmetric( horizontal: 12, @@ -194,7 +194,7 @@ class _ManageEpicboxViewState extends ConsumerState { ), ), ), - ...customBoxes.map( + ...epicBoxes.map( (epicBox) => Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: EpicBoxCard( diff --git a/lib/pages_desktop_specific/settings/settings_menu/epicbox_settings/add_edit_epicbox_view.dart b/lib/pages_desktop_specific/settings/settings_menu/epicbox_settings/add_edit_epicbox_view.dart index 2a091b6d5..07f504621 100644 --- a/lib/pages_desktop_specific/settings/settings_menu/epicbox_settings/add_edit_epicbox_view.dart +++ b/lib/pages_desktop_specific/settings/settings_menu/epicbox_settings/add_edit_epicbox_view.dart @@ -12,6 +12,7 @@ import '../../../../utilities/constants.dart'; import '../../../../utilities/test_epicbox_server_connection.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../../../widgets/desktop/delete_button.dart'; import '../../../../widgets/desktop/desktop_dialog.dart'; import '../../../../widgets/desktop/primary_button.dart'; import '../../../../widgets/desktop/secondary_button.dart'; @@ -27,7 +28,10 @@ class AddEditEpicBoxView extends ConsumerStatefulWidget { required this.viewType, this.epicBoxId, required this.onSave, - }); + }) : assert( + (viewType == .edit && epicBoxId != null) || + viewType == .add && epicBoxId == null, + ); final AddEditEpicBoxViewType viewType; final String? epicBoxId; @@ -198,6 +202,8 @@ class _AddEditEpicBoxViewState extends ConsumerState { } } + late final bool canDelete; + @override void initState() { super.initState(); @@ -205,20 +211,25 @@ class _AddEditEpicBoxViewState extends ConsumerState { _hostController = TextEditingController(); _portController = TextEditingController(); - if (widget.epicBoxId != null) { - final epicBox = ref - .read(nodeServiceChangeNotifierProvider) - .getEpicBoxById(id: widget.epicBoxId!); - if (epicBox != null) { + switch (widget.viewType) { + case .add: + _portController.text = "443"; + port = 443; + canDelete = false; + break; + + case .edit: + final epicBox = ref + .read(nodeServiceChangeNotifierProvider) + .getEpicBoxById(id: widget.epicBoxId!)!; + _nameController.text = epicBox.name; _hostController.text = epicBox.host; _portController.text = (epicBox.port ?? 443).toString(); _useSSL = epicBox.useSSL ?? true; port = epicBox.port ?? 443; - } - } else { - _portController.text = "443"; - port = 443; + canDelete = !epicBox.isDefault; + break; } } @@ -416,7 +427,30 @@ class _AddEditEpicBoxViewState extends ConsumerState { ), ], ), - const SizedBox(height: 78), + const SizedBox(height: 22), + if (canDelete) + SizedBox( + height: 56, + child: Row( + children: [ + Expanded( + child: DeleteButton( + label: "Delete node", + desktopMed: true, + onPressed: () { + Navigator.of(context).pop(); + ref + .read(nodeServiceChangeNotifierProvider) + .deleteEpicBox(widget.epicBoxId!, true); + }, + ), + ), + const SizedBox(width: 16), + const Spacer(), + ], + ), + ), + if (canDelete) const SizedBox(height: 45), Row( children: [ Expanded( diff --git a/lib/pages_desktop_specific/settings/settings_menu/epicbox_settings/desktop_manage_epicbox_dialog.dart b/lib/pages_desktop_specific/settings/settings_menu/epicbox_settings/desktop_manage_epicbox_dialog.dart index 886e564ed..5dab7e2d5 100644 --- a/lib/pages_desktop_specific/settings/settings_menu/epicbox_settings/desktop_manage_epicbox_dialog.dart +++ b/lib/pages_desktop_specific/settings/settings_menu/epicbox_settings/desktop_manage_epicbox_dialog.dart @@ -7,6 +7,7 @@ import '../../../../notifications/show_flush_bar.dart'; import '../../../../providers/providers.dart'; import '../../../../themes/stack_colors.dart'; import '../../../../utilities/assets.dart'; +import '../../../../utilities/default_epicboxes.dart'; import '../../../../utilities/test_epicbox_server_connection.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../wallets/wallet/impl/epiccash_wallet.dart'; @@ -29,11 +30,11 @@ class DesktopManageEpicBoxDialog extends ConsumerStatefulWidget { class _DesktopManageEpicBoxDialogState extends ConsumerState { Future _onConnect(String epicBoxId) async { - final epicBox = ref - .read(nodeServiceChangeNotifierProvider) - .getEpicBoxById(id: epicBoxId); - - if (epicBox == null) return; + final epicBox = + ref + .read(nodeServiceChangeNotifierProvider) + .getEpicBoxById(id: epicBoxId) ?? + DefaultEpicBoxes.all.firstWhere((e) => e.id == epicBoxId); final data = EpicBoxFormData() ..host = epicBox.host @@ -106,9 +107,6 @@ class _DesktopManageEpicBoxDialogState ), ); - final defaultBoxes = epicBoxes.where((e) => e.isDefault).toList(); - final customBoxes = epicBoxes.where((e) => !e.isDefault).toList(); - return DesktopDialog( maxHeight: double.infinity, maxWidth: 580, @@ -146,35 +144,33 @@ class _DesktopManageEpicBoxDialogState child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (defaultBoxes.isNotEmpty) ...[ - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), - child: Text( - "Default servers", - style: STextStyles.smallMed12(context).copyWith( - color: Theme.of( - context, - ).extension()!.textDark3, - ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + child: Text( + "Default servers", + style: STextStyles.smallMed12(context).copyWith( + color: Theme.of( + context, + ).extension()!.textDark3, ), ), - ...defaultBoxes.map( - (epicBox) => Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: EpicBoxCard( - key: Key("${epicBox.id}_card_key"), - epicBoxId: epicBox.id, - onConnect: () => _onConnect(epicBox.id), - onEdit: () => _onEdit(epicBox.id), - testOnInit: primaryEpicBox?.id == epicBox.id, - ), + ), + ...DefaultEpicBoxes.all.map( + (epicBox) => Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: EpicBoxCard( + key: Key("${epicBox.id}_card_key"), + epicBoxId: epicBox.id, + onConnect: () => _onConnect(epicBox.id), + onEdit: () {}, // do nothing for defaults + testOnInit: primaryEpicBox?.id == epicBox.id, ), ), - ], - if (customBoxes.isNotEmpty) ...[ + ), + if (epicBoxes.isNotEmpty) ...[ Padding( padding: const EdgeInsets.symmetric( horizontal: 12, @@ -189,7 +185,7 @@ class _DesktopManageEpicBoxDialogState ), ), ), - ...customBoxes.map( + ...epicBoxes.map( (epicBox) => Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: EpicBoxCard( diff --git a/lib/route_generator.dart b/lib/route_generator.dart index fcf57dab9..cad05cbcd 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -1444,9 +1444,7 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ManageEpicboxView( - walletId: args, - ), + builder: (_) => ManageEpicboxView(walletId: args), settings: RouteSettings(name: settings.name), ); } @@ -1457,12 +1455,14 @@ class RouteGenerator { is ({ AddEditEpicboxMobileViewType viewType, String? epicBoxId, + String routeOnSuccessOrDelete, })) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => AddEditEpicboxMobileView( viewType: args.viewType, epicBoxId: args.epicBoxId, + routeOnSuccessOrDelete: args.routeOnSuccessOrDelete, ), settings: RouteSettings(name: settings.name), ); diff --git a/lib/services/node_service.dart b/lib/services/node_service.dart index 487e60d11..6a8329911 100644 --- a/lib/services/node_service.dart +++ b/lib/services/node_service.dart @@ -294,33 +294,33 @@ class NodeService extends ChangeNotifier { //============================================================================ Future updateDefaultEpicBoxes() async { - final primaryEpicBox = getPrimaryEpicBox(); - - for (final defaultEpicBox in DefaultEpicBoxes.all) { - final savedEpicBox = DB.instance.get( - boxName: DB.boxNameEpicBoxModels, - key: defaultEpicBox.id, - ); - if (savedEpicBox == null) { - await DB.instance.put( - boxName: DB.boxNameEpicBoxModels, - key: defaultEpicBox.id, - value: defaultEpicBox, - ); - } else { - await DB.instance.put( - boxName: DB.boxNameEpicBoxModels, - key: savedEpicBox.id, - value: defaultEpicBox.copyWith(enabled: savedEpicBox.enabled), - ); - } - - if (primaryEpicBox != null && primaryEpicBox.id == defaultEpicBox.id) { - await setPrimaryEpicBox( - epicBox: defaultEpicBox.copyWith(enabled: primaryEpicBox.enabled), - ); - } - } + // final primaryEpicBox = getPrimaryEpicBox(); + // + // for (final defaultEpicBox in DefaultEpicBoxes.all) { + // final savedEpicBox = DB.instance.get( + // boxName: DB.boxNameEpicBoxModels, + // key: defaultEpicBox.id, + // ); + // if (savedEpicBox == null) { + // await DB.instance.put( + // boxName: DB.boxNameEpicBoxModels, + // key: defaultEpicBox.id, + // value: defaultEpicBox, + // ); + // } else { + // await DB.instance.put( + // boxName: DB.boxNameEpicBoxModels, + // key: savedEpicBox.id, + // value: defaultEpicBox.copyWith(enabled: savedEpicBox.enabled), + // ); + // } + // + // if (primaryEpicBox != null && primaryEpicBox.id == defaultEpicBox.id) { + // await setPrimaryEpicBox( + // epicBox: defaultEpicBox.copyWith(enabled: primaryEpicBox.enabled), + // ); + // } + // } // set default primary if none exists if (getPrimaryEpicBox() == null) { diff --git a/lib/widgets/epicbox_card.dart b/lib/widgets/epicbox_card.dart index 3db6ebb12..ca80683e8 100644 --- a/lib/widgets/epicbox_card.dart +++ b/lib/widgets/epicbox_card.dart @@ -5,6 +5,7 @@ import 'package:flutter_svg/svg.dart'; import '../providers/global/node_service_provider.dart'; import '../themes/stack_colors.dart'; import '../utilities/assets.dart'; +import '../utilities/default_epicboxes.dart'; import '../utilities/test_epicbox_server_connection.dart'; import '../utilities/text_styles.dart'; import '../utilities/util.dart'; @@ -55,10 +56,11 @@ class _EpicBoxCardState extends ConsumerState { } Future _testConnection() async { - final epicBox = ref - .read(nodeServiceChangeNotifierProvider) - .getEpicBoxById(id: widget.epicBoxId); - if (epicBox == null) return; + final epicBox = + ref + .read(nodeServiceChangeNotifierProvider) + .getEpicBoxById(id: widget.epicBoxId) ?? + DefaultEpicBoxes.all.firstWhere((e) => e.id == widget.epicBoxId); setState(() { _testing = true; @@ -82,15 +84,13 @@ class _EpicBoxCardState extends ConsumerState { @override Widget build(BuildContext context) { - final epicBox = ref.watch( - nodeServiceChangeNotifierProvider.select( - (value) => value.getEpicBoxById(id: widget.epicBoxId), - ), - ); - - if (epicBox == null) { - return const SizedBox.shrink(); - } + final epicBox = + ref.watch( + nodeServiceChangeNotifierProvider.select( + (value) => value.getEpicBoxById(id: widget.epicBoxId), + ), + ) ?? + DefaultEpicBoxes.all.firstWhere((e) => e.id == widget.epicBoxId); final primaryEpicBox = ref.watch( nodeServiceChangeNotifierProvider.select( @@ -107,14 +107,18 @@ class _EpicBoxCardState extends ConsumerState { status = "Testing..."; } else if (_testResult == true) { status = isPrimary ? "Connected" : "Reachable"; - statusColor = Theme.of(context).extension()!.accentColorGreen; + statusColor = Theme.of( + context, + ).extension()!.accentColorGreen; } else if (_testResult == false) { status = "Unreachable"; statusColor = Theme.of(context).extension()!.accentColorRed; } else { status = isPrimary ? "Selected" : ""; if (isPrimary) { - statusColor = Theme.of(context).extension()!.accentColorBlue; + statusColor = Theme.of( + context, + ).extension()!.accentColorBlue; } }