diff --git a/CHANGES.md b/CHANGES.md index 69efb8c8..77df5359 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,7 @@ - Handle wrong scheme when pasting a custom server URL #407 - Make "Connect using TCP only" work with APIv3 - Suppress 'Renew Session' for 30 mins after authentication time #417 +- macOS: Ability to reset the app #259 ## 2.2.3 diff --git a/EduVPN.xcodeproj/project.pbxproj b/EduVPN.xcodeproj/project.pbxproj index a62d7243..5da4c4a5 100644 --- a/EduVPN.xcodeproj/project.pbxproj +++ b/EduVPN.xcodeproj/project.pbxproj @@ -67,6 +67,7 @@ 6F54C93825E0359200A42C8F /* AddServerViewController+iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F54C93725E0359200A42C8F /* AddServerViewController+iOS.swift */; }; 6F54C9E725E2D6A500A42C8F /* MainViewController+StatusItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F54C9E625E2D6A500A42C8F /* MainViewController+StatusItem.swift */; }; 6F57338724CD1570008912D4 /* CertificateExpiryHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F57338624CD1570008912D4 /* CertificateExpiryHelper.swift */; }; + 6F5820F826EE036800906397 /* AppDataRemover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5820F726EE036800906397 /* AppDataRemover.swift */; }; 6F59A58824F51DEB00560155 /* server_list.json in Resources */ = {isa = PBXBuildFile; fileRef = 6F59A58624F51DEB00560155 /* server_list.json */; }; 6F59A58924F51DEB00560155 /* organization_list.json in Resources */ = {isa = PBXBuildFile; fileRef = 6F59A58724F51DEB00560155 /* organization_list.json */; }; 6F59A58B24F67CE500560155 /* OAuthExternalUserAgent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F59A58A24F67CE500560155 /* OAuthExternalUserAgent.swift */; }; @@ -405,6 +406,7 @@ 6F54C93725E0359200A42C8F /* AddServerViewController+iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AddServerViewController+iOS.swift"; sourceTree = ""; }; 6F54C9E625E2D6A500A42C8F /* MainViewController+StatusItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MainViewController+StatusItem.swift"; sourceTree = ""; }; 6F57338624CD1570008912D4 /* CertificateExpiryHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateExpiryHelper.swift; sourceTree = ""; }; + 6F5820F726EE036800906397 /* AppDataRemover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDataRemover.swift; sourceTree = ""; }; 6F59A58624F51DEB00560155 /* server_list.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = server_list.json; sourceTree = ""; }; 6F59A58724F51DEB00560155 /* organization_list.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = organization_list.json; sourceTree = ""; }; 6F59A58A24F67CE500560155 /* OAuthExternalUserAgent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuthExternalUserAgent.swift; sourceTree = ""; }; @@ -897,6 +899,7 @@ 6F49FAA8263C1A18005DB8D3 /* Mac */ = { isa = PBXGroup; children = ( + 6F5820F726EE036800906397 /* AppDataRemover.swift */, 6F49FAAA263C1A55005DB8D3 /* OAuthRedirectHTTPHandler.h */, 6F49FAAB263C1A55005DB8D3 /* OAuthRedirectHTTPHandler.m */, ); @@ -2179,6 +2182,7 @@ 6F59A58B24F67CE500560155 /* OAuthExternalUserAgent.swift in Sources */, 6F49FAB5263C1A56005DB8D3 /* OAuthRedirectHTTPHandler.m in Sources */, 6FEF30FF24A325860026C786 /* RowCell.swift in Sources */, + 6F5820F826EE036800906397 /* AppDataRemover.swift in Sources */, 6FADF83124ADF86700B75E8D /* Log.swift in Sources */, 6FEF313B24A717570026C786 /* ServerAuthService.swift in Sources */, 6F4C1ED625D12D710042AD95 /* SharedTunnelOptions.swift in Sources */, diff --git a/EduVPN/AppDelegate.swift b/EduVPN/AppDelegate.swift index 5b8f4987..c88e13b1 100644 --- a/EduVPN/AppDelegate.swift +++ b/EduVPN/AppDelegate.swift @@ -144,6 +144,46 @@ class AppDelegate: NSObject, NSApplicationDelegate { return .terminateLater } + func resetAppAfterConfirming() { + guard let environment = self.environment else { + return + } + + let alert = NSAlert() + alert.alertStyle = .warning + + alert.messageText = NSLocalizedString( + "Are you sure you want to reset the app \(Config.shared.appName)?", + comment: "macOS alert title on attempt to reset app") + alert.informativeText = NSLocalizedString( + "All user data and preferences will be removed.", + comment: "macOS alert text on attempt to reset app") + alert.addButton(withTitle: NSLocalizedString( + "Reset App", + comment: "macOS alert button on attempt to reset app")) + alert.addButton(withTitle: NSLocalizedString( + "Cancel", comment: "button title")) + + if let window = NSApp.windows.first { + alert.beginSheetModal(for: window) { result in + if case .alertFirstButtonReturn = result { + firstly { + environment.connectionService.disableVPN() + }.map { + AppDataRemover.removeAllData(persistenceService: self.environment?.persistenceService) + self.environment?.navigationController?.popToRoot() + self.mainViewController?.pushSearchOrAddVCIfNoEntries() + self.setShowInStatusBarEnabled( + UserDefaults.standard.showInStatusBar, + shouldUseColorIcons: UserDefaults.standard.isStatusItemInColor) + self.setShowInDockEnabled(UserDefaults.standard.showInDock) + self.setLaunchAtLoginEnabled(UserDefaults.standard.launchAtLogin) + }.cauterize() + } + } + } + } + func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { guard let connectionService = environment?.connectionService else { return .terminateNow diff --git a/EduVPN/Controllers/Mac/PreferencesViewController.swift b/EduVPN/Controllers/Mac/PreferencesViewController.swift index 0652074b..10be3348 100644 --- a/EduVPN/Controllers/Mac/PreferencesViewController.swift +++ b/EduVPN/Controllers/Mac/PreferencesViewController.swift @@ -108,6 +108,12 @@ class PreferencesViewController: ViewController, ParametrizedViewController { } } + @IBAction func resetAppClicked(_ sender: Any) { + guard let appDelegate = NSApp.delegate as? AppDelegate else { return } + self.presentingViewController?.dismiss(self) + appDelegate.resetAppAfterConfirming() + } + @IBAction func doneClicked(_ sender: Any) { self.presentingViewController?.dismiss(self) } diff --git a/EduVPN/Controllers/MainViewController.swift b/EduVPN/Controllers/MainViewController.swift index 805419f5..b9404f1f 100644 --- a/EduVPN/Controllers/MainViewController.swift +++ b/EduVPN/Controllers/MainViewController.swift @@ -374,6 +374,10 @@ extension MainViewController { break } viewModel.update() + pushSearchOrAddVCIfNoEntries() + } + + func pushSearchOrAddVCIfNoEntries() { if !environment.persistenceService.hasServers { if Config.shared.apiDiscoveryEnabled ?? false { let searchVC = environment.instantiateSearchViewController( @@ -386,10 +390,9 @@ extension MainViewController { predefinedProvider: Config.shared.predefinedProvider, shouldAutoFocusURLField: false) addServerVC.delegate = self - environment.navigationController?.pushViewController(addServerVC, animated: true) + environment.navigationController?.pushViewController(addServerVC, animated: false) } } - } } diff --git a/EduVPN/Helpers/Mac/AppDataRemover.swift b/EduVPN/Helpers/Mac/AppDataRemover.swift new file mode 100644 index 00000000..3f787683 --- /dev/null +++ b/EduVPN/Helpers/Mac/AppDataRemover.swift @@ -0,0 +1,49 @@ +// +// AppDataRemover.swift +// EduVPN +// + +import Foundation + +class AppDataRemover { + static func removeAllData(persistenceService: PersistenceService?) { + persistenceService?.removeAllData() + removeLegacyData() + clearCaches() + resetPreferences() + } + + static func clearCaches() { + DiscoveryDataFetcher.diskCache.removeAllCachedResponses() + URLCache.shared.removeAllCachedResponses() + } + + static func resetPreferences() { + UserDefaults.standard.clearPreferences() + } + + static func removeLegacyData() { + let coreDataDbURL = NSPersistentContainer.defaultDirectoryURL() + removeContentsOfDirectory(at: coreDataDbURL, where: { url in + url.isFileURL && url.pathExtension.starts(with: "sqlite") + }) + + if let appSupportURL = FileHelper.applicationSupportDirectoryUrl() { + removeContentsOfDirectory(at: appSupportURL, where: { url in + url.lastPathComponent != "AddedServers" + }) + } + } + + private static func removeContentsOfDirectory(at directoryURL: URL, where condition: (URL) -> Bool) { + let fileManager = FileManager.default + let enumerator = fileManager.enumerator( + at: directoryURL, includingPropertiesForKeys: nil, + options: [.skipsSubdirectoryDescendants]) + while let url = enumerator?.nextObject() as? URL { + if condition(url) { + try? fileManager.removeItem(at: url) + } + } + } +} diff --git a/EduVPN/Helpers/UserDefaults+Preferences.swift b/EduVPN/Helpers/UserDefaults+Preferences.swift index 829d6468..ffa3aefa 100644 --- a/EduVPN/Helpers/UserDefaults+Preferences.swift +++ b/EduVPN/Helpers/UserDefaults+Preferences.swift @@ -18,6 +18,25 @@ extension UserDefaults { private static let launchAtLoginKey = "launchAtLogin" #endif + func clearPreferences() { + var keys = [ + Self.forceTCPDefaultsKey, + Self.shouldNotifyBeforeSessionExpiryKey, + Self.hasAskedUserOnNotifyBeforeSessionExpiryKey + ] + #if os(macOS) + keys.append(contentsOf: [ + Self.showInStatusBarKey, + Self.isStatusItemInColorKey, + Self.showInDockKey, + Self.launchAtLoginKey + ]) + #endif + for key in keys { + removeObject(forKey: key) + } + } + var forceTCP: Bool { get { return bool(forKey: Self.forceTCPDefaultsKey) diff --git a/EduVPN/Resources/Mac/Base.lproj/Main.storyboard b/EduVPN/Resources/Mac/Base.lproj/Main.storyboard index e8f09f49..16d91c5c 100644 --- a/EduVPN/Resources/Mac/Base.lproj/Main.storyboard +++ b/EduVPN/Resources/Mac/Base.lproj/Main.storyboard @@ -584,13 +584,13 @@ Gw - + - + - + @@ -819,11 +819,11 @@ Gw - + - + @@ -831,7 +831,7 @@ Gw - + @@ -845,12 +845,12 @@ Gw - + - + - + @@ -1550,15 +1550,15 @@ Gw - - + + - - + + - + @@ -1566,10 +1566,10 @@ Gw - + - + - + - + - + - + - + - + - + @@ -1622,7 +1622,7 @@ Gw - + @@ -1684,7 +1684,7 @@ Gw - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +