diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2022-11-28 10:39:21 +0100 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2022-11-28 10:39:21 +0100 |
| commit | 5699d3f30333a7cc90eefb987b6c7e79ac14f423 (patch) | |
| tree | 72726da701e7deccb68baef00f12087741b7839f | |
| parent | 044b3868dfa1bd8c4573624aecd4c17b053d256e (diff) | |
| parent | 15825b53b7d8f4688dbcd028ba3cf888a9d9ee16 (diff) | |
| download | mullvadvpn-5699d3f30333a7cc90eefb987b6c7e79ac14f423.tar.xz mullvadvpn-5699d3f30333a7cc90eefb987b6c7e79ac14f423.zip | |
Merge branch 'reset-settings-on-migration-failure'
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 12 | ||||
| -rw-r--r-- | ios/MullvadVPN/AppDelegate.swift | 50 | ||||
| -rw-r--r-- | ios/MullvadVPN/SceneDelegate.swift | 62 | ||||
| -rw-r--r-- | ios/MullvadVPN/SettingsManager/KeychainSettingsStore.swift | 12 | ||||
| -rw-r--r-- | ios/MullvadVPN/SettingsManager/Migration.swift (renamed from ios/MullvadVPN/SettingsManager/SettingsMigration/Migration.swift) | 0 | ||||
| -rw-r--r-- | ios/MullvadVPN/SettingsManager/Migrations/MigrationFromV1ToV2.swift (renamed from ios/MullvadVPN/SettingsManager/SettingsMigration/MigrationFromV1ToV2.swift) | 57 | ||||
| -rw-r--r-- | ios/MullvadVPN/SettingsManager/SettingsManager.swift | 203 | ||||
| -rw-r--r-- | ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift | 5 | ||||
| -rw-r--r-- | ios/MullvadVPN/SettingsMigrationUIHandler.swift | 13 | ||||
| -rw-r--r-- | ios/Operations/OperationCompletion.swift | 14 |
10 files changed, 304 insertions, 124 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 4f7aaa8a3e..042c517930 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -293,6 +293,7 @@ 58E0729F28814ACC008902F8 /* WireGuardLogLevel+Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E0729E28814ACC008902F8 /* WireGuardLogLevel+Logging.swift */; }; 58E072A128814B0E008902F8 /* MullvadEndpoint+WgEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E072A028814B0E008902F8 /* MullvadEndpoint+WgEndpoint.swift */; }; 58E0A98827C8F46300FE6BDD /* Tunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E0A98727C8F46300FE6BDD /* Tunnel.swift */; }; + 58E11188292FA11F009FCA84 /* SettingsMigrationUIHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E11187292FA11F009FCA84 /* SettingsMigrationUIHandler.swift */; }; 58E20771274672CA00DE5D77 /* LaunchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E20770274672CA00DE5D77 /* LaunchViewController.swift */; }; 58E25F812837BBBB002CFB2C /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E25F802837BBBB002CFB2C /* SceneDelegate.swift */; }; 58E511E628DDDEAC00B0BCDE /* CodingErrors+CustomErrorDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E511E528DDDEAC00B0BCDE /* CodingErrors+CustomErrorDescription.swift */; }; @@ -788,6 +789,7 @@ 58E072A028814B0E008902F8 /* MullvadEndpoint+WgEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MullvadEndpoint+WgEndpoint.swift"; sourceTree = "<group>"; }; 58E072A428814C28008902F8 /* TunnelMonitorDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelMonitorDelegate.swift; sourceTree = "<group>"; }; 58E0A98727C8F46300FE6BDD /* Tunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tunnel.swift; sourceTree = "<group>"; }; + 58E11187292FA11F009FCA84 /* SettingsMigrationUIHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsMigrationUIHandler.swift; sourceTree = "<group>"; }; 58E20770274672CA00DE5D77 /* LaunchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchViewController.swift; sourceTree = "<group>"; }; 58E25F802837BBBB002CFB2C /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; }; 58E511E028DDB7F100B0BCDE /* WrappingError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappingError.swift; sourceTree = "<group>"; }; @@ -1026,20 +1028,20 @@ path = MullvadREST; sourceTree = "<group>"; }; - 068CE57129278F5F00A068BB /* SettingsMigration */ = { + 068CE57129278F5F00A068BB /* Migrations */ = { isa = PBXGroup; children = ( - 068CE5732927B7A400A068BB /* Migration.swift */, 068CE56F29278F5300A068BB /* MigrationFromV1ToV2.swift */, ); - path = SettingsMigration; + path = Migrations; sourceTree = "<group>"; }; 580F8B88281A79A7002E0998 /* SettingsManager */ = { isa = PBXGroup; children = ( - 068CE57129278F5F00A068BB /* SettingsMigration */, + 068CE57129278F5F00A068BB /* Migrations */, 06410DFD292CE18F00AFC18C /* KeychainSettingsStore.swift */, + 068CE5732927B7A400A068BB /* Migration.swift */, 58FF2C02281BDE02009EF542 /* SettingsManager.swift */, 06410E03292D0F7100AFC18C /* SettingsParser.swift */, 06410E06292D108E00AFC18C /* SettingsStore.swift */, @@ -1410,6 +1412,7 @@ 58CCA0152242560B004F3011 /* UIColor+Palette.swift */, 585CA70E25F8C44600B47C62 /* UIMetrics.swift */, 58F7CA872692E34000FC59FD /* WireguardKeysContentView.swift */, + 58E11187292FA11F009FCA84 /* SettingsMigrationUIHandler.swift */, ); path = MullvadVPN; sourceTree = "<group>"; @@ -2261,6 +2264,7 @@ 5888AD83227B11080051EB06 /* SelectLocationCell.swift in Sources */, 5891BF1C25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift in Sources */, 5878A26F2907E7E00096FC88 /* ProblemReportInteractor.swift in Sources */, + 58E11188292FA11F009FCA84 /* SettingsMigrationUIHandler.swift in Sources */, 5807E2C02432038B00F5FF30 /* String+Split.swift in Sources */, 58677712290976FB006F721F /* SettingsInteractor.swift in Sources */, 58CE5E66224146200008646E /* LoginViewController.swift in Sources */, diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index b11ada5f7c..1721f80261 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -110,28 +110,48 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD setupNotificationHandler() addApplicationNotifications(application: application) - let setupTunnelManagerOperation = - AsyncBlockOperation(dispatchQueue: .main) { operation in - SettingsManager.migrateStore(with: self.proxyFactory) { error in - precondition(error == nil) - - self.tunnelManager.loadConfiguration { error in - // TODO: avoid throwing fatal error and show the problem report UI instead. - if let error = error { - fatalError(error.localizedDescription) - } + let migrateSettingsOperation = AsyncBlockOperation(dispatchQueue: .main) { operation in + SettingsManager.migrateStore(with: self.proxyFactory) { error in + guard let error = error else { + operation.finish() + return + } - self.logger.debug("Finished initialization.") + guard let migrationUIHandler = application.connectedScenes.compactMap({ scene in + return scene.delegate as? SettingsMigrationUIHandler + }).first else { + operation.finish() + return + } - NotificationManager.shared.updateNotifications() - self.storePaymentManager.startPaymentQueueMonitoring() + migrationUIHandler.showMigrationError(error) { + operation.finish() + } + } + } - operation.finish() + let loadTunnelConfigurationOperation = + AsyncBlockOperation(dispatchQueue: .main) { operation in + self.tunnelManager.loadConfiguration { error in + // TODO: avoid throwing fatal error and show the problem report UI instead. + if let error = error { + fatalError(error.localizedDescription) } + + self.logger.debug("Finished initialization.") + + NotificationManager.shared.updateNotifications() + self.storePaymentManager.startPaymentQueueMonitoring() + + operation.finish() } } + loadTunnelConfigurationOperation.addDependency(migrateSettingsOperation) - operationQueue.addOperation(setupTunnelManagerOperation) + operationQueue.addOperations( + [migrateSettingsOperation, loadTunnelConfigurationOperation], + waitUntilFinished: false + ) return true } diff --git a/ios/MullvadVPN/SceneDelegate.swift b/ios/MullvadVPN/SceneDelegate.swift index 27fbfa7bf2..03f6bc3674 100644 --- a/ios/MullvadVPN/SceneDelegate.swift +++ b/ios/MullvadVPN/SceneDelegate.swift @@ -19,7 +19,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDe SettingsNavigationControllerDelegate, ConnectViewControllerDelegate, OutOfTimeViewControllerDelegate, SelectLocationViewControllerDelegate, RevokedDeviceViewControllerDelegate, NotificationManagerDelegate, TunnelObserver, - RelayCacheTrackerObserver + RelayCacheTrackerObserver, SettingsMigrationUIHandler { private let logger = Logger(label: "SceneDelegate") @@ -952,4 +952,64 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDe } return nil } + + // MARK: - SettingsMigrationUIHandler + + func showMigrationError(_ error: Error, completionHandler: @escaping () -> Void) { + let alertController = UIAlertController( + title: NSLocalizedString( + "ALERT_TITLE", + tableName: "SettingsMigrationUI", + value: "Settings migration error", + comment: "" + ), + message: Self.migrationErrorReason(error), + preferredStyle: .alert + ) + alertController.addAction( + UIAlertAction( + title: NSLocalizedString("OK", tableName: "SettingsMigrationUI", comment: ""), + style: .default, + handler: { _ in + completionHandler() + } + ) + ) + + if let rootViewController = window?.rootViewController { + rootViewController.present(alertController, animated: true) + } else { + completionHandler() + } + } + + private static func migrationErrorReason(_ error: Error) -> String { + if error is UnsupportedSettingsVersionError { + return NSLocalizedString( + "NEWER_STORED_SETTINGS_ERROR", + tableName: "SettingsMigrationUI", + value: """ + The version of settings stored on device is from a newer app than is currently \ + running. Settings will be reset to defaults. + """, + comment: "" + ) + } else if let error = error as? SettingsMigrationError, + error.underlyingError is REST.Error + { + return NSLocalizedString( + "NETWORK_ERROR", + tableName: "SettingsMigrationUI", + value: "Network error occurred. Settings will be reset to defaults.", + comment: "" + ) + } else { + return NSLocalizedString( + "INTERNAL_ERROR", + tableName: "SettingsMigrationUI", + value: "Internal error occurred. Settings will be reset to defaults.", + comment: "" + ) + } + } } diff --git a/ios/MullvadVPN/SettingsManager/KeychainSettingsStore.swift b/ios/MullvadVPN/SettingsManager/KeychainSettingsStore.swift index f98ee06a6f..eb662b8d11 100644 --- a/ios/MullvadVPN/SettingsManager/KeychainSettingsStore.swift +++ b/ios/MullvadVPN/SettingsManager/KeychainSettingsStore.swift @@ -11,10 +11,12 @@ import MullvadTypes import Security class KeychainSettingsStore: SettingsStore { - let keychainServiceName: String + let serviceName: String + let accessGroup: String - init(keychainServiceName: String) { - self.keychainServiceName = keychainServiceName + init(serviceName: String, accessGroup: String) { + self.serviceName = serviceName + self.accessGroup = accessGroup } func read(key: SettingsKey) throws -> Data { @@ -89,14 +91,14 @@ class KeychainSettingsStore: SettingsStore { private func createDefaultAttributes(item: SettingsKey) -> [CFString: Any] { return [ kSecClass: kSecClassGenericPassword, - kSecAttrService: keychainServiceName, + kSecAttrService: serviceName, kSecAttrAccount: item.rawValue, ] } private func createAccessAttributes() -> [CFString: Any] { return [ - kSecAttrAccessGroup: ApplicationConfiguration.securityGroupIdentifier, + kSecAttrAccessGroup: accessGroup, kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock, ] } diff --git a/ios/MullvadVPN/SettingsManager/SettingsMigration/Migration.swift b/ios/MullvadVPN/SettingsManager/Migration.swift index 79704c7b9b..79704c7b9b 100644 --- a/ios/MullvadVPN/SettingsManager/SettingsMigration/Migration.swift +++ b/ios/MullvadVPN/SettingsManager/Migration.swift diff --git a/ios/MullvadVPN/SettingsManager/SettingsMigration/MigrationFromV1ToV2.swift b/ios/MullvadVPN/SettingsManager/Migrations/MigrationFromV1ToV2.swift index ea95cb9632..34956cd1e0 100644 --- a/ios/MullvadVPN/SettingsManager/SettingsMigration/MigrationFromV1ToV2.swift +++ b/ios/MullvadVPN/SettingsManager/Migrations/MigrationFromV1ToV2.swift @@ -19,8 +19,8 @@ class MigrationFromV1ToV2: Migration { private var accountTask: Cancellable? private var deviceTask: Cancellable? - private var accountCompletion: OperationCompletion<REST.AccountData, REST.Error>? - private var devicesCompletion: OperationCompletion<[REST.Device], REST.Error>? + private var accountCompletion: OperationCompletion<REST.AccountData, REST.Error> = .cancelled + private var devicesCompletion: OperationCompletion<[REST.Device], REST.Error> = .cancelled private let legacySettings: LegacyTunnelSettings @@ -42,6 +42,18 @@ class MigrationFromV1ToV2: Migration { ) { let storedAccountNumber = legacySettings.accountNumber + // Store last used account number. + logger.debug("Store legacy account number as last used account.") + do { + if let accountData = storedAccountNumber.data(using: .utf8) { + try store.write(accountData, for: .lastUsedAccount) + } else { + logger.error("Failed to encode account number into utf-8 data.") + } + } catch { + logger.error(error: error, message: "Failed to store last used account.") + } + // Fetch remote data concurrently. logger.debug("Fetching account and device data...") let dispatchGroup = DispatchGroup() @@ -67,27 +79,16 @@ class MigrationFromV1ToV2: Migration { } dispatchGroup.notify(queue: .main) { - switch (self.accountCompletion, self.devicesCompletion) { - case let (.success(accountData), .success(deviceData)): - // Migrate settings if all data is available. - - let result = Result { - try self.migrateSettings( - store: store, - parser: parser, - settings: self.legacySettings, - accountData: accountData, - devices: deviceData - ) - } - - completion(result.error) - - default: - let errors = [self.accountCompletion?.error, self.devicesCompletion?.error] - .compactMap { $0 } - completion(MigrateLegacySettingsError(underlyingErrors: errors)) + let result = Result { + try self.migrateSettings( + store: store, + parser: parser, + settings: self.legacySettings, + accountData: try self.accountCompletion.get(), + devices: try self.devicesCompletion.get() + ) } + completion(result.error) } } @@ -163,15 +164,3 @@ class MigrationFromV1ToV2: Migration { try store.write(deviceData, for: .deviceState) } } - -struct MigrateLegacySettingsError: WrappingError { - let underlyingErrors: [REST.Error] - - var underlyingError: Error? { - return underlyingErrors.first - } - - var errorDescription: String? { - return "Failed to migrate legacy settings to v2" - } -} diff --git a/ios/MullvadVPN/SettingsManager/SettingsManager.swift b/ios/MullvadVPN/SettingsManager/SettingsManager.swift index f0e88185b1..2940937ae4 100644 --- a/ios/MullvadVPN/SettingsManager/SettingsManager.swift +++ b/ios/MullvadVPN/SettingsManager/SettingsManager.swift @@ -16,8 +16,15 @@ private let accountTokenKey = "accountToken" private let accountExpiryKey = "accountExpiry" enum SettingsManager { + private static let logger = Logger(label: "SettingsManager") + + private static let store: SettingsStore = KeychainSettingsStore( + serviceName: keychainServiceName, + accessGroup: ApplicationConfiguration.securityGroupIdentifier + ) + private static func makeParser() -> SettingsParser { - SettingsParser(decoder: JSONDecoder(), encoder: JSONEncoder()) + return SettingsParser(decoder: JSONDecoder(), encoder: JSONEncoder()) } // MARK: - Last used account @@ -50,23 +57,29 @@ enum SettingsManager { } } - private static let store: SettingsStore = KeychainSettingsStore( - keychainServiceName: keychainServiceName - ) - // MARK: - Settings static func readSettings() throws -> TunnelSettingsV2 { - let data = try store.read(key: .settings) + let storedVersion: Int + let data: Data let parser = makeParser() - let version = try parser.parseVersion(data: data) - let currentVersion = SchemaVersion.current.rawValue + do { + data = try store.read(key: .settings) + storedVersion = try parser.parseVersion(data: data) + } catch { + throw ReadSettingsVersionError(underlyingError: error) + } + + let currentVersion = SchemaVersion.current - if version == currentVersion { + if storedVersion == currentVersion.rawValue { return try parser.parsePayload(as: TunnelSettingsV2.self, from: data) } else { - throw UnsupportedVersionSettings(storedVersion: version, currentVersion: currentVersion) + throw UnsupportedSettingsVersionError( + storedVersion: storedVersion, + currentVersion: currentVersion + ) } } @@ -95,21 +108,38 @@ enum SettingsManager { // MARK: - Migration + /// Migrate settings store if needed. + /// + /// The error returned in `completion` handler is set to `nil` upon success or when no + /// migration is not needed. + /// + /// The following types of error are expected to be returned by this method: + /// `SettingsMigrationError`, `UnsupportedSettingsVersionError`, `ReadSettingsVersionError`. static func migrateStore( with restFactory: REST.ProxyFactory, completion: @escaping (Error?) -> Void ) { + let handleCompletion = { (error: Error?) in + // Reset store upon failure to migrate settings. + if error != nil { + self.resetStore() + } + completion(error) + } + if let legacySettings = readLegacySettings() { migrateLegacySettings( restFactory: restFactory, legacySettings: legacySettings, - completion: completion + completion: handleCompletion ) } else { - migrateModernSettings(completion: completion) + migrateModernSettings(completion: handleCompletion) } } + // MARK: - Private + private static func migrateLegacySettings( restFactory: REST.ProxyFactory, legacySettings: LegacyTunnelSettings, @@ -124,22 +154,17 @@ enum SettingsManager { migration.migrate(with: store, parser: parser) { error in if let error = error { - logger.error( - error: error, - message: "Failed to migrate from legacy settings to v2." + let migrationError = SettingsMigrationError( + sourceVersion: .v1, + targetVersion: .v2, + underlyingError: error ) - completion(error) - } else { - let userDefaults = UserDefaults.standard - - logger.debug("Remove legacy settings from keychain.") - Self.deleteLegacySettings() + logger.error(error: migrationError) - logger.debug("Remove legacy settings from user defaults.") - - userDefaults.removeObject(forKey: accountTokenKey) - userDefaults.removeObject(forKey: accountExpiryKey) + completion(migrationError) + } else { + Self.deleteAllLegacySettings() completion(nil) } @@ -147,66 +172,68 @@ enum SettingsManager { } private static func migrateModernSettings(completion: @escaping (Error?) -> Void) { - let parser = makeParser() - do { + let parser = makeParser() let settingsData = try store.read(key: .settings) let settingsVersion = try parser.parseVersion(data: settingsData) if settingsVersion != SchemaVersion.current.rawValue { - let error = UnsupportedVersionSettings( + let error = UnsupportedSettingsVersionError( storedVersion: settingsVersion, - currentVersion: SchemaVersion.current.rawValue + currentVersion: SchemaVersion.current ) - logger.error( - error: error, - message: "Encountered an unknown version." - ) + logger.error(error: error, message: "Encountered an unknown version.") completion(error) - } else { completion(nil) } - } catch .itemNotFound as KeychainError { completion(nil) } catch { - completion(error) + completion(ReadSettingsVersionError(underlyingError: error)) } } - // MARK: - Legacy settings + /// Removes all legacy settings, device state and tunnel settings but keeps the last used + /// account number stored. + private static func resetStore() { + logger.debug("Reset store.") - private static func readLegacySettings() -> LegacyTunnelSettings? { - let storedAccountNumber = UserDefaults.standard.string(forKey: accountTokenKey) - - guard let storedAccountNumber = storedAccountNumber else { - logger.debug("Account number is not found in user defaults. Nothing to migrate.") - - return nil + do { + try store.delete(key: .deviceState) + } catch { + if (error as? KeychainError) != .itemNotFound { + logger.error(error: error, message: "Failed to delete device state.") + } } - // Set legacy account number as last used. - logger.debug("Found legacy account number.") - logger.debug("Store last used account.") - do { - try Self.setLastUsedAccount(storedAccountNumber) + try store.delete(key: .settings) } catch { - logger.error( - error: error, - message: "Failed to store last used account." - ) + if (error as? KeychainError) != .itemNotFound { + logger.error(error: error, message: "Failed to delete settings.") + } + } + + Self.deleteAllLegacySettings() + } + + // MARK: - Legacy settings + + private static func readLegacySettings() -> LegacyTunnelSettings? { + guard let storedAccountNumber = UserDefaults.standard.string(forKey: accountTokenKey) else { + logger.debug("Legacy account number is not found in user defaults. Nothing to migrate.") + return nil } // List legacy settings stored in keychain. - logger.debug("Read legacy settings...") + logger.debug("List legacy settings in keychain...") var storedSettings: [LegacyTunnelSettings] = [] do { - storedSettings = try Self.readLegacySettings() + storedSettings = try findAllLegacySettingsInKeychain() } catch .itemNotFound as KeychainError { logger.debug("Legacy settings are not found in keychain.") @@ -236,11 +263,7 @@ enum SettingsManager { return matchingSettings } - // MARK: - Legacy settings support - - private static let logger = Logger(label: "SettingsManager") - - static func readLegacySettings() throws -> [LegacyTunnelSettings] { + private static func findAllLegacySettingsInKeychain() throws -> [LegacyTunnelSettings] { let query: [CFString: Any] = [ kSecClass: kSecClassGenericPassword, kSecAttrService: keychainServiceName, @@ -287,7 +310,17 @@ enum SettingsManager { } } - static func deleteLegacySettings() { + private static func deleteAllLegacySettings() { + logger.debug("Remove legacy settings from keychain.") + deleteLegacySettingsFromKeychain() + + logger.debug("Remove legacy settings from user defaults.") + let userDefaults = UserDefaults.standard + userDefaults.removeObject(forKey: accountTokenKey) + userDefaults.removeObject(forKey: accountExpiryKey) + } + + private static func deleteLegacySettingsFromKeychain() { let query: [CFString: Any] = [ kSecClass: kSecClassGenericPassword, kSecAttrService: keychainServiceName, @@ -362,11 +395,53 @@ enum SettingsKey: String, CaseIterable { case lastUsedAccount = "LastUsedAccount" } -struct UnsupportedVersionSettings: LocalizedError { - let storedVersion, currentVersion: Int +/// An error type describing a failure to read or parse settings version. +struct ReadSettingsVersionError: LocalizedError, WrappingError { + private let inner: Error + + var underlyingError: Error? { + return inner + } + + var errorDescription: String? { + return "Failed to read settings version." + } + + init(underlyingError: Error) { + inner = underlyingError + } +} + +/// An error returned when stored settings version is unknown to the currently running app. +struct UnsupportedSettingsVersionError: LocalizedError { + let storedVersion: Int + let currentVersion: SchemaVersion var errorDescription: String? { - return "Stored settings version was not the same as current version, stored version: \(storedVersion), current version: \(currentVersion)" + return """ + Stored settings version was not the same as current version, \ + stored version: \(storedVersion), current version: \(currentVersion) + """ + } +} + +/// A wrapper type for errors returned by concrete migrations. +struct SettingsMigrationError: LocalizedError, WrappingError { + private let inner: Error + let sourceVersion, targetVersion: SchemaVersion + + var underlyingError: Error? { + return inner + } + + var errorDescription: String? { + return "Failed to migrate settings from \(sourceVersion) to \(targetVersion)." + } + + init(sourceVersion: SchemaVersion, targetVersion: SchemaVersion, underlyingError: Error) { + self.sourceVersion = sourceVersion + self.targetVersion = targetVersion + inner = underlyingError } } diff --git a/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift b/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift index 6c0ada1415..76b794eba9 100644 --- a/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift +++ b/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift @@ -14,7 +14,10 @@ import class WireGuardKitTypes.PrivateKey import class WireGuardKitTypes.PublicKey /// Settings and device state schema versions. -enum SchemaVersion: Int { +enum SchemaVersion: Int, Equatable { + /// Legacy settings format, stored as `TunnelSettingsV1`. + case v1 = 1 + /// New settings format, stored as `TunnelSettingsV2`. case v2 = 2 diff --git a/ios/MullvadVPN/SettingsMigrationUIHandler.swift b/ios/MullvadVPN/SettingsMigrationUIHandler.swift new file mode 100644 index 0000000000..949458e394 --- /dev/null +++ b/ios/MullvadVPN/SettingsMigrationUIHandler.swift @@ -0,0 +1,13 @@ +// +// SettingsMigrationUIHandler.swift +// MullvadVPN +// +// Created by pronebird on 24/11/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +protocol SettingsMigrationUIHandler { + func showMigrationError(_ error: Error, completionHandler: @escaping () -> Void) +} diff --git a/ios/Operations/OperationCompletion.swift b/ios/Operations/OperationCompletion.swift index 0e004bfe50..49a34e0884 100644 --- a/ios/Operations/OperationCompletion.swift +++ b/ios/Operations/OperationCompletion.swift @@ -65,6 +65,14 @@ public enum OperationCompletion<Success, Failure: Error> { } } + public func get() throws -> Success { + if let result = result { + return try result.get() + } else { + throw OperationCancellationError() + } + } + public func map<NewSuccess>(_ block: (Success) -> NewSuccess) -> OperationCompletion<NewSuccess, Failure> { @@ -143,3 +151,9 @@ public enum OperationCompletion<Success, Failure: Error> { return mapError { $0 } } } + +public struct OperationCancellationError: LocalizedError { + public var errorDescription: String? { + return "Operation was cancelled." + } +} |
