summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2022-03-15 12:45:35 +0100
committerAndrej Mihajlov <and@mullvad.net>2022-03-24 14:00:24 +0100
commit0cd501d8173087cc46593e6aaf00a15c0f3ca25f (patch)
tree8144b8f60d8165b7261a4ee109b0f5963b554328
parentc69ebd383d617efbba1b0afac98db783f0143b3d (diff)
downloadmullvadvpn-0cd501d8173087cc46593e6aaf00a15c0f3ca25f.tar.xz
mullvadvpn-0cd501d8173087cc46593e6aaf00a15c0f3ca25f.zip
Drop Promises
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj112
-rw-r--r--ios/MullvadVPN/Account.swift205
-rw-r--r--ios/MullvadVPN/AccountViewController.swift80
-rw-r--r--ios/MullvadVPN/AppDelegate.swift93
-rw-r--r--ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManager.swift196
-rw-r--r--ios/MullvadVPN/AppStoreReceipt.swift173
-rw-r--r--ios/MullvadVPN/Cancellable.swift (renamed from ios/MullvadVPN/Promise/Cancellable.swift)14
-rw-r--r--ios/MullvadVPN/Operations/ExclusivityController.swift2
-rw-r--r--ios/MullvadVPN/Operations/OperationCompletion.swift11
-rw-r--r--ios/MullvadVPN/Operations/ProductsRequestOperation.swift22
-rw-r--r--ios/MullvadVPN/Operations/ReceiptRefreshOperation.swift69
-rw-r--r--ios/MullvadVPN/ProblemReportViewController.swift10
-rw-r--r--ios/MullvadVPN/Promise/AnyOptional.swift22
-rw-r--r--ios/MullvadVPN/Promise/AnyResult.swift23
-rw-r--r--ios/MullvadVPN/Promise/Promise+BackgroundTask.swift49
-rw-r--r--ios/MullvadVPN/Promise/Promise+Delay.swift46
-rw-r--r--ios/MullvadVPN/Promise/Promise+OperationQueue.swift40
-rw-r--r--ios/MullvadVPN/Promise/Promise+Optional.swift38
-rw-r--r--ios/MullvadVPN/Promise/Promise+ReceiveOn.swift61
-rw-r--r--ios/MullvadVPN/Promise/Promise+Result.swift153
-rw-r--r--ios/MullvadVPN/Promise/Promise.swift356
-rw-r--r--ios/MullvadVPN/Promise/PromiseCompletion.swift68
-rw-r--r--ios/MullvadVPN/Promise/PromiseObserver.swift27
-rw-r--r--ios/MullvadVPN/REST/RESTRequestAdapter.swift12
-rw-r--r--ios/MullvadVPN/RelayCache/RelayCacheTracker.swift329
-rw-r--r--ios/MullvadVPN/Result+Extensions.swift29
-rw-r--r--ios/MullvadVPN/Result+UIBackgroundFetchResult.swift2
-rw-r--r--ios/MullvadVPN/WireguardKeysViewController.swift58
-rw-r--r--ios/MullvadVPNTests/PromiseTests.swift298
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: &copyToPasteboardCancellationToken)
- .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: &copyToPasteboardCancellationToken)
- .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)
- }
-
-}