From 3e19d6917ff245863be43a39a76daa7010ecda6f Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Wed, 01 Apr 2026 18:35:25 +0200
Subject: [PATCH] feat: auto-reconnect on network change (WiFi/cellular/VPN switch)

---
 windows/flutter/generated_plugins.cmake        |    1 
 windows/flutter/generated_plugin_registrant.cc |    3 +
 ios/Podfile.lock                               |    6 +++
 macos/Flutter/GeneratedPluginRegistrant.swift  |    2 +
 lib/services/mqtt_service.dart                 |   35 +++++++++++++++++
 pubspec.lock                                   |   24 ++++++++++++
 pubspec.yaml                                   |    1 
 7 files changed, 72 insertions(+), 0 deletions(-)

diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 251c69f..f5d7ae0 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -5,6 +5,8 @@
   - bonsoir_darwin (0.0.1):
     - Flutter
     - FlutterMacOS
+  - connectivity_plus (0.0.1):
+    - Flutter
   - device_info_plus (0.0.1):
     - Flutter
   - DKImagePickerController/Core (4.3.9):
@@ -70,6 +72,7 @@
 DEPENDENCIES:
   - audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/darwin`)
   - bonsoir_darwin (from `.symlinks/plugins/bonsoir_darwin/darwin`)
+  - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
   - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
   - file_picker (from `.symlinks/plugins/file_picker/ios`)
   - Flutter (from `Flutter`)
@@ -95,6 +98,8 @@
     :path: ".symlinks/plugins/audioplayers_darwin/darwin"
   bonsoir_darwin:
     :path: ".symlinks/plugins/bonsoir_darwin/darwin"
+  connectivity_plus:
+    :path: ".symlinks/plugins/connectivity_plus/ios"
   device_info_plus:
     :path: ".symlinks/plugins/device_info_plus/ios"
   file_picker:
@@ -123,6 +128,7 @@
 SPEC CHECKSUMS:
   audioplayers_darwin: 835ced6edd4c9fc8ebb0a7cc9e294a91d99917d5
   bonsoir_darwin: 29c7ccf356646118844721f36e1de4b61f6cbd0e
+  connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
   device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
   DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
   DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
diff --git a/lib/services/mqtt_service.dart b/lib/services/mqtt_service.dart
index e50adcf..841182d 100644
--- a/lib/services/mqtt_service.dart
+++ b/lib/services/mqtt_service.dart
@@ -5,6 +5,7 @@
 import 'package:crypto/crypto.dart';
 
 import 'package:bonsoir/bonsoir.dart';
+import 'package:connectivity_plus/connectivity_plus.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/widgets.dart';
 import 'package:path_provider/path_provider.dart' as pp;
@@ -50,6 +51,8 @@
   bool _intentionalClose = false;
   String? _clientId;
   String? _lastDiscoveredHost;
+  StreamSubscription? _connectivitySub;
+  List<ConnectivityResult>? _lastConnectivity;
   StreamSubscription? _updatesSub;
 
   // Message deduplication
@@ -103,6 +106,36 @@
 
     _intentionalClose = false;
     _setStatus(ConnectionStatus.connecting);
+
+    // Start listening for network changes (WiFi↔cellular, VPN connect/disconnect)
+    _connectivitySub ??= Connectivity().onConnectivityChanged.listen((results) {
+      if (_lastConnectivity != null && !_intentionalClose) {
+        final changed = results.length != _lastConnectivity!.length ||
+            !results.every((r) => _lastConnectivity!.contains(r));
+        if (changed) {
+          _mqttLog('MQTT: network changed: ${results.map((r) => r.name).join(",")} — forcing reconnect');
+          // Force disconnect and reconnect on new network
+          final client = _client;
+          if (client != null) {
+            _intentionalClose = true;
+            client.autoReconnect = false;
+            try { client.disconnect(); } catch (_) {}
+            _client = null;
+            _updatesSub?.cancel();
+            _updatesSub = null;
+            _intentionalClose = false;
+          }
+          _lastDiscoveredHost = null; // Clear cached discovery — subnet may have changed
+          connectedHost = null;
+          connectedVia = null;
+          _setStatus(ConnectionStatus.reconnecting);
+          Future.delayed(const Duration(milliseconds: 500), () {
+            if (!_intentionalClose) connect();
+          });
+        }
+      }
+      _lastConnectivity = results;
+    });
 
     // Load trusted cert fingerprint for TOFU verification
     if (_trustedFingerprint == null) await _loadTrustedFingerprint();
@@ -724,6 +757,8 @@
     _intentionalClose = true;
     _updatesSub?.cancel();
     _updatesSub = null;
+    _connectivitySub?.cancel();
+    _connectivitySub = null;
 
     try {
       _client?.disconnect();
diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift
index 0a6522c..96555f3 100644
--- a/macos/Flutter/GeneratedPluginRegistrant.swift
+++ b/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -7,6 +7,7 @@
 
 import audioplayers_darwin
 import bonsoir_darwin
+import connectivity_plus
 import device_info_plus
 import file_picker
 import file_selector_macos
@@ -20,6 +21,7 @@
 func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
   AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
   SwiftBonsoirPlugin.register(with: registry.registrar(forPlugin: "SwiftBonsoirPlugin"))
+  ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
   DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
   FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
   FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
diff --git a/pubspec.lock b/pubspec.lock
index 60df114..f3d42d4 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -161,6 +161,22 @@
       url: "https://pub.dev"
     source: hosted
     version: "1.19.1"
+  connectivity_plus:
+    dependency: "direct main"
+    description:
+      name: connectivity_plus
+      sha256: b8fe52979ff12432ecf8f0abf6ff70410b1bb734be1c9e4f2f86807ad7166c79
+      url: "https://pub.dev"
+    source: hosted
+    version: "7.1.0"
+  connectivity_plus_platform_interface:
+    dependency: transitive
+    description:
+      name: connectivity_plus_platform_interface
+      sha256: "3c09627c536d22fd24691a905cdd8b14520de69da52c7a97499c8be5284a32ed"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.0"
   cross_file:
     dependency: transitive
     description:
@@ -608,6 +624,14 @@
       url: "https://pub.dev"
     source: hosted
     version: "0.17.6"
+  nm:
+    dependency: transitive
+    description:
+      name: nm
+      sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.5.0"
   objective_c:
     dependency: transitive
     description:
diff --git a/pubspec.yaml b/pubspec.yaml
index ef8929f..c084091 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -33,6 +33,7 @@
   crypto: ^3.0.7
   push: ^3.3.3
   flutter_app_badger: ^1.5.0
+  connectivity_plus: ^7.1.0
 
 dev_dependencies:
   flutter_test:
diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc
index 5b79aa2..4d9149a 100644
--- a/windows/flutter/generated_plugin_registrant.cc
+++ b/windows/flutter/generated_plugin_registrant.cc
@@ -8,6 +8,7 @@
 
 #include <audioplayers_windows/audioplayers_windows_plugin.h>
 #include <bonsoir_windows/bonsoir_windows_plugin_c_api.h>
+#include <connectivity_plus/connectivity_plus_windows_plugin.h>
 #include <file_selector_windows/file_selector_windows.h>
 #include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
 #include <permission_handler_windows/permission_handler_windows_plugin.h>
@@ -20,6 +21,8 @@
       registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin"));
   BonsoirWindowsPluginCApiRegisterWithRegistrar(
       registry->GetRegistrarForPlugin("BonsoirWindowsPluginCApi"));
+  ConnectivityPlusWindowsPluginRegisterWithRegistrar(
+      registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
   FileSelectorWindowsRegisterWithRegistrar(
       registry->GetRegistrarForPlugin("FileSelectorWindows"));
   FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake
index 2d751af..2f3a067 100644
--- a/windows/flutter/generated_plugins.cmake
+++ b/windows/flutter/generated_plugins.cmake
@@ -5,6 +5,7 @@
 list(APPEND FLUTTER_PLUGIN_LIST
   audioplayers_windows
   bonsoir_windows
+  connectivity_plus
   file_selector_windows
   flutter_secure_storage_windows
   permission_handler_windows

--
Gitblit v1.3.1