summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2022-06-22 11:39:30 +0200
committerAndrej Mihajlov <and@mullvad.net>2022-06-22 11:39:30 +0200
commite508a91977d4ccf1390f0e002da4e1fd688fa30c (patch)
tree728e7d9d5503e0d30d73e7812516968ce7edfc6d
parent1a585cae979e3e9e6e190331a1c3988d1f6c1a08 (diff)
parentc6ba4af468a8abbd2448eafd4286afba744a5f15 (diff)
downloadmullvadvpn-e508a91977d4ccf1390f0e002da4e1fd688fa30c.tar.xz
mullvadvpn-e508a91977d4ccf1390f0e002da4e1fd688fa30c.zip
Merge branch 'scene-delegate'
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj24
-rw-r--r--ios/MullvadVPN/AppDelegate.swift649
-rw-r--r--ios/MullvadVPN/Info.plist5
-rw-r--r--ios/MullvadVPN/SceneDelegate.swift707
-rw-r--r--ios/MullvadVPN/SimulatorTunnelProvider.swift2
-rw-r--r--ios/MullvadVPN/TermsOfService.swift21
-rw-r--r--ios/MullvadVPN/TermsOfServiceContentView.swift (renamed from ios/MullvadVPN/ConsentContentView.swift)12
-rw-r--r--ios/MullvadVPN/TermsOfServiceViewController.swift (renamed from ios/MullvadVPN/ConsentViewController.swift)6
-rw-r--r--ios/MullvadVPNScreenshots/MullvadVPNScreenshots.swift2
9 files changed, 810 insertions, 618 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 81453daf7e..04b69c8b65 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -93,7 +93,7 @@
58421030282D8A3C00F24E46 /* UpdateAccountDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5842102F282D8A3C00F24E46 /* UpdateAccountDataOperation.swift */; };
58421032282E42B000F24E46 /* UpdateDeviceDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58421031282E42B000F24E46 /* UpdateDeviceDataOperation.swift */; };
58421034282E4B1500F24E46 /* TunnelSettingsV2+REST.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58421033282E4B1500F24E46 /* TunnelSettingsV2+REST.swift */; };
- 584592612639B4A200EF967F /* ConsentContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584592602639B4A200EF967F /* ConsentContentView.swift */; };
+ 584592612639B4A200EF967F /* TermsOfServiceContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584592602639B4A200EF967F /* TermsOfServiceContentView.swift */; };
5846226526E0D9630035F7C2 /* ProductsRequestOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846226426E0D9630035F7C2 /* ProductsRequestOperation.swift */; };
5846227126E229F20035F7C2 /* AppStoreSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846227026E229F20035F7C2 /* AppStoreSubscription.swift */; };
5846227326E22A160035F7C2 /* AppStorePaymentObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846227226E22A160035F7C2 /* AppStorePaymentObserver.swift */; };
@@ -162,6 +162,7 @@
5871FB96254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5871FB95254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift */; };
5871FBA0254C26C00051A0A4 /* NSRegularExpression+IPAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5871FB9F254C26BF0051A0A4 /* NSRegularExpression+IPAddress.swift */; };
58727283265D173C00F315B2 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 58727282265D173C00F315B2 /* LaunchScreen.storyboard */; };
+ 5872D6E8286304DE00DB5F4E /* TermsOfService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5872D6E7286304DE00DB5F4E /* TermsOfService.swift */; };
587425C12299833500CA2045 /* RootContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587425C02299833500CA2045 /* RootContainerViewController.swift */; };
5875960726F36B3A00BF6711 /* TunnelIPCError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5875960626F36B3A00BF6711 /* TunnelIPCError.swift */; };
5875960A26F371FC00BF6711 /* TunnelIPCSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5875960926F371FC00BF6711 /* TunnelIPCSession.swift */; };
@@ -214,7 +215,7 @@
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 */; };
- 58A99ED3240014A0006599E9 /* ConsentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A99ED2240014A0006599E9 /* ConsentViewController.swift */; };
+ 58A99ED3240014A0006599E9 /* TermsOfServiceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A99ED2240014A0006599E9 /* TermsOfServiceViewController.swift */; };
58ACF6492655365700ACE4B7 /* PreferencesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ACF6482655365700ACE4B7 /* PreferencesViewController.swift */; };
58ACF64B26553C3F00ACE4B7 /* SettingsSwitchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ACF64A26553C3F00ACE4B7 /* SettingsSwitchCell.swift */; };
58ACF64D26567A5000ACE4B7 /* CustomSwitch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ACF64C26567A4F00ACE4B7 /* CustomSwitch.swift */; };
@@ -268,6 +269,7 @@
58DF5B7F2852778600E92647 /* OperationSmokeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DF5B7E2852778600E92647 /* OperationSmokeTests.swift */; };
58E0A98827C8F46300FE6BDD /* Tunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E0A98727C8F46300FE6BDD /* Tunnel.swift */; };
58E20771274672CA00DE5D77 /* LaunchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E20770274672CA00DE5D77 /* LaunchViewController.swift */; };
+ 58E25F812837BBBB002CFB2C /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E25F802837BBBB002CFB2C /* SceneDelegate.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 */; };
@@ -411,7 +413,7 @@
5842102F282D8A3C00F24E46 /* UpdateAccountDataOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateAccountDataOperation.swift; sourceTree = "<group>"; };
58421031282E42B000F24E46 /* UpdateDeviceDataOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateDeviceDataOperation.swift; sourceTree = "<group>"; };
58421033282E4B1500F24E46 /* TunnelSettingsV2+REST.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TunnelSettingsV2+REST.swift"; sourceTree = "<group>"; };
- 584592602639B4A200EF967F /* ConsentContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsentContentView.swift; sourceTree = "<group>"; };
+ 584592602639B4A200EF967F /* TermsOfServiceContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TermsOfServiceContentView.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>"; };
5846227026E229F20035F7C2 /* AppStoreSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreSubscription.swift; sourceTree = "<group>"; };
@@ -459,6 +461,7 @@
5871FB95254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsolidatedApplicationLog.swift; sourceTree = "<group>"; };
5871FB9F254C26BF0051A0A4 /* NSRegularExpression+IPAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSRegularExpression+IPAddress.swift"; sourceTree = "<group>"; };
58727282265D173C00F315B2 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
+ 5872D6E7286304DE00DB5F4E /* TermsOfService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TermsOfService.swift; sourceTree = "<group>"; };
587425C02299833500CA2045 /* RootContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootContainerViewController.swift; sourceTree = "<group>"; };
5875960626F36B3A00BF6711 /* TunnelIPCError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelIPCError.swift; sourceTree = "<group>"; };
5875960926F371FC00BF6711 /* TunnelIPCSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelIPCSession.swift; sourceTree = "<group>"; };
@@ -502,7 +505,7 @@
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>"; };
- 58A99ED2240014A0006599E9 /* ConsentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsentViewController.swift; sourceTree = "<group>"; };
+ 58A99ED2240014A0006599E9 /* TermsOfServiceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TermsOfServiceViewController.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>"; };
58ACF64C26567A4F00ACE4B7 /* CustomSwitch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSwitch.swift; sourceTree = "<group>"; };
@@ -549,6 +552,7 @@
58DF5B7E2852778600E92647 /* OperationSmokeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationSmokeTests.swift; sourceTree = "<group>"; };
58E0A98727C8F46300FE6BDD /* Tunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tunnel.swift; sourceTree = "<group>"; };
58E20770274672CA00DE5D77 /* LaunchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchViewController.swift; sourceTree = "<group>"; };
+ 58E25F802837BBBB002CFB2C /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
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>"; };
58ECD29123F178FD004298B6 /* Screenshots.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Screenshots.xcconfig; sourceTree = "<group>"; };
@@ -880,8 +884,8 @@
58A1AA8B23F5584B009F7EA6 /* ConnectionPanelView.swift */,
58B43C1825F77DB60002C8C3 /* ConnectMainContentView.swift */,
58CCA00F224249A1004F3011 /* ConnectViewController.swift */,
- 584592602639B4A200EF967F /* ConsentContentView.swift */,
- 58A99ED2240014A0006599E9 /* ConsentViewController.swift */,
+ 584592602639B4A200EF967F /* TermsOfServiceContentView.swift */,
+ 58A99ED2240014A0006599E9 /* TermsOfServiceViewController.swift */,
5871FB95254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift */,
5896AE83246D5889005B36CB /* CustomDateComponentsFormatting.swift */,
582BB1B0229569620055B6EF /* CustomNavigationBar.swift */,
@@ -939,6 +943,7 @@
58F1311427E0B2AB007AC5BC /* Result+Extensions.swift */,
5820676726E79E7B00655B05 /* Result+UIBackgroundFetchResult.swift */,
587425C02299833500CA2045 /* RootContainerViewController.swift */,
+ 58E25F802837BBBB002CFB2C /* SceneDelegate.swift */,
5888AD82227B11080051EB06 /* SelectLocationCell.swift */,
5857F24224C8662600CF6F47 /* SelectLocationHeaderView.swift */,
5857F24624C882D700CF6F47 /* SelectLocationNavigationController.swift */,
@@ -974,6 +979,7 @@
58FD5BF12424F7D700112C88 /* UserInterfaceInteractionRestriction.swift */,
58F7CA872692E34000FC59FD /* WireguardKeysContentView.swift */,
5877152F23981F7B001F8237 /* WireguardKeysViewController.swift */,
+ 5872D6E7286304DE00DB5F4E /* TermsOfService.swift */,
);
path = MullvadVPN;
sourceTree = "<group>";
@@ -1373,7 +1379,7 @@
5888AD87227B17950051EB06 /* SelectLocationViewController.swift in Sources */,
58293FB3251241B4005D0BB5 /* CustomTextView.swift in Sources */,
58F19E35228C15BA00C7710B /* SpinnerActivityIndicatorView.swift in Sources */,
- 58A99ED3240014A0006599E9 /* ConsentViewController.swift in Sources */,
+ 58A99ED3240014A0006599E9 /* TermsOfServiceViewController.swift in Sources */,
58CCA0162242560B004F3011 /* UIColor+Palette.swift in Sources */,
58095C4F2760BA9100890776 /* AddressCacheStore.swift in Sources */,
587CBFE322807F530028DED3 /* UIColor+Helpers.swift in Sources */,
@@ -1395,6 +1401,7 @@
584E96BC240FD4DA00D3334F /* Location.swift in Sources */,
58554F73280AFA5A00013055 /* RESTAuthenticationProxy.swift in Sources */,
581503A124D6F01F00C9C50E /* LogRotation.swift in Sources */,
+ 58E25F812837BBBB002CFB2C /* SceneDelegate.swift in Sources */,
585E820327F3285E00939F0E /* SendAppStoreReceiptOperation.swift in Sources */,
584B17AB27637DE40057F3B8 /* ReloadTunnelOperation.swift in Sources */,
5820676426E771DB00655B05 /* TunnelManagerError.swift in Sources */,
@@ -1430,6 +1437,7 @@
5820675E26E6839900655B05 /* PresentAlertOperation.swift in Sources */,
5815039D24D6ECE600C9C50E /* TextFileOutputStream.swift in Sources */,
58CE5E64224146200008646E /* AppDelegate.swift in Sources */,
+ 5872D6E8286304DE00DB5F4E /* TermsOfService.swift in Sources */,
58E0A98827C8F46300FE6BDD /* Tunnel.swift in Sources */,
58ACF64F26567A7100ACE4B7 /* CustomSwitchContainer.swift in Sources */,
5857F24324C8662600CF6F47 /* SelectLocationHeaderView.swift in Sources */,
@@ -1452,7 +1460,7 @@
5868BD33261DCD2600E6027F /* CustomSplitViewController.swift in Sources */,
58CCA01E2242787B004F3011 /* AccountTextField.swift in Sources */,
586E54FB27A2DF6D0029B88B /* TunnelIPCRequestOperation.swift in Sources */,
- 584592612639B4A200EF967F /* ConsentContentView.swift in Sources */,
+ 584592612639B4A200EF967F /* TermsOfServiceContentView.swift in Sources */,
58B5A895280AACC4009FDE99 /* RESTRequestFactory.swift in Sources */,
584EBDBD2747C98F00A0C9FD /* NSAttributedString+Markdown.swift in Sources */,
58059DDC28465E8F002B1049 /* TransformOperation.swift in Sources */,
diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift
index 1084014e3d..bfdd04f016 100644
--- a/ios/MullvadVPN/AppDelegate.swift
+++ b/ios/MullvadVPN/AppDelegate.swift
@@ -7,43 +7,28 @@
//
import UIKit
+import BackgroundTasks
import StoreKit
import UserNotifications
import Logging
-import BackgroundTasks
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
-
- var window: UIWindow?
-
private var logger: Logger!
#if targetEnvironment(simulator)
private let simulatorTunnelProvider = SimulatorTunnelProviderHost()
#endif
- private lazy var occlusionWindow: UIWindow = {
- let window = UIWindow(frame: UIScreen.main.bounds)
- window.rootViewController = LaunchViewController()
- window.windowLevel = .alert + 1
- return window
- }()
-
- private var rootContainer: RootContainerViewController?
- private var splitViewController: CustomSplitViewController?
- private var selectLocationViewController: SelectLocationViewController?
- private var connectController: ConnectViewController?
- private weak var settingsNavController: SettingsNavigationController?
-
- private var relayConstraints: RelayConstraints?
-
private let operationQueue: AsyncOperationQueue = {
let operationQueue = AsyncOperationQueue()
operationQueue.maxConcurrentOperationCount = 1
return operationQueue
}()
+ // An instance of scene delegate used on iOS 12 or earlier.
+ private var sceneDelegate: SceneDelegate?
+
// MARK: - Application lifecycle
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
@@ -70,9 +55,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
setupPaymentHandler()
setupNotificationHandler()
- // Add relay cache observer
- RelayCache.Tracker.shared.addObserver(self)
-
// Start initialization
let setupTunnelManagerOperation = AsyncBlockOperation(dispatchQueue: .main) { blockOperation in
TunnelManager.shared.loadConfiguration { error in
@@ -92,24 +74,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
let setupUIOperation = AsyncBlockOperation(dispatchQueue: .main) {
- self.logger.debug("Finished initialization. Show user interface.")
-
- self.relayConstraints = TunnelManager.shared.tunnelSettings?.relayConstraints
-
- self.rootContainer = RootContainerViewController()
- self.rootContainer?.delegate = self
- self.window?.rootViewController = self.rootContainer
-
- switch UIDevice.current.userInterfaceIdiom {
- case .pad:
- self.setupPadUI()
-
- case .phone:
- self.setupPhoneUI()
-
- default:
- fatalError()
- }
+ self.logger.debug("Finished initialization.")
NotificationManager.shared.updateNotifications()
AppStorePaymentManager.shared.startPaymentQueueMonitoring()
@@ -120,53 +85,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
setupUIOperation
], waitUntilFinished: false)
- // Create an app window
- self.window = UIWindow(frame: UIScreen.main.bounds)
-
- // Set an empty view controller while loading tunnels
- self.window?.rootViewController = LaunchViewController()
-
- // Show the window
- self.window?.makeKeyAndVisible()
-
- return true
- }
-
- func applicationDidBecomeActive(_ application: UIApplication) {
- // Refresh tunnel status.
- TunnelManager.shared.refreshTunnelStatus()
-
- // Start periodic relays updates
- RelayCache.Tracker.shared.startPeriodicUpdates()
-
- // Start periodic private key rotation
- TunnelManager.shared.startPeriodicPrivateKeyRotation()
-
- // Start periodic API address list updates
- AddressCache.Tracker.shared.startPeriodicUpdates()
-
- // Reveal application content
- occlusionWindow.isHidden = true
- window?.makeKeyAndVisible()
- }
-
- func applicationWillResignActive(_ application: UIApplication) {
- // Stop periodic relays updates
- RelayCache.Tracker.shared.stopPeriodicUpdates()
-
- // Stop periodic private key rotation
- TunnelManager.shared.stopPeriodicPrivateKeyRotation()
-
- // Stop periodic API address list updates
- AddressCache.Tracker.shared.stopPeriodicUpdates()
-
- // Hide application content
- occlusionWindow.makeKeyAndVisible()
- }
-
- func applicationDidEnterBackground(_ application: UIApplication) {
if #available(iOS 13, *) {
- scheduleBackgroundTasks()
+ return true
+ } else {
+ sceneDelegate = SceneDelegate()
+ sceneDelegate?.setupScene(windowFactory: ClassicWindowFactory())
+
+ return true
}
}
@@ -259,7 +184,37 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
operationQueue.addOperation(completeOperation)
}
+ // MARK: - UISceneSession lifecycle
+
+ @available(iOS 13.0, *)
+ func application(
+ _ application: UIApplication,
+ configurationForConnecting connectingSceneSession: UISceneSession,
+ options: UIScene.ConnectionOptions
+ ) -> UISceneConfiguration {
+ // Called when a new scene session is being created.
+ // Use this method to select a configuration to create the new scene with.
+ let sceneConfiguration = UISceneConfiguration(
+ name: "Default Configuration",
+ sessionRole: connectingSceneSession.role
+ )
+ sceneConfiguration.delegateClass = SceneDelegate.self
+
+ return sceneConfiguration
+ }
+
+ @available(iOS 13.0, *)
+ func application(
+ _ application: UIApplication,
+ didDiscardSceneSessions sceneSessions: Set<UISceneSession>
+ ) {
+ // Called when the user discards a scene session.
+ // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
+ // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
+ }
+
// MARK: - Background tasks
+
@available(iOS 13, *)
private func registerBackgroundTasks() {
registerAppRefreshTask()
@@ -427,477 +382,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
]
UNUserNotificationCenter.current().delegate = self
}
-
- private func setupPadUI() {
- let selectLocationController = makeSelectLocationController()
- let connectController = makeConnectViewController()
-
- let splitViewController = CustomSplitViewController()
- splitViewController.delegate = self
- splitViewController.minimumPrimaryColumnWidth = UIMetrics.minimumSplitViewSidebarWidth
- splitViewController.preferredPrimaryColumnWidthFraction = UIMetrics.maximumSplitViewSidebarWidthFraction
- splitViewController.primaryEdge = .trailing
- splitViewController.dividerColor = UIColor.MainSplitView.dividerColor
- splitViewController.viewControllers = [selectLocationController, connectController]
-
- self.selectLocationViewController = selectLocationController
- self.splitViewController = splitViewController
- self.connectController = connectController
-
- self.rootContainer?.setViewControllers([splitViewController], animated: false)
- showSplitViewMaster(TunnelManager.shared.isAccountSet, animated: false)
-
- let rootContainerWrapper = makeLoginContainerController()
-
- if !isAgreedToTermsOfService() {
- let consentViewController = self.makeConsentController { [weak self] (viewController) in
- guard let self = self else { return }
-
- if TunnelManager.shared.isAccountSet {
- rootContainerWrapper.dismiss(animated: true) {
- self.showAccountSettingsControllerIfAccountExpired()
- }
- } else {
- rootContainerWrapper.pushViewController(self.makeLoginController(), animated: true)
- }
- }
- rootContainerWrapper.setViewControllers([consentViewController], animated: false)
- self.rootContainer?.present(rootContainerWrapper, animated: false)
- } else if !TunnelManager.shared.isAccountSet {
- rootContainerWrapper.setViewControllers([makeLoginController()], animated: false)
- self.rootContainer?.present(rootContainerWrapper, animated: false)
- } else {
- self.showAccountSettingsControllerIfAccountExpired()
- }
- }
-
- private func setupPhoneUI() {
- let showNextController = { [weak self] (_ animated: Bool) in
- guard let self = self else { return }
-
- let loginViewController = self.makeLoginController()
- var viewControllers: [UIViewController] = [loginViewController]
-
- if TunnelManager.shared.isAccountSet {
- let connectController = self.makeConnectViewController()
- viewControllers.append(connectController)
- self.connectController = connectController
- }
-
- self.rootContainer?.setViewControllers(viewControllers, animated: animated) {
- self.showAccountSettingsControllerIfAccountExpired()
- }
- }
-
- if isAgreedToTermsOfService() {
- showNextController(false)
- } else {
- let consentViewController = self.makeConsentController { (consentController) in
- showNextController(true)
- }
-
- self.rootContainer?.setViewControllers([consentViewController], animated: false)
- }
- }
-
- private func makeConnectViewController() -> ConnectViewController {
- let connectController = ConnectViewController()
- connectController.delegate = self
- NotificationManager.shared.delegate = self
-
- return connectController
- }
-
- private func makeSelectLocationController() -> SelectLocationViewController {
- let selectLocationController = SelectLocationViewController()
- selectLocationController.delegate = self
-
- if let cachedRelays = RelayCache.Tracker.shared.getCachedRelays() {
- selectLocationController.setCachedRelays(cachedRelays)
- }
-
- if let relayLocation = relayConstraints?.location.value {
- selectLocationController.setSelectedRelayLocation(
- relayLocation,
- animated: false,
- scrollPosition: .middle
- )
- }
-
- return selectLocationController
- }
-
- private func makeConsentController(completion: @escaping (UIViewController) -> Void) -> ConsentViewController {
- let consentViewController = ConsentViewController()
-
- if UIDevice.current.userInterfaceIdiom == .pad {
- consentViewController.modalPresentationStyle = .formSheet
- if #available(iOS 13.0, *) {
- consentViewController.isModalInPresentation = true
- }
- }
-
- consentViewController.completionHandler = { (consentViewController) in
- setAgreedToTermsOfService()
- completion(consentViewController)
- }
-
- return consentViewController
- }
-
- private func makeLoginContainerController() -> RootContainerViewController {
- let rootContainerWrapper = RootContainerViewController()
- rootContainerWrapper.delegate = self
- rootContainerWrapper.preferredContentSize = CGSize(width: 480, height: 600)
-
- if UIDevice.current.userInterfaceIdiom == .pad {
- rootContainerWrapper.modalPresentationStyle = .formSheet
- if #available(iOS 13.0, *) {
- // Prevent swiping off the login or consent controllers
- rootContainerWrapper.isModalInPresentation = true
- }
- }
-
- rootContainerWrapper.presentationController?.delegate = self
-
- return rootContainerWrapper
- }
-
- private func makeLoginController() -> LoginViewController {
- let controller = LoginViewController()
- controller.delegate = self
- return controller
- }
-
- private func makeSettingsNavigationController(route: SettingsNavigationRoute?) -> SettingsNavigationController {
- let navController = SettingsNavigationController()
- navController.settingsDelegate = self
-
- if UIDevice.current.userInterfaceIdiom == .pad {
- navController.preferredContentSize = CGSize(width: 480, height: 568)
- navController.modalPresentationStyle = .formSheet
- }
-
- navController.presentationController?.delegate = navController
-
- if let route = route {
- navController.navigate(to: route, animated: false)
- }
-
- return navController
- }
-
- private func showAccountSettingsControllerIfAccountExpired() {
- guard let accountExpiry = TunnelManager.shared.accountExpiry, accountExpiry <= Date() else { return }
-
- rootContainer?.showSettings(navigateTo: .account, animated: true)
- }
-
- private func showSplitViewMaster(_ show: Bool, animated: Bool) {
- if show {
- splitViewController?.preferredDisplayMode = .allVisible
- connectController?.setMainContentHidden(false, animated: animated)
- } else {
- splitViewController?.preferredDisplayMode = .primaryHidden
- connectController?.setMainContentHidden(true, animated: animated)
- }
- }
-}
-
-// MARK: - RootContainerViewControllerDelegate
-
-extension AppDelegate: RootContainerViewControllerDelegate {
-
- func rootContainerViewControllerShouldShowSettings(_ controller: RootContainerViewController, navigateTo route: SettingsNavigationRoute?, animated: Bool) {
- // Check if settings controller is already presented.
- if let settingsNavController = self.settingsNavController {
- if let route = route {
- settingsNavController.navigate(to: route, animated: animated)
- } else {
- settingsNavController.popToRootViewController(animated: animated)
- }
- } else {
- let navController = makeSettingsNavigationController(route: route)
-
- // On iPad the login controller can be presented modally above the root container.
- // in that case we have to use the presented controller to present the next modal.
- if let presentedController = controller.presentedViewController {
- presentedController.present(navController, animated: true)
- } else {
- controller.present(navController, animated: true)
- }
-
- // Save the reference for later.
- self.settingsNavController = navController
- }
- }
-
- func rootContainerViewSupportedInterfaceOrientations(_ controller: RootContainerViewController) -> UIInterfaceOrientationMask {
- switch UIDevice.current.userInterfaceIdiom {
- case .pad:
- return [.landscape, .portrait]
- case .phone:
- return [.portrait]
- default:
- return controller.supportedInterfaceOrientations
- }
- }
-
- func rootContainerViewAccessibilityPerformMagicTap(_ controller: RootContainerViewController) -> Bool {
- guard TunnelManager.shared.isAccountSet else { return false }
-
- switch TunnelManager.shared.tunnelState {
- case .connected, .connecting, .reconnecting:
- TunnelManager.shared.reconnectTunnel(completionHandler: nil)
- case .disconnecting, .disconnected:
- TunnelManager.shared.startTunnel()
- case .pendingReconnect:
- break
- }
- return true
- }
-}
-
-// MARK: - NotificationManagerDelegate
-extension AppDelegate: NotificationManagerDelegate {
- func notificationManagerDidUpdateInAppNotifications(_ manager: NotificationManager, notifications: [InAppNotificationDescriptor]) {
- connectController?.notificationController.setNotifications(notifications, animated: true)
- }
-}
-
-// MARK: - LoginViewControllerDelegate
-
-extension AppDelegate: LoginViewControllerDelegate {
-
- func loginViewController(_ controller: LoginViewController, loginWithAccountToken accountNumber: String, completion: @escaping (OperationCompletion<StoredAccountData?, TunnelManager.Error>) -> Void) {
- self.rootContainer?.setEnableSettingsButton(false)
-
- TunnelManager.shared.setAccount(action: .existing(accountNumber)) { operationCompletion in
- switch operationCompletion {
- case .success:
- self.logger.debug("Logged in with existing account.")
- // RootContainer's settings button will be re-enabled in `loginViewControllerDidLogin`
-
- case .failure(let error):
- self.logger.error(chainedError: error, message: "Failed to log in with existing account.")
- fallthrough
-
- case .cancelled:
- self.rootContainer?.setEnableSettingsButton(true)
- }
-
- completion(operationCompletion)
- }
- }
-
- func loginViewControllerLoginWithNewAccount(_ controller: LoginViewController, completion: @escaping (OperationCompletion<StoredAccountData?, TunnelManager.Error>) -> Void) {
- self.rootContainer?.setEnableSettingsButton(false)
-
- TunnelManager.shared.setAccount(action: .new) { operationCompletion in
- switch operationCompletion {
- case .success:
- self.logger.debug("Logged in with new account number.")
- // RootContainer's settings button will be re-enabled in `loginViewControllerDidLogin`
-
- case .failure(let error):
- self.logger.error(chainedError: error, message: "Failed to log in with new account.")
- fallthrough
-
- case .cancelled:
- self.rootContainer?.setEnableSettingsButton(true)
- }
-
- completion(operationCompletion)
- }
- }
-
- func loginViewControllerDidLogin(_ controller: LoginViewController) {
- self.window?.isUserInteractionEnabled = false
-
- // Move the settings button back into header bar
- self.rootContainer?.removeSettingsButtonFromPresentationContainer()
-
- self.relayConstraints = TunnelManager.shared.tunnelSettings?.relayConstraints
- self.selectLocationViewController?.setSelectedRelayLocation(relayConstraints?.location.value, animated: false, scrollPosition: .middle)
-
- switch UIDevice.current.userInterfaceIdiom {
- case .phone:
- let connectController = self.makeConnectViewController()
- self.rootContainer?.pushViewController(connectController, animated: true) {
- self.showAccountSettingsControllerIfAccountExpired()
- }
- self.connectController = connectController
- case .pad:
- self.showSplitViewMaster(true, animated: true)
-
- controller.dismiss(animated: true) {
- self.showAccountSettingsControllerIfAccountExpired()
- }
- default:
- fatalError()
- }
-
- self.window?.isUserInteractionEnabled = true
- self.rootContainer?.setEnableSettingsButton(true)
- }
-
-}
-
-// MARK: - SettingsNavigationControllerDelegate
-
-extension AppDelegate: SettingsNavigationControllerDelegate {
-
- func settingsNavigationController(_ controller: SettingsNavigationController, didFinishWithReason reason: SettingsDismissReason) {
- switch UIDevice.current.userInterfaceIdiom {
- case .phone:
- if case .userLoggedOut = reason {
- rootContainer?.popToRootViewController(animated: false)
-
- let loginController = rootContainer?.topViewController as? LoginViewController
-
- loginController?.reset()
- }
- controller.dismiss(animated: true)
-
- case .pad:
- if case .userLoggedOut = reason {
- self.showSplitViewMaster(false, animated: true)
- }
-
- controller.dismiss(animated: true) {
- if case .userLoggedOut = reason {
- let rootContainerWrapper = self.makeLoginContainerController()
- rootContainerWrapper.setViewControllers([self.makeLoginController()], animated: false)
- self.rootContainer?.present(rootContainerWrapper, animated: true)
- }
- }
-
- default:
- fatalError()
- }
-
- }
-
-}
-
-// MARK: - ConnectViewControllerDelegate
-
-extension AppDelegate: ConnectViewControllerDelegate {
-
- func connectViewControllerShouldShowSelectLocationPicker(_ controller: ConnectViewController) {
- let contentController = makeSelectLocationController()
- contentController.navigationItem.largeTitleDisplayMode = .never
- contentController.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(handleDismissSelectLocationController(_:)))
-
- let navController = SelectLocationNavigationController(contentController: contentController)
- self.rootContainer?.present(navController, animated: true)
- self.selectLocationViewController = contentController
- }
-
- @objc private func handleDismissSelectLocationController(_ sender: Any) {
- self.selectLocationViewController?.dismiss(animated: true)
- }
-}
-
-// MARK: - SelectLocationViewControllerDelegate
-
-extension AppDelegate: SelectLocationViewControllerDelegate {
- func selectLocationViewController(_ controller: SelectLocationViewController, didSelectRelayLocation relayLocation: RelayLocation) {
- // Dismiss view controller in modal presentation
- if controller.presentingViewController != nil {
- self.window?.isUserInteractionEnabled = false
- DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250)) {
- self.window?.isUserInteractionEnabled = true
- controller.dismiss(animated: true) {
- self.selectLocationControllerDidSelectRelayLocation(relayLocation)
- }
- }
- } else {
- selectLocationControllerDidSelectRelayLocation(relayLocation)
- }
- }
-
- private func selectLocationControllerDidSelectRelayLocation(_ relayLocation: RelayLocation) {
- let relayConstraints = RelayConstraints(location: .only(relayLocation))
-
- TunnelManager.shared.setRelayConstraints(relayConstraints) { error in
- self.relayConstraints = relayConstraints
-
- if let error = error {
- self.logger.error(chainedError: error, message: "Failed to update relay constraints")
- } else {
- self.logger.debug("Updated relay constraints: \(relayConstraints)")
- TunnelManager.shared.startTunnel()
- }
- }
- }
-}
-
-// MARK: - UIAdaptivePresentationControllerDelegate
-
-extension AppDelegate: UIAdaptivePresentationControllerDelegate {
-
- func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
- if controller.presentedViewController is RootContainerViewController {
- // Use .formSheet presentation in regular horizontal environment and .fullScreen
- // in compact environment.
- if traitCollection.horizontalSizeClass == .regular {
- return .formSheet
- } else {
- return .fullScreen
- }
- } else {
- return .none
- }
- }
-
- func presentationController(_ presentationController: UIPresentationController, willPresentWithAdaptiveStyle style: UIModalPresentationStyle, transitionCoordinator: UIViewControllerTransitionCoordinator?) {
- let actualStyle: UIModalPresentationStyle
-
- // When adaptive presentation is not changing, the `style` is set to `.none`
- if case .none = style {
- actualStyle = presentationController.presentedViewController.modalPresentationStyle
- } else {
- actualStyle = style
- }
-
- // Force hide header bar in .formSheet presentation and show it in .fullScreen presentation
- if let wrapper = presentationController.presentedViewController as? RootContainerViewController {
- wrapper.setOverrideHeaderBarHidden(actualStyle == .formSheet, animated: false)
- }
-
- guard actualStyle == .formSheet else {
- // Move the settings button back into header bar
- self.rootContainer?.removeSettingsButtonFromPresentationContainer()
-
- return
- }
-
- // Add settings button into the modal container to make it accessible by user
- if let transitionCoordinator = transitionCoordinator {
- transitionCoordinator.animate(alongsideTransition: { (context) in
- self.rootContainer?.addSettingsButtonToPresentationContainer(context.containerView)
- }, completion: { (context) in
- // no-op
- })
- } else {
- if let containerView = presentationController.containerView {
- rootContainer?.addSettingsButtonToPresentationContainer(containerView)
- } else {
- logger.warning("Cannot obtain the containerView for presentation controller when presenting with adaptive style \(actualStyle.rawValue) and missing transition coordinator.")
- }
- }
- }
-}
-
-// MARK: - RelayCacheObserver
-
-extension AppDelegate: RelayCacheObserver {
-
- func relayCache(_ relayCache: RelayCache.Tracker, didUpdateCachedRelays cachedRelays: RelayCache.CachedRelays) {
- selectLocationViewController?.setCachedRelays(cachedRelays)
- }
-
}
// MARK: - AppStorePaymentManagerDelegate
@@ -909,32 +393,7 @@ extension AppDelegate: AppStorePaymentManagerDelegate {
{
// Since we do not persist the relation between the payment and account token between the
// app launches, we assume that all successful purchases belong to the active account token.
- return TunnelManager.shared.tunnelSettings?.account.number
- }
-
-}
-
-
-// MARK: - UISplitViewControllerDelegate
-
-extension AppDelegate: UISplitViewControllerDelegate {
-
- func primaryViewController(forExpanding splitViewController: UISplitViewController) -> UIViewController? {
- // Restore the select location controller as primary when expanding the split view
- return selectLocationViewController
- }
-
- func primaryViewController(forCollapsing splitViewController: UISplitViewController) -> UIViewController? {
- // Set the connect controller as primary when collapsing the split view
- return connectController
- }
-
- func splitViewController(_ splitViewController: UISplitViewController, separateSecondaryFrom primaryViewController: UIViewController) -> UIViewController? {
- // Dismiss the select location controller when expanding the split view
- if self.selectLocationViewController?.presentingViewController != nil {
- self.selectLocationViewController?.dismiss(animated: false)
- }
- return nil
+ return TunnelManager.shared.accountNumber
}
}
@@ -947,7 +406,14 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
let blockOperation = AsyncBlockOperation(dispatchQueue: .main) {
if response.notification.request.identifier == accountExpiryNotificationIdentifier,
response.actionIdentifier == UNNotificationDefaultActionIdentifier {
- self.rootContainer?.showSettings(navigateTo: .account, animated: true)
+ if #available(iOS 13.0, *) {
+ let sceneDelegate = UIApplication.shared.connectedScenes
+ .first?.delegate as? SceneDelegate
+
+ sceneDelegate?.showUserAccount()
+ } else {
+ self.sceneDelegate?.showUserAccount()
+ }
}
completionHandler()
@@ -965,18 +431,3 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
}
}
-
-// MARK: -
-
-/// A enum holding the `UserDefaults` string keys
-private let isAgreedToTermsOfServiceKey = "isAgreedToTermsOfService"
-
-/// Returns true if user agreed to terms of service, otherwise false.
-func isAgreedToTermsOfService() -> Bool {
- return UserDefaults.standard.bool(forKey: isAgreedToTermsOfServiceKey)
-}
-
-/// Save the boolean flag in preferences indicating that the user agreed to terms of service.
-func setAgreedToTermsOfService() {
- UserDefaults.standard.set(true, forKey: isAgreedToTermsOfServiceKey)
-}
diff --git a/ios/MullvadVPN/Info.plist b/ios/MullvadVPN/Info.plist
index 4c782bc735..d4a1405adb 100644
--- a/ios/MullvadVPN/Info.plist
+++ b/ios/MullvadVPN/Info.plist
@@ -4,6 +4,11 @@
<dict>
<key>ApplicationSecurityGroupIdentifier</key>
<string>$(SECURITY_GROUP_IDENTIFIER)</string>
+ <key>UIApplicationSceneManifest</key>
+ <dict>
+ <key>UIApplicationSupportsMultipleScenes</key>
+ <true/>
+ </dict>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>net.mullvad.MullvadVPN.AppRefresh</string>
diff --git a/ios/MullvadVPN/SceneDelegate.swift b/ios/MullvadVPN/SceneDelegate.swift
new file mode 100644
index 0000000000..00c8a2ce3e
--- /dev/null
+++ b/ios/MullvadVPN/SceneDelegate.swift
@@ -0,0 +1,707 @@
+//
+// SceneDelegate.swift
+// MullvadVPN
+//
+// Created by pronebird on 20/05/2022.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import UIKit
+import Logging
+
+class SceneDelegate: UIResponder {
+ private let logger = Logger(label: "SceneDelegate")
+
+ var window: UIWindow?
+ private var privacyOverlayWindow: UIWindow?
+ private var isSceneConfigured = false
+
+ private let rootContainer = RootContainerViewController()
+ private var splitViewController: CustomSplitViewController?
+ private var selectLocationViewController: SelectLocationViewController?
+ private var connectController: ConnectViewController?
+ private weak var settingsNavController: SettingsNavigationController?
+
+ override init() {
+ super.init()
+
+ addSceneEvents()
+ }
+
+ func setupScene(windowFactory: WindowFactory) {
+ window = windowFactory.create()
+ window?.rootViewController = LaunchViewController()
+
+ privacyOverlayWindow = windowFactory.create()
+ privacyOverlayWindow?.rootViewController = LaunchViewController()
+ privacyOverlayWindow?.windowLevel = .alert + 1
+
+ window?.makeKeyAndVisible()
+
+ TunnelManager.shared.addObserver(self)
+ if TunnelManager.shared.isLoadedConfiguration {
+ configureScene()
+ }
+ }
+
+ func showUserAccount() {
+ rootContainer.showSettings(navigateTo: .account, animated: true)
+ }
+
+ private func configureScene() {
+ guard !isSceneConfigured else { return }
+
+ isSceneConfigured = true
+
+ rootContainer.delegate = self
+ window?.rootViewController = rootContainer
+
+ switch UIDevice.current.userInterfaceIdiom {
+ case .pad:
+ setupPadUI()
+ case .phone:
+ setupPhoneUI()
+ default:
+ fatalError()
+ }
+
+ RelayCache.Tracker.shared.addObserver(self)
+ NotificationManager.shared.delegate = self
+ }
+
+ private func setShowsPrivacyOverlay(_ showOverlay: Bool) {
+ if showOverlay {
+ privacyOverlayWindow?.isHidden = false
+ privacyOverlayWindow?.makeKeyAndVisible()
+ } else {
+ privacyOverlayWindow?.isHidden = true
+ window?.makeKeyAndVisible()
+ }
+ }
+
+ private func addSceneEvents() {
+ if #available(iOS 13, *) {
+ // no-op
+ } else {
+ let notificationCenter = NotificationCenter.default
+
+ notificationCenter.addObserver(
+ self,
+ selector: #selector(sceneDidBecomeActive),
+ name: UIApplication.didBecomeActiveNotification,
+ object: nil
+ )
+ notificationCenter.addObserver(
+ self,
+ selector: #selector(sceneDidEnterBackground),
+ name: UIApplication.didEnterBackgroundNotification,
+ object: nil
+ )
+ notificationCenter.addObserver(
+ self,
+ selector: #selector(sceneWillResignActive),
+ name: UIApplication.willResignActiveNotification,
+ object: nil
+ )
+ }
+ }
+
+ @objc private func sceneDidBecomeActive() {
+ TunnelManager.shared.refreshTunnelStatus()
+
+ RelayCache.Tracker.shared.startPeriodicUpdates()
+ TunnelManager.shared.startPeriodicPrivateKeyRotation()
+ AddressCache.Tracker.shared.startPeriodicUpdates()
+
+ setShowsPrivacyOverlay(false)
+ }
+
+ @objc private func sceneWillResignActive() {
+ RelayCache.Tracker.shared.stopPeriodicUpdates()
+ TunnelManager.shared.stopPeriodicPrivateKeyRotation()
+ AddressCache.Tracker.shared.stopPeriodicUpdates()
+
+ setShowsPrivacyOverlay(true)
+ }
+
+ @objc private func sceneDidEnterBackground() {
+ if #available(iOS 13, *) {
+ let appDelegate = UIApplication.shared.delegate as? AppDelegate
+
+ appDelegate?.scheduleBackgroundTasks()
+ }
+ }
+}
+
+// MARK: - UIWindowSceneDelegate
+
+@available(iOS 13.0, *)
+extension SceneDelegate: UIWindowSceneDelegate {
+ func scene(
+ _ scene: UIScene,
+ willConnectTo session: UISceneSession,
+ options connectionOptions: UIScene.ConnectionOptions
+ ) {
+ guard let windowScene = scene as? UIWindowScene else { return }
+
+ setupScene(windowFactory: SceneWindowFactory(windowScene: windowScene))
+ }
+
+ func sceneDidDisconnect(_ scene: UIScene) {
+ // no-op
+ }
+
+ func sceneDidBecomeActive(_ scene: UIScene) {
+ sceneDidBecomeActive()
+ }
+
+ func sceneWillResignActive(_ scene: UIScene) {
+ sceneWillResignActive()
+ }
+
+ func sceneWillEnterForeground(_ scene: UIScene) {
+ // no-op
+ }
+
+ func sceneDidEnterBackground(_ scene: UIScene) {
+ sceneDidEnterBackground()
+ }
+}
+
+// MARK: - RootContainerViewControllerDelegate
+
+extension SceneDelegate: RootContainerViewControllerDelegate {
+ func rootContainerViewControllerShouldShowSettings(_ controller: RootContainerViewController, navigateTo route: SettingsNavigationRoute?, animated: Bool) {
+ // Check if settings controller is already presented.
+ if let settingsNavController = settingsNavController {
+ if let route = route {
+ settingsNavController.navigate(to: route, animated: animated)
+ } else {
+ settingsNavController.popToRootViewController(animated: animated)
+ }
+ } else {
+ let navController = makeSettingsNavigationController(route: route)
+
+ // On iPad the login controller can be presented modally above the root container.
+ // in that case we have to use the presented controller to present the next modal.
+ if let presentedController = controller.presentedViewController {
+ presentedController.present(navController, animated: true)
+ } else {
+ controller.present(navController, animated: true)
+ }
+
+ // Save the reference for later.
+ self.settingsNavController = navController
+ }
+ }
+
+ func rootContainerViewSupportedInterfaceOrientations(_ controller: RootContainerViewController) -> UIInterfaceOrientationMask {
+ switch UIDevice.current.userInterfaceIdiom {
+ case .pad:
+ return [.landscape, .portrait]
+ case .phone:
+ return [.portrait]
+ default:
+ return controller.supportedInterfaceOrientations
+ }
+ }
+
+ func rootContainerViewAccessibilityPerformMagicTap(_ controller: RootContainerViewController) -> Bool {
+ guard TunnelManager.shared.isAccountSet else { return false }
+
+ switch TunnelManager.shared.tunnelState {
+ case .connected, .connecting, .reconnecting:
+ TunnelManager.shared.reconnectTunnel()
+ case .disconnecting, .disconnected:
+ TunnelManager.shared.startTunnel()
+ case .pendingReconnect:
+ break
+ }
+ return true
+ }
+}
+
+extension SceneDelegate {
+
+ private func setupPadUI() {
+ let selectLocationController = makeSelectLocationController()
+ let connectController = makeConnectViewController()
+
+ let splitViewController = CustomSplitViewController()
+ splitViewController.delegate = self
+ splitViewController.minimumPrimaryColumnWidth = UIMetrics.minimumSplitViewSidebarWidth
+ splitViewController.preferredPrimaryColumnWidthFraction = UIMetrics.maximumSplitViewSidebarWidthFraction
+ splitViewController.primaryEdge = .trailing
+ splitViewController.dividerColor = UIColor.MainSplitView.dividerColor
+ splitViewController.viewControllers = [selectLocationController, connectController]
+
+ self.selectLocationViewController = selectLocationController
+ self.splitViewController = splitViewController
+ self.connectController = connectController
+
+ rootContainer.setViewControllers([splitViewController], animated: false)
+ showSplitViewMaster(TunnelManager.shared.isAccountSet, animated: false)
+
+ let rootContainerWrapper = makeLoginContainerController()
+
+ if !TermsOfService.isAgreed {
+ let termsOfServiceViewController = self.makeTermsOfServiceController { [weak self] viewController in
+ guard let self = self else { return }
+
+ if TunnelManager.shared.isAccountSet {
+ rootContainerWrapper.dismiss(animated: true) {
+ self.showAccountSettingsControllerIfAccountExpired()
+ }
+ } else {
+ rootContainerWrapper.pushViewController(self.makeLoginController(), animated: true)
+ }
+ }
+ rootContainerWrapper.setViewControllers([termsOfServiceViewController], animated: false)
+ rootContainer.present(rootContainerWrapper, animated: false)
+ } else if !TunnelManager.shared.isAccountSet {
+ rootContainerWrapper.setViewControllers([makeLoginController()], animated: false)
+ rootContainer.present(rootContainerWrapper, animated: false)
+ } else {
+ self.showAccountSettingsControllerIfAccountExpired()
+ }
+ }
+
+ private func setupPhoneUI() {
+ let showNextController = { [weak self] (animated: Bool) in
+ guard let self = self else { return }
+
+ let loginViewController = self.makeLoginController()
+ var viewControllers: [UIViewController] = [loginViewController]
+
+ if TunnelManager.shared.isAccountSet {
+ let connectController = self.makeConnectViewController()
+ viewControllers.append(connectController)
+ self.connectController = connectController
+ }
+
+ self.rootContainer.setViewControllers(viewControllers, animated: animated) {
+ self.showAccountSettingsControllerIfAccountExpired()
+ }
+ }
+
+ if TermsOfService.isAgreed {
+ showNextController(false)
+ } else {
+ let termsOfServiceController = self.makeTermsOfServiceController { _ in
+ showNextController(true)
+ }
+
+ rootContainer.setViewControllers([termsOfServiceController], animated: false)
+ }
+ }
+
+ private func makeSettingsNavigationController(route: SettingsNavigationRoute?) -> SettingsNavigationController {
+ let navController = SettingsNavigationController()
+ navController.settingsDelegate = self
+
+ if UIDevice.current.userInterfaceIdiom == .pad {
+ navController.preferredContentSize = CGSize(width: 480, height: 568)
+ navController.modalPresentationStyle = .formSheet
+ }
+
+ navController.presentationController?.delegate = navController
+
+ if let route = route {
+ navController.navigate(to: route, animated: false)
+ }
+
+ return navController
+ }
+
+ private func makeConnectViewController() -> ConnectViewController {
+ let connectController = ConnectViewController()
+ connectController.delegate = self
+
+ return connectController
+ }
+
+ private func makeSelectLocationController() -> SelectLocationViewController {
+ let selectLocationController = SelectLocationViewController()
+ selectLocationController.delegate = self
+
+ if let cachedRelays = RelayCache.Tracker.shared.getCachedRelays() {
+ selectLocationController.setCachedRelays(cachedRelays)
+ }
+
+ let relayConstraints = TunnelManager.shared.tunnelSettings?.relayConstraints
+ if let relayLocation = relayConstraints?.location.value {
+ selectLocationController.setSelectedRelayLocation(
+ relayLocation,
+ animated: false,
+ scrollPosition: .middle
+ )
+ }
+
+ return selectLocationController
+ }
+
+ private func makeTermsOfServiceController(
+ completion: @escaping (UIViewController) -> Void
+ ) -> TermsOfServiceViewController
+ {
+ let controller = TermsOfServiceViewController()
+
+ if UIDevice.current.userInterfaceIdiom == .pad {
+ controller.modalPresentationStyle = .formSheet
+ if #available(iOS 13.0, *) {
+ controller.isModalInPresentation = true
+ }
+ }
+
+ controller.completionHandler = { controller in
+ TermsOfService.setAgreed()
+ completion(controller)
+ }
+
+ return controller
+ }
+
+ private func makeLoginContainerController() -> RootContainerViewController {
+ let rootContainerWrapper = RootContainerViewController()
+ rootContainerWrapper.delegate = self
+ rootContainerWrapper.preferredContentSize = CGSize(width: 480, height: 600)
+
+ if UIDevice.current.userInterfaceIdiom == .pad {
+ rootContainerWrapper.modalPresentationStyle = .formSheet
+ if #available(iOS 13.0, *) {
+ // Prevent swiping off the login or terms of service controllers
+ rootContainerWrapper.isModalInPresentation = true
+ }
+ }
+
+ rootContainerWrapper.presentationController?.delegate = self
+
+ return rootContainerWrapper
+ }
+
+ private func makeLoginController() -> LoginViewController {
+ let controller = LoginViewController()
+ controller.delegate = self
+ return controller
+ }
+
+ private func showAccountSettingsControllerIfAccountExpired() {
+ guard let accountExpiry = TunnelManager.shared.accountExpiry, accountExpiry <= Date() else { return }
+
+ rootContainer.showSettings(navigateTo: .account, animated: true)
+ }
+
+ private func showSplitViewMaster(_ show: Bool, animated: Bool) {
+ splitViewController?.preferredDisplayMode = show ? .allVisible : .primaryHidden
+ connectController?.setMainContentHidden(!show, animated: animated)
+ }
+}
+
+// MARK: - LoginViewControllerDelegate
+
+extension SceneDelegate: LoginViewControllerDelegate {
+
+ func loginViewController(_ controller: LoginViewController, loginWithAccountToken accountNumber: String, completion: @escaping (OperationCompletion<StoredAccountData?, TunnelManager.Error>) -> Void) {
+ rootContainer.setEnableSettingsButton(false)
+
+ TunnelManager.shared.setAccount(action: .existing(accountNumber)) { operationCompletion in
+ switch operationCompletion {
+ case .success:
+ self.logger.debug("Logged in with existing account.")
+ // RootContainer's settings button will be re-enabled in `loginViewControllerDidLogin`
+
+ case .failure(let error):
+ self.logger.error(chainedError: error, message: "Failed to log in with existing account.")
+ fallthrough
+
+ case .cancelled:
+ self.rootContainer.setEnableSettingsButton(true)
+ }
+
+ completion(operationCompletion)
+ }
+ }
+
+ func loginViewControllerLoginWithNewAccount(_ controller: LoginViewController, completion: @escaping (OperationCompletion<StoredAccountData?, TunnelManager.Error>) -> Void) {
+ rootContainer.setEnableSettingsButton(false)
+
+ TunnelManager.shared.setAccount(action: .new) { operationCompletion in
+ switch operationCompletion {
+ case .success:
+ self.logger.debug("Logged in with new account number.")
+ // RootContainer's settings button will be re-enabled in `loginViewControllerDidLogin`
+
+ case .failure(let error):
+ self.logger.error(chainedError: error, message: "Failed to log in with new account.")
+ fallthrough
+
+ case .cancelled:
+ self.rootContainer.setEnableSettingsButton(true)
+ }
+
+ completion(operationCompletion)
+ }
+ }
+
+ func loginViewControllerDidLogin(_ controller: LoginViewController) {
+ window?.isUserInteractionEnabled = false
+
+ // Move the settings button back into header bar
+ rootContainer.removeSettingsButtonFromPresentationContainer()
+
+ let relayConstraints = TunnelManager.shared.tunnelSettings?.relayConstraints
+ self.selectLocationViewController?.setSelectedRelayLocation(
+ relayConstraints?.location.value,
+ animated: false,
+ scrollPosition: .middle
+ )
+
+ switch UIDevice.current.userInterfaceIdiom {
+ case .phone:
+ let connectController = self.makeConnectViewController()
+ rootContainer.pushViewController(connectController, animated: true) {
+ self.showAccountSettingsControllerIfAccountExpired()
+ }
+ self.connectController = connectController
+ case .pad:
+ self.showSplitViewMaster(true, animated: true)
+
+ controller.dismiss(animated: true) {
+ self.showAccountSettingsControllerIfAccountExpired()
+ }
+ default:
+ fatalError()
+ }
+
+ window?.isUserInteractionEnabled = true
+ rootContainer.setEnableSettingsButton(true)
+ }
+
+}
+
+// MARK: - SettingsNavigationControllerDelegate
+
+extension SceneDelegate: SettingsNavigationControllerDelegate {
+
+ func settingsNavigationController(_ controller: SettingsNavigationController, didFinishWithReason reason: SettingsDismissReason) {
+ switch UIDevice.current.userInterfaceIdiom {
+ case .phone:
+ if case .userLoggedOut = reason {
+ rootContainer.popToRootViewController(animated: false)
+
+ let loginController = rootContainer.topViewController as? LoginViewController
+
+ loginController?.reset()
+ }
+ controller.dismiss(animated: true)
+
+ case .pad:
+ if case .userLoggedOut = reason {
+ self.showSplitViewMaster(false, animated: true)
+ }
+
+ controller.dismiss(animated: true) {
+ if case .userLoggedOut = reason {
+ let rootContainerWrapper = self.makeLoginContainerController()
+ rootContainerWrapper.setViewControllers([self.makeLoginController()], animated: false)
+ self.rootContainer.present(rootContainerWrapper, animated: true)
+ }
+ }
+
+ default:
+ fatalError()
+ }
+
+ }
+
+}
+
+// MARK: - ConnectViewControllerDelegate
+
+extension SceneDelegate: ConnectViewControllerDelegate {
+
+ func connectViewControllerShouldShowSelectLocationPicker(_ controller: ConnectViewController) {
+ let contentController = makeSelectLocationController()
+ contentController.navigationItem.largeTitleDisplayMode = .never
+ contentController.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(handleDismissSelectLocationController(_:)))
+
+ let navController = SelectLocationNavigationController(contentController: contentController)
+ rootContainer.present(navController, animated: true)
+
+ selectLocationViewController = contentController
+ }
+
+ @objc private func handleDismissSelectLocationController(_ sender: Any) {
+ selectLocationViewController?.dismiss(animated: true)
+ }
+}
+
+// MARK: - NotificationManagerDelegate
+
+extension SceneDelegate: NotificationManagerDelegate {
+ func notificationManagerDidUpdateInAppNotifications(_ manager: NotificationManager, notifications: [InAppNotificationDescriptor]) {
+ connectController?.notificationController.setNotifications(notifications, animated: true)
+ }
+}
+
+// MARK: - SelectLocationViewControllerDelegate
+
+extension SceneDelegate: SelectLocationViewControllerDelegate {
+ func selectLocationViewController(_ controller: SelectLocationViewController, didSelectRelayLocation relayLocation: RelayLocation) {
+ // Dismiss view controller in modal presentation
+ if controller.presentingViewController != nil {
+ self.window?.isUserInteractionEnabled = false
+ DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250)) {
+ self.window?.isUserInteractionEnabled = true
+ controller.dismiss(animated: true) {
+ self.selectLocationControllerDidSelectRelayLocation(relayLocation)
+ }
+ }
+ } else {
+ selectLocationControllerDidSelectRelayLocation(relayLocation)
+ }
+ }
+
+ private func selectLocationControllerDidSelectRelayLocation(_ relayLocation: RelayLocation) {
+ let relayConstraints = RelayConstraints(location: .only(relayLocation))
+
+ TunnelManager.shared.setRelayConstraints(relayConstraints) { error in
+ if let error = error {
+ self.logger.error(chainedError: error, message: "Failed to update relay constraints")
+ } else {
+ self.logger.debug("Updated relay constraints: \(relayConstraints)")
+
+ TunnelManager.shared.startTunnel()
+ }
+ }
+ }
+}
+
+// MARK: - UIAdaptivePresentationControllerDelegate
+
+extension SceneDelegate: UIAdaptivePresentationControllerDelegate {
+
+ func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
+ if controller.presentedViewController is RootContainerViewController {
+ return traitCollection.horizontalSizeClass == .regular ? .formSheet : .fullScreen
+ } else {
+ return .none
+ }
+ }
+
+ func presentationController(_ presentationController: UIPresentationController, willPresentWithAdaptiveStyle style: UIModalPresentationStyle, transitionCoordinator: UIViewControllerTransitionCoordinator?) {
+ let actualStyle: UIModalPresentationStyle
+
+ // When adaptive presentation is not changing, the `style` is set to `.none`
+ if case .none = style {
+ actualStyle = presentationController.presentedViewController.modalPresentationStyle
+ } else {
+ actualStyle = style
+ }
+
+ // Force hide header bar in .formSheet presentation and show it in .fullScreen presentation
+ if let wrapper = presentationController.presentedViewController as? RootContainerViewController {
+ wrapper.setOverrideHeaderBarHidden(actualStyle == .formSheet, animated: false)
+ }
+
+ guard actualStyle == .formSheet else {
+ // Move the settings button back into header bar
+ rootContainer.removeSettingsButtonFromPresentationContainer()
+
+ return
+ }
+
+ // Add settings button into the modal container to make it accessible by user
+ if let transitionCoordinator = transitionCoordinator {
+ transitionCoordinator.animate { context in
+ self.rootContainer.addSettingsButtonToPresentationContainer(context.containerView)
+ }
+ } else if let containerView = presentationController.containerView {
+ rootContainer.addSettingsButtonToPresentationContainer(containerView)
+ } else {
+ logger.warning(
+ """
+ Cannot obtain the containerView for presentation controller when presenting with \
+ adaptive style \(actualStyle.rawValue) and missing transition coordinator.
+ """
+ )
+ }
+ }
+}
+
+// MARK: - TunnelObserver
+
+extension SceneDelegate: TunnelObserver {
+ func tunnelManagerDidLoadConfiguration(_ manager: TunnelManager) {
+ configureScene()
+ }
+
+ func tunnelManager(_ manager: TunnelManager, didUpdateTunnelState tunnelState: TunnelState) {
+ // no-op
+ }
+
+ func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2?) {
+ // no-op
+ }
+
+ func tunnelManager(_ manager: TunnelManager, didFailWithError error: TunnelManager.Error) {
+ // no-op
+ }
+}
+
+// MARK: - RelayCacheObserver
+
+extension SceneDelegate: RelayCacheObserver {
+
+ func relayCache(_ relayCache: RelayCache.Tracker, didUpdateCachedRelays cachedRelays: RelayCache.CachedRelays) {
+ DispatchQueue.main.async {
+ self.selectLocationViewController?.setCachedRelays(cachedRelays)
+ }
+ }
+
+}
+
+
+// MARK: - UISplitViewControllerDelegate
+
+extension SceneDelegate: UISplitViewControllerDelegate {
+
+ func primaryViewController(forExpanding splitViewController: UISplitViewController) -> UIViewController? {
+ // Restore the select location controller as primary when expanding the split view
+ return selectLocationViewController
+ }
+
+ func primaryViewController(forCollapsing splitViewController: UISplitViewController) -> UIViewController? {
+ // Set the connect controller as primary when collapsing the split view
+ return connectController
+ }
+
+ func splitViewController(_ splitViewController: UISplitViewController, separateSecondaryFrom primaryViewController: UIViewController) -> UIViewController? {
+ // Dismiss the select location controller when expanding the split view
+ if self.selectLocationViewController?.presentingViewController != nil {
+ self.selectLocationViewController?.dismiss(animated: false)
+ }
+ return nil
+ }
+
+}
+
+// MARK: - Window factory
+
+protocol WindowFactory {
+ func create() -> UIWindow
+}
+
+struct ClassicWindowFactory: WindowFactory {
+ func create() -> UIWindow {
+ return UIWindow(frame: UIScreen.main.bounds)
+ }
+}
+@available(iOS 13.0, *)
+struct SceneWindowFactory: WindowFactory {
+ let windowScene: UIWindowScene
+
+ func create() -> UIWindow {
+ return UIWindow(windowScene: windowScene)
+ }
+}
diff --git a/ios/MullvadVPN/SimulatorTunnelProvider.swift b/ios/MullvadVPN/SimulatorTunnelProvider.swift
index db70a94063..657337f82c 100644
--- a/ios/MullvadVPN/SimulatorTunnelProvider.swift
+++ b/ios/MullvadVPN/SimulatorTunnelProvider.swift
@@ -187,7 +187,7 @@ class SimulatorVPNConnection: NSObject, VPNConnectionProtocol {
status = .connecting
- SimulatorTunnelProvider.shared.delegate.startTunnel(options: options) { (error) in
+ SimulatorTunnelProvider.shared.delegate.startTunnel(options: options) { error in
if error == nil {
self.status = .connected
self.connectedDate = Date()
diff --git a/ios/MullvadVPN/TermsOfService.swift b/ios/MullvadVPN/TermsOfService.swift
new file mode 100644
index 0000000000..fe1cc71003
--- /dev/null
+++ b/ios/MullvadVPN/TermsOfService.swift
@@ -0,0 +1,21 @@
+//
+// TermsOfService.swift
+// MullvadVPN
+//
+// Created by pronebird on 22/06/2022.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+enum TermsOfService {
+ private static let userDefaultsKey = "isAgreedToTermsOfService"
+
+ static var isAgreed: Bool {
+ return UserDefaults.standard.bool(forKey: userDefaultsKey)
+ }
+
+ static func setAgreed() {
+ UserDefaults.standard.set(true, forKey: userDefaultsKey)
+ }
+}
diff --git a/ios/MullvadVPN/ConsentContentView.swift b/ios/MullvadVPN/TermsOfServiceContentView.swift
index 1578962499..4e18593681 100644
--- a/ios/MullvadVPN/ConsentContentView.swift
+++ b/ios/MullvadVPN/TermsOfServiceContentView.swift
@@ -1,5 +1,5 @@
//
-// ConsentContentView.swift
+// TermsOfServiceContentView.swift
// MullvadVPN
//
// Created by pronebird on 28/04/2021.
@@ -8,7 +8,7 @@
import UIKit
-class ConsentContentView: UIView {
+class TermsOfServiceContentView: UIView {
let titleLabel: UILabel = {
let titleLabel = UILabel()
@@ -19,7 +19,7 @@ class ConsentContentView: UIView {
titleLabel.allowsDefaultTighteningForTruncation = true
titleLabel.text = NSLocalizedString(
"PRIVACY_NOTICE_HEADING",
- tableName: "Consent",
+ tableName: "TermsOfService",
value: "Do you agree to remaining anonymous?",
comment: ""
)
@@ -41,7 +41,7 @@ class ConsentContentView: UIView {
bodyLabel.numberOfLines = 0
bodyLabel.text = NSLocalizedString(
"PRIVACY_NOTICE_BODY",
- tableName: "Consent",
+ tableName: "TermsOfService",
value: "You have a right to privacy. That’s why we never store activity logs, don't ask for personal information, and encourage anonymous payments.\n\nIn some situations, as outlined in our privacy policy, we might process personal data that you choose to send, for example if you email us.\n\nWe strongly believe in retaining as little data as possible because we want you to remain anonymous.",
comment: ""
)
@@ -53,7 +53,7 @@ class ConsentContentView: UIView {
button.translatesAutoresizingMaskIntoConstraints = false
button.titleString = NSLocalizedString(
"PRIVACY_POLICY_LINK_TITLE",
- tableName: "Consent",
+ tableName: "TermsOfService",
value: "Privacy policy",
comment: ""
)
@@ -67,7 +67,7 @@ class ConsentContentView: UIView {
button.accessibilityIdentifier = "AgreeButton"
button.setTitle(NSLocalizedString(
"CONTINUE_BUTTON_TITLE",
- tableName: "Consent",
+ tableName: "TermsOfService",
value: "Agree and continue",
comment: ""
), for: .normal)
diff --git a/ios/MullvadVPN/ConsentViewController.swift b/ios/MullvadVPN/TermsOfServiceViewController.swift
index 70d37e8f79..dbc04555dc 100644
--- a/ios/MullvadVPN/ConsentViewController.swift
+++ b/ios/MullvadVPN/TermsOfServiceViewController.swift
@@ -1,5 +1,5 @@
//
-// ConsentViewController.swift
+// TermsOfServiceViewController.swift
// MullvadVPN
//
// Created by pronebird on 21/02/2020.
@@ -9,7 +9,7 @@
import SafariServices
import UIKit
-class ConsentViewController: UIViewController, RootContainment, SFSafariViewControllerDelegate {
+class TermsOfServiceViewController: UIViewController, RootContainment, SFSafariViewControllerDelegate {
var completionHandler: ((UIViewController) -> Void)?
@@ -30,7 +30,7 @@ class ConsentViewController: UIViewController, RootContainment, SFSafariViewCont
override func viewDidLoad() {
super.viewDidLoad()
- let contentView = ConsentContentView()
+ let contentView = TermsOfServiceContentView()
contentView.translatesAutoresizingMaskIntoConstraints = false
contentView.agreeButton.addTarget(self, action: #selector(handleAgreeButton(_:)), for: .touchUpInside)
contentView.privacyPolicyLink.addTarget(self, action: #selector(handlePrivacyPolicyButton(_:)), for: .touchUpInside)
diff --git a/ios/MullvadVPNScreenshots/MullvadVPNScreenshots.swift b/ios/MullvadVPNScreenshots/MullvadVPNScreenshots.swift
index 6de279d85b..ab39539c3f 100644
--- a/ios/MullvadVPNScreenshots/MullvadVPNScreenshots.swift
+++ b/ios/MullvadVPNScreenshots/MullvadVPNScreenshots.swift
@@ -32,7 +32,7 @@ class MullvadVPNScreenshots: XCTestCase {
app.launch()
- // Dismiss consent screen
+ // Dismiss terms of service screen
_ = app.buttons["AgreeButton"].waitForExistence(timeout: 10)
app.buttons["AgreeButton"].tap()