diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2023-06-08 16:19:27 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2023-06-08 16:19:27 +0200 |
| commit | 04e49a9f00467a35138ba12d4c812397797ff1ce (patch) | |
| tree | 7e0bdaebeb5f5e2187b0fa7b586d3d0cf364f732 | |
| parent | ac8deecc4ebd4a71810006d221292c03e36ad120 (diff) | |
| parent | d79201b3c90828e056b7c5bbf516ce551723afed (diff) | |
| download | mullvadvpn-04e49a9f00467a35138ba12d4c812397797ff1ce.tar.xz mullvadvpn-04e49a9f00467a35138ba12d4c812397797ff1ce.zip | |
Merge branch 'set-account-flow'
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 20 | ||||
| -rw-r--r-- | ios/MullvadVPN/AppDelegate.swift | 59 | ||||
| -rw-r--r-- | ios/MullvadVPN/TunnelManager/SetAccountOperation.swift | 451 | ||||
| -rw-r--r-- | ios/Operations/InputInjectionBuilder.swift | 96 | ||||
| -rw-r--r-- | ios/Operations/InputOperation.swift | 47 | ||||
| -rw-r--r-- | ios/Operations/TransformOperation.swift | 110 | ||||
| -rw-r--r-- | ios/OperationsTests/OperationInputInjectionTests.swift | 89 | ||||
| -rw-r--r-- | ios/OperationsTests/TransformOperationTests.swift | 93 |
8 files changed, 247 insertions, 718 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index ad13c84ad4..2be0a3ed02 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -221,7 +221,6 @@ 589A454C28DDF5E100565204 /* Swizzle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 589A454B28DDF5E100565204 /* Swizzle.swift */; }; 589A455C28E094BF00565204 /* OperationSmokeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DF5B7E2852778600E92647 /* OperationSmokeTests.swift */; }; 589A455D28E094BF00565204 /* OperationObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583E1E292848DF67004838B3 /* OperationObserverTests.swift */; }; - 589A455E28E094BF00565204 /* OperationInputInjectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DF5B772852178600E92647 /* OperationInputInjectionTests.swift */; }; 589A455F28E094BF00565204 /* OperationConditionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580CBFB72848D503007878F0 /* OperationConditionTests.swift */; }; 58A1AA8C23F5584C009F7EA6 /* ConnectionPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A1AA8B23F5584B009F7EA6 /* ConnectionPanelView.swift */; }; 58A3BDB028A1821A00C8C2C6 /* WgStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A3BDAF28A1821A00C8C2C6 /* WgStats.swift */; }; @@ -234,7 +233,6 @@ 58ACF64F26567A7100ACE4B7 /* CustomSwitchContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ACF64E26567A7100ACE4B7 /* CustomSwitchContainer.swift */; }; 58AFC99529F96F7B000829DE /* AsyncBlockOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AFC99429F96F7B000829DE /* AsyncBlockOperationTests.swift */; }; 58AFC99729F9753D000829DE /* AsyncResultBlockOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AFC99629F9753D000829DE /* AsyncResultBlockOperationTests.swift */; }; - 58AFC99929F97856000829DE /* TransformOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AFC99829F97856000829DE /* TransformOperationTests.swift */; }; 58B0A2A8238EE68200BC001D /* RelaySelectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584B26F3237434D00073B10E /* RelaySelectorTests.swift */; }; 58B26E1E2943514300D5980C /* InAppNotificationDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B26E1D2943514300D5980C /* InAppNotificationDescriptor.swift */; }; 58B26E22294351EA00D5980C /* InAppNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B26E21294351EA00D5980C /* InAppNotificationProvider.swift */; }; @@ -280,18 +278,15 @@ 58D223AD294C8A630029F5F8 /* AsyncBlockOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE22324B3243100F9D8A1 /* AsyncBlockOperation.swift */; }; 58D223AE294C8A630029F5F8 /* OutputOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58059DDD28468158002B1049 /* OutputOperation.swift */; }; 58D223AF294C8A630029F5F8 /* NoFailedDependenciesCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581813A228E09DCD002817DE /* NoFailedDependenciesCondition.swift */; }; - 58D223B0294C8A630029F5F8 /* TransformOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58059DDB28465E8F002B1049 /* TransformOperation.swift */; }; 58D223B1294C8A630029F5F8 /* OperationCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 589D28772846250500F9A7B3 /* OperationCondition.swift */; }; 58D223B2294C8A630029F5F8 /* OperationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5840BE34279EDB16002836BA /* OperationError.swift */; }; 58D223B3294C8A630029F5F8 /* OperationObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 589D28792846250500F9A7B3 /* OperationObserver.swift */; }; - 58D223B4294C8A630029F5F8 /* InputInjectionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DF5B752852108E00E92647 /* InputInjectionBuilder.swift */; }; 58D223B6294C8A630029F5F8 /* BlockCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581813A428E09DE2002817DE /* BlockCondition.swift */; }; 58D223B7294C8A630029F5F8 /* MutuallyExclusive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581813A628E09DF2002817DE /* MutuallyExclusive.swift */; }; 58D223B8294C8A630029F5F8 /* ResultOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F7D26427EB50A300E4D821 /* ResultOperation.swift */; }; 58D223B9294C8A630029F5F8 /* AsyncOperationQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 589D28782846250500F9A7B3 /* AsyncOperationQueue.swift */; }; 58D223BA294C8A630029F5F8 /* BackgroundObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 589D287F28462CB000F9A7B3 /* BackgroundObserver.swift */; }; 58D223BB294C8A630029F5F8 /* NoCancelledDependenciesCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581813A028E09DBB002817DE /* NoCancelledDependenciesCondition.swift */; }; - 58D223BC294C8A630029F5F8 /* InputOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DF5B732851FF3F00E92647 /* InputOperation.swift */; }; 58D223BD294C8A630029F5F8 /* AsyncOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E973DD24850EB600096F90 /* AsyncOperation.swift */; }; 58D223BE294C8A630029F5F8 /* ResultBlockOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5842102D282D3FC200F24E46 /* ResultBlockOperation.swift */; }; 58D223BF294C8AE90029F5F8 /* Operations.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58D223A5294C8A480029F5F8 /* Operations.framework */; }; @@ -807,7 +802,6 @@ 06FAE67D28F83CA50033DD93 /* RESTTransport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RESTTransport.swift; sourceTree = "<group>"; }; 5803B4AF2940A47300C23744 /* TunnelConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelConfiguration.swift; sourceTree = "<group>"; }; 5803B4B12940A48700C23744 /* TunnelStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelStore.swift; sourceTree = "<group>"; }; - 58059DDB28465E8F002B1049 /* TransformOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransformOperation.swift; sourceTree = "<group>"; }; 58059DDD28468158002B1049 /* OutputOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutputOperation.swift; sourceTree = "<group>"; }; 5807E2BF2432038B00F5FF30 /* String+Split.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Split.swift"; sourceTree = "<group>"; }; 5807E2C1243203D000F5FF30 /* StringTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringTests.swift; sourceTree = "<group>"; }; @@ -1003,7 +997,6 @@ 58AEEF642344A36000C9BBD5 /* KeychainError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainError.swift; sourceTree = "<group>"; }; 58AFC99429F96F7B000829DE /* AsyncBlockOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncBlockOperationTests.swift; sourceTree = "<group>"; }; 58AFC99629F9753D000829DE /* AsyncResultBlockOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncResultBlockOperationTests.swift; sourceTree = "<group>"; }; - 58AFC99829F97856000829DE /* TransformOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransformOperationTests.swift; sourceTree = "<group>"; }; 58B0A2A0238EE67E00BC001D /* MullvadVPNTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MullvadVPNTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 58B0A2A4238EE67E00BC001D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 58B26E1D2943514300D5980C /* InAppNotificationDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppNotificationDescriptor.swift; sourceTree = "<group>"; }; @@ -1057,9 +1050,6 @@ 58D223F5294C8FF00029F5F8 /* MullvadLogging.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MullvadLogging.h; sourceTree = "<group>"; }; 58D229B6298D1D5200BB5A2D /* URLRequestProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLRequestProxy.swift; sourceTree = "<group>"; }; 58DF28A42417CB4B00E836B0 /* StorePaymentManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorePaymentManager.swift; sourceTree = "<group>"; }; - 58DF5B732851FF3F00E92647 /* InputOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputOperation.swift; sourceTree = "<group>"; }; - 58DF5B752852108E00E92647 /* InputInjectionBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputInjectionBuilder.swift; sourceTree = "<group>"; }; - 58DF5B772852178600E92647 /* OperationInputInjectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationInputInjectionTests.swift; sourceTree = "<group>"; }; 58DF5B7E2852778600E92647 /* OperationSmokeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationSmokeTests.swift; sourceTree = "<group>"; }; 58E07298288031D5008902F8 /* WireGuardAdapterError+Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WireGuardAdapterError+Localization.swift"; sourceTree = "<group>"; }; 58E0729C28814AAE008902F8 /* PacketTunnelConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelConfiguration.swift; sourceTree = "<group>"; }; @@ -1843,10 +1833,8 @@ 58AFC99429F96F7B000829DE /* AsyncBlockOperationTests.swift */, 58AFC99629F9753D000829DE /* AsyncResultBlockOperationTests.swift */, 580CBFB72848D503007878F0 /* OperationConditionTests.swift */, - 58DF5B772852178600E92647 /* OperationInputInjectionTests.swift */, 583E1E292848DF67004838B3 /* OperationObserverTests.swift */, 58DF5B7E2852778600E92647 /* OperationSmokeTests.swift */, - 58AFC99829F97856000829DE /* TransformOperationTests.swift */, ); path = OperationsTests; sourceTree = "<group>"; @@ -2045,8 +2033,6 @@ 589D287F28462CB000F9A7B3 /* BackgroundObserver.swift */, 581813A428E09DE2002817DE /* BlockCondition.swift */, 589D28812846306C00F9A7B3 /* GroupOperation.swift */, - 58DF5B752852108E00E92647 /* InputInjectionBuilder.swift */, - 58DF5B732851FF3F00E92647 /* InputOperation.swift */, 581813A628E09DF2002817DE /* MutuallyExclusive.swift */, 581813A028E09DBB002817DE /* NoCancelledDependenciesCondition.swift */, 581813A228E09DCD002817DE /* NoFailedDependenciesCondition.swift */, @@ -2056,7 +2042,6 @@ 58059DDD28468158002B1049 /* OutputOperation.swift */, 5842102D282D3FC200F24E46 /* ResultBlockOperation.swift */, 58F7D26427EB50A300E4D821 /* ResultOperation.swift */, - 58059DDB28465E8F002B1049 /* TransformOperation.swift */, ); path = Operations; sourceTree = "<group>"; @@ -2838,12 +2823,10 @@ buildActionMask = 2147483647; files = ( 589A455F28E094BF00565204 /* OperationConditionTests.swift in Sources */, - 589A455E28E094BF00565204 /* OperationInputInjectionTests.swift in Sources */, 58AFC99529F96F7B000829DE /* AsyncBlockOperationTests.swift in Sources */, 58AFC99729F9753D000829DE /* AsyncResultBlockOperationTests.swift in Sources */, 589A455C28E094BF00565204 /* OperationSmokeTests.swift in Sources */, 589A455D28E094BF00565204 /* OperationObserverTests.swift in Sources */, - 58AFC99929F97856000829DE /* TransformOperationTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3122,8 +3105,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 58D223B0294C8A630029F5F8 /* TransformOperation.swift in Sources */, - 58D223B4294C8A630029F5F8 /* InputInjectionBuilder.swift in Sources */, 58D223B6294C8A630029F5F8 /* BlockCondition.swift in Sources */, 58D223BA294C8A630029F5F8 /* BackgroundObserver.swift in Sources */, 58D223AD294C8A630029F5F8 /* AsyncBlockOperation.swift in Sources */, @@ -3136,7 +3117,6 @@ 58D223B9294C8A630029F5F8 /* AsyncOperationQueue.swift in Sources */, 58D223B8294C8A630029F5F8 /* ResultOperation.swift in Sources */, 58D223AE294C8A630029F5F8 /* OutputOperation.swift in Sources */, - 58D223BC294C8A630029F5F8 /* InputOperation.swift in Sources */, 58D223BD294C8A630029F5F8 /* AsyncOperation.swift in Sources */, 58D223BE294C8A630029F5F8 /* ResultBlockOperation.swift in Sources */, 58D223B1294C8A630029F5F8 /* OperationCondition.swift in Sources */, diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index 6090ccdd82..c105797191 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -120,7 +120,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD registerBackgroundTasks() setupPaymentHandler() - setupNotificationHandler() + setupNotifications() addApplicationNotifications(application: application) startInitialization(application: application) @@ -362,7 +362,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD storePaymentManager.addPaymentObserver(tunnelManager) } - private func setupNotificationHandler() { + private func setupNotifications() { NotificationManager.shared.notificationProviders = [ RegisteredDeviceInAppNotificationProvider(tunnelManager: tunnelManager), TunnelStatusNotificationProvider(tunnelManager: tunnelManager), @@ -375,10 +375,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD private func startInitialization(application: UIApplication) { let wipeSettingsOperation = getWipeSettingsOperation() - let loadTunnelStoreOperation = AsyncBlockOperation(dispatchQueue: .main) { finish in - self.tunnelStore.loadPersistentTunnels { error in + let loadTunnelStoreOperation = AsyncBlockOperation(dispatchQueue: .main) { [self] finish in + tunnelStore.loadPersistentTunnels { [self] error in if let error { - self.logger.error( + logger.error( error: error, message: "Failed to load persistent tunnels." ) @@ -387,22 +387,30 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } } - let migrateSettingsOperation = ResultBlockOperation<SettingsMigrationResult>(dispatchQueue: .main) { finish in - SettingsManager.migrateStore(with: self.proxyFactory) { migrationResult in - let finishHandler = { - finish(.success(migrationResult)) - } + let migrateSettingsOperation = AsyncBlockOperation(dispatchQueue: .main) { [self] finish in + SettingsManager.migrateStore(with: proxyFactory) { [self] migrationResult in + switch migrationResult { + case .success: + // Tell the tunnel to re-read tunnel configuration after migration. + logger.debug("Reconnect the tunnel after settings migration.") + tunnelManager.reconnectTunnel(selectNewRelay: true) + fallthrough - guard case let .failure(error) = migrationResult, - let migrationUIHandler = application.connectedScenes.compactMap({ scene in - return scene.delegate as? SettingsMigrationUIHandler - }).first - else { - finishHandler() - return - } + case .nothing: + finish(nil) + + case let .failure(error): + let migrationUIHandler = application.connectedScenes.first { $0 is SettingsMigrationUIHandler } + as? SettingsMigrationUIHandler - migrationUIHandler.showMigrationError(error, completionHandler: finishHandler) + if let migrationUIHandler { + migrationUIHandler.showMigrationError(error) { + finish(error) + } + } else { + finish(error) + } + } } } migrateSettingsOperation.addDependencies([wipeSettingsOperation, loadTunnelStoreOperation]) @@ -424,25 +432,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } initTunnelManagerOperation.addDependency(migrateSettingsOperation) - let reconnectTunnelOperation = TransformOperation<SettingsMigrationResult, Void>( - dispatchQueue: .main - ) { migrationResult in - if case .success = migrationResult { - self.logger.debug("Reconnect the tunnel after settings migration.") - - self.tunnelManager.reconnectTunnel(selectNewRelay: true) - } - } - reconnectTunnelOperation.inject(from: migrateSettingsOperation) - reconnectTunnelOperation.addDependency(initTunnelManagerOperation) - operationQueue.addOperations( [ wipeSettingsOperation, loadTunnelStoreOperation, migrateSettingsOperation, initTunnelManagerOperation, - reconnectTunnelOperation, ], waitUntilFinished: false ) diff --git a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift index 1b85516871..4c6ddace88 100644 --- a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift +++ b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift @@ -12,7 +12,6 @@ import MullvadREST import MullvadTypes import Operations import class WireGuardKitTypes.PrivateKey -import class WireGuardKitTypes.PublicKey enum SetAccountAction { /// Set new account. @@ -36,33 +35,6 @@ enum SetAccountAction { } } -private struct SetAccountResult { - let accountData: StoredAccountData - let privateKey: PrivateKey - let device: Device -} - -private struct SetAccountContext: OperationInputContext { - var accountData: StoredAccountData? - var privateKey: PrivateKey? - var device: Device? - - func reduce() -> SetAccountResult? { - guard let accountData, - let privateKey, - let device - else { - return nil - } - - return SetAccountResult( - accountData: accountData, - privateKey: privateKey, - device: device - ) - } -} - class SetAccountOperation: ResultOperation<StoredAccountData?> { private let interactor: TunnelInteractor private let accountsProxy: REST.AccountsProxy @@ -70,9 +42,7 @@ class SetAccountOperation: ResultOperation<StoredAccountData?> { private let action: SetAccountAction private let logger = Logger(label: "SetAccountOperation") - private let operationQueue = AsyncOperationQueue() - - private var children: [Operation] = [] + private var tasks: [Cancellable] = [] init( dispatchQueue: DispatchQueue, @@ -89,114 +59,168 @@ class SetAccountOperation: ResultOperation<StoredAccountData?> { super.init(dispatchQueue: dispatchQueue) } - override func main() { - let deleteDeviceOperation = getDeleteDeviceOperation() - let unsetDeviceStateOperation = getUnsetDeviceStateOperation() + // MARK: - - deleteDeviceOperation.flatMap { unsetDeviceStateOperation.addDependency($0) } + override func main() { + startLogoutFlow { [self] in + switch action { + case .new: + startNewAccountFlow { [self] result in + finish(result: result.map { .some($0) }) + } - let setupAccountOperations = getAccountDataOperation() - .flatMap { accountOperation -> [Operation] in - accountOperation.addCondition( - NoFailedDependenciesCondition(ignoreCancellations: false) - ) - accountOperation.addDependency(unsetDeviceStateOperation) + case let .existing(accountNumber): + startExistingAccountFlow(accountNumber: accountNumber) { [self] result in + finish(result: result.map { .some($0) }) + } - let createDeviceOperation = getCreateDeviceOperation() - createDeviceOperation.addCondition( - NoFailedDependenciesCondition(ignoreCancellations: false) - ) - createDeviceOperation.inject(from: accountOperation) + case .unset: + finish(result: .success(nil)) + } + } + } - let saveSettingsOperation = getSaveSettingsOperation() - saveSettingsOperation.addCondition( - NoFailedDependenciesCondition(ignoreCancellations: false) - ) + override func operationDidCancel() { + tasks.forEach { $0.cancel() } + tasks.removeAll() + } - saveSettingsOperation.injectMany(context: SetAccountContext()) - .inject(from: accountOperation, assignOutputTo: \.accountData) - .inject(from: createDeviceOperation, via: { context, output in - let (privateKey, device) = output + // MARK: - Private - context.privateKey = privateKey - context.device = device - }) - .reduce() + /** + Begin logout flow by performing the following steps: - saveSettingsOperation.onFinish { operation, error in - self.completeOperation(accountData: operation.output) - } + 1. Delete currently logged in device from the API if device is logged in. + 2. Transition device state to logged out state. + 3. Remove system VPN configuration if exists. + 4. Reset tunnel status to disconnected state. - return [accountOperation, createDeviceOperation, saveSettingsOperation] - } ?? [] + Does nothing if device is already logged out. + */ + private func startLogoutFlow(completion: @escaping () -> Void) { + switch interactor.deviceState { + case let .loggedIn(accountData, deviceData): + deleteDevice(accountNumber: accountData.number, deviceIdentifier: deviceData.identifier) { [self] error in + unsetDeviceState(completion: completion) + } - var enqueueOperations: [Operation] = [deleteDeviceOperation, unsetDeviceStateOperation] - .compactMap { $0 } - enqueueOperations.append(contentsOf: setupAccountOperations) + case .revoked: + unsetDeviceState(completion: completion) - if setupAccountOperations.isEmpty { - let finishingOperation = BlockOperation() - finishingOperation.completionBlock = { [weak self] in - self?.completeOperation(accountData: nil) - } - finishingOperation.addDependencies(enqueueOperations) - enqueueOperations.append(finishingOperation) + case .loggedOut: + completion() } - - children = enqueueOperations - operationQueue.addOperations(enqueueOperations, waitUntilFinished: false) } - override func operationDidCancel() { - operationQueue.cancelAllOperations() + /** + Begin login flow with a new account and performing the following steps: + + 1. Create new account via API. + 2. Call `continueLoginFlow()` passing the result of account creation request. + */ + private func startNewAccountFlow(completion: @escaping (Result<StoredAccountData, Error>) -> Void) { + createAccount { [self] result in + continueLoginFlow(result, completion: completion) + } } - // MARK: - Private + /** + Begin login flow with an existing account by performing the following steps: - private func completeOperation(accountData: StoredAccountData?) { - guard !isCancelled else { - finish(result: .failure(OperationError.cancelled)) - return + 1. Retrieve existing account from the API. + 2. Call `continueLoginFlow()` passing the result of account retrieval request. + */ + private func startExistingAccountFlow( + accountNumber: String, + completion: @escaping (Result<StoredAccountData, Error>) -> Void + ) { + getAccount(accountNumber: accountNumber) { [self] result in + continueLoginFlow(result, completion: completion) } + } + + /** + Continue login flow after receiving account data as a part of creating new or retrieving existing account from + the API by performing the following steps: - let errors = children.compactMap { operation -> Error? in - return (operation as? AsyncOperation)?.error + 1. Store last used account number. + 2. Create new device with the API. + 3. Persist settings. + */ + private func continueLoginFlow( + _ result: Result<StoredAccountData, Error>, + completion: @escaping (Result<StoredAccountData, Error>) -> Void + ) { + do { + let accountData = try result.get() + + storeLastUsedAccount(accountNumber: accountData.number) + + createDevice(accountNumber: accountData.number) { [self] result in + completion(result.map { newDevice in + storeSettings(accountData: accountData, newDevice: newDevice) + + return accountData + }) + } + } catch { + completion(.failure(error)) } + } + + /// Store last used account number in settings. + /// Errors are ignored but logged. + private func storeLastUsedAccount(accountNumber: String) { + logger.debug("Store last used account.") - if let error = errors.first { - finish(result: .failure(error)) - } else { - finish(result: .success(accountData)) + do { + try SettingsManager.setLastUsedAccount(accountNumber) + } catch { + logger.error(error: error, message: "Failed to store last used account number.") } } - private func getAccountDataOperation() -> ResultOperation<StoredAccountData>? { - switch action { - case .new: - return getCreateAccountOperation() + /// Store account data and newly created device in settings and transition device state to logged in state. + private func storeSettings(accountData: StoredAccountData, newDevice: NewDevice) { + logger.debug("Saving settings...") + + // Create stored device data. + let restDevice = newDevice.device + let storedDeviceData = StoredDeviceData( + creationDate: restDevice.created, + identifier: restDevice.id, + name: restDevice.name, + hijackDNS: restDevice.hijackDNS, + ipv4Address: restDevice.ipv4Address, + ipv6Address: restDevice.ipv6Address, + wgKeyData: StoredWgKeyData( + creationDate: Date(), + privateKey: newDevice.privateKey + ) + ) - case let .existing(accountNumber): - return getExistingAccountOperation(accountNumber: accountNumber) + // Reset tunnel settings. + interactor.setSettings(TunnelSettingsV2(), persist: true) - case .unset: - return nil - } + // Transition device state to logged in. + interactor.setDeviceState(.loggedIn(accountData, storedDeviceData), persist: true) } - private func getCreateAccountOperation() -> ResultBlockOperation<StoredAccountData> { - return ResultBlockOperation<StoredAccountData>(dispatchQueue: dispatchQueue) { finish -> Cancellable in - self.logger.debug("Create new account...") + /// Create new account and produce `StoredAccountData` upon success. + private func createAccount(completion: @escaping (Result<StoredAccountData, Error>) -> Void) { + logger.debug("Create new account...") - return self.accountsProxy.createAccount(retryStrategy: .default) { result in + let task = accountsProxy.createAccount(retryStrategy: .default) { [self] result in + dispatchQueue.async { [self] in let result = result.inspectError { error in guard !error.isOperationCancellationError else { return } - self.logger.error( + logger.error( error: error, message: "Failed to create new account." ) }.map { newAccountData -> StoredAccountData in - self.logger.debug("Created new account.") + logger.debug("Created new account.") return StoredAccountData( identifier: newAccountData.id, @@ -205,26 +229,26 @@ class SetAccountOperation: ResultOperation<StoredAccountData?> { ) } - finish(result) + completion(result) } } + + tasks.append(task) } - private func getExistingAccountOperation(accountNumber: String) -> ResultOperation<StoredAccountData> { - return ResultBlockOperation<StoredAccountData>(dispatchQueue: dispatchQueue) { finish -> Cancellable in - self.logger.debug("Request account data...") + /// Get account data from the API and produce `StoredAccountData` upon success. + private func getAccount(accountNumber: String, completion: @escaping (Result<StoredAccountData, Error>) -> Void) { + logger.debug("Request account data...") - return self.accountsProxy - .getAccountData(accountNumber: accountNumber, retryStrategy: .default) { result in + let task = accountsProxy + .getAccountData(accountNumber: accountNumber, retryStrategy: .default) { [self] result in + dispatchQueue.async { [self] in let result = result.inspectError { error in guard !error.isOperationCancellationError else { return } - self.logger.error( - error: error, - message: "Failed to receive account data." - ) + logger.error(error: error, message: "Failed to receive account data.") }.map { accountData -> StoredAccountData in - self.logger.debug("Received account data.") + logger.debug("Received account data.") return StoredAccountData( identifier: accountData.id, @@ -233,151 +257,116 @@ class SetAccountOperation: ResultOperation<StoredAccountData?> { ) } - finish(result) + completion(result) } - } - } + } - private func getDeleteDeviceOperation() -> AsyncBlockOperation? { - guard case let .loggedIn(accountData, deviceData) = interactor.deviceState else { - return nil - } + tasks.append(task) + } - let operation = AsyncBlockOperation(dispatchQueue: dispatchQueue) { finish -> Cancellable in - self.logger.debug("Delete current device...") + /// Delete device from API. + private func deleteDevice(accountNumber: String, deviceIdentifier: String, completion: @escaping (Error?) -> Void) { + logger.debug("Delete current device...") - return self.devicesProxy.deleteDevice( - accountNumber: accountData.number, - identifier: deviceData.identifier, - retryStrategy: .default - ) { result in + let task = devicesProxy.deleteDevice( + accountNumber: accountNumber, + identifier: deviceIdentifier, + retryStrategy: .default + ) { [self] result in + dispatchQueue.async { [self] in switch result { case let .success(isDeleted): - self.logger.debug(isDeleted ? "Deleted device." : "Device is already deleted.") + logger.debug(isDeleted ? "Deleted device." : "Device is already deleted.") - case let .failure(error) where !error.isOperationCancellationError: - self.logger.error( - error: error, - message: "Failed to delete device." - ) - - default: - break + case let .failure(error): + if !error.isOperationCancellationError { + logger.error(error: error, message: "Failed to delete device.") + } } - finish(result.error) + completion(result.error) } } - return operation + tasks.append(task) } - private func getUnsetDeviceStateOperation() -> AsyncBlockOperation { - return AsyncBlockOperation(dispatchQueue: dispatchQueue) { finish in - // Tell the caller to unsubscribe from VPN status notifications. - self.interactor.prepareForVPNConfigurationDeletion() - - // Reset tunnel and device state. - self.interactor.updateTunnelStatus { tunnelStatus in - tunnelStatus = TunnelStatus() - tunnelStatus.state = .disconnected - } - self.interactor.setDeviceState(.loggedOut, persist: true) + /** + Transitions device state into logged out state by performing the following tasks: - // Finish immediately if tunnel provider is not set. - guard let tunnel = self.interactor.tunnel else { - finish(nil) - return - } + 1. Prepare tunnel manager for removal of VPN configuration. In response tunnel manager stops processing VPN status + notifications coming from VPN configuration. + 2. Reset device staate to logged out and persist it. + 3. Remove VPN configuration and release an instance of `Tunnel` object. + */ + private func unsetDeviceState(completion: @escaping () -> Void) { + // Tell the caller to unsubscribe from VPN status notifications. + interactor.prepareForVPNConfigurationDeletion() - // Remove VPN configuration. - tunnel.removeFromPreferences { error in - self.dispatchQueue.async { - // Ignore error but log it. - if let error { - self.logger.error( - error: error, - message: "Failed to remove VPN configuration." - ) - } + // Reset tunnel and device state. + interactor.updateTunnelStatus { tunnelStatus in + tunnelStatus = TunnelStatus() + tunnelStatus.state = .disconnected + } + interactor.setDeviceState(.loggedOut, persist: true) - self.interactor.setTunnel(nil, shouldRefreshTunnelState: false) + // Finish immediately if tunnel provider is not set. + guard let tunnel = interactor.tunnel else { + completion() + return + } - finish(nil) + // Remove VPN configuration. + tunnel.removeFromPreferences { [self] error in + dispatchQueue.async { [self] in + // Ignore error but log it. + if let error { + logger.error( + error: error, + message: "Failed to remove VPN configuration." + ) } + + interactor.setTunnel(nil, shouldRefreshTunnelState: false) + + completion() } } } - private func getCreateDeviceOperation() -> TransformOperation<StoredAccountData, (PrivateKey, Device)> { - return TransformOperation< - StoredAccountData, - (PrivateKey, Device) - >(dispatchQueue: dispatchQueue) { storedAccountData, finish -> Cancellable in - self.logger.debug("Store last used account.") + /// Create new private key and create new device via API. + private func createDevice(accountNumber: String, completion: @escaping (Result<NewDevice, Error>) -> Void) { + let privateKey = PrivateKey() - do { - try SettingsManager.setLastUsedAccount(storedAccountData.number) - } catch { - self.logger.error( - error: error, - message: "Failed to store last used account number." - ) - } - - self.logger.debug("Create device...") + let request = REST.CreateDeviceRequest( + publicKey: privateKey.publicKey, + hijackDNS: false + ) - let privateKey = PrivateKey() + logger.debug("Create device...") - let request = REST.CreateDeviceRequest( - publicKey: privateKey.publicKey, - hijackDNS: false - ) + let task = devicesProxy + .createDevice(accountNumber: accountNumber, request: request, retryStrategy: .default) { [self] result in + dispatchQueue.async { [self] in + let result = result + .map { device in + return NewDevice(privateKey: privateKey, device: device) + } + .inspectError { error in + logger.error(error: error, message: "Failed to create device.") + } - return self.devicesProxy.createDevice( - accountNumber: storedAccountData.number, - request: request, - retryStrategy: .default - ) { result in - let result = result - .map { device in - return (privateKey, device) - } - .inspectError { error in - self.logger.error(error: error, message: "Failed to create device.") - } - - finish(result) + completion(result) + } } - } - } - - private func getSaveSettingsOperation() -> TransformOperation<SetAccountResult, StoredAccountData> { - return TransformOperation<SetAccountResult, StoredAccountData>(dispatchQueue: dispatchQueue) { input in - self.logger.debug("Saving settings...") - let device = input.device - let newDeviceState = DeviceState.loggedIn( - input.accountData, - StoredDeviceData( - creationDate: device.created, - identifier: device.id, - name: device.name, - hijackDNS: device.hijackDNS, - ipv4Address: device.ipv4Address, - ipv6Address: device.ipv6Address, - wgKeyData: StoredWgKeyData( - creationDate: Date(), - lastRotationAttemptDate: nil, - privateKey: input.privateKey - ) - ) - ) - - self.interactor.setSettings(TunnelSettingsV2(), persist: true) - self.interactor.setDeviceState(newDeviceState, persist: true) + tasks.append(task) + } - return input.accountData - } + /// Struct that holds a private key that was used for creating a new device on the API along with the successful + /// response from the API. + private struct NewDevice { + var privateKey: PrivateKey + var device: Device } } diff --git a/ios/Operations/InputInjectionBuilder.swift b/ios/Operations/InputInjectionBuilder.swift deleted file mode 100644 index 0c87ab767a..0000000000 --- a/ios/Operations/InputInjectionBuilder.swift +++ /dev/null @@ -1,96 +0,0 @@ -// -// InputInjectionBuilder.swift -// Operations -// -// Created by pronebird on 09/06/2022. -// Copyright © 2022 Mullvad VPN AB. All rights reserved. -// - -import Foundation - -public protocol OperationInputContext { - associatedtype Input - - func reduce() -> Input? -} - -public final class InputInjectionBuilder<OperationType, Context> - where OperationType: InputOperation -{ - public typealias InputBlock = (inout Context) -> Void - - private let operation: OperationType - private var context: Context - private var inputBlocks: [InputBlock] = [] - - public init(operation: OperationType, context: Context) { - self.operation = operation - self.context = context - } - - public func inject<T>( - from dependency: T, - assignOutputTo keyPath: WritableKeyPath<Context, T.Output?> - ) -> Self - where T: OutputOperation - { - return inject(from: dependency) { context, output in - context[keyPath: keyPath] = output - } - } - - public func inject<T>( - from dependency: T, - via block: @escaping (inout Context, T.Output) -> Void - ) -> Self - where T: OutputOperation - { - inputBlocks.append { context in - if let output = dependency.output { - block(&context, output) - } - } - - operation.addDependency(dependency) - - return self - } - - public func injectResult<T, Success>( - from dependency: T, - via block: @escaping (inout Context, Result<T.Output, Error>) -> Void - ) -> Self - where T: ResultOperation<Success> - { - inputBlocks.append { context in - if let result = dependency.result { - block(&context, result) - } - } - - operation.addDependency(dependency) - - return self - } - - public func reduce(_ reduceBlock: @escaping (Context) -> OperationType.Input?) { - operation.setInputBlock { - for inputBlock in self.inputBlocks { - inputBlock(&self.context) - } - - return reduceBlock(self.context) - } - } -} - -extension InputInjectionBuilder - where Context: OperationInputContext, - Context.Input == OperationType.Input -{ - public func reduce() { - reduce { context in - return context.reduce() - } - } -} diff --git a/ios/Operations/InputOperation.swift b/ios/Operations/InputOperation.swift deleted file mode 100644 index 9c141671ec..0000000000 --- a/ios/Operations/InputOperation.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// InputOperation.swift -// Operations -// -// Created by pronebird on 09/06/2022. -// Copyright © 2022 Mullvad VPN AB. All rights reserved. -// - -import Foundation - -public protocol InputOperation: Operation { - associatedtype Input - - var input: Input? { get } - - func setInputBlock(_ block: @escaping () -> Input?) - - func inject<T>(from dependency: T) - where T: OutputOperation, T.Output == Input - - func inject<T>(from dependency: T, via block: @escaping (T.Output) -> Input) - where T: OutputOperation -} - -extension InputOperation { - public func inject<T>(from dependency: T) where T: OutputOperation, T.Output == Input { - inject(from: dependency, via: { $0 }) - } - - public func inject<T>(from dependency: T, via block: @escaping (T.Output) -> Input) - where T: OutputOperation - { - setInputBlock { - return dependency.output.map { value in - return block(value) - } - } - addDependency(dependency) - } - - public func injectMany<Context>(context: Context) -> InputInjectionBuilder<Self, Context> { - return InputInjectionBuilder( - operation: self, - context: context - ) - } -} diff --git a/ios/Operations/TransformOperation.swift b/ios/Operations/TransformOperation.swift deleted file mode 100644 index 22946cbe1a..0000000000 --- a/ios/Operations/TransformOperation.swift +++ /dev/null @@ -1,110 +0,0 @@ -// -// TransformOperation.swift -// Operations -// -// Created by pronebird on 31/05/2022. -// Copyright © 2022 Mullvad VPN AB. All rights reserved. -// - -import Foundation -import protocol MullvadTypes.Cancellable - -public final class TransformOperation<Input, Output>: ResultOperation<Output>, InputOperation { - public typealias InputBlock = () -> Input? - - private let nslock = NSLock() - - public var input: Input? { - return _input - } - - private var __input: Input? - private var _input: Input? { - get { - nslock.lock() - defer { nslock.unlock() } - return __input - } - set { - nslock.lock() - __input = newValue - nslock.unlock() - } - } - - private var inputBlock: InputBlock? - - private var executor: ((Input, @escaping (Result<Output, Error>) -> Void) -> Cancellable?)? - private var cancellableTask: Cancellable? - - public init( - dispatchQueue: DispatchQueue? = nil, - input: Input? = nil, - block: @escaping (_ input: Input, _ finish: @escaping (Result<Output, Error>) -> Void) -> Void - ) { - super.init(dispatchQueue: dispatchQueue) - __input = input - executor = { input, finish in - block(input, finish) - return nil - } - } - - public init( - dispatchQueue: DispatchQueue? = nil, - input: Input? = nil, - throwingBlock: @escaping (_ input: Input) throws -> Output - ) { - super.init(dispatchQueue: dispatchQueue) - __input = input - executor = { input, finish in - finish(Result { try throwingBlock(input) }) - return nil - } - } - - public init( - dispatchQueue: DispatchQueue? = nil, - input: Input? = nil, - cancellableTask: @escaping (_ input: Input, _ finish: @escaping (Result<Output, Error>) -> Void) -> Cancellable - ) { - super.init(dispatchQueue: dispatchQueue) - __input = input - executor = cancellableTask - } - - override public func main() { - if let inputBlock { - _input = inputBlock() - } - - guard let inputValue = _input else { - finish(result: .failure(OperationError.unsatisfiedRequirement)) - return - } - - let executor = executor - self.executor = nil - - assert(executor != nil) - - cancellableTask = executor?(inputValue, self.finish) - } - - override public func operationDidCancel() { - cancellableTask?.cancel() - } - - override public func operationDidFinish() { - executor = nil - cancellableTask = nil - } - - // MARK: - Input injection - - public func setInputBlock(_ block: @escaping () -> Input?) { - dispatchQueue.async { - self.inputBlock = block - } - } -} diff --git a/ios/OperationsTests/OperationInputInjectionTests.swift b/ios/OperationsTests/OperationInputInjectionTests.swift deleted file mode 100644 index 119c699954..0000000000 --- a/ios/OperationsTests/OperationInputInjectionTests.swift +++ /dev/null @@ -1,89 +0,0 @@ -// -// OperationInputInjectionTests.swift -// MullvadVPNTests -// -// Created by pronebird on 09/06/2022. -// Copyright © 2022 Mullvad VPN AB. All rights reserved. -// - -import Operations -import XCTest - -class OperationInputInjectionTests: XCTestCase { - func testInject() throws { - let provider = ResultBlockOperation<Int> { - return 1 - } - - let consumer = TransformOperation<Int, Int> { input in - return input + 1 - } - - consumer.inject(from: provider) - - let operationQueue = AsyncOperationQueue() - - operationQueue.addOperations([provider, consumer], waitUntilFinished: true) - - XCTAssertEqual(consumer.output, 2) - } - - func testInjectVia() throws { - let provider = ResultBlockOperation<Int> { - return 1 - } - - let consumer = TransformOperation<String, Int> { input in - return Int(input)! - } - - consumer.inject(from: provider) { output in - return "\(output)" - } - - let operationQueue = AsyncOperationQueue() - - operationQueue.addOperations([provider, consumer], waitUntilFinished: true) - - XCTAssertEqual(consumer.output, 1) - } - - func testInjectMany() throws { - struct Context: OperationInputContext { - var a: Int? - var b: Int? - - func reduce() -> Int? { - guard let a, let b else { return nil } - - return a + b - } - } - - let operationQueue = AsyncOperationQueue() - - let providerA = ResultBlockOperation<Int> { - return 1 - } - - let providerB = ResultBlockOperation<Int> { - return 2 - } - - let consumer = TransformOperation<Int, String> { input in - return "\(input)" - } - - consumer.injectMany(context: Context()) - .inject(from: providerA, assignOutputTo: \.a) - .inject(from: providerB, assignOutputTo: \.b) - .reduce() - - operationQueue.addOperations( - [providerA, providerB, consumer], - waitUntilFinished: true - ) - - XCTAssertEqual(consumer.output, "3") - } -} diff --git a/ios/OperationsTests/TransformOperationTests.swift b/ios/OperationsTests/TransformOperationTests.swift deleted file mode 100644 index c16bcd3be1..0000000000 --- a/ios/OperationsTests/TransformOperationTests.swift +++ /dev/null @@ -1,93 +0,0 @@ -// -// TransformOperationTests.swift -// OperationsTests -// -// Created by pronebird on 26/04/2023. -// Copyright © 2023 Mullvad VPN AB. All rights reserved. -// - -import MullvadTypes -import Operations -import XCTest - -final class TransformOperationTests: XCTestCase { - let operationQueue = AsyncOperationQueue() - - func testBlockTransformOperation() { - let finishExpectation = expectation(description: "Should finish") - - let transform = TransformOperation(input: Int.zero) { input, finish in - finish(.success(input + 1)) - } - - transform.onFinish { op, error in - XCTAssertEqual(op.result?.value, 1) - - finishExpectation.fulfill() - } - - operationQueue.addOperation(transform) - - waitForExpectations(timeout: 1) - } - - func testThrowingBlockTransformOperation() { - let finishExpectation = expectation(description: "Should finish") - - let transform = TransformOperation(input: Int.zero) { value in - throw URLError(.badURL) - } - - transform.onFinish { op, error in - XCTAssertEqual(error as? URLError, URLError(.badURL)) - - finishExpectation.fulfill() - } - - operationQueue.addOperation(transform) - - waitForExpectations(timeout: 1) - } - - func testCancellableTaskBlockTransformOperation() { - let finishExpectation = expectation(description: "Should finish") - - let transform = TransformOperation<Int, Int>(input: Int.zero) { _, finish -> Cancellable in - return AnyCancellable { - finish(.failure(URLError(.cancelled))) - } - } - - transform.onStart { op in - op.cancel() - } - - transform.onFinish { op, error in - XCTAssertEqual(error as? URLError, URLError(.cancelled)) - - finishExpectation.fulfill() - } - - operationQueue.addOperation(transform) - - waitForExpectations(timeout: 1) - } - - func testShouldFailWithUnsatisfiedRequirement() { - let finishExpectation = expectation(description: "Should finish") - - let transform = TransformOperation<Int, Int> { input, finish in - finish(.success(input)) - } - - transform.onFinish { _, error in - XCTAssertEqual(error as? OperationError, .unsatisfiedRequirement) - - finishExpectation.fulfill() - } - - operationQueue.addOperation(transform) - - waitForExpectations(timeout: 1) - } -} |
