Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 6 additions & 10 deletions NetBird.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1408,7 +1408,7 @@
CODE_SIGN_ENTITLEMENTS = NetBird/NetBird.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 7;
CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = TA739QLA7A;
ENABLE_PREVIEWS = YES;
Expand All @@ -1429,10 +1429,8 @@
"$(inherited)",
"@executable_path/Frameworks",
);
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
);
MARKETING_VERSION = 0.0.15;
LIBRARY_SEARCH_PATHS = "$(inherited)";
MARKETING_VERSION = 0.0.16;
PRODUCT_BUNDLE_IDENTIFIER = io.netbird.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand All @@ -1456,7 +1454,7 @@
CODE_SIGN_ENTITLEMENTS = NetBird/NetBird.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 7;
CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = TA739QLA7A;
ENABLE_PREVIEWS = YES;
Expand All @@ -1477,10 +1475,8 @@
"$(inherited)",
"@executable_path/Frameworks",
);
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
);
MARKETING_VERSION = 0.0.15;
LIBRARY_SEARCH_PATHS = "$(inherited)";
MARKETING_VERSION = 0.0.16;
PRODUCT_BUNDLE_IDENTIFIER = io.netbird.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand Down
39 changes: 34 additions & 5 deletions NetBird/Source/App/NetBirdApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ struct NetBirdApp: App {
// Create ViewModel on background thread to avoid blocking app launch with Go runtime init
@StateObject private var viewModelLoader = ViewModelLoader()
@Environment(\.scenePhase) var scenePhase
@State private var activationTask: Task<Void, Never>?

#if os(iOS)
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
Expand All @@ -62,12 +63,26 @@ struct NetBirdApp: App {
#if os(iOS)
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
print("App is active!")
viewModel.checkExtensionState()
viewModel.checkLoginRequiredFlag()
viewModel.startPollingDetails()
activationTask?.cancel()
activationTask = Task { @MainActor in
guard UIApplication.shared.applicationState == .active else { return }
// Load existing VPN manager first to establish session for status polling.
// This must complete before polling starts to avoid returning default disconnected status
// when the VPN is actually connected.
if let initialStatus = await viewModel.networkExtensionAdapter.loadCurrentConnectionState() {
// Set the initial extension state immediately so the UI shows the correct status
viewModel.extensionState = initialStatus
}
guard UIApplication.shared.applicationState == .active else { return }
viewModel.checkExtensionState()
viewModel.checkLoginRequiredFlag()
viewModel.startPollingDetails()
}
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)) { _ in
print("App is inactive!")
activationTask?.cancel()
activationTask = nil
viewModel.stopPollingDetails()
}
#endif
Expand All @@ -77,10 +92,24 @@ struct NetBirdApp: App {
switch newPhase {
case .active:
print("App is active!")
viewModel.checkExtensionState()
viewModel.startPollingDetails()
activationTask?.cancel()
activationTask = Task { @MainActor in
guard scenePhase == .active else { return }
// Load existing VPN manager first to establish session for status polling.
// This must complete before polling starts to avoid returning default disconnected status
// when the VPN is actually connected.
if let initialStatus = await viewModel.networkExtensionAdapter.loadCurrentConnectionState() {
// Set the initial extension state immediately so the UI shows the correct status
viewModel.extensionState = initialStatus
}
guard scenePhase == .active else { return }
viewModel.checkExtensionState()
viewModel.startPollingDetails()
}
case .inactive, .background:
print("App is inactive!")
activationTask?.cancel()
activationTask = nil
viewModel.stopPollingDetails()
@unknown default:
break
Expand Down
24 changes: 24 additions & 0 deletions NetbirdKit/NetworkExtensionAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,30 @@ public class NetworkExtensionAdapter: ObservableObject {
self.session = self.vpnManager?.connection as? NETunnelProviderSession
}

/// Loads an existing VPN manager from preferences and returns the current connection state.
/// This is used on app startup to establish the session for status polling and get the
/// initial connection state, without triggering VPN configuration or starting a connection.
/// Returns the current VPN connection status if a manager was found, nil otherwise.
@MainActor
public func loadCurrentConnectionState() async -> NEVPNStatus? {
do {
let managers = try await NETunnelProviderManager.loadAllFromPreferences()
if let manager = managers.first(where: { $0.localizedDescription == self.extensionName }) {
self.vpnManager = manager
self.session = manager.connection as? NETunnelProviderSession
let status = manager.connection.status
logger.info("loadCurrentConnectionState: Found existing manager, session established, status: \(status.rawValue)")
return status
} else {
logger.info("loadCurrentConnectionState: No existing manager found")
return nil
}
} catch {
logger.error("loadCurrentConnectionState: Error loading managers: \(error.localizedDescription)")
return nil
}
}

private func createNewManager() -> NETunnelProviderManager {
let tunnelProviderProtocol = NETunnelProviderProtocol()
tunnelProviderProtocol.providerBundleIdentifier = self.extensionID
Expand Down