From 32c999cbe477b2c4ab6a53b8ddcef35e80dd553e Mon Sep 17 00:00:00 2001 From: Jigar-f Date: Tue, 24 Mar 2026 17:43:59 +0530 Subject: [PATCH 1/3] Added check if server is available or not. --- .../lantern/handler/MethodHandler.kt | 15 +++ ios/Runner/Handlers/MethodHandler.swift | 12 +++ lantern-core/ffi/ffi.go | 23 +++++ lantern-core/mobile/mobile.go | 15 +++ lantern-core/vpn_tunnel/vpn_tunnel.go | 2 +- lib/features/vpn/provider/vpn_notifier.dart | 92 +++++++++++-------- lib/lantern/lantern_core_service.dart | 2 + lib/lantern/lantern_ffi_service.dart | 13 +++ lib/lantern/lantern_generated_bindings.dart | 13 +++ lib/lantern/lantern_platform_service.dart | 11 +++ lib/lantern/lantern_service.dart | 8 ++ macos/Runner/Handlers/MethodHandler.swift | 12 +++ 12 files changed, 177 insertions(+), 41 deletions(-) diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/handler/MethodHandler.kt b/android/app/src/main/kotlin/org/getlantern/lantern/handler/MethodHandler.kt index ba43f833d8..4688dce3a3 100644 --- a/android/app/src/main/kotlin/org/getlantern/lantern/handler/MethodHandler.kt +++ b/android/app/src/main/kotlin/org/getlantern/lantern/handler/MethodHandler.kt @@ -36,6 +36,7 @@ enum class Methods(val method: String) { Stop("stopVPN"), ConnectToServer("connectToServer"), IsVpnConnected("isVPNConnected"), + IsTagAvailable("isTagAvailable"), //Payment methods StripeSubscription("stripeSubscription"), @@ -195,6 +196,20 @@ class MethodHandler : FlutterPlugin, } } + Methods.IsTagAvailable.method -> { + scope.launch { + runCatching { + val tag = call.arguments as String? ?: "" + val available = Mobile.isTagAvailable(tag) + withContext(Dispatchers.Main) { + result.success(available) + } + }.onFailure { e -> + result.error("tag_check_failed", e.localizedMessage ?: "Error", e) + } + } + } + Methods.IsVpnConnected.method -> { scope.launch { runCatching { diff --git a/ios/Runner/Handlers/MethodHandler.swift b/ios/Runner/Handlers/MethodHandler.swift index f5cf3b982f..e9c6ecbdc2 100644 --- a/ios/Runner/Handlers/MethodHandler.swift +++ b/ios/Runner/Handlers/MethodHandler.swift @@ -32,6 +32,13 @@ class MethodHandler { case "startVPN": self.startVPN(result: result) + case "isTagAvailable": + guard let tag = call.arguments as? String else { + result(true) + return + } + self.isTagAvailable(result: result, tag: tag) + case "connectToServer": guard let data = self.decodeDict(from: call.arguments, result: result) else { return } self.connectToServer(result: result, data: data) @@ -301,6 +308,11 @@ class MethodHandler { } } + private func isTagAvailable(result: @escaping FlutterResult, tag: String) { + let available = MobileIsTagAvailable(tag) + result(available) + } + private func connectToServer(result: @escaping FlutterResult, data: [String: Any]) { Task { do { diff --git a/lantern-core/ffi/ffi.go b/lantern-core/ffi/ffi.go index b88bdb169a..1609c82687 100644 --- a/lantern-core/ffi/ffi.go +++ b/lantern-core/ffi/ffi.go @@ -277,6 +277,29 @@ func getAutoLocation() *C.char { return C.CString(string(jsonBytes)) } +// isTagAvailable checks if a server with the given tag exists in the server list. +// Returns "true" if found, "false" if not found, or "true" when the check cannot be +// performed (fail-open: allows connection attempts to proceed normally). +// +//export isTagAvailable +func isTagAvailable(_tag *C.char) *C.char { + tag := C.GoString(_tag) + c, errStr := requireCore() + if errStr != nil { + slog.Warn("Unable to check tag availability (core not ready), assuming available", "tag", tag) + return C.CString("true") + } + _, found, err := c.GetServerByTagJSON(tag) + if err != nil { + slog.Warn("Error checking tag availability, assuming available", "tag", tag, "error", err) + return C.CString("true") + } + if found { + return C.CString("true") + } + return C.CString("false") +} + // startAutoLocationListener starts the auto location listener. // //export startAutoLocationListener diff --git a/lantern-core/mobile/mobile.go b/lantern-core/mobile/mobile.go index 7d817fb88e..e5391a57a4 100644 --- a/lantern-core/mobile/mobile.go +++ b/lantern-core/mobile/mobile.go @@ -180,6 +180,21 @@ func CloseIPC() error { return vpn_tunnel.CloseIPC() } +// IsTagAvailable checks if a server with the given tag exists in the server list. +// Returns true if the tag is found. Returns true when the check cannot be performed +// (fail-open: allows connection attempts to proceed normally). +func IsTagAvailable(tag string) bool { + found, err := withCoreR(func(c lanterncore.Core) (bool, error) { + _, ok, err := c.GetServerByTagJSON(tag) + return ok, err + }) + if err != nil { + slog.Warn("Unable to check tag availability, assuming available", "tag", tag, "error", err) + return true + } + return found +} + // ConnectToServer connects to a server using the provided location type and tag. // It works with private servers and lantern location servers. func ConnectToServer(locationType, tag string, platIfce utils.PlatformInterface, options *utils.Opts) error { diff --git a/lantern-core/vpn_tunnel/vpn_tunnel.go b/lantern-core/vpn_tunnel/vpn_tunnel.go index 0388e996c4..ed10e090a9 100644 --- a/lantern-core/vpn_tunnel/vpn_tunnel.go +++ b/lantern-core/vpn_tunnel/vpn_tunnel.go @@ -35,7 +35,7 @@ func StartVPN(platform rvpn.PlatformInterface, opts *utils.Opts) error { } // it should use InternalTagLantern so it will connect to best lantern server by default. // if you want to connect to user server, use ConnectToServer with InternalTagUser - err := vpn.QuickConnect("", platform) + err := vpn.AutoConnect("") if err != nil { return fmt.Errorf("failed to start VPN: %w", err) } diff --git a/lib/features/vpn/provider/vpn_notifier.dart b/lib/features/vpn/provider/vpn_notifier.dart index 4242e738ee..5ca164bac1 100644 --- a/lib/features/vpn/provider/vpn_notifier.dart +++ b/lib/features/vpn/provider/vpn_notifier.dart @@ -18,45 +18,42 @@ class VpnNotifier extends _$VpnNotifier { @override VPNStatus build() { ref.read(lanternServiceProvider).isVPNConnected(); - ref.listen( - vPNStatusProvider, - (previous, next) { - final previousStatus = previous?.value?.status; - final nextStatus = next.value!.status; - - if (previous != null && - previous.value != null && - previousStatus != nextStatus) { - if (previousStatus != VPNStatus.connecting && - nextStatus == VPNStatus.disconnected) { - sl().showNotification( - id: NotificationEvent.vpnDisconnected.id, - title: 'app_name'.i18n, - body: 'vpn_disconnected'.i18n, - ); - } else if (nextStatus == VPNStatus.connected) { - if (PlatformUtils.isMobile) { - HapticFeedback.mediumImpact(); - } - - /// Mark successful connection in app settings - ref.read(appSettingProvider.notifier).setSuccessfulConnection(true); - - // Server location is updated via the "server-location" push event - // from the Go side (handled by AppEventNotifier), not by polling - // getAutoServerLocation here. This avoids a race where the NE - // reports "connected" before the Go tunnel is fully ready. - - sl().showNotification( - id: NotificationEvent.vpnConnected.id, - title: 'app_name'.i18n, - body: 'vpn_connected'.i18n, - ); + ref.listen(vPNStatusProvider, (previous, next) { + final previousStatus = previous?.value?.status; + final nextStatus = next.value!.status; + + if (previous != null && + previous.value != null && + previousStatus != nextStatus) { + if (previousStatus != VPNStatus.connecting && + nextStatus == VPNStatus.disconnected) { + sl().showNotification( + id: NotificationEvent.vpnDisconnected.id, + title: 'app_name'.i18n, + body: 'vpn_disconnected'.i18n, + ); + } else if (nextStatus == VPNStatus.connected) { + if (PlatformUtils.isMobile) { + HapticFeedback.mediumImpact(); } + + /// Mark successful connection in app settings + ref.read(appSettingProvider.notifier).setSuccessfulConnection(true); + + // Server location is updated via the "server-location" push event + // from the Go side (handled by AppEventNotifier), not by polling + // getAutoServerLocation here. This avoids a race where the NE + // reports "connected" before the Go tunnel is fully ready. + + sl().showNotification( + id: NotificationEvent.vpnConnected.id, + title: 'app_name'.i18n, + body: 'vpn_connected'.i18n, + ); } - state = nextStatus; - }, - ); + } + state = nextStatus; + }); return VPNStatus.disconnected; } @@ -86,17 +83,32 @@ class VpnNotifier extends _$VpnNotifier { final type = serverLocation.serverType.toServerLocationType; if (type == ServerLocationType.auto || force) { appLogger.debug( - 'Got server location with type auto or force is true, starting VPN with auto'); + 'Got server location with type auto or force is true, starting VPN with auto', + ); + return lantern.startVPN(); + } + + final tag = serverLocation.serverName; + + /// Check if the tag is still available before trying to connect to it, if not fallback to auto VPN. + /// This is to avoid trying to connect to a server that is no longer available + final tagAvailable = await lantern.isTagAvailable(tag); + if (!tagAvailable) { + appLogger.debug( + 'Server tag "$tag" not available, falling back to auto VPN', + ); return lantern.startVPN(); } - return connectToServer(type, serverLocation.serverName); + return connectToServer(type, tag); } /// Connects to a specific server location. /// it supports lantern locations and private servers. Future> connectToServer( - ServerLocationType location, String tag) async { + ServerLocationType location, + String tag, + ) async { appLogger.debug("Connecting to server: $location with tag: $tag"); final result = await ref .read(lanternServiceProvider) diff --git a/lib/lantern/lantern_core_service.dart b/lib/lantern/lantern_core_service.dart index c7ddfae219..a4d2b43e8a 100644 --- a/lib/lantern/lantern_core_service.dart +++ b/lib/lantern/lantern_core_service.dart @@ -35,6 +35,8 @@ abstract class LanternCoreService { Future> connectToServer(String location, String tag); + Future isTagAvailable(String tag); + Stream watchVPNStatus(); Stream> watchLogs(String path); diff --git a/lib/lantern/lantern_ffi_service.dart b/lib/lantern/lantern_ffi_service.dart index d5b42bb1ae..151b945267 100644 --- a/lib/lantern/lantern_ffi_service.dart +++ b/lib/lantern/lantern_ffi_service.dart @@ -619,6 +619,19 @@ class LanternFFIService implements LanternCoreService { } } + @override + Future isTagAvailable(String tag) async { + try { + final result = await runInBackground(() async { + return _ffiService.isTagAvailable(tag.toCharPtr).toDartString(); + }); + return result == 'true'; + } catch (e, st) { + appLogger.error('Error checking tag availability, assuming available', e, st); + return true; + } + } + /// connectToServer is used to connect to a server /// this will work with lantern customer and private server /// requires location and tag diff --git a/lib/lantern/lantern_generated_bindings.dart b/lib/lantern/lantern_generated_bindings.dart index bc0c34bfc7..38bb0916d3 100644 --- a/lib/lantern/lantern_generated_bindings.dart +++ b/lib/lantern/lantern_generated_bindings.dart @@ -2602,6 +2602,19 @@ class LanternBindings { late final _getAutoLocation = _getAutoLocationPtr.asFunction Function()>(); + ffi.Pointer isTagAvailable( + ffi.Pointer _tag, + ) { + return _isTagAvailable(_tag); + } + + late final _isTagAvailablePtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer)>>('isTagAvailable'); + late final _isTagAvailable = _isTagAvailablePtr + .asFunction Function(ffi.Pointer)>(); + ffi.Pointer startAutoLocationListener() { return _startAutoLocationListener(); } diff --git a/lib/lantern/lantern_platform_service.dart b/lib/lantern/lantern_platform_service.dart index 082419dcf6..97567180cf 100644 --- a/lib/lantern/lantern_platform_service.dart +++ b/lib/lantern/lantern_platform_service.dart @@ -1097,6 +1097,17 @@ class LanternPlatformService implements LanternCoreService { } } + @override + Future isTagAvailable(String tag) async { + try { + final result = + await _methodChannel.invokeMethod('isTagAvailable', tag); + return result ?? true; + } catch (e) { + return true; + } + } + @override Future> connectToServer( String location, String tag) async { diff --git a/lib/lantern/lantern_service.dart b/lib/lantern/lantern_service.dart index 01d9c0abc8..8843fd09ea 100644 --- a/lib/lantern/lantern_service.dart +++ b/lib/lantern/lantern_service.dart @@ -551,6 +551,14 @@ class LanternService implements LanternCoreService { ); } + @override + Future isTagAvailable(String tag) { + if (PlatformUtils.isFFISupported) { + return _ffiService.isTagAvailable(tag); + } + return _platformService.isTagAvailable(tag); + } + /// connectToServer is used to connect to a server /// this will work with lantern customer and private server /// requires location and tag diff --git a/macos/Runner/Handlers/MethodHandler.swift b/macos/Runner/Handlers/MethodHandler.swift index 17259c5162..a3b09214d7 100644 --- a/macos/Runner/Handlers/MethodHandler.swift +++ b/macos/Runner/Handlers/MethodHandler.swift @@ -31,6 +31,13 @@ class MethodHandler { case "startVPN": self.startVPN(result: result) + case "isTagAvailable": + guard let tag = call.arguments as? String else { + result(true) + return + } + self.isTagAvailable(result: result, tag: tag) + case "connectToServer": guard let data = self.decodeDict(from: call.arguments, result: result) else { return } self.connectToServer(result: result, data: data) @@ -327,6 +334,11 @@ class MethodHandler { } } + private func isTagAvailable(result: @escaping FlutterResult, tag: String) { + let available = MobileIsTagAvailable(tag) + result(available) + } + private func connectToServer(result: @escaping FlutterResult, data: [String: Any]) { Task { do { From 7794eaa7966f794675e388a1f69e64d64db13d65 Mon Sep 17 00:00:00 2001 From: atavism Date: Tue, 24 Mar 2026 10:50:41 -0700 Subject: [PATCH 2/3] Tighten isTagAvailable handling (#8572) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update radiance + lantern-box for bandit distributed tracing (#8566) - radiance: picks up lantern-box v0.0.51 - lantern-box v0.0.51: propagates traceparent from bandit callback URLs, enabling contiguous distributed tracing across the feedback loop Co-authored-by: Claude Opus 4.6 (1M context) * System tray changes (#8568) * added back sys tray changes * server location changes * add flags only on desktop versions. * code review updates (#8569) * Update lantern_platform_service.dart --------- Co-authored-by: atavism * Update radiance with bandit callback fixes (#8570) Picks up: - Pre-test uses live network config (not stale disk config) - updateGroup fires SetURLOverrides + CheckOutbounds on repeat configs - Pre-test timeout 5s β†’ 15s for proxy callback tests Co-authored-by: Claude Opus 4.6 (1M context) * code review updates --------- Co-authored-by: Myles Horton Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: jigar-f <132374182+jigar-f@users.noreply.github.com> --- .../lantern/handler/MethodHandler.kt | 11 +- go.mod | 4 +- go.sum | 8 +- ios/Runner/Handlers/MethodHandler.swift | 3 +- lib/features/home/home.dart | 6 +- lib/features/home/provider/home_notifier.dart | 20 +- .../provider/system_tray_notifier.dart | 250 +++++++++++++----- .../provider/system_tray_notifier.g.dart | 2 +- lib/features/vpn/location_setting.dart | 135 ++++------ .../provider/server_location_notifier.dart | 65 ++--- .../provider/server_location_notifier.g.dart | 22 +- lib/features/vpn/provider/vpn_notifier.dart | 11 +- lib/features/vpn/provider/vpn_notifier.g.dart | 2 +- lib/features/vpn/server_selection.dart | 17 +- lib/lantern/lantern_ffi_service.dart | 79 ++++-- lib/lantern/lantern_generated_bindings.dart | 13 + lib/lantern/lantern_platform_service.dart | 10 + lib/lantern/lantern_service.dart | 8 + lib/lantern/lantern_service_notifier.g.dart | 2 +- macos/Runner/Handlers/MethodHandler.swift | 3 +- pubspec.yaml | 6 +- 21 files changed, 414 insertions(+), 263 deletions(-) diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/handler/MethodHandler.kt b/android/app/src/main/kotlin/org/getlantern/lantern/handler/MethodHandler.kt index 4688dce3a3..96f3d5e036 100644 --- a/android/app/src/main/kotlin/org/getlantern/lantern/handler/MethodHandler.kt +++ b/android/app/src/main/kotlin/org/getlantern/lantern/handler/MethodHandler.kt @@ -198,14 +198,17 @@ class MethodHandler : FlutterPlugin, Methods.IsTagAvailable.method -> { scope.launch { - runCatching { - val tag = call.arguments as String? ?: "" + try { + val tag = call.arguments as? String + ?: throw IllegalArgumentException("Missing or invalid tag") val available = Mobile.isTagAvailable(tag) withContext(Dispatchers.Main) { result.success(available) } - }.onFailure { e -> - result.error("tag_check_failed", e.localizedMessage ?: "Error", e) + } catch (e: Throwable) { + withContext(Dispatchers.Main) { + result.error("tag_check_failed", e.localizedMessage ?: "Error", e) + } } } } diff --git a/go.mod b/go.mod index 336f540635..c3af21e356 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( github.com/alecthomas/assert/v2 v2.3.0 github.com/getlantern/common v1.2.1-0.20260224184656-5aefb9c21c85 github.com/getlantern/lantern-server-provisioner v0.0.0-20251031121934-8ea031fccfa9 - github.com/getlantern/radiance v0.0.0-20260323184750-279e5efda556 + github.com/getlantern/radiance v0.0.0-20260324154403-6c425327102b github.com/sagernet/sing-box v1.12.22 golang.org/x/mobile v0.0.0-20250711185624-d5bb5ecc55c0 golang.org/x/sys v0.41.0 @@ -172,7 +172,7 @@ require ( github.com/getlantern/fronted v0.0.0-20260319225233-cf2160f85053 // indirect github.com/getlantern/keepcurrent v0.0.0-20260304213122-017d542145ae // indirect github.com/getlantern/kindling v0.0.0-20260319225424-4736208dd171 // indirect - github.com/getlantern/lantern-box v0.0.50 // indirect + github.com/getlantern/lantern-box v0.0.51 // indirect github.com/getlantern/lantern-water v0.0.0-20260317143726-e0ee64a11d90 // indirect github.com/getlantern/osversion v0.0.0-20240418205916-2e84a4a4e175 // indirect github.com/getlantern/pluriconfig v0.0.0-20251126214241-8cc8bc561535 // indirect diff --git a/go.sum b/go.sum index bce863a583..2e2d638d4a 100644 --- a/go.sum +++ b/go.sum @@ -247,8 +247,8 @@ github.com/getlantern/keepcurrent v0.0.0-20260304213122-017d542145ae h1:NMq3K7h3 github.com/getlantern/keepcurrent v0.0.0-20260304213122-017d542145ae/go.mod h1:ag5g9aWUw2FJcX5RVRpJ9EBQBy5yJuy2WXDouIn/m4w= github.com/getlantern/kindling v0.0.0-20260319225424-4736208dd171 h1:UEjX+Gg+T6oGVUbzHJ4JfLhlsIh8Wl8PmTXZYWGS43A= github.com/getlantern/kindling v0.0.0-20260319225424-4736208dd171/go.mod h1:c5cFjpNrqX8wQ0PUE2blHrO7knAlRCVx3j1/G6zaVlY= -github.com/getlantern/lantern-box v0.0.50 h1:WKmw56+IClysCIg6VB7P4Hti1Vc7tEC405xbV6I7dwA= -github.com/getlantern/lantern-box v0.0.50/go.mod h1:Luj0rLyuokADHg2B+eXlAdxVXYO+T5Reeds+hKuQkZA= +github.com/getlantern/lantern-box v0.0.51 h1:ahneSAcd75m8jHqx9k85dghmboEhLp6NpjMtp1/zBcM= +github.com/getlantern/lantern-box v0.0.51/go.mod h1:Luj0rLyuokADHg2B+eXlAdxVXYO+T5Reeds+hKuQkZA= github.com/getlantern/lantern-server-provisioner v0.0.0-20251031121934-8ea031fccfa9 h1:6seyD2f9tz2am0YQd/Qn+q7LFiiQgnmxgwWFnVceGZw= github.com/getlantern/lantern-server-provisioner v0.0.0-20251031121934-8ea031fccfa9/go.mod h1:s0VKrlJf/z+M0U8IKHFL2hfuflocRw3SINmMacrTlMA= github.com/getlantern/lantern-water v0.0.0-20260317143726-e0ee64a11d90 h1:P9JX1yAu2uq3b5YiT0sLtHkTrkZuttV8gPZh81nUuag= @@ -259,8 +259,8 @@ github.com/getlantern/osversion v0.0.0-20240418205916-2e84a4a4e175 h1:JWH5BB2o0e github.com/getlantern/osversion v0.0.0-20240418205916-2e84a4a4e175/go.mod h1:h3S9LBmmzN/xM+lwYZHE4abzTtCTtidKtG+nxZcCZX0= github.com/getlantern/pluriconfig v0.0.0-20251126214241-8cc8bc561535 h1:rtDmW8YLAuT8r51ApR5z0d8/qjhHu3TW+divQ2C98Ac= github.com/getlantern/pluriconfig v0.0.0-20251126214241-8cc8bc561535/go.mod h1:WKJEdjMOD4IuTRYwjQHjT4bmqDl5J82RShMLxPAvi0Q= -github.com/getlantern/radiance v0.0.0-20260323184750-279e5efda556 h1:Az3xs4Hf9+ocTgztidJmomFsh53+c/CSdPupvTm2oJc= -github.com/getlantern/radiance v0.0.0-20260323184750-279e5efda556/go.mod h1:mKokYt24akTTI1LR4n6LT4C57lL+j0C97q3vQljpX8o= +github.com/getlantern/radiance v0.0.0-20260324154403-6c425327102b h1:Rl7D7DjBDxz1p+u9f53MavQFQabh4rdbhMA5aWRARr8= +github.com/getlantern/radiance v0.0.0-20260324154403-6c425327102b/go.mod h1:bqqrKshKfTPIEOjsedL9p1N5bZBRYTZ2UeUZMtgFWWY= github.com/getlantern/samizdat v0.0.3-0.20260310125445-325cf1bd1b60 h1:m9eXjDK9vllbVH467+QXbrxUFFM9Yp7YJ90wZLw4dwU= github.com/getlantern/samizdat v0.0.3-0.20260310125445-325cf1bd1b60/go.mod h1:uEeykQSW2/6rTjfPlj3MTTo59poSHXfAHTGgzYDkbr0= github.com/getlantern/sing v0.7.18-lantern h1:QKGgIUA3LwmKYP/7JlQTRkxj9jnP4cX2Q/B+nd8XEjo= diff --git a/ios/Runner/Handlers/MethodHandler.swift b/ios/Runner/Handlers/MethodHandler.swift index e9c6ecbdc2..dbfe80d157 100644 --- a/ios/Runner/Handlers/MethodHandler.swift +++ b/ios/Runner/Handlers/MethodHandler.swift @@ -33,8 +33,7 @@ class MethodHandler { self.startVPN(result: result) case "isTagAvailable": - guard let tag = call.arguments as? String else { - result(true) + guard let tag: String = self.decodeValue(from: call.arguments, result: result) else { return } self.isTagAvailable(result: result, tag: tag) diff --git a/lib/features/home/home.dart b/lib/features/home/home.dart index 1f77c95344..22a9ca4266 100644 --- a/lib/features/home/home.dart +++ b/lib/features/home/home.dart @@ -145,11 +145,9 @@ class _HomeState extends ConsumerState { } Widget _buildBody(WidgetRef ref, bool isUserPro) { - final serverLocationAsync = ref.watch(serverLocationProvider); + final serverLocation = ref.watch(serverLocationProvider); - // Choose a safe default while loading/error - final serverLocation = serverLocationAsync.value; - final serverType = (serverLocation?.serverType ?? '').toServerLocationType; + final serverType = serverLocation.serverType.toServerLocationType; return Padding( padding: EdgeInsets.symmetric(horizontal: defaultSize), diff --git a/lib/features/home/provider/home_notifier.dart b/lib/features/home/provider/home_notifier.dart index a1b968705d..0e121719aa 100644 --- a/lib/features/home/provider/home_notifier.dart +++ b/lib/features/home/provider/home_notifier.dart @@ -97,20 +97,14 @@ class HomeNotifier extends _$HomeNotifier { /// if user logs out or downgrade to free plan /// we need to reset the server location set to smart location void resetServerLocation() { - final serverLocationAsync = ref.read(serverLocationProvider); + final serverLocation = ref.read(serverLocationProvider); - serverLocationAsync.when( - data: (serverLocation) { - if (serverLocation.serverType.toServerLocationType == - ServerLocationType.lanternLocation) { - ref - .read(serverLocationProvider.notifier) - .updateServerLocation(initialServerLocation()); - } - }, - loading: () {}, - error: (_, __) {}, - ); + if (serverLocation.serverType.toServerLocationType == + ServerLocationType.lanternLocation) { + ref + .read(serverLocationProvider.notifier) + .updateServerLocation(initialServerLocation()); + } } /// Fetches the latest user data from the server if not cached locally. diff --git a/lib/features/system_tray/provider/system_tray_notifier.dart b/lib/features/system_tray/provider/system_tray_notifier.dart index ee91cfc0f2..bd30627fd0 100644 --- a/lib/features/system_tray/provider/system_tray_notifier.dart +++ b/lib/features/system_tray/provider/system_tray_notifier.dart @@ -1,8 +1,10 @@ import 'dart:io'; +import 'package:lantern/core/models/app_setting.dart'; import 'package:lantern/core/models/available_servers.dart'; import 'package:lantern/core/models/macos_extension_state.dart'; import 'package:lantern/core/models/server_location.dart'; +import 'package:lantern/features/home/provider/app_setting_notifier.dart'; import 'package:lantern/features/vpn/provider/available_servers_notifier.dart'; import 'package:lantern/features/vpn/provider/vpn_notifier.dart'; import 'package:lantern/features/window/provider/window_notifier.dart'; @@ -21,9 +23,15 @@ class SystemTrayNotifier extends _$SystemTrayNotifier with TrayListener { VPNStatus _currentStatus = VPNStatus.disconnected; bool _isUserPro = false; List _locations = []; + RoutingMode _currentRoutingMode = RoutingMode.full; + ServerLocation? _serverLocation; bool get isConnected => _currentStatus == VPNStatus.connected; + bool get _isAutoLocation => + _serverLocation?.serverType.toServerLocationType == + ServerLocationType.auto; + @override Future build() async { if (!PlatformUtils.isDesktop) return; @@ -42,48 +50,62 @@ class SystemTrayNotifier extends _$SystemTrayNotifier with TrayListener { void _initializeState() { _currentStatus = ref.read(vpnProvider); _isUserPro = ref.read(isUserProProvider); + _currentRoutingMode = ref.read(appSettingProvider).routingMode; + _serverLocation = ref.read(serverLocationProvider); } void _setupListeners() { _listenToVPNStatus(); _listenToProStatus(); _listenToAvailableServers(); + _listenToServerLocation(); + _listenToRoutingMode(); } void _listenToVPNStatus() { - ref.listen( - vpnProvider, - (previous, next) async { - _currentStatus = next; - await updateTrayMenu(); - }, - ); + ref.listen(vpnProvider, (previous, next) async { + _currentStatus = next; + await updateTrayMenu(); + }); } void _listenToProStatus() { - ref.listen( - isUserProProvider, - (previous, next) async { - _isUserPro = next; - await updateTrayMenu(); - }, - ); + ref.listen(isUserProProvider, (previous, next) async { + _isUserPro = next; + await updateTrayMenu(); + }); } void _listenToAvailableServers() { - ref.listen>( - availableServersProvider, - (previous, next) async { - final data = next.value; - _locations = data?.lantern.locations.values.toList() ?? []; - _locations.sort((a, b) { - final cmp = a.country.compareTo(b.country); - if (cmp != 0) return cmp; - return a.city.compareTo(b.city); - }); + ref.listen>(availableServersProvider, ( + previous, + next, + ) async { + final data = next.value; + _locations = data?.lantern.locations.values.toList() ?? []; + _locations.sort((a, b) { + final cmp = a.country.compareTo(b.country); + if (cmp != 0) return cmp; + return a.city.compareTo(b.city); + }); + await updateTrayMenu(); + }); + } + + void _listenToServerLocation() { + ref.listen(serverLocationProvider, (previous, next) async { + _serverLocation = next; + await updateTrayMenu(); + }); + } + + void _listenToRoutingMode() { + ref.listen(appSettingProvider, (previous, next) async { + if (previous?.routingMode != next.routingMode) { + _currentRoutingMode = next.routingMode; await updateTrayMenu(); - }, - ); + } + }); } Future toggleVPN() async { @@ -97,43 +119,96 @@ class SystemTrayNotifier extends _$SystemTrayNotifier with TrayListener { /// Handle location selection from tray menu Future _onLocationSelected(Location_ location) async { - /// Check if extension is installed and up to date before connecting + if (!_checkMacOSExtension()) return; + + final result = await ref + .read(vpnProvider.notifier) + .connectToServer(ServerLocationType.lanternLocation, location.tag); + result.fold( + (failure) => appLogger.error( + 'Failed to connect: ${failure.localizedErrorMessage}', + ), + (success) { + appLogger.info('Connecting to ${location.country} - ${location.city}'); + _saveServerLocation(location); + }, + ); + } + + /// Handle smart location selection from tray menu + Future _onSmartLocationSelected() async { + if (!_checkMacOSExtension()) return; + + await ref + .read(serverLocationProvider.notifier) + .updateServerLocation(initialServerLocation()); + await ref.read(vpnProvider.notifier).startVPN(force: true); + } + + /// Handle routing mode selection from tray menu + Future _onRoutingModeSelected(RoutingMode mode) async { + await ref.read(appSettingProvider.notifier).setRoutingMode(mode); + } + + /// Returns true if OK to proceed, false if blocked by missing extension + bool _checkMacOSExtension() { if (PlatformUtils.isMacOS) { final systemExtensionStatus = ref.read(macosExtensionProvider); if (systemExtensionStatus.status != SystemExtensionStatus.installed && systemExtensionStatus.status != SystemExtensionStatus.activated) { windowManager.show(); appRouter.push(const MacOSExtensionDialog()); - return; + return false; } } - - final result = await ref.read(vpnProvider.notifier).connectToServer( - ServerLocationType.lanternLocation, - location.tag, - ); - result.fold( - (failure) => appLogger - .error('Failed to connect: ${failure.localizedErrorMessage}'), - (success) { - appLogger.info('Connecting to ${location.country} - ${location.city}'); - _saveServerLocation(location); - }, - ); + return true; } Future _saveServerLocation(Location_ location) async { - final serverLocation = ServerLocation.fromLanternLocation( - server: location, - ); + final serverLocation = ServerLocation.fromLanternLocation(server: location); await ref .read(serverLocationProvider.notifier) .updateServerLocation(serverLocation); } + /// Build the current location display string (flag emoji + city) + /// shown when connected + String get _currentLocationDisplay { + try { + if (_serverLocation == null) return ''; + + final loc = _serverLocation!; + String countryCode = ''; + String displayName = ''; + + if (loc.serverType.toServerLocationType == ServerLocationType.auto) { + /// For auto location, we use the autoLocation info which contains the actual connected server details + final auto_ = loc.autoLocation; + if (auto_ == null) return ''; + countryCode = auto_.countryCode; + displayName = auto_.displayName; + } else { + countryCode = loc.countryCode; + displayName = loc.displayName; + } + + if (displayName.isEmpty) return ''; + + final flag = _countryCodeToFlagEmoji(countryCode); + return flag.isNotEmpty ? '$flag $displayName' : displayName; + } catch (e) { + appLogger.error('Error building location display', e); + return ''; + } + } + Future updateTrayMenu() async { + final locationDisplay = _currentLocationDisplay; + final menu = Menu( items: [ + MenuItem.separator(), + // Status: Connected / Disconnected (greyed out, non-clickable) MenuItem( key: 'status_label', disabled: true, @@ -141,34 +216,79 @@ class SystemTrayNotifier extends _$SystemTrayNotifier with TrayListener { ? 'status_on'.i18n : 'status_off'.i18n, ), + + if (isConnected && locationDisplay.isNotEmpty) + MenuItem( + key: 'current_location', + disabled: true, + label: locationDisplay, + ), + MenuItem.separator(), + MenuItem( key: 'toggle', label: _currentStatus == VPNStatus.connected ? 'disconnect'.i18n : 'connect'.i18n, - disabled: _currentStatus == VPNStatus.connecting || + disabled: + _currentStatus == VPNStatus.connecting || _currentStatus == VPNStatus.disconnecting, onClick: (_) => toggleVPN(), ), MenuItem.separator(), + if (_isUserPro && _locations.isNotEmpty) MenuItem.submenu( key: 'select_location', label: 'select_location'.i18n, + disabled: + _currentStatus == VPNStatus.connecting || + _currentStatus == VPNStatus.disconnecting, submenu: Menu( - items: _locations.map((location) { - final displayName = location.city.isNotEmpty - ? '${location.country} - ${location.city}' - : location.country; - return MenuItem( - key: 'location_${location.tag}', - label: displayName, - icon: AppImagePaths.safeFlagPath(location.countryCode), - onClick: (_) => _onLocationSelected(location), - ); - }).toList(), + items: [ + // Smart Location as first option with checkmark + MenuItem.checkbox( + key: 'smart_location', + label: 'smart_location'.i18n, + checked: _isAutoLocation, + onClick: (_) => _onSmartLocationSelected(), + ), + MenuItem.separator(), + // Server list + ..._locations.map((location) { + final displayName = location.city.isNotEmpty + ? '${location.country} - ${location.city}' + : location.country; + return MenuItem( + key: 'location_${location.tag}', + label: displayName, + icon: AppImagePaths.safeFlagPath(location.countryCode), + onClick: (_) => _onLocationSelected(location), + ); + }), + ], ), ), + MenuItem.submenu( + key: 'routing_mode', + label: 'routing_mode'.i18n, + submenu: Menu( + items: [ + MenuItem.checkbox( + key: 'smart_routing', + label: 'smart_routing'.i18n, + checked: _currentRoutingMode == RoutingMode.smart, + onClick: (_) => _onRoutingModeSelected(RoutingMode.smart), + ), + MenuItem.checkbox( + key: 'full_tunnel', + label: 'full_tunnel'.i18n, + checked: _currentRoutingMode == RoutingMode.full, + onClick: (_) => _onRoutingModeSelected(RoutingMode.full), + ), + ], + ), + ), if (!_isUserPro) MenuItem( key: 'upgrade_to_pro', @@ -183,7 +303,6 @@ class SystemTrayNotifier extends _$SystemTrayNotifier with TrayListener { key: 'join_server', label: 'join_server'.i18n, onClick: (_) { - // Open Lantern and navigate to the join server page ref.read(windowProvider.notifier).open(focus: true); appRouter.push(JoinPrivateServer()); }, @@ -209,8 +328,10 @@ class SystemTrayNotifier extends _$SystemTrayNotifier with TrayListener { ); await trayManager.setContextMenu(menu); - trayManager.setIcon(_trayIconPath(isConnected), - isTemplate: Platform.isMacOS); + trayManager.setIcon( + _trayIconPath(isConnected), + isTemplate: Platform.isMacOS, + ); trayManager.setToolTip('app_name'.i18n); } @@ -244,3 +365,14 @@ class SystemTrayNotifier extends _$SystemTrayNotifier with TrayListener { await trayManager.popUpContextMenu(); } } + +/// Converts a 2-letter ISO country code to a flag emoji +/// e.g. "US" β†’ "πŸ‡ΊπŸ‡Έ", "GB" β†’ "πŸ‡¬πŸ‡§" +String _countryCodeToFlagEmoji(String countryCode) { + final code = countryCode.toUpperCase(); + if (code.length != 2) return ''; + // Ensure both characters are ASCII letters A–Z before computing the emoji. + final isAsciiLetters = code.codeUnits.every((c) => c >= 0x41 && c <= 0x5A); + if (!isAsciiLetters) return ''; + return String.fromCharCodes(code.codeUnits.map((c) => c - 0x41 + 0x1F1E6)); +} diff --git a/lib/features/system_tray/provider/system_tray_notifier.g.dart b/lib/features/system_tray/provider/system_tray_notifier.g.dart index dd00082804..26939f7b7d 100644 --- a/lib/features/system_tray/provider/system_tray_notifier.g.dart +++ b/lib/features/system_tray/provider/system_tray_notifier.g.dart @@ -34,7 +34,7 @@ final class SystemTrayNotifierProvider } String _$systemTrayNotifierHash() => - r'df4cb92a49f9fe51b0b54b16e51da661f000f7c6'; + r'63fb171a34e7ef783d7bb6675e511dec7638f041'; abstract class _$SystemTrayNotifier extends $AsyncNotifier { FutureOr build(); diff --git a/lib/features/vpn/location_setting.dart b/lib/features/vpn/location_setting.dart index dbde135ed7..71d09db9db 100644 --- a/lib/features/vpn/location_setting.dart +++ b/lib/features/vpn/location_setting.dart @@ -10,95 +10,66 @@ class LocationSetting extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final serverLocationAsync = ref.watch(serverLocationProvider); + final serverLocation = ref.watch(serverLocationProvider); + final serverType = serverLocation.serverType.toServerLocationType; - return serverLocationAsync.when( - loading: () => SettingTile( - label: 'selected_location'.i18n, - value: 'loading'.i18n, - subtitle: '', - icon: AppImage(path: AppImagePaths.location), - actions: const [], - onTap: null, - ), - error: (err, stack) => SettingTile( - label: 'selected_location'.i18n, - value: 'error'.i18n, - subtitle: '', - icon: AppImage(path: AppImagePaths.location), - actions: [ - IconButton( - onPressed: () => appRouter.push(const ServerSelection()), - icon: AppImage(path: AppImagePaths.arrowForward), - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - visualDensity: VisualDensity.compact, - ), - ], - onTap: () => appRouter.push(const ServerSelection()), - ), - data: (serverLocation) { - final serverType = serverLocation.serverType.toServerLocationType; - - String title = ''; - String value = ''; - String flag = ''; - String protocol = ''; + String title = ''; + String value = ''; + String flag = ''; + String protocol = ''; - switch (serverType) { - case ServerLocationType.auto: - title = 'smart_location'.i18n; - final autoLoc = serverLocation.autoLocation; + switch (serverType) { + case ServerLocationType.auto: + title = 'smart_location'.i18n; + final autoLoc = serverLocation.autoLocation; - value = autoLoc != null && autoLoc.displayName.isNotEmpty - ? autoLoc.displayName - : 'fastest_server'.i18n; + value = autoLoc != null && autoLoc.displayName.isNotEmpty + ? autoLoc.displayName + : 'fastest_server'.i18n; - flag = autoLoc?.countryCode ?? ''; - protocol = autoLoc?.protocol ?? ''; - break; + flag = autoLoc?.countryCode ?? ''; + protocol = autoLoc?.protocol ?? ''; + break; - case ServerLocationType.lanternLocation: - title = 'selected_location'.i18n; - value = serverLocation.displayName; - flag = serverLocation.countryCode; - protocol = serverLocation.protocol; - break; + case ServerLocationType.lanternLocation: + title = 'selected_location'.i18n; + value = serverLocation.displayName; + flag = serverLocation.countryCode; + protocol = serverLocation.protocol; + break; - case ServerLocationType.privateServer: - title = serverLocation.serverName; - value = serverLocation.displayName; - flag = serverLocation.countryCode; - protocol = serverLocation.protocol; - break; - } + case ServerLocationType.privateServer: + title = serverLocation.serverName; + value = serverLocation.displayName; + flag = serverLocation.countryCode; + protocol = serverLocation.protocol; + break; + } - return SettingTile( - label: title, - value: value.i18n, - subtitle: protocol, - icon: flag.isEmpty ? AppImagePaths.location : Flag(countryCode: flag), - actions: [ - if (serverType == ServerLocationType.auto) - AppImage( - path: AppImagePaths.blot, - useThemeColor: false, - ), - const SizedBox(width: 8), - IconButton( - onPressed: () => appRouter.push(const ServerSelection()), - style: ElevatedButton.styleFrom( - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - icon: AppImage(path: AppImagePaths.arrowForward), - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - visualDensity: VisualDensity.compact, - ), - ], - onTap: () => appRouter.push(const ServerSelection()), - ); - }, + return SettingTile( + label: title, + value: value.i18n, + subtitle: protocol, + icon: flag.isEmpty ? AppImagePaths.location : Flag(countryCode: flag), + actions: [ + if (serverType == ServerLocationType.auto) + AppImage( + path: AppImagePaths.blot, + useThemeColor: false, + ), + const SizedBox(width: 8), + IconButton( + onPressed: () => appRouter.push(const ServerSelection()), + style: ElevatedButton.styleFrom( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + icon: AppImage(path: AppImagePaths.arrowForward), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + visualDensity: VisualDensity.compact, + ), + ], + onTap: () => appRouter.push(const ServerSelection()), ); } } diff --git a/lib/features/vpn/provider/server_location_notifier.dart b/lib/features/vpn/provider/server_location_notifier.dart index cd266b4a33..a405a8347b 100644 --- a/lib/features/vpn/provider/server_location_notifier.dart +++ b/lib/features/vpn/provider/server_location_notifier.dart @@ -13,63 +13,66 @@ class ServerLocationNotifier extends _$ServerLocationNotifier { LocalStorageService get _storage => sl(); @override - Future build() async { + ServerLocation build() { return _storage.getServerLocation() ?? _defaultLocation(); } Future updateServerLocation(ServerLocation entity) async { - final current = state.value; + final current = state; if (entity.serverType != ServerLocationType.auto.name) { //Preserve auto location metadata when switching to a non-auto server, // so we can show user smart location - final updated = entity.copyWith(autoLocation: current?.autoLocation); - state = AsyncData(updated); + final updated = entity.copyWith(autoLocation: current.autoLocation); + state = updated; await _storage.saveServerLocation(updated); } else { - state = AsyncData(entity); + state = entity; await _storage.saveServerLocation(entity); } } Future ifNeededGetAutoServerLocation() async { final status = ref.read(vpnProvider); - final current = state.value; + final current = state; if (status == VPNStatus.connected && - current != null && current.serverType.toServerLocationType == ServerLocationType.auto) { - final result = - await ref.read(lanternServiceProvider).getAutoServerLocation(); - result.fold( - (error) => - appLogger.error("Failed to fetch auto server location: $error"), - (autoLocation) { + final result = await ref + .read(lanternServiceProvider) + .getAutoServerLocation(); + await result.fold( + (error) async { + appLogger.error("Failed to fetch auto server location: $error"); + }, + (autoLocation) async { final countryName = autoLocation.location!.country; final cityName = autoLocation.location!.city; - updateServerLocation(ServerLocation( - serverType: ServerLocationType.auto.name, - serverName: '', - displayName: '', - protocol: '', - city: cityName, - autoLocation: AutoLocation( - countryCode: autoLocation.location!.countryCode, - country: countryName, - displayName: '$countryName - $cityName', - tag: autoLocation.tag, + await updateServerLocation( + ServerLocation( + serverType: ServerLocationType.auto.name, + serverName: '', + displayName: '', + protocol: '', + city: cityName, + autoLocation: AutoLocation( + countryCode: autoLocation.location!.countryCode, + country: countryName, + displayName: '$countryName - $cityName', + tag: autoLocation.tag, + ), ), - )); + ); }, ); } } static ServerLocation _defaultLocation() => ServerLocation( - serverType: ServerLocationType.auto.name, - serverName: '', - displayName: '', - protocol: '', - city: '', - ); + serverType: ServerLocationType.auto.name, + serverName: '', + displayName: '', + protocol: '', + city: '', + ); } diff --git a/lib/features/vpn/provider/server_location_notifier.g.dart b/lib/features/vpn/provider/server_location_notifier.g.dart index d8ed9c3647..9c8d5922bf 100644 --- a/lib/features/vpn/provider/server_location_notifier.g.dart +++ b/lib/features/vpn/provider/server_location_notifier.g.dart @@ -13,7 +13,7 @@ part of 'server_location_notifier.dart'; final serverLocationProvider = ServerLocationNotifierProvider._(); final class ServerLocationNotifierProvider - extends $AsyncNotifierProvider { + extends $NotifierProvider { ServerLocationNotifierProvider._() : super( from: null, @@ -31,22 +31,30 @@ final class ServerLocationNotifierProvider @$internal @override ServerLocationNotifier create() => ServerLocationNotifier(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(ServerLocation value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } } String _$serverLocationNotifierHash() => - r'9b7c13306682f80e6ce31eea5a9f9a5e74baedbc'; + r'cf58012d44d48e3d21c9a56c90c4ae80d724aec6'; -abstract class _$ServerLocationNotifier extends $AsyncNotifier { - FutureOr build(); +abstract class _$ServerLocationNotifier extends $Notifier { + ServerLocation build(); @$mustCallSuper @override void runBuild() { - final ref = this.ref as $Ref, ServerLocation>; + final ref = this.ref as $Ref; final element = ref.element as $ClassProviderElement< - AnyNotifier, ServerLocation>, - AsyncValue, + AnyNotifier, + ServerLocation, Object?, Object? >; diff --git a/lib/features/vpn/provider/vpn_notifier.dart b/lib/features/vpn/provider/vpn_notifier.dart index 5ca164bac1..d19c77162f 100644 --- a/lib/features/vpn/provider/vpn_notifier.dart +++ b/lib/features/vpn/provider/vpn_notifier.dart @@ -73,12 +73,7 @@ class VpnNotifier extends _$VpnNotifier { Future> startVPN({bool force = false}) async { final lantern = ref.read(lanternServiceProvider); - final serverLocation = ref.read(serverLocationProvider).value; - - if (serverLocation == null) { - appLogger.debug('No cached server location, starting VPN with auto'); - return lantern.startVPN(); - } + final serverLocation = ref.read(serverLocationProvider); final type = serverLocation.serverType.toServerLocationType; if (type == ServerLocationType.auto || force) { @@ -89,9 +84,6 @@ class VpnNotifier extends _$VpnNotifier { } final tag = serverLocation.serverName; - - /// Check if the tag is still available before trying to connect to it, if not fallback to auto VPN. - /// This is to avoid trying to connect to a server that is no longer available final tagAvailable = await lantern.isTagAvailable(tag); if (!tagAvailable) { appLogger.debug( @@ -99,7 +91,6 @@ class VpnNotifier extends _$VpnNotifier { ); return lantern.startVPN(); } - return connectToServer(type, tag); } diff --git a/lib/features/vpn/provider/vpn_notifier.g.dart b/lib/features/vpn/provider/vpn_notifier.g.dart index 9ea941ffac..bd3f283e98 100644 --- a/lib/features/vpn/provider/vpn_notifier.g.dart +++ b/lib/features/vpn/provider/vpn_notifier.g.dart @@ -41,7 +41,7 @@ final class VpnNotifierProvider } } -String _$vpnNotifierHash() => r'575b3455ebab8d413e060fc644b7ace6b36a2236'; +String _$vpnNotifierHash() => r'9d5685dcb24bd12386049fdbc1f7aea0db2ebe46'; abstract class _$VpnNotifier extends $Notifier { VPNStatus build(); diff --git a/lib/features/vpn/server_selection.dart b/lib/features/vpn/server_selection.dart index 5c79d4b57c..02788f072d 100644 --- a/lib/features/vpn/server_selection.dart +++ b/lib/features/vpn/server_selection.dart @@ -48,7 +48,7 @@ class _ServerSelectionState extends ConsumerState { ], ); - if (selected.isLoading || availableServers.isLoading) { + if (availableServers.isLoading) { return BaseScreen( title: '', appBar: appBar, @@ -56,7 +56,7 @@ class _ServerSelectionState extends ConsumerState { ); } - final err = selected.asError ?? availableServers.asError; + final err = availableServers.asError; if (err != null) { return BaseScreen( title: '', @@ -67,7 +67,7 @@ class _ServerSelectionState extends ConsumerState { ); } - final selectedServer = selected.requireValue; + final selectedServer = selected; final isPrivateServerFound = availableServers.requireValue.user.outbounds.isNotEmpty; @@ -269,10 +269,7 @@ class _ServerLocationListViewState const verticalSpacing = 12.0; - final selectedTag = selected.maybeWhen( - data: (s) => (s.serverName).toString(), - orElse: () => '', - ); + final selectedTag = selected.serverName; return SafeArea( child: Column( @@ -553,11 +550,11 @@ class _PrivateServerLocationListViewState final availableServers = ref.watch(availableServersProvider); final selected = ref.watch(serverLocationProvider); - if (availableServers.isLoading || selected.isLoading) { + if (availableServers.isLoading) { return const Center(child: Spinner()); } - final err = availableServers.asError ?? selected.asError; + final err = availableServers.asError; if (err != null) { return Center( child: Text(err.error.toString(), textAlign: TextAlign.center), @@ -567,7 +564,7 @@ class _PrivateServerLocationListViewState final userLocations = availableServers.requireValue.user.locations.values .toList(); - final selectedTag = selected.requireValue.serverName; + final selectedTag = selected.serverName; if (userLocations.isEmpty) { return Column( diff --git a/lib/lantern/lantern_ffi_service.dart b/lib/lantern/lantern_ffi_service.dart index 151b945267..ae7895a430 100644 --- a/lib/lantern/lantern_ffi_service.dart +++ b/lib/lantern/lantern_ffi_service.dart @@ -192,7 +192,8 @@ class LanternFFIService implements LanternCoreService { final dataDir = await AppStorageUtils.getAppDirectory(); final logDir = await AppStorageUtils.getAppLogDirectory(); appLogger.info( - "Radiance configuration - env: $env, dataDir: ${dataDir.path}, logDir: $logDir, telemetryConsent: $consent"); + "Radiance configuration - env: $env, dataDir: ${dataDir.path}, logDir: $logDir, telemetryConsent: $consent", + ); final dataDirPtr = dataDir.path.toCharPtr; final logDirPtr = logDir.toCharPtr; @@ -218,10 +219,7 @@ class LanternFFIService implements LanternCoreService { checkAPIError(result); if (result != 'ok' && result != 'true') { - throw PlatformException( - code: 'radiance_setup_failed', - message: result, - ); + throw PlatformException(code: 'radiance_setup_failed', message: result); } return right(unit); } catch (e, st) { @@ -340,8 +338,9 @@ class LanternFFIService implements LanternCoreService { }); checkAPIError(enabledJson); - final enabledKeys = - (jsonDecode(enabledJson) as List).cast().toSet(); + final enabledKeys = (jsonDecode(enabledJson) as List) + .cast() + .toSet(); final decoded = jsonDecode(jsonApps) as List; final rawApps = decoded.cast>(); @@ -429,7 +428,8 @@ class LanternFFIService implements LanternCoreService { return left( Failure( error: result['error'] ?? 'Unknown error', - localizedErrorMessage: result['localizedErrorMessage'] ?? + localizedErrorMessage: + result['localizedErrorMessage'] ?? result['error'] ?? 'Unknown error', ), @@ -525,8 +525,9 @@ class LanternFFIService implements LanternCoreService { return left( Failure( error: e.toString(), - localizedErrorMessage: - (e is Exception) ? e.localizedDescription : e.toString(), + localizedErrorMessage: (e is Exception) + ? e.localizedDescription + : e.toString(), ), ); } finally { @@ -623,11 +624,28 @@ class LanternFFIService implements LanternCoreService { Future isTagAvailable(String tag) async { try { final result = await runInBackground(() async { - return _ffiService.isTagAvailable(tag.toCharPtr).toDartString(); + final tagPtr = tag.toCharPtr; + try { + final resultPtr = _ffiService.isTagAvailable(tagPtr); + if (resultPtr == nullptr) { + return 'true'; + } + try { + return resultPtr.toDartString(); + } finally { + _ffiService.freeCString(resultPtr); + } + } finally { + malloc.free(tagPtr); + } }); return result == 'true'; } catch (e, st) { - appLogger.error('Error checking tag availability, assuming available', e, st); + appLogger.error( + 'Error checking tag availability, assuming available', + e, + st, + ); return true; } } @@ -1253,19 +1271,21 @@ class LanternFFIService implements LanternCoreService { } @override - Future> addServerBasedOnURLs( - {required String urls, - required bool skipCertVerification, - required String serverName}) async { + Future> addServerBasedOnURLs({ + required String urls, + required bool skipCertVerification, + required String serverName, + }) async { try { - final result = await runInBackground( - () async { - return _ffiService - .addServerBasedOnURLs(urls.toCharPtr, - skipCertVerification ? 1 : 0, serverName.toCharPtr) - .toDartString(); - }, - ); + final result = await runInBackground(() async { + return _ffiService + .addServerBasedOnURLs( + urls.toCharPtr, + skipCertVerification ? 1 : 0, + serverName.toCharPtr, + ) + .toDartString(); + }); checkAPIError(result); return Right(unit); } catch (e, stackTrace) { @@ -1275,11 +1295,12 @@ class LanternFFIService implements LanternCoreService { } @override - Future> inviteToServerManagerInstance( - {required String ip, - required String port, - required String accessToken, - required String inviteName}) async { + Future> inviteToServerManagerInstance({ + required String ip, + required String port, + required String accessToken, + required String inviteName, + }) async { try { final result = await runInBackground(() async { return _ffiService diff --git a/lib/lantern/lantern_generated_bindings.dart b/lib/lantern/lantern_generated_bindings.dart index 38bb0916d3..597c62d6d8 100644 --- a/lib/lantern/lantern_generated_bindings.dart +++ b/lib/lantern/lantern_generated_bindings.dart @@ -3325,6 +3325,19 @@ class LanternBindings { ffi.Pointer Function(ffi.Pointer, ffi.Pointer, ffi.Pointer)>(); + ffi.Pointer isTagAvailable( + ffi.Pointer _tag, + ) { + return _isTagAvailable(_tag); + } + + late final _isTagAvailablePtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer)>>('isTagAvailable'); + late final _isTagAvailable = _isTagAvailablePtr + .asFunction Function(ffi.Pointer)>(); + ffi.Pointer stopVPN() { return _stopVPN(); } diff --git a/lib/lantern/lantern_platform_service.dart b/lib/lantern/lantern_platform_service.dart index 97567180cf..47c095cc99 100644 --- a/lib/lantern/lantern_platform_service.dart +++ b/lib/lantern/lantern_platform_service.dart @@ -138,6 +138,16 @@ class LanternPlatformService implements LanternCoreService { } @override + Future isTagAvailable(String tag) async { + try { + final result = await _methodChannel.invokeMethod('isTagAvailable', tag); + return result ?? true; + } catch (e) { + appLogger.error('Error checking if tag is available', e); + return true; + } + } + Future> stopVPN() async { try { final _ = await _methodChannel.invokeMethod('stopVPN'); diff --git a/lib/lantern/lantern_service.dart b/lib/lantern/lantern_service.dart index 8843fd09ea..9936ed851e 100644 --- a/lib/lantern/lantern_service.dart +++ b/lib/lantern/lantern_service.dart @@ -46,6 +46,14 @@ class LanternService implements LanternCoreService { return _platformService.startVPN(); } + @override + Future isTagAvailable(String tag) { + if (PlatformUtils.isFFISupported) { + return _ffiService.isTagAvailable(tag); + } + return _platformService.isTagAvailable(tag); + } + @override Future> stopVPN() { if (PlatformUtils.isFFISupported) { diff --git a/lib/lantern/lantern_service_notifier.g.dart b/lib/lantern/lantern_service_notifier.g.dart index 63cc421061..6104a14e14 100644 --- a/lib/lantern/lantern_service_notifier.g.dart +++ b/lib/lantern/lantern_service_notifier.g.dart @@ -48,4 +48,4 @@ final class LanternServiceProvider } } -String _$lanternServiceHash() => r'3adf724e5fa29199106b8e0999b9fc21c0c9f721'; +String _$lanternServiceHash() => r'3e4ab1e15ccf41a3d913e21f1dd6414a29c8a840'; diff --git a/macos/Runner/Handlers/MethodHandler.swift b/macos/Runner/Handlers/MethodHandler.swift index a3b09214d7..3854bcb1d1 100644 --- a/macos/Runner/Handlers/MethodHandler.swift +++ b/macos/Runner/Handlers/MethodHandler.swift @@ -32,8 +32,7 @@ class MethodHandler { self.startVPN(result: result) case "isTagAvailable": - guard let tag = call.arguments as? String else { - result(true) + guard let tag: String = self.decodeValue(from: call.arguments, result: result) else { return } self.isTagAvailable(result: result, tag: tag) diff --git a/pubspec.yaml b/pubspec.yaml index 34c38d7e2f..96c5f02664 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -148,7 +148,11 @@ flutter: assets: - assets/ - assets/images/ - - assets/images/flags/ + - path: assets/images/flags/ + platforms: + - windows + - macos + - linux - assets/locales/ - app.env From a2ec3a4ef2f5e063389df41e92c6fabb6da94841 Mon Sep 17 00:00:00 2001 From: atavism Date: Tue, 24 Mar 2026 11:04:46 -0700 Subject: [PATCH 3/3] code review updates --- lantern-core/ffi/ffi.go | 1 + lib/lantern/lantern_service.dart | 8 -------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/lantern-core/ffi/ffi.go b/lantern-core/ffi/ffi.go index 1609c82687..9bf94982ba 100644 --- a/lantern-core/ffi/ffi.go +++ b/lantern-core/ffi/ffi.go @@ -287,6 +287,7 @@ func isTagAvailable(_tag *C.char) *C.char { c, errStr := requireCore() if errStr != nil { slog.Warn("Unable to check tag availability (core not ready), assuming available", "tag", tag) + C.free(unsafe.Pointer(errStr)) return C.CString("true") } _, found, err := c.GetServerByTagJSON(tag) diff --git a/lib/lantern/lantern_service.dart b/lib/lantern/lantern_service.dart index 9936ed851e..b002a1e1a2 100644 --- a/lib/lantern/lantern_service.dart +++ b/lib/lantern/lantern_service.dart @@ -559,14 +559,6 @@ class LanternService implements LanternCoreService { ); } - @override - Future isTagAvailable(String tag) { - if (PlatformUtils.isFFISupported) { - return _ffiService.isTagAvailable(tag); - } - return _platformService.isTagAvailable(tag); - } - /// connectToServer is used to connect to a server /// this will work with lantern customer and private server /// requires location and tag