diff options
29 files changed, 795 insertions, 1803 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 72029955a0..0f47216ca9 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -8,28 +8,13 @@ /* Begin PBXBuildFile section */ 5801C9A527A14B2A0031566A /* TunnelManagerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5801C9A427A14B2A0031566A /* TunnelManagerState.swift */; }; - 5806766B27048E3C00C858CB /* AnyOptional.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1337026D2BE9C00CC316B /* AnyOptional.swift */; }; - 5806766C27048E3E00C858CB /* AnyOptional.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1337026D2BE9C00CC316B /* AnyOptional.swift */; }; 5806766D27048E5500C858CB /* KeychainMatchLimit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDFC24533A5500CB0F5B /* KeychainMatchLimit.swift */; }; 5806766E27048E5600C858CB /* KeychainMatchLimit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDFC24533A5500CB0F5B /* KeychainMatchLimit.swift */; }; - 5806766F27048E6900C858CB /* Promise.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA8AE26B9492500B8C587 /* Promise.swift */; }; - 5806767027048E6A00C858CB /* Promise.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA8AE26B9492500B8C587 /* Promise.swift */; }; - 5806767227048E7400C858CB /* Promise+Optional.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1337426D2BEC400CC316B /* Promise+Optional.swift */; }; - 5806767327048E7400C858CB /* Promise+Optional.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1337426D2BEC400CC316B /* Promise+Optional.swift */; }; - 5806767427048E7400C858CB /* Promise+Optional.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1337426D2BEC400CC316B /* Promise+Optional.swift */; }; - 5806767527048E7C00C858CB /* Promise+Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1338026D2BF5C00CC316B /* Promise+Result.swift */; }; - 5806767627048E7D00C858CB /* Promise+Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1338026D2BF5C00CC316B /* Promise+Result.swift */; }; - 5806767827048E7E00C858CB /* Promise+Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1338026D2BF5C00CC316B /* Promise+Result.swift */; }; 5806767927048E8800C858CB /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDF6245088E100CB0F5B /* Keychain.swift */; }; 5806767A27048E8800C858CB /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDF6245088E100CB0F5B /* Keychain.swift */; }; 5806767B27048E8900C858CB /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDF6245088E100CB0F5B /* Keychain.swift */; }; 5806767C27048E9B00C858CB /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CE5E7B224146470008646E /* PacketTunnelProvider.swift */; }; - 5806767E27048EBE00C858CB /* Promise+ReceiveOn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1337826D2BEDD00CC316B /* Promise+ReceiveOn.swift */; }; - 5806767F27048EC000C858CB /* Promise+ReceiveOn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1337826D2BEDD00CC316B /* Promise+ReceiveOn.swift */; }; - 5806768027048ECF00C858CB /* Promise+ReceiveOn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1337826D2BEDD00CC316B /* Promise+ReceiveOn.swift */; }; 5806768127048EE000C858CB /* KeychainMatchLimit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDFC24533A5500CB0F5B /* KeychainMatchLimit.swift */; }; - 5806768227048F6800C858CB /* AnyOptional.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1337026D2BE9C00CC316B /* AnyOptional.swift */; }; - 5806768327048F7A00C858CB /* Promise.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA8AE26B9492500B8C587 /* Promise.swift */; }; 5807483B27DB8A980020ECBF /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = 5807483A27DB8A980020ECBF /* WireGuardKitTypes */; }; 5807E2C02432038B00F5FF30 /* String+Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2BF2432038B00F5FF30 /* String+Split.swift */; }; 5807E2C2243203D000F5FF30 /* StringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2C1243203D000F5FF30 /* StringTests.swift */; }; @@ -58,7 +43,6 @@ 5819C2152726CC9400D6EC38 /* DataSourceSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587EB66F27143B6500123C75 /* DataSourceSnapshot.swift */; }; 5819C2172729595500D6EC38 /* SettingsAddDNSEntryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5819C2162729595500D6EC38 /* SettingsAddDNSEntryCell.swift */; }; 581FC4FA2695ACE100AA97BA /* Account.strings in Resources */ = {isa = PBXBuildFile; fileRef = 581FC4F82695ACE100AA97BA /* Account.strings */; }; - 5820674926E63EC900655B05 /* Promise+BackgroundTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820674826E63EC800655B05 /* Promise+BackgroundTask.swift */; }; 5820674E26E6510200655B05 /* REST.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820674D26E6510200655B05 /* REST.swift */; }; 5820675026E6514100655B05 /* HTTP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820674F26E6514100655B05 /* HTTP.swift */; }; 5820675526E6528200655B05 /* RelayCacheError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA87926B024F900B8C587 /* RelayCacheError.swift */; }; @@ -69,8 +53,6 @@ 5820675B26E6576800655B05 /* RelayCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820675A26E6576800655B05 /* RelayCache.swift */; }; 5820675C26E6576800655B05 /* RelayCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820675A26E6576800655B05 /* RelayCache.swift */; }; 5820675E26E6839900655B05 /* PresentAlertOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820675D26E6839900655B05 /* PresentAlertOperation.swift */; }; - 5820676026E75A4D00655B05 /* Promise+Delay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820675F26E75A4D00655B05 /* Promise+Delay.swift */; }; - 5820676126E75A4D00655B05 /* Promise+Delay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820675F26E75A4D00655B05 /* Promise+Delay.swift */; }; 5820676226E75D8500655B05 /* REST.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820674D26E6510200655B05 /* REST.swift */; }; 5820676426E771DB00655B05 /* TunnelManagerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820676326E771DB00655B05 /* TunnelManagerError.swift */; }; 5820676826E79E7B00655B05 /* Result+UIBackgroundFetchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820676726E79E7B00655B05 /* Result+UIBackgroundFetchResult.swift */; }; @@ -100,9 +82,6 @@ 5840BE35279EDB16002836BA /* OperationCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5840BE34279EDB16002836BA /* OperationCompletion.swift */; }; 584592612639B4A200EF967F /* ConsentContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584592602639B4A200EF967F /* ConsentContentView.swift */; }; 5846226526E0D9630035F7C2 /* ProductsRequestOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846226426E0D9630035F7C2 /* ProductsRequestOperation.swift */; }; - 5846226726E0DF960035F7C2 /* Promise+OperationQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846226626E0DF960035F7C2 /* Promise+OperationQueue.swift */; }; - 5846226826E0DF960035F7C2 /* Promise+OperationQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846226626E0DF960035F7C2 /* Promise+OperationQueue.swift */; }; - 5846226A26E0E6FA0035F7C2 /* ReceiptRefreshOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846226926E0E6FA0035F7C2 /* ReceiptRefreshOperation.swift */; }; 5846227126E229F20035F7C2 /* AppStoreSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846227026E229F20035F7C2 /* AppStoreSubscription.swift */; }; 5846227326E22A160035F7C2 /* AppStorePaymentObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846227226E22A160035F7C2 /* AppStorePaymentObserver.swift */; }; 5846227726E22A7C0035F7C2 /* AppStorePaymentManagerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846227626E22A7C0035F7C2 /* AppStorePaymentManagerDelegate.swift */; }; @@ -157,9 +136,6 @@ 585DA8A326B14E0D00B8C587 /* ServerRelaysResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA88326B0270700B8C587 /* ServerRelaysResponse.swift */; }; 585DA8A526B14EE000B8C587 /* PacketTunnelStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89826B0329200B8C587 /* PacketTunnelStatus.swift */; }; 585DA8A626B14F5100B8C587 /* SSLPinningURLSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584789DF26529D72000E45FB /* SSLPinningURLSessionDelegate.swift */; }; - 5860392926DCE7AB00554C79 /* PromiseCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5860392826DCE7AB00554C79 /* PromiseCompletion.swift */; }; - 5860392A26DCE7AB00554C79 /* PromiseCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5860392826DCE7AB00554C79 /* PromiseCompletion.swift */; }; - 5860392B26DCEE6300554C79 /* PromiseCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5860392826DCE7AB00554C79 /* PromiseCompletion.swift */; }; 5862805422428EF100F5A6E1 /* TranslucentButtonBlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.swift */; }; 58655DCE27DA0A5D00911834 /* TunnelMonitorConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58655DCD27DA0A5D00911834 /* TunnelMonitorConfiguration.swift */; }; 58655DCF27DA0A5D00911834 /* TunnelMonitorConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58655DCD27DA0A5D00911834 /* TunnelMonitorConfiguration.swift */; }; @@ -206,9 +182,6 @@ 5888AD87227B17950051EB06 /* SelectLocationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5888AD86227B17950051EB06 /* SelectLocationViewController.swift */; }; 588CD8BB275A0A0B00CF902E /* RESTRequestAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588CD8BA275A0A0B00CF902E /* RESTRequestAdapter.swift */; }; 588D2FE3248AC27F00E313F7 /* AsyncOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E973DD24850EB600096F90 /* AsyncOperation.swift */; }; - 588DD76B26FCB49E006F6233 /* Cancellable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588DD76A26FCB49E006F6233 /* Cancellable.swift */; }; - 588DD76C26FCB49E006F6233 /* Cancellable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588DD76A26FCB49E006F6233 /* Cancellable.swift */; }; - 588DD76D26FCB4A2006F6233 /* Cancellable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588DD76A26FCB49E006F6233 /* Cancellable.swift */; }; 58906DE02445C7A5002F0673 /* NEProviderStopReason+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58906DDF2445C7A5002F0673 /* NEProviderStopReason+Debug.swift */; }; 58907D9524D17B4E00CFC3F5 /* DisconnectSplitButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58907D9424D17B4E00CFC3F5 /* DisconnectSplitButton.swift */; }; 5891BF1C25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5891BF1B25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift */; }; @@ -224,7 +197,6 @@ 58A1AA8C23F5584C009F7EA6 /* ConnectionPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A1AA8B23F5584B009F7EA6 /* ConnectionPanelView.swift */; }; 58A8055E2716EA6700681642 /* AnyIPAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584D26BE270C550B004EA533 /* AnyIPAddress.swift */; }; 58A8BE81239FBE62006B74AC /* IPEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58561C98239A5D1500BD6B5E /* IPEndpoint.swift */; }; - 58A94AE626D23C3D001CB97C /* PromiseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A94AE526D23C3D001CB97C /* PromiseTests.swift */; }; 58A99ED3240014A0006599E9 /* ConsentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A99ED2240014A0006599E9 /* ConsentViewController.swift */; }; 58ACF6492655365700ACE4B7 /* PreferencesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ACF6482655365700ACE4B7 /* PreferencesViewController.swift */; }; 58ACF64B26553C3F00ACE4B7 /* SettingsSwitchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ACF64A26553C3F00ACE4B7 /* SettingsSwitchCell.swift */; }; @@ -271,18 +243,14 @@ 58D67A0A26D7AE3300557C3C /* OSLogHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5823FA4F26CA690600283BF8 /* OSLogHandler.swift */; }; 58DF28A52417CB4B00E836B0 /* AppStorePaymentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DF28A42417CB4B00E836B0 /* AppStorePaymentManager.swift */; }; 58E0A98827C8F46300FE6BDD /* Tunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E0A98727C8F46300FE6BDD /* Tunnel.swift */; }; - 58E1336926D2BE3700CC316B /* PromiseObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1336826D2BE3700CC316B /* PromiseObserver.swift */; }; - 58E1336A26D2BE3700CC316B /* PromiseObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1336826D2BE3700CC316B /* PromiseObserver.swift */; }; - 58E1336B26D2BE3700CC316B /* PromiseObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1336826D2BE3700CC316B /* PromiseObserver.swift */; }; - 58E1336D26D2BE7500CC316B /* AnyResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1336C26D2BE7500CC316B /* AnyResult.swift */; }; - 58E1336E26D2BE7500CC316B /* AnyResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1336C26D2BE7500CC316B /* AnyResult.swift */; }; - 58E1336F26D2BE7500CC316B /* AnyResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1336C26D2BE7500CC316B /* AnyResult.swift */; }; 58E20771274672CA00DE5D77 /* LaunchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E20770274672CA00DE5D77 /* LaunchViewController.swift */; }; 58E6771F24ADFE7800AA26E7 /* SettingsNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E6771E24ADFE7800AA26E7 /* SettingsNavigationController.swift */; }; 58EE2E3A272FF814003BFF93 /* SettingsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EE2E38272FF814003BFF93 /* SettingsDataSource.swift */; }; 58EE2E3B272FF814003BFF93 /* SettingsDataSourceDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EE2E39272FF814003BFF93 /* SettingsDataSourceDelegate.swift */; }; 58EF580B25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EF580A25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift */; }; 58EF581125D69DB400AEBA94 /* StatusImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EF581025D69DB400AEBA94 /* StatusImageView.swift */; }; + 58F1311327E09B00007AC5BC /* Cancellable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F1311227E09B00007AC5BC /* Cancellable.swift */; }; + 58F1311527E0B2AB007AC5BC /* Result+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F1311427E0B2AB007AC5BC /* Result+Extensions.swift */; }; 58F19E35228C15BA00C7710B /* SpinnerActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F19E34228C15BA00C7710B /* SpinnerActivityIndicatorView.swift */; }; 58F2E144276A13F300A79513 /* StartTunnelOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F2E143276A13F300A79513 /* StartTunnelOperation.swift */; }; 58F2E146276A2C9900A79513 /* StopTunnelOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F2E145276A2C9900A79513 /* StopTunnelOperation.swift */; }; @@ -401,12 +369,10 @@ 5819C2132726CC8D00D6EC38 /* DataSourceSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataSourceSnapshotTests.swift; sourceTree = "<group>"; }; 5819C2162729595500D6EC38 /* SettingsAddDNSEntryCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAddDNSEntryCell.swift; sourceTree = "<group>"; }; 581FC4F92695ACE100AA97BA /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Account.strings; sourceTree = "<group>"; }; - 5820674826E63EC800655B05 /* Promise+BackgroundTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+BackgroundTask.swift"; sourceTree = "<group>"; }; 5820674D26E6510200655B05 /* REST.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = REST.swift; sourceTree = "<group>"; }; 5820674F26E6514100655B05 /* HTTP.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTP.swift; sourceTree = "<group>"; }; 5820675A26E6576800655B05 /* RelayCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayCache.swift; sourceTree = "<group>"; }; 5820675D26E6839900655B05 /* PresentAlertOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentAlertOperation.swift; sourceTree = "<group>"; }; - 5820675F26E75A4D00655B05 /* Promise+Delay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+Delay.swift"; sourceTree = "<group>"; }; 5820676326E771DB00655B05 /* TunnelManagerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelManagerError.swift; sourceTree = "<group>"; }; 5820676726E79E7B00655B05 /* Result+UIBackgroundFetchResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Result+UIBackgroundFetchResult.swift"; sourceTree = "<group>"; }; 5823FA4F26CA690600283BF8 /* OSLogHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLogHandler.swift; sourceTree = "<group>"; }; @@ -433,8 +399,6 @@ 584592602639B4A200EF967F /* ConsentContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsentContentView.swift; sourceTree = "<group>"; }; 5845F841236CBACD00B2D93C /* TunnelIPC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelIPC.swift; sourceTree = "<group>"; }; 5846226426E0D9630035F7C2 /* ProductsRequestOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductsRequestOperation.swift; sourceTree = "<group>"; }; - 5846226626E0DF960035F7C2 /* Promise+OperationQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+OperationQueue.swift"; sourceTree = "<group>"; }; - 5846226926E0E6FA0035F7C2 /* ReceiptRefreshOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptRefreshOperation.swift; sourceTree = "<group>"; }; 5846227026E229F20035F7C2 /* AppStoreSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreSubscription.swift; sourceTree = "<group>"; }; 5846227226E22A160035F7C2 /* AppStorePaymentObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStorePaymentObserver.swift; sourceTree = "<group>"; }; 5846227626E22A7C0035F7C2 /* AppStorePaymentManagerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStorePaymentManagerDelegate.swift; sourceTree = "<group>"; }; @@ -463,8 +427,6 @@ 585DA89226B0323E00B8C587 /* TunnelIPCRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelIPCRequest.swift; sourceTree = "<group>"; }; 585DA89526B0328000B8C587 /* TunnelIPCResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelIPCResponse.swift; sourceTree = "<group>"; }; 585DA89826B0329200B8C587 /* PacketTunnelStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelStatus.swift; sourceTree = "<group>"; }; - 585DA8AE26B9492500B8C587 /* Promise.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Promise.swift; sourceTree = "<group>"; }; - 5860392826DCE7AB00554C79 /* PromiseCompletion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromiseCompletion.swift; sourceTree = "<group>"; }; 5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslucentButtonBlurView.swift; sourceTree = "<group>"; }; 58655DCD27DA0A5D00911834 /* TunnelMonitorConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelMonitorConfiguration.swift; sourceTree = "<group>"; }; 5866F39B2243B82D00168AE5 /* MullvadVPN.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MullvadVPN.entitlements; sourceTree = "<group>"; }; @@ -503,7 +465,6 @@ 5888AD82227B11080051EB06 /* SelectLocationCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationCell.swift; sourceTree = "<group>"; }; 5888AD86227B17950051EB06 /* SelectLocationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationViewController.swift; sourceTree = "<group>"; }; 588CD8BA275A0A0B00CF902E /* RESTRequestAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTRequestAdapter.swift; sourceTree = "<group>"; }; - 588DD76A26FCB49E006F6233 /* Cancellable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cancellable.swift; sourceTree = "<group>"; }; 58906DDF2445C7A5002F0673 /* NEProviderStopReason+Debug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEProviderStopReason+Debug.swift"; sourceTree = "<group>"; }; 58907D9424D17B4E00CFC3F5 /* DisconnectSplitButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisconnectSplitButton.swift; sourceTree = "<group>"; }; 5891BF1B25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+ProductVersion.swift"; sourceTree = "<group>"; }; @@ -515,7 +476,6 @@ 58A1AA8623F43901009F7EA6 /* Location.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Location.swift; sourceTree = "<group>"; }; 58A1AA8B23F5584B009F7EA6 /* ConnectionPanelView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionPanelView.swift; sourceTree = "<group>"; }; 58A94AE326CFD945001CB97C /* TunnelErrorNotificationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelErrorNotificationProvider.swift; sourceTree = "<group>"; }; - 58A94AE526D23C3D001CB97C /* PromiseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromiseTests.swift; sourceTree = "<group>"; }; 58A99ED2240014A0006599E9 /* ConsentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsentViewController.swift; sourceTree = "<group>"; }; 58ACF6482655365700ACE4B7 /* PreferencesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesViewController.swift; sourceTree = "<group>"; }; 58ACF64A26553C3F00ACE4B7 /* SettingsSwitchCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSwitchCell.swift; sourceTree = "<group>"; }; @@ -559,12 +519,6 @@ 58D0C7A023F1CECF00FE9BA7 /* MullvadVPNScreenshots.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MullvadVPNScreenshots.swift; sourceTree = "<group>"; }; 58DF28A42417CB4B00E836B0 /* AppStorePaymentManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStorePaymentManager.swift; sourceTree = "<group>"; }; 58E0A98727C8F46300FE6BDD /* Tunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tunnel.swift; sourceTree = "<group>"; }; - 58E1336826D2BE3700CC316B /* PromiseObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromiseObserver.swift; sourceTree = "<group>"; }; - 58E1336C26D2BE7500CC316B /* AnyResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyResult.swift; sourceTree = "<group>"; }; - 58E1337026D2BE9C00CC316B /* AnyOptional.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyOptional.swift; sourceTree = "<group>"; }; - 58E1337426D2BEC400CC316B /* Promise+Optional.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+Optional.swift"; sourceTree = "<group>"; }; - 58E1337826D2BEDD00CC316B /* Promise+ReceiveOn.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+ReceiveOn.swift"; sourceTree = "<group>"; }; - 58E1338026D2BF5C00CC316B /* Promise+Result.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+Result.swift"; sourceTree = "<group>"; }; 58E20770274672CA00DE5D77 /* LaunchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchViewController.swift; sourceTree = "<group>"; }; 58E6771E24ADFE7800AA26E7 /* SettingsNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsNavigationController.swift; sourceTree = "<group>"; }; 58E973DD24850EB600096F90 /* AsyncOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncOperation.swift; sourceTree = "<group>"; }; @@ -573,6 +527,8 @@ 58EE2E39272FF814003BFF93 /* SettingsDataSourceDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsDataSourceDelegate.swift; sourceTree = "<group>"; }; 58EF580A25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProblemReportSubmissionOverlayView.swift; sourceTree = "<group>"; }; 58EF581025D69DB400AEBA94 /* StatusImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusImageView.swift; sourceTree = "<group>"; }; + 58F1311227E09B00007AC5BC /* Cancellable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cancellable.swift; sourceTree = "<group>"; }; + 58F1311427E0B2AB007AC5BC /* Result+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Result+Extensions.swift"; sourceTree = "<group>"; }; 58F19E34228C15BA00C7710B /* SpinnerActivityIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpinnerActivityIndicatorView.swift; sourceTree = "<group>"; }; 58F2E143276A13F300A79513 /* StartTunnelOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartTunnelOperation.swift; sourceTree = "<group>"; }; 58F2E145276A2C9900A79513 /* StopTunnelOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StopTunnelOperation.swift; sourceTree = "<group>"; }; @@ -684,7 +640,6 @@ 580EE20524B3222200F9D8A1 /* ExclusivityController.swift */, 5820675D26E6839900655B05 /* PresentAlertOperation.swift */, 5846226426E0D9630035F7C2 /* ProductsRequestOperation.swift */, - 5846226926E0E6FA0035F7C2 /* ReceiptRefreshOperation.swift */, 5840BE34279EDB16002836BA /* OperationCompletion.swift */, ); path = Operations; @@ -835,7 +790,6 @@ 582AE3112440CA0D00E6733A /* AccountTokenInputTests.swift */, 5896AE85246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift */, 58B0A2A4238EE67E00BC001D /* Info.plist */, - 58A94AE526D23C3D001CB97C /* PromiseTests.swift */, 584B26F3237434D00073B10E /* RelaySelectorTests.swift */, 5807E2C1243203D000F5FF30 /* StringTests.swift */, 5819C2132726CC8D00D6EC38 /* DataSourceSnapshotTests.swift */, @@ -891,6 +845,7 @@ 58CE5E6A224146210008646E /* Assets.xcassets */, 58FEEB57260B662E00A621A8 /* AutomaticKeyboardResponder.swift */, 5891BF1B25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift */, + 58F1311227E09B00007AC5BC /* Cancellable.swift */, 58F840B12464491D0044E708 /* ChainedError.swift */, 587EB669270EFACB00123C75 /* CharacterSet+IPAddress.swift */, 582AD43F27BE616E002A6BFC /* CodingErrors+ChainedError.swift */, @@ -950,11 +905,11 @@ 58F8AC0D25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift */, 58EF580A25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift */, 58293FAC2510CA58005D0BB5 /* ProblemReportViewController.swift */, - 58E1338C26D2BFB500CC316B /* Promise */, 585DA87526B0249A00B8C587 /* RelayCache */, 58781CC822AE7CA8009B9D8E /* RelayConstraints.swift */, 58781CD422AFBA39009B9D8E /* RelaySelector.swift */, 585DA87F26B0268500B8C587 /* REST */, + 58F1311427E0B2AB007AC5BC /* Result+Extensions.swift */, 5820676726E79E7B00655B05 /* Result+UIBackgroundFetchResult.swift */, 587425C02299833500CA2045 /* RootContainerViewController.swift */, 5888AD82227B11080051EB06 /* SelectLocationCell.swift */, @@ -1019,25 +974,6 @@ path = MullvadVPNScreenshots; sourceTree = "<group>"; }; - 58E1338C26D2BFB500CC316B /* Promise */ = { - isa = PBXGroup; - children = ( - 58E1337026D2BE9C00CC316B /* AnyOptional.swift */, - 58E1336C26D2BE7500CC316B /* AnyResult.swift */, - 588DD76A26FCB49E006F6233 /* Cancellable.swift */, - 585DA8AE26B9492500B8C587 /* Promise.swift */, - 5820674826E63EC800655B05 /* Promise+BackgroundTask.swift */, - 5820675F26E75A4D00655B05 /* Promise+Delay.swift */, - 5846226626E0DF960035F7C2 /* Promise+OperationQueue.swift */, - 58E1337426D2BEC400CC316B /* Promise+Optional.swift */, - 58E1337826D2BEDD00CC316B /* Promise+ReceiveOn.swift */, - 58E1338026D2BF5C00CC316B /* Promise+Result.swift */, - 5860392826DCE7AB00554C79 /* PromiseCompletion.swift */, - 58E1336826D2BE3700CC316B /* PromiseObserver.swift */, - ); - path = Promise; - sourceTree = "<group>"; - }; 58ECD29023F178FD004298B6 /* Configurations */ = { isa = PBXGroup; children = ( @@ -1307,11 +1243,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 5806767527048E7C00C858CB /* Promise+Result.swift in Sources */, - 58E1336F26D2BE7500CC316B /* AnyResult.swift in Sources */, 5896AE80246ACE79005B36CB /* KeychainClass.swift in Sources */, 582AE3132440CA2700E6733A /* AccountTokenInput.swift in Sources */, - 5820676126E75A4D00655B05 /* Promise+Delay.swift in Sources */, 58CAF4EF26025954007C5886 /* SimulatorTunnelProvider.swift in Sources */, 58B0A2AA238EE6A900BC001D /* RelaySelector.swift in Sources */, 5896AE86246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift in Sources */, @@ -1326,22 +1259,14 @@ 585DA8A626B14F5100B8C587 /* SSLPinningURLSessionDelegate.swift in Sources */, 58B0A2AC238EE6D500BC001D /* IPAddress+Codable.swift in Sources */, 58B0A2AD238EE6EC00BC001D /* MullvadEndpoint.swift in Sources */, - 5846226826E0DF960035F7C2 /* Promise+OperationQueue.swift in Sources */, - 5806767227048E7400C858CB /* Promise+Optional.swift in Sources */, - 5860392B26DCEE6300554C79 /* PromiseCompletion.swift in Sources */, 58FAEDF4245088B300CB0F5B /* KeychainError.swift in Sources */, 5896AE88246D7FAF005B36CB /* CustomDateComponentsFormatting.swift in Sources */, - 5806766F27048E6900C858CB /* Promise.swift in Sources */, - 5806767E27048EBE00C858CB /* Promise+ReceiveOn.swift in Sources */, - 58A94AE626D23C3D001CB97C /* PromiseTests.swift in Sources */, 5857F23824C8446700CF6F47 /* AsyncBlockOperation.swift in Sources */, 582AE3122440CA0D00E6733A /* AccountTokenInputTests.swift in Sources */, 585DA8A526B14EE000B8C587 /* PacketTunnelStatus.swift in Sources */, - 588DD76D26FCB4A2006F6233 /* Cancellable.swift in Sources */, 5896AE7E246ACE65005B36CB /* KeychainAttributes.swift in Sources */, 58B0A2A9238EE6A100BC001D /* RelayConstraints.swift in Sources */, 5807E2C2243203D000F5FF30 /* StringTests.swift in Sources */, - 5806766B27048E3C00C858CB /* AnyOptional.swift in Sources */, 5819C2142726CC8D00D6EC38 /* DataSourceSnapshotTests.swift in Sources */, 585DA8A326B14E0D00B8C587 /* ServerRelaysResponse.swift in Sources */, 5820676226E75D8500655B05 /* REST.swift in Sources */, @@ -1349,7 +1274,6 @@ 5857F23024C843ED00CF6F47 /* ChainedError.swift in Sources */, 58A8BE81239FBE62006B74AC /* IPEndpoint.swift in Sources */, 5846227A26E24F1F0035F7C2 /* ExclusivityController.swift in Sources */, - 58E1336B26D2BE3700CC316B /* PromiseObserver.swift in Sources */, 58871D2325D535D2002297FA /* IPAddressRange+Codable.swift in Sources */, 5806767B27048E8900C858CB /* Keychain.swift in Sources */, ); @@ -1368,7 +1292,6 @@ 5840250122B1124600E4CFEC /* IPAddress+Codable.swift in Sources */, 5857F24724C882D700CF6F47 /* SelectLocationNavigationController.swift in Sources */, 5846227126E229F20035F7C2 /* AppStoreSubscription.swift in Sources */, - 5806767027048E6A00C858CB /* Promise.swift in Sources */, 5820675B26E6576800655B05 /* RelayCache.swift in Sources */, 5846226526E0D9630035F7C2 /* ProductsRequestOperation.swift in Sources */, 587EB672271451E300123C75 /* PreferencesViewModel.swift in Sources */, @@ -1378,17 +1301,17 @@ 582AD44027BE616E002A6BFC /* CodingErrors+ChainedError.swift in Sources */, 58F2E148276A307400A79513 /* MapConnectionStatusOperation.swift in Sources */, 58BA693123EADA6A009DC256 /* SimulatorTunnelProvider.swift in Sources */, + 58F1311327E09B00007AC5BC /* Cancellable.swift in Sources */, 587C575326D2615F005EF767 /* PacketTunnelOptions.swift in Sources */, - 58E1336D26D2BE7500CC316B /* AnyResult.swift in Sources */, 587B753B2666467500DEF7E9 /* NotificationBannerView.swift in Sources */, 58B993B12608A34500BA7811 /* LoginContentView.swift in Sources */, 58E6771F24ADFE7800AA26E7 /* SettingsNavigationController.swift in Sources */, - 5806767427048E7400C858CB /* Promise+Optional.swift in Sources */, 58A1AA8C23F5584C009F7EA6 /* ConnectionPanelView.swift in Sources */, 588CD8BB275A0A0B00CF902E /* RESTRequestAdapter.swift in Sources */, 582BB1B52295780F0055B6EF /* AccountExpiry.swift in Sources */, 582BB1B3229574F40055B6EF /* SettingsAccountCell.swift in Sources */, 588527B2276B3F0700BAA373 /* LoadTunnelOperation.swift in Sources */, + 58F1311527E0B2AB007AC5BC /* Result+Extensions.swift in Sources */, 585DA88426B0270700B8C587 /* ServerRelaysResponse.swift in Sources */, 5875960726F36B3A00BF6711 /* TunnelIPCError.swift in Sources */, 58F8AC0E25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift in Sources */, @@ -1398,7 +1321,6 @@ 58B3F30F2742708B00A2DD38 /* HeaderBarButton.swift in Sources */, 584789E026529D72000E45FB /* SSLPinningURLSessionDelegate.swift in Sources */, 58ACF6492655365700ACE4B7 /* PreferencesViewController.swift in Sources */, - 5860392926DCE7AB00554C79 /* PromiseCompletion.swift in Sources */, 588D2FE3248AC27F00E313F7 /* AsyncOperation.swift in Sources */, 5820675026E6514100655B05 /* HTTP.swift in Sources */, 585DA89126B0322700B8C587 /* TunnelIPC.swift in Sources */, @@ -1406,7 +1328,6 @@ 5877153023981F7B001F8237 /* WireguardKeysViewController.swift in Sources */, 587B7536266528A200DEF7E9 /* NotificationManager.swift in Sources */, 58FB865A26EA214400F188BC /* RelayCacheObserver.swift in Sources */, - 5806766C27048E3E00C858CB /* AnyOptional.swift in Sources */, 58ACF64D26567A5000ACE4B7 /* CustomSwitch.swift in Sources */, 58655DCE27DA0A5D00911834 /* TunnelMonitorConfiguration.swift in Sources */, 5850367F25A481D800A43E93 /* IPAddressRange+Codable.swift in Sources */, @@ -1419,11 +1340,9 @@ 585DA87A26B024F900B8C587 /* RelayCacheError.swift in Sources */, 5856D13727450A8A00DFD627 /* UIImage+TintColor.swift in Sources */, 58CB0EE024B86751001EF0D8 /* RESTClient.swift in Sources */, - 5846226A26E0E6FA0035F7C2 /* ReceiptRefreshOperation.swift in Sources */, 58095C532760EEC700890776 /* RESTNetworkOperation.swift in Sources */, 58293FB125124117005D0BB5 /* CustomTextField.swift in Sources */, 582AE3102440A6CA00E6733A /* AccountTokenInput.swift in Sources */, - 5806767827048E7E00C858CB /* Promise+Result.swift in Sources */, 5846227726E22A7C0035F7C2 /* AppStorePaymentManagerDelegate.swift in Sources */, 5871FB8325498CA20051A0A4 /* Swizzle.swift in Sources */, 58EF581125D69DB400AEBA94 /* StatusImageView.swift in Sources */, @@ -1431,8 +1350,6 @@ 58EE2E3B272FF814003BFF93 /* SettingsDataSourceDelegate.swift in Sources */, 5823FA5426CE49F700283BF8 /* TunnelObserver.swift in Sources */, 5888AD87227B17950051EB06 /* SelectLocationViewController.swift in Sources */, - 5820676026E75A4D00655B05 /* Promise+Delay.swift in Sources */, - 58E1336926D2BE3700CC316B /* PromiseObserver.swift in Sources */, 58293FB3251241B4005D0BB5 /* CustomTextView.swift in Sources */, 58F19E35228C15BA00C7710B /* SpinnerActivityIndicatorView.swift in Sources */, 58A99ED3240014A0006599E9 /* ConsentViewController.swift in Sources */, @@ -1460,7 +1377,6 @@ 584B17AB27637DE40057F3B8 /* ReloadTunnelOperation.swift in Sources */, 5820676426E771DB00655B05 /* TunnelManagerError.swift in Sources */, 585B4B8726D9098900555C4C /* TunnelErrorNotificationProvider.swift in Sources */, - 5846226726E0DF960035F7C2 /* Promise+OperationQueue.swift in Sources */, 5850368C25A49E2200A43E93 /* PrivateKeyWithMetadata.swift in Sources */, 58FEAFB92750DA2F003C1625 /* AddressCache.swift in Sources */, 58B67B482602079E008EF58E /* RelaySelector.swift in Sources */, @@ -1470,7 +1386,6 @@ 5878BA1426DD0B01004147D7 /* OSLogHandler.swift in Sources */, 582BB1AF229566420055B6EF /* SettingsCell.swift in Sources */, 58F3C0A4249CB069003E76BE /* HeaderBarView.swift in Sources */, - 5820674926E63EC900655B05 /* Promise+BackgroundTask.swift in Sources */, 58B9EB132488ED2100095626 /* AlertPresenter.swift in Sources */, 587A01FC23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift in Sources */, 5819C2172729595500D6EC38 /* SettingsAddDNSEntryCell.swift in Sources */, @@ -1496,7 +1411,6 @@ 5820675E26E6839900655B05 /* PresentAlertOperation.swift in Sources */, 5815039D24D6ECE600C9C50E /* TextFileOutputStream.swift in Sources */, 58CE5E64224146200008646E /* AppDelegate.swift in Sources */, - 588DD76B26FCB49E006F6233 /* Cancellable.swift in Sources */, 58E0A98827C8F46300FE6BDD /* Tunnel.swift in Sources */, 58ACF64F26567A7100ACE4B7 /* CustomSwitchContainer.swift in Sources */, 5857F24324C8662600CF6F47 /* SelectLocationHeaderView.swift in Sources */, @@ -1542,7 +1456,6 @@ 58F840B22464491D0044E708 /* ChainedError.swift in Sources */, 58FAEDFF24533A7000CB0F5B /* KeychainReturn.swift in Sources */, 58ACF64B26553C3F00ACE4B7 /* SettingsSwitchCell.swift in Sources */, - 5806767F27048EC000C858CB /* Promise+ReceiveOn.swift in Sources */, 587EB67027143B6500123C75 /* DataSourceSnapshot.swift in Sources */, 585DA88A26B027A300B8C587 /* RESTCoding.swift in Sources */, 587B753D2666468F00DEF7E9 /* NotificationController.swift in Sources */, @@ -1554,13 +1467,10 @@ buildActionMask = 2147483647; files = ( 5850366825A47AC700A43E93 /* IPAddressRange+Codable.swift in Sources */, - 5806767327048E7400C858CB /* Promise+Optional.swift in Sources */, 58B93A1826C54D7E00A55733 /* Locking.swift in Sources */, - 5860392A26DCE7AB00554C79 /* PromiseCompletion.swift in Sources */, 58FB865F26EA2E6D00F188BC /* LogFormatting.swift in Sources */, 5806767A27048E8800C858CB /* Keychain.swift in Sources */, 585DA89726B0328000B8C587 /* TunnelIPCResponse.swift in Sources */, - 5806768327048F7A00C858CB /* Promise.swift in Sources */, 5806768127048EE000C858CB /* KeychainMatchLimit.swift in Sources */, 587C575426D2615F005EF767 /* PacketTunnelOptions.swift in Sources */, 58FAEE0324533ABE00CB0F5B /* KeychainReturn.swift in Sources */, @@ -1583,15 +1493,10 @@ 58FAEDF1245069CA00CB0F5B /* KeychainAttributes.swift in Sources */, 585DA89A26B0329200B8C587 /* PacketTunnelStatus.swift in Sources */, 585DA88526B0270700B8C587 /* ServerRelaysResponse.swift in Sources */, - 5806767627048E7D00C858CB /* Promise+Result.swift in Sources */, 581503A724D6F4AE00C9C50E /* Logging.swift in Sources */, 58FAEE0424533AC000CB0F5B /* KeychainClass.swift in Sources */, - 5806768227048F6800C858CB /* AnyOptional.swift in Sources */, 58AEEF6C2344A49D00C9BBD5 /* TunnelSettingsManager.swift in Sources */, - 58E1336E26D2BE7500CC316B /* AnyResult.swift in Sources */, 581503A424D6F1EC00C9C50E /* ChainedError+Logger.swift in Sources */, - 5806768027048ECF00C858CB /* Promise+ReceiveOn.swift in Sources */, - 58E1336A26D2BE3700CC316B /* PromiseObserver.swift in Sources */, 5815039824D6ECAE00C9C50E /* CustomFormatLogHandler.swift in Sources */, 5840250522B11AB700E4CFEC /* MullvadEndpoint.swift in Sources */, 58906DE02445C7A5002F0673 /* NEProviderStopReason+Debug.swift in Sources */, @@ -1599,7 +1504,6 @@ 5815039E24D6ECE600C9C50E /* TextFileOutputStream.swift in Sources */, 585DA87826B024A900B8C587 /* CachedRelays.swift in Sources */, 584E96BD240FD4DA00D3334F /* Location.swift in Sources */, - 588DD76C26FCB49E006F6233 /* Cancellable.swift in Sources */, 58F840B32464491D0044E708 /* ChainedError.swift in Sources */, 58D67A0A26D7AE3300557C3C /* OSLogHandler.swift in Sources */, 5820675626E6528A00655B05 /* RESTError.swift in Sources */, diff --git a/ios/MullvadVPN/Account.swift b/ios/MullvadVPN/Account.swift index a9bcd75323..f1b4a4ca28 100644 --- a/ios/MullvadVPN/Account.swift +++ b/ios/MullvadVPN/Account.swift @@ -54,6 +54,12 @@ class Account { private let logger = Logger(label: "Account") private var observerList = ObserverList<AccountObserver>() + private let operationQueue: OperationQueue = { + let operationQueue = OperationQueue() + operationQueue.maxConcurrentOperationCount = 1 + return operationQueue + }() + /// Returns true if user agreed to terms of service, otherwise false var isAgreedToTermsOfService: Bool { return UserDefaults.standard.bool(forKey: UserDefaultsKeys.isAgreedToTermsOfService.rawValue) @@ -83,8 +89,6 @@ class Account { } } - private let dispatchQueue = DispatchQueue(label: "AccountQueue") - var isLoggedIn: Bool { return token != nil } @@ -94,120 +98,145 @@ class Account { UserDefaults.standard.set(true, forKey: UserDefaultsKeys.isAgreedToTermsOfService.rawValue) } - func loginWithNewAccount() -> Result<REST.AccountResponse, Account.Error>.Promise { - return REST.Client.shared.createAccount() - .execute() - .mapError { error in - return Error.createAccount(error) - } - .receive(on: .main) - .mapThen { response in - return self.setupTunnel(accountToken: response.token, expiry: response.expires) - .map { _ in - self.observerList.forEach { (observer) in - observer.account(self, didLoginWithToken: response.token, expiry: response.expires) + func loginWithNewAccount(completionHandler: @escaping (Result<REST.AccountResponse, Account.Error>) -> Void) { + let operation = AsyncBlockOperation { operation in + _ = REST.Client.shared.createAccount().execute { result in + DispatchQueue.main.async { + switch result { + case .success(let response): + self.setupTunnel(accountToken: response.token, expiry: response.expires) { error in + if let error = error { + completionHandler(.failure(error)) + } else { + self.observerList.forEach { observer in + observer.account(self, didLoginWithToken: response.token, expiry: response.expires) + } + + completionHandler(.success(response)) + } + + operation.finish() } - return response + + case .failure(let error): + completionHandler(.failure(.createAccount(error))) + + operation.finish() } + } } - .block(on: dispatchQueue) - .receive(on: .main) + } + + operationQueue.addOperation(operation) } /// Perform the login and save the account token along with expiry (if available) to the /// application preferences. - func login(with accountToken: String) -> Result<REST.AccountResponse, Account.Error>.Promise { - return REST.Client.shared.getAccountExpiry(token: accountToken) - .execute(retryStrategy: .default) - .mapError { error in - return Account.Error.verifyAccount(error) - } - .mapThen { response in - return self.setupTunnel(accountToken: response.token, expiry: response.expires) - .map { _ in - self.observerList.forEach { (observer) in - observer.account(self, didLoginWithToken: response.token, expiry: response.expires) + func login(accountToken: String, completionHandler: @escaping (Result<REST.AccountResponse, Account.Error>) -> Void) { + let operation = AsyncBlockOperation { operation in + _ = REST.Client.shared.getAccountExpiry(token: accountToken) + .execute(retryStrategy: .default) { result in + DispatchQueue.main.async { + switch result { + case .success(let response): + self.setupTunnel(accountToken: response.token, expiry: response.expires) { error in + if let error = error { + completionHandler(.failure(error)) + } else { + self.observerList.forEach { observer in + observer.account(self, didLoginWithToken: response.token, expiry: response.expires) + } + completionHandler(.success(response)) + } + operation.finish() + } + + case .failure(let error): + completionHandler(.failure(.verifyAccount(error))) + operation.finish() } - return response } - } - .block(on: dispatchQueue) - .receive(on: .main) + } + } + + operationQueue.addOperation(operation) } /// Perform the logout by erasing the account token and expiry from the application preferences. - func logout() -> Promise<Void> { - return Promise { resolver in + func logout(completionHandler: @escaping () -> Void) { + let operation = AsyncBlockOperation { operation in TunnelManager.shared.unsetAccount { - resolver.resolve(value: ()) + self.removeFromPreferences() + self.observerList.forEach { (observer) in + observer.accountDidLogout(self) + } + + completionHandler() + operation.finish() } } - .receive(on: .main) - .then { _ -> () in - self.removeFromPreferences() - self.observerList.forEach { (observer) in - observer.accountDidLogout(self) - } - return () - } - .block(on: dispatchQueue) - .receive(on: .main) + operationQueue.addOperation(operation) } /// Forget that user was logged in, but do not attempt to unset account in `TunnelManager`. /// This function is used in cases where the tunnel or tunnel settings are corrupt. - func forget() -> Promise<Void> { - return Promise<Void> { resolver in - self.removeFromPreferences() - self.observerList.forEach { (observer) in - observer.accountDidLogout(self) + func forget(completionHandler: @escaping () -> Void) { + let operation = AsyncBlockOperation { operation in + DispatchQueue.main.async { + self.removeFromPreferences() + self.observerList.forEach { (observer) in + observer.accountDidLogout(self) + } + operation.finish() } - resolver.resolve(value: ()) } - .schedule(on: .main) - .block(on: dispatchQueue) - .receive(on: .main) + + operationQueue.addOperation(operation) } func updateAccountExpiry() { - Promise<String?>.deferred { self.token } - .mapThen(defaultValue: nil) { token in - return REST.Client.shared.getAccountExpiry(token: token) - .execute(retryStrategy: .default) - .onFailure { error in - self.logger.error(chainedError: error, message: "Failed to update account expiry") - } - .success() - } - .schedule(on: .main) - .block(on: dispatchQueue) - .receive(on: .main) - .observe { completion in - guard let response = completion.flattenUnwrappedValue else { return } + let operation = AsyncBlockOperation { operation in + DispatchQueue.main.async { + guard let token = self.token else { + operation.finish() + return + } - if self.expiry != response.expires { - self.expiry = response.expires - self.observerList.forEach { (observer) in - observer.account(self, didUpdateExpiry: response.expires) + _ = REST.Client.shared.getAccountExpiry(token: token) + .execute(retryStrategy: .default) { result in + switch result { + case .success(let response): + if self.expiry != response.expires { + self.expiry = response.expires + self.observerList.forEach { (observer) in + observer.account(self, didUpdateExpiry: response.expires) + } + } + + case .failure(let error): + self.logger.error(chainedError: error, message: "Failed to update account expiry.") + } + + operation.finish() } - } } + } + + operationQueue.addOperation(operation) } - private func setupTunnel(accountToken: String, expiry: Date) -> Result<(), Account.Error>.Promise { - return Promise { resolver in - TunnelManager.shared.setAccount(accountToken: accountToken) { error in - dispatchPrecondition(condition: .onQueue(.main)) + private func setupTunnel(accountToken: String, expiry: Date, completionHandler: @escaping (Account.Error?) -> Void) { + TunnelManager.shared.setAccount(accountToken: accountToken) { error in + dispatchPrecondition(condition: .onQueue(.main)) - if let error = error { - resolver.resolve(value: .failure(Account.Error.tunnelConfiguration(error))) - } else { - self.token = accountToken - self.expiry = expiry + if let error = error { + completionHandler(.tunnelConfiguration(error)) + } else { + self.token = accountToken + self.expiry = expiry - resolver.resolve(value: .success(())) - } + completionHandler(nil) } } } @@ -241,8 +270,8 @@ extension Account: AppStorePaymentObserver { } func appStorePaymentManager(_ manager: AppStorePaymentManager, transaction: SKPaymentTransaction, accountToken: String, didFinishWithResponse response: REST.CreateApplePaymentResponse) { - dispatchQueue.async { - DispatchQueue.main.sync { + let operation = AsyncBlockOperation { operation in + DispatchQueue.main.async { let newExpiry = response.newExpiry // Make sure that payment corresponds to the active account token @@ -252,7 +281,11 @@ extension Account: AppStorePaymentObserver { observer.account(self, didUpdateExpiry: newExpiry) } } + + operation.finish() } } + + operationQueue.addOperation(operation) } } diff --git a/ios/MullvadVPN/AccountViewController.swift b/ios/MullvadVPN/AccountViewController.swift index 305c37cc06..34cc20345c 100644 --- a/ios/MullvadVPN/AccountViewController.swift +++ b/ios/MullvadVPN/AccountViewController.swift @@ -22,7 +22,7 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO return contentView }() - private var copyToPasteboardCancellationToken: PromiseCancellationToken? + private var copyToPasteboardWork: DispatchWorkItem? private var pendingPayment: SKPayment? private let alertPresenter = AlertPresenter() @@ -120,26 +120,25 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO purchaseButtonInteractionRestriction.increase(animated: true) - AppStorePaymentManager.shared.requestProducts(with: [inAppPurchase]) - .receive(on: .main) - .observe { [weak self] completion in - guard let self = self else { return } + _ = AppStorePaymentManager.shared.requestProducts(with: [inAppPurchase]) { [weak self] completion in + guard let self = self else { return } - if let result = completion.unwrappedValue { - switch result { - case .success(let response): - if let product = response.products.first { - self.setProduct(product, animated: true) - } - - case .failure(let error): - self.didFailLoadingProducts(with: error) - } + switch completion { + case .success(let response): + if let product = response.products.first { + self.setProduct(product, animated: true) } - self.contentView.purchaseButton.isLoading = false - self.purchaseButtonInteractionRestriction.decrease(animated: true) + case .failure(let error): + self.didFailLoadingProducts(with: error) + + case .cancelled: + break } + + self.contentView.purchaseButton.isLoading = false + self.purchaseButtonInteractionRestriction.decrease(animated: true) + } } private func setProduct(_ product: SKProduct, animated: Bool) { @@ -233,7 +232,7 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO alertPresenter.enqueue(alertController, presentingController: self) } - private func showLogoutConfirmation(completion: @escaping (Bool) -> Void, animated: Bool) { + private func showLogoutConfirmation(animated: Bool, completion: @escaping (Bool) -> Void) { let alertController = UIAlertController( title: NSLocalizedString( "LOGOUT_CONFIRMATION_ALERT_TITLE", @@ -294,13 +293,13 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO ) alertPresenter.enqueue(alertController, presentingController: self) { - Account.shared.logout() - .receive(on: .main, after: .seconds(1), timerType: .deadline) - .observe { _ in + Account.shared.logout { + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { alertController.dismiss(animated: true) { self.delegate?.accountViewControllerDidLogout(self) } } + } } } @@ -361,11 +360,11 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO // MARK: - Actions @objc private func doLogout() { - showLogoutConfirmation(completion: { (confirmed) in + showLogoutConfirmation(animated: true) { confirmed in if confirmed { self.confirmLogout() } - }, animated: true) + } } private func copyAccountToken() { @@ -377,14 +376,16 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO comment: "Message, temporarily displayed in place account token, after copying the account token to pasteboard on tap." ) - Promise.deferred { Account.shared.formattedToken } - .delay(by: .seconds(3), timerType: .walltime, queue: .main) - .storeCancellationToken(in: ©ToPasteboardCancellationToken) - .observe { [weak self] completion in - guard let formattedToken = completion.unwrappedValue else { return } + let workItem = DispatchWorkItem { [weak self] in + guard let formattedToken = Account.shared.formattedToken else { return } - self?.contentView.accountTokenRowView.value = formattedToken - } + self?.contentView.accountTokenRowView.value = formattedToken + } + + copyToPasteboardWork?.cancel() + copyToPasteboardWork = workItem + + DispatchQueue.main.asyncAfter(wallDeadline: .now() + .seconds(3), execute: workItem) } @objc private func doPurchase() { @@ -403,12 +404,12 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO compoundInteractionRestriction.increase(animated: true) - AppStorePaymentManager.shared.restorePurchases(for: accountToken) - .receive(on: .main) - .onSuccess { response in + _ = AppStorePaymentManager.shared.restorePurchases(for: accountToken) { completion in + switch completion { + case .success(let response): self.showTimeAddedConfirmationAlert(with: response, context: .restoration) - } - .onFailure { error in + + case .failure(let error): let alertController = UIAlertController( title: NSLocalizedString( "RESTORE_PURCHASES_FAILURE_ALERT_TITLE", @@ -428,10 +429,13 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO ), style: .cancel) ) self.alertPresenter.enqueue(alertController, presentingController: self) + + case .cancelled: + break } - .observe { _ in - self.compoundInteractionRestriction.decrease(animated: true) - } + + self.compoundInteractionRestriction.decrease(animated: true) + } } } diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index aeb4dde33e..2d719ebc0b 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -90,11 +90,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { RelayCache.Tracker.shared.addObserver(self) // Load initial relays - RelayCache.Tracker.shared.read() - .receive(on: .main) - .observe { completion in - guard let result = completion.unwrappedValue else { return } - + RelayCache.Tracker.shared.read { result in + DispatchQueue.main.async { switch result { case .success(let cachedRelays): self.cachedRelays = cachedRelays @@ -102,6 +99,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { case .failure(let error): self.logger?.error(chainedError: error, message: "Failed to load initial relays") } + } } // Load tunnels @@ -119,10 +117,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { case .migrateTunnelSettings(_), .readTunnelSettings(_): // Forget that user was logged in since tunnel settings are likely corrupt // or missing. - Account.shared.forget() - .observe { _ in - self.didFinishInitialization() - } + Account.shared.forget { + self.didFinishInitialization() + } default: fatalError("Unexpected error coming from loadTunnel()") @@ -195,27 +192,23 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } let updateRelaysOperation = AsyncBlockOperation { operation in - var cancellationToken: PromiseCancellationToken? - - RelayCache.Tracker.shared.updateRelays() - .storeCancellationToken(in: &cancellationToken) - .observe { completion in - switch completion.unwrappedValue { - case .success(let result): - self.logger?.debug("Finished updating relays: \(result)") - case .failure(let error): - self.logger?.error(chainedError: error, message: "Failed to update relays") - case .none: - break - } + let cancellable = RelayCache.Tracker.shared.updateRelays { completion in + switch completion { + case .success(let result): + self.logger?.debug("Finished updating relays: \(result)") + case .failure(let error): + self.logger?.error(chainedError: error, message: "Failed to update relays") + case .cancelled: + break + } - relaysFetchResult = completion.unwrappedValue?.backgroundFetchResult + relaysFetchResult = completion.result?.backgroundFetchResult - operation.finish() - } + operation.finish() + } operation.addCancellationBlock { - cancellationToken?.cancel() + cancellable.cancel() } } @@ -277,28 +270,26 @@ class AppDelegate: UIResponder, UIApplicationDelegate { @available(iOS 13.0, *) private func scheduleBackgroundTasks() { - if case .finished(let result) = RelayCache.Tracker.shared.scheduleAppRefreshTask().await() { - switch result { - case .success: - self.logger?.debug("Scheduled app refresh task") - case .failure(let error): - self.logger?.error(chainedError: error, message: "Could not schedule app refresh task") - } + switch RelayCache.Tracker.shared.scheduleAppRefreshTask() { + case .success: + self.logger?.debug("Scheduled app refresh task.") + case .failure(let error): + self.logger?.error(chainedError: error, message: "Could not schedule app refresh task.") } switch TunnelManager.shared.scheduleBackgroundTask() { case .success: self.logger?.debug("Scheduled private key rotation task") case .failure(let error): - self.logger?.error(chainedError: error, message: "Could not schedule private key rotation task") + self.logger?.error(chainedError: error, message: "Could not schedule private key rotation task.") } do { try addressCacheTracker.scheduleBackgroundTask() - self.logger?.debug("Scheduled address cache update task") + self.logger?.debug("Scheduled address cache update task.") } catch { - self.logger?.error(chainedError: AnyChainedError(error), message: "Could not schedule address cache update task") + self.logger?.error(chainedError: AnyChainedError(error), message: "Could not schedule address cache update task.") } } @@ -571,39 +562,37 @@ extension AppDelegate: LoginViewControllerDelegate { func loginViewController(_ controller: LoginViewController, loginWithAccountToken accountToken: String, completion: @escaping (Result<REST.AccountResponse, Account.Error>) -> Void) { self.rootContainer?.setEnableSettingsButton(false) - Account.shared.login(with: accountToken) - .onSuccess { _ in + Account.shared.login(accountToken: accountToken) { result in + switch result { + case .success: self.logger?.debug("Logged in with existing token") // RootContainer's settings button will be re-enabled in `loginViewControllerDidLogin` - } - .onFailure { error in + + case .failure(let error): self.logger?.error(chainedError: error, message: "Failed to log in with existing account") self.rootContainer?.setEnableSettingsButton(true) } - .observe { promiseCompletion in - guard let result = promiseCompletion.unwrappedValue else { return } - completion(result) - } + completion(result) + } } func loginViewControllerLoginWithNewAccount(_ controller: LoginViewController, completion: @escaping (Result<REST.AccountResponse, Account.Error>) -> Void) { self.rootContainer?.setEnableSettingsButton(false) - Account.shared.loginWithNewAccount() - .onSuccess { _ in + Account.shared.loginWithNewAccount { result in + switch result { + case .success: self.logger?.debug("Logged in with new account token") // RootContainer's settings button will be re-enabled in `loginViewControllerDidLogin` - } - .onFailure { error in + + case .failure(let error): self.logger?.error(chainedError: error, message: "Failed to log in with new account") self.rootContainer?.setEnableSettingsButton(true) } - .observe { promiseCompletion in - guard let result = promiseCompletion.unwrappedValue else { return } - completion(result) - } + completion(result) + } } func loginViewControllerDidLogin(_ controller: LoginViewController) { diff --git a/ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManager.swift b/ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManager.swift index e40ba834fe..32fc7d2a25 100644 --- a/ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManager.swift +++ b/ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManager.swift @@ -28,6 +28,8 @@ class AppStorePaymentManager: NSObject, SKPaymentTransactionObserver { return queue }() + private let exclusivityController = ExclusivityController() + private let paymentQueue: SKPaymentQueue private var observerList = ObserverList<AppStorePaymentObserver>() @@ -95,19 +97,16 @@ class AppStorePaymentManager: NSObject, SKPaymentTransactionObserver { // MARK: - Products and payments - func requestProducts(with productIdentifiers: Set<AppStoreSubscription>) -> Result<SKProductsResponse, Swift.Error>.Promise { - return Promise { resolver in - let productIdentifiers = productIdentifiers.productIdentifiersSet - let operation = ProductsRequestOperation(productIdentifiers: productIdentifiers) { result in - resolver.resolve(value: result) - } + func requestProducts(with productIdentifiers: Set<AppStoreSubscription>, completionHandler: @escaping (OperationCompletion<SKProductsResponse, Swift.Error>) -> Void) -> AnyCancellable { + let productIdentifiers = productIdentifiers.productIdentifiersSet + let operation = ProductsRequestOperation(productIdentifiers: productIdentifiers, completionHandler: completionHandler) - resolver.setCancelHandler { - operation.cancel() - } + exclusivityController.addOperation(operation, categories: [OperationCategory.productsRequest]) + + operationQueue.addOperation(operation) - ExclusivityController.shared.addOperation(operation, categories: [OperationCategory.productsRequest]) - self.operationQueue.addOperation(operation) + return AnyCancellable { + operation.cancel() } } @@ -121,9 +120,8 @@ class AppStorePaymentManager: NSObject, SKPaymentTransactionObserver { } } - func restorePurchases(for accountToken: String) -> Result<REST.CreateApplePaymentResponse, AppStorePaymentManager.Error>.Promise { - return sendAppStoreReceipt(accountToken: accountToken, forceRefresh: true) - .requestBackgroundTime(taskName: "AppStorePaymentManager.restorePurchases") + func restorePurchases(for accountToken: String, completionHandler: @escaping (OperationCompletion<REST.CreateApplePaymentResponse, AppStorePaymentManager.Error>) -> Void) -> AnyCancellable { + return sendAppStoreReceipt(accountToken: accountToken, forceRefresh: true, completionHandler: completionHandler) } @@ -153,26 +151,26 @@ class AppStorePaymentManager: NSObject, SKPaymentTransactionObserver { paymentQueue.add(payment) } - private func sendAppStoreReceipt(accountToken: String, forceRefresh: Bool) -> Result<REST.CreateApplePaymentResponse, Error>.Promise { - return AppStoreReceipt.fetch(forceRefresh: forceRefresh) - .mapError { error in - self.logger.error(chainedError: error, message: "Failed to fetch the AppStore receipt") + private func sendAppStoreReceipt(accountToken: String, forceRefresh: Bool, completionHandler: @escaping (OperationCompletion<REST.CreateApplePaymentResponse, Error>) -> Void) -> AnyCancellable { + let operation = SendAppStoreReceiptOperation(restClient: REST.Client.shared, accountToken: accountToken, forceRefresh: forceRefresh, receiptProperties: nil) { completion in + completionHandler(completion) + } - return .readReceipt(error) - } - .mapThen { receiptData in - return REST.Client.shared.createApplePayment(token: accountToken, receiptString: receiptData) - .execute() - .mapError { error in - self.logger.error(chainedError: error, message: "Failed to upload the AppStore receipt") + let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "Send AppStore receipt") { + operation.cancel() + } - return .sendReceipt(error) - } - .onSuccess{ response in - self.logger.info("AppStore receipt was processed. Time added: \(response.timeAdded), New expiry: \(response.newExpiry.logFormatDate())") - } - } - .run(on: operationQueue, categories: [OperationCategory.sendAppStoreReceipt]) + operation.completionBlock = { + UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier) + } + + exclusivityController.addOperation(operation, categories: [OperationCategory.sendAppStoreReceipt]) + + operationQueue.addOperation(operation) + + return AnyCancellable { + operation.cancel() + } } private func handleTransactions(_ transactions: [SKPaymentTransaction]) { @@ -232,32 +230,7 @@ class AppStorePaymentManager: NSObject, SKPaymentTransactionObserver { } private func didFinishOrRestorePurchase(transaction: SKPaymentTransaction) { - if let accountToken = deassociateAccountToken(transaction.payment) { - sendAppStoreReceipt(accountToken: accountToken, forceRefresh: false) - .receive(on: .main) - .onSuccess { response in - self.paymentQueue.finishTransaction(transaction) - - self.observerList.forEach { (observer) in - observer.appStorePaymentManager( - self, - transaction: transaction, - accountToken: accountToken, - didFinishWithResponse: response) - } - } - .onFailure { error in - self.observerList.forEach { (observer) in - observer.appStorePaymentManager( - self, - transaction: transaction, - accountToken: accountToken, - didFailWithError: error) - } - } - .requestBackgroundTime(taskName: "AppStorePaymentManager.didFinishOrRestorePurchase") - .observe { _ in } - } else { + guard let accountToken = deassociateAccountToken(transaction.payment) else { observerList.forEach { (observer) in observer.appStorePaymentManager( self, @@ -265,7 +238,114 @@ class AppStorePaymentManager: NSObject, SKPaymentTransactionObserver { accountToken: nil, didFailWithError: .noAccountSet) } + return + } + + _ = sendAppStoreReceipt(accountToken: accountToken, forceRefresh: false) { completion in + switch completion { + case .success(let response): + self.paymentQueue.finishTransaction(transaction) + + self.observerList.forEach { observer in + observer.appStorePaymentManager( + self, + transaction: transaction, + accountToken: accountToken, + didFinishWithResponse: response) + } + + case .failure(let error): + self.observerList.forEach { observer in + observer.appStorePaymentManager( + self, + transaction: transaction, + accountToken: accountToken, + didFailWithError: error) + } + + case .cancelled: + break + } } } } + +private class SendAppStoreReceiptOperation: AsyncOperation { + typealias CompletionHandler = (OperationCompletion<REST.CreateApplePaymentResponse, AppStorePaymentManager.Error>) -> Void + + private let restClient: REST.Client + private let accountToken: String + private let forceRefresh: Bool + private let receiptProperties: [String: Any]? + private var completionHandler: CompletionHandler? + private var fetchReceiptCancellable: AnyCancellable? + private var submitReceiptCancellable: AnyCancellable? + + private let logger = Logger(label: "AppStorePaymentManager.SendAppStoreReceiptOperation") + + init(restClient: REST.Client, accountToken: String, forceRefresh: Bool, receiptProperties: [String: Any]?, completionHandler: @escaping CompletionHandler) { + self.restClient = restClient + self.accountToken = accountToken + self.forceRefresh = forceRefresh + self.receiptProperties = receiptProperties + self.completionHandler = completionHandler + } + + override func cancel() { + super.cancel() + + DispatchQueue.main.async { + self.fetchReceiptCancellable?.cancel() + self.fetchReceiptCancellable = nil + + self.submitReceiptCancellable?.cancel() + self.submitReceiptCancellable = nil + } + } + + override func main() { + DispatchQueue.main.async { + self.fetchReceiptCancellable = AppStoreReceipt.fetch(forceRefresh: self.forceRefresh, receiptProperties: self.receiptProperties) { completion in + DispatchQueue.main.async { + switch completion { + case .success(let receiptData): + self.sendReceipt(receiptData) + + case .failure(let error): + self.logger.error(chainedError: error, message: "Failed to fetch the AppStore receipt.") + self.finish(completion: .failure(.readReceipt(error))) + + case .cancelled: + self.finish(completion: .cancelled) + } + } + } + } + } + + private func sendReceipt(_ receiptData: Data) { + submitReceiptCancellable = restClient.createApplePayment(token: self.accountToken, receiptString: receiptData) + .execute { result in + DispatchQueue.main.async { + switch result { + case .success(let response): + self.logger.info("AppStore receipt was processed. Time added: \(response.timeAdded), New expiry: \(response.newExpiry.logFormatDate())") + self.finish(completion: .success(response)) + + case .failure(let error): + self.logger.error(chainedError: error, message: "Failed to send the AppStore receipt.") + self.finish(completion: .failure(.sendReceipt(error))) + } + } + } + } + + private func finish(completion: OperationCompletion<REST.CreateApplePaymentResponse, AppStorePaymentManager.Error>) { + let block = completionHandler + completionHandler = nil + + block?(completion) + finish() + } +} diff --git a/ios/MullvadVPN/AppStoreReceipt.swift b/ios/MullvadVPN/AppStoreReceipt.swift index 995c17e719..be8015f98b 100644 --- a/ios/MullvadVPN/AppStoreReceipt.swift +++ b/ios/MullvadVPN/AppStoreReceipt.swift @@ -11,13 +11,13 @@ import StoreKit enum AppStoreReceipt { enum Error: ChainedError { - /// AppStore receipt file does not exist or file URL is not available + /// AppStore receipt file does not exist or file URL is not available. case doesNotExist - /// IO error + /// IO error. case io(Swift.Error) - /// Failure to refresh the receipt from AppStore + /// Failure to refresh the receipt from AppStore. case refresh(Swift.Error) var errorDescription: String? { @@ -32,7 +32,10 @@ enum AppStoreReceipt { } } - /// An operation queue used to run receipt refresh requests + /// Internal dispatch queue. + private static let dispatchQueue = DispatchQueue(label: "AppStoreReceiptDispatchQueue") + + /// Internal operation queue. private static let operationQueue: OperationQueue = { let queue = OperationQueue() queue.name = "AppStoreReceiptQueue" @@ -42,52 +45,144 @@ enum AppStoreReceipt { /// Read AppStore receipt from disk or refresh it from AppStore if it's missing. /// This call may trigger a sign in with AppStore prompt to appear. - static func fetch(forceRefresh: Bool = false, receiptProperties: [String: Any]? = nil) -> Result<Data, Error>.Promise { - if forceRefresh { - return refreshReceipt(receiptProperties: receiptProperties) + static func fetch(forceRefresh: Bool = false, receiptProperties: [String: Any]? = nil, completionHandler: @escaping (OperationCompletion<Data, Error>) -> Void) -> AnyCancellable { + let operation = FetchAppStoreReceiptOperation( + dispatchQueue: dispatchQueue, + forceRefresh: forceRefresh, + receiptProperties: receiptProperties, + completionHandler: completionHandler + ) + + let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "Fetch AppStore receipt") { + operation.cancel() + } + + operation.completionBlock = { + UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier) + } + + operationQueue.addOperation(operation) + + return AnyCancellable { + operation.cancel() + } + } +} + +fileprivate class FetchAppStoreReceiptOperation: AsyncOperation, SKRequestDelegate { + private var request: SKReceiptRefreshRequest? + private let receiptProperties: [String: Any]? + private let forceRefresh: Bool + + private let dispatchQueue: DispatchQueue + private var completionHandler: ((OperationCompletion<Data, AppStoreReceipt.Error>) -> Void)? + + init(dispatchQueue: DispatchQueue, forceRefresh: Bool, receiptProperties: [String: Any]?, completionHandler: @escaping (OperationCompletion<Data, AppStoreReceipt.Error>) -> Void) { + self.dispatchQueue = dispatchQueue + self.forceRefresh = forceRefresh + self.receiptProperties = receiptProperties + self.completionHandler = completionHandler + } + + override func main() { + dispatchQueue.async { + guard !self.isCancelled else { + self.finish(completion: .cancelled) + return + } + + // Pull receipt from AppStore if requested. + guard !self.forceRefresh else { + self.startRefreshRequest() + return + } + + // Read AppStore receipt from disk. + let result = self.readReceiptFromDisk() + + // Pull receipt from AppStore if it's not cached locally. + if case .failure(.doesNotExist) = result { + self.startRefreshRequest() + } else { + self.finish(completion: OperationCompletion(result: result)) + } + } + } + + override func cancel() { + dispatchQueue.async { + super.cancel() + + self.request?.cancel() + } + } + + // - MARK: SKRequestDelegate + + func requestDidFinish(_ request: SKRequest) { + dispatchQueue.async { + self.didFinishRefreshRequest(error: nil) + } + } + + func request(_ request: SKRequest, didFailWithError error: Error) { + dispatchQueue.async { + self.didFinishRefreshRequest(error: error) + } + } + + // - MARK: Private + + private func startRefreshRequest() { + let request = SKReceiptRefreshRequest(receiptProperties: receiptProperties) + request.delegate = self + request.start() + + self.request = request + } + + private func didFinishRefreshRequest(error: Error?) { + guard !isCancelled else { + finish(completion: .cancelled) + return + } + + let result: Result<Data, AppStoreReceipt.Error> + + if let error = error { + result = .failure(.refresh(error)) } else { - return self.readFromDisk() - .asPromise() - .flatMapErrorThen { error in - if case .doesNotExist = error { - return refreshReceipt(receiptProperties: receiptProperties) - } else { - return .failure(error) - } - } + result = readReceiptFromDisk() } + + finish(completion: OperationCompletion(result: result)) } - /// Read AppStore receipt from disk - private static func readFromDisk() -> Result<Data, Error> { + private func readReceiptFromDisk() -> Result<Data, AppStoreReceipt.Error> { guard let appStoreReceiptURL = Bundle.main.appStoreReceiptURL else { return .failure(.doesNotExist) } - return Result { try Data(contentsOf: appStoreReceiptURL) } - .mapError { (error) -> Error in - if let cocoaError = error as? CocoaError, cocoaError.code == .fileReadNoSuchFile || cocoaError.code == .fileNoSuchFile { - return .doesNotExist - } else { - return .io(error) - } - } - } + let readResult = Result { try Data(contentsOf: appStoreReceiptURL) } - /// Refresh receipt from AppStore - private static func refreshReceipt(receiptProperties: [String: Any]?) -> Result<Data, Error>.Promise { - return Result<(), Swift.Error>.Promise { resolver in - let operation = ReceiptRefreshOperation(receiptProperties: receiptProperties) { result in - resolver.resolve(value: result) + return readResult.mapError { (error) -> AppStoreReceipt.Error in + if let cocoaError = error as? CocoaError, cocoaError.code == .fileReadNoSuchFile || cocoaError.code == .fileNoSuchFile { + return .doesNotExist + } else { + return .io(error) } - self.operationQueue.addOperation(operation) - } - .mapError { error in - return .refresh(error) } - .flatMap { - return Self.readFromDisk() + } + + private func finish(completion: OperationCompletion<Data, AppStoreReceipt.Error>) { + let block = completionHandler + completionHandler = nil + + DispatchQueue.main.async { + block?(completion) } + + finish() } -} +} diff --git a/ios/MullvadVPN/Promise/Cancellable.swift b/ios/MullvadVPN/Cancellable.swift index 966dfb4fd2..4d693c3bcd 100644 --- a/ios/MullvadVPN/Promise/Cancellable.swift +++ b/ios/MullvadVPN/Cancellable.swift @@ -2,8 +2,8 @@ // Cancellable.swift // MullvadVPN // -// Created by pronebird on 23/09/2021. -// Copyright © 2021 Mullvad VPN AB. All rights reserved. +// Created by pronebird on 15/03/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. // import Foundation @@ -21,9 +21,11 @@ class AnyCancellable: Cancellable { } func cancel() { - lock.withCriticalBlock { - self.closure?() - self.closure = nil - } + lock.lock() + let block = closure + closure = nil + lock.unlock() + + block?() } } diff --git a/ios/MullvadVPN/Operations/ExclusivityController.swift b/ios/MullvadVPN/Operations/ExclusivityController.swift index d198ffd497..cfbd3d7668 100644 --- a/ios/MullvadVPN/Operations/ExclusivityController.swift +++ b/ios/MullvadVPN/Operations/ExclusivityController.swift @@ -13,8 +13,6 @@ class ExclusivityController: NSObject { private var operations: [String: [Operation]] = [:] private var categoriesByOperation: [Operation: [String]] = [:] - static let shared = ExclusivityController() - func addOperation(_ operation: Operation, categories: [String]) { lock.withCriticalBlock { categories.forEach { category in diff --git a/ios/MullvadVPN/Operations/OperationCompletion.swift b/ios/MullvadVPN/Operations/OperationCompletion.swift index 21cb92e169..0157075319 100644 --- a/ios/MullvadVPN/Operations/OperationCompletion.swift +++ b/ios/MullvadVPN/Operations/OperationCompletion.swift @@ -21,6 +21,17 @@ enum OperationCompletion<Success, Failure: Error> { } } + var result: Result<Success, Failure>? { + switch self { + case .success(let value): + return .success(value) + case .failure(let error): + return .failure(error) + case .cancelled: + return nil + } + } + init(result: Result<Success, Failure>) { switch result { case .success(let value): diff --git a/ios/MullvadVPN/Operations/ProductsRequestOperation.swift b/ios/MullvadVPN/Operations/ProductsRequestOperation.swift index 6e34e05977..953fc28dab 100644 --- a/ios/MullvadVPN/Operations/ProductsRequestOperation.swift +++ b/ios/MullvadVPN/Operations/ProductsRequestOperation.swift @@ -11,7 +11,7 @@ import StoreKit class ProductsRequestOperation: AsyncOperation, SKProductsRequestDelegate { private let productIdentifiers: Set<String> - private var completionHandler: ((Result<SKProductsResponse, Error>) -> Void)? + private var completionHandler: ((OperationCompletion<SKProductsResponse, Error>) -> Void)? private let maxRetryCount = 10 private let retryDelay: DispatchTimeInterval = .seconds(2) @@ -20,13 +20,7 @@ class ProductsRequestOperation: AsyncOperation, SKProductsRequestDelegate { private var retryTimer: DispatchSourceTimer? private var request: SKProductsRequest? - struct OperationCancelledError: LocalizedError { - var errorDescription: String? { - return "Operation is cancelled" - } - } - - init(productIdentifiers: Set<String>, completionHandler: @escaping (Result<SKProductsResponse, Error>) -> Void) { + init(productIdentifiers: Set<String>, completionHandler: @escaping (OperationCompletion<SKProductsResponse, Error>) -> Void) { self.productIdentifiers = productIdentifiers self.completionHandler = completionHandler @@ -36,7 +30,7 @@ class ProductsRequestOperation: AsyncOperation, SKProductsRequestDelegate { override func main() { DispatchQueue.main.async { guard !self.isCancelled else { - self.finish(with: .failure(OperationCancelledError())) + self.finish(completion: .cancelled) return } @@ -65,14 +59,14 @@ class ProductsRequestOperation: AsyncOperation, SKProductsRequestDelegate { self.retryCount += 1 self.retry(error: error) } else { - self.finish(with: .failure(error)) + self.finish(completion: .failure(error)) } } } func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { DispatchQueue.main.async { - self.finish(with: .success(response)) + self.finish(completion: .success(response)) } } @@ -92,17 +86,17 @@ class ProductsRequestOperation: AsyncOperation, SKProductsRequestDelegate { } retryTimer?.setCancelHandler { [weak self] in - self?.finish(with: .failure(error)) + self?.finish(completion: .failure(error)) } retryTimer?.schedule(wallDeadline: .now() + self.retryDelay) retryTimer?.activate() } - private func finish(with result: Result<SKProductsResponse, Error>) { + private func finish(completion: OperationCompletion<SKProductsResponse, Error>) { assert(Thread.isMainThread) - completionHandler?(result) + completionHandler?(completion) completionHandler = nil finish() diff --git a/ios/MullvadVPN/Operations/ReceiptRefreshOperation.swift b/ios/MullvadVPN/Operations/ReceiptRefreshOperation.swift deleted file mode 100644 index 61d9857904..0000000000 --- a/ios/MullvadVPN/Operations/ReceiptRefreshOperation.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// ReceiptRefreshOperation.swift -// ReceiptRefreshOperation -// -// Created by pronebird on 02/09/2021. -// Copyright © 2021 Mullvad VPN AB. All rights reserved. -// - -import Foundation -import StoreKit - -class ReceiptRefreshOperation: AsyncOperation, SKRequestDelegate { - private let request: SKReceiptRefreshRequest - private var completionHandler: ((Result<(), Error>) -> Void)? - - struct OperationCancelledError: LocalizedError { - var errorDescription: String? { - return "Operation is cancelled" - } - } - - init(receiptProperties: [String: Any]?, completionHandler completion: @escaping (Result<(), Error>) -> Void) { - request = SKReceiptRefreshRequest(receiptProperties: receiptProperties) - completionHandler = completion - } - - override func main() { - DispatchQueue.main.async { - guard !self.isCancelled else { - self.finish(with: .failure(OperationCancelledError())) - return - } - - self.request.delegate = self - self.request.start() - } - } - - override func cancel() { - DispatchQueue.main.async { - super.cancel() - - self.request.cancel() - } - } - - // - MARK: SKRequestDelegate - - func requestDidFinish(_ request: SKRequest) { - DispatchQueue.main.async { - self.finish(with: .success(())) - } - } - - func request(_ request: SKRequest, didFailWithError error: Error) { - DispatchQueue.main.async { - self.finish(with: .failure(error)) - } - } - - private func finish(with result: Result<(), Error>) { - assert(Thread.isMainThread) - - completionHandler?(result) - completionHandler = nil - - finish() - } -} diff --git a/ios/MullvadVPN/ProblemReportViewController.swift b/ios/MullvadVPN/ProblemReportViewController.swift index af22ca98ff..39965754a5 100644 --- a/ios/MullvadVPN/ProblemReportViewController.swift +++ b/ios/MullvadVPN/ProblemReportViewController.swift @@ -597,11 +597,11 @@ class ProblemReportViewController: UIViewController, UITextFieldDelegate, Condit willSendProblemReport() - REST.Client.shared.sendProblemReport(request) - .execute(retryStrategy: .default) - .receive(on: .main) - .observe { completion in - self.didSendProblemReport(viewModel: viewModel, result: completion.unwrappedValue!) + _ = REST.Client.shared.sendProblemReport(request) + .execute(retryStrategy: .default) { result in + DispatchQueue.main.async { + self.didSendProblemReport(viewModel: viewModel, result: result) + } } } diff --git a/ios/MullvadVPN/Promise/AnyOptional.swift b/ios/MullvadVPN/Promise/AnyOptional.swift deleted file mode 100644 index 95382ac864..0000000000 --- a/ios/MullvadVPN/Promise/AnyOptional.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// AnyOptional.swift -// AnyOptional -// -// Created by pronebird on 22/08/2021. -// Copyright © 2021 Mullvad VPN AB. All rights reserved. -// - -import Foundation - -/// Protocol uniting all Optional types. -protocol AnyOptional { - associatedtype Wrapped - - func asConcreteType() -> Optional<Wrapped> -} - -extension Optional: AnyOptional { - func asConcreteType() -> Optional<Wrapped> { - return self - } -} diff --git a/ios/MullvadVPN/Promise/AnyResult.swift b/ios/MullvadVPN/Promise/AnyResult.swift deleted file mode 100644 index 24e8e1fd11..0000000000 --- a/ios/MullvadVPN/Promise/AnyResult.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// AnyResult.swift -// AnyResult -// -// Created by pronebird on 22/08/2021. -// Copyright © 2021 Mullvad VPN AB. All rights reserved. -// - -import Foundation - -/// Protocol uniting all Result types. -protocol AnyResult { - associatedtype Success - associatedtype Failure: Error - - func asConcreteType() -> Result<Success, Failure> -} - -extension Result: AnyResult { - func asConcreteType() -> Result<Success, Failure> { - return self - } -} diff --git a/ios/MullvadVPN/Promise/Promise+BackgroundTask.swift b/ios/MullvadVPN/Promise/Promise+BackgroundTask.swift deleted file mode 100644 index 95561717a8..0000000000 --- a/ios/MullvadVPN/Promise/Promise+BackgroundTask.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// Promise+BackgroundTask.swift -// Promise+BackgroundTask -// -// Created by pronebird on 06/09/2021. -// Copyright © 2021 Mullvad VPN AB. All rights reserved. -// - -import UIKit - -extension Promise { - - /// Start the background task for the duration of the upstream execution. - func requestBackgroundTime(taskName: String? = nil) -> Promise<Value> { - return Promise<Value>(parent: self) { resolver in - var backgroundTaskIdentifier: UIBackgroundTaskIdentifier? - - let beginBackgroundTask = { - backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: taskName) { - resolver.resolve(completion: .cancelled) - } - } - - let endBackgroundTask = { - guard let taskIdentifier = backgroundTaskIdentifier, - taskIdentifier != .invalid else { return } - - UIApplication.shared.endBackgroundTask(taskIdentifier) - backgroundTaskIdentifier = nil - } - - if Thread.isMainThread { - beginBackgroundTask() - } else { - DispatchQueue.main.async(execute: beginBackgroundTask) - } - - self.observe { completion in - resolver.resolve(completion: completion) - - if Thread.isMainThread { - endBackgroundTask() - } else { - DispatchQueue.main.async(execute: endBackgroundTask) - } - } - } - } -} diff --git a/ios/MullvadVPN/Promise/Promise+Delay.swift b/ios/MullvadVPN/Promise/Promise+Delay.swift deleted file mode 100644 index 4cc6cf354a..0000000000 --- a/ios/MullvadVPN/Promise/Promise+Delay.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// Promise+Delay.swift -// Promise+Delay -// -// Created by pronebird on 07/09/2021. -// Copyright © 2021 Mullvad VPN AB. All rights reserved. -// - -import Foundation - -extension Promise { - /// Delay observing the upstream by the given interval. - func delay(by timeInterval: DispatchTimeInterval, timerType: TimerType, queue: DispatchQueue? = nil) -> Promise<Value> { - return Promise<Value>(parent: self) { resolver in - let timer = DispatchSource.makeTimerSource(flags: [], queue: queue) - - let timerCancelHandler = DispatchWorkItem { - resolver.resolve(completion: .cancelled, queue: queue) - } - - timer.setEventHandler { - // Prevent potential further invocation of cancel handler - timerCancelHandler.cancel() - - self.observe { completion in - resolver.resolve(completion: completion, queue: queue) - } - } - - timer.setCancelHandler(handler: timerCancelHandler) - - resolver.setCancelHandler { - timer.cancel() - } - - switch timerType { - case .deadline: - timer.schedule(deadline: .now() + timeInterval) - case .walltime: - timer.schedule(wallDeadline: .now() + timeInterval) - } - - timer.activate() - } - } -} diff --git a/ios/MullvadVPN/Promise/Promise+OperationQueue.swift b/ios/MullvadVPN/Promise/Promise+OperationQueue.swift deleted file mode 100644 index e79e5f25fe..0000000000 --- a/ios/MullvadVPN/Promise/Promise+OperationQueue.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// Promise+OperationQueue.swift -// Promise+OperationQueue -// -// Created by pronebird on 02/09/2021. -// Copyright © 2021 Mullvad VPN AB. All rights reserved. -// - -import Foundation - -extension Promise { - /// Returns a promise that adds a mutually exclusive operation that finishes along with the upstream. - func run(on operationQueue: OperationQueue, categories: [String] = []) -> Promise<Value> { - return Promise(parent: self) { resolver in - let operation = AsyncBlockOperation { operation in - let completionQueue = operationQueue.underlyingQueue - - if operation.isCancelled { - resolver.resolve(completion: .cancelled, queue: completionQueue) - operation.finish() - } else { - self.observe { completion in - resolver.resolve(completion: completion, queue: completionQueue) - operation.finish() - } - } - } - - resolver.setCancelHandler { - operation.cancel() - } - - if !categories.isEmpty { - ExclusivityController.shared.addOperation(operation, categories: categories) - } - - operationQueue.addOperation(operation) - } - } -} diff --git a/ios/MullvadVPN/Promise/Promise+Optional.swift b/ios/MullvadVPN/Promise/Promise+Optional.swift deleted file mode 100644 index eca39ae3e6..0000000000 --- a/ios/MullvadVPN/Promise/Promise+Optional.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// Promise+Optional.swift -// Promise+Optional -// -// Created by pronebird on 22/08/2021. -// Copyright © 2021 Mullvad VPN AB. All rights reserved. -// - -import Foundation - -extension Optional { - func asPromise() -> Promise<Self> { - return .resolved(self) - } -} - -extension Promise where Value: AnyOptional { - /// Map the value when present. Returns `defaultValue` otherwise. - func map<NewValue>(defaultValue: NewValue, transform: @escaping (Value.Wrapped) -> NewValue) -> Promise<NewValue> { - return then { value -> NewValue in - return value.asConcreteType().map(transform) ?? defaultValue - } - } - - /// Map the value when present, producing new promise to compute the new value. Returns `defaultValue` otherwise. - func mapThen<NewValue>(defaultValue: NewValue, producePromise: @escaping (Value.Wrapped) -> Promise<NewValue>) -> Promise<NewValue> { - return then { value in - return value.asConcreteType().map(producePromise) ?? .resolved(defaultValue) - } - } - - /// Map contained value to result providing failure when the value is `nil`. - func some<Failure: Error>(or failure: Failure) -> Result<Value.Wrapped, Failure>.Promise { - return then { value -> Result<Value.Wrapped, Failure> in - return value.asConcreteType().map { .success($0) } ?? .failure(failure) - } - } -} diff --git a/ios/MullvadVPN/Promise/Promise+ReceiveOn.swift b/ios/MullvadVPN/Promise/Promise+ReceiveOn.swift deleted file mode 100644 index ce94f6278e..0000000000 --- a/ios/MullvadVPN/Promise/Promise+ReceiveOn.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// Promise+ReceiveOn.swift -// Promise+ReceiveOn -// -// Created by pronebird on 22/08/2021. -// Copyright © 2021 Mullvad VPN AB. All rights reserved. -// - -import Foundation - -extension Promise { - /// A type of timer. - enum TimerType { - case deadline - case walltime - } - - /// Dispatch the upstream value on another queue. - func receive(on queue: DispatchQueue) -> Promise<Value> { - return Promise<Value>(parent: self) { resolver in - self.observe { completion in - let work = DispatchWorkItem { - resolver.resolve(completion: completion, queue: queue) - } - - resolver.setCancelHandler { - work.cancel() - - resolver.resolve(completion: .cancelled, queue: queue) - } - - queue.async(execute: work) - } - } - } - - /// Dispatch the upstream value on another queue after delay. - func receive(on queue: DispatchQueue, after timeInterval: DispatchTimeInterval, timerType: TimerType) -> Promise<Value> { - return Promise<Value>(parent: self) { resolver in - self.observe { completion in - let work = DispatchWorkItem { - resolver.resolve(completion: completion, queue: queue) - } - - resolver.setCancelHandler { - work.cancel() - - resolver.resolve(completion: .cancelled, queue: queue) - } - - switch timerType { - case .deadline: - queue.asyncAfter(deadline: .now() + timeInterval, execute: work) - - case .walltime: - queue.asyncAfter(wallDeadline: .now() + timeInterval, execute: work) - } - } - } - } -} diff --git a/ios/MullvadVPN/Promise/Promise+Result.swift b/ios/MullvadVPN/Promise/Promise+Result.swift deleted file mode 100644 index e1f0587d4c..0000000000 --- a/ios/MullvadVPN/Promise/Promise+Result.swift +++ /dev/null @@ -1,153 +0,0 @@ -// -// Promise+Result.swift -// Promise+Result -// -// Created by pronebird on 22/08/2021. -// Copyright © 2021 Mullvad VPN AB. All rights reserved. -// - -import Foundation - -typealias _Promise = Promise - -extension Result { - typealias Promise = _Promise<Result<Success, Failure>> -} - -extension Promise where Value: AnyResult { - typealias Success = Value.Success - typealias Failure = Value.Failure - - static func failure(_ error: Failure) -> Result<Success, Failure>.Promise { - return Result<Success, Failure>.Promise(value: .failure(error)) - } - - static func success(_ value: Success) -> Result<Success, Failure>.Promise { - return Result<Success, Failure>.Promise(value: .success(value)) - } - - /// Replace value in Result. Passes failure result downstream. - func setOutput<NewSuccess>(_ newValue: NewSuccess) -> Result<NewSuccess, Failure>.Promise { - return map { _ in - return newValue - } - } - /// Returns a Promise containing resolved value or nil. - func success() -> Promise<Success?> { - return then { result -> Success? in - switch result.asConcreteType() { - case .success(let value): - return value - case .failure: - return nil - } - } - } - - /// Map value. Passes failure result downstream. - func map<NewSuccess>(_ transform: @escaping (Success) -> NewSuccess) -> Result<NewSuccess, Failure>.Promise { - return then { result in - return result.asConcreteType().map(transform) - } - } - - /// Perform action on success. - func onSuccess(_ onResolve: @escaping (Success) -> Void) -> Result<Success, Failure>.Promise { - return map { value -> Success in - onResolve(value) - return value - } - } - - /// Perform action on failure. - func onFailure(_ onResolve: @escaping (Failure) -> Void) -> Result<Success, Failure>.Promise { - return mapError { error -> Failure in - onResolve(error) - return error - } - } - - /// Map value producing Promise. Passes failure result downstream. - func mapThen<NewSuccess>(_ transform: @escaping (Success) -> Result<NewSuccess, Failure>.Promise) -> Result<NewSuccess, Failure>.Promise { - return then { result in - switch result.asConcreteType() { - case .success(let value): - return transform(value) - case .failure(let error): - return .failure(error) - } - } - } - - /// Map failure. Passes successful result downstream. - func mapError<NewFailure>(_ transform: @escaping (Failure) -> NewFailure) -> Result<Success, NewFailure>.Promise { - return then { result in - return result.asConcreteType().mapError(transform) - } - } - - /// Map value to Result. Passes failure result downstream. - func flatMap<NewSuccess>(_ transform: @escaping (Success) -> Result<NewSuccess, Failure>) -> Result<NewSuccess, Failure>.Promise { - return then { result in - return result.asConcreteType().flatMap(transform) - } - } - - /// Map failure to Result. Passes successful result downstream. - func flatMapError<NewFailure>(_ transform: @escaping (Failure) -> Result<Success, NewFailure>) -> Result<Success, NewFailure>.Promise { - return then { result in - return result.asConcreteType().flatMapError(transform) - } - } - - /// Map failure to Result producing Promise. Passes successful result downstream. - func flatMapErrorThen<NewFailure>(_ transform: @escaping (Failure) -> Result<Success, NewFailure>.Promise) -> Result<Success, NewFailure>.Promise { - return then { result in - switch result.asConcreteType() { - case .success(let value): - return .success(value) - case .failure(let error): - return transform(error) - } - } - } -} - -extension Promise where Value: AnyResult { - func tryAwait() throws -> PromiseCompletion<Value.Success> { - return try self.await().map { result in - return try result.asConcreteType().get() - } - } -} - -extension Result { - func asPromise() -> Result<Success, Failure>.Promise { - return .resolved(self) - } - - var error: Failure? { - switch self { - case .success: - return nil - case .failure(let error): - return error - } - } - - var value: Success? { - switch self { - case .success(let value): - return value - case .failure: - return nil - } - } -} - -extension Result where Success: AnyOptional { - /// Same as `value` except it flattens `T??` producing single Optional (`T?`) - var flattenValue: Success.Wrapped? { - return value?.asConcreteType().flatMap { $0 } - } -} diff --git a/ios/MullvadVPN/Promise/Promise.swift b/ios/MullvadVPN/Promise/Promise.swift deleted file mode 100644 index 55091360bf..0000000000 --- a/ios/MullvadVPN/Promise/Promise.swift +++ /dev/null @@ -1,356 +0,0 @@ -// -// Promise.swift -// Promise -// -// Created by pronebird on 03/08/2021. -// Copyright © 2021 Mullvad VPN AB. All rights reserved. -// - -import Foundation - -/// Enum describing the state of the Promise lifecycle. -private enum PromiseState<Value> { - case pending((PromiseResolver<Value>) -> Void) - case executing - case resolved(Value) - case cancelling - case cancelled -} - -/// Class describing a block of asynchronous computation that can either resolve or be cancelled. -final class Promise<Value>: Cancellable { - private var state: PromiseState<Value> - private var observers: [AnyPromiseObserver<Value>] = [] - private let lock = NSRecursiveLock() - - /// Execution queue used for running the Promise body. - private var executionQueue: DispatchQueue? - - /// Completion queue used for delivering results to observers. - private var completionQueue: DispatchQueue? - - /// Parent promise. - private var parent: Cancellable? - - /// Cancellation handler. - private var cancelHandler: (() -> Void)? - - /// Returns Promise resolved with the given value. - class func resolved(_ value: Value) -> Self { - return Self.init(value: value) - } - - /// Returns Promise with lazily resolved value. - class func deferred(_ producer: @escaping () -> Value) -> Self { - return Self.init { resolver in - resolver.resolve(value: producer()) - } - } - - /// Initialize Promise with the execution block. - init(body: @escaping (PromiseResolver<Value>) -> Void) { - state = .pending(body) - } - - /// Initialize Promise with the execution block and parent. - init(parent aParent: Cancellable?, body: @escaping (PromiseResolver<Value>) -> Void) { - state = .pending(body) - parent = aParent - } - - /// Initialize resolved Promise with the given value. - init(value: Value) { - state = .resolved(value) - } - - deinit { - switch state { - case .resolved, .cancelled, .pending: - break - case .executing, .cancelling: - preconditionFailure("\(Self.self) is deallocated in \(state) state without being resolved or cancelled.") - } - } - - /// Observe the result of Promise. - /// This method starts the promise execution if it hasn't started yet. - func observe(_ receiveCompletion: @escaping (PromiseCompletion<Value>) -> Void) { - return lock.withCriticalBlock { - switch state { - case .resolved(let value): - let completion = PromiseCompletion<Value>.finished(value) - completionQueue?.async { receiveCompletion(completion) } ?? receiveCompletion(completion) - - case .cancelled: - let completion = PromiseCompletion<Value>.cancelled - completionQueue?.async { receiveCompletion(completion) } ?? receiveCompletion(completion) - - case .pending: - observers.append(AnyPromiseObserver<Value>(receiveCompletion)) - execute() - - case .executing, .cancelling: - observers.append(AnyPromiseObserver<Value>(receiveCompletion)) - } - } - } - - /// Cancel Promise. - func cancel() { - lock.withCriticalBlock { - switch state { - case .pending: - state = .cancelled - - case .executing: - state = .cancelling - - parent?.cancel() - triggerCancelHandler() - - case .cancelling, .cancelled, .resolved: - break - } - } - } - - /// Trasform the value by producing a promise. - func then<NewValue>(_ onResolve: @escaping (Value) -> Promise<NewValue>) -> Promise<NewValue> { - return Promise<NewValue>(parent: self) { resolver in - self.observe { completion in - switch completion { - case .finished(let value): - let child = onResolve(value) - - resolver.setCancelHandler { - child.cancel() - } - - child.observe { completion in - resolver.resolve(completion: completion) - } - - case .cancelled: - resolver.resolve(completion: .cancelled) - } - } - } - } - - /// Transform the value producing new value. - func then<NewValue>(_ onResolve: @escaping (Value) -> NewValue) -> Promise<NewValue> { - return Promise<NewValue>(parent: self) { resolver in - self.observe { completion in - resolver.resolve(completion: completion.map(onResolve)) - } - } - } - - /// Assign the cancellation token into the given variable. - /// Releasing the cancellation token cancels the given Promise and all downstream Promises. - func storeCancellationToken(in token: inout PromiseCancellationToken?) -> Self { - token = PromiseCancellationToken { [weak self] in - self?.cancel() - } - return self - } - - /// Returns a Promise that does not propagate cancellation to the parent. - func doNotPropagateCancellation() -> Promise<Value> { - return Promise { resolver in - self.observe { completion in - resolver.resolve(completion: completion) - } - } - } - - /// Set the queue on which to execute the promise's body block. - func schedule(on queue: DispatchQueue) -> Self { - return lock.withCriticalBlock { - switch state { - case .pending: - executionQueue = queue - case .cancelling, .cancelled, .executing, .resolved: - break - } - return self - } - } - - /// Block the given queue until the promise finished executing. - func block(on dispatchQueue: DispatchQueue) -> Promise<Value> { - return Promise { resolver in - dispatchQueue.async { - let completion = self.await() - - resolver.resolve(completion: completion) - } - } - } - - /// Block current queue until the promise finished executing. - func await() -> PromiseCompletion<Value> { - let condition = NSCondition() - condition.lock() - defer { condition.unlock() } - - var returnValue: PromiseCompletion<Value>! - observe { completion in - returnValue = completion - condition.signal() - } - - condition.wait() - return returnValue - } - - // MARK: - Private - - /// Execute the promise's body if still pending execution. - private func execute() { - lock.withCriticalBlock { - guard case .pending(let block) = state else { return } - - state = .executing - - let resolver = PromiseResolver(promise: self) - - executionQueue?.async { block(resolver) } ?? block(resolver) - } - } - - /// Resolve Promise with `PromiseCompletion`. - fileprivate func resolve(completion: PromiseCompletion<Value>, queue: DispatchQueue?) { - lock.withCriticalBlock { - switch completion { - case .finished(let value): - resolve(value: value, queue: queue) - case .cancelled: - resolveCancelled(queue: queue) - } - } - } - - /// Resolve Promise with the given value. - /// - /// Provide the optional `queue` parameter which will be used to dispatch the resolved value to observers added - /// after the promise was already resolved. When providing a `queue`, the call to `resolve()` must happen on - /// the same queue. - private func resolve(value: Value, queue: DispatchQueue?) { - lock.withCriticalBlock { - switch state { - case .pending, .executing: - // Oblige caller to resolve the value on the same queue. - queue.map { dispatchPrecondition(condition: .onQueue($0)) } - - completionQueue = queue - state = .resolved(value) - - observers.forEach { observer in - observer.receiveCompletion(.finished(value)) - } - observers.removeAll() - - case .cancelling: - // Oblige caller to resolve the value on the same queue. - queue.map { dispatchPrecondition(condition: .onQueue($0)) } - - completionQueue = queue - state = .cancelled - - observers.forEach { observer in - observer.receiveCompletion(.cancelled) - } - observers.removeAll() - - case .cancelled, .resolved: - break - } - } - } - - private func resolveCancelled(queue: DispatchQueue?) { - lock.withCriticalBlock { - switch state { - case .pending, .executing, .cancelling: - // Oblige caller to resolve the value on the same queue. - queue.map { dispatchPrecondition(condition: .onQueue($0)) } - - completionQueue = queue - state = .cancelled - - observers.forEach { observer in - observer.receiveCompletion(.cancelled) - } - observers.removeAll() - - case .cancelled, .resolved: - break - } - } - } - - /// Set cancellation handler. - fileprivate func setCancelHandler(_ handler: @escaping () -> Void) { - lock.withCriticalBlock { - cancelHandler = handler - } - } - - /// Trigger cancellation handler, then reset it. - private func triggerCancelHandler() { - lock.withCriticalBlock { - let cancelHandlerCopy = cancelHandler - cancelHandler = nil - cancelHandlerCopy?() - } - } - -} - -final class PromiseCancellationToken { - private var handler: (() -> Void)? - private let lock = NSLock() - - fileprivate init(_ aHandler: @escaping () -> Void) { - handler = aHandler - } - - func cancel() { - lock.withCriticalBlock { - handler?() - handler = nil - } - } - - deinit { - cancel() - } -} - -struct PromiseResolver<Value> { - /// Target promise. - private let promise: Promise<Value> - - /// Private initializer. - fileprivate init(promise aPromise: Promise<Value>) { - promise = aPromise - } - - /// Resolve the promise with `PromiseCompletion` and optional queue on which to dispatch the value to observers - /// added after the promise was already resolved. - func resolve(completion: PromiseCompletion<Value>, queue: DispatchQueue? = nil) { - promise.resolve(completion: completion, queue: queue) - } - - /// Resolve the promise with the given value and optional queue on which to dispatch the value to observers added - /// after the promise was already resolved. - func resolve(value: Value, queue: DispatchQueue? = nil) { - promise.resolve(completion: .finished(value), queue: queue) - } - - /// Set cancellation handler. - func setCancelHandler(_ handler: @escaping () -> Void) { - promise.setCancelHandler(handler) - } -} diff --git a/ios/MullvadVPN/Promise/PromiseCompletion.swift b/ios/MullvadVPN/Promise/PromiseCompletion.swift deleted file mode 100644 index 65499ed021..0000000000 --- a/ios/MullvadVPN/Promise/PromiseCompletion.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// PromiseCompletion.swift -// PromiseCompletion -// -// Created by pronebird on 30/08/2021. -// Copyright © 2021 Mullvad VPN AB. All rights reserved. -// - -import Foundation - -/// Promise result type. -enum PromiseCompletion<Value> { - /// Promise is finished with value. - case finished(Value) - - /// Promise is cancelled. - case cancelled - - /// Return the contained value, otherwise `nil`. - var unwrappedValue: Value? { - switch self { - case .finished(let value): - return value - case .cancelled: - return nil - } - } - - /// Returns `true` when the completion is `.cancelled`. - var isCancelled: Bool { - switch self { - case .cancelled: - return true - case .finished: - return false - } - } - - /// Map the contained value, producing new `PromiseCompletion` type. - func map<NewValue>(_ transform: (Value) throws -> NewValue) rethrows -> PromiseCompletion<NewValue> { - switch self { - case .finished(let value): - return .finished(try transform(value)) - case .cancelled: - return .cancelled - } - } -} - -extension PromiseCompletion where Value: AnyOptional { - /// Same as `unwrappedValue` except it flattens `T??` producing single Optional (`T?`) - var flattenUnwrappedValue: Value.Wrapped? { - return unwrappedValue?.asConcreteType().flatMap { $0 } - } -} - -extension PromiseCompletion: Equatable where Value: Equatable { - static func == (lhs: PromiseCompletion<Value>, rhs: PromiseCompletion<Value>) -> Bool { - switch (lhs, rhs) { - case (.finished(let lhsValue), .finished(let rhsValue)): - return lhsValue == rhsValue - case (.cancelled, .cancelled): - return true - case (.finished, .cancelled), (.cancelled, .finished): - return false - } - } -} diff --git a/ios/MullvadVPN/Promise/PromiseObserver.swift b/ios/MullvadVPN/Promise/PromiseObserver.swift deleted file mode 100644 index 89e2059249..0000000000 --- a/ios/MullvadVPN/Promise/PromiseObserver.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// PromiseObserver.swift -// PromiseObserver -// -// Created by pronebird on 22/08/2021. -// Copyright © 2021 Mullvad VPN AB. All rights reserved. -// - -import Foundation - -protocol PromiseObserver { - associatedtype Value - - func receiveCompletion(_ completion: PromiseCompletion<Value>) -} - -final class AnyPromiseObserver<Value>: PromiseObserver { - private let onReceiveCompletion: (PromiseCompletion<Value>) -> Void - - init(_ receiveCompletionHandler: @escaping (PromiseCompletion<Value>) -> Void) { - onReceiveCompletion = receiveCompletionHandler - } - - func receiveCompletion(_ completion: PromiseCompletion<Value>) { - onReceiveCompletion(completion) - } -} diff --git a/ios/MullvadVPN/REST/RESTRequestAdapter.swift b/ios/MullvadVPN/REST/RESTRequestAdapter.swift index b44038e7d0..93b868615e 100644 --- a/ios/MullvadVPN/REST/RESTRequestAdapter.swift +++ b/ios/MullvadVPN/REST/RESTRequestAdapter.swift @@ -22,18 +22,6 @@ extension REST { func execute(retryStrategy: RetryStrategy = RetryStrategy.noRetry, completionHandler: @escaping CompletionHandler) -> AnyCancellable { return self.block(retryStrategy, completionHandler) } - - func execute(retryStrategy: RetryStrategy = RetryStrategy.noRetry) -> Result<Success, REST.Error>.Promise { - return Promise { resolver in - let cancellable = self.execute(retryStrategy: retryStrategy) { result in - resolver.resolve(value: result) - } - - resolver.setCancelHandler { - cancellable.cancel() - } - } - } } diff --git a/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift b/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift index a42d8245df..fdb842afab 100644 --- a/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift +++ b/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift @@ -9,6 +9,7 @@ import BackgroundTasks import Foundation import Logging +import UIKit extension RelayCache { @@ -28,8 +29,13 @@ extension RelayCache { /// A dispatch queue used for thread synchronization private let stateQueue = DispatchQueue(label: "RelayCacheTrackerStateQueue") - /// A dispatch queue used for serializing relay cache updates - private let updateQueue = DispatchQueue(label: "RelayCacheTrackerUpdateQueue") + /// Internal operation queue. + private let operationQueue: OperationQueue = { + let operationQueue = OperationQueue() + operationQueue.name = "RelayCacheTrackerQueue" + operationQueue.maxConcurrentOperationCount = 1 + return operationQueue + }() /// A timer source used for periodic updates private var timerSource: DispatchSourceTimer? @@ -60,7 +66,7 @@ extension RelayCache { stateQueue.async { guard !self.isPeriodicUpdatesEnabled else { return } - self.logger.debug("Start periodic relay updates") + self.logger.debug("Start periodic relay updates.") self.isPeriodicUpdatesEnabled = true @@ -70,7 +76,7 @@ extension RelayCache { self.scheduleRepeatingTimer(startTime: .now() + nextUpdate.timeIntervalSinceNow) case .failure(let readError): - self.logger.error(chainedError: readError, message: "Failed to read the relay cache") + self.logger.error(chainedError: readError, message: "Failed to read the relay cache.") if Self.shouldDownloadRelaysOnReadFailure(readError) { self.scheduleRepeatingTimer(startTime: .now()) @@ -83,7 +89,7 @@ extension RelayCache { stateQueue.async { guard self.isPeriodicUpdatesEnabled else { return } - self.logger.debug("Stop periodic relay updates") + self.logger.debug("Stop periodic relay updates.") self.isPeriodicUpdatesEnabled = false @@ -92,34 +98,37 @@ extension RelayCache { } } - func updateRelays() -> Result<RelayCache.FetchResult, RelayCache.Error>.Promise { - return Promise.deferred { - return RelayCache.IO.read(cacheFileURL: self.cacheFileURL) - } - .schedule(on: stateQueue) - .then { result in - switch result { - case .success(let cachedRelays): - let nextUpdate = cachedRelays.updatedAt.addingTimeInterval(Self.relayUpdateInterval) + func updateRelays(completionHandler: @escaping (OperationCompletion<RelayCache.FetchResult, RelayCache.Error>) -> Void) -> AnyCancellable { + let operation = UpdateRelaysOperation( + dispatchQueue: stateQueue, + restClient: REST.Client.shared, + cacheFileURL: self.cacheFileURL, + relayUpdateInterval: Self.relayUpdateInterval, + updateHandler: { [weak self] newCachedRelays in + guard let self = self else { return } - if nextUpdate <= Date() { - return self.downloadRelays(previouslyCachedRelays: cachedRelays) - } else { - return .success(.throttled) + DispatchQueue.main.async { + self.observerList.forEach { observer in + observer.relayCache(self, didUpdateCachedRelays: newCachedRelays) + } } + }, + completionHandler: completionHandler + ) - case .failure(let readError): - self.logger.error(chainedError: readError, message: "Failed to read the relay cache to determine if it needs to be updated") + let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "Update relays") { + operation.cancel() + } - if Self.shouldDownloadRelaysOnReadFailure(readError) { - return self.downloadRelays(previouslyCachedRelays: nil) - } else { - return .failure(readError) - } - } + operation.completionBlock = { + UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier) + } + + operationQueue.addOperation(operation) + + return AnyCancellable { + operation.cancel() } - .block(on: updateQueue) - .requestBackgroundTime(taskName: "RelayCacheTracker.updateRelays") } func read(completionHandler: @escaping (Result<CachedRelays, RelayCache.Error>) -> Void) { @@ -133,13 +142,13 @@ extension RelayCache { } } - func read() -> Result<CachedRelays, RelayCache.Error>.Promise { - return Promise.deferred { + func readAndWait() -> Result<CachedRelays, RelayCache.Error> { + return stateQueue.sync { return RelayCache.IO.readWithFallback( cacheFileURL: self.cacheFileURL, preBundledRelaysFileURL: self.prebundledRelaysFileURL ) - }.schedule(on: stateQueue) + } } // MARK: - Observation @@ -154,58 +163,12 @@ extension RelayCache { // MARK: - Private instance methods - private func downloadRelays(previouslyCachedRelays: CachedRelays?) -> Result<RelayCache.FetchResult, RelayCache.Error>.Promise { - return REST.Client.shared.getRelays(etag: previouslyCachedRelays?.etag) - .execute() - .receive(on: stateQueue) - .mapError { error in - self.logger.error(chainedError: error, message: "Failed to download relays") - return RelayCache.Error.rest(error) - } - .mapThen { result in - switch result { - case .newContent(let etag, let relays): - let numRelays = relays.wireguard.relays.count - - self.logger.info("Downloaded \(numRelays) relays") - - let cachedRelays = CachedRelays(etag: etag, relays: relays, updatedAt: Date()) - - return RelayCache.IO.write(cacheFileURL: self.cacheFileURL, record: cachedRelays) - .asPromise() - .map { _ in - self.observerList.forEach { (observer) in - observer.relayCache(self, didUpdateCachedRelays: cachedRelays) - } - - return .newContent - } - .onFailure { error in - self.logger.error(chainedError: error, message: "Failed to store downloaded relays") - } - - case .notModified: - self.logger.info("Relays haven't changed since last check.") - - var cachedRelays = previouslyCachedRelays! - cachedRelays.updatedAt = Date() - - return RelayCache.IO.write(cacheFileURL: self.cacheFileURL, record: cachedRelays) - .asPromise() - .map { _ in - return .sameContent - } - .onFailure { error in - self.logger.error(chainedError: error, message: "Failed to update cached relays timestamp") - } - } - } - } - private func scheduleRepeatingTimer(startTime: DispatchWallTime) { let timerSource = DispatchSource.makeTimerSource(queue: stateQueue) timerSource.setEventHandler { [weak self] in - self?.updateRelays().observe { _ in } + _ = self?.updateRelays(completionHandler: { _ in + // no-op + }) } timerSource.schedule(wallDeadline: startTime, repeating: .seconds(Int(Self.relayUpdateInterval))) @@ -273,15 +236,15 @@ extension RelayCache.Tracker { } if isRegistered { - logger.debug("Registered app refresh task") + logger.debug("Registered app refresh task.") } else { - logger.error("Failed to register app refresh task") + logger.error("Failed to register app refresh task.") } } /// Schedules app refresh task relative to the last relays update. - func scheduleAppRefreshTask() -> Result<(), RelayCache.Error>.Promise { - return self.read().flatMap { cachedRelays in + func scheduleAppRefreshTask() -> Result<(), RelayCache.Error> { + return readAndWait().flatMap { cachedRelays in let beginDate = cachedRelays.updatedAt.addingTimeInterval(Self.relayUpdateInterval) return self.submitAppRefreshTask(at: beginDate) @@ -303,34 +266,30 @@ extension RelayCache.Tracker { /// Background task handler private func handleAppRefreshTask(_ task: BGAppRefreshTask) { - var cancellationToken: PromiseCancellationToken? + logger.debug("Start app refresh task.") - self.logger.debug("Start app refresh task") + let cancellable = self.updateRelays { completion in + let isTaskCompleted: Bool - self.updateRelays() - .storeCancellationToken(in: &cancellationToken) - .observe { completion in - let isTaskCompleted: Bool + switch completion { + case .success(let fetchResult): + self.logger.debug("Finished updating relays in app refresh task: \(fetchResult).") + isTaskCompleted = true - switch completion { - case .finished(.success(let fetchResult)): - self.logger.debug("Finished updating relays in app refresh task: \(fetchResult)") - isTaskCompleted = true - - case .finished(.failure(let error)): - self.logger.error(chainedError: error, message: "Failed to update relays in app refresh task") - isTaskCompleted = false - - case .cancelled: - self.logger.debug("App refresh task was cancelled") - isTaskCompleted = false - } + case .failure(let error): + self.logger.error(chainedError: error, message: "Failed to update relays in app refresh task.") + isTaskCompleted = false - task.setTaskCompleted(success: isTaskCompleted) + case .cancelled: + self.logger.debug("App refresh task was cancelled.") + isTaskCompleted = false } + task.setTaskCompleted(success: isTaskCompleted) + } + task.expirationHandler = { - cancellationToken?.cancel() + cancellable.cancel() } // Schedule next refresh @@ -338,10 +297,166 @@ extension RelayCache.Tracker { switch self.submitAppRefreshTask(at: scheduleDate) { case .success: - self.logger.debug("Scheduled next app refresh task at \(scheduleDate.logFormatDate())") + logger.debug("Scheduled next app refresh task at \(scheduleDate.logFormatDate()).") + + case .failure(let error): + logger.error(chainedError: error, message: "Failed to schedule next app refresh task.") + } + } +} + +fileprivate class UpdateRelaysOperation: AsyncOperation { + typealias UpdateHandler = (RelayCache.CachedRelays) -> Void + typealias CompletionHandler = (OperationCompletion<RelayCache.FetchResult, RelayCache.Error>) -> Void + + private let dispatchQueue: DispatchQueue + private let restClient: REST.Client + private let cacheFileURL: URL + private let relayUpdateInterval: TimeInterval + + private let logger = Logger(label: "RelayCacheTracker.UpdateRelaysOperation") + + private let updateHandler: UpdateHandler + private var completionHandler: CompletionHandler? + private var downloadCancellable: AnyCancellable? + + init(dispatchQueue: DispatchQueue, + restClient: REST.Client, + cacheFileURL: URL, + relayUpdateInterval: TimeInterval, + updateHandler: @escaping UpdateHandler, + completionHandler: @escaping CompletionHandler) { + self.dispatchQueue = dispatchQueue + self.restClient = restClient + self.cacheFileURL = cacheFileURL + self.relayUpdateInterval = relayUpdateInterval + self.updateHandler = updateHandler + self.completionHandler = completionHandler + } + + override func main() { + dispatchQueue.async { + guard !self.isCancelled else { + self.finish(completion: .cancelled) + return + } + + let readResult = RelayCache.IO.read(cacheFileURL: self.cacheFileURL) + switch readResult { + case .success(let cachedRelays): + let nextUpdate = cachedRelays.updatedAt.addingTimeInterval(self.relayUpdateInterval) + + if nextUpdate <= Date() { + self.downloadRelays(previouslyCachedRelays: cachedRelays) + } else { + self.finish(completion: .success(.throttled)) + } + + case .failure(let readError): + self.logger.error(chainedError: readError, message: "Failed to read the relay cache to determine if it needs to be updated.") + + if self.shouldDownloadRelaysOnReadFailure(readError) { + self.downloadRelays(previouslyCachedRelays: nil) + } else { + self.finish(completion: .failure(readError)) + } + } + } + } + + override func cancel() { + super.cancel() + + dispatchQueue.async { + self.downloadCancellable?.cancel() + } + } + + private func finish(completion: OperationCompletion<RelayCache.FetchResult, RelayCache.Error>) { + let block = completionHandler + completionHandler = nil + + block?(completion) + + finish() + } + + private func didReceiveNewRelays(etag: String?, relays: REST.ServerRelaysResponse) { + let numRelays = relays.wireguard.relays.count + + logger.info("Downloaded \(numRelays) relays.") + + let cachedRelays = RelayCache.CachedRelays(etag: etag, relays: relays, updatedAt: Date()) + let writeResult = RelayCache.IO.write(cacheFileURL: cacheFileURL, record: cachedRelays) + + switch writeResult { + case .success: + updateHandler(cachedRelays) + + finish(completion: .success(.newContent)) + + case .failure(let error): + logger.error(chainedError: error, message: "Failed to store downloaded relays.") + + finish(completion: .failure(.writeCache(error))) + } + } + + private func didReceiveNotModified(previouslyCachedRelays: RelayCache.CachedRelays) { + logger.info("Relays haven't changed since last check.") + + var cachedRelays = previouslyCachedRelays + cachedRelays.updatedAt = Date() + + let writeResult = RelayCache.IO.write(cacheFileURL: self.cacheFileURL, record: cachedRelays) + + switch writeResult { + case .success: + finish(completion: .success(.sameContent)) case .failure(let error): - self.logger.error(chainedError: error, message: "Failed to schedule next app refresh task") + logger.error(chainedError: error, message: "Failed to update cached relays timestamp.") + + finish(completion: .failure(.writeCache(error))) + } + } + + private func didFailToDownloadRelays(error: REST.Error) { + logger.error(chainedError: error, message: "Failed to download relays.") + + finish(completion: .failure(.rest(error))) + } + + private func downloadRelays(previouslyCachedRelays: RelayCache.CachedRelays?) { + downloadCancellable = REST.Client.shared.getRelays(etag: previouslyCachedRelays?.etag) + .execute { [weak self] result in + guard let self = self else { return } + + self.dispatchQueue.async { + switch result { + case .success(.newContent(let etag, let relays)): + self.didReceiveNewRelays(etag: etag, relays: relays) + + case .success(.notModified): + self.didReceiveNotModified(previouslyCachedRelays: previouslyCachedRelays!) + + case .failure(let error): + self.didFailToDownloadRelays(error: error) + } + } + } + } + + private func shouldDownloadRelaysOnReadFailure(_ error: RelayCache.Error) -> Bool { + switch error { + case .readPrebundledRelays, .decodePrebundledRelays, .decodeCache: + return true + + case .readCache(CocoaError.fileReadNoSuchFile): + return true + + default: + return false } } } diff --git a/ios/MullvadVPN/Result+Extensions.swift b/ios/MullvadVPN/Result+Extensions.swift new file mode 100644 index 0000000000..795ad8901d --- /dev/null +++ b/ios/MullvadVPN/Result+Extensions.swift @@ -0,0 +1,29 @@ +// +// Result+Extensions.swift +// MullvadVPN +// +// Created by pronebird on 15/03/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +extension Result { + var value: Success? { + switch self { + case .success(let value): + return value + case .failure: + return nil + } + } + + var error: Failure? { + switch self { + case .success: + return nil + case .failure(let error): + return error + } + } +} diff --git a/ios/MullvadVPN/Result+UIBackgroundFetchResult.swift b/ios/MullvadVPN/Result+UIBackgroundFetchResult.swift index 544c06dafe..0727d961cd 100644 --- a/ios/MullvadVPN/Result+UIBackgroundFetchResult.swift +++ b/ios/MullvadVPN/Result+UIBackgroundFetchResult.swift @@ -25,7 +25,7 @@ extension AddressCache.CacheUpdateResult { extension Result where Success == RelayCache.FetchResult { var backgroundFetchResult: UIBackgroundFetchResult { - switch self.asConcreteType() { + switch self { case .success(.newContent): return .newData diff --git a/ios/MullvadVPN/WireguardKeysViewController.swift b/ios/MullvadVPN/WireguardKeysViewController.swift index e630285121..b28dad07a2 100644 --- a/ios/MullvadVPN/WireguardKeysViewController.swift +++ b/ios/MullvadVPN/WireguardKeysViewController.swift @@ -33,8 +33,8 @@ class WireguardKeysViewController: UIViewController, TunnelObserver { }() private var publicKeyPeriodicUpdateTimer: DispatchSourceTimer? - private var copyToPasteboardCancellationToken: PromiseCancellationToken? - private var verifyKeyCancellationToken: PromiseCancellationToken? + private var copyToPasteboardWork: DispatchWorkItem? + private var verifyKeyCancellable: AnyCancellable? private let alertPresenter = AlertPresenter() private var state: WireguardKeysViewState = .default { @@ -124,14 +124,15 @@ class WireguardKeysViewController: UIViewController, TunnelObserver { string: NSLocalizedString("COPIED_TO_PASTEBOARD_LABEL", tableName: "WireguardKeys", comment: ""), animated: true) - Promise.deferred { TunnelManager.shared.tunnelInfo?.tunnelSettings } - .delay(by: .seconds(3), timerType: .walltime, queue: .main) - .storeCancellationToken(in: ©ToPasteboardCancellationToken) - .observe { [weak self] completion in - guard let tunnelSettings = completion.unwrappedValue else { return } + let workItem = DispatchWorkItem { [weak self] in + let tunnelSettings = TunnelManager.shared.tunnelInfo?.tunnelSettings - self?.updatePublicKey(tunnelSettings: tunnelSettings, animated: true) - } + self?.updatePublicKey(tunnelSettings: tunnelSettings, animated: true) + } + + DispatchQueue.main.asyncAfter(wallDeadline: .now() + .seconds(3), execute: workItem) + copyToPasteboardWork?.cancel() + copyToPasteboardWork = workItem } @objc private func handleRegenerateKey(_ sender: Any) { @@ -210,28 +211,27 @@ class WireguardKeysViewController: UIViewController, TunnelObserver { self.updateViewState(.verifyingKey) - REST.Client.shared.getWireguardKey(token: tunnelInfo.token, publicKey: tunnelInfo.tunnelSettings.interface.publicKey) - .execute(retryStrategy: .default) - .map { _ in - return true - } - .flatMapError { error -> Result<Bool, REST.Error> in - if case .server(.pubKeyNotFound) = error { - return .success(false) - } else { - return .failure(error) + verifyKeyCancellable?.cancel() + + verifyKeyCancellable = REST.Client.shared.getWireguardKey(token: tunnelInfo.token, publicKey: tunnelInfo.tunnelSettings.interface.publicKey) + .execute(retryStrategy: .default) { [weak self] result in + guard let self = self else { return } + + DispatchQueue.main.async { + switch result { + case .success: + self.updateViewState(.verifiedKey(true)) + + case .failure(let error): + if case .server(.pubKeyNotFound) = error { + self.updateViewState(.verifiedKey(false)) + } else { + self.showKeyVerificationFailureAlert(error) + self.updateViewState(.default) + } + } } } - .receive(on: .main) - .storeCancellationToken(in: &verifyKeyCancellationToken) - .onSuccess { [weak self] isValid in - self?.updateViewState(.verifiedKey(isValid)) - } - .onFailure { [weak self] error in - self?.showKeyVerificationFailureAlert(error) - self?.updateViewState(.default) - } - .observe { _ in } } private func regeneratePrivateKey() { diff --git a/ios/MullvadVPNTests/PromiseTests.swift b/ios/MullvadVPNTests/PromiseTests.swift deleted file mode 100644 index 4adcb0e4e6..0000000000 --- a/ios/MullvadVPNTests/PromiseTests.swift +++ /dev/null @@ -1,298 +0,0 @@ -// -// PromiseTests.swift -// PromiseTests -// -// Created by pronebird on 22/08/2021. -// Copyright © 2021 Mullvad VPN AB. All rights reserved. -// - -import XCTest - -class PromiseTests: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testObserveResolvedPromise() throws { - let expect = expectation(description: "Wait for promise") - - Promise(value: 1) - .observe { completion in - XCTAssertEqual(completion, .finished(1)) - expect.fulfill() - } - - wait(for: [expect], timeout: 1) - } - - func testObservePromise() throws { - let expect = expectation(description: "Wait for promise") - Promise<Int> { resolver in - resolver.resolve(value: 1) - } - .observe { completion in - XCTAssertEqual(completion, .finished(1)) - expect.fulfill() - } - - wait(for: [expect], timeout: 1) - } - - func testReceiveOn() throws { - let expect = expectation(description: "Wait for promise") - let queue = DispatchQueue(label: "TestQueue") - - Promise(value: 1) - .receive(on: queue) - .observe { completion in - dispatchPrecondition(condition: .onQueue(queue)) - expect.fulfill() - } - - wait(for: [expect], timeout: 1) - } - - func testReceiveOnCancellation() { - let expect = expectation(description: "Wait for promise to complete") - - let promise = Promise(value: 1) - .receive(on: .main) - - promise.observe { completion in - XCTAssertEqual(completion, .cancelled) - expect.fulfill() - } - - promise.cancel() - - wait(for: [expect], timeout: 1) - } - - func testDelay() throws { - let expect = expectation(description: "Wait for promise") - let queue = DispatchQueue(label: "TestQueue") - - let startDate = Date() - Promise.deferred { () -> Int in - let elapsed = startDate.timeIntervalSinceNow * -1000 - XCTAssertGreaterThanOrEqual(elapsed, 100) - dispatchPrecondition(condition: .onQueue(queue)) - expect.fulfill() - return 1 - } - .delay(by: .milliseconds(100), timerType: .walltime, queue: queue) - .observe { _ in } - - wait(for: [expect], timeout: 1) - } - - func testDelayCancellation() throws { - let expect = expectation(description: "Should never fulfill") - expect.isInverted = true - - let promise = Promise.deferred { () -> Int in - expect.fulfill() - return 1 - }.delay(by: .milliseconds(100), timerType: .walltime) - - promise.observe { completion in - XCTAssertEqual(completion, .cancelled) - } - - promise.cancel() - - wait(for: [expect], timeout: 1) - } - - func testScheduleOn() throws { - let expect = expectation(description: "Wait for promise") - let queue = DispatchQueue(label: "TestQueue") - - Promise<Int> { resolver in - dispatchPrecondition(condition: .onQueue(queue)) - resolver.resolve(value: 1) - } - .schedule(on: queue) - .observe { completion in - expect.fulfill() - } - - wait(for: [expect], timeout: 1) - } - - func testBlockOn() throws { - let expect1 = expectation(description: "Wait for promise") - let expect2 = expectation(description: "Wait for queue to be unblocked") - let queue = DispatchQueue(label: "TestQueue") - - Promise<Int> { resolver in - DispatchQueue.main.async { - resolver.resolve(value: 1) - } - } - .block(on: queue) - .observe { completion in - dispatchPrecondition(condition: .onQueue(queue)) - expect1.fulfill() - } - - queue.async { - expect2.fulfill() - } - - wait(for: [expect1, expect2], timeout: 1, enforceOrder: true) - } - - func testOptionalMapNoneWithDefaultValue() { - let value: Int? = nil - - value.asPromise() - .map(defaultValue: 1) { _ in - return 2 - }.observe { completion in - XCTAssertEqual(completion.unwrappedValue, 1) - } - } - - func testOptionalMapSomeWithDefaultValue() { - let value: Int? = 0 - - value.asPromise() - .map(defaultValue: 1) { _ in - return 2 - }.observe { completion in - XCTAssertEqual(completion.unwrappedValue, 2) - } - } - - func testRunOnOperationQueue() { - let operationQueue = OperationQueue() - operationQueue.name = "SerialOperationQueue" - operationQueue.maxConcurrentOperationCount = 1 - - let expect1 = expectation(description: "Wait for the first promise") - let expect2 = expectation(description: "Wait for the second promise") - - Promise(value: 1) - .receive(on: .main, after: .milliseconds(100), timerType: .deadline) - .run(on: operationQueue) - .observe { completion in - expect1.fulfill() - } - - Promise(value: 2) - .run(on: operationQueue) - .observe { completion in - expect2.fulfill() - } - - wait(for: [expect1, expect2], timeout: 1, enforceOrder: true) - } - - func testRunOnOperationQueueWithExcusiveCategory() { - let operationQueue = OperationQueue() - operationQueue.name = "ConcurrentOperationQueue" - - let expect1 = expectation(description: "Wait for the first promise") - let expect2 = expectation(description: "Wait for the second promise") - - Promise(value: 1) - .receive(on: .main, after: .milliseconds(100), timerType: .deadline) - .run(on: operationQueue, categories: ["MutuallyExclusive"]) - .observe { completion in - expect1.fulfill() - } - - Promise(value: 2) - .run(on: operationQueue, categories: ["MutuallyExclusive"]) - .observe { completion in - expect2.fulfill() - } - - wait(for: [expect1, expect2], timeout: 1, enforceOrder: true) - } - - func testExecutingPromiseCancellation() throws { - let cancelExpectation = expectation(description: "Expect cancellation handler to trigger") - let completionExpectation = expectation(description: "Expect promise to complete") - - let promise = Promise<Int> { resolver in - let work = DispatchWorkItem { - XCTFail() - resolver.resolve(value: 1) - } - - resolver.setCancelHandler { - work.cancel() - cancelExpectation.fulfill() - - // Resolve promise since `work` is cancelled now. - resolver.resolve(completion: .cancelled) - } - - DispatchQueue.main.async(execute: work) - } - - promise.observe { completion in - XCTAssertEqual(completion, .cancelled) - completionExpectation.fulfill() - } - - promise.cancel() - - wait(for: [cancelExpectation, completionExpectation], timeout: 1, enforceOrder: true) - } - - func testPendingPromiseCancellation() { - let completionExpectation = expectation(description: "Expect promise to complete") - - let promise = Promise.deferred { () -> Int in - XCTFail() - return 1 - } - - promise.cancel() - - promise.observe { completion in - XCTAssertEqual(completion, .cancelled) - completionExpectation.fulfill() - } - - wait(for: [completionExpectation], timeout: 1) - } - - func testUnhandledCancellation() { - let expectObserve = expectation(description: "Wait for observer") - let expectCancelHandler = expectation(description: "Wait for cancellation handler") - let expectResolve = expectation(description: "Wait for resolver") - - let promise = Promise<Bool> { resolver in - resolver.setCancelHandler { - expectCancelHandler.fulfill() - // Do nothing and let the promise continue execution. - } - - DispatchQueue.main.async { - expectResolve.fulfill() - - // Resolve the cancelling promise. This should yield the `.cancelled` completion anyway. - resolver.resolve(value: true) - } - } - - promise.observe { completion in - XCTAssertEqual(completion, .cancelled) - expectObserve.fulfill() - } - - promise.cancel() - - wait(for: [expectCancelHandler, expectResolve, expectObserve], timeout: 1, enforceOrder: true) - } - -} |
