feat: add mobile epicbox server mgmt ui
What changed, and why it matters
This commit adds a new mobile screen for managing 'Epicbox' servers used by Epic Cash wallets. It lets users add, edit, and pick custom server addresses. The change is a feature addition; there is no direct evidence in the commit that it fixes a security bug or introduces a clear vulnerability, but it does expose network endpoint configuration to the user and performs live connection tests from the app.
Treat as a routine feature commit. If auditing, review the implementation of testEpicBoxServerConnection and EpicBoxServerModel storage for SSRF, open redirect, or certificate validation issues, and verify that user-supplied hosts cannot be used to probe internal services or bypass intended server allowlists.
Security signals we found
New user-facing network endpoint configuration UI
Custom server host/port accepted from user input and stored via node service
Live network connection test performed against user-supplied host before save
SSL toggle defaults to true but can be disabled by user
No visible hostname format validation, certificate pinning, or allowlist in the diff
Evidence from the diff
The patch introduces two new Flutter views (ManageEpicboxView and AddEditEpicboxMobileView), wires them into the route generator, and adds an ‘Epicbox Servers’ entry in wallet settings for Epic Cash wallets. Users can input host, port, and SSL toggle; the app calls testEpicBoxServerConnection() before saving and when connecting. The didUpdateWidget lifecycle in EpicBoxCard triggers an automatic connection test when a card becomes the primary selection. No input validation beyond port range and non-empty host is visible in the diff.
Changed components
lib/pages/settings_views/wallet_settings_view/epicbox_settings/add_edit_epicbox_mobile_view.dartlib/pages/settings_views/wallet_settings_view/epicbox_settings/manage_epicbox_view.dartlib/pages/settings_views/wallet_settings_view/wallet_settings_view.dartlib/route_generator.dartlib/widgets/epicbox_card.dartInspect captured patch +685 / −0
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 0000000..9208387
--- /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<AddEditEpicboxMobileView> createState() =>
+ _AddEditEpicboxMobileViewState();
+}
+
+class _AddEditEpicboxMobileViewState
+ extends ConsumerState<AddEditEpicboxMobileView> {
+ 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<void> _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<void> _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<bool>(
+ 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<StackColors>()!
+ .accentColorDark,
+ ),
+ ),
+ ),
+ TextButton(
+ onPressed: () => Navigator.of(context).pop(true),
+ child: Text(
+ "Save",
+ style: STextStyles.button(context).copyWith(
+ color: Theme.of(context)
+ .extension<StackColors>()!
+ .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<StackColors>()!.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<StackColors>()!
+ .getPrimaryEnabledButtonStyle(context)
+ : Theme.of(context)
+ .extension<StackColors>()!
+ .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 0000000..46765b2
--- /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<ManageEpicboxView> createState() => _ManageEpicboxViewState();
+}
+
+class _ManageEpicboxViewState extends ConsumerState<ManageEpicboxView> {
+ Future<void> _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<StackColors>()!.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<StackColors>()!
+ .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<StackColors>()!.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<StackColors>()!.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 58ffaad..4fd2e8f 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<WalletSettingsView> {
);
},
),
+ 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 21c20a0..fcf57da 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<String> mnemonic})) {
return getRoute(
diff --git a/lib/widgets/epicbox_card.dart b/lib/widgets/epicbox_card.dart
index 2ca335a..3db6ebb 100644
--- a/lib/widgets/epicbox_card.dart
+++ b/lib/widgets/epicbox_card.dart
@@ -43,6 +43,17 @@ class _EpicBoxCardState extends ConsumerState<EpicBoxCard> {
}
}
+ @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<void> _testConnection() async {
final epicBox = ref
.read(nodeServiceChangeNotifierProvider)
Why this scored 16/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.