From 520511774ccbee4a9e248dc391895a4ded442868 Mon Sep 17 00:00:00 2001 From: Roopesh Chander Date: Fri, 10 Feb 2023 11:19:50 +0530 Subject: [PATCH 1/7] ConnectionViewModel: Introduce .continueWithProfile(profileId) policy --- EduVPN/ViewModels/ConnectionViewModel.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/EduVPN/ViewModels/ConnectionViewModel.swift b/EduVPN/ViewModels/ConnectionViewModel.swift index 61428e35..2f0ed0fb 100644 --- a/EduVPN/ViewModels/ConnectionViewModel.swift +++ b/EduVPN/ViewModels/ConnectionViewModel.swift @@ -115,6 +115,7 @@ class ConnectionViewModel { // swiftlint:disable:this type_body_length enum FlowContinuationPolicy { // After getting the profile list, deciding whether to continue to connect or not case continueWithSingleOrLastUsedProfile + case continueWithProfile(profileId: String) case continueWithAnyProfile case doNotContinue case notApplicable @@ -366,6 +367,15 @@ class ConnectionViewModel { // swiftlint:disable:this type_body_length return self.continueServerConnectionFlow( profile: profile, from: viewController, serverInfo: serverInfo) + case .continueWithProfile(let chosenProfileId): + guard let profile = profiles.first(where: { $0.profileId == chosenProfileId }) else { + self.internalState = .idle + return Promise.value(()) + } + self.delegate?.connectionViewModel(self, willAutomaticallySelectProfileId: profile.profileId) + return self.continueServerConnectionFlow( + profile: profile, from: viewController, + serverInfo: serverInfo) case .continueWithAnyProfile: let anyProfile = profiles.first(where: { $0.profileId == lastUsedProfileId }) ?? profiles.first guard let profile = anyProfile else { From 121cb92538b1f45700b04c64ecb69fcf66947948 Mon Sep 17 00:00:00 2001 From: Roopesh Chander Date: Fri, 10 Feb 2023 12:11:38 +0530 Subject: [PATCH 2/7] ConnectionVC: Introduce PostLoadAction initialConnectionFlowContinuationPolicy and restoringPreConnectionState are mutually exclusive, so we merge them together into one postLoadAction, which is always required. --- .../ConnectionViewController.swift | 50 +++++++++++++++---- .../Mac/MainViewController+StatusItem.swift | 3 +- EduVPN/Controllers/MainViewController.swift | 40 +++++++-------- EduVPN/Services/Environment.swift | 6 +-- 4 files changed, 59 insertions(+), 40 deletions(-) diff --git a/EduVPN/Controllers/ConnectionViewController.swift b/EduVPN/Controllers/ConnectionViewController.swift index b86f614f..194a5e73 100644 --- a/EduVPN/Controllers/ConnectionViewController.swift +++ b/EduVPN/Controllers/ConnectionViewController.swift @@ -51,11 +51,19 @@ final class ConnectionViewController: ViewController, ParametrizedViewController let connectableInstance: ConnectableInstance let serverDisplayInfo: ServerDisplayInfo let authURLTemplate: String? - let initialConnectionFlowContinuationPolicy: ConnectionViewModel.FlowContinuationPolicy + let postLoadAction: PostLoadAction + } + + enum PostLoadAction { + // What should happen after this VC is initialized or loaded + + // Begin the connection flow after the view loads + case beginConnectionFlow(continuationPolicy: ConnectionViewModel.FlowContinuationPolicy) - // If restoringPreConnectionState is non-nil, then we're restoring - // the UI at app launch for an already-on VPN - let restoringPreConnectionState: ConnectionAttempt.PreConnectionState? + // A VPN is already on. Initialize the ConnectionViewModel with the appropriate state. + // This can happen when we detect that the VPN is on at app launch. + // If shouldRenewSession, initiate renew session after the view loads. + case restoreAlreadyConnectedState(preConnectionState: ConnectionAttempt.PreConnectionState, shouldRenewSession: Bool) } weak var delegate: ConnectionViewControllerDelegate? @@ -75,7 +83,6 @@ final class ConnectionViewController: ViewController, ParametrizedViewController var sessionExpiresAt: Date? { viewModel?.sessionExpiresAt } private var parameters: Parameters! - private var isRestored: Bool = false private var viewModel: ConnectionViewModel! private var dataStore: PersistenceService.DataStore! @@ -160,7 +167,12 @@ final class ConnectionViewController: ViewController, ParametrizedViewController self.parameters = parameters if let server = parameters.connectableInstance as? ServerInstance { - let serverPreConnectionState = parameters.restoringPreConnectionState?.serverState + let serverPreConnectionState: ConnectionAttempt.ServerPreConnectionState? + if case .restoreAlreadyConnectedState(let preConnectionState, _) = parameters.postLoadAction { + serverPreConnectionState = preConnectionState.serverState + } else { + serverPreConnectionState = nil + } self.viewModel = ConnectionViewModel( server: server, connectionService: parameters.environment.connectionService, @@ -171,7 +183,12 @@ final class ConnectionViewController: ViewController, ParametrizedViewController authURLTemplate: parameters.authURLTemplate, restoringPreConnectionState: serverPreConnectionState) } else if let vpnConfigInstance = parameters.connectableInstance as? VPNConfigInstance { - let vpnConfigPreConnectionState = parameters.restoringPreConnectionState?.vpnConfigState + let vpnConfigPreConnectionState: ConnectionAttempt.VPNConfigPreConnectionState? + if case .restoreAlreadyConnectedState(let preConnectionState, _) = parameters.postLoadAction { + vpnConfigPreConnectionState = preConnectionState.vpnConfigState + } else { + vpnConfigPreConnectionState = nil + } self.viewModel = ConnectionViewModel( vpnConfigInstance: vpnConfigInstance, connectionService: parameters.environment.connectionService, @@ -182,11 +199,15 @@ final class ConnectionViewController: ViewController, ParametrizedViewController fatalError("Unknown connectable instance: \(parameters.connectableInstance)") } - self.isRestored = (parameters.restoringPreConnectionState != nil) self.dataStore = PersistenceService.DataStore(path: parameters.connectableInstance.localStoragePath) if let server = parameters.connectableInstance as? ServerInstance { - let serverPreConnectionState = parameters.restoringPreConnectionState?.serverState + let serverPreConnectionState: ConnectionAttempt.ServerPreConnectionState? + if case .restoreAlreadyConnectedState(let preConnectionState, _) = parameters.postLoadAction { + serverPreConnectionState = preConnectionState.serverState + } else { + serverPreConnectionState = nil + } if let serverPreConnectionState = serverPreConnectionState { self.profiles = serverPreConnectionState.profiles self.selectedProfileId = serverPreConnectionState.selectedProfileId @@ -203,9 +224,16 @@ final class ConnectionViewController: ViewController, ParametrizedViewController // to receive updates from the view model viewModel.delegate = self setupInitialView(viewModel: viewModel) - if !isRestored { - beginConnectionFlow(continuationPolicy: parameters.initialConnectionFlowContinuationPolicy) + + switch parameters.postLoadAction { + case .beginConnectionFlow(let continuationPolicy): + beginConnectionFlow(continuationPolicy: continuationPolicy) + case .restoreAlreadyConnectedState(_, let shouldRenewSession): + if shouldRenewSession { + renewSession() + } } + #if os(macOS) vpnSwitch.setAccessibilityIdentifier("Connection") #elseif os(iOS) diff --git a/EduVPN/Controllers/Mac/MainViewController+StatusItem.swift b/EduVPN/Controllers/Mac/MainViewController+StatusItem.swift index b7009432..b353d688 100644 --- a/EduVPN/Controllers/Mac/MainViewController+StatusItem.swift +++ b/EduVPN/Controllers/Mac/MainViewController+StatusItem.swift @@ -36,8 +36,7 @@ extension MainViewController: StatusItemControllerDelegate { }.map { self.pushConnectionVC( connectableInstance: connectableInstance, - preConnectionState: nil, - continuationPolicy: .continueWithAnyProfile) + postLoadAction: .beginConnectionFlow(continuationPolicy: .continueWithAnyProfile)) }.cauterize() } } diff --git a/EduVPN/Controllers/MainViewController.swift b/EduVPN/Controllers/MainViewController.swift index 0613d071..9714731b 100644 --- a/EduVPN/Controllers/MainViewController.swift +++ b/EduVPN/Controllers/MainViewController.swift @@ -132,28 +132,23 @@ class MainViewController: ViewController { } func pushConnectionVC(connectableInstance: ConnectableInstance, - preConnectionState: ConnectionAttempt.PreConnectionState?, - continuationPolicy: ConnectionViewModel.FlowContinuationPolicy, - shouldRenewSessionOnRestoration: Bool = false) { + postLoadAction: ConnectionViewController.PostLoadAction) { if let currentConnectionVC = currentConnectionVC, - currentConnectionVC.connectableInstance.isEqual(to: connectableInstance), - preConnectionState == nil { - currentConnectionVC.beginConnectionFlow(continuationPolicy: continuationPolicy) + currentConnectionVC.connectableInstance.isEqual(to: connectableInstance) { + if case .beginConnectionFlow(let continuationPolicy) = postLoadAction { + currentConnectionVC.beginConnectionFlow(continuationPolicy: continuationPolicy) + } } else { let serverDisplayInfo = viewModel.serverDisplayInfo(for: connectableInstance) let authURLTemplate = viewModel.authURLTemplate(for: connectableInstance) let connectionVC = environment.instantiateConnectionViewController( connectableInstance: connectableInstance, serverDisplayInfo: serverDisplayInfo, - initialConnectionFlowContinuationPolicy: continuationPolicy, authURLTemplate: authURLTemplate, - restoringPreConnectionState: preConnectionState) + postLoadAction: postLoadAction) connectionVC.delegate = self environment.navigationController?.popToRoot() environment.navigationController?.pushViewController(connectionVC, animated: true) - if preConnectionState != nil && shouldRenewSessionOnRestoration { - connectionVC.renewSession() - } } } @@ -280,21 +275,20 @@ extension MainViewController: ConnectionServiceInitializationDelegate { let connectableInstance = lastConnectionAttempt.connectableInstance let preConnectionState = lastConnectionAttempt.preConnectionState - if connectableInstance is ServerInstance { - precondition(lastConnectionAttempt.preConnectionState.serverState != nil) - } else if connectableInstance is VPNConfigInstance { - precondition(lastConnectionAttempt.preConnectionState.vpnConfigState != nil) + + let postLoadAction: ConnectionViewController.PostLoadAction + if (connectableInstance is ServerInstance && preConnectionState.serverState != nil) || + (connectableInstance is VPNConfigInstance && preConnectionState.vpnConfigState != nil) { + postLoadAction = .restoreAlreadyConnectedState( + preConnectionState: preConnectionState, + shouldRenewSession: shouldRenewSessionWhenConnectionServiceInitialized) + pushConnectionVC(connectableInstance: connectableInstance, postLoadAction: postLoadAction) } else { os_log("VPN is enabled at launch, but unable to identify the server from the info in last_connection_attempt.json. Disabling VPN.", log: Log.general, type: .debug) environment.connectionService.disableVPN() .cauterize() } - pushConnectionVC( - connectableInstance: connectableInstance, - preConnectionState: preConnectionState, - continuationPolicy: .doNotContinue, - shouldRenewSessionOnRestoration: shouldRenewSessionWhenConnectionServiceInitialized) case .vpnDisabled: environment.persistenceService.removeLastConnectionAttempt() @@ -394,9 +388,9 @@ extension MainViewController { let row = viewModel.row(at: index) if let connectableInstance = row.connectableInstance { - pushConnectionVC(connectableInstance: connectableInstance, - preConnectionState: nil, - continuationPolicy: .continueWithSingleOrLastUsedProfile) + pushConnectionVC( + connectableInstance: connectableInstance, + postLoadAction: .beginConnectionFlow(continuationPolicy: .continueWithSingleOrLastUsedProfile)) } } diff --git a/EduVPN/Services/Environment.swift b/EduVPN/Services/Environment.swift index 0468cf26..e8199698 100644 --- a/EduVPN/Services/Environment.swift +++ b/EduVPN/Services/Environment.swift @@ -70,14 +70,12 @@ class Environment { func instantiateConnectionViewController( connectableInstance: ConnectableInstance, serverDisplayInfo: ServerDisplayInfo, - initialConnectionFlowContinuationPolicy: ConnectionViewModel.FlowContinuationPolicy, authURLTemplate: String? = nil, - restoringPreConnectionState: ConnectionAttempt.PreConnectionState? = nil) -> ConnectionViewController { + postLoadAction: ConnectionViewController.PostLoadAction) -> ConnectionViewController { let parameters = ConnectionViewController.Parameters( environment: self, connectableInstance: connectableInstance, serverDisplayInfo: serverDisplayInfo, authURLTemplate: authURLTemplate, - initialConnectionFlowContinuationPolicy: initialConnectionFlowContinuationPolicy, - restoringPreConnectionState: restoringPreConnectionState) + postLoadAction: postLoadAction) return instantiate(ConnectionViewController.self, identifier: "Connection", parameters: parameters) } From d77e82611a39e3cb0c0b265a3740a4a65331e925 Mon Sep 17 00:00:00 2001 From: Roopesh Chander Date: Fri, 10 Feb 2023 20:37:54 +0530 Subject: [PATCH 3/7] MainVC: Add reconnectLastUsedConnectionWhenPossible() --- EduVPN/Controllers/MainViewController.swift | 37 +++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/EduVPN/Controllers/MainViewController.swift b/EduVPN/Controllers/MainViewController.swift index 9714731b..d93d5c9a 100644 --- a/EduVPN/Controllers/MainViewController.swift +++ b/EduVPN/Controllers/MainViewController.swift @@ -69,6 +69,8 @@ class MainViewController: ViewController { private var isConnectionServiceInitialized = false // swiftlint:disable:next identifier_name private var shouldRenewSessionWhenConnectionServiceInitialized = false + // swiftlint:disable:next identifier_name + private var shouldReconnectWhenConnectionServiceInitialized = false #if os(macOS) var shouldPerformActionOnSelection = true @@ -152,6 +154,36 @@ class MainViewController: ViewController { } } + func reconnectLastUsedConnectionWhenPossible() { + if isConnectionServiceInitialized { + reconnectLastUsedConnection() + } else { + shouldReconnectWhenConnectionServiceInitialized = true + } + } + + @discardableResult + private func reconnectLastUsedConnection() -> Bool { + if let lastConnectionAttempt = environment.persistenceService.loadLastConnectionAttempt() { + let connectableInstance = lastConnectionAttempt.connectableInstance + let preConnectionState = lastConnectionAttempt.preConnectionState + if (connectableInstance is ServerInstance && preConnectionState.serverState != nil) || + (connectableInstance is VPNConfigInstance && preConnectionState.vpnConfigState != nil) { + if let lastConnectedProfileId = lastConnectionAttempt.preConnectionState.serverState?.selectedProfileId { + pushConnectionVC( + connectableInstance: connectableInstance, + postLoadAction: .beginConnectionFlow(continuationPolicy: .continueWithProfile(profileId: lastConnectedProfileId))) + } else { + pushConnectionVC( + connectableInstance: connectableInstance, + postLoadAction: .beginConnectionFlow(continuationPolicy: .doNotContinue)) + } + return true + } + } + return false + } + func scheduleSessionExpiryNotificationOnActiveVPN() -> Guarantee { guard let currentConnectionVC = currentConnectionVC else { return Guarantee.value(false) @@ -291,6 +323,11 @@ extension MainViewController: ConnectionServiceInitializationDelegate { } case .vpnDisabled: + if shouldReconnectWhenConnectionServiceInitialized { + if reconnectLastUsedConnection() { + return + } + } environment.persistenceService.removeLastConnectionAttempt() environment.notificationService.descheduleSessionExpiryNotifications() } From 5bafd7a4d51b8f97deaad3ec0a594d7d2792af1d Mon Sep 17 00:00:00 2001 From: Roopesh Chander Date: Mon, 13 Feb 2023 10:59:41 +0530 Subject: [PATCH 4/7] Remove unused case --- EduVPN/ViewModels/ConnectionViewModel.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/EduVPN/ViewModels/ConnectionViewModel.swift b/EduVPN/ViewModels/ConnectionViewModel.swift index 2f0ed0fb..72d28536 100644 --- a/EduVPN/ViewModels/ConnectionViewModel.swift +++ b/EduVPN/ViewModels/ConnectionViewModel.swift @@ -118,7 +118,6 @@ class ConnectionViewModel { // swiftlint:disable:this type_body_length case continueWithProfile(profileId: String) case continueWithAnyProfile case doNotContinue - case notApplicable } private(set) var header: Header { @@ -386,7 +385,7 @@ class ConnectionViewModel { // swiftlint:disable:this type_body_length return self.continueServerConnectionFlow( profile: profile, from: viewController, serverInfo: serverInfo) - case .doNotContinue, .notApplicable: + case .doNotContinue: self.internalState = .idle return Promise.value(()) } From 49b022a49f2bc72c1555b411ef640438e7acad40 Mon Sep 17 00:00:00 2001 From: Roopesh Chander Date: Mon, 13 Feb 2023 11:49:05 +0530 Subject: [PATCH 5/7] UserDefaults: Add shouldReconnectOnLaunchAtLogin --- EduVPN/Helpers/UserDefaults+Preferences.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/EduVPN/Helpers/UserDefaults+Preferences.swift b/EduVPN/Helpers/UserDefaults+Preferences.swift index 53f93bef..d2090152 100644 --- a/EduVPN/Helpers/UserDefaults+Preferences.swift +++ b/EduVPN/Helpers/UserDefaults+Preferences.swift @@ -17,6 +17,7 @@ extension UserDefaults { private static let isStatusItemInColorKey = "isStatusItemInColor" private static let showInDockKey = "showInDock" private static let launchAtLoginKey = "launchAtLogin" + private static let shouldReconnectOnLaunchAtLoginKey = "shouldReconnectOnLaunchAtLogin" #endif func clearPreferences() { @@ -111,6 +112,15 @@ extension UserDefaults { } } + var shouldReconnectOnLaunchAtLogin: Bool { + get { + return bool(forKey: Self.shouldReconnectOnLaunchAtLoginKey) + } + set { + set(newValue, forKey: Self.shouldReconnectOnLaunchAtLoginKey) + } + } + func registerAppDefaults() { register(defaults: [ Self.showInStatusBarKey: true, From 215831b7d0e834680becaec5c191c475bf0bd8c3 Mon Sep 17 00:00:00 2001 From: Roopesh Chander Date: Mon, 13 Feb 2023 12:09:14 +0530 Subject: [PATCH 6/7] AppDelegate: Use reconnectLastUsedConnectionWhenPossible() --- EduVPN/AppDelegate.swift | 11 +++++++++-- EduVPN/Controllers/MainViewController.swift | 9 ++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/EduVPN/AppDelegate.swift b/EduVPN/AppDelegate.swift index e75f129e..75a7c6f6 100644 --- a/EduVPN/AppDelegate.swift +++ b/EduVPN/AppDelegate.swift @@ -76,8 +76,14 @@ class AppDelegate: NSObject, NSApplicationDelegate { shouldUseColorIcons: UserDefaults.standard.isStatusItemInColor) setShowInDockEnabled(UserDefaults.standard.showInDock) - if LaunchAtLoginHelper.isOpenedOrReopenedByLoginItemHelper() && - UserDefaults.standard.showInStatusBar { + let isLauncedAtLogin = LaunchAtLoginHelper.isOpenedOrReopenedByLoginItemHelper() + + if isLauncedAtLogin && UserDefaults.standard.shouldReconnectOnLaunchAtLogin { + mainViewController?.reconnectLastUsedConnectionWhenPossible() + } + UserDefaults.standard.shouldReconnectOnLaunchAtLogin = false + + if isLauncedAtLogin && UserDefaults.standard.showInStatusBar { // If we're showing a status item and the app was launched because // the user logged in, don't show the window window.close() @@ -226,6 +232,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { } if isQuittingForLogoutShutdownOrRestart() { + UserDefaults.standard.shouldReconnectOnLaunchAtLogin = true return silentlyStopVPNAndQuit(connectionService: connectionService) } else { return showAlertConfirmingStopVPNAndQuit(connectionService: connectionService) diff --git a/EduVPN/Controllers/MainViewController.swift b/EduVPN/Controllers/MainViewController.swift index d93d5c9a..1523b105 100644 --- a/EduVPN/Controllers/MainViewController.swift +++ b/EduVPN/Controllers/MainViewController.swift @@ -156,7 +156,11 @@ class MainViewController: ViewController { func reconnectLastUsedConnectionWhenPossible() { if isConnectionServiceInitialized { - reconnectLastUsedConnection() + if reconnectLastUsedConnection() { + #if os(macOS) + (NSApp.delegate as? AppDelegate)?.showMainWindow(self) + #endif + } } else { shouldReconnectWhenConnectionServiceInitialized = true } @@ -325,6 +329,9 @@ extension MainViewController: ConnectionServiceInitializationDelegate { case .vpnDisabled: if shouldReconnectWhenConnectionServiceInitialized { if reconnectLastUsedConnection() { + #if os(macOS) + (NSApp.delegate as? AppDelegate)?.showMainWindow(self) + #endif return } } From 70d7e7ff13de22e987c4a2c77e9e0e48eee9b4dc Mon Sep 17 00:00:00 2001 From: Roopesh Chander Date: Thu, 16 Feb 2023 10:20:18 +0530 Subject: [PATCH 7/7] Update changelog --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index dc820676..8154a7dd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,6 +9,7 @@ - Avoid using cached token endpoints from OIDAuthState #487 - macOS: Point the user to the OS' please-enable-notification prompt #474 - Improve error alerts #489 +- macOS: Autoconnect on launch at login if machine was shutdown with VPN on #478 ## 3.0.6