Fix UI issues and Update Version Number

This commit is contained in:
Stossy11
2025-10-03 10:54:04 +10:00
parent 47f553ed27
commit a8e60f27b2
3 changed files with 238 additions and 128 deletions

View File

@@ -1,7 +1,7 @@
// Configuration settings file format documentation can be found at: // Configuration settings file format documentation can be found at:
// https://help.apple.com/xcode/#/dev745c5c974 // https://help.apple.com/xcode/#/dev745c5c974
MARKETING_VERSION = 1.2 MARKETING_VERSION = 1.2.1
CURRENT_PROJECT_VERSION = 1 CURRENT_PROJECT_VERSION = 1
// Vars to be overwritten by `CodeSigning.xcconfig` if exists // Vars to be overwritten by `CodeSigning.xcconfig` if exists

View File

@@ -44,6 +44,7 @@ class TunnelManager: ObservableObject {
@Published var waitingOnSettings: Bool = false @Published var waitingOnSettings: Bool = false
@Published var vpnManager: NETunnelProviderManager? @Published var vpnManager: NETunnelProviderManager?
private var vpnObserver: NSObjectProtocol? private var vpnObserver: NSObjectProtocol?
private var isProcessingStatusChange = false
private var tunnelDeviceIp: String { private var tunnelDeviceIp: String {
UserDefaults.standard.string(forKey: "TunnelDeviceIP") ?? "10.7.0.0" UserDefaults.standard.string(forKey: "TunnelDeviceIP") ?? "10.7.0.0"
@@ -105,8 +106,8 @@ class TunnelManager: ObservableObject {
} }
private init() { private init() {
loadTunnelPreferences()
setupStatusObserver() setupStatusObserver()
loadTunnelPreferences()
} }
// MARK: - Private Methods // MARK: - Private Methods
@@ -118,32 +119,29 @@ class TunnelManager: ObservableObject {
if let error = error { if let error = error {
VPNLogger.shared.log("Error loading preferences: \(error.localizedDescription)") VPNLogger.shared.log("Error loading preferences: \(error.localizedDescription)")
self.tunnelStatus = .error self.tunnelStatus = .error
self.waitingOnSettings = true
return return
} }
defer {
self.waitingOnSettings = true
}
self.hasLocalDeviceSupport = true self.hasLocalDeviceSupport = true
self.waitingOnSettings = true
if let managers = managers, !managers.isEmpty { if let managers = managers, !managers.isEmpty {
var stosManagers = [NETunnelProviderManager]() let stosManagers = managers.filter { manager in
guard let proto = manager.protocolConfiguration as? NETunnelProviderProtocol else {
for manager in managers { return false
if let proto = manager.protocolConfiguration as? NETunnelProviderProtocol,
proto.providerBundleIdentifier == self.tunnelBundleId {
stosManagers.append(manager)
} }
return proto.providerBundleIdentifier == self.tunnelBundleId
} }
if !stosManagers.isEmpty { if !stosManagers.isEmpty {
if stosManagers.count > 1 { if stosManagers.count > 1 {
self.cleanupDuplicateManagers(stosManagers) self.cleanupDuplicateManagers(stosManagers)
} else { } else if let manager = stosManagers.first {
self.vpnManager = stosManagers.first self.vpnManager = manager
self.updateTunnelStatus(from: stosManagers.first!.connection.status) let currentStatus = manager.connection.status
VPNLogger.shared.log("Loaded existing StosVPN tunnel configuration") VPNLogger.shared.log("Loaded existing StosVPN tunnel configuration with status: \(currentStatus.rawValue)")
self.updateTunnelStatus(from: currentStatus)
} }
} else { } else {
VPNLogger.shared.log("No StosVPN tunnel configuration found") VPNLogger.shared.log("No StosVPN tunnel configuration found")
@@ -163,19 +161,18 @@ class TunnelManager: ObservableObject {
} }
let managerToKeep = activeManager ?? managers.first! let managerToKeep = activeManager ?? managers.first!
self.vpnManager = managerToKeep
self.updateTunnelStatus(from: managerToKeep.connection.status)
var removedCount = 0 DispatchQueue.main.async { [weak self] in
for manager in managers { self?.vpnManager = managerToKeep
if manager != managerToKeep { self?.updateTunnelStatus(from: managerToKeep.connection.status)
manager.removeFromPreferences { error in }
if let error = error {
VPNLogger.shared.log("Error removing duplicate VPN: \(error.localizedDescription)") for manager in managers where manager != managerToKeep {
} else { manager.removeFromPreferences { error in
removedCount += 1 if let error = error {
VPNLogger.shared.log("Successfully removed duplicate VPN configuration") VPNLogger.shared.log("Error removing duplicate VPN: \(error.localizedDescription)")
} } else {
VPNLogger.shared.log("Successfully removed duplicate VPN configuration")
} }
} }
} }
@@ -187,9 +184,14 @@ class TunnelManager: ObservableObject {
object: nil, object: nil,
queue: .main queue: .main
) { [weak self] notification in ) { [weak self] notification in
guard let self = self, guard let self = self else { return }
let connection = notification.object as? NEVPNConnection else { guard let connection = notification.object as? NEVPNConnection else { return }
return
VPNLogger.shared.log("VPN Status notification received: \(connection.status.rawValue)")
// Update status immediately if it's our manager
if let manager = self.vpnManager, connection == manager.connection {
self.updateTunnelStatus(from: connection.status)
} }
self.handleVPNStatusChange(notification: notification) self.handleVPNStatusChange(notification: notification)
@@ -197,29 +199,37 @@ class TunnelManager: ObservableObject {
} }
private func updateTunnelStatus(from connectionStatus: NEVPNStatus) { private func updateTunnelStatus(from connectionStatus: NEVPNStatus) {
DispatchQueue.main.async { let newStatus: TunnelStatus
switch connectionStatus { switch connectionStatus {
case .invalid, .disconnected: case .invalid, .disconnected:
self.tunnelStatus = .disconnected newStatus = .disconnected
case .connecting: case .connecting:
self.tunnelStatus = .connecting newStatus = .connecting
case .connected: case .connected:
self.tunnelStatus = .connected newStatus = .connected
case .disconnecting: case .disconnecting:
self.tunnelStatus = .disconnecting newStatus = .disconnecting
case .reasserting: case .reasserting:
self.tunnelStatus = .connecting newStatus = .connecting
@unknown default: @unknown default:
self.tunnelStatus = .error newStatus = .error
}
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
if self.tunnelStatus != newStatus {
VPNLogger.shared.log("StosVPN status updated from \(self.tunnelStatus) to \(newStatus)")
} }
self.tunnelStatus = newStatus
VPNLogger.shared.log("StosVPN status updated: \(self.tunnelStatus)")
} }
} }
private func createStosVPNConfiguration(completion: @escaping (NETunnelProviderManager?) -> Void) { private func createStosVPNConfiguration(completion: @escaping (NETunnelProviderManager?) -> Void) {
NETunnelProviderManager.loadAllFromPreferences { [weak self] (managers, error) in NETunnelProviderManager.loadAllFromPreferences { [weak self] (managers, error) in
guard let self = self else { return } guard let self = self else {
completion(nil)
return
}
if let error = error { if let error = error {
VPNLogger.shared.log("Error checking existing VPN configurations: \(error.localizedDescription)") VPNLogger.shared.log("Error checking existing VPN configurations: \(error.localizedDescription)")
@@ -229,16 +239,15 @@ class TunnelManager: ObservableObject {
if let managers = managers { if let managers = managers {
let stosManagers = managers.filter { manager in let stosManagers = managers.filter { manager in
if let proto = manager.protocolConfiguration as? NETunnelProviderProtocol { guard let proto = manager.protocolConfiguration as? NETunnelProviderProtocol else {
return proto.providerBundleIdentifier == self.tunnelBundleId return false
} }
return false return proto.providerBundleIdentifier == self.tunnelBundleId
} }
if !stosManagers.isEmpty { if let existingManager = stosManagers.first {
let manager = stosManagers.first!
VPNLogger.shared.log("Found existing StosVPN configuration, using it instead of creating new one") VPNLogger.shared.log("Found existing StosVPN configuration, using it instead of creating new one")
completion(manager) completion(existingManager)
return return
} }
} }
@@ -291,15 +300,13 @@ class TunnelManager: ObservableObject {
} }
let activeManager = managers.first { manager in let activeManager = managers.first { manager in
return manager.connection.status == .connected || manager.connection.status == .connected || manager.connection.status == .connecting
manager.connection.status == .connecting
} }
completion(activeManager) completion(activeManager)
} }
} }
// MARK: - Public Methods // MARK: - Public Methods
func toggleVPNConnection() { func toggleVPNConnection() {
@@ -311,6 +318,27 @@ class TunnelManager: ObservableObject {
} }
func startVPN() { func startVPN() {
if let manager = vpnManager {
let currentStatus = manager.connection.status
VPNLogger.shared.log("Current manager status: \(currentStatus.rawValue)")
if currentStatus == .connected {
VPNLogger.shared.log("VPN already connected, updating UI")
DispatchQueue.main.async { [weak self] in
self?.tunnelStatus = .connected
}
return
}
if currentStatus == .connecting {
VPNLogger.shared.log("VPN already connecting, updating UI")
DispatchQueue.main.async { [weak self] in
self?.tunnelStatus = .connecting
}
return
}
}
getActiveVPNManager { [weak self] activeManager in getActiveVPNManager { [weak self] activeManager in
guard let self = self else { return } guard let self = self else { return }
@@ -329,71 +357,123 @@ class TunnelManager: ObservableObject {
private func initializeAndStartStosVPN() { private func initializeAndStartStosVPN() {
if let manager = vpnManager { if let manager = vpnManager {
startExistingVPN(manager: manager) manager.loadFromPreferences { [weak self] error in
} else {
NETunnelProviderManager.loadAllFromPreferences { [weak self] managers, error in
guard let self = self else { return } guard let self = self else { return }
if let error = error { if let error = error {
VPNLogger.shared.log("Error reloading VPN configurations: \(error.localizedDescription)") VPNLogger.shared.log("Error reloading manager: \(error.localizedDescription)")
self.createStosVPNConfiguration { manager in self.createAndStartVPN()
guard let manager = manager else { return } return
self.vpnManager = manager }
self.startExistingVPN(manager: manager)
}
} else {
createAndStartVPN()
}
}
private func createAndStartVPN() {
NETunnelProviderManager.loadAllFromPreferences { [weak self] managers, error in
guard let self = self else { return }
if let error = error {
VPNLogger.shared.log("Error reloading VPN configurations: \(error.localizedDescription)")
}
if let managers = managers {
let stosManagers = managers.filter { manager in
guard let proto = manager.protocolConfiguration as? NETunnelProviderProtocol else {
return false
}
return proto.providerBundleIdentifier == self.tunnelBundleId
}
if !stosManagers.isEmpty {
DispatchQueue.main.async { [weak self] in
self?.vpnManager = stosManagers.first
}
if stosManagers.count > 1 {
self.cleanupDuplicateManagers(stosManagers)
}
if let manager = stosManagers.first {
self.startExistingVPN(manager: manager) self.startExistingVPN(manager: manager)
} }
return return
} }
}
if let managers = managers {
let stosManagers = managers.filter { manager in self.createStosVPNConfiguration { [weak self] manager in
if let proto = manager.protocolConfiguration as? NETunnelProviderProtocol { guard let self = self, let manager = manager else { return }
return proto.providerBundleIdentifier == self.tunnelBundleId DispatchQueue.main.async { [weak self] in
} self?.vpnManager = manager
return false
}
if !stosManagers.isEmpty {
self.vpnManager = stosManagers.first
if stosManagers.count > 1 {
self.cleanupDuplicateManagers(stosManagers)
}
self.startExistingVPN(manager: stosManagers.first!)
return
}
}
self.createStosVPNConfiguration { manager in
guard let manager = manager else { return }
self.vpnManager = manager
self.startExistingVPN(manager: manager)
} }
self.startExistingVPN(manager: manager)
} }
} }
} }
private func startExistingVPN(manager: NETunnelProviderManager) { private func startExistingVPN(manager: NETunnelProviderManager) {
guard tunnelStatus != .connected else { // First check the actual current status
let currentStatus = manager.connection.status
VPNLogger.shared.log("Current VPN status before start attempt: \(currentStatus.rawValue)")
if currentStatus == .connected {
VPNLogger.shared.log("StosVPN tunnel is already connected") VPNLogger.shared.log("StosVPN tunnel is already connected")
DispatchQueue.main.async { [weak self] in
self?.tunnelStatus = .connected
}
return
}
if currentStatus == .connecting {
VPNLogger.shared.log("StosVPN tunnel is already connecting")
DispatchQueue.main.async { [weak self] in
self?.tunnelStatus = .connecting
}
return return
} }
manager.isEnabled = true manager.isEnabled = true
manager.saveToPreferences { error in manager.saveToPreferences { [weak self] error in
guard let self = self else { return }
if let error = error { if let error = error {
VPNLogger.shared.log(error.localizedDescription) VPNLogger.shared.log("Error saving preferences: \(error.localizedDescription)")
DispatchQueue.main.async { [weak self] in
self?.tunnelStatus = .error
}
return return
} }
// Reload it to apply manager.loadFromPreferences { [weak self] error in
manager.loadFromPreferences { error in guard let self = self else { return }
if let error = error { if let error = error {
VPNLogger.shared.log(error.localizedDescription) VPNLogger.shared.log("Error reloading preferences: \(error.localizedDescription)")
DispatchQueue.main.async { [weak self] in
self?.tunnelStatus = .error
}
return return
} }
self.tunnelStatus = .connecting // Check status again after reload
let statusAfterReload = manager.connection.status
VPNLogger.shared.log("VPN status after reload: \(statusAfterReload.rawValue)")
if statusAfterReload == .connected {
VPNLogger.shared.log("VPN is already connected after reload")
DispatchQueue.main.async { [weak self] in
self?.tunnelStatus = .connected
}
return
}
DispatchQueue.main.async { [weak self] in
self?.tunnelStatus = .connecting
}
let options: [String: NSObject] = [ let options: [String: NSObject] = [
"TunnelDeviceIP": self.tunnelDeviceIp as NSObject, "TunnelDeviceIP": self.tunnelDeviceIp as NSObject,
@@ -405,7 +485,9 @@ class TunnelManager: ObservableObject {
try manager.connection.startVPNTunnel(options: options) try manager.connection.startVPNTunnel(options: options)
VPNLogger.shared.log("StosVPN tunnel start initiated") VPNLogger.shared.log("StosVPN tunnel start initiated")
} catch { } catch {
self.tunnelStatus = .error DispatchQueue.main.async { [weak self] in
self?.tunnelStatus = .error
}
VPNLogger.shared.log("Failed to start StosVPN tunnel: \(error.localizedDescription)") VPNLogger.shared.log("Failed to start StosVPN tunnel: \(error.localizedDescription)")
} }
} }
@@ -413,9 +495,15 @@ class TunnelManager: ObservableObject {
} }
func stopVPN() { func stopVPN() {
guard let manager = vpnManager else { return } guard let manager = vpnManager else {
VPNLogger.shared.log("No VPN manager available to stop")
return
}
DispatchQueue.main.async { [weak self] in
self?.tunnelStatus = .disconnecting
}
tunnelStatus = .disconnecting
manager.connection.stopVPNTunnel() manager.connection.stopVPNTunnel()
VPNLogger.shared.log("StosVPN tunnel stop initiated") VPNLogger.shared.log("StosVPN tunnel stop initiated")
@@ -425,9 +513,12 @@ class TunnelManager: ObservableObject {
func handleVPNStatusChange(notification: Notification) { func handleVPNStatusChange(notification: Notification) {
guard let connection = notification.object as? NEVPNConnection else { return } guard let connection = notification.object as? NEVPNConnection else { return }
VPNLogger.shared.log("Handling VPN status change: \(connection.status.rawValue)")
// Always update status if it's our manager's connection
if let manager = vpnManager, connection == manager.connection { if let manager = vpnManager, connection == manager.connection {
VPNLogger.shared.log("Status change is for our StosVPN manager")
updateTunnelStatus(from: connection.status) updateTunnelStatus(from: connection.status)
return
} }
if connection.status == .disconnected && if connection.status == .disconnected &&
@@ -437,23 +528,40 @@ class TunnelManager: ObservableObject {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
self?.initializeAndStartStosVPN() self?.initializeAndStartStosVPN()
} }
return
} }
// Check if this is a different StosVPN manager (perhaps a duplicate) // Prevent recursive calls when checking for duplicates
// This helps discover duplicates created by other means guard !isProcessingStatusChange else { return }
NETunnelProviderManager.loadAllFromPreferences { [weak self] managers, error in isProcessingStatusChange = true
// Check for duplicates asynchronously without blocking
DispatchQueue.global(qos: .utility).async { [weak self] in
guard let self = self else { return } guard let self = self else { return }
if let managers = managers, !managers.isEmpty { NETunnelProviderManager.loadAllFromPreferences { [weak self] managers, error in
let stosManagers = managers.filter { manager in guard let self = self, let managers = managers, !managers.isEmpty else {
if let proto = manager.protocolConfiguration as? NETunnelProviderProtocol { DispatchQueue.main.async { [weak self] in
return proto.providerBundleIdentifier == self.tunnelBundleId self?.isProcessingStatusChange = false
} }
return false return
}
let stosManagers = managers.filter { manager in
guard let proto = manager.protocolConfiguration as? NETunnelProviderProtocol else {
return false
}
return proto.providerBundleIdentifier == self.tunnelBundleId
} }
if stosManagers.count > 1 { if stosManagers.count > 1 {
self.cleanupDuplicateManagers(stosManagers) DispatchQueue.main.async { [weak self] in
self?.cleanupDuplicateManagers(stosManagers)
}
}
DispatchQueue.main.async { [weak self] in
self?.isProcessingStatusChange = false
} }
} }
} }
@@ -462,7 +570,9 @@ class TunnelManager: ObservableObject {
// MARK: - Cleanup Utilities // MARK: - Cleanup Utilities
func cleanupAllVPNConfigurations() { func cleanupAllVPNConfigurations() {
NETunnelProviderManager.loadAllFromPreferences { managers, error in NETunnelProviderManager.loadAllFromPreferences { [weak self] managers, error in
guard let self = self else { return }
if let error = error { if let error = error {
VPNLogger.shared.log("Error loading VPN configurations for cleanup: \(error.localizedDescription)") VPNLogger.shared.log("Error loading VPN configurations for cleanup: \(error.localizedDescription)")
return return
@@ -471,28 +581,28 @@ class TunnelManager: ObservableObject {
guard let managers = managers else { return } guard let managers = managers else { return }
for manager in managers { for manager in managers {
if let proto = manager.protocolConfiguration as? NETunnelProviderProtocol, guard let proto = manager.protocolConfiguration as? NETunnelProviderProtocol,
proto.providerBundleIdentifier == self.tunnelBundleId { proto.providerBundleIdentifier == self.tunnelBundleId else {
continue
// If connected, disconnect first }
if manager.connection.status == .connected ||
manager.connection.status == .connecting { if manager.connection.status == .connected || manager.connection.status == .connecting {
manager.connection.stopVPNTunnel() manager.connection.stopVPNTunnel()
} }
manager.removeFromPreferences { error in manager.removeFromPreferences { error in
if let error = error { if let error = error {
VPNLogger.shared.log("Error removing VPN configuration: \(error.localizedDescription)") VPNLogger.shared.log("Error removing VPN configuration: \(error.localizedDescription)")
} else { } else {
VPNLogger.shared.log("Successfully removed VPN configuration") VPNLogger.shared.log("Successfully removed VPN configuration")
}
} }
} }
} }
DispatchQueue.main.async { [weak self] in
self.vpnManager = nil self?.vpnManager = nil
self.tunnelStatus = .disconnected self?.tunnelStatus = .disconnected
}
} }
} }