summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2023-03-07 15:16:59 +0100
committerAndrej Mihajlov <and@mullvad.net>2023-03-22 16:42:30 +0100
commita51757ce590b5063c1c8099b3ed8ea0fa8b3bcdb (patch)
tree85935823680100affad563ebeca45a07a71938ee
parent1c2c6f58dc1d175d00bea8037ca989ca80b1fcb8 (diff)
downloadmullvadvpn-a51757ce590b5063c1c8099b3ed8ea0fa8b3bcdb.tar.xz
mullvadvpn-a51757ce590b5063c1c8099b3ed8ea0fa8b3bcdb.zip
Add coordinators and app router
Fixes IOS-10
-rw-r--r--ios/MullvadTypes/Promise.swift49
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj622
-rw-r--r--ios/MullvadVPN/AddressCacheTracker/AddressCacheTracker.swift (renamed from ios/MullvadVPN/AddressCacheTracker.swift)0
-rw-r--r--ios/MullvadVPN/AppDelegate.swift2
-rw-r--r--ios/MullvadVPN/Classes/AccountDataThrottling.swift (renamed from ios/MullvadVPN/AccountDataThrottling.swift)0
-rw-r--r--ios/MullvadVPN/Classes/AutomaticKeyboardResponder.swift (renamed from ios/MullvadVPN/AutomaticKeyboardResponder.swift)0
-rw-r--r--ios/MullvadVPN/Classes/ConsolidatedApplicationLog.swift (renamed from ios/MullvadVPN/ConsolidatedApplicationLog.swift)0
-rw-r--r--ios/MullvadVPN/Classes/CustomDateComponentsFormatting.swift (renamed from ios/MullvadVPN/CustomDateComponentsFormatting.swift)0
-rw-r--r--ios/MullvadVPN/Classes/DataSourceSnapshot.swift (renamed from ios/MullvadVPN/DataSourceSnapshot.swift)0
-rw-r--r--ios/MullvadVPN/Classes/DeviceDataThrottling.swift (renamed from ios/MullvadVPN/DeviceDataThrottling.swift)0
-rw-r--r--ios/MullvadVPN/Classes/ObserverList.swift (renamed from ios/MullvadVPN/ObserverList.swift)0
-rw-r--r--ios/MullvadVPN/Classes/Swizzle.swift (renamed from ios/MullvadVPN/Swizzle.swift)0
-rw-r--r--ios/MullvadVPN/Classes/TermsOfService.swift (renamed from ios/MullvadVPN/TermsOfService.swift)4
-rw-r--r--ios/MullvadVPN/Containers/CustomSplitViewController.swift (renamed from ios/MullvadVPN/CustomSplitViewController.swift)3
-rw-r--r--ios/MullvadVPN/Containers/Navigation/CustomNavigationController.swift25
-rw-r--r--ios/MullvadVPN/Containers/Navigation/UINavigationBar+Appearance.swift (renamed from ios/MullvadVPN/CustomNavigationBar.swift)68
-rw-r--r--ios/MullvadVPN/Containers/Root/HeaderBarButton.swift (renamed from ios/MullvadVPN/HeaderBarButton.swift)0
-rw-r--r--ios/MullvadVPN/Containers/Root/HeaderBarView.swift (renamed from ios/MullvadVPN/HeaderBarView.swift)0
-rw-r--r--ios/MullvadVPN/Containers/Root/RootContainerViewController.swift (renamed from ios/MullvadVPN/RootContainerViewController.swift)70
-rw-r--r--ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift719
-rw-r--r--ios/MullvadVPN/Coordinators/App/ApplicationRouter.swift666
-rw-r--r--ios/MullvadVPN/Coordinators/App/LoginCoordinator.swift121
-rw-r--r--ios/MullvadVPN/Coordinators/App/OutOfTimeCoordinator.swift70
-rw-r--r--ios/MullvadVPN/Coordinators/App/RevokedCoordinator.swift34
-rw-r--r--ios/MullvadVPN/Coordinators/App/SelectLocationCoordinator.swift82
-rw-r--r--ios/MullvadVPN/Coordinators/App/SettingsCoordinator.swift193
-rw-r--r--ios/MullvadVPN/Coordinators/App/TermsOfServiceCoordinator.swift33
-rw-r--r--ios/MullvadVPN/Coordinators/App/TunnelCoordinator.swift65
-rw-r--r--ios/MullvadVPN/Coordinators/Base/Coordinator.swift163
-rw-r--r--ios/MullvadVPN/Coordinators/Base/ModalPresentationConfiguration.swift52
-rw-r--r--ios/MullvadVPN/Coordinators/Base/PresentationControllerDismissalInterceptor.swift78
-rw-r--r--ios/MullvadVPN/Extensions/Bundle+ProductVersion.swift (renamed from ios/MullvadVPN/Bundle+ProductVersion.swift)0
-rw-r--r--ios/MullvadVPN/Extensions/CharacterSet+IPAddress.swift (renamed from ios/MullvadVPN/CharacterSet+IPAddress.swift)0
-rw-r--r--ios/MullvadVPN/Extensions/CodingErrors+CustomErrorDescription.swift (renamed from ios/MullvadVPN/CodingErrors+CustomErrorDescription.swift)0
-rw-r--r--ios/MullvadVPN/Extensions/NEVPNStatus+Debug.swift (renamed from ios/MullvadVPN/NEVPNStatus+Debug.swift)1
-rw-r--r--ios/MullvadVPN/Extensions/NSAttributedString+Markdown.swift (renamed from ios/MullvadVPN/NSAttributedString+Markdown.swift)0
-rw-r--r--ios/MullvadVPN/Extensions/NSLayoutConstraint+Helpers.swift (renamed from ios/MullvadVPN/NSLayoutConstraint+Helpers.swift)0
-rw-r--r--ios/MullvadVPN/Extensions/NSRegularExpression+IPAddress.swift (renamed from ios/MullvadVPN/NSRegularExpression+IPAddress.swift)0
-rw-r--r--ios/MullvadVPN/Extensions/RESTCreateApplePaymentResponse+Localization.swift (renamed from ios/MullvadVPN/RESTCreateApplePaymentResponse+Localization.swift)0
-rw-r--r--ios/MullvadVPN/Extensions/RESTError+Display.swift (renamed from ios/MullvadVPN/RESTError+Display.swift)16
-rw-r--r--ios/MullvadVPN/Extensions/Result+Extensions.swift (renamed from ios/MullvadVPN/Result+Extensions.swift)0
-rw-r--r--ios/MullvadVPN/Extensions/SKError+Localized.swift (renamed from ios/MullvadVPN/SKError+Localized.swift)0
-rw-r--r--ios/MullvadVPN/Extensions/SKProduct+Formatting.swift (renamed from ios/MullvadVPN/SKProduct+Formatting.swift)0
-rw-r--r--ios/MullvadVPN/Extensions/StorePaymentManagerError+Display.swift (renamed from ios/MullvadVPN/StorePaymentManagerError+Display.swift)0
-rw-r--r--ios/MullvadVPN/Extensions/String+AccountFormatting.swift15
-rw-r--r--ios/MullvadVPN/Extensions/String+Split.swift (renamed from ios/MullvadVPN/String+Split.swift)0
-rw-r--r--ios/MullvadVPN/Extensions/UIBarButtonItem+KeyboardNavigation.swift (renamed from ios/MullvadVPN/UIBarButtonItem+KeyboardNavigation.swift)0
-rw-r--r--ios/MullvadVPN/Extensions/UIColor+Helpers.swift (renamed from ios/MullvadVPN/UIColor+Helpers.swift)0
-rw-r--r--ios/MullvadVPN/ModalRootAdaptivePresentationDelegate.swift142
-rw-r--r--ios/MullvadVPN/Presentation controllers/FormsheetPresentationController.swift216
-rw-r--r--ios/MullvadVPN/Presentation controllers/SecondaryContextPresentationController.swift67
-rw-r--r--ios/MullvadVPN/Protocols/CellFactoryProtocol.swift (renamed from ios/MullvadVPN/CellFactoryProtocol.swift)0
-rw-r--r--ios/MullvadVPN/Protocols/SettingsMigrationUIHandler.swift (renamed from ios/MullvadVPN/SettingsMigrationUIHandler.swift)0
-rw-r--r--ios/MullvadVPN/SceneDelegate.swift856
-rw-r--r--ios/MullvadVPN/SelectLocationNavigationController.swift30
-rw-r--r--ios/MullvadVPN/SettingsManager/DNSSettings.swift (renamed from ios/MullvadVPN/DNSSettings.swift)0
-rw-r--r--ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift5
-rw-r--r--ios/MullvadVPN/SettingsNavigationController.swift168
-rw-r--r--ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProvider.swift (renamed from ios/MullvadVPN/SimulatorTunnelProvider.swift)0
-rw-r--r--ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift (renamed from ios/MullvadVPN/SimulatorTunnelProviderHost.swift)0
-rw-r--r--ios/MullvadVPN/StringFormatter.swift15
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/AppIcon.appiconset/AppIcon.png (renamed from ios/MullvadVPN/Assets.xcassets/AppIcon.appiconset/AppIcon.png)bin42242 -> 42242 bytes
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/AppIcon.appiconset/Contents.json (renamed from ios/MullvadVPN/Assets.xcassets/AppIcon.appiconset/Contents.json)0
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/Contents.json (renamed from ios/MullvadVPN/Assets.xcassets/Contents.json)0
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/DangerButton.imageset/Contents.json (renamed from ios/MullvadVPN/Assets.xcassets/DangerButton.imageset/Contents.json)0
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/DangerButton.imageset/DangerButton.pdf (renamed from ios/MullvadVPN/Assets.xcassets/DangerButton.imageset/DangerButton.pdf)bin994 -> 994 bytes
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/DefaultButton.imageset/Contents.json (renamed from ios/MullvadVPN/Assets.xcassets/DefaultButton.imageset/Contents.json)0
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/DefaultButton.imageset/DefaultButton.pdf (renamed from ios/MullvadVPN/Assets.xcassets/DefaultButton.imageset/DefaultButton.pdf)bin994 -> 994 bytes
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconArrow.imageset/Contents.json (renamed from ios/MullvadVPN/Assets.xcassets/IconArrow.imageset/Contents.json)0
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconArrow.imageset/IconArrow.pdf (renamed from ios/MullvadVPN/Assets.xcassets/IconArrow.imageset/IconArrow.pdf)bin1103 -> 1103 bytes
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconBack.imageset/Contents.json (renamed from ios/MullvadVPN/Assets.xcassets/IconBack.imageset/Contents.json)0
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconBack.imageset/IconBack.pdf (renamed from ios/MullvadVPN/Assets.xcassets/IconBack.imageset/IconBack.pdf)bin1123 -> 1123 bytes
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconBackTransitionMask.imageset/Contents.json (renamed from ios/MullvadVPN/Assets.xcassets/IconBackTransitionMask.imageset/Contents.json)0
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconBackTransitionMask.imageset/IconBackTransitionMask.pdf (renamed from ios/MullvadVPN/Assets.xcassets/IconBackTransitionMask.imageset/IconBackTransitionMask.pdf)bin967 -> 967 bytes
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconChevron.imageset/Contents.json (renamed from ios/MullvadVPN/Assets.xcassets/IconChevron.imageset/Contents.json)0
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconChevron.imageset/IconChevron.pdf (renamed from ios/MullvadVPN/Assets.xcassets/IconChevron.imageset/IconChevron.pdf)0
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconChevronDown.imageset/Contents.json (renamed from ios/MullvadVPN/Assets.xcassets/IconChevronDown.imageset/Contents.json)0
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconChevronDown.imageset/IconChevronDown.pdf (renamed from ios/MullvadVPN/Assets.xcassets/IconChevronDown.imageset/IconChevronDown.pdf)0
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconChevronUp.imageset/Contents.json (renamed from ios/MullvadVPN/Assets.xcassets/IconChevronUp.imageset/Contents.json)0
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconChevronUp.imageset/IconChevronUp.pdf (renamed from ios/MullvadVPN/Assets.xcassets/IconChevronUp.imageset/IconChevronUp.pdf)bin1059 -> 1059 bytes
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconClose.imageset/Contents.json (renamed from ios/MullvadVPN/Assets.xcassets/IconClose.imageset/Contents.json)0
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconClose.imageset/IconClose.pdf (renamed from ios/MullvadVPN/Assets.xcassets/IconClose.imageset/IconClose.pdf)bin1128 -> 1128 bytes
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconCloseSml.imageset/Contents.json (renamed from ios/MullvadVPN/Assets.xcassets/IconCloseSml.imageset/Contents.json)0
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconCloseSml.imageset/IconCloseSml.pdf (renamed from ios/MullvadVPN/Assets.xcassets/IconCloseSml.imageset/IconCloseSml.pdf)bin1137 -> 1137 bytes
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconCopy.imageset/Contents.json (renamed from ios/MullvadVPN/Assets.xcassets/IconCopy.imageset/Contents.json)0
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconCopy.imageset/IconCopy.pdf (renamed from ios/MullvadVPN/Assets.xcassets/IconCopy.imageset/IconCopy.pdf)bin1174 -> 1174 bytes
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconExtlink.imageset/Contents.json (renamed from ios/MullvadVPN/Assets.xcassets/IconExtlink.imageset/Contents.json)0
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconExtlink.imageset/IconExtlink.pdf (renamed from ios/MullvadVPN/Assets.xcassets/IconExtlink.imageset/IconExtlink.pdf)bin1124 -> 1124 bytes
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconFail.imageset/Contents.json (renamed from ios/MullvadVPN/Assets.xcassets/IconFail.imageset/Contents.json)0
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconFail.imageset/IconFail.pdf (renamed from ios/MullvadVPN/Assets.xcassets/IconFail.imageset/IconFail.pdf)bin1161 -> 1161 bytes
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconObscure.imageset/Contents.json (renamed from ios/MullvadVPN/Assets.xcassets/IconObscure.imageset/Contents.json)0
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconObscure.imageset/IconObscure.pdf (renamed from ios/MullvadVPN/Assets.xcassets/IconObscure.imageset/IconObscure.pdf)bin1481 -> 1481 bytes
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconReload.imageset/Contents.json (renamed from ios/MullvadVPN/Assets.xcassets/IconReload.imageset/Contents.json)0
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconReload.imageset/IconReload.pdf (renamed from ios/MullvadVPN/Assets.xcassets/IconReload.imageset/IconReload.pdf)0
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconSettings.imageset/Contents.json (renamed from ios/MullvadVPN/Assets.xcassets/IconSettings.imageset/Contents.json)0
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconSettings.imageset/IconSettings.pdf (renamed from ios/MullvadVPN/Assets.xcassets/IconSettings.imageset/IconSettings.pdf)bin1399 -> 1399 bytes
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconSpinner.imageset/Contents.json (renamed from ios/MullvadVPN/Assets.xcassets/IconSpinner.imageset/Contents.json)0
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconSpinner.imageset/IconSpinner.pdf (renamed from ios/MullvadVPN/Assets.xcassets/IconSpinner.imageset/IconSpinner.pdf)0
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconSuccess.imageset/Contents.json (renamed from ios/MullvadVPN/Assets.xcassets/IconSuccess.imageset/Contents.json)0
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconSuccess.imageset/IconSuccess.pdf (renamed from ios/MullvadVPN/Assets.xcassets/IconSuccess.imageset/IconSuccess.pdf)0
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconTick.imageset/Contents.json (renamed from ios/MullvadVPN/Assets.xcassets/IconTick.imageset/Contents.json)0
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconTick.imageset/IconTick.pdf (renamed from ios/MullvadVPN/Assets.xcassets/IconTick.imageset/IconTick.pdf)0
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconTickSml.imageset/Contents.json (renamed from ios/MullvadVPN/Assets.xcassets/IconTickSml.imageset/Contents.json)0
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconTickSml.imageset/IconTickSml.pdf (renamed from ios/MullvadVPN/Assets.xcassets/IconTickSml.imageset/IconTickSml.pdf)bin1043 -> 1043 bytes
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconUnobscure.imageset/Contents.json (renamed from ios/MullvadVPN/Assets.xcassets/IconUnobscure.imageset/Contents.json)0
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconUnobscure.imageset/IconUnobscure.pdf (renamed from ios/MullvadVPN/Assets.xcassets/IconUnobscure.imageset/IconUnobscure.pdf)bin1154 -> 1154 bytes
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/LocationMarkerSecure.imageset/Contents.json (renamed from ios/MullvadVPN/Assets.xcassets/LocationMarkerSecure.imageset/Contents.json)0
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/LocationMarkerSecure.imageset/LocationMarkerSecure.pdf (renamed from ios/MullvadVPN/Assets.xcassets/LocationMarkerSecure.imageset/LocationMarkerSecure.pdf)bin2405 -> 2405 bytes
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/LocationMarkerUnsecure.imageset/Contents.json (renamed from ios/MullvadVPN/Assets.xcassets/LocationMarkerUnsecure.imageset/Contents.json)0
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/LocationMarkerUnsecure.imageset/LocationMarkerUnsecure.pdf (renamed from ios/MullvadVPN/Assets.xcassets/LocationMarkerUnsecure.imageset/LocationMarkerUnsecure.pdf)bin2403 -> 2403 bytes
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/LogoIcon.imageset/Contents.json (renamed from ios/MullvadVPN/Assets.xcassets/LogoIcon.imageset/Contents.json)0
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/LogoIcon.imageset/LogoIcon.pdf (renamed from ios/MullvadVPN/Assets.xcassets/LogoIcon.imageset/LogoIcon.pdf)bin3057 -> 3057 bytes
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/LogoText.imageset/Contents.json (renamed from ios/MullvadVPN/Assets.xcassets/LogoText.imageset/Contents.json)0
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/LogoText.imageset/LogoText.pdf (renamed from ios/MullvadVPN/Assets.xcassets/LogoText.imageset/LogoText.pdf)bin2742 -> 2742 bytes
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/SuccessButton.imageset/Contents.json (renamed from ios/MullvadVPN/Assets.xcassets/SuccessButton.imageset/Contents.json)0
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/SuccessButton.imageset/SuccessButton.pdf (renamed from ios/MullvadVPN/Assets.xcassets/SuccessButton.imageset/SuccessButton.pdf)bin995 -> 995 bytes
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/TranslucentDangerButton.imageset/Contents.json (renamed from ios/MullvadVPN/Assets.xcassets/TranslucentDangerButton.imageset/Contents.json)0
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/TranslucentDangerButton.imageset/TranslucentDangerButton.pdf (renamed from ios/MullvadVPN/Assets.xcassets/TranslucentDangerButton.imageset/TranslucentDangerButton.pdf)bin998 -> 998 bytes
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/TranslucentDangerSplitLeftButton.imageset/Contents.json (renamed from ios/MullvadVPN/Assets.xcassets/TranslucentDangerSplitLeftButton.imageset/Contents.json)0
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/TranslucentDangerSplitLeftButton.imageset/TranslucentDangerSplitLeftButton.pdf (renamed from ios/MullvadVPN/Assets.xcassets/TranslucentDangerSplitLeftButton.imageset/TranslucentDangerSplitLeftButton.pdf)bin980 -> 980 bytes
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/TranslucentDangerSplitRightButton.imageset/Contents.json (renamed from ios/MullvadVPN/Assets.xcassets/TranslucentDangerSplitRightButton.imageset/Contents.json)0
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/TranslucentDangerSplitRightButton.imageset/TranslucentDangerSplitRightButton.pdf (renamed from ios/MullvadVPN/Assets.xcassets/TranslucentDangerSplitRightButton.imageset/TranslucentDangerSplitRightButton.pdf)bin980 -> 980 bytes
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/TranslucentNeutralButton.imageset/Contents.json (renamed from ios/MullvadVPN/Assets.xcassets/TranslucentNeutralButton.imageset/Contents.json)0
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/TranslucentNeutralButton.imageset/TranslucentNeutralButton.pdf (renamed from ios/MullvadVPN/Assets.xcassets/TranslucentNeutralButton.imageset/TranslucentNeutralButton.pdf)bin998 -> 998 bytes
-rw-r--r--ios/MullvadVPN/Supporting Files/Info.plist (renamed from ios/MullvadVPN/Info.plist)0
-rw-r--r--ios/MullvadVPN/Supporting Files/LaunchScreen.storyboard (renamed from ios/MullvadVPN/LaunchScreen.storyboard)0
-rw-r--r--ios/MullvadVPN/Supporting Files/MullvadVPN.entitlements (renamed from ios/MullvadVPN/MullvadVPN.entitlements)0
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelManager.swift17
-rw-r--r--ios/MullvadVPN/UI appearance/UIColor+Palette.swift (renamed from ios/MullvadVPN/UIColor+Palette.swift)0
-rw-r--r--ios/MullvadVPN/UI appearance/UIMetrics.swift (renamed from ios/MullvadVPN/UIMetrics.swift)0
-rw-r--r--ios/MullvadVPN/UIPresentationController+Private.swift35
-rw-r--r--ios/MullvadVPN/View controllers/Account/AccountContentView.swift (renamed from ios/MullvadVPN/AccountContentView.swift)2
-rw-r--r--ios/MullvadVPN/View controllers/Account/AccountInteractor.swift (renamed from ios/MullvadVPN/AccountInteractor.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/Account/AccountViewController.swift (renamed from ios/MullvadVPN/AccountViewController.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/Account/PaymentState.swift (renamed from ios/MullvadVPN/PaymentState.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/Account/ProductState.swift (renamed from ios/MullvadVPN/ProductState.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/DeviceList/DeviceManagementContentView.swift (renamed from ios/MullvadVPN/DeviceManagementContentView.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/DeviceList/DeviceManagementInteractor.swift (renamed from ios/MullvadVPN/DeviceManagementInteractor.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/DeviceList/DeviceManagementViewController.swift (renamed from ios/MullvadVPN/DeviceManagementViewController.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/DeviceList/DeviceRowView.swift (renamed from ios/MullvadVPN/DeviceRowView.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/Launch/LaunchViewController.swift (renamed from ios/MullvadVPN/LaunchViewController.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/Login/AccountInputGroupView.swift (renamed from ios/MullvadVPN/AccountInputGroupView.swift)39
-rw-r--r--ios/MullvadVPN/View controllers/Login/AccountTextField.swift (renamed from ios/MullvadVPN/AccountTextField.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/Login/AccountTokenInput.swift (renamed from ios/MullvadVPN/AccountTokenInput.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/Login/LoginContentView.swift (renamed from ios/MullvadVPN/LoginContentView.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/Login/LoginInteractor.swift54
-rw-r--r--ios/MullvadVPN/View controllers/Login/LoginViewController.swift (renamed from ios/MullvadVPN/LoginViewController.swift)157
-rw-r--r--ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeContentView.swift (renamed from ios/MullvadVPN/OutOfTimeContentView.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeInteractor.swift (renamed from ios/MullvadVPN/OutOfTimeInteractor.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeViewController.swift (renamed from ios/MullvadVPN/OutOfTimeViewController.swift)57
-rw-r--r--ios/MullvadVPN/View controllers/Preferences/PreferencesCellFactory.swift (renamed from ios/MullvadVPN/PreferencesCellFactory.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/Preferences/PreferencesDataSource.swift (renamed from ios/MullvadVPN/PreferencesDataSource.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/Preferences/PreferencesDataSourceDelegate.swift (renamed from ios/MullvadVPN/PreferencesDataSourceDelegate.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/Preferences/PreferencesInteractor.swift (renamed from ios/MullvadVPN/PreferencesInteractor.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift (renamed from ios/MullvadVPN/PreferencesViewController.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/Preferences/PreferencesViewModel.swift (renamed from ios/MullvadVPN/PreferencesViewModel.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/ProblemReport/ProblemReportInteractor.swift (renamed from ios/MullvadVPN/ProblemReportInteractor.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/ProblemReport/ProblemReportReviewViewController.swift (renamed from ios/MullvadVPN/ProblemReportReviewViewController.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/ProblemReport/ProblemReportSubmissionOverlayView.swift (renamed from ios/MullvadVPN/ProblemReportSubmissionOverlayView.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController.swift (renamed from ios/MullvadVPN/ProblemReportViewController.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/RevokedDevice/RevokedDeviceInteractor.swift (renamed from ios/MullvadVPN/RevokedDeviceInteractor.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/RevokedDevice/RevokedDeviceViewController.swift (renamed from ios/MullvadVPN/RevokedDeviceViewController.swift)8
-rw-r--r--ios/MullvadVPN/View controllers/SelectLocation/LocationCellFactory.swift (renamed from ios/MullvadVPN/LocationCellFactory.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/SelectLocation/LocationDataSource.swift (renamed from ios/MullvadVPN/LocationDataSource.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/SelectLocation/SelectLocationCell.swift (renamed from ios/MullvadVPN/SelectLocationCell.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/SelectLocation/SelectLocationHeaderView.swift (renamed from ios/MullvadVPN/SelectLocationHeaderView.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/SelectLocation/SelectLocationViewController.swift (renamed from ios/MullvadVPN/SelectLocationViewController.swift)29
-rw-r--r--ios/MullvadVPN/View controllers/Settings/SettingsAccountCell.swift (renamed from ios/MullvadVPN/SettingsAccountCell.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/Settings/SettingsAddDNSEntryCell.swift (renamed from ios/MullvadVPN/SettingsAddDNSEntryCell.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/Settings/SettingsCell.swift (renamed from ios/MullvadVPN/SettingsCell.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/Settings/SettingsCellFactory.swift (renamed from ios/MullvadVPN/SettingsCellFactory.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/Settings/SettingsDNSTextCell.swift (renamed from ios/MullvadVPN/SettingsDNSTextCell.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/Settings/SettingsDataSource.swift (renamed from ios/MullvadVPN/SettingsDataSource.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/Settings/SettingsDataSourceDelegate.swift (renamed from ios/MullvadVPN/SettingsDataSourceDelegate.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/Settings/SettingsInteractor.swift (renamed from ios/MullvadVPN/SettingsInteractor.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/Settings/SettingsInteractorFactory.swift (renamed from ios/MullvadVPN/SettingsInteractorFactory.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/Settings/SettingsStaticTextFooterView.swift (renamed from ios/MullvadVPN/SettingsStaticTextFooterView.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/Settings/SettingsSwitchCell.swift (renamed from ios/MullvadVPN/SettingsSwitchCell.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/Settings/SettingsViewController.swift (renamed from ios/MullvadVPN/SettingsViewController.swift)35
-rw-r--r--ios/MullvadVPN/View controllers/TermsOfService/TermsOfServiceContentView.swift (renamed from ios/MullvadVPN/TermsOfServiceContentView.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/TermsOfService/TermsOfServiceViewController.swift (renamed from ios/MullvadVPN/TermsOfServiceViewController.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/ConnectionPanelView.swift (renamed from ios/MullvadVPN/ConnectionPanelView.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/CustomOverlayRenderer.swift (renamed from ios/MullvadVPN/CustomOverlayRenderer.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/DisconnectSplitButton.swift (renamed from ios/MullvadVPN/DisconnectSplitButton.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/GeoJSON.swift (renamed from ios/MullvadVPN/GeoJSON.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/MapViewController.swift (renamed from ios/MullvadVPN/MapViewController.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/TranslucentButtonBlurView.swift (renamed from ios/MullvadVPN/TranslucentButtonBlurView.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift (renamed from ios/MullvadVPN/TunnelControlView.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift (renamed from ios/MullvadVPN/TunnelViewController.swift)0
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/TunnelViewControllerInteractor.swift (renamed from ios/MullvadVPN/TunnelViewControllerInteractor.swift)0
-rw-r--r--ios/MullvadVPN/Views/AppButton.swift (renamed from ios/MullvadVPN/AppButton.swift)0
-rw-r--r--ios/MullvadVPN/Views/CustomSwitch.swift (renamed from ios/MullvadVPN/CustomSwitch.swift)0
-rw-r--r--ios/MullvadVPN/Views/CustomSwitchContainer.swift (renamed from ios/MullvadVPN/CustomSwitchContainer.swift)0
-rw-r--r--ios/MullvadVPN/Views/CustomTextField.swift (renamed from ios/MullvadVPN/CustomTextField.swift)0
-rw-r--r--ios/MullvadVPN/Views/CustomTextView.swift (renamed from ios/MullvadVPN/CustomTextView.swift)0
-rw-r--r--ios/MullvadVPN/Views/EmptyTableViewHeaderFooterView.swift (renamed from ios/MullvadVPN/EmptyTableViewHeaderFooterView.swift)0
-rw-r--r--ios/MullvadVPN/Views/InAppPurchaseButton.swift (renamed from ios/MullvadVPN/InAppPurchaseButton.swift)0
-rw-r--r--ios/MullvadVPN/Views/SpinnerActivityIndicatorView.swift (renamed from ios/MullvadVPN/SpinnerActivityIndicatorView.swift)0
-rw-r--r--ios/MullvadVPN/Views/StatusActivityView.swift (renamed from ios/MullvadVPN/StatusActivityView.swift)0
-rw-r--r--ios/MullvadVPN/Views/StatusImageView.swift (renamed from ios/MullvadVPN/StatusImageView.swift)0
-rw-r--r--ios/MullvadVPN/WireguardKeysContentView.swift388
-rw-r--r--ios/PacketTunnel/NEProviderStopReason+Debug.swift (renamed from ios/MullvadVPN/NEProviderStopReason+Debug.swift)0
202 files changed, 3490 insertions, 1981 deletions
diff --git a/ios/MullvadTypes/Promise.swift b/ios/MullvadTypes/Promise.swift
new file mode 100644
index 0000000000..432316e872
--- /dev/null
+++ b/ios/MullvadTypes/Promise.swift
@@ -0,0 +1,49 @@
+//
+// Promise.swift
+// MullvadVPN
+//
+// Created by pronebird on 28/01/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+public final class Promise<Success, Failure: Error> {
+ public typealias Result = Swift.Result<Success, Failure>
+
+ private let nslock = NSLock()
+ private var observers: [(Result) -> Void] = []
+ private var result: Result?
+
+ public init(_ executor: (@escaping (Result) -> Void) -> Void) {
+ executor(resolve)
+ }
+
+ public func observe(_ completion: @escaping (Result) -> Void) {
+ nslock.lock()
+ if let result = result {
+ nslock.unlock()
+ completion(result)
+ } else {
+ observers.append(completion)
+ nslock.unlock()
+ }
+ }
+
+ private func resolve(result: Result) {
+ nslock.lock()
+ if self.result == nil {
+ self.result = result
+
+ let observers = observers
+ self.observers.removeAll()
+ nslock.unlock()
+
+ for observer in observers {
+ observer(result)
+ }
+ } else {
+ nslock.unlock()
+ }
+ }
+}
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 35fa04a23d..cd258fb365 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -71,13 +71,10 @@
580909D32876D09A0078138D /* RevokedDeviceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580909D22876D09A0078138D /* RevokedDeviceViewController.swift */; };
580F8B8328197881002E0998 /* TunnelSettingsV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580F8B8228197881002E0998 /* TunnelSettingsV2.swift */; };
580F8B8428197884002E0998 /* TunnelSettingsV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580F8B8228197881002E0998 /* TunnelSettingsV2.swift */; };
- 580F8B8628197958002E0998 /* DNSSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580F8B8528197958002E0998 /* DNSSettings.swift */; };
- 580F8B872819795C002E0998 /* DNSSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580F8B8528197958002E0998 /* DNSSettings.swift */; };
5811DE50239014550011EB53 /* NEVPNStatus+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5811DE4F239014550011EB53 /* NEVPNStatus+Debug.swift */; };
58138E61294871C600684F0C /* DeviceDataThrottling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58138E60294871C600684F0C /* DeviceDataThrottling.swift */; };
58153071294CBE8B00D1702E /* MullvadREST.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 06799ABC28F98E1D00ACD94E /* MullvadREST.framework */; };
5819C2142726CC8D00D6EC38 /* DataSourceSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5819C2132726CC8D00D6EC38 /* DataSourceSnapshotTests.swift */; };
- 5819C2152726CC9400D6EC38 /* DataSourceSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587EB66F27143B6500123C75 /* DataSourceSnapshot.swift */; };
5819C2172729595500D6EC38 /* SettingsAddDNSEntryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5819C2162729595500D6EC38 /* SettingsAddDNSEntryCell.swift */; };
5820676426E771DB00655B05 /* TunnelManagerErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820676326E771DB00655B05 /* TunnelManagerErrors.swift */; };
5820EDA9288FE064006BF4E4 /* DeviceManagementInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820EDA8288FE064006BF4E4 /* DeviceManagementInteractor.swift */; };
@@ -89,22 +86,27 @@
582A8A3A28BCE19B00D0F9FB /* FixedWidthIntegerArithmeticsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582A8A3928BCE19B00D0F9FB /* FixedWidthIntegerArithmeticsTests.swift */; };
582AE3102440A6CA00E6733A /* AccountTokenInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582AE30F2440A6CA00E6733A /* AccountTokenInput.swift */; };
582AE3122440CA0D00E6733A /* AccountTokenInputTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582AE3112440CA0D00E6733A /* AccountTokenInputTests.swift */; };
- 582AE3132440CA2700E6733A /* AccountTokenInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582AE30F2440A6CA00E6733A /* AccountTokenInput.swift */; };
582BB1AF229566420055B6EF /* SettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582BB1AE229566420055B6EF /* SettingsCell.swift */; };
- 582BB1B1229569620055B6EF /* CustomNavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582BB1B0229569620055B6EF /* CustomNavigationBar.swift */; };
+ 582BB1B1229569620055B6EF /* UINavigationBar+Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582BB1B0229569620055B6EF /* UINavigationBar+Appearance.swift */; };
582BB1B3229574F40055B6EF /* SettingsAccountCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582BB1B2229574F40055B6EF /* SettingsAccountCell.swift */; };
5835B7CC233B76CB0096D79F /* TunnelManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5835B7CB233B76CB0096D79F /* TunnelManager.swift */; };
5838318B27C40A3900000571 /* Pinger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5838318A27C40A3900000571 /* Pinger.swift */; };
583DA21425FA4B5C00318683 /* LocationDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583DA21325FA4B5C00318683 /* LocationDataSource.swift */; };
583E1E2C2848E1A1004838B3 /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = 583E1E2B2848E1A1004838B3 /* WireGuardKitTypes */; };
+ 583FE00C29C0C7FD006E85F9 /* ModalPresentationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583FE00B29C0C7FD006E85F9 /* ModalPresentationConfiguration.swift */; };
+ 583FE00E29C0D586006E85F9 /* OutOfTimeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583FE00D29C0D586006E85F9 /* OutOfTimeCoordinator.swift */; };
+ 583FE01029C0F532006E85F9 /* CustomSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583FE00F29C0F532006E85F9 /* CustomSplitViewController.swift */; };
+ 583FE01229C0F99A006E85F9 /* PresentationControllerDismissalInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583FE01129C0F99A006E85F9 /* PresentationControllerDismissalInterceptor.swift */; };
+ 583FE02429C1ACB3006E85F9 /* RESTCreateApplePaymentResponse+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06FAE67828F83CA50033DD93 /* RESTCreateApplePaymentResponse+Localization.swift */; };
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 */; };
- 584555F42991176200DD0657 /* UIPresentationController+Private.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584555F32991176200DD0657 /* UIPresentationController+Private.swift */; };
+ 58435AC229CB2A350099C71B /* LocationCellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58435AC129CB2A350099C71B /* LocationCellFactory.swift */; };
584592612639B4A200EF967F /* TermsOfServiceContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584592602639B4A200EF967F /* TermsOfServiceContentView.swift */; };
5846227126E229F20035F7C2 /* StoreSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846227026E229F20035F7C2 /* StoreSubscription.swift */; };
5846227326E22A160035F7C2 /* StorePaymentObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846227226E22A160035F7C2 /* StorePaymentObserver.swift */; };
5846227726E22A7C0035F7C2 /* StorePaymentManagerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846227626E22A7C0035F7C2 /* StorePaymentManagerDelegate.swift */; };
+ 5847D58D29B7740F008C3808 /* RevokedCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5847D58C29B7740F008C3808 /* RevokedCoordinator.swift */; };
584B17AB27637DE40057F3B8 /* ReconnectTunnelOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584B17AA27637DE40057F3B8 /* ReconnectTunnelOperation.swift */; };
584D26C2270C8542004EA533 /* SettingsStaticTextFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584D26C1270C8542004EA533 /* SettingsStaticTextFooterView.swift */; };
584D26C4270C855B004EA533 /* PreferencesDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584D26C3270C855A004EA533 /* PreferencesDataSource.swift */; };
@@ -112,20 +114,23 @@
584EBDBD2747C98F00A0C9FD /* NSAttributedString+Markdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584EBDBC2747C98F00A0C9FD /* NSAttributedString+Markdown.swift */; };
584F99202902CBDD001F858D /* libRelaySelector.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5898D29829017DAC00EB5EBA /* libRelaySelector.a */; };
5857F24324C8662600CF6F47 /* SelectLocationHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5857F24224C8662600CF6F47 /* SelectLocationHeaderView.swift */; };
- 5857F24724C882D700CF6F47 /* SelectLocationNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5857F24624C882D700CF6F47 /* SelectLocationNavigationController.swift */; };
585B4B8726D9098900555C4C /* TunnelStatusNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A94AE326CFD945001CB97C /* TunnelStatusNotificationProvider.swift */; };
585CA70F25F8C44600B47C62 /* UIMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585CA70E25F8C44600B47C62 /* UIMetrics.swift */; };
585E820327F3285E00939F0E /* SendStoreReceiptOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585E820227F3285E00939F0E /* SendStoreReceiptOperation.swift */; };
58607A4D2947287800BC467D /* AccountExpiryInAppNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58607A4C2947287800BC467D /* AccountExpiryInAppNotificationProvider.swift */; };
586168692976F6BD00EF8598 /* DisplayError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586168682976F6BD00EF8598 /* DisplayError.swift */; };
5862805422428EF100F5A6E1 /* TranslucentButtonBlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.swift */; };
+ 5864859929A0D028006C5743 /* FormsheetPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5864859829A0D028006C5743 /* FormsheetPresentationController.swift */; };
+ 5864859B29A0EAF2006C5743 /* SecondaryContextPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5864859A29A0EAF2006C5743 /* SecondaryContextPresentationController.swift */; };
+ 5864AF0729C78843005B0CD9 /* SettingsCellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5864AF0029C7879B005B0CD9 /* SettingsCellFactory.swift */; };
+ 5864AF0829C78849005B0CD9 /* CellFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5864AF0129C7879B005B0CD9 /* CellFactoryProtocol.swift */; };
+ 5864AF0929C78850005B0CD9 /* PreferencesCellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5864AF0229C7879B005B0CD9 /* PreferencesCellFactory.swift */; };
5867770E29096984006F721F /* OutOfTimeInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5867770D29096984006F721F /* OutOfTimeInteractor.swift */; };
58677710290975E9006F721F /* SettingsInteractorFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5867770F290975E8006F721F /* SettingsInteractorFactory.swift */; };
58677712290976FB006F721F /* SettingsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58677711290976FB006F721F /* SettingsInteractor.swift */; };
5867771429097BCD006F721F /* PaymentState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5867771329097BCD006F721F /* PaymentState.swift */; };
5867771629097C5B006F721F /* ProductState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5867771529097C5B006F721F /* ProductState.swift */; };
5868585524054096000B8131 /* AppButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5868585424054096000B8131 /* AppButton.swift */; };
- 5868BD33261DCD2600E6027F /* CustomSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5868BD32261DCD2600E6027F /* CustomSplitViewController.swift */; };
586A950C290125EE007BAF2B /* AlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B9EB122488ED2100095626 /* AlertPresenter.swift */; };
586A950D290125F0007BAF2B /* PresentAlertOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820675D26E6839900655B05 /* PresentAlertOperation.swift */; };
586A950E290125F3007BAF2B /* ProductsRequestOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846226426E0D9630035F7C2 /* ProductsRequestOperation.swift */; };
@@ -156,6 +161,8 @@
587B753D2666468F00DEF7E9 /* NotificationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587B753C2666468F00DEF7E9 /* NotificationController.swift */; };
587B753F2668E5A700DEF7E9 /* NotificationContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587B753E2668E5A700DEF7E9 /* NotificationContainerView.swift */; };
587B75412668FD7800DEF7E9 /* AccountExpirySystemNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587B75402668FD7700DEF7E9 /* AccountExpirySystemNotificationProvider.swift */; };
+ 587C92FE2986E28100FB9664 /* SelectLocationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587C92FD2986E28100FB9664 /* SelectLocationCoordinator.swift */; };
+ 587C93002986E2B600FB9664 /* TermsOfServiceCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587C92FF2986E2B600FB9664 /* TermsOfServiceCoordinator.swift */; };
587CBFE322807F530028DED3 /* UIColor+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587CBFE222807F530028DED3 /* UIColor+Helpers.swift */; };
587D96742886D87C00CD8F1C /* DeviceManagementContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587D96732886D87C00CD8F1C /* DeviceManagementContentView.swift */; };
587D9676288989DB00CD8F1C /* NSLayoutConstraint+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587D9675288989DB00CD8F1C /* NSLayoutConstraint+Helpers.swift */; };
@@ -176,10 +183,12 @@
5891BF5125E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5891BF5025E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift */; };
5892A45E265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5892A45D265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift */; };
5893716A28817A45004EE76C /* DeviceManagementViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5893716928817A45004EE76C /* DeviceManagementViewController.swift */; };
+ 5893C6F929C1B480009090D1 /* DNSSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580F8B8528197958002E0998 /* DNSSettings.swift */; };
+ 5893C6FA29C1B481009090D1 /* DNSSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580F8B8528197958002E0998 /* DNSSettings.swift */; };
+ 5893C6FC29C311E9009090D1 /* ApplicationRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5893C6FB29C311E9009090D1 /* ApplicationRouter.swift */; };
58968FAE28743E2000B799DC /* TunnelInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58968FAD28743E2000B799DC /* TunnelInteractor.swift */; };
5896AE84246D5889005B36CB /* CustomDateComponentsFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5896AE83246D5889005B36CB /* CustomDateComponentsFormatting.swift */; };
5896AE86246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5896AE85246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift */; };
- 5896AE88246D7FAF005B36CB /* CustomDateComponentsFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5896AE83246D5889005B36CB /* CustomDateComponentsFormatting.swift */; };
5896CEF226972DEB00B0FAE8 /* AccountContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5896CEF126972DEB00B0FAE8 /* AccountContentView.swift */; };
5897F1742913EAF800AF5695 /* ExponentialBackoff.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5897F1732913EAF800AF5695 /* ExponentialBackoff.swift */; };
5897F1762914E62E00AF5695 /* Duration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5897F1752914E62E00AF5695 /* Duration.swift */; };
@@ -205,7 +214,6 @@
58A8EE5A2976BFBB009C0F8D /* SKError+Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A8EE592976BFBB009C0F8D /* SKError+Localized.swift */; };
58A8EE5E2976DB00009C0F8D /* StorePaymentManagerError+Display.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A8EE5D2976DB00009C0F8D /* StorePaymentManagerError+Display.swift */; };
58A99ED3240014A0006599E9 /* TermsOfServiceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A99ED2240014A0006599E9 /* TermsOfServiceViewController.swift */; };
- 58ACA9ED2979569500B5825C /* ModalRootAdaptivePresentationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ACA9EC2979569500B5825C /* ModalRootAdaptivePresentationDelegate.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 */; };
@@ -219,14 +227,24 @@
58B26E2A2943545A00D5980C /* NotificationManagerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B26E292943545A00D5980C /* NotificationManagerDelegate.swift */; };
58B3F30F2742708B00A2DD38 /* HeaderBarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B3F30E2742708B00A2DD38 /* HeaderBarButton.swift */; };
58B43C1925F77DB60002C8C3 /* TunnelControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B43C1825F77DB60002C8C3 /* TunnelControlView.swift */; };
+ 58B8644529C7971B005E107C /* AccountTokenInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582AE30F2440A6CA00E6733A /* AccountTokenInput.swift */; };
+ 58B8644629C7972F005E107C /* CustomDateComponentsFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5896AE83246D5889005B36CB /* CustomDateComponentsFormatting.swift */; };
+ 58B8644729C79737005E107C /* DataSourceSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587EB66F27143B6500123C75 /* DataSourceSnapshot.swift */; };
58B93A1326C3F13600A55733 /* TunnelState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B93A1226C3F13600A55733 /* TunnelState.swift */; };
58B993B12608A34500BA7811 /* LoginContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B993B02608A34500BA7811 /* LoginContentView.swift */; };
58B9EB152489139B00095626 /* RESTError+Display.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B9EB142489139B00095626 /* RESTError+Display.swift */; };
58BA693123EADA6A009DC256 /* SimulatorTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BA693023EADA6A009DC256 /* SimulatorTunnelProvider.swift */; };
+ 58BBB39729717E0C00C8DB7C /* ApplicationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BBB39629717E0C00C8DB7C /* ApplicationCoordinator.swift */; };
58BFA5C622A7C97F00A6173D /* RelayCacheTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5C522A7C97F00A6173D /* RelayCacheTracker.swift */; };
58BFA5CC22A7CE1F00A6173D /* ApplicationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */; };
58C3A4B222456F1B00340BDB /* AccountInputGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C3A4B122456F1A00340BDB /* AccountInputGroupView.swift */; };
58C3F4F92964B08300D72515 /* MapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C3F4F82964B08300D72515 /* MapViewController.swift */; };
+ 58C3F4FB296C3AD500D72515 /* SettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C3F4FA296C3AD500D72515 /* SettingsCoordinator.swift */; };
+ 58C774BE29A7A249003A1A56 /* CustomNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C774BD29A7A249003A1A56 /* CustomNavigationController.swift */; };
+ 58CAF9F82983D36800BE19F7 /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CAF9F72983D36800BE19F7 /* Coordinator.swift */; };
+ 58CAF9FA2983E0C600BE19F7 /* LoginCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CAF9F92983E0C600BE19F7 /* LoginCoordinator.swift */; };
+ 58CAFA002983FF0200BE19F7 /* LoginInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CAF9FF2983FF0200BE19F7 /* LoginInteractor.swift */; };
+ 58CAFA032985367600BE19F7 /* Promise.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CAFA01298530DC00BE19F7 /* Promise.swift */; };
58CC40EF24A601900019D96E /* ObserverList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CC40EE24A601900019D96E /* ObserverList.swift */; };
58CCA010224249A1004F3011 /* TunnelViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CCA00F224249A1004F3011 /* TunnelViewController.swift */; };
58CCA01222424D11004F3011 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CCA01122424D11004F3011 /* SettingsViewController.swift */; };
@@ -315,19 +333,18 @@
58E25F812837BBBB002CFB2C /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E25F802837BBBB002CFB2C /* SceneDelegate.swift */; };
58E511E628DDDEAC00B0BCDE /* CodingErrors+CustomErrorDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E511E528DDDEAC00B0BCDE /* CodingErrors+CustomErrorDescription.swift */; };
58E511E828DDDF2400B0BCDE /* CodingErrors+CustomErrorDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E511E528DDDEAC00B0BCDE /* CodingErrors+CustomErrorDescription.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 */; };
58F1311527E0B2AB007AC5BC /* Result+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F1311427E0B2AB007AC5BC /* Result+Extensions.swift */; };
+ 58F185AA298A3E3E00075977 /* TunnelCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F185A9298A3E3E00075977 /* TunnelCoordinator.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 */; };
58F2E148276A307400A79513 /* MapConnectionStatusOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F2E147276A307400A79513 /* MapConnectionStatusOperation.swift */; };
58F2E14C276A61C000A79513 /* RotateKeyOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F2E14B276A61C000A79513 /* RotateKeyOperation.swift */; };
58F3C0A4249CB069003E76BE /* HeaderBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F3C0A3249CB069003E76BE /* HeaderBarView.swift */; };
- 58F7CA882692E34000FC59FD /* WireguardKeysContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F7CA872692E34000FC59FD /* WireguardKeysContentView.swift */; };
58F8AC0E25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F8AC0D25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift */; };
58FB865526E8BF3100F188BC /* StorePaymentManagerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FB865426E8BF3100F188BC /* StorePaymentManagerError.swift */; };
58FB865A26EA214400F188BC /* RelayCacheTrackerObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FB865926EA214400F188BC /* RelayCacheTrackerObserver.swift */; };
@@ -340,13 +357,9 @@
58FEEB46260A028D00A621A8 /* GeoJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FEEB45260A028D00A621A8 /* GeoJSON.swift */; };
58FEEB58260B662E00A621A8 /* AutomaticKeyboardResponder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FEEB57260B662E00A621A8 /* AutomaticKeyboardResponder.swift */; };
58FF2C03281BDE02009EF542 /* SettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FF2C02281BDE02009EF542 /* SettingsManager.swift */; };
- 7AD8490D29BA1EC500878E53 /* SettingsCellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD8490C29BA1EC500878E53 /* SettingsCellFactory.swift */; };
- 7AD8490F29BA26B000878E53 /* CellFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD8490E29BA26B000878E53 /* CellFactoryProtocol.swift */; };
- 7AD8491129BA316500878E53 /* PreferencesCellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD8491029BA316500878E53 /* PreferencesCellFactory.swift */; };
- 7AF1E73A29C47727002C6633 /* LocationCellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF1E73929C47727002C6633 /* LocationCellFactory.swift */; };
E1187ABC289BBB850024E748 /* OutOfTimeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1187ABA289BBB850024E748 /* OutOfTimeViewController.swift */; };
E1187ABD289BBB850024E748 /* OutOfTimeContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1187ABB289BBB850024E748 /* OutOfTimeContentView.swift */; };
- E158B360285381C60002F069 /* StringFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E158B35F285381C60002F069 /* StringFormatter.swift */; };
+ E158B360285381C60002F069 /* String+AccountFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = E158B35F285381C60002F069 /* String+AccountFormatting.swift */; };
E1FD0DF528AA7CE400299DB4 /* StatusActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FD0DF428AA7CE400299DB4 /* StatusActivityView.swift */; };
/* End PBXBuildFile section */
@@ -687,25 +700,30 @@
582AE30F2440A6CA00E6733A /* AccountTokenInput.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountTokenInput.swift; sourceTree = "<group>"; };
582AE3112440CA0D00E6733A /* AccountTokenInputTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountTokenInputTests.swift; sourceTree = "<group>"; };
582BB1AE229566420055B6EF /* SettingsCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsCell.swift; sourceTree = "<group>"; };
- 582BB1B0229569620055B6EF /* CustomNavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomNavigationBar.swift; sourceTree = "<group>"; };
+ 582BB1B0229569620055B6EF /* UINavigationBar+Appearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationBar+Appearance.swift"; sourceTree = "<group>"; };
582BB1B2229574F40055B6EF /* SettingsAccountCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAccountCell.swift; sourceTree = "<group>"; };
582FFA82290A84E700895745 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
5835B7CB233B76CB0096D79F /* TunnelManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelManager.swift; sourceTree = "<group>"; };
5838318A27C40A3900000571 /* Pinger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pinger.swift; sourceTree = "<group>"; };
583DA21325FA4B5C00318683 /* LocationDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationDataSource.swift; sourceTree = "<group>"; };
583E1E292848DF67004838B3 /* OperationObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationObserverTests.swift; sourceTree = "<group>"; };
+ 583FE00B29C0C7FD006E85F9 /* ModalPresentationConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalPresentationConfiguration.swift; sourceTree = "<group>"; };
+ 583FE00D29C0D586006E85F9 /* OutOfTimeCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutOfTimeCoordinator.swift; sourceTree = "<group>"; };
+ 583FE00F29C0F532006E85F9 /* CustomSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSplitViewController.swift; sourceTree = "<group>"; };
+ 583FE01129C0F99A006E85F9 /* PresentationControllerDismissalInterceptor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationControllerDismissalInterceptor.swift; sourceTree = "<group>"; };
5840250322B11AB700E4CFEC /* MullvadEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadEndpoint.swift; sourceTree = "<group>"; };
5840BE34279EDB16002836BA /* OperationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationError.swift; sourceTree = "<group>"; };
5842102D282D3FC200F24E46 /* ResultBlockOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultBlockOperation.swift; sourceTree = "<group>"; };
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>"; };
- 584555F32991176200DD0657 /* UIPresentationController+Private.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIPresentationController+Private.swift"; sourceTree = "<group>"; };
+ 58435AC129CB2A350099C71B /* LocationCellFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationCellFactory.swift; sourceTree = "<group>"; };
584592602639B4A200EF967F /* TermsOfServiceContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TermsOfServiceContentView.swift; sourceTree = "<group>"; };
5846226426E0D9630035F7C2 /* ProductsRequestOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductsRequestOperation.swift; sourceTree = "<group>"; };
5846227026E229F20035F7C2 /* StoreSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreSubscription.swift; sourceTree = "<group>"; };
5846227226E22A160035F7C2 /* StorePaymentObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorePaymentObserver.swift; sourceTree = "<group>"; };
5846227626E22A7C0035F7C2 /* StorePaymentManagerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorePaymentManagerDelegate.swift; sourceTree = "<group>"; };
+ 5847D58C29B7740F008C3808 /* RevokedCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RevokedCoordinator.swift; sourceTree = "<group>"; };
584B17AA27637DE40057F3B8 /* ReconnectTunnelOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReconnectTunnelOperation.swift; sourceTree = "<group>"; };
584B26F3237434D00073B10E /* RelaySelectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelectorTests.swift; sourceTree = "<group>"; };
584D0111299134AB00531822 /* Version.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Version.xcconfig; sourceTree = "<group>"; };
@@ -716,7 +734,6 @@
584EBDBC2747C98F00A0C9FD /* NSAttributedString+Markdown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+Markdown.swift"; sourceTree = "<group>"; };
58561C98239A5D1500BD6B5E /* IPv4Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPv4Endpoint.swift; sourceTree = "<group>"; };
5857F24224C8662600CF6F47 /* SelectLocationHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationHeaderView.swift; sourceTree = "<group>"; };
- 5857F24624C882D700CF6F47 /* SelectLocationNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationNavigationController.swift; sourceTree = "<group>"; };
585CA70E25F8C44600B47C62 /* UIMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIMetrics.swift; sourceTree = "<group>"; };
585DA87626B024A600B8C587 /* CachedRelays.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedRelays.swift; sourceTree = "<group>"; };
585DA89226B0323E00B8C587 /* TunnelProviderMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelProviderMessage.swift; sourceTree = "<group>"; };
@@ -725,6 +742,11 @@
58607A4C2947287800BC467D /* AccountExpiryInAppNotificationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiryInAppNotificationProvider.swift; sourceTree = "<group>"; };
586168682976F6BD00EF8598 /* DisplayError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayError.swift; sourceTree = "<group>"; };
5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslucentButtonBlurView.swift; sourceTree = "<group>"; };
+ 5864859829A0D028006C5743 /* FormsheetPresentationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormsheetPresentationController.swift; sourceTree = "<group>"; };
+ 5864859A29A0EAF2006C5743 /* SecondaryContextPresentationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondaryContextPresentationController.swift; sourceTree = "<group>"; };
+ 5864AF0029C7879B005B0CD9 /* SettingsCellFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsCellFactory.swift; sourceTree = "<group>"; };
+ 5864AF0129C7879B005B0CD9 /* CellFactoryProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CellFactoryProtocol.swift; sourceTree = "<group>"; };
+ 5864AF0229C7879B005B0CD9 /* PreferencesCellFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesCellFactory.swift; sourceTree = "<group>"; };
5866F39B2243B82D00168AE5 /* MullvadVPN.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MullvadVPN.entitlements; sourceTree = "<group>"; };
5867770D29096984006F721F /* OutOfTimeInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutOfTimeInteractor.swift; sourceTree = "<group>"; };
5867770F290975E8006F721F /* SettingsInteractorFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInteractorFactory.swift; sourceTree = "<group>"; };
@@ -732,7 +754,6 @@
5867771329097BCD006F721F /* PaymentState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentState.swift; sourceTree = "<group>"; };
5867771529097C5B006F721F /* ProductState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductState.swift; sourceTree = "<group>"; };
5868585424054096000B8131 /* AppButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppButton.swift; sourceTree = "<group>"; };
- 5868BD32261DCD2600E6027F /* CustomSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSplitViewController.swift; sourceTree = "<group>"; };
586A95112901321B007BAF2B /* IPv6Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPv6Endpoint.swift; sourceTree = "<group>"; };
586A951329013235007BAF2B /* AnyIPEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyIPEndpoint.swift; sourceTree = "<group>"; };
586E54FA27A2DF6D0029B88B /* SendTunnelProviderMessageOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendTunnelProviderMessageOperation.swift; sourceTree = "<group>"; };
@@ -763,6 +784,8 @@
587B75402668FD7700DEF7E9 /* AccountExpirySystemNotificationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpirySystemNotificationProvider.swift; sourceTree = "<group>"; };
587B7544266922BF00DEF7E9 /* en */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
587C575226D2615F005EF767 /* PacketTunnelOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelOptions.swift; sourceTree = "<group>"; };
+ 587C92FD2986E28100FB9664 /* SelectLocationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationCoordinator.swift; sourceTree = "<group>"; };
+ 587C92FF2986E2B600FB9664 /* TermsOfServiceCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TermsOfServiceCoordinator.swift; sourceTree = "<group>"; };
587CBFE222807F530028DED3 /* UIColor+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Helpers.swift"; sourceTree = "<group>"; };
587D96732886D87C00CD8F1C /* DeviceManagementContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceManagementContentView.swift; sourceTree = "<group>"; };
587D9675288989DB00CD8F1C /* NSLayoutConstraint+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSLayoutConstraint+Helpers.swift"; sourceTree = "<group>"; };
@@ -782,6 +805,7 @@
5891BF5025E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBarButtonItem+KeyboardNavigation.swift"; sourceTree = "<group>"; };
5892A45D265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyTableViewHeaderFooterView.swift; sourceTree = "<group>"; };
5893716928817A45004EE76C /* DeviceManagementViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceManagementViewController.swift; sourceTree = "<group>"; };
+ 5893C6FB29C311E9009090D1 /* ApplicationRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationRouter.swift; sourceTree = "<group>"; };
58968FAD28743E2000B799DC /* TunnelInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelInteractor.swift; sourceTree = "<group>"; };
5896AE83246D5889005B36CB /* CustomDateComponentsFormatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDateComponentsFormatting.swift; sourceTree = "<group>"; };
5896AE85246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDateComponentsFormattingTests.swift; sourceTree = "<group>"; };
@@ -809,7 +833,6 @@
58A8EE5D2976DB00009C0F8D /* StorePaymentManagerError+Display.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StorePaymentManagerError+Display.swift"; sourceTree = "<group>"; };
58A94AE326CFD945001CB97C /* TunnelStatusNotificationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelStatusNotificationProvider.swift; sourceTree = "<group>"; };
58A99ED2240014A0006599E9 /* TermsOfServiceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TermsOfServiceViewController.swift; sourceTree = "<group>"; };
- 58ACA9EC2979569500B5825C /* ModalRootAdaptivePresentationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalRootAdaptivePresentationDelegate.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>"; };
@@ -830,10 +853,17 @@
58B9EB122488ED2100095626 /* AlertPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertPresenter.swift; sourceTree = "<group>"; };
58B9EB142489139B00095626 /* RESTError+Display.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RESTError+Display.swift"; sourceTree = "<group>"; };
58BA693023EADA6A009DC256 /* SimulatorTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorTunnelProvider.swift; sourceTree = "<group>"; };
+ 58BBB39629717E0C00C8DB7C /* ApplicationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationCoordinator.swift; sourceTree = "<group>"; };
58BFA5C522A7C97F00A6173D /* RelayCacheTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayCacheTracker.swift; sourceTree = "<group>"; };
58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationConfiguration.swift; sourceTree = "<group>"; };
58C3A4B122456F1A00340BDB /* AccountInputGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountInputGroupView.swift; sourceTree = "<group>"; };
58C3F4F82964B08300D72515 /* MapViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewController.swift; sourceTree = "<group>"; };
+ 58C3F4FA296C3AD500D72515 /* SettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsCoordinator.swift; sourceTree = "<group>"; };
+ 58C774BD29A7A249003A1A56 /* CustomNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomNavigationController.swift; sourceTree = "<group>"; };
+ 58CAF9F72983D36800BE19F7 /* Coordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Coordinator.swift; sourceTree = "<group>"; };
+ 58CAF9F92983E0C600BE19F7 /* LoginCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginCoordinator.swift; sourceTree = "<group>"; };
+ 58CAF9FF2983FF0200BE19F7 /* LoginInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginInteractor.swift; sourceTree = "<group>"; };
+ 58CAFA01298530DC00BE19F7 /* Promise.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Promise.swift; sourceTree = "<group>"; };
58CC40EE24A601900019D96E /* ObserverList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObserverList.swift; sourceTree = "<group>"; };
58CCA00F224249A1004F3011 /* TunnelViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelViewController.swift; sourceTree = "<group>"; };
58CCA01122424D11004F3011 /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
@@ -878,7 +908,6 @@
58E511E328DDDE8900B0BCDE /* CustomErrorDescriptionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomErrorDescriptionProtocol.swift; sourceTree = "<group>"; };
58E511E528DDDEAC00B0BCDE /* CodingErrors+CustomErrorDescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CodingErrors+CustomErrorDescription.swift"; sourceTree = "<group>"; };
58E511EA28DDE18400B0BCDE /* Error+Chain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Error+Chain.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>"; };
58EE2E38272FF814003BFF93 /* SettingsDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsDataSource.swift; sourceTree = "<group>"; };
@@ -886,6 +915,7 @@
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>"; };
58F1311427E0B2AB007AC5BC /* Result+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Result+Extensions.swift"; sourceTree = "<group>"; };
+ 58F185A9298A3E3E00075977 /* TunnelCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelCoordinator.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>"; };
@@ -893,7 +923,6 @@
58F2E14B276A61C000A79513 /* RotateKeyOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RotateKeyOperation.swift; sourceTree = "<group>"; };
58F3C0A3249CB069003E76BE /* HeaderBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderBarView.swift; sourceTree = "<group>"; };
58F3C0A524A50155003E76BE /* relays.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = relays.json; sourceTree = "<group>"; };
- 58F7CA872692E34000FC59FD /* WireguardKeysContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireguardKeysContentView.swift; sourceTree = "<group>"; };
58F7D26427EB50A300E4D821 /* ResultOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultOperation.swift; sourceTree = "<group>"; };
58F8AC0D25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProblemReportReviewViewController.swift; sourceTree = "<group>"; };
58FB865426E8BF3100F188BC /* StorePaymentManagerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorePaymentManagerError.swift; sourceTree = "<group>"; };
@@ -910,10 +939,9 @@
7AD8490C29BA1EC500878E53 /* SettingsCellFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsCellFactory.swift; sourceTree = "<group>"; };
7AD8490E29BA26B000878E53 /* CellFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellFactoryProtocol.swift; sourceTree = "<group>"; };
7AD8491029BA316500878E53 /* PreferencesCellFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesCellFactory.swift; sourceTree = "<group>"; };
- 7AF1E73929C47727002C6633 /* LocationCellFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationCellFactory.swift; sourceTree = "<group>"; };
E1187ABA289BBB850024E748 /* OutOfTimeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutOfTimeViewController.swift; sourceTree = "<group>"; };
E1187ABB289BBB850024E748 /* OutOfTimeContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutOfTimeContentView.swift; sourceTree = "<group>"; };
- E158B35F285381C60002F069 /* StringFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringFormatter.swift; sourceTree = "<group>"; };
+ E158B35F285381C60002F069 /* String+AccountFormatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+AccountFormatting.swift"; sourceTree = "<group>"; };
E1FD0DF428AA7CE400299DB4 /* StatusActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActivityView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -1123,6 +1151,7 @@
58FF2C02281BDE02009EF542 /* SettingsManager.swift */,
06410E03292D0F7100AFC18C /* SettingsParser.swift */,
06410E06292D108E00AFC18C /* SettingsStore.swift */,
+ 580F8B8528197958002E0998 /* DNSSettings.swift */,
587AD7C523421D7000E93A53 /* TunnelSettingsV1.swift */,
580F8B8228197881002E0998 /* TunnelSettingsV2.swift */,
58421033282E4B1500F24E46 /* TunnelSettingsV2+REST.swift */,
@@ -1153,6 +1182,7 @@
5898D2B62902A9EA00EB5EBA /* PacketTunnelRelay.swift */,
58900D0228BBDCC70094E4F0 /* FixedWidthInteger+Arithmetics.swift */,
06410E172934F43B00AFC18C /* PacketTunnelErrorWrapper.swift */,
+ 58CAFA01298530DC00BE19F7 /* Promise.swift */,
58D223D7294C8E5E0029F5F8 /* MullvadTypes.h */,
);
path = MullvadTypes;
@@ -1194,6 +1224,263 @@
path = MullvadVPN;
sourceTree = "<group>";
};
+ 583FE01329C102EB006E85F9 /* Navigation */ = {
+ isa = PBXGroup;
+ children = (
+ 582BB1B0229569620055B6EF /* UINavigationBar+Appearance.swift */,
+ 58C774BD29A7A249003A1A56 /* CustomNavigationController.swift */,
+ );
+ path = Navigation;
+ sourceTree = "<group>";
+ };
+ 583FE01629C196E8006E85F9 /* View controllers */ = {
+ isa = PBXGroup;
+ children = (
+ 583FE02529C1AD0E006E85F9 /* Launch */,
+ 583FE02229C1AC68006E85F9 /* TermsOfService */,
+ 583FE02129C1A0F4006E85F9 /* Login */,
+ 583FE02029C1A0B1006E85F9 /* Account */,
+ 583FE01E29C197D5006E85F9 /* Tunnel */,
+ 583FE01D29C197C1006E85F9 /* DeviceList */,
+ 583FE01C29C19793006E85F9 /* RevokedDevice */,
+ 583FE01B29C19786006E85F9 /* OutOfTime */,
+ 583FE01A29C19777006E85F9 /* Preferences */,
+ 583FE01929C19760006E85F9 /* ProblemReport */,
+ 583FE01829C19709006E85F9 /* Settings */,
+ 583FE01729C196F3006E85F9 /* SelectLocation */,
+ );
+ path = "View controllers";
+ sourceTree = "<group>";
+ };
+ 583FE01729C196F3006E85F9 /* SelectLocation */ = {
+ isa = PBXGroup;
+ children = (
+ 58435AC129CB2A350099C71B /* LocationCellFactory.swift */,
+ 583DA21325FA4B5C00318683 /* LocationDataSource.swift */,
+ 5888AD82227B11080051EB06 /* SelectLocationCell.swift */,
+ 5857F24224C8662600CF6F47 /* SelectLocationHeaderView.swift */,
+ 5888AD86227B17950051EB06 /* SelectLocationViewController.swift */,
+ );
+ path = SelectLocation;
+ sourceTree = "<group>";
+ };
+ 583FE01829C19709006E85F9 /* Settings */ = {
+ isa = PBXGroup;
+ children = (
+ 5864AF0029C7879B005B0CD9 /* SettingsCellFactory.swift */,
+ 58CCA01122424D11004F3011 /* SettingsViewController.swift */,
+ 5867770F290975E8006F721F /* SettingsInteractorFactory.swift */,
+ 58677711290976FB006F721F /* SettingsInteractor.swift */,
+ 582BB1AE229566420055B6EF /* SettingsCell.swift */,
+ 582BB1B2229574F40055B6EF /* SettingsAccountCell.swift */,
+ 584D26C1270C8542004EA533 /* SettingsStaticTextFooterView.swift */,
+ 58ACF64A26553C3F00ACE4B7 /* SettingsSwitchCell.swift */,
+ 5819C2162729595500D6EC38 /* SettingsAddDNSEntryCell.swift */,
+ 58EE2E38272FF814003BFF93 /* SettingsDataSource.swift */,
+ 58EE2E39272FF814003BFF93 /* SettingsDataSourceDelegate.swift */,
+ 584D26C5270C8741004EA533 /* SettingsDNSTextCell.swift */,
+ );
+ path = Settings;
+ sourceTree = "<group>";
+ };
+ 583FE01929C19760006E85F9 /* ProblemReport */ = {
+ isa = PBXGroup;
+ children = (
+ 58F8AC0D25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift */,
+ 58EF580A25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift */,
+ 58293FAC2510CA58005D0BB5 /* ProblemReportViewController.swift */,
+ 5878A26E2907E7E00096FC88 /* ProblemReportInteractor.swift */,
+ );
+ path = ProblemReport;
+ sourceTree = "<group>";
+ };
+ 583FE01A29C19777006E85F9 /* Preferences */ = {
+ isa = PBXGroup;
+ children = (
+ 5864AF0229C7879B005B0CD9 /* PreferencesCellFactory.swift */,
+ 584D26C3270C855A004EA533 /* PreferencesDataSource.swift */,
+ 587EB6732714520600123C75 /* PreferencesDataSourceDelegate.swift */,
+ 58ACF6482655365700ACE4B7 /* PreferencesViewController.swift */,
+ 5871167E2910035700D41AAC /* PreferencesInteractor.swift */,
+ 587EB671271451E300123C75 /* PreferencesViewModel.swift */,
+ );
+ path = Preferences;
+ sourceTree = "<group>";
+ };
+ 583FE01B29C19786006E85F9 /* OutOfTime */ = {
+ isa = PBXGroup;
+ children = (
+ E1187ABB289BBB850024E748 /* OutOfTimeContentView.swift */,
+ E1187ABA289BBB850024E748 /* OutOfTimeViewController.swift */,
+ 5867770D29096984006F721F /* OutOfTimeInteractor.swift */,
+ );
+ path = OutOfTime;
+ sourceTree = "<group>";
+ };
+ 583FE01C29C19793006E85F9 /* RevokedDevice */ = {
+ isa = PBXGroup;
+ children = (
+ 580909D22876D09A0078138D /* RevokedDeviceViewController.swift */,
+ 5878A27C2909657C0096FC88 /* RevokedDeviceInteractor.swift */,
+ );
+ path = RevokedDevice;
+ sourceTree = "<group>";
+ };
+ 583FE01D29C197C1006E85F9 /* DeviceList */ = {
+ isa = PBXGroup;
+ children = (
+ 587D96732886D87C00CD8F1C /* DeviceManagementContentView.swift */,
+ 5820EDA8288FE064006BF4E4 /* DeviceManagementInteractor.swift */,
+ 5893716928817A45004EE76C /* DeviceManagementViewController.swift */,
+ 5820EDAA288FF0D2006BF4E4 /* DeviceRowView.swift */,
+ );
+ path = DeviceList;
+ sourceTree = "<group>";
+ };
+ 583FE01E29C197D5006E85F9 /* Tunnel */ = {
+ isa = PBXGroup;
+ children = (
+ 58FEEB45260A028D00A621A8 /* GeoJSON.swift */,
+ 58B43C1825F77DB60002C8C3 /* TunnelControlView.swift */,
+ 58C3F4F82964B08300D72515 /* MapViewController.swift */,
+ 58A1AA8B23F5584B009F7EA6 /* ConnectionPanelView.swift */,
+ 58CCA00F224249A1004F3011 /* TunnelViewController.swift */,
+ 5878A27A2909649A0096FC88 /* CustomOverlayRenderer.swift */,
+ 5878A278290954790096FC88 /* TunnelViewControllerInteractor.swift */,
+ 58907D9424D17B4E00CFC3F5 /* DisconnectSplitButton.swift */,
+ 5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.swift */,
+ );
+ path = Tunnel;
+ sourceTree = "<group>";
+ };
+ 583FE01F29C197ED006E85F9 /* Views */ = {
+ isa = PBXGroup;
+ children = (
+ 5868585424054096000B8131 /* AppButton.swift */,
+ 58ACF64C26567A4F00ACE4B7 /* CustomSwitch.swift */,
+ 58ACF64E26567A7100ACE4B7 /* CustomSwitchContainer.swift */,
+ 58293FB025124117005D0BB5 /* CustomTextField.swift */,
+ 58293FB2251241B3005D0BB5 /* CustomTextView.swift */,
+ 5892A45D265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift */,
+ 58FD5BF32428C67600112C88 /* InAppPurchaseButton.swift */,
+ 58F19E34228C15BA00C7710B /* SpinnerActivityIndicatorView.swift */,
+ E1FD0DF428AA7CE400299DB4 /* StatusActivityView.swift */,
+ 58EF581025D69DB400AEBA94 /* StatusImageView.swift */,
+ );
+ path = Views;
+ sourceTree = "<group>";
+ };
+ 583FE02029C1A0B1006E85F9 /* Account */ = {
+ isa = PBXGroup;
+ children = (
+ 58CCA01722426713004F3011 /* AccountViewController.swift */,
+ 5896CEF126972DEB00B0FAE8 /* AccountContentView.swift */,
+ 5878A27029091CF20096FC88 /* AccountInteractor.swift */,
+ 5867771329097BCD006F721F /* PaymentState.swift */,
+ 5867771529097C5B006F721F /* ProductState.swift */,
+ );
+ path = Account;
+ sourceTree = "<group>";
+ };
+ 583FE02129C1A0F4006E85F9 /* Login */ = {
+ isa = PBXGroup;
+ children = (
+ 58B993B02608A34500BA7811 /* LoginContentView.swift */,
+ 58CE5E65224146200008646E /* LoginViewController.swift */,
+ 58CAF9FF2983FF0200BE19F7 /* LoginInteractor.swift */,
+ 582AE30F2440A6CA00E6733A /* AccountTokenInput.swift */,
+ 58C3A4B122456F1A00340BDB /* AccountInputGroupView.swift */,
+ 58CCA01D2242787B004F3011 /* AccountTextField.swift */,
+ );
+ path = Login;
+ sourceTree = "<group>";
+ };
+ 583FE02229C1AC68006E85F9 /* TermsOfService */ = {
+ isa = PBXGroup;
+ children = (
+ 584592602639B4A200EF967F /* TermsOfServiceContentView.swift */,
+ 58A99ED2240014A0006599E9 /* TermsOfServiceViewController.swift */,
+ );
+ path = TermsOfService;
+ sourceTree = "<group>";
+ };
+ 583FE02329C1AC9F006E85F9 /* Extensions */ = {
+ isa = PBXGroup;
+ children = (
+ 5891BF1B25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift */,
+ 587EB669270EFACB00123C75 /* CharacterSet+IPAddress.swift */,
+ 58E511E528DDDEAC00B0BCDE /* CodingErrors+CustomErrorDescription.swift */,
+ 5811DE4F239014550011EB53 /* NEVPNStatus+Debug.swift */,
+ 584EBDBC2747C98F00A0C9FD /* NSAttributedString+Markdown.swift */,
+ 587D9675288989DB00CD8F1C /* NSLayoutConstraint+Helpers.swift */,
+ 5871FB9F254C26BF0051A0A4 /* NSRegularExpression+IPAddress.swift */,
+ 06FAE67828F83CA50033DD93 /* RESTCreateApplePaymentResponse+Localization.swift */,
+ 58B9EB142489139B00095626 /* RESTError+Display.swift */,
+ 58F1311427E0B2AB007AC5BC /* Result+Extensions.swift */,
+ 58A8EE592976BFBB009C0F8D /* SKError+Localized.swift */,
+ 58FD5BEF24238EB300112C88 /* SKProduct+Formatting.swift */,
+ 58A8EE5D2976DB00009C0F8D /* StorePaymentManagerError+Display.swift */,
+ 5807E2BF2432038B00F5FF30 /* String+Split.swift */,
+ E158B35F285381C60002F069 /* String+AccountFormatting.swift */,
+ 5891BF5025E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift */,
+ 587CBFE222807F530028DED3 /* UIColor+Helpers.swift */,
+ );
+ path = Extensions;
+ sourceTree = "<group>";
+ };
+ 583FE02529C1AD0E006E85F9 /* Launch */ = {
+ isa = PBXGroup;
+ children = (
+ 58E20770274672CA00DE5D77 /* LaunchViewController.swift */,
+ );
+ path = Launch;
+ sourceTree = "<group>";
+ };
+ 583FE02629C1ADB6006E85F9 /* SimulatorTunnelProvider */ = {
+ isa = PBXGroup;
+ children = (
+ 58BA693023EADA6A009DC256 /* SimulatorTunnelProvider.swift */,
+ 587A01FB23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift */,
+ );
+ path = SimulatorTunnelProvider;
+ sourceTree = "<group>";
+ };
+ 583FE02729C1ADF7006E85F9 /* UI appearance */ = {
+ isa = PBXGroup;
+ children = (
+ 585CA70E25F8C44600B47C62 /* UIMetrics.swift */,
+ 58CCA0152242560B004F3011 /* UIColor+Palette.swift */,
+ );
+ path = "UI appearance";
+ sourceTree = "<group>";
+ };
+ 583FE02829C1B079006E85F9 /* Classes */ = {
+ isa = PBXGroup;
+ children = (
+ 58CC40EE24A601900019D96E /* ObserverList.swift */,
+ 58FEEB57260B662E00A621A8 /* AutomaticKeyboardResponder.swift */,
+ 5871FB95254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift */,
+ 5896AE83246D5889005B36CB /* CustomDateComponentsFormatting.swift */,
+ 587EB66F27143B6500123C75 /* DataSourceSnapshot.swift */,
+ 5872D6E7286304DE00DB5F4E /* TermsOfService.swift */,
+ 587988C628A2A01F00E3DF54 /* AccountDataThrottling.swift */,
+ 58138E60294871C600684F0C /* DeviceDataThrottling.swift */,
+ 589A454B28DDF5E100565204 /* Swizzle.swift */,
+ );
+ path = Classes;
+ sourceTree = "<group>";
+ };
+ 583FE02929C1B0E1006E85F9 /* Supporting Files */ = {
+ isa = PBXGroup;
+ children = (
+ 58CE5E6A224146210008646E /* Assets.xcassets */,
+ 58CE5E6F224146210008646E /* Info.plist */,
+ 58727282265D173C00F315B2 /* LaunchScreen.storyboard */,
+ 5866F39B2243B82D00168AE5 /* MullvadVPN.entitlements */,
+ );
+ path = "Supporting Files";
+ sourceTree = "<group>";
+ };
5846226F26E229CD0035F7C2 /* StorePaymentManager */ = {
isa = PBXGroup;
children = (
@@ -1209,6 +1496,16 @@
path = StorePaymentManager;
sourceTree = "<group>";
};
+ 5847D58729B75833008C3808 /* Base */ = {
+ isa = PBXGroup;
+ children = (
+ 58CAF9F72983D36800BE19F7 /* Coordinator.swift */,
+ 583FE01129C0F99A006E85F9 /* PresentationControllerDismissalInterceptor.swift */,
+ 583FE00B29C0C7FD006E85F9 /* ModalPresentationConfiguration.swift */,
+ );
+ path = Base;
+ sourceTree = "<group>";
+ };
584F991F2902CBDD001F858D /* Frameworks */ = {
isa = PBXGroup;
children = (
@@ -1225,6 +1522,34 @@
path = RelayCacheTracker;
sourceTree = "<group>";
};
+ 5864859729A0D012006C5743 /* Presentation controllers */ = {
+ isa = PBXGroup;
+ children = (
+ 5864859829A0D028006C5743 /* FormsheetPresentationController.swift */,
+ 5864859A29A0EAF2006C5743 /* SecondaryContextPresentationController.swift */,
+ );
+ path = "Presentation controllers";
+ sourceTree = "<group>";
+ };
+ 5864AEFF29C78760005B0CD9 /* Recovered References */ = {
+ isa = PBXGroup;
+ children = (
+ 7AD8490C29BA1EC500878E53 /* SettingsCellFactory.swift */,
+ 7AD8491029BA316500878E53 /* PreferencesCellFactory.swift */,
+ 7AD8490E29BA26B000878E53 /* CellFactoryProtocol.swift */,
+ );
+ name = "Recovered References";
+ sourceTree = "<group>";
+ };
+ 5864AF0629C78816005B0CD9 /* Protocols */ = {
+ isa = PBXGroup;
+ children = (
+ 58E11187292FA11F009FCA84 /* SettingsMigrationUIHandler.swift */,
+ 5864AF0129C7879B005B0CD9 /* CellFactoryProtocol.swift */,
+ );
+ path = Protocols;
+ sourceTree = "<group>";
+ };
586A950B2901250A007BAF2B /* Operations */ = {
isa = PBXGroup;
children = (
@@ -1245,6 +1570,22 @@
path = "Notification Providers";
sourceTree = "<group>";
};
+ 589453E0297807DB0015DA3B /* App */ = {
+ isa = PBXGroup;
+ children = (
+ 5893C6FB29C311E9009090D1 /* ApplicationRouter.swift */,
+ 58BBB39629717E0C00C8DB7C /* ApplicationCoordinator.swift */,
+ 58CAF9F92983E0C600BE19F7 /* LoginCoordinator.swift */,
+ 587C92FD2986E28100FB9664 /* SelectLocationCoordinator.swift */,
+ 587C92FF2986E2B600FB9664 /* TermsOfServiceCoordinator.swift */,
+ 58F185A9298A3E3E00075977 /* TunnelCoordinator.swift */,
+ 58C3F4FA296C3AD500D72515 /* SettingsCoordinator.swift */,
+ 5847D58C29B7740F008C3808 /* RevokedCoordinator.swift */,
+ 583FE00D29C0D586006E85F9 /* OutOfTimeCoordinator.swift */,
+ );
+ path = App;
+ sourceTree = "<group>";
+ };
5898D28A29017BD400EB5EBA /* TunnelProviderMessaging */ = {
isa = PBXGroup;
children = (
@@ -1334,6 +1675,25 @@
path = UI;
sourceTree = "<group>";
};
+ 58C774C929AB543C003A1A56 /* Containers */ = {
+ isa = PBXGroup;
+ children = (
+ 58D1560C29C0B27600749324 /* Root */,
+ 583FE01329C102EB006E85F9 /* Navigation */,
+ 583FE00F29C0F532006E85F9 /* CustomSplitViewController.swift */,
+ );
+ path = Containers;
+ sourceTree = "<group>";
+ };
+ 58CAF9F22983D32200BE19F7 /* Coordinators */ = {
+ isa = PBXGroup;
+ children = (
+ 5847D58729B75833008C3808 /* Base */,
+ 589453E0297807DB0015DA3B /* App */,
+ );
+ path = Coordinators;
+ sourceTree = "<group>";
+ };
58CE5E57224146200008646E = {
isa = PBXGroup;
children = (
@@ -1356,6 +1716,7 @@
58CE5E7A224146470008646E /* PacketTunnel */,
58CE5E61224146200008646E /* Products */,
584F991F2902CBDD001F858D /* Frameworks */,
+ 5864AEFF29C78760005B0CD9 /* Recovered References */,
);
sourceTree = "<group>";
};
@@ -1382,132 +1743,27 @@
58CE5E62224146200008646E /* MullvadVPN */ = {
isa = PBXGroup;
children = (
- 5896CEF126972DEB00B0FAE8 /* AccountContentView.swift */,
- 587988C628A2A01F00E3DF54 /* AccountDataThrottling.swift */,
- 58C3A4B122456F1A00340BDB /* AccountInputGroupView.swift */,
- 5878A27029091CF20096FC88 /* AccountInteractor.swift */,
- 58CCA01D2242787B004F3011 /* AccountTextField.swift */,
- 582AE30F2440A6CA00E6733A /* AccountTokenInput.swift */,
- 58CCA01722426713004F3011 /* AccountViewController.swift */,
- 06AC114028F841390037AF9A /* AddressCacheTracker.swift */,
- 5868585424054096000B8131 /* AppButton.swift */,
58CE5E63224146200008646E /* AppDelegate.swift */,
- 58CE5E6A224146210008646E /* Assets.xcassets */,
- 58FEEB57260B662E00A621A8 /* AutomaticKeyboardResponder.swift */,
- 5891BF1B25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift */,
- 7AD8490E29BA26B000878E53 /* CellFactoryProtocol.swift */,
- 587EB669270EFACB00123C75 /* CharacterSet+IPAddress.swift */,
- 58E511E528DDDEAC00B0BCDE /* CodingErrors+CustomErrorDescription.swift */,
- 58A1AA8B23F5584B009F7EA6 /* ConnectionPanelView.swift */,
- 5871FB95254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift */,
- 5896AE83246D5889005B36CB /* CustomDateComponentsFormatting.swift */,
- 582BB1B0229569620055B6EF /* CustomNavigationBar.swift */,
- 5878A27A2909649A0096FC88 /* CustomOverlayRenderer.swift */,
- 5868BD32261DCD2600E6027F /* CustomSplitViewController.swift */,
- 58ACF64C26567A4F00ACE4B7 /* CustomSwitch.swift */,
- 58ACF64E26567A7100ACE4B7 /* CustomSwitchContainer.swift */,
- 58293FB025124117005D0BB5 /* CustomTextField.swift */,
- 58293FB2251241B3005D0BB5 /* CustomTextView.swift */,
- 587EB66F27143B6500123C75 /* DataSourceSnapshot.swift */,
- 58138E60294871C600684F0C /* DeviceDataThrottling.swift */,
- 587D96732886D87C00CD8F1C /* DeviceManagementContentView.swift */,
- 5820EDA8288FE064006BF4E4 /* DeviceManagementInteractor.swift */,
- 5893716928817A45004EE76C /* DeviceManagementViewController.swift */,
- 5820EDAA288FF0D2006BF4E4 /* DeviceRowView.swift */,
- 58907D9424D17B4E00CFC3F5 /* DisconnectSplitButton.swift */,
- 580F8B8528197958002E0998 /* DNSSettings.swift */,
- 5892A45D265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift */,
- 58FEEB45260A028D00A621A8 /* GeoJSON.swift */,
- 58B3F30E2742708B00A2DD38 /* HeaderBarButton.swift */,
- 58F3C0A3249CB069003E76BE /* HeaderBarView.swift */,
- 58FD5BF32428C67600112C88 /* InAppPurchaseButton.swift */,
- 58CE5E6F224146210008646E /* Info.plist */,
- 58727282265D173C00F315B2 /* LaunchScreen.storyboard */,
- 58E20770274672CA00DE5D77 /* LaunchViewController.swift */,
- 583DA21325FA4B5C00318683 /* LocationDataSource.swift */,
- 7AF1E73929C47727002C6633 /* LocationCellFactory.swift */,
- 58B993B02608A34500BA7811 /* LoginContentView.swift */,
- 58CE5E65224146200008646E /* LoginViewController.swift */,
- 58C3F4F82964B08300D72515 /* MapViewController.swift */,
- 58ACA9EC2979569500B5825C /* ModalRootAdaptivePresentationDelegate.swift */,
- 5866F39B2243B82D00168AE5 /* MullvadVPN.entitlements */,
- 58906DDF2445C7A5002F0673 /* NEProviderStopReason+Debug.swift */,
- 5811DE4F239014550011EB53 /* NEVPNStatus+Debug.swift */,
+ 58E25F802837BBBB002CFB2C /* SceneDelegate.swift */,
+ 583FE02829C1B079006E85F9 /* Classes */,
+ 5864AF0629C78816005B0CD9 /* Protocols */,
58B26E1F2943516500D5980C /* Notifications */,
- 584EBDBC2747C98F00A0C9FD /* NSAttributedString+Markdown.swift */,
- 587D9675288989DB00CD8F1C /* NSLayoutConstraint+Helpers.swift */,
- 5871FB9F254C26BF0051A0A4 /* NSRegularExpression+IPAddress.swift */,
- 58CC40EE24A601900019D96E /* ObserverList.swift */,
+ 583FE02729C1ADF7006E85F9 /* UI appearance */,
+ 583FE02329C1AC9F006E85F9 /* Extensions */,
+ 583FE01F29C197ED006E85F9 /* Views */,
+ 58C774C929AB543C003A1A56 /* Containers */,
+ 583FE01629C196E8006E85F9 /* View controllers */,
+ 58CAF9F22983D32200BE19F7 /* Coordinators */,
+ 5864859729A0D012006C5743 /* Presentation controllers */,
586A950B2901250A007BAF2B /* Operations */,
- E1187ABB289BBB850024E748 /* OutOfTimeContentView.swift */,
- 5867770D29096984006F721F /* OutOfTimeInteractor.swift */,
- E1187ABA289BBB850024E748 /* OutOfTimeViewController.swift */,
- 5867771329097BCD006F721F /* PaymentState.swift */,
- 584D26C3270C855A004EA533 /* PreferencesDataSource.swift */,
- 587EB6732714520600123C75 /* PreferencesDataSourceDelegate.swift */,
- 5871167E2910035700D41AAC /* PreferencesInteractor.swift */,
- 58ACF6482655365700ACE4B7 /* PreferencesViewController.swift */,
- 587EB671271451E300123C75 /* PreferencesViewModel.swift */,
- 5878A26E2907E7E00096FC88 /* ProblemReportInteractor.swift */,
- 58F8AC0D25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift */,
- 7AD8491029BA316500878E53 /* PreferencesCellFactory.swift */,
- 58EF580A25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift */,
- 58293FAC2510CA58005D0BB5 /* ProblemReportViewController.swift */,
- 5867771529097C5B006F721F /* ProductState.swift */,
- 585DA87526B0249A00B8C587 /* RelayCacheTracker */,
- 06FAE67828F83CA50033DD93 /* RESTCreateApplePaymentResponse+Localization.swift */,
- 58B9EB142489139B00095626 /* RESTError+Display.swift */,
- 58F1311427E0B2AB007AC5BC /* Result+Extensions.swift */,
- 5878A27C2909657C0096FC88 /* RevokedDeviceInteractor.swift */,
- 580909D22876D09A0078138D /* RevokedDeviceViewController.swift */,
- 587425C02299833500CA2045 /* RootContainerViewController.swift */,
- 58E25F802837BBBB002CFB2C /* SceneDelegate.swift */,
- 5888AD82227B11080051EB06 /* SelectLocationCell.swift */,
- 5857F24224C8662600CF6F47 /* SelectLocationHeaderView.swift */,
- 5857F24624C882D700CF6F47 /* SelectLocationNavigationController.swift */,
- 5888AD86227B17950051EB06 /* SelectLocationViewController.swift */,
- 582BB1B2229574F40055B6EF /* SettingsAccountCell.swift */,
- 5819C2162729595500D6EC38 /* SettingsAddDNSEntryCell.swift */,
- 582BB1AE229566420055B6EF /* SettingsCell.swift */,
- 7AD8490C29BA1EC500878E53 /* SettingsCellFactory.swift */,
- 58EE2E38272FF814003BFF93 /* SettingsDataSource.swift */,
- 58EE2E39272FF814003BFF93 /* SettingsDataSourceDelegate.swift */,
- 584D26C5270C8741004EA533 /* SettingsDNSTextCell.swift */,
- 58677711290976FB006F721F /* SettingsInteractor.swift */,
- 5867770F290975E8006F721F /* SettingsInteractorFactory.swift */,
- 580F8B88281A79A7002E0998 /* SettingsManager */,
- 58E11187292FA11F009FCA84 /* SettingsMigrationUIHandler.swift */,
- 58E6771E24ADFE7800AA26E7 /* SettingsNavigationController.swift */,
- 584D26C1270C8542004EA533 /* SettingsStaticTextFooterView.swift */,
- 58ACF64A26553C3F00ACE4B7 /* SettingsSwitchCell.swift */,
- 58CCA01122424D11004F3011 /* SettingsViewController.swift */,
- 58BA693023EADA6A009DC256 /* SimulatorTunnelProvider.swift */,
- 587A01FB23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift */,
- 58A8EE592976BFBB009C0F8D /* SKError+Localized.swift */,
- 58FD5BEF24238EB300112C88 /* SKProduct+Formatting.swift */,
- 58F19E34228C15BA00C7710B /* SpinnerActivityIndicatorView.swift */,
- E1FD0DF428AA7CE400299DB4 /* StatusActivityView.swift */,
- 58EF581025D69DB400AEBA94 /* StatusImageView.swift */,
+ 583FE02629C1ADB6006E85F9 /* SimulatorTunnelProvider */,
5846226F26E229CD0035F7C2 /* StorePaymentManager */,
- 58A8EE5D2976DB00009C0F8D /* StorePaymentManagerError+Display.swift */,
- 5807E2BF2432038B00F5FF30 /* String+Split.swift */,
- E158B35F285381C60002F069 /* StringFormatter.swift */,
- 589A454B28DDF5E100565204 /* Swizzle.swift */,
- 5872D6E7286304DE00DB5F4E /* TermsOfService.swift */,
- 584592602639B4A200EF967F /* TermsOfServiceContentView.swift */,
- 58A99ED2240014A0006599E9 /* TermsOfServiceViewController.swift */,
- 5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.swift */,
+ 58D7E3D629C78A130044B058 /* AddressCacheTracker */,
+ 585DA87526B0249A00B8C587 /* RelayCacheTracker */,
589E63DA28F7E9E7005FAB05 /* TransportMonitor */,
- 58B43C1825F77DB60002C8C3 /* TunnelControlView.swift */,
+ 580F8B88281A79A7002E0998 /* SettingsManager */,
5823FA5726CE4A4100283BF8 /* TunnelManager */,
- 58CCA00F224249A1004F3011 /* TunnelViewController.swift */,
- 5878A278290954790096FC88 /* TunnelViewControllerInteractor.swift */,
- 5891BF5025E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift */,
- 587CBFE222807F530028DED3 /* UIColor+Helpers.swift */,
- 58CCA0152242560B004F3011 /* UIColor+Palette.swift */,
- 585CA70E25F8C44600B47C62 /* UIMetrics.swift */,
- 584555F32991176200DD0657 /* UIPresentationController+Private.swift */,
- 58F7CA872692E34000FC59FD /* WireguardKeysContentView.swift */,
+ 583FE02929C1B0E1006E85F9 /* Supporting Files */,
);
path = MullvadVPN;
sourceTree = "<group>";
@@ -1524,6 +1780,7 @@
58E072A228814B96008902F8 /* TunnelMonitor */,
58E07298288031D5008902F8 /* WireGuardAdapterError+Localization.swift */,
58E0729E28814ACC008902F8 /* WireGuardLogLevel+Logging.swift */,
+ 58906DDF2445C7A5002F0673 /* NEProviderStopReason+Debug.swift */,
);
path = PacketTunnel;
sourceTree = "<group>";
@@ -1538,6 +1795,16 @@
path = MullvadVPNScreenshots;
sourceTree = "<group>";
};
+ 58D1560C29C0B27600749324 /* Root */ = {
+ isa = PBXGroup;
+ children = (
+ 587425C02299833500CA2045 /* RootContainerViewController.swift */,
+ 58B3F30E2742708B00A2DD38 /* HeaderBarButton.swift */,
+ 58F3C0A3249CB069003E76BE /* HeaderBarView.swift */,
+ );
+ path = Root;
+ sourceTree = "<group>";
+ };
58D223A6294C8A490029F5F8 /* Operations */ = {
isa = PBXGroup;
children = (
@@ -1580,6 +1847,14 @@
path = MullvadLogging;
sourceTree = "<group>";
};
+ 58D7E3D629C78A130044B058 /* AddressCacheTracker */ = {
+ isa = PBXGroup;
+ children = (
+ 06AC114028F841390037AF9A /* AddressCacheTracker.swift */,
+ );
+ path = AddressCacheTracker;
+ sourceTree = "<group>";
+ };
58E072A228814B96008902F8 /* TunnelMonitor */ = {
isa = PBXGroup;
children = (
@@ -2275,16 +2550,16 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 58B8644529C7971B005E107C /* AccountTokenInput.swift in Sources */,
582AE3122440CA0D00E6733A /* AccountTokenInputTests.swift in Sources */,
+ 58B8644729C79737005E107C /* DataSourceSnapshot.swift in Sources */,
582A8A3A28BCE19B00D0F9FB /* FixedWidthIntegerArithmeticsTests.swift in Sources */,
5896AE86246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift in Sources */,
5819C2142726CC8D00D6EC38 /* DataSourceSnapshotTests.swift in Sources */,
+ 58B8644629C7972F005E107C /* CustomDateComponentsFormatting.swift in Sources */,
5807E2C2243203D000F5FF30 /* StringTests.swift in Sources */,
- 582AE3132440CA2700E6733A /* AccountTokenInput.swift in Sources */,
5807E2C3243203E700F5FF30 /* String+Split.swift in Sources */,
58B0A2A8238EE68200BC001D /* RelaySelectorTests.swift in Sources */,
- 5819C2152726CC9400D6EC38 /* DataSourceSnapshot.swift in Sources */,
- 5896AE88246D7FAF005B36CB /* CustomDateComponentsFormatting.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -2293,16 +2568,17 @@
buildActionMask = 2147483647;
files = (
58BFA5CC22A7CE1F00A6173D /* ApplicationConfiguration.swift in Sources */,
- 7AD8490D29BA1EC500878E53 /* SettingsCellFactory.swift in Sources */,
5891BF5125E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift in Sources */,
58E511E628DDDEAC00B0BCDE /* CodingErrors+CustomErrorDescription.swift in Sources */,
+ 5864859B29A0EAF2006C5743 /* SecondaryContextPresentationController.swift in Sources */,
+ 5864AF0729C78843005B0CD9 /* SettingsCellFactory.swift in Sources */,
587B75412668FD7800DEF7E9 /* AccountExpirySystemNotificationProvider.swift in Sources */,
587988C728A2A01F00E3DF54 /* AccountDataThrottling.swift in Sources */,
5896CEF226972DEB00B0FAE8 /* AccountContentView.swift in Sources */,
5867771429097BCD006F721F /* PaymentState.swift in Sources */,
587D96742886D87C00CD8F1C /* DeviceManagementContentView.swift in Sources */,
589A454C28DDF5E100565204 /* Swizzle.swift in Sources */,
- 5857F24724C882D700CF6F47 /* SelectLocationNavigationController.swift in Sources */,
+ 58C3F4FB296C3AD500D72515 /* SettingsCoordinator.swift in Sources */,
5846227126E229F20035F7C2 /* StoreSubscription.swift in Sources */,
58421030282D8A3C00F24E46 /* UpdateAccountDataOperation.swift in Sources */,
58FF2C03281BDE02009EF542 /* SettingsManager.swift in Sources */,
@@ -2317,28 +2593,33 @@
58B993B12608A34500BA7811 /* LoginContentView.swift in Sources */,
5878A27529093A310096FC88 /* StorePaymentEvent.swift in Sources */,
58B26E2A2943545A00D5980C /* NotificationManagerDelegate.swift in Sources */,
- 58E6771F24ADFE7800AA26E7 /* SettingsNavigationController.swift in Sources */,
58A1AA8C23F5584C009F7EA6 /* ConnectionPanelView.swift in Sources */,
5878A27B2909649A0096FC88 /* CustomOverlayRenderer.swift in Sources */,
+ 5847D58D29B7740F008C3808 /* RevokedCoordinator.swift in Sources */,
582BB1B3229574F40055B6EF /* SettingsAccountCell.swift in Sources */,
588527B2276B3F0700BAA373 /* LoadTunnelConfigurationOperation.swift in Sources */,
58F1311527E0B2AB007AC5BC /* Result+Extensions.swift in Sources */,
5867770E29096984006F721F /* OutOfTimeInteractor.swift in Sources */,
+ 58F185AA298A3E3E00075977 /* TunnelCoordinator.swift in Sources */,
58F8AC0E25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift in Sources */,
5878A27129091CF20096FC88 /* AccountInteractor.swift in Sources */,
068CE5742927B7A400A068BB /* Migration.swift in Sources */,
58CCA010224249A1004F3011 /* TunnelViewController.swift in Sources */,
58B26E22294351EA00D5980C /* InAppNotificationProvider.swift in Sources */,
5893716A28817A45004EE76C /* DeviceManagementViewController.swift in Sources */,
+ 58435AC229CB2A350099C71B /* LocationCellFactory.swift in Sources */,
58BFA5C622A7C97F00A6173D /* RelayCacheTracker.swift in Sources */,
- E158B360285381C60002F069 /* StringFormatter.swift in Sources */,
- 582BB1B1229569620055B6EF /* CustomNavigationBar.swift in Sources */,
+ E158B360285381C60002F069 /* String+AccountFormatting.swift in Sources */,
+ 582BB1B1229569620055B6EF /* UINavigationBar+Appearance.swift in Sources */,
58B3F30F2742708B00A2DD38 /* HeaderBarButton.swift in Sources */,
58ACF6492655365700ACE4B7 /* PreferencesViewController.swift in Sources */,
+ 58C774BE29A7A249003A1A56 /* CustomNavigationController.swift in Sources */,
E1FD0DF528AA7CE400299DB4 /* StatusActivityView.swift in Sources */,
0697D6E728F01513007A9E99 /* TransportMonitor.swift in Sources */,
58968FAE28743E2000B799DC /* TunnelInteractor.swift in Sources */,
584D26C2270C8542004EA533 /* SettingsStaticTextFooterView.swift in Sources */,
+ 587C93002986E2B600FB9664 /* TermsOfServiceCoordinator.swift in Sources */,
+ 5864AF0929C78850005B0CD9 /* PreferencesCellFactory.swift in Sources */,
587B7536266528A200DEF7E9 /* NotificationManager.swift in Sources */,
5820EDA9288FE064006BF4E4 /* DeviceManagementInteractor.swift in Sources */,
58FB865A26EA214400F188BC /* RelayCacheTrackerObserver.swift in Sources */,
@@ -2350,6 +2631,7 @@
5846227326E22A160035F7C2 /* StorePaymentObserver.swift in Sources */,
58F2E146276A2C9900A79513 /* StopTunnelOperation.swift in Sources */,
E1187ABC289BBB850024E748 /* OutOfTimeViewController.swift in Sources */,
+ 58CAF9FA2983E0C600BE19F7 /* LoginCoordinator.swift in Sources */,
58293FB125124117005D0BB5 /* CustomTextField.swift in Sources */,
58138E61294871C600684F0C /* DeviceDataThrottling.swift in Sources */,
5878A279290954790096FC88 /* TunnelViewControllerInteractor.swift in Sources */,
@@ -2361,10 +2643,12 @@
58EE2E3B272FF814003BFF93 /* SettingsDataSourceDelegate.swift in Sources */,
5823FA5426CE49F700283BF8 /* TunnelObserver.swift in Sources */,
5888AD87227B17950051EB06 /* SelectLocationViewController.swift in Sources */,
- 7AD8491129BA316500878E53 /* PreferencesCellFactory.swift in Sources */,
+ 583FE00C29C0C7FD006E85F9 /* ModalPresentationConfiguration.swift in Sources */,
58293FB3251241B4005D0BB5 /* CustomTextView.swift in Sources */,
586A950E290125F3007BAF2B /* ProductsRequestOperation.swift in Sources */,
+ 5893C6F929C1B480009090D1 /* DNSSettings.swift in Sources */,
58F19E35228C15BA00C7710B /* SpinnerActivityIndicatorView.swift in Sources */,
+ 5864859929A0D028006C5743 /* FormsheetPresentationController.swift in Sources */,
58A99ED3240014A0006599E9 /* TermsOfServiceViewController.swift in Sources */,
58CCA0162242560B004F3011 /* UIColor+Palette.swift in Sources */,
587CBFE322807F530028DED3 /* UIColor+Helpers.swift in Sources */,
@@ -2374,9 +2658,9 @@
58CC40EF24A601900019D96E /* ObserverList.swift in Sources */,
58CCA01822426713004F3011 /* AccountViewController.swift in Sources */,
5871FBA0254C26C00051A0A4 /* NSRegularExpression+IPAddress.swift in Sources */,
- 58F7CA882692E34000FC59FD /* WireguardKeysContentView.swift in Sources */,
5878A27729093A4F0096FC88 /* StorePaymentBlockObserver.swift in Sources */,
5868585524054096000B8131 /* AppButton.swift in Sources */,
+ 5893C6FC29C311E9009090D1 /* ApplicationRouter.swift in Sources */,
58E25F812837BBBB002CFB2C /* SceneDelegate.swift in Sources */,
5867771629097C5B006F721F /* ProductState.swift in Sources */,
585E820327F3285E00939F0E /* SendStoreReceiptOperation.swift in Sources */,
@@ -2387,11 +2671,11 @@
58DF28A52417CB4B00E836B0 /* StorePaymentManager.swift in Sources */,
583DA21425FA4B5C00318683 /* LocationDataSource.swift in Sources */,
587EB6742714520600123C75 /* PreferencesDataSourceDelegate.swift in Sources */,
- 584555F42991176200DD0657 /* UIPresentationController+Private.swift in Sources */,
582BB1AF229566420055B6EF /* SettingsCell.swift in Sources */,
- 7AF1E73A29C47727002C6633 /* LocationCellFactory.swift in Sources */,
58F3C0A4249CB069003E76BE /* HeaderBarView.swift in Sources */,
+ 5864AF0829C78849005B0CD9 /* CellFactoryProtocol.swift in Sources */,
587A01FC23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift in Sources */,
+ 58CAF9F82983D36800BE19F7 /* Coordinator.swift in Sources */,
5819C2172729595500D6EC38 /* SettingsAddDNSEntryCell.swift in Sources */,
5862805422428EF100F5A6E1 /* TranslucentButtonBlurView.swift in Sources */,
587EB66A270EFACB00123C75 /* CharacterSet+IPAddress.swift in Sources */,
@@ -2399,10 +2683,13 @@
5891BF1C25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift in Sources */,
5878A26F2907E7E00096FC88 /* ProblemReportInteractor.swift in Sources */,
58E11188292FA11F009FCA84 /* SettingsMigrationUIHandler.swift in Sources */,
+ 58CAFA002983FF0200BE19F7 /* LoginInteractor.swift in Sources */,
5807E2C02432038B00F5FF30 /* String+Split.swift in Sources */,
58B26E242943520C00D5980C /* NotificationProviderProtocol.swift in Sources */,
+ 583FE01229C0F99A006E85F9 /* PresentationControllerDismissalInterceptor.swift in Sources */,
58677712290976FB006F721F /* SettingsInteractor.swift in Sources */,
58CE5E66224146200008646E /* LoginViewController.swift in Sources */,
+ 583FE01029C0F532006E85F9 /* CustomSplitViewController.swift in Sources */,
58EF580B25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift in Sources */,
5892A45E265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift in Sources */,
580909D32876D09A0078138D /* RevokedDeviceViewController.swift in Sources */,
@@ -2419,7 +2706,6 @@
58E0A98827C8F46300FE6BDD /* Tunnel.swift in Sources */,
58ACF64F26567A7100ACE4B7 /* CustomSwitchContainer.swift in Sources */,
5857F24324C8662600CF6F47 /* SelectLocationHeaderView.swift in Sources */,
- 7AD8490F29BA26B000878E53 /* CellFactoryProtocol.swift in Sources */,
58EE2E3A272FF814003BFF93 /* SettingsDataSource.swift in Sources */,
58B26E1E2943514300D5980C /* InAppNotificationDescriptor.swift in Sources */,
58421032282E42B000F24E46 /* UpdateDeviceDataOperation.swift in Sources */,
@@ -2429,7 +2715,6 @@
58B26E282943527300D5980C /* SystemNotificationProvider.swift in Sources */,
58CCA01222424D11004F3011 /* SettingsViewController.swift in Sources */,
06410DFE292CE18F00AFC18C /* KeychainSettingsStore.swift in Sources */,
- 580F8B8628197958002E0998 /* DNSSettings.swift in Sources */,
58FB865526E8BF3100F188BC /* StorePaymentManagerError.swift in Sources */,
58FD5BF42428C67600112C88 /* InAppPurchaseButton.swift in Sources */,
587D9676288989DB00CD8F1C /* NSLayoutConstraint+Helpers.swift in Sources */,
@@ -2437,9 +2722,7 @@
58B9EB152489139B00095626 /* RESTError+Display.swift in Sources */,
587B753F2668E5A700DEF7E9 /* NotificationContainerView.swift in Sources */,
58421034282E4B1500F24E46 /* TunnelSettingsV2+REST.swift in Sources */,
- 58ACA9ED2979569500B5825C /* ModalRootAdaptivePresentationDelegate.swift in Sources */,
58F2E144276A13F300A79513 /* StartTunnelOperation.swift in Sources */,
- 5868BD33261DCD2600E6027F /* CustomSplitViewController.swift in Sources */,
58CCA01E2242787B004F3011 /* AccountTextField.swift in Sources */,
586E54FB27A2DF6D0029B88B /* SendTunnelProviderMessageOperation.swift in Sources */,
584592612639B4A200EF967F /* TermsOfServiceContentView.swift in Sources */,
@@ -2458,12 +2741,15 @@
58C3A4B222456F1B00340BDB /* AccountInputGroupView.swift in Sources */,
58ACF64B26553C3F00ACE4B7 /* SettingsSwitchCell.swift in Sources */,
587EB67027143B6500123C75 /* DataSourceSnapshot.swift in Sources */,
+ 583FE00E29C0D586006E85F9 /* OutOfTimeCoordinator.swift in Sources */,
58A8EE5E2976DB00009C0F8D /* StorePaymentManagerError+Display.swift in Sources */,
580F8B8328197881002E0998 /* TunnelSettingsV2.swift in Sources */,
58A8EE5A2976BFBB009C0F8D /* SKError+Localized.swift in Sources */,
+ 58BBB39729717E0C00C8DB7C /* ApplicationCoordinator.swift in Sources */,
5803B4B22940A48700C23744 /* TunnelStore.swift in Sources */,
586A950F29012BEE007BAF2B /* AddressCacheTracker.swift in Sources */,
587B753D2666468F00DEF7E9 /* NotificationController.swift in Sources */,
+ 587C92FE2986E28100FB9664 /* SelectLocationCoordinator.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -2474,6 +2760,7 @@
5806767C27048E9B00C858CB /* PacketTunnelProvider.swift in Sources */,
587AD7C723421D8600E93A53 /* TunnelSettingsV1.swift in Sources */,
06410E09292D990C00AFC18C /* Result+Extensions.swift in Sources */,
+ 5893C6FA29C1B481009090D1 /* DNSSettings.swift in Sources */,
58CE38C828992C9200A6D6E5 /* TunnelMonitorDelegate.swift in Sources */,
068CE5782927BE4800A068BB /* Migration.swift in Sources */,
58FC040A27B3EE03001C21F0 /* TunnelMonitor.swift in Sources */,
@@ -2486,10 +2773,10 @@
58906DE02445C7A5002F0673 /* NEProviderStopReason+Debug.swift in Sources */,
068CE57229278F6D00A068BB /* MigrationFromV1ToV2.swift in Sources */,
06410DFF292CF16C00AFC18C /* KeychainSettingsStore.swift in Sources */,
- 580F8B872819795C002E0998 /* DNSSettings.swift in Sources */,
58E072A128814B0E008902F8 /* MullvadEndpoint+WgEndpoint.swift in Sources */,
06AC116228F94C450037AF9A /* ApplicationConfiguration.swift in Sources */,
58A3BDB028A1821A00C8C2C6 /* WgStats.swift in Sources */,
+ 583FE02429C1ACB3006E85F9 /* RESTCreateApplePaymentResponse+Localization.swift in Sources */,
5877D70F282137E8002FCFC7 /* SettingsManager.swift in Sources */,
58CE38C728992C8700A6D6E5 /* WireGuardAdapterError+Localization.swift in Sources */,
58E511E828DDDF2400B0BCDE /* CodingErrors+CustomErrorDescription.swift in Sources */,
@@ -2536,6 +2823,7 @@
files = (
58D22406294C90210029F5F8 /* IPv4Endpoint.swift in Sources */,
58D22407294C90210029F5F8 /* IPv6Endpoint.swift in Sources */,
+ 58CAFA032985367600BE19F7 /* Promise.swift in Sources */,
58D22408294C90210029F5F8 /* AnyIPEndpoint.swift in Sources */,
58D22409294C90210029F5F8 /* AnyIPAddress.swift in Sources */,
58D2240A294C90210029F5F8 /* IPAddress+Codable.swift in Sources */,
@@ -3122,9 +3410,9 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
- CODE_SIGN_ENTITLEMENTS = MullvadVPN/MullvadVPN.entitlements;
+ CODE_SIGN_ENTITLEMENTS = "MullvadVPN/Supporting Files/MullvadVPN.entitlements";
ENABLE_BITCODE = NO;
- INFOPLIST_FILE = MullvadVPN/Info.plist;
+ INFOPLIST_FILE = "MullvadVPN/Supporting Files/Info.plist";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -3143,9 +3431,9 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
- CODE_SIGN_ENTITLEMENTS = MullvadVPN/MullvadVPN.entitlements;
+ CODE_SIGN_ENTITLEMENTS = "MullvadVPN/Supporting Files/MullvadVPN.entitlements";
ENABLE_BITCODE = NO;
- INFOPLIST_FILE = MullvadVPN/Info.plist;
+ INFOPLIST_FILE = "MullvadVPN/Supporting Files/Info.plist";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
diff --git a/ios/MullvadVPN/AddressCacheTracker.swift b/ios/MullvadVPN/AddressCacheTracker/AddressCacheTracker.swift
index 8c3100f92b..8c3100f92b 100644
--- a/ios/MullvadVPN/AddressCacheTracker.swift
+++ b/ios/MullvadVPN/AddressCacheTracker/AddressCacheTracker.swift
diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift
index 3ed73a43fa..fcc5f2fe92 100644
--- a/ios/MullvadVPN/AppDelegate.swift
+++ b/ios/MullvadVPN/AppDelegate.swift
@@ -312,6 +312,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
loggerBuilder.addOSLogOutput(subsystem: bundleIdentifier)
#endif
+ loggerBuilder.logLevel = .debug
+
loggerBuilder.install()
}
diff --git a/ios/MullvadVPN/AccountDataThrottling.swift b/ios/MullvadVPN/Classes/AccountDataThrottling.swift
index 3f8ae0daf4..3f8ae0daf4 100644
--- a/ios/MullvadVPN/AccountDataThrottling.swift
+++ b/ios/MullvadVPN/Classes/AccountDataThrottling.swift
diff --git a/ios/MullvadVPN/AutomaticKeyboardResponder.swift b/ios/MullvadVPN/Classes/AutomaticKeyboardResponder.swift
index 443bfc3889..443bfc3889 100644
--- a/ios/MullvadVPN/AutomaticKeyboardResponder.swift
+++ b/ios/MullvadVPN/Classes/AutomaticKeyboardResponder.swift
diff --git a/ios/MullvadVPN/ConsolidatedApplicationLog.swift b/ios/MullvadVPN/Classes/ConsolidatedApplicationLog.swift
index ad5f53eda0..ad5f53eda0 100644
--- a/ios/MullvadVPN/ConsolidatedApplicationLog.swift
+++ b/ios/MullvadVPN/Classes/ConsolidatedApplicationLog.swift
diff --git a/ios/MullvadVPN/CustomDateComponentsFormatting.swift b/ios/MullvadVPN/Classes/CustomDateComponentsFormatting.swift
index 556c445ec1..556c445ec1 100644
--- a/ios/MullvadVPN/CustomDateComponentsFormatting.swift
+++ b/ios/MullvadVPN/Classes/CustomDateComponentsFormatting.swift
diff --git a/ios/MullvadVPN/DataSourceSnapshot.swift b/ios/MullvadVPN/Classes/DataSourceSnapshot.swift
index b68935000c..b68935000c 100644
--- a/ios/MullvadVPN/DataSourceSnapshot.swift
+++ b/ios/MullvadVPN/Classes/DataSourceSnapshot.swift
diff --git a/ios/MullvadVPN/DeviceDataThrottling.swift b/ios/MullvadVPN/Classes/DeviceDataThrottling.swift
index cb30b130ff..cb30b130ff 100644
--- a/ios/MullvadVPN/DeviceDataThrottling.swift
+++ b/ios/MullvadVPN/Classes/DeviceDataThrottling.swift
diff --git a/ios/MullvadVPN/ObserverList.swift b/ios/MullvadVPN/Classes/ObserverList.swift
index a2093c9809..a2093c9809 100644
--- a/ios/MullvadVPN/ObserverList.swift
+++ b/ios/MullvadVPN/Classes/ObserverList.swift
diff --git a/ios/MullvadVPN/Swizzle.swift b/ios/MullvadVPN/Classes/Swizzle.swift
index 6073384cf4..6073384cf4 100644
--- a/ios/MullvadVPN/Swizzle.swift
+++ b/ios/MullvadVPN/Classes/Swizzle.swift
diff --git a/ios/MullvadVPN/TermsOfService.swift b/ios/MullvadVPN/Classes/TermsOfService.swift
index fe1cc71003..8672f92e4d 100644
--- a/ios/MullvadVPN/TermsOfService.swift
+++ b/ios/MullvadVPN/Classes/TermsOfService.swift
@@ -18,4 +18,8 @@ enum TermsOfService {
static func setAgreed() {
UserDefaults.standard.set(true, forKey: userDefaultsKey)
}
+
+ static func unsetAgreed() {
+ UserDefaults.standard.set(false, forKey: userDefaultsKey)
+ }
}
diff --git a/ios/MullvadVPN/CustomSplitViewController.swift b/ios/MullvadVPN/Containers/CustomSplitViewController.swift
index a73441bd46..d4eb5ffd8f 100644
--- a/ios/MullvadVPN/CustomSplitViewController.swift
+++ b/ios/MullvadVPN/Containers/CustomSplitViewController.swift
@@ -71,8 +71,7 @@ class CustomSplitViewController: UISplitViewController, RootContainment {
// view is expanded.
if !isCollapsed, childViewController == viewControllers.last {
let sizeOverrideTraitCollection = UITraitCollection(
- horizontalSizeClass: self
- .traitCollection.horizontalSizeClass
+ horizontalSizeClass: self.traitCollection.horizontalSizeClass
)
return UITraitCollection(traitsFrom: [traitCollection, sizeOverrideTraitCollection])
diff --git a/ios/MullvadVPN/Containers/Navigation/CustomNavigationController.swift b/ios/MullvadVPN/Containers/Navigation/CustomNavigationController.swift
new file mode 100644
index 0000000000..55cf9794e7
--- /dev/null
+++ b/ios/MullvadVPN/Containers/Navigation/CustomNavigationController.swift
@@ -0,0 +1,25 @@
+//
+// CustomNavigationController.swift
+// MullvadVPN
+//
+// Created by pronebird on 23/02/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import UIKit
+
+class CustomNavigationController: UINavigationController {
+ override var childForStatusBarHidden: UIViewController? {
+ return topViewController
+ }
+
+ override var childForStatusBarStyle: UIViewController? {
+ return topViewController
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ navigationBar.configureCustomAppeareance()
+ }
+}
diff --git a/ios/MullvadVPN/CustomNavigationBar.swift b/ios/MullvadVPN/Containers/Navigation/UINavigationBar+Appearance.swift
index f326dd57da..f749f09e07 100644
--- a/ios/MullvadVPN/CustomNavigationBar.swift
+++ b/ios/MullvadVPN/Containers/Navigation/UINavigationBar+Appearance.swift
@@ -8,25 +8,7 @@
import UIKit
-class CustomNavigationBar: UINavigationBar {
- private static let titleTextAttributes: [NSAttributedString.Key: Any] = [
- .foregroundColor: UIColor.NavigationBar.titleColor,
- ]
-
- private static let backButtonTitlePositionOffset = UIOffset(horizontal: 4, vertical: 0)
- private static let backButtonTitleTextAttributes: [NSAttributedString.Key: Any] = [
- .foregroundColor: UIColor.NavigationBar.backButtonTitleColor,
- ]
-
- private let customBackIndicatorImage = UIImage(named: "IconBack")?
- .withTintColor(
- UIColor.NavigationBar.backButtonIndicatorColor,
- renderingMode: .alwaysOriginal
- )
-
- private let customBackIndicatorTransitionMask = UIImage(named: "IconBackTransitionMask")
-
- // Returns the distance from the title label to the bottom of navigation bar
+extension UINavigationBar {
var titleLabelBottomInset: CGFloat {
// Go two levels deep only
let subviewsToExamine = subviews.flatMap { view -> [UIView] in
@@ -45,22 +27,12 @@ class CustomNavigationBar: UINavigationBar {
}
}
- override init(frame: CGRect) {
- super.init(frame: frame)
-
+ func configureCustomAppeareance() {
var margins = layoutMargins
margins.left = UIMetrics.contentLayoutMargins.left
margins.right = UIMetrics.contentLayoutMargins.right
- layoutMargins = margins
- setupNavigationBarAppearance()
- }
-
- required init?(coder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
- private func setupNavigationBarAppearance() {
+ layoutMargins = margins
tintColor = UIColor.NavigationBar.titleColor
backgroundColor = UIColor.NavigationBar.backgroundColor
isTranslucent = false
@@ -70,20 +42,34 @@ class CustomNavigationBar: UINavigationBar {
}
private func makeNavigationBarAppearance() -> UINavigationBarAppearance {
+ let backIndicatorImage = UIImage(named: "IconBack")?.withTintColor(
+ UIColor.NavigationBar.backButtonIndicatorColor,
+ renderingMode: .alwaysOriginal
+ )
+ let backIndicatorTransitionMask = UIImage(named: "IconBackTransitionMask")
+
+ let titleTextAttributes: [NSAttributedString.Key: Any] = [
+ .foregroundColor: UIColor.NavigationBar.titleColor,
+ ]
+ let backButtonTitlePositionOffset = UIOffset(horizontal: 4, vertical: 0)
+ let backButtonTitleTextAttributes: [NSAttributedString.Key: Any] = [
+ .foregroundColor: UIColor.NavigationBar.backButtonTitleColor,
+ ]
+
let navigationBarAppearance = UINavigationBarAppearance()
navigationBarAppearance.configureWithTransparentBackground()
- navigationBarAppearance.titleTextAttributes = Self.titleTextAttributes
- navigationBarAppearance.largeTitleTextAttributes = Self.titleTextAttributes
+ navigationBarAppearance.titleTextAttributes = titleTextAttributes
+ navigationBarAppearance.largeTitleTextAttributes = titleTextAttributes
let plainBarButtonAppearance = UIBarButtonItemAppearance(style: .plain)
- plainBarButtonAppearance.normal.titleTextAttributes = Self.titleTextAttributes
+ plainBarButtonAppearance.normal.titleTextAttributes = titleTextAttributes
let doneBarButtonAppearance = UIBarButtonItemAppearance(style: .done)
- doneBarButtonAppearance.normal.titleTextAttributes = Self.titleTextAttributes
+ doneBarButtonAppearance.normal.titleTextAttributes = titleTextAttributes
let backButtonAppearance = UIBarButtonItemAppearance(style: .plain)
- backButtonAppearance.normal.titlePositionAdjustment = Self.backButtonTitlePositionOffset
- backButtonAppearance.normal.titleTextAttributes = Self.backButtonTitleTextAttributes
+ backButtonAppearance.normal.titlePositionAdjustment = backButtonTitlePositionOffset
+ backButtonAppearance.normal.titleTextAttributes = backButtonTitleTextAttributes
navigationBarAppearance.buttonAppearance = plainBarButtonAppearance
navigationBarAppearance.doneButtonAppearance = doneBarButtonAppearance
@@ -91,15 +77,15 @@ class CustomNavigationBar: UINavigationBar {
if #available(iOS 14, *) {
navigationBarAppearance.setBackIndicatorImage(
- customBackIndicatorImage,
- transitionMaskImage: customBackIndicatorTransitionMask
+ backIndicatorImage,
+ transitionMaskImage: backIndicatorTransitionMask
)
} else {
// Bug: on iOS 13 setBackIndicatorImage accepts parameters in backward order
// https://stackoverflow.com/a/58171229/351305
navigationBarAppearance.setBackIndicatorImage(
- customBackIndicatorTransitionMask,
- transitionMaskImage: customBackIndicatorImage
+ backIndicatorTransitionMask,
+ transitionMaskImage: backIndicatorImage
)
}
diff --git a/ios/MullvadVPN/HeaderBarButton.swift b/ios/MullvadVPN/Containers/Root/HeaderBarButton.swift
index df3858d42c..df3858d42c 100644
--- a/ios/MullvadVPN/HeaderBarButton.swift
+++ b/ios/MullvadVPN/Containers/Root/HeaderBarButton.swift
diff --git a/ios/MullvadVPN/HeaderBarView.swift b/ios/MullvadVPN/Containers/Root/HeaderBarView.swift
index 467bdb37da..467bdb37da 100644
--- a/ios/MullvadVPN/HeaderBarView.swift
+++ b/ios/MullvadVPN/Containers/Root/HeaderBarView.swift
diff --git a/ios/MullvadVPN/RootContainerViewController.swift b/ios/MullvadVPN/Containers/Root/RootContainerViewController.swift
index cfeb445484..65ebaffd47 100644
--- a/ios/MullvadVPN/RootContainerViewController.swift
+++ b/ios/MullvadVPN/Containers/Root/RootContainerViewController.swift
@@ -102,12 +102,13 @@ class RootContainerViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
-
var margins = view.layoutMargins
- margins.left = 24
- margins.right = 24
+ margins.left = UIMetrics.contentLayoutMargins.left
+ margins.right = UIMetrics.contentLayoutMargins.right
view.layoutMargins = margins
+ definesPresentationContext = true
+
addTransitionView()
addHeaderBarView()
updateHeaderBarBackground()
@@ -203,6 +204,23 @@ class RootContainerViewController: UIViewController {
)
}
+ func popToViewController(
+ _ controller: UIViewController,
+ animated: Bool,
+ completion: CompletionHandler? = nil
+ ) {
+ guard let index = viewControllers.firstIndex(of: controller) else { return }
+
+ let newViewControllers = Array(viewControllers[...index])
+
+ setViewControllersInternal(
+ newViewControllers,
+ isUnwinding: true,
+ animated: animated,
+ completion: completion
+ )
+ }
+
func popViewController(animated: Bool, completion: CompletionHandler? = nil) {
guard viewControllers.count > 1 else { return }
@@ -356,6 +374,16 @@ class RootContainerViewController: UIViewController {
animated: Bool,
completion: CompletionHandler? = nil
) {
+ assert(
+ Set(newViewControllers).count == newViewControllers.count,
+ "All view controllers in root container controller must be distinct"
+ )
+
+ guard viewControllers != newViewControllers else {
+ completion?()
+ return
+ }
+
// Dot not handle appearance events when the container itself is not visible
let shouldHandleAppearanceEvents = view.window != nil
@@ -369,6 +397,25 @@ class RootContainerViewController: UIViewController {
let viewControllersToRemove = viewControllers.filter { !newViewControllers.contains($0) }
let finishTransition = {
+ /*
+ Finish transition appearance.
+ Note this has to be done before the call to `didMove(to:)` or `removeFromParent()`
+ otherwise `endAppearanceTransition()` will fire `didMove(to:)` twice.
+ */
+ if shouldHandleAppearanceEvents {
+ if let targetViewController = targetViewController,
+ sourceViewController != targetViewController
+ {
+ self.endChildControllerTransition(targetViewController)
+ }
+
+ if let sourceViewController = sourceViewController,
+ sourceViewController != targetViewController
+ {
+ self.endChildControllerTransition(sourceViewController)
+ }
+ }
+
// Notify the added controllers that they finished a transition into the container
for child in viewControllersToAdd {
child.didMove(toParent: self)
@@ -386,19 +433,6 @@ class RootContainerViewController: UIViewController {
sourceViewController?.view.removeFromSuperview()
}
- // Finish appearance transition
- if shouldHandleAppearanceEvents {
- if let sourceViewController = sourceViewController {
- self.endChildControllerTransition(sourceViewController)
- }
-
- if let targetViewController = targetViewController,
- sourceViewController != targetViewController
- {
- self.endChildControllerTransition(targetViewController)
- }
- }
-
self.updateInterfaceOrientation(attemptRotateToDeviceOrientation: true)
self.updateAccessibilityElementsAndNotifyScreenChange()
@@ -439,7 +473,9 @@ class RootContainerViewController: UIViewController {
// Begin appearance transition
if shouldHandleAppearanceEvents {
- if let sourceViewController = sourceViewController {
+ if let sourceViewController = sourceViewController,
+ sourceViewController != targetViewController
+ {
beginChildControllerTransition(
sourceViewController,
isAppearing: false,
diff --git a/ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift b/ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift
new file mode 100644
index 0000000000..8cbdfe8a13
--- /dev/null
+++ b/ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift
@@ -0,0 +1,719 @@
+//
+// ApplicationCoordinator.swift
+// MullvadVPN
+//
+// Created by pronebird on 13/01/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import MullvadREST
+import MullvadTypes
+import RelayCache
+import UIKit
+
+/**
+ Preferred content size for controllers presented using formsheet modal presentation style.
+ */
+private let preferredFormSheetContentSize = CGSize(width: 480, height: 640)
+
+/**
+ Application coordinator managing split view and two navigation contexts.
+ */
+final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewControllerDelegate,
+ UISplitViewControllerDelegate, ApplicationRouterDelegate
+{
+ /**
+ Application router.
+ */
+ private var router: ApplicationRouter!
+
+ /**
+ Primary navigation container.
+
+ On iPhone, it is used as a container for horizontal flows (TOS, Login, Revoked, Out-of-time).
+
+ On iPad, it is used as a container to hold split view controller. Secondary navigation
+ container presented modally is used for horizontal flows.
+ */
+ private let primaryNavigationContainer = RootContainerViewController()
+
+ /**
+ Secondary navigation container.
+
+ On iPad, it is used in place of primary container for horizontal flows and displayed modally
+ above primary container. Unused on iPhone.
+ */
+ private let secondaryNavigationContainer = RootContainerViewController()
+
+ private lazy var secondaryRootConfiguration = ModalPresentationConfiguration(
+ preferredContentSize: preferredFormSheetContentSize,
+ modalPresentationStyle: .custom,
+ isModalInPresentation: true,
+ transitioningDelegate: SecondaryContextTransitioningDelegate()
+ )
+
+ private let splitViewController: CustomSplitViewController = {
+ let svc = CustomSplitViewController()
+ svc.minimumPrimaryColumnWidth = UIMetrics.minimumSplitViewSidebarWidth
+ svc.preferredPrimaryColumnWidthFraction = UIMetrics.maximumSplitViewSidebarWidthFraction
+ svc.dividerColor = UIColor.MainSplitView.dividerColor
+ svc.primaryEdge = .trailing
+ return svc
+ }()
+
+ private var splitTunnelCoordinator: TunnelCoordinator?
+ private var splitLocationCoordinator: SelectLocationCoordinator?
+
+ private let tunnelManager: TunnelManager
+ private let storePaymentManager: StorePaymentManager
+ private let relayCacheTracker: RelayCacheTracker
+
+ private let apiProxy: REST.APIProxy
+ private let devicesProxy: REST.DevicesProxy
+ private var tunnelObserver: TunnelObserver?
+
+ private var outOfTimeTimer: Timer?
+
+ var rootViewController: UIViewController {
+ return primaryNavigationContainer
+ }
+
+ init(
+ tunnelManager: TunnelManager,
+ storePaymentManager: StorePaymentManager,
+ relayCacheTracker: RelayCacheTracker,
+ apiProxy: REST.APIProxy,
+ devicesProxy: REST.DevicesProxy
+ ) {
+ self.tunnelManager = tunnelManager
+ self.storePaymentManager = storePaymentManager
+ self.relayCacheTracker = relayCacheTracker
+ self.apiProxy = apiProxy
+ self.devicesProxy = devicesProxy
+
+ /*
+ Uncomment if you'd like to test TOS again
+ TermsOfService.unsetAgreed()
+ */
+
+ super.init()
+
+ primaryNavigationContainer.delegate = self
+ secondaryNavigationContainer.delegate = self
+
+ router = ApplicationRouter(self)
+
+ addTunnelObserver()
+ }
+
+ func start() {
+ if isPad {
+ setupSplitView()
+ }
+
+ continueFlow(animated: false)
+ }
+
+ // MARK: - ApplicationRouterDelegate
+
+ func applicationRouter(
+ _ router: ApplicationRouter,
+ route: AppRoute,
+ animated: Bool,
+ completion: @escaping (Coordinator) -> Void
+ ) {
+ switch route {
+ case let .settings(subRoute):
+ presentSettings(route: subRoute, animated: animated, completion: completion)
+
+ case .selectLocation:
+ presentSelectLocation(animated: animated, completion: completion)
+
+ case .outOfTime:
+ presentOutOfTime(animated: animated, completion: completion)
+
+ case .revoked:
+ presentRevoked(animated: animated, completion: completion)
+
+ case .login:
+ presentLogin(animated: animated, completion: completion)
+
+ case .tos:
+ presentTOS(animated: animated, completion: completion)
+
+ case .main:
+ presentMain(animated: animated, completion: completion)
+ }
+ }
+
+ func applicationRouter(
+ _ router: ApplicationRouter,
+ dismissWithContext context: RouteDismissalContext,
+ completion: @escaping () -> Void
+ ) {
+ if context.isClosing {
+ let dismissedRoute = context.dismissedRoutes.first!
+
+ switch dismissedRoute.route.routeGroup {
+ case .primary:
+ endHorizontalFlow(animated: context.isAnimated, completion: completion)
+ context.dismissedRoutes.forEach { $0.coordinator.removeFromParent() }
+
+ case .selectLocation, .settings:
+ let coordinator = dismissedRoute.coordinator as! Presentable
+
+ coordinator.dismiss(animated: context.isAnimated, completion: completion)
+ }
+ } else {
+ let dismissedRoute = context.dismissedRoutes.first!
+ assert(context.dismissedRoutes.count == 1)
+
+ if case .outOfTime = dismissedRoute.route {
+ let coordinator = dismissedRoute.coordinator as! OutOfTimeCoordinator
+
+ coordinator.popFromNavigationStack(
+ animated: context.isAnimated,
+ completion: completion
+ )
+
+ coordinator.removeFromParent()
+ } else {
+ assertionFailure("Unhandled dismissal for \(dismissedRoute.route)")
+ completion()
+ }
+ }
+ }
+
+ func applicationRouter(_ router: ApplicationRouter, shouldPresent route: AppRoute) -> Bool {
+ switch route {
+ case .revoked:
+ // Check if device is still revoked.
+ return tunnelManager.deviceState == .revoked
+
+ case .outOfTime:
+ // Check if device is still out of time.
+ return tunnelManager.deviceState.accountData?.isExpired ?? false
+
+ default:
+ return true
+ }
+ }
+
+ func applicationRouter(
+ _ router: ApplicationRouter,
+ shouldDismissWithContext context: RouteDismissalContext
+ ) -> Bool {
+ return context.dismissedRoutes.allSatisfy { dismissedRoute in
+ /*
+ Prevent dismissal of "out of time" route in response to device state change when
+ making payment. It will dismiss itself once done.
+ */
+ if dismissedRoute.route == .outOfTime {
+ let coordinator = dismissedRoute.coordinator as! OutOfTimeCoordinator
+
+ return !coordinator.isMakingPayment
+ }
+
+ return true
+ }
+ }
+
+ func applicationRouter(
+ _ router: ApplicationRouter,
+ handleSubNavigationWithContext context: RouteSubnavigationContext,
+ completion: @escaping () -> Void
+ ) {
+ switch context.route {
+ case let .settings(subRoute):
+ let coordinator = context.presentedRoute.coordinator as! SettingsCoordinator
+
+ if let subRoute = subRoute {
+ coordinator.navigate(
+ to: subRoute,
+ animated: context.isAnimated,
+ completion: completion
+ )
+ } else {
+ completion()
+ }
+
+ default:
+ completion()
+ }
+ }
+
+ // MARK: - Private
+
+ /**
+ Continues application flow by evaluating what route to present next.
+ */
+ private func continueFlow(animated: Bool) {
+ let next = evaluateNextRoute()
+
+ /*
+ On iPad the main route is always visible as it's a part of root controller hence we never
+ ask router to navigate to it. Instead this is when we hide the primary horizontal
+ navigation.
+ */
+ if isPad, next == .main {
+ router.dismissAll(.primary, animated: animated)
+ } else {
+ router.present(next, animated: animated)
+ }
+ }
+
+ private func evaluateNextRoute() -> AppRoute {
+ guard TermsOfService.isAgreed else {
+ return .tos
+ }
+
+ switch tunnelManager.deviceState {
+ case .revoked:
+ return .revoked
+
+ case .loggedOut:
+ return .login
+
+ case let .loggedIn(accountData, _):
+ return accountData.isExpired ? .outOfTime : .main
+ }
+ }
+
+ private func logoutRevokedDevice() {
+ tunnelManager.unsetAccount { [weak self] in
+ self?.continueFlow(animated: true)
+ }
+ }
+
+ private func didLogout() {
+ router.dismissAll(.primary, animated: false)
+
+ continueFlow(animated: true)
+ }
+
+ /**
+ Navigation controller used for horizontal flows.
+ */
+ private var horizontalFlowController: RootContainerViewController {
+ if isPad {
+ return secondaryNavigationContainer
+ } else {
+ return primaryNavigationContainer
+ }
+ }
+
+ /**
+ Begins horizontal flow presenting a navigation controller suitable for current user interface
+ idiom.
+
+ On iPad this takes care of presenting a secondary navigation context using modal presentation
+ after calling the given `block`.
+
+ On iPhone this function simply passes the primary navigation container to the `block` and
+ nothing else.
+ */
+ private func beginHorizontalFlow(_ completion: (() -> Void)? = nil) {
+ if isPad {
+ if secondaryNavigationContainer.presentingViewController == nil {
+ secondaryRootConfiguration.apply(to: secondaryNavigationContainer)
+
+ primaryNavigationContainer.present(
+ secondaryNavigationContainer,
+ animated: true,
+ completion: completion
+ )
+ } else {
+ completion?()
+ }
+ } else {
+ completion?()
+ }
+ }
+
+ /**
+ Marks the end of horizontal flow.
+
+ On iPad this method dismisses the modally presented secondary navigation container and
+ releases all child view controllers from it.
+
+ Does nothing on iPhone.
+ */
+ private func endHorizontalFlow(animated: Bool = true, completion: (() -> Void)? = nil) {
+ if isPad {
+ secondaryNavigationContainer.dismiss(animated: animated, completion: completion)
+ } else {
+ completion?()
+ }
+ }
+
+ private var isPad: Bool {
+ return UIDevice.current.userInterfaceIdiom == .pad
+ }
+
+ private func setupSplitView() {
+ let tunnelCoordinator = makeTunnelCoordinator()
+ let selectLocationCoordinator = makeSelectLocationCoordinator(forModalPresentation: false)
+
+ addChild(tunnelCoordinator)
+ addChild(selectLocationCoordinator)
+
+ splitTunnelCoordinator = tunnelCoordinator
+ splitLocationCoordinator = selectLocationCoordinator
+
+ splitViewController.delegate = self
+ splitViewController.viewControllers = [
+ selectLocationCoordinator.navigationController,
+ tunnelCoordinator.rootViewController,
+ ]
+
+ primaryNavigationContainer.setViewControllers([splitViewController], animated: false)
+
+ tunnelCoordinator.start()
+ selectLocationCoordinator.start()
+ }
+
+ private func presentTOS(animated: Bool, completion: @escaping (Coordinator) -> Void) {
+ let coordinator = TermsOfServiceCoordinator(navigationController: horizontalFlowController)
+
+ coordinator.didFinish = { [weak self] coordinator in
+ self?.continueFlow(animated: true)
+ }
+
+ addChild(coordinator)
+ coordinator.start()
+
+ beginHorizontalFlow {
+ completion(coordinator)
+ }
+ }
+
+ private func presentMain(animated: Bool, completion: @escaping (Coordinator) -> Void) {
+ precondition(!isPad)
+
+ let tunnelCoordinator = makeTunnelCoordinator()
+
+ horizontalFlowController.pushViewController(
+ tunnelCoordinator.rootViewController,
+ animated: animated
+ )
+
+ addChild(tunnelCoordinator)
+ tunnelCoordinator.start()
+
+ beginHorizontalFlow {
+ completion(tunnelCoordinator)
+ }
+ }
+
+ private func presentRevoked(animated: Bool, completion: @escaping (Coordinator) -> Void) {
+ let coordinator = RevokedCoordinator(
+ navigationController: horizontalFlowController,
+ tunnelManager: tunnelManager
+ )
+
+ coordinator.didFinish = { [weak self] coordinator in
+ self?.logoutRevokedDevice()
+ }
+
+ addChild(coordinator)
+ coordinator.start(animated: animated)
+
+ beginHorizontalFlow {
+ completion(coordinator)
+ }
+ }
+
+ private func presentOutOfTime(animated: Bool, completion: @escaping (Coordinator) -> Void) {
+ let coordinator = OutOfTimeCoordinator(
+ navigationController: horizontalFlowController,
+ storePaymentManager: storePaymentManager,
+ tunnelManager: tunnelManager
+ )
+
+ coordinator.didFinishPayment = { [weak self] coordinator in
+ guard let self = self else { return }
+
+ if self.shouldDismissOutOfTime() {
+ self.router.dismiss(.outOfTime, animated: true)
+
+ self.continueFlow(animated: true)
+ }
+ }
+
+ addChild(coordinator)
+ coordinator.start(animated: animated)
+
+ beginHorizontalFlow {
+ completion(coordinator)
+ }
+ }
+
+ private func shouldDismissOutOfTime() -> Bool {
+ return !(tunnelManager.deviceState.accountData?.isExpired ?? false)
+ }
+
+ private func presentSelectLocation(
+ animated: Bool,
+ completion: @escaping (Coordinator) -> Void
+ ) {
+ let coordinator = makeSelectLocationCoordinator(forModalPresentation: true)
+ coordinator.start()
+
+ presentChild(coordinator, animated: animated) {
+ completion(coordinator)
+ }
+ }
+
+ private func presentLogin(animated: Bool, completion: @escaping (Coordinator) -> Void) {
+ let coordinator = LoginCoordinator(
+ navigationController: horizontalFlowController,
+ tunnelManager: tunnelManager,
+ devicesProxy: devicesProxy
+ )
+
+ coordinator.didFinish = { [weak self] coordinator in
+ self?.continueFlow(animated: true)
+ }
+
+ addChild(coordinator)
+ coordinator.start(animated: animated)
+
+ beginHorizontalFlow {
+ completion(coordinator)
+ }
+ }
+
+ private func makeTunnelCoordinator() -> TunnelCoordinator {
+ let tunnelCoordinator = TunnelCoordinator(tunnelManager: tunnelManager)
+
+ tunnelCoordinator.showSelectLocationPicker = { [weak self] in
+ self?.router.present(.selectLocation, animated: true)
+ }
+
+ return tunnelCoordinator
+ }
+
+ private func makeSelectLocationCoordinator(forModalPresentation isModalPresentation: Bool)
+ -> SelectLocationCoordinator
+ {
+ let navigationController = CustomNavigationController()
+ navigationController.isNavigationBarHidden = !isModalPresentation
+
+ let selectLocationCoordinator = SelectLocationCoordinator(
+ navigationController: navigationController,
+ tunnelManager: tunnelManager,
+ relayCacheTracker: relayCacheTracker
+ )
+
+ selectLocationCoordinator.didFinish = { [weak self] coordinator, relay in
+ if isModalPresentation {
+ self?.router.dismiss(.selectLocation, animated: true)
+ }
+ }
+
+ return selectLocationCoordinator
+ }
+
+ private func presentSettings(
+ route: SettingsNavigationRoute?,
+ animated: Bool,
+ completion: @escaping (Coordinator) -> Void
+ ) {
+ let interactorFactory = SettingsInteractorFactory(
+ storePaymentManager: storePaymentManager,
+ tunnelManager: tunnelManager,
+ apiProxy: apiProxy
+ )
+
+ let navigationController = CustomNavigationController()
+ let coordinator = SettingsCoordinator(
+ navigationController: navigationController,
+ interactorFactory: interactorFactory
+ )
+
+ coordinator.didFinish = { [weak self] coordinator, reason in
+ self?.router.dismissAll(.settings, animated: true)
+
+ if reason == .userLoggedOut {
+ self?.didLogout()
+ }
+ }
+
+ coordinator.willNavigate = { [weak self] coordinator, from, to in
+ if to == .root {
+ self?.onShowSettings?()
+ }
+ }
+
+ coordinator.navigate(to: route ?? .root, animated: false)
+
+ coordinator.start()
+
+ presentChild(
+ coordinator,
+ animated: animated,
+ configuration: ModalPresentationConfiguration(
+ preferredContentSize: preferredFormSheetContentSize,
+ modalPresentationStyle: .formSheet
+ )
+ ) {
+ completion(coordinator)
+ }
+ }
+
+ private func addTunnelObserver() {
+ let tunnelObserver =
+ TunnelBlockObserver(didUpdateDeviceState: { [weak self] manager, deviceState in
+ self?.deviceStateDidChange(deviceState)
+ })
+
+ tunnelManager.addObserver(tunnelObserver)
+
+ self.tunnelObserver = tunnelObserver
+
+ splitViewController.preferredDisplayMode = tunnelManager.deviceState.splitViewMode
+ }
+
+ private func deviceStateDidChange(_ deviceState: DeviceState) {
+ splitViewController.preferredDisplayMode = deviceState.splitViewMode
+
+ switch deviceState {
+ case let .loggedIn(accountData, _):
+ updateOutOfTimeTimer()
+
+ if !accountData.isExpired {
+ router.dismiss(.outOfTime, animated: true)
+ }
+
+ case .revoked:
+ cancelOutOfTimeTimer()
+ router.present(.revoked, animated: true)
+
+ case .loggedOut:
+ cancelOutOfTimeTimer()
+ }
+ }
+
+ // MARK: - Out of time
+
+ private func updateOutOfTimeTimer() {
+ cancelOutOfTimeTimer()
+
+ guard let expiry = tunnelManager.deviceState.accountData?.expiry else { return }
+
+ let timer = Timer(fire: expiry, interval: 0, repeats: false, block: { [weak self] _ in
+ self?.outOfTimeTimerDidFire()
+ })
+
+ RunLoop.main.add(timer, forMode: .common)
+
+ outOfTimeTimer = timer
+ }
+
+ private func outOfTimeTimerDidFire() {
+ router.present(.outOfTime, animated: true)
+ }
+
+ private func cancelOutOfTimeTimer() {
+ outOfTimeTimer?.invalidate()
+ outOfTimeTimer = nil
+ }
+
+ // MARK: - Settings
+
+ var onShowSettings: (() -> Void)?
+
+ var isPresentingSettings: Bool {
+ return router.isPresenting(.settings)
+ }
+
+ // MARK: - Deep link
+
+ func showAccountSettings() {
+ router.present(.settings(.account))
+ }
+
+ // MARK: - UISplitViewControllerDelegate
+
+ func primaryViewController(forExpanding splitViewController: UISplitViewController)
+ -> UIViewController?
+ {
+ return splitLocationCoordinator?.navigationController
+ }
+
+ func primaryViewController(forCollapsing splitViewController: UISplitViewController)
+ -> UIViewController?
+ {
+ return splitTunnelCoordinator?.rootViewController
+ }
+
+ func splitViewController(
+ _ splitViewController: UISplitViewController,
+ collapseSecondary secondaryViewController: UIViewController,
+ onto primaryViewController: UIViewController
+ ) -> Bool {
+ return true
+ }
+
+ func splitViewController(
+ _ splitViewController: UISplitViewController,
+ separateSecondaryFrom primaryViewController: UIViewController
+ ) -> UIViewController? {
+ return nil
+ }
+
+ func splitViewControllerDidExpand(_ svc: UISplitViewController) {
+ router.dismissAll(.selectLocation, animated: false)
+ }
+
+ // MARK: - RootContainerViewControllerDelegate
+
+ func rootContainerViewControllerShouldShowSettings(
+ _ controller: RootContainerViewController,
+ navigateTo route: SettingsNavigationRoute?,
+ animated: Bool
+ ) {
+ router.present(.settings(route), animated: animated)
+ }
+
+ func rootContainerViewSupportedInterfaceOrientations(_ controller: RootContainerViewController)
+ -> UIInterfaceOrientationMask
+ {
+ if isPad {
+ return [.landscape, .portrait]
+ } else {
+ return [.portrait]
+ }
+ }
+
+ func rootContainerViewAccessibilityPerformMagicTap(_ controller: RootContainerViewController)
+ -> Bool
+ {
+ guard tunnelManager.deviceState.isLoggedIn else { return false }
+
+ switch tunnelManager.tunnelStatus.state {
+ case .connected, .connecting, .reconnecting, .waitingForConnectivity:
+ tunnelManager.reconnectTunnel(selectNewRelay: true)
+
+ case .disconnecting, .disconnected:
+ tunnelManager.startTunnel()
+
+ case .pendingReconnect:
+ break
+ }
+
+ return true
+ }
+
+ // MARK: - Presenting
+
+ var presentationContext: UIViewController {
+ return primaryNavigationContainer.presentedViewController ?? primaryNavigationContainer
+ }
+}
+
+extension DeviceState {
+ var splitViewMode: UISplitViewController.DisplayMode {
+ return isLoggedIn ? .allVisible : .secondaryOnly
+ }
+}
diff --git a/ios/MullvadVPN/Coordinators/App/ApplicationRouter.swift b/ios/MullvadVPN/Coordinators/App/ApplicationRouter.swift
new file mode 100644
index 0000000000..ac88c74926
--- /dev/null
+++ b/ios/MullvadVPN/Coordinators/App/ApplicationRouter.swift
@@ -0,0 +1,666 @@
+//
+// ApplicationRouter.swift
+// MullvadVPN
+//
+// Created by pronebird on 16/03/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadLogging
+import UIKit
+
+/**
+ Enum type describing groups of routes. Each group is a modal layer with horizontal navigation
+ inside with exception where primary navigation is a part of root controller on iPhone.
+ */
+enum AppRouteGroup: Comparable, Equatable, Hashable {
+ /**
+ Primary horizontal navigation group.
+ */
+ case primary
+
+ /**
+ Select location group.
+ */
+ case selectLocation
+
+ /**
+ Setings group.
+ */
+ case settings
+
+ /**
+ Returns `true` if group is presented modally, otherwise `false` if group is a part of root view
+ controller.
+ */
+ var isModal: Bool {
+ switch self {
+ case .primary:
+ return UIDevice.current.userInterfaceIdiom == .pad
+
+ case .selectLocation, .settings:
+ return true
+ }
+ }
+
+ private var order: Int {
+ switch self {
+ case .primary:
+ return 0
+ case .settings, .selectLocation:
+ return 1
+ }
+ }
+
+ static func < (lhs: AppRouteGroup, rhs: AppRouteGroup) -> Bool {
+ return lhs.order < rhs.order
+ }
+}
+
+/**
+ Enum type describing primary application routes.
+ */
+enum AppRoute: Equatable, Hashable {
+ /**
+ Settings route. Contains sub-route to display.
+ */
+ case settings(SettingsNavigationRoute?)
+
+ /**
+ Select location route.
+ */
+ case selectLocation
+
+ /**
+ Routes that are part of primary horizontal navigation group.
+ */
+ case tos, login, main, revoked, outOfTime
+
+ /**
+ Returns `true` when only one route of a kind can be displayed.
+ */
+ var isExclusive: Bool {
+ switch self {
+ case .selectLocation, .settings:
+ return true
+ default:
+ return false
+ }
+ }
+
+ /**
+ Returns `true` if the route supports sub-navigation.
+ */
+ var supportsSubNavigation: Bool {
+ if case .settings = self {
+ return true
+ } else {
+ return false
+ }
+ }
+
+ /**
+ Navigation group to which the route belongs to.
+ */
+ var routeGroup: AppRouteGroup {
+ switch self {
+ case .tos, .login, .main, .revoked, .outOfTime:
+ return .primary
+ case .selectLocation:
+ return .selectLocation
+ case .settings:
+ return .settings
+ }
+ }
+}
+
+/**
+ Struct describing a routing request for presentation or dismissal.
+ */
+struct PendingRoute: Equatable {
+ var operation: RouteOperation
+ var animated: Bool
+}
+
+/**
+ Enum type describing an attempt to fullfill the route presentation request.
+ **/
+enum PendingPresentationResult {
+ /**
+ Successfully presented the route.
+ */
+ case success
+
+ /**
+ The request to present this route should be dropped.
+ */
+ case drop
+
+ /**
+ The request to present this route cannot be fulfilled because the modal context does not allow
+ for that.
+
+ For example, on iPad, primary context cannot be presented above settings, because it enables
+ access to settings by making the settings cog accessible via custom presentation controller.
+ In such case the router will attempt to fulfill other requests in hope that perhaps settings
+ can be dismissed first before getting back to that request.
+ */
+ case blockedByModalContext
+}
+
+/**
+ Enum type describing an attempt to fulfill the route dismissal request.
+ */
+enum PendingDismissalResult {
+ /**
+ Successfully dismissed the route.
+ */
+ case success
+
+ /**
+ The request to present this route should be dropped.
+ */
+ case drop
+
+ /**
+ The route cannot be dismissed immediately because it's blocked by another modal presented
+ above.
+
+ The router will attempt to fulfill other requests first in hope to unblock the route by
+ dismissing the modal above before getting back to that request.
+ */
+ case blockedByModalAbove
+}
+
+/**
+ Enum descibing operation over the route.
+ */
+enum RouteOperation: Equatable {
+ /**
+ Present route.
+ */
+ case present(AppRoute)
+
+ /**
+ Dismiss route.
+ */
+ case dismiss(DismissMatch)
+
+ /**
+ Returns a group of affected routes.
+ */
+ var routeGroup: AppRouteGroup {
+ switch self {
+ case let .present(route):
+ return route.routeGroup
+ case let .dismiss(dismissMatch):
+ return dismissMatch.routeGroup
+ }
+ }
+}
+
+/**
+ Enum type describing a single route or a group of routes requested to be dismissed.
+ */
+enum DismissMatch: Equatable {
+ case group(AppRouteGroup)
+ case singleRoute(AppRoute)
+
+ /**
+ Returns a group of affected routes.
+ */
+ var routeGroup: AppRouteGroup {
+ switch self {
+ case let .group(group):
+ return group
+ case let .singleRoute(route):
+ return route.routeGroup
+ }
+ }
+}
+
+/**
+ Struct describing presented route.
+ */
+struct PresentedRoute: Equatable {
+ var route: AppRoute
+ var coordinator: Coordinator
+}
+
+/**
+ Struct holding information used by delegate to perform dismissal of the route(s) in subject.
+ */
+struct RouteDismissalContext {
+ /**
+ Specific routes that are being dismissed.
+ */
+ var dismissedRoutes: [PresentedRoute]
+
+ /**
+ Whether the entire group is being dismissed.
+ */
+ var isClosing: Bool
+
+ /**
+ Whether transition is animated.
+ */
+ var isAnimated: Bool
+}
+
+/**
+ Struct holding information used by delegate to perform sub-navigation of the route in subject.
+ */
+struct RouteSubnavigationContext {
+ var presentedRoute: PresentedRoute
+ var route: AppRoute
+ var isAnimated: Bool
+}
+
+/**
+ Application router delegate
+ */
+protocol ApplicationRouterDelegate: AnyObject {
+ /**
+ Delegate should present the route and pass corresponding `Coordinator` upon completion.
+ */
+ func applicationRouter(
+ _ router: ApplicationRouter,
+ route: AppRoute,
+ animated: Bool,
+ completion: @escaping (Coordinator) -> Void
+ )
+
+ /**
+ Delegate should dismiss the route.
+ */
+ func applicationRouter(
+ _ router: ApplicationRouter,
+ dismissWithContext context: RouteDismissalContext,
+ completion: @escaping () -> Void
+ )
+
+ /**
+ Delegate may reconsider if route presentation is still needed.
+
+ Return `true` to proceed with presenation, otherwise `false` to prevent it.
+ */
+ func applicationRouter(_ router: ApplicationRouter, shouldPresent route: AppRoute) -> Bool
+
+ /**
+ Delegate may reconsider if route dismissal should be done.
+
+ Return `true` to proceed with dismissal, otherwise `false` to prevent it.
+ */
+ func applicationRouter(
+ _ router: ApplicationRouter,
+ shouldDismissWithContext context: RouteDismissalContext
+ ) -> Bool
+
+ /**
+ Delegate should handle sub-navigation for routes supporting it then call completion to tell
+ router when it's done.
+ */
+ func applicationRouter(
+ _ router: ApplicationRouter,
+ handleSubNavigationWithContext context: RouteSubnavigationContext,
+ completion: @escaping () -> Void
+ )
+}
+
+/**
+ Main application router.
+ */
+final class ApplicationRouter {
+ private let logger = Logger(label: "ApplicationRouter")
+
+ private(set) var modalStack: [AppRouteGroup] = []
+ private var presentedRoutes: [AppRouteGroup: [PresentedRoute]] = [:]
+
+ private var pendingRoutes = [PendingRoute]()
+ private var isProcessingPendingRoutes = false
+
+ private unowned let delegate: ApplicationRouterDelegate
+
+ /**
+ Designated initializer.
+
+ Delegate object is unonwed and the caller has to guarantee that the router does not outlive it.
+ */
+ init(_ delegate: ApplicationRouterDelegate) {
+ self.delegate = delegate
+ }
+
+ /**
+ Returns `true` is the given route group is currently being presented.
+ */
+ func isPresenting(_ group: AppRouteGroup) -> Bool {
+ return modalStack.contains(group)
+ }
+
+ /**
+ Enqueue route for presetnation.
+ */
+ func present(_ route: AppRoute, animated: Bool = true) {
+ enqueue(PendingRoute(
+ operation: .present(route),
+ animated: animated
+ ))
+ }
+
+ /**
+ Enqueue dismissal of the route.
+ */
+ func dismiss(_ route: AppRoute, animated: Bool = true) {
+ enqueue(PendingRoute(
+ operation: .dismiss(.singleRoute(route)),
+ animated: animated
+ ))
+ }
+
+ /**
+ Enqueue dismissal of a group of routes.
+ */
+ func dismissAll(_ group: AppRouteGroup, animated: Bool = true) {
+ enqueue(PendingRoute(
+ operation: .dismiss(.group(group)),
+ animated: animated
+ ))
+ }
+
+ private func enqueue(_ pendingRoute: PendingRoute) {
+ logger.debug("Enqueue \(pendingRoute.operation).")
+
+ pendingRoutes.append(pendingRoute)
+
+ if !isProcessingPendingRoutes {
+ processPendingRoutes()
+ }
+ }
+
+ private func presentRoute(
+ _ route: AppRoute,
+ animated: Bool,
+ completion: @escaping (PendingPresentationResult) -> Void
+ ) {
+ /**
+ Pass sub-route for routes suppporting sub-navigation.
+ */
+ if route.supportsSubNavigation, modalStack.contains(route.routeGroup),
+ var presentedRoute = presentedRoutes[route.routeGroup]?.first
+ {
+ let context = RouteSubnavigationContext(
+ presentedRoute: presentedRoute,
+ route: route,
+ isAnimated: animated
+ )
+
+ presentedRoute.route = route
+ presentedRoutes[route.routeGroup] = [presentedRoute]
+
+ delegate.applicationRouter(self, handleSubNavigationWithContext: context) {
+ completion(.success)
+ }
+
+ return
+ }
+
+ /**
+ Drop duplicate routes.
+ */
+ if route.isExclusive, modalStack.contains(route.routeGroup) {
+ completion(.drop)
+ return
+ }
+
+ /**
+ Drop if the last presented route within the group is the same.
+ */
+ if !route.isExclusive, presentedRoutes[route.routeGroup]?.last?.route == route {
+ completion(.drop)
+ return
+ }
+
+ /**
+ Check if route can be presented above the last route in the modal stack.
+ */
+ if let lastRouteGroup = modalStack.last, lastRouteGroup >= route.routeGroup,
+ route.routeGroup.isModal
+ {
+ completion(.blockedByModalContext)
+ return
+ }
+
+ /**
+ Consult with delegate whether the route should still be presented.
+ */
+ if delegate.applicationRouter(self, shouldPresent: route) {
+ delegate.applicationRouter(self, route: route, animated: animated) { coordinator in
+ /*
+ Synchronize router when modal controllers are removed by swipe.
+ */
+ if let presentable = coordinator as? Presentable {
+ presentable.onInteractiveDismissal { [weak self] coordinator in
+ self?.handleInteractiveDismissal(route: route, coordinator: coordinator)
+ }
+ }
+
+ self.addPresentedRoute(PresentedRoute(route: route, coordinator: coordinator))
+
+ completion(.success)
+ }
+ } else {
+ completion(.drop)
+ }
+ }
+
+ private func dismissGroup(
+ _ dismissGroup: AppRouteGroup,
+ animated: Bool,
+ completion: @escaping (PendingDismissalResult) -> Void
+ ) {
+ /**
+ Check if routes corresponding to the group requested for dismissal are present.
+ */
+ guard modalStack.contains(dismissGroup) else {
+ completion(.drop)
+ return
+ }
+
+ /**
+ Check if the group can be dismissed and it's not blocked by another group presented above.
+ */
+ if modalStack.last != dismissGroup, dismissGroup.isModal {
+ completion(.blockedByModalAbove)
+ return
+ }
+
+ let dismissedRoutes = presentedRoutes[dismissGroup] ?? []
+ assert(!dismissedRoutes.isEmpty)
+
+ let context = RouteDismissalContext(
+ dismissedRoutes: dismissedRoutes,
+ isClosing: true,
+ isAnimated: animated
+ )
+
+ /**
+ Consult with delegate whether the route should still be dismissed.
+ */
+ guard delegate.applicationRouter(self, shouldDismissWithContext: context) else {
+ completion(.drop)
+ return
+ }
+
+ presentedRoutes.removeValue(forKey: dismissGroup)
+ modalStack.removeAll { $0 == dismissGroup }
+
+ delegate.applicationRouter(self, dismissWithContext: context) {
+ completion(.success)
+ }
+ }
+
+ private func dismissRoute(
+ _ dismissRoute: AppRoute,
+ animated: Bool,
+ completion: @escaping (PendingDismissalResult) -> Void
+ ) {
+ var routes = presentedRoutes[dismissRoute.routeGroup] ?? []
+
+ // Find the index of route to pop.
+ guard let index = routes.lastIndex(where: { $0.route == dismissRoute }) else {
+ completion(.drop)
+ return
+ }
+
+ // Check if dismissing the last route in horizontal navigation group.
+ let isLastRoute = routes.count == 1
+
+ // Check if the route can be dismissed and there is no other modal above.
+ if let lastModalGroup = modalStack.last,
+ lastModalGroup != dismissRoute.routeGroup,
+ dismissRoute.routeGroup.isModal,
+ isLastRoute
+ {
+ completion(.blockedByModalAbove)
+ return
+ }
+
+ let context = RouteDismissalContext(
+ dismissedRoutes: [routes[index]],
+ isClosing: isLastRoute,
+ isAnimated: animated
+ )
+
+ /**
+ Consult with delegate whether the route should still be dismissed.
+ */
+ guard delegate.applicationRouter(self, shouldDismissWithContext: context) else {
+ completion(.drop)
+ return
+ }
+
+ if isLastRoute {
+ presentedRoutes.removeValue(forKey: dismissRoute.routeGroup)
+ modalStack.removeAll { $0 == dismissRoute.routeGroup }
+ } else {
+ routes.remove(at: index)
+ presentedRoutes[dismissRoute.routeGroup] = routes
+ }
+
+ delegate.applicationRouter(self, dismissWithContext: context) {
+ completion(.success)
+ }
+ }
+
+ private func processPendingRoutes(skipRouteGroups: Set<AppRouteGroup> = []) {
+ isProcessingPendingRoutes = true
+
+ let pendingRoute = pendingRoutes.first { pendingRoute in
+ return !skipRouteGroups.contains(pendingRoute.operation.routeGroup)
+ }
+
+ guard let pendingRoute = pendingRoute else {
+ isProcessingPendingRoutes = false
+ return
+ }
+
+ switch pendingRoute.operation {
+ case let .present(route):
+ presentRoute(route, animated: pendingRoute.animated) { result in
+ switch result {
+ case .success, .drop:
+ self.finishPendingRoute(pendingRoute)
+
+ case .blockedByModalContext:
+ /**
+ Present next route if this one is not ready to be presented.
+ */
+ self.processPendingRoutes(
+ skipRouteGroups: skipRouteGroups.union([route.routeGroup])
+ )
+ }
+ }
+
+ case let .dismiss(dismissMatch):
+ handleDismissal(dismissMatch, animated: pendingRoute.animated) { result in
+ switch result {
+ case .success, .drop:
+ self.finishPendingRoute(pendingRoute)
+
+ case .blockedByModalAbove:
+ /**
+ If router cannot dismiss modal because there is one above,
+ try walking down the queue and see if there is a dismissal request that could
+ resolve that.
+ */
+ self.processPendingRoutes(
+ skipRouteGroups: skipRouteGroups.union([dismissMatch.routeGroup])
+ )
+ }
+ }
+ }
+ }
+
+ private func handleDismissal(
+ _ dismissMatch: DismissMatch,
+ animated: Bool,
+ completion: @escaping (PendingDismissalResult) -> Void
+ ) {
+ switch dismissMatch {
+ case let .singleRoute(route):
+ dismissRoute(route, animated: animated, completion: completion)
+
+ case let .group(group):
+ dismissGroup(group, animated: animated, completion: completion)
+ }
+ }
+
+ private func finishPendingRoute(_ pendingRoute: PendingRoute) {
+ if let index = pendingRoutes.firstIndex(of: pendingRoute) {
+ pendingRoutes.remove(at: index)
+ }
+
+ processPendingRoutes()
+ }
+
+ private func handleInteractiveDismissal(route: AppRoute, coordinator: Coordinator) {
+ var routes = presentedRoutes[route.routeGroup] ?? []
+
+ routes.removeAll { presentedRoute in
+ return presentedRoute.coordinator == coordinator
+ }
+
+ if routes.isEmpty {
+ presentedRoutes.removeValue(forKey: route.routeGroup)
+ modalStack.removeAll { $0 == route.routeGroup }
+ } else {
+ presentedRoutes[route.routeGroup] = routes
+ }
+
+ if !isProcessingPendingRoutes {
+ processPendingRoutes()
+ }
+ }
+
+ private func addPresentedRoute(_ presented: PresentedRoute) {
+ let group = presented.route.routeGroup
+ var routes = presentedRoutes[group] ?? []
+
+ if presented.route.isExclusive {
+ routes = [presented]
+ } else {
+ routes.append(presented)
+ }
+
+ presentedRoutes[group] = routes
+
+ if !modalStack.contains(group) {
+ if group.isModal {
+ modalStack.append(group)
+ } else {
+ modalStack.insert(group, at: 0)
+ }
+ }
+ }
+}
diff --git a/ios/MullvadVPN/Coordinators/App/LoginCoordinator.swift b/ios/MullvadVPN/Coordinators/App/LoginCoordinator.swift
new file mode 100644
index 0000000000..a558634066
--- /dev/null
+++ b/ios/MullvadVPN/Coordinators/App/LoginCoordinator.swift
@@ -0,0 +1,121 @@
+//
+// LoginCoordinator.swift
+// MullvadVPN
+//
+// Created by pronebird on 27/01/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import MullvadREST
+import MullvadTypes
+import Operations
+import UIKit
+
+final class LoginCoordinator: Coordinator, DeviceManagementViewControllerDelegate {
+ private let tunnelManager: TunnelManager
+ private let devicesProxy: REST.DevicesProxy
+
+ private var loginController: LoginViewController?
+ private var lastLoginAction: LoginAction?
+
+ var didFinish: ((LoginCoordinator) -> Void)?
+
+ let navigationController: RootContainerViewController
+
+ init(
+ navigationController: RootContainerViewController,
+ tunnelManager: TunnelManager,
+ devicesProxy: REST.DevicesProxy
+ ) {
+ self.navigationController = navigationController
+ self.tunnelManager = tunnelManager
+ self.devicesProxy = devicesProxy
+ }
+
+ func start(animated: Bool) {
+ let interactor = LoginInteractor(tunnelManager: tunnelManager)
+ let loginController = LoginViewController(interactor: interactor)
+
+ loginController.didFinishLogin = { [weak self] action, error in
+ return self?.didFinishLogin(action: action, error: error) ?? .nothing
+ }
+
+ navigationController.pushViewController(loginController, animated: animated)
+
+ self.loginController = loginController
+ }
+
+ // MARK: - DeviceManagementViewControllerDelegate
+
+ func deviceManagementViewControllerDidCancel(_ controller: DeviceManagementViewController) {
+ returnToLogin(repeatLogin: false)
+ }
+
+ func deviceManagementViewControllerDidFinish(_ controller: DeviceManagementViewController) {
+ returnToLogin(repeatLogin: true)
+ }
+
+ // MARK: - Private
+
+ private func didFinishLogin(action: LoginAction, error: Error?) -> EndLoginAction {
+ guard let error = error else {
+ callDidFinishAfterDelay()
+ return .nothing
+ }
+
+ if case let .useExistingAccount(accountNumber) = action {
+ if let error = error as? REST.Error, error.compareErrorCode(.maxDevicesReached) {
+ return .wait(Promise { resolve in
+ self.showDeviceList(for: accountNumber) { error in
+ self.lastLoginAction = action
+
+ resolve(error.map { .failure($0) } ?? .success(()))
+ }
+ })
+ } else {
+ return .activateTextField
+ }
+ }
+
+ return .nothing
+ }
+
+ private func callDidFinishAfterDelay() {
+ DispatchQueue.main.asyncAfter(wallDeadline: .now() + .seconds(1)) { [weak self] in
+ guard let self = self else { return }
+ self.didFinish?(self)
+ }
+ }
+
+ private func returnToLogin(repeatLogin: Bool) {
+ guard let loginController = loginController else { return }
+
+ navigationController.popToViewController(loginController, animated: true) {
+ if let lastLoginAction = self.lastLoginAction, repeatLogin {
+ self.loginController?.start(action: lastLoginAction)
+ }
+ }
+ }
+
+ private func showDeviceList(for accountNumber: String, completion: @escaping (Error?) -> Void) {
+ let interactor = DeviceManagementInteractor(
+ accountNumber: accountNumber,
+ devicesProxy: devicesProxy
+ )
+ let controller = DeviceManagementViewController(interactor: interactor)
+ controller.delegate = self
+ controller.fetchDevices(animateUpdates: false) { [weak self] result in
+ guard let self = self else { return }
+
+ switch result {
+ case .success:
+ self.navigationController.pushViewController(controller, animated: true) {
+ completion(nil)
+ }
+
+ case let .failure(error):
+ completion(error)
+ }
+ }
+ }
+}
diff --git a/ios/MullvadVPN/Coordinators/App/OutOfTimeCoordinator.swift b/ios/MullvadVPN/Coordinators/App/OutOfTimeCoordinator.swift
new file mode 100644
index 0000000000..30ddeeb1ee
--- /dev/null
+++ b/ios/MullvadVPN/Coordinators/App/OutOfTimeCoordinator.swift
@@ -0,0 +1,70 @@
+//
+// OutOfTimeCoordinator.swift
+// MullvadVPN
+//
+// Created by pronebird on 10/03/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import UIKit
+
+class OutOfTimeCoordinator: Coordinator, OutOfTimeViewControllerDelegate {
+ let navigationController: RootContainerViewController
+ let storePaymentManager: StorePaymentManager
+ let tunnelManager: TunnelManager
+
+ var didFinishPayment: ((OutOfTimeCoordinator) -> Void)?
+
+ private(set) var isMakingPayment = false
+ private var viewController: OutOfTimeViewController?
+
+ init(
+ navigationController: RootContainerViewController,
+ storePaymentManager: StorePaymentManager,
+ tunnelManager: TunnelManager
+ ) {
+ self.navigationController = navigationController
+ self.storePaymentManager = storePaymentManager
+ self.tunnelManager = tunnelManager
+ }
+
+ func start(animated: Bool) {
+ let interactor = OutOfTimeInteractor(
+ storePaymentManager: storePaymentManager,
+ tunnelManager: tunnelManager
+ )
+ let controller = OutOfTimeViewController(interactor: interactor)
+ controller.delegate = self
+
+ viewController = controller
+
+ navigationController.pushViewController(controller, animated: animated)
+ }
+
+ func popFromNavigationStack(animated: Bool, completion: @escaping () -> Void) {
+ guard let viewController = viewController else {
+ completion()
+ return
+ }
+
+ let viewControllers = navigationController.viewControllers.filter { $0 != viewController }
+
+ navigationController.setViewControllers(
+ viewControllers,
+ animated: animated,
+ completion: completion
+ )
+ }
+
+ // MARK: - OutOfTimeViewControllerDelegate
+
+ func outOfTimeViewControllerDidBeginPayment(_ controller: OutOfTimeViewController) {
+ isMakingPayment = true
+ }
+
+ func outOfTimeViewControllerDidEndPayment(_ controller: OutOfTimeViewController) {
+ isMakingPayment = false
+
+ didFinishPayment?(self)
+ }
+}
diff --git a/ios/MullvadVPN/Coordinators/App/RevokedCoordinator.swift b/ios/MullvadVPN/Coordinators/App/RevokedCoordinator.swift
new file mode 100644
index 0000000000..254c016cd3
--- /dev/null
+++ b/ios/MullvadVPN/Coordinators/App/RevokedCoordinator.swift
@@ -0,0 +1,34 @@
+//
+// RevokedCoordinator.swift
+// MullvadVPN
+//
+// Created by pronebird on 07/03/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import UIKit
+
+final class RevokedCoordinator: Coordinator {
+ let navigationController: RootContainerViewController
+ private let tunnelManager: TunnelManager
+
+ var didFinish: ((RevokedCoordinator) -> Void)?
+
+ init(navigationController: RootContainerViewController, tunnelManager: TunnelManager) {
+ self.navigationController = navigationController
+ self.tunnelManager = tunnelManager
+ }
+
+ func start(animated: Bool) {
+ let interactor = RevokedDeviceInteractor(tunnelManager: tunnelManager)
+ let controller = RevokedDeviceViewController(interactor: interactor)
+
+ controller.didFinish = { [weak self] in
+ guard let self = self else { return }
+
+ self.didFinish?(self)
+ }
+
+ navigationController.pushViewController(controller, animated: animated)
+ }
+}
diff --git a/ios/MullvadVPN/Coordinators/App/SelectLocationCoordinator.swift b/ios/MullvadVPN/Coordinators/App/SelectLocationCoordinator.swift
new file mode 100644
index 0000000000..6571f01f70
--- /dev/null
+++ b/ios/MullvadVPN/Coordinators/App/SelectLocationCoordinator.swift
@@ -0,0 +1,82 @@
+//
+// SelectLocationCoordinator.swift
+// MullvadVPN
+//
+// Created by pronebird on 29/01/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import MullvadTypes
+import RelayCache
+import UIKit
+
+class SelectLocationCoordinator: Coordinator, Presentable, RelayCacheTrackerObserver {
+ let navigationController: UINavigationController
+
+ var presentedViewController: UIViewController {
+ return navigationController
+ }
+
+ private let tunnelManager: TunnelManager
+ private let relayCacheTracker: RelayCacheTracker
+
+ var didFinish: ((SelectLocationCoordinator, RelayLocation?) -> Void)?
+
+ init(
+ navigationController: UINavigationController,
+ tunnelManager: TunnelManager,
+ relayCacheTracker: RelayCacheTracker
+ ) {
+ self.navigationController = navigationController
+ self.tunnelManager = tunnelManager
+ self.relayCacheTracker = relayCacheTracker
+ }
+
+ func start() {
+ let controller = SelectLocationViewController()
+
+ controller.didSelectRelay = { [weak self] _, relay in
+ guard let self = self else { return }
+
+ let newConstraints = RelayConstraints(location: .only(relay))
+
+ self.tunnelManager.setRelayConstraints(newConstraints) {
+ self.tunnelManager.startTunnel()
+ }
+
+ self.didFinish?(self, relay)
+ }
+
+ controller.didFinish = { [weak self] _ in
+ guard let self = self else { return }
+
+ self.didFinish?(self, nil)
+ }
+
+ relayCacheTracker.addObserver(self)
+
+ if let cachedRelays = try? relayCacheTracker.getCachedRelays() {
+ controller.setCachedRelays(cachedRelays)
+ }
+
+ let relayConstraints = tunnelManager.settings.relayConstraints
+
+ controller.setSelectedRelayLocation(
+ relayConstraints.location.value,
+ animated: false,
+ scrollPosition: .middle
+ )
+
+ navigationController.pushViewController(controller, animated: false)
+ }
+
+ func relayCacheTracker(
+ _ tracker: RelayCacheTracker,
+ didUpdateCachedRelays cachedRelays: CachedRelays
+ ) {
+ guard let controller = navigationController.viewControllers
+ .first as? SelectLocationViewController else { return }
+
+ controller.setCachedRelays(cachedRelays)
+ }
+}
diff --git a/ios/MullvadVPN/Coordinators/App/SettingsCoordinator.swift b/ios/MullvadVPN/Coordinators/App/SettingsCoordinator.swift
new file mode 100644
index 0000000000..498a55b7bd
--- /dev/null
+++ b/ios/MullvadVPN/Coordinators/App/SettingsCoordinator.swift
@@ -0,0 +1,193 @@
+//
+// SettingsCoordinator.swift
+// MullvadVPN
+//
+// Created by pronebird on 09/01/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import MullvadLogging
+import Operations
+import SafariServices
+import UIKit
+
+enum SettingsNavigationRoute: Equatable {
+ case root
+ case account
+ case preferences
+ case problemReport
+ case faq
+}
+
+enum SettingsDismissReason: Equatable {
+ case none
+ case userLoggedOut
+}
+
+final class SettingsCoordinator: Coordinator, Presentable, SettingsViewControllerDelegate,
+ AccountViewControllerDelegate, UINavigationControllerDelegate, SFSafariViewControllerDelegate
+{
+ private let logger = Logger(label: "SettingsNavigationCoordinator")
+
+ private let interactorFactory: SettingsInteractorFactory
+ private var currentRoute: SettingsNavigationRoute?
+
+ let navigationController: UINavigationController
+
+ var presentedViewController: UIViewController {
+ return navigationController
+ }
+
+ var willNavigate: ((
+ _ coordinator: SettingsCoordinator,
+ _ from: SettingsNavigationRoute?,
+ _ to: SettingsNavigationRoute
+ ) -> Void)?
+
+ var didFinish: ((SettingsCoordinator, SettingsDismissReason) -> Void)?
+
+ init(
+ navigationController: UINavigationController,
+ interactorFactory: SettingsInteractorFactory
+ ) {
+ self.navigationController = navigationController
+ self.interactorFactory = interactorFactory
+ }
+
+ func start() {
+ navigationController.navigationBar.prefersLargeTitles = true
+ navigationController.delegate = self
+ navigationController.pushViewController(makeViewController(for: .root), animated: false)
+ }
+
+ // MARK: - Navigation
+
+ func navigate(
+ to route: SettingsNavigationRoute,
+ animated: Bool,
+ completion: (() -> Void)? = nil
+ ) {
+ switch route {
+ case .root:
+ navigationController.popToRootViewController(animated: animated)
+
+ case .faq:
+ let safariController = makeViewController(for: route)
+
+ navigationController.present(safariController, animated: true)
+
+ default:
+ let nextViewController = makeViewController(for: route)
+ let viewControllers = navigationController.viewControllers
+
+ if let rootController = viewControllers.first, viewControllers.count > 1 {
+ navigationController.setViewControllers(
+ [rootController, nextViewController],
+ animated: animated
+ )
+ } else {
+ navigationController.pushViewController(nextViewController, animated: animated)
+ }
+ }
+ }
+
+ // MARK: - UINavigationControllerDelegate
+
+ func navigationController(
+ _ navigationController: UINavigationController,
+ willShow viewController: UIViewController,
+ animated: Bool
+ ) {
+ guard let route = route(for: viewController) else { return }
+
+ logger.debug(
+ "Navigate from \(currentRoute.map { "\($0)" } ?? "none") -> \(route)"
+ )
+
+ willNavigate?(self, currentRoute, route)
+
+ currentRoute = route
+ }
+
+ // MARK: - SettingsViewControllerDelegate
+
+ func settingsViewControllerDidFinish(_ controller: SettingsViewController) {
+ didFinish?(self, .none)
+ }
+
+ func settingsViewController(
+ _ controller: SettingsViewController,
+ didRequestRoutePresentation route: SettingsNavigationRoute
+ ) {
+ navigate(to: route, animated: true)
+ }
+
+ // MARK: - AccountViewControllerDelegate
+
+ func accountViewControllerDidLogout(_ controller: AccountViewController) {
+ didFinish?(self, .userLoggedOut)
+ }
+
+ // MARK: - SFSafariViewControllerDelegate
+
+ func safariViewControllerWillOpenInBrowser(_ controller: SFSafariViewController) {
+ controller.dismiss(animated: false)
+ }
+
+ func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
+ controller.dismiss(animated: true)
+ }
+
+ // MARK: - Route mapping
+
+ private func makeViewController(for route: SettingsNavigationRoute) -> UIViewController {
+ switch route {
+ case .root:
+ let controller = SettingsViewController(
+ interactor: interactorFactory.makeSettingsInteractor()
+ )
+ controller.delegate = self
+ return controller
+
+ case .account:
+ let controller = AccountViewController(
+ interactor: interactorFactory.makeAccountInteractor()
+ )
+ controller.delegate = self
+ return controller
+
+ case .preferences:
+ return PreferencesViewController(
+ interactor: interactorFactory.makePreferencesInteractor()
+ )
+
+ case .problemReport:
+ return ProblemReportViewController(
+ interactor: interactorFactory.makeProblemReportInteractor()
+ )
+
+ case .faq:
+ let safariController = SFSafariViewController(
+ url: ApplicationConfiguration
+ .faqAndGuidesURL
+ )
+ safariController.delegate = self
+ return safariController
+ }
+ }
+
+ private func route(for viewController: UIViewController) -> SettingsNavigationRoute? {
+ switch viewController {
+ case is SettingsViewController:
+ return .root
+ case is AccountViewController:
+ return .account
+ case is PreferencesViewController:
+ return .preferences
+ case is ProblemReportViewController:
+ return .problemReport
+ default:
+ return nil
+ }
+ }
+}
diff --git a/ios/MullvadVPN/Coordinators/App/TermsOfServiceCoordinator.swift b/ios/MullvadVPN/Coordinators/App/TermsOfServiceCoordinator.swift
new file mode 100644
index 0000000000..61d0b8ff31
--- /dev/null
+++ b/ios/MullvadVPN/Coordinators/App/TermsOfServiceCoordinator.swift
@@ -0,0 +1,33 @@
+//
+// TermsOfServiceCoordinator.swift
+// MullvadVPN
+//
+// Created by pronebird on 29/01/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import UIKit
+
+class TermsOfServiceCoordinator: Coordinator {
+ private let navigationController: RootContainerViewController
+
+ var didFinish: ((TermsOfServiceCoordinator) -> Void)?
+
+ init(navigationController: RootContainerViewController) {
+ self.navigationController = navigationController
+ }
+
+ func start() {
+ let controller = TermsOfServiceViewController()
+
+ controller.completionHandler = { [weak self] controller in
+ guard let self = self else { return }
+
+ TermsOfService.setAgreed()
+
+ self.didFinish?(self)
+ }
+
+ navigationController.pushViewController(controller, animated: false)
+ }
+}
diff --git a/ios/MullvadVPN/Coordinators/App/TunnelCoordinator.swift b/ios/MullvadVPN/Coordinators/App/TunnelCoordinator.swift
new file mode 100644
index 0000000000..d619f1ebc5
--- /dev/null
+++ b/ios/MullvadVPN/Coordinators/App/TunnelCoordinator.swift
@@ -0,0 +1,65 @@
+//
+// TunnelCoordinator.swift
+// MullvadVPN
+//
+// Created by pronebird on 01/02/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import UIKit
+
+class TunnelCoordinator: Coordinator, NotificationManagerDelegate {
+ private let tunnelManager: TunnelManager
+ private let controller: TunnelViewController
+
+ private var tunnelObserver: TunnelObserver?
+
+ var rootViewController: UIViewController {
+ return controller
+ }
+
+ var showSelectLocationPicker: (() -> Void)?
+
+ init(tunnelManager: TunnelManager) {
+ self.tunnelManager = tunnelManager
+
+ let interactor = TunnelViewControllerInteractor(tunnelManager: tunnelManager)
+ controller = TunnelViewController(interactor: interactor)
+
+ super.init()
+
+ controller.shouldShowSelectLocationPicker = { [weak self] in
+ self?.showSelectLocationPicker?()
+ }
+ }
+
+ func start() {
+ let tunnelObserver =
+ TunnelBlockObserver(didUpdateDeviceState: { [weak self] _, deviceState in
+ self?.updateVisibility(animated: true)
+ })
+
+ self.tunnelObserver = tunnelObserver
+
+ tunnelManager.addObserver(tunnelObserver)
+
+ updateVisibility(animated: false)
+
+ NotificationManager.shared.delegate = self
+ }
+
+ private func updateVisibility(animated: Bool) {
+ let deviceState = tunnelManager.deviceState
+
+ controller.setMainContentHidden(!deviceState.isLoggedIn, animated: animated)
+ }
+
+ // MARK: - NotificationManagerDelegate
+
+ func notificationManagerDidUpdateInAppNotifications(
+ _ manager: NotificationManager,
+ notifications: [InAppNotificationDescriptor]
+ ) {
+ controller.notificationController.setNotifications(notifications, animated: true)
+ }
+}
diff --git a/ios/MullvadVPN/Coordinators/Base/Coordinator.swift b/ios/MullvadVPN/Coordinators/Base/Coordinator.swift
new file mode 100644
index 0000000000..c6cd67138c
--- /dev/null
+++ b/ios/MullvadVPN/Coordinators/Base/Coordinator.swift
@@ -0,0 +1,163 @@
+//
+// Coordinator.swift
+// MullvadVPN
+//
+// Created by pronebird on 27/01/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import MullvadLogging
+import UIKit
+
+/**
+ Base coordinator class.
+
+ Coordinators help to abstract the navigation and business logic from view controllers making them
+ more manageable and reusable.
+ */
+class Coordinator: NSObject {
+ /// Private trace log.
+ private lazy var logger = Logger(label: "\(Self.self)")
+
+ /// Weak reference to parent coordinator.
+ private weak var _parent: Coordinator?
+
+ /// Mutable collection of child coordinators.
+ private var _children: [Coordinator] = []
+
+ /// Modal presentation configuration assigned on presented coordinator.
+ fileprivate var modalConfiguration: ModalPresentationConfiguration?
+
+ /// An array of blocks that are invoked upon interactive dismissal.
+ fileprivate var interactiveDismissalObservers: [(Coordinator) -> Void] = []
+
+ /// Child coordinators.
+ var childCoordinators: [Coordinator] {
+ return _children
+ }
+
+ /// Parent coordinator.
+ var parent: Coordinator? {
+ return _parent
+ }
+
+ // MARK: - Children
+
+ /**
+ Add child coordinator.
+
+ Adding the same coordinator twice is a no-op.
+ */
+ func addChild(_ child: Coordinator) {
+ guard !_children.contains(child) else { return }
+
+ _children.append(child)
+ child._parent = self
+
+ logger.trace("Add child \(child)")
+ }
+
+ /**
+ Remove child coordinator.
+
+ Removing coordinator that's no longer a child of this coordinator is a no-op.
+ */
+ func removeChild(_ child: Coordinator) {
+ guard let index = _children.firstIndex(where: { $0 == child }) else { return }
+
+ _children.remove(at: index)
+ child._parent = nil
+
+ logger.trace("Remove child \(child)")
+ }
+
+ /**
+ Remove coordinator from its parent.
+ */
+ func removeFromParent() {
+ _parent?.removeChild(self)
+ }
+}
+
+/**
+ Protocol describing coordinators that can be presented using modal presentation.
+ */
+protocol Presentable: Coordinator {
+ /**
+ View controller that is presented modally. It's expected it to be the top-most view controller
+ managed by coordinator.
+ */
+ var presentedViewController: UIViewController { get }
+}
+
+/**
+ Protocol describing coordinators that provide modal presentation context.
+ */
+protocol Presenting: Coordinator {
+ /**
+ View controller providing modal presentation context.
+ */
+ var presentationContext: UIViewController { get }
+}
+
+extension Presenting {
+ /**
+ Present child coordinator.
+
+ Automatically adds child and removes it upon interactive dismissal.
+ */
+ func presentChild<T: Presentable>(
+ _ child: T,
+ animated: Bool,
+ configuration: ModalPresentationConfiguration = ModalPresentationConfiguration(),
+ completion: (() -> Void)? = nil
+ ) {
+ var configuration = configuration
+
+ configuration.notifyInteractiveDismissal { [weak child] in
+ guard let child = child else { return }
+
+ child.modalConfiguration = nil
+ child.removeFromParent()
+
+ let observers = child.interactiveDismissalObservers
+ child.interactiveDismissalObservers = []
+
+ for observer in observers {
+ observer(child)
+ }
+ }
+
+ configuration.apply(to: child.presentedViewController)
+
+ child.modalConfiguration = configuration
+
+ addChild(child)
+
+ presentationContext.present(
+ child.presentedViewController,
+ animated: animated,
+ completion: completion
+ )
+ }
+}
+
+extension Presentable {
+ /**
+ Dismiss this coordinator.
+
+ Automatically removes itself from parent.
+ */
+ func dismiss(animated: Bool, completion: (() -> Void)? = nil) {
+ removeFromParent()
+
+ presentedViewController.dismiss(animated: animated, completion: completion)
+ }
+
+ /**
+ Add block based observer triggered if coordinator is dismissed via user interaction.
+ */
+ func onInteractiveDismissal(_ handler: @escaping (Coordinator) -> Void) {
+ interactiveDismissalObservers.append(handler)
+ }
+}
diff --git a/ios/MullvadVPN/Coordinators/Base/ModalPresentationConfiguration.swift b/ios/MullvadVPN/Coordinators/Base/ModalPresentationConfiguration.swift
new file mode 100644
index 0000000000..da99738669
--- /dev/null
+++ b/ios/MullvadVPN/Coordinators/Base/ModalPresentationConfiguration.swift
@@ -0,0 +1,52 @@
+//
+// ModalPresentationConfiguration.swift
+// MullvadVPN
+//
+// Created by pronebird on 14/03/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import UIKit
+
+/**
+ A struct holding modal presentation configuration.
+ */
+struct ModalPresentationConfiguration {
+ var preferredContentSize: CGSize?
+ var modalPresentationStyle: UIModalPresentationStyle?
+ var isModalInPresentation: Bool?
+ var transitioningDelegate: UIViewControllerTransitioningDelegate?
+ var presentationControllerDelegate: UIAdaptivePresentationControllerDelegate?
+
+ func apply(to vc: UIViewController) {
+ vc.transitioningDelegate = transitioningDelegate
+
+ if let modalPresentationStyle = modalPresentationStyle {
+ vc.modalPresentationStyle = modalPresentationStyle
+ }
+
+ if let preferredContentSize = preferredContentSize {
+ vc.preferredContentSize = preferredContentSize
+ }
+
+ if let isModalInPresentation = isModalInPresentation {
+ vc.isModalInPresentation = isModalInPresentation
+ }
+
+ vc.presentationController?.delegate = presentationControllerDelegate
+ }
+
+ /**
+ Wraps `presentationControllerDelegate` into forwarding delegate that intercepts interactive
+ dismissal and calls `dismissalHandler` while proxying all delegate calls to the former
+ delegate.
+ */
+ mutating func notifyInteractiveDismissal(_ dismissalHandler: @escaping () -> Void) {
+ presentationControllerDelegate =
+ PresentationControllerDismissalInterceptor(
+ forwardingTarget: presentationControllerDelegate
+ ) { _ in
+ dismissalHandler()
+ }
+ }
+}
diff --git a/ios/MullvadVPN/Coordinators/Base/PresentationControllerDismissalInterceptor.swift b/ios/MullvadVPN/Coordinators/Base/PresentationControllerDismissalInterceptor.swift
new file mode 100644
index 0000000000..89951f0665
--- /dev/null
+++ b/ios/MullvadVPN/Coordinators/Base/PresentationControllerDismissalInterceptor.swift
@@ -0,0 +1,78 @@
+//
+// PresentationControllerDismissalInterceptor.swift
+// MullvadVPN
+//
+// Created by pronebird on 20/02/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import UIKit
+
+/**
+ Presentation controller delegate class that intercepts interactive dismissal and calls
+ `dismissHandler` closure. Forwards all delegate calls to the `forwardingTarget`.
+ */
+final class PresentationControllerDismissalInterceptor: NSObject,
+ UIAdaptivePresentationControllerDelegate
+{
+ private let dismissHandler: (UIPresentationController) -> Void
+ private let forwardingTarget: UIAdaptivePresentationControllerDelegate?
+ private let protocolSelectors: [Selector]
+
+ init(
+ forwardingTarget: UIAdaptivePresentationControllerDelegate?,
+ dismissHandler: @escaping (UIPresentationController) -> Void
+ ) {
+ self.forwardingTarget = forwardingTarget
+ self.dismissHandler = dismissHandler
+
+ protocolSelectors = getProtocolMethods(
+ UIAdaptivePresentationControllerDelegate.self,
+ isRequired: false,
+ isInstanceMethod: true
+ )
+ }
+
+ override func responds(to aSelector: Selector!) -> Bool {
+ return super.responds(to: aSelector) || (
+ protocolSelectors.contains(aSelector) &&
+ forwardingTarget?.responds(to: aSelector) ?? false
+ )
+ }
+
+ override func forwardingTarget(for aSelector: Selector!) -> Any? {
+ if protocolSelectors.contains(aSelector) {
+ if super.responds(to: aSelector) {
+ return nil
+ } else if forwardingTarget?.responds(to: aSelector) ?? false {
+ return forwardingTarget
+ }
+ }
+ return super.forwardingTarget(for: aSelector)
+ }
+
+ func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
+ dismissHandler(presentationController)
+ forwardingTarget?.presentationControllerDidDismiss?(presentationController)
+ }
+}
+
+private func getProtocolMethods(
+ _ protocolType: Protocol,
+ isRequired: Bool,
+ isInstanceMethod: Bool
+) -> [Selector] {
+ var methodCount: UInt32 = 0
+ let methodDescriptions = protocol_copyMethodDescriptionList(
+ protocolType,
+ isRequired,
+ isInstanceMethod,
+ &methodCount
+ )
+
+ defer { methodDescriptions.map { free($0) } }
+
+ return (0 ..< methodCount).compactMap { index in
+ return methodDescriptions?[Int(index)].name
+ }
+}
diff --git a/ios/MullvadVPN/Bundle+ProductVersion.swift b/ios/MullvadVPN/Extensions/Bundle+ProductVersion.swift
index a3631c1872..a3631c1872 100644
--- a/ios/MullvadVPN/Bundle+ProductVersion.swift
+++ b/ios/MullvadVPN/Extensions/Bundle+ProductVersion.swift
diff --git a/ios/MullvadVPN/CharacterSet+IPAddress.swift b/ios/MullvadVPN/Extensions/CharacterSet+IPAddress.swift
index 2131398cc9..2131398cc9 100644
--- a/ios/MullvadVPN/CharacterSet+IPAddress.swift
+++ b/ios/MullvadVPN/Extensions/CharacterSet+IPAddress.swift
diff --git a/ios/MullvadVPN/CodingErrors+CustomErrorDescription.swift b/ios/MullvadVPN/Extensions/CodingErrors+CustomErrorDescription.swift
index 2ab52cb8c0..2ab52cb8c0 100644
--- a/ios/MullvadVPN/CodingErrors+CustomErrorDescription.swift
+++ b/ios/MullvadVPN/Extensions/CodingErrors+CustomErrorDescription.swift
diff --git a/ios/MullvadVPN/NEVPNStatus+Debug.swift b/ios/MullvadVPN/Extensions/NEVPNStatus+Debug.swift
index 37520fb3d5..49320084d4 100644
--- a/ios/MullvadVPN/NEVPNStatus+Debug.swift
+++ b/ios/MullvadVPN/Extensions/NEVPNStatus+Debug.swift
@@ -6,7 +6,6 @@
// Copyright © 2019 Mullvad VPN AB. All rights reserved.
//
-import Foundation
import NetworkExtension
extension NEVPNStatus: CustomStringConvertible {
diff --git a/ios/MullvadVPN/NSAttributedString+Markdown.swift b/ios/MullvadVPN/Extensions/NSAttributedString+Markdown.swift
index 5e7c064b9d..5e7c064b9d 100644
--- a/ios/MullvadVPN/NSAttributedString+Markdown.swift
+++ b/ios/MullvadVPN/Extensions/NSAttributedString+Markdown.swift
diff --git a/ios/MullvadVPN/NSLayoutConstraint+Helpers.swift b/ios/MullvadVPN/Extensions/NSLayoutConstraint+Helpers.swift
index 3a41101a96..3a41101a96 100644
--- a/ios/MullvadVPN/NSLayoutConstraint+Helpers.swift
+++ b/ios/MullvadVPN/Extensions/NSLayoutConstraint+Helpers.swift
diff --git a/ios/MullvadVPN/NSRegularExpression+IPAddress.swift b/ios/MullvadVPN/Extensions/NSRegularExpression+IPAddress.swift
index 30e698f9c4..30e698f9c4 100644
--- a/ios/MullvadVPN/NSRegularExpression+IPAddress.swift
+++ b/ios/MullvadVPN/Extensions/NSRegularExpression+IPAddress.swift
diff --git a/ios/MullvadVPN/RESTCreateApplePaymentResponse+Localization.swift b/ios/MullvadVPN/Extensions/RESTCreateApplePaymentResponse+Localization.swift
index 02b9eb06fd..02b9eb06fd 100644
--- a/ios/MullvadVPN/RESTCreateApplePaymentResponse+Localization.swift
+++ b/ios/MullvadVPN/Extensions/RESTCreateApplePaymentResponse+Localization.swift
diff --git a/ios/MullvadVPN/RESTError+Display.swift b/ios/MullvadVPN/Extensions/RESTError+Display.swift
index e943fc7a7e..55347f774b 100644
--- a/ios/MullvadVPN/RESTError+Display.swift
+++ b/ios/MullvadVPN/Extensions/RESTError+Display.swift
@@ -27,21 +27,31 @@ extension REST.Error: DisplayError {
case let .unhandledResponse(statusCode, serverResponse):
guard let serverResponse = serverResponse else {
return String(format: NSLocalizedString(
- "INVALID_ACCOUNT_ERROR",
+ "UNEXPECTED_RESPONSE",
tableName: "REST",
value: "Unexpected server response: %d",
comment: ""
), statusCode)
}
- if serverResponse.code == .invalidAccount {
+ switch serverResponse.code {
+ case .invalidAccount:
return NSLocalizedString(
"INVALID_ACCOUNT_ERROR",
tableName: "REST",
value: "Invalid account",
comment: ""
)
- } else {
+
+ case .maxDevicesReached:
+ return NSLocalizedString(
+ "MAX_DEVICES_REACHED_ERROR",
+ tableName: "REST",
+ value: "Too many devices registered with account",
+ comment: ""
+ )
+
+ default:
return String(
format: NSLocalizedString(
"SERVER_ERROR",
diff --git a/ios/MullvadVPN/Result+Extensions.swift b/ios/MullvadVPN/Extensions/Result+Extensions.swift
index 204df0714e..204df0714e 100644
--- a/ios/MullvadVPN/Result+Extensions.swift
+++ b/ios/MullvadVPN/Extensions/Result+Extensions.swift
diff --git a/ios/MullvadVPN/SKError+Localized.swift b/ios/MullvadVPN/Extensions/SKError+Localized.swift
index 6367877fc4..6367877fc4 100644
--- a/ios/MullvadVPN/SKError+Localized.swift
+++ b/ios/MullvadVPN/Extensions/SKError+Localized.swift
diff --git a/ios/MullvadVPN/SKProduct+Formatting.swift b/ios/MullvadVPN/Extensions/SKProduct+Formatting.swift
index b5bf461ef6..b5bf461ef6 100644
--- a/ios/MullvadVPN/SKProduct+Formatting.swift
+++ b/ios/MullvadVPN/Extensions/SKProduct+Formatting.swift
diff --git a/ios/MullvadVPN/StorePaymentManagerError+Display.swift b/ios/MullvadVPN/Extensions/StorePaymentManagerError+Display.swift
index a2adb551ad..a2adb551ad 100644
--- a/ios/MullvadVPN/StorePaymentManagerError+Display.swift
+++ b/ios/MullvadVPN/Extensions/StorePaymentManagerError+Display.swift
diff --git a/ios/MullvadVPN/Extensions/String+AccountFormatting.swift b/ios/MullvadVPN/Extensions/String+AccountFormatting.swift
new file mode 100644
index 0000000000..a13446948b
--- /dev/null
+++ b/ios/MullvadVPN/Extensions/String+AccountFormatting.swift
@@ -0,0 +1,15 @@
+//
+// String+AccountFormatting.swift
+// MullvadVPN
+//
+// Created by Andreas Lif on 2022-06-10.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+extension String {
+ var formattedAccountNumber: String {
+ return split(every: 4).joined(separator: " ")
+ }
+}
diff --git a/ios/MullvadVPN/String+Split.swift b/ios/MullvadVPN/Extensions/String+Split.swift
index f62317343b..f62317343b 100644
--- a/ios/MullvadVPN/String+Split.swift
+++ b/ios/MullvadVPN/Extensions/String+Split.swift
diff --git a/ios/MullvadVPN/UIBarButtonItem+KeyboardNavigation.swift b/ios/MullvadVPN/Extensions/UIBarButtonItem+KeyboardNavigation.swift
index ca17e2510a..ca17e2510a 100644
--- a/ios/MullvadVPN/UIBarButtonItem+KeyboardNavigation.swift
+++ b/ios/MullvadVPN/Extensions/UIBarButtonItem+KeyboardNavigation.swift
diff --git a/ios/MullvadVPN/UIColor+Helpers.swift b/ios/MullvadVPN/Extensions/UIColor+Helpers.swift
index a119f64660..a119f64660 100644
--- a/ios/MullvadVPN/UIColor+Helpers.swift
+++ b/ios/MullvadVPN/Extensions/UIColor+Helpers.swift
diff --git a/ios/MullvadVPN/ModalRootAdaptivePresentationDelegate.swift b/ios/MullvadVPN/ModalRootAdaptivePresentationDelegate.swift
deleted file mode 100644
index 9aab78da2e..0000000000
--- a/ios/MullvadVPN/ModalRootAdaptivePresentationDelegate.swift
+++ /dev/null
@@ -1,142 +0,0 @@
-//
-// ModalRootAdaptivePresentationDelegate.swift
-// MullvadVPN
-//
-// Created by pronebird on 19/01/2023.
-// Copyright © 2023 Mullvad VPN AB. All rights reserved.
-//
-
-import UIKit
-
-/**
-
- Adaptive presentation delegate for `SceneDelegate.modalRootContainer` used for presenting
- the login flow on iPad.
-
- The primary purpose of this class is to swap between fullscreen and formsheet presentation based
- on horizontal size class and make settings (cog) accessible even when parent root is overlayed with
- modal root.
-
- Unlike iPhone where only one `RootContainerViewController` is used and behaves very much like
- navigation controller, iPad uses two of such controllers defined as parent and (think child) modal
- within this class.
-
- ## iPhone view controller hierarchy
-
- - UIWindow
- - RootContainerViewController
- - LoginViewController
- - etc.
-
- ## iPad view controller hierarchy
-
- - UIWindow
- - RootContainerViewController (parent)
- - UISplitViewController
- - TunnelViewController
- - SelectLocationViewController
- - RootContainerViewController (child [modal])
- - LoginViewController
- - etc.
-
- */
-final class ModalRootAdaptivePresentationDelegate: NSObject,
- UIAdaptivePresentationControllerDelegate
-{
- let parentRootContainer: RootContainerViewController
- let modalRootContainer: RootContainerViewController
-
- init(
- parentRootContainer: RootContainerViewController,
- modalRootContainer: RootContainerViewController
- ) {
- self.parentRootContainer = parentRootContainer
- self.modalRootContainer = modalRootContainer
-
- super.init()
-
- NotificationCenter.default.addObserver(
- self,
- selector: #selector(dismissalTransitionDidEnd(_:)),
- name: UIPresentationController.dismissalTransitionDidEndNotification,
- object: modalRootContainer
- )
- }
-
- private func finishPresentation() {
- parentRootContainer.removeSettingsButtonFromPresentationContainer()
- }
-
- func adaptivePresentationStyle(
- for controller: UIPresentationController,
- traitCollection: UITraitCollection
- ) -> UIModalPresentationStyle {
- return traitCollection.horizontalSizeClass == .regular ? .formSheet : .fullScreen
- }
-
- func presentationController(
- _ presentationController: UIPresentationController,
- willPresentWithAdaptiveStyle style: UIModalPresentationStyle,
- transitionCoordinator: UIViewControllerTransitionCoordinator?
- ) {
- // The style is set to none when adaptive presentation is not changing.
- let actualStyle: UIModalPresentationStyle = style == .none
- ? presentationController.presentedViewController.modalPresentationStyle
- : style
-
- // Force hide header bar in .formSheet presentation and show it in .fullScreen presentation
- modalRootContainer.setOverrideHeaderBarHidden(actualStyle == .formSheet, animated: false)
-
- let transitionActions = {
- if let containerView = self.modalRootContainer.modalPresentationContainerView {
- self.parentRootContainer.addSettingsButtonToPresentationContainer(containerView)
- }
- }
-
- if actualStyle == .formSheet {
- // Add settings button into the modal container to make it accessible by users
- if let transitionCoordinator = transitionCoordinator {
- transitionCoordinator.animate { _ in
- transitionActions()
- }
- } else {
- transitionActions()
- }
- } else {
- // Move settings button back into header bar
- finishPresentation()
- }
- }
-
- func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
- finishPresentation()
- }
-
- @objc private func dismissalTransitionDidEnd(_ notification: Notification) {
- guard let isCompleted = notification
- .userInfo?[
- UIPresentationController
- .dismissalTransitionDidEndCompletedUserInfoKey
- ] as? NSNumber else { return }
-
- if isCompleted.boolValue {
- finishPresentation()
- }
- }
-}
-
-private extension UIViewController {
- /// Returns private `UITransitionView` used by UIKit that acts as a container view for modally
- /// presented controllers. When implementing a presentation controller subclass, this view
- /// is the one that is used to add additional decorations.
- var modalPresentationContainerView: UIView? {
- var currentView = view
- let iterator = AnyIterator { () -> UIView? in
- currentView = currentView?.superview
- return currentView
- }
- return iterator.first { view -> Bool in
- return view.description.starts(with: "<UITransitionView")
- }
- }
-}
diff --git a/ios/MullvadVPN/Presentation controllers/FormsheetPresentationController.swift b/ios/MullvadVPN/Presentation controllers/FormsheetPresentationController.swift
new file mode 100644
index 0000000000..047fd84bca
--- /dev/null
+++ b/ios/MullvadVPN/Presentation controllers/FormsheetPresentationController.swift
@@ -0,0 +1,216 @@
+//
+// FormsheetPresentationController.swift
+// MullvadVPN
+//
+// Created by pronebird on 18/02/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import UIKit
+
+private let dimmingViewOpacity: CGFloat = 0.5
+private let presentedViewCornerRadius: CGFloat = 8
+private let animationDuration: TimeInterval = 0.5
+
+/**
+ Custom implementation of a formsheet presentation controller.
+ */
+class FormsheetPresentationController: UIPresentationController {
+ private let dimmingView: UIView = {
+ let dimmingView = UIView()
+ dimmingView.backgroundColor = .black
+ return dimmingView
+ }()
+
+ override var shouldRemovePresentersView: Bool {
+ return false
+ }
+
+ /**
+ Flag indicating whether presentation controller should use fullscreen presentation when in
+ compact width environment
+ */
+ var useFullScreenPresentationInCompactWidth = false
+
+ /**
+ Returns `true` if presentation controller is in fullscreen presentation.
+ */
+ var isInFullScreenPresentation: Bool {
+ return useFullScreenPresentationInCompactWidth &&
+ traitCollection.horizontalSizeClass == .compact
+ }
+
+ override var frameOfPresentedViewInContainerView: CGRect {
+ guard let containerView = containerView else {
+ return super.frameOfPresentedViewInContainerView
+ }
+
+ if isInFullScreenPresentation {
+ return containerView.bounds
+ }
+
+ let preferredContentSize = presentedViewController.preferredContentSize
+
+ assert(preferredContentSize.width > 0 && preferredContentSize.height > 0)
+
+ return CGRect(
+ origin: CGPoint(
+ x: containerView.bounds.midX - preferredContentSize.width * 0.5,
+ y: containerView.bounds.midY - preferredContentSize.height * 0.5
+ ),
+ size: preferredContentSize
+ )
+ }
+
+ override func containerViewWillLayoutSubviews() {
+ dimmingView.frame = containerView?.bounds ?? .zero
+ presentedView?.frame = frameOfPresentedViewInContainerView
+ }
+
+ override func presentationTransitionWillBegin() {
+ dimmingView.alpha = 0
+ containerView?.addSubview(dimmingView)
+
+ presentedView?.layer.cornerRadius = presentedViewCornerRadius
+ presentedView?.clipsToBounds = true
+
+ let revealDimmingView = {
+ self.dimmingView.alpha = dimmingViewOpacity
+ }
+
+ if let transitionCoordinator = presentingViewController.transitionCoordinator {
+ transitionCoordinator.animate { context in
+ revealDimmingView()
+ }
+ } else {
+ revealDimmingView()
+ }
+ }
+
+ override func presentationTransitionDidEnd(_ completed: Bool) {
+ if !completed {
+ dimmingView.removeFromSuperview()
+ }
+ }
+
+ override func dismissalTransitionWillBegin() {
+ let fadeDimmingView = {
+ self.dimmingView.alpha = 0
+ }
+
+ if let transitionCoordinator = presentingViewController.transitionCoordinator {
+ transitionCoordinator.animate { context in
+ fadeDimmingView()
+ }
+ } else {
+ fadeDimmingView()
+ }
+ }
+
+ override func dismissalTransitionDidEnd(_ completed: Bool) {
+ if completed {
+ dimmingView.removeFromSuperview()
+ }
+ }
+}
+
+class FormsheetTransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate {
+ func animationController(
+ forPresented presented: UIViewController,
+ presenting: UIViewController,
+ source: UIViewController
+ ) -> UIViewControllerAnimatedTransitioning? {
+ return FormsheetPresentationAnimator()
+ }
+
+ func animationController(forDismissed dismissed: UIViewController)
+ -> UIViewControllerAnimatedTransitioning?
+ {
+ return FormsheetPresentationAnimator()
+ }
+
+ func presentationController(
+ forPresented presented: UIViewController,
+ presenting: UIViewController?,
+ source: UIViewController
+ ) -> UIPresentationController? {
+ return FormsheetPresentationController(
+ presentedViewController: presented,
+ presenting: source
+ )
+ }
+}
+
+class FormsheetPresentationAnimator: NSObject, UIViewControllerAnimatedTransitioning {
+ func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?)
+ -> TimeInterval
+ {
+ return (transitionContext?.isAnimated ?? true) ? animationDuration : 0
+ }
+
+ func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
+ let destination = transitionContext.viewController(forKey: .to)
+
+ if destination?.isBeingPresented ?? false {
+ animatePresentation(transitionContext)
+ } else {
+ animateDismissal(transitionContext)
+ }
+ }
+
+ private func animatePresentation(_ transitionContext: UIViewControllerContextTransitioning) {
+ let duration = transitionDuration(using: transitionContext)
+ let containerView = transitionContext.containerView
+ let destinationView = transitionContext.view(forKey: .to)!
+ let destinationController = transitionContext.viewController(forKey: .to)!
+
+ containerView.addSubview(destinationView)
+
+ var initialFrame = transitionContext.finalFrame(for: destinationController)
+ initialFrame.origin.y = containerView.bounds.maxY
+ destinationView.frame = initialFrame
+
+ if transitionContext.isAnimated {
+ UIView.animate(
+ withDuration: duration,
+ delay: 0,
+ options: [.curveEaseInOut],
+ animations: {
+ destinationView.frame = transitionContext.finalFrame(for: destinationController)
+ },
+ completion: { _ in
+ transitionContext.completeTransition(true)
+ }
+ )
+ } else {
+ destinationView.frame = transitionContext.finalFrame(for: destinationController)
+ }
+ }
+
+ private func animateDismissal(_ transitionContext: UIViewControllerContextTransitioning) {
+ let duration = transitionDuration(using: transitionContext)
+ let containerView = transitionContext.containerView
+ let sourceView = transitionContext.view(forKey: .from)!
+ let sourceController = transitionContext.viewController(forKey: .from)!
+
+ var initialFrame = transitionContext.finalFrame(for: sourceController)
+ initialFrame.origin.y = containerView.bounds.maxY
+
+ if transitionContext.isAnimated {
+ UIView.animate(
+ withDuration: duration,
+ delay: 0,
+ options: [.curveEaseInOut],
+ animations: {
+ sourceView.frame = initialFrame
+ },
+ completion: { _ in
+ transitionContext.completeTransition(true)
+ }
+ )
+ } else {
+ sourceView.frame = initialFrame
+ transitionContext.completeTransition(true)
+ }
+ }
+}
diff --git a/ios/MullvadVPN/Presentation controllers/SecondaryContextPresentationController.swift b/ios/MullvadVPN/Presentation controllers/SecondaryContextPresentationController.swift
new file mode 100644
index 0000000000..6a0397efa2
--- /dev/null
+++ b/ios/MullvadVPN/Presentation controllers/SecondaryContextPresentationController.swift
@@ -0,0 +1,67 @@
+//
+// SecondaryContextPresentationController.swift
+// MullvadVPN
+//
+// Created by pronebird on 18/02/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import UIKit
+
+/**
+ This is a presentation controller class used for presentation of secondary navigation context
+ in application coordinator.
+ */
+class SecondaryContextPresentationController: FormsheetPresentationController {
+ override func presentationTransitionWillBegin() {
+ super.presentationTransitionWillBegin()
+
+ updateHeaderBarHidden()
+
+ if let containerView = containerView,
+ let rootContainer = presentingViewController as? RootContainerViewController
+ {
+ rootContainer.addSettingsButtonToPresentationContainer(containerView)
+ }
+ }
+
+ override func dismissalTransitionDidEnd(_ completed: Bool) {
+ super.dismissalTransitionDidEnd(completed)
+
+ if let rootContainer = presentingViewController as? RootContainerViewController, completed {
+ rootContainer.removeSettingsButtonFromPresentationContainer()
+ }
+ }
+
+ override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
+ super.traitCollectionDidChange(previousTraitCollection)
+
+ updateHeaderBarHidden()
+ }
+
+ private func updateHeaderBarHidden() {
+ let presentedController = presentedViewController as? RootContainerViewController
+
+ presentedController?.setOverrideHeaderBarHidden(
+ isInFullScreenPresentation ? nil : true,
+ animated: false
+ )
+ }
+}
+
+class SecondaryContextTransitioningDelegate: FormsheetTransitioningDelegate {
+ override func presentationController(
+ forPresented presented: UIViewController,
+ presenting: UIViewController?,
+ source: UIViewController
+ ) -> UIPresentationController? {
+ let presentationController = SecondaryContextPresentationController(
+ presentedViewController: presented,
+ presenting: source
+ )
+
+ presentationController.useFullScreenPresentationInCompactWidth = true
+
+ return presentationController
+ }
+}
diff --git a/ios/MullvadVPN/CellFactoryProtocol.swift b/ios/MullvadVPN/Protocols/CellFactoryProtocol.swift
index 7bf3cbda21..7bf3cbda21 100644
--- a/ios/MullvadVPN/CellFactoryProtocol.swift
+++ b/ios/MullvadVPN/Protocols/CellFactoryProtocol.swift
diff --git a/ios/MullvadVPN/SettingsMigrationUIHandler.swift b/ios/MullvadVPN/Protocols/SettingsMigrationUIHandler.swift
index 949458e394..949458e394 100644
--- a/ios/MullvadVPN/SettingsMigrationUIHandler.swift
+++ b/ios/MullvadVPN/Protocols/SettingsMigrationUIHandler.swift
diff --git a/ios/MullvadVPN/SceneDelegate.swift b/ios/MullvadVPN/SceneDelegate.swift
index 92e3867dcd..689d904be8 100644
--- a/ios/MullvadVPN/SceneDelegate.swift
+++ b/ios/MullvadVPN/SceneDelegate.swift
@@ -13,83 +13,48 @@ import Operations
import RelayCache
import UIKit
-class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDelegate,
- RootContainerViewControllerDelegate, LoginViewControllerDelegate,
- DeviceManagementViewControllerDelegate, SettingsNavigationControllerDelegate,
- OutOfTimeViewControllerDelegate, SelectLocationViewControllerDelegate,
- RevokedDeviceViewControllerDelegate, NotificationManagerDelegate, TunnelObserver,
- RelayCacheTrackerObserver, SettingsMigrationUIHandler
-{
+class SceneDelegate: UIResponder, UIWindowSceneDelegate, SettingsMigrationUIHandler {
private let logger = Logger(label: "SceneDelegate")
var window: UIWindow?
private var privacyOverlayWindow: UIWindow?
private var isSceneConfigured = false
- private let rootContainer = RootContainerViewController()
-
- // Modal root container is used on iPad to present login, TOS, revoked device, device management
- // view controllers above `rootContainer` which only contains split controller.
- private lazy var modalRootContainer = RootContainerViewController()
- private lazy var modalRootAdaptivePresentationDelegate = ModalRootAdaptivePresentationDelegate(
- parentRootContainer: rootContainer,
- modalRootContainer: modalRootContainer
- )
-
- private var splitViewController: CustomSplitViewController?
- private var selectLocationViewController: SelectLocationViewController?
- private var tunnelViewController: TunnelViewController?
- private weak var settingsNavController: SettingsNavigationController?
- private var lastLoginAction: LoginAction?
-
+ private var appCoordinator: ApplicationCoordinator?
private var accountDataThrottling: AccountDataThrottling?
private var deviceDataThrottling: DeviceDataThrottling?
- private var outOfTimeTimer: Timer?
+ private var tunnelObserver: TunnelObserver?
private var appDelegate: AppDelegate {
return UIApplication.shared.delegate as! AppDelegate
}
- private var storePaymentManager: StorePaymentManager {
- return appDelegate.storePaymentManager
- }
-
- private var relayCacheTracker: RelayCacheTracker {
- return appDelegate.relayCacheTracker
- }
-
private var tunnelManager: TunnelManager {
return appDelegate.tunnelManager
}
- private var apiProxy: REST.APIProxy {
- return appDelegate.apiProxy
- }
+ // MARK: - Deep link
- private var devicesProxy: REST.DevicesProxy {
- return appDelegate.devicesProxy
+ func showUserAccount() {
+ appCoordinator?.showAccountSettings()
}
- deinit {
- clearOutOfTimeTimer()
- }
+ // MARK: - Private
- var isShowingOutOfTimeView: Bool {
- switch UIDevice.current.userInterfaceIdiom {
- case .pad:
- return modalRootContainer.viewControllers
- .contains(where: { $0 is OutOfTimeViewController })
- case .phone:
- return rootContainer.viewControllers
- .contains(where: { $0 is OutOfTimeViewController })
- default:
- return false
- }
- }
+ private func addTunnelObserver() {
+ let tunnelObserver = TunnelBlockObserver(
+ didLoadConfiguration: { [weak self] _ in
+ self?.configureScene()
+ },
+ didUpdateDeviceState: { [weak self] _, deviceState in
+ self?.deviceStateDidChange(deviceState)
+ }
+ )
- func showUserAccount() {
- rootContainer.showSettings(navigateTo: .account, animated: true)
+ self.tunnelObserver = tunnelObserver
+
+ tunnelManager.addObserver(tunnelObserver)
}
private func configureScene() {
@@ -99,23 +64,23 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDe
accountDataThrottling = AccountDataThrottling(tunnelManager: tunnelManager)
deviceDataThrottling = DeviceDataThrottling(tunnelManager: tunnelManager)
+ refreshDeviceAndAccountData(forceUpdate: true)
- rootContainer.delegate = self
- window?.rootViewController = rootContainer
+ appCoordinator = ApplicationCoordinator(
+ tunnelManager: tunnelManager,
+ storePaymentManager: appDelegate.storePaymentManager,
+ relayCacheTracker: appDelegate.relayCacheTracker,
+ apiProxy: appDelegate.apiProxy,
+ devicesProxy: appDelegate.devicesProxy
+ )
- switch UIDevice.current.userInterfaceIdiom {
- case .pad:
- setupPadUI()
- case .phone:
- setupPhoneUI()
- default:
- fatalError()
+ appCoordinator?.onShowSettings = { [weak self] in
+ // Refresh account data each time user opens settings
+ self?.refreshDeviceAndAccountData(forceUpdate: true)
}
- relayCacheTracker.addObserver(self)
- NotificationManager.shared.delegate = self
-
- refreshDeviceAndAccountData(forceUpdate: true)
+ window?.rootViewController = appCoordinator?.rootViewController
+ appCoordinator?.start()
}
private func setShowsPrivacyOverlay(_ showOverlay: Bool) {
@@ -128,11 +93,29 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDe
}
}
+ private func deviceStateDidChange(_ deviceState: DeviceState) {
+ switch deviceState {
+ case .loggedOut:
+ resetDeviceAndAccountDataThrottling()
+
+ case .revoked:
+ resetDeviceAndAccountDataThrottling()
+
+ case .loggedIn:
+ break
+ }
+ }
+
private func refreshDeviceAndAccountData(forceUpdate: Bool) {
- let condition: AccountDataThrottling.Condition =
- settingsNavController == nil && !forceUpdate
- ? .whenCloseToExpiryAndBeyond
- : .always
+ let isPresentingSettings = appCoordinator?.isPresentingSettings ?? false
+
+ let condition: AccountDataThrottling.Condition
+
+ if forceUpdate {
+ condition = .always
+ } else {
+ condition = isPresentingSettings ? .always : .whenCloseToExpiryAndBeyond
+ }
accountDataThrottling?.requestUpdate(condition: condition)
deviceDataThrottling?.requestUpdate(forceUpdate: forceUpdate)
@@ -143,24 +126,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDe
deviceDataThrottling?.reset()
}
- private func showSelectLocationController() {
- let contentController = makeSelectLocationController()
- 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: - UIWindowSceneDelegate
func scene(
@@ -179,7 +144,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDe
window?.makeKeyAndVisible()
- tunnelManager.addObserver(self)
+ addTunnelObserver()
+
if tunnelManager.isConfigurationLoaded {
configureScene()
}
@@ -203,716 +169,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDe
func sceneDidEnterBackground(_ scene: UIScene) {}
- // MARK: - OutOfTimeViewControllerDelegate
-
- func outOfTimeViewControllerDidBeginPayment(_ controller: OutOfTimeViewController) {
- setEnableSettingsButton(isEnabled: false, from: controller)
- }
-
- func outOfTimeViewControllerDidEndPayment(_ controller: OutOfTimeViewController) {
- setEnableSettingsButton(isEnabled: true, from: controller)
- }
-
- // MARK: - RootContainerViewControllerDelegate
-
- func rootContainerViewControllerShouldShowSettings(
- _ controller: RootContainerViewController,
- navigateTo route: SettingsNavigationRoute?,
- animated: Bool
- ) {
- // Check if settings controller is already presented.
- if let settingsNavController = settingsNavController {
- settingsNavController.navigate(to: route ?? .root, animated: animated)
- } else {
- let navController = makeSettingsNavigationController(route: route)
-
- // Refresh account data each time user opens settings
- refreshDeviceAndAccountData(forceUpdate: true)
-
- // 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.
- 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.deviceState.isLoggedIn else { return false }
-
- switch tunnelManager.tunnelStatus.state {
- case .connected, .connecting, .reconnecting, .waitingForConnectivity:
- tunnelManager.reconnectTunnel(selectNewRelay: true)
-
- case .disconnecting, .disconnected:
- tunnelManager.startTunnel()
-
- case .pendingReconnect:
- break
- }
-
- return true
- }
-
- private func setupPadUI() {
- let selectLocationController = makeSelectLocationController()
- let tunnelController = makeTunnelViewController()
-
- 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, tunnelController]
-
- selectLocationViewController = selectLocationController
- self.splitViewController = splitViewController
- tunnelViewController = tunnelController
-
- rootContainer.setViewControllers([splitViewController], animated: false)
- showSplitViewMaster(tunnelManager.deviceState.isLoggedIn, animated: false)
-
- modalRootContainer.delegate = self
-
- let showNextController = { [weak self] (animated: Bool) in
- guard let self = self else { return }
-
- lazy var viewControllers: [UIViewController] = [self.makeLoginController()]
-
- switch self.tunnelManager.deviceState {
- case .loggedIn:
- self.modalRootContainer.setViewControllers(
- viewControllers,
- animated: self.isModalRootPresented && animated
- )
-
- // Dismiss modal root container if needed before proceeding.
- self.dismissModalRootContainerIfNeeded(animated: animated) {
- self.handleExpiredAccount()
- }
- return
-
- case .loggedOut:
- break
-
- case .revoked:
- viewControllers.append(self.makeRevokedDeviceController())
- }
-
- // Configure modal container.
- self.modalRootContainer.setViewControllers(
- viewControllers,
- animated: self.isModalRootPresented && animated
- )
-
- // Present modal container if not presented yet.
- self.presentModalRootContainerIfNeeded(animated: animated)
- }
-
- if TermsOfService.isAgreed {
- showNextController(false)
- } else {
- let termsOfServiceController = makeTermsOfServiceController { _ in
- showNextController(true)
- }
-
- modalRootContainer.setViewControllers([termsOfServiceController], animated: false)
- presentModalRootContainerIfNeeded(animated: false)
- }
- }
-
- private func presentModalRootContainerIfNeeded(animated: Bool) {
- guard !isModalRootPresented else { return }
-
- modalRootContainer.preferredContentSize = CGSize(width: 480, height: 600)
- modalRootContainer.presentationController?.delegate = modalRootAdaptivePresentationDelegate
- modalRootContainer.isModalInPresentation = true
-
- rootContainer.present(modalRootContainer, animated: animated)
- }
-
- private func dismissModalRootContainerIfNeeded(
- animated: Bool,
- completion: @escaping () -> Void
- ) {
- guard isModalRootPresented else {
- completion()
- return
- }
-
- modalRootContainer.dismiss(animated: animated, completion: completion)
- }
-
- private var isModalRootPresented: Bool {
- return modalRootContainer.presentingViewController != nil
- }
-
- private func setupPhoneUI() {
- let showNextController = { [weak self] (animated: Bool) in
- guard let self = self else { return }
-
- var viewControllers: [UIViewController] = [self.makeLoginController()]
-
- switch self.tunnelManager.deviceState {
- case .loggedIn:
- let tunnelViewController = self.makeTunnelViewController()
- self.tunnelViewController = tunnelViewController
- viewControllers.append(tunnelViewController)
-
- case .loggedOut:
- break
-
- case .revoked:
- viewControllers.append(self.makeRevokedDeviceController())
- }
-
- self.rootContainer.setViewControllers(viewControllers, animated: animated) {
- self.handleExpiredAccount()
- }
- }
-
- if TermsOfService.isAgreed {
- showNextController(false)
- } else {
- let termsOfServiceController = makeTermsOfServiceController { _ in
- showNextController(true)
- }
- rootContainer.setViewControllers([termsOfServiceController], animated: false)
- }
- }
-
- private func makeSettingsNavigationController(route: SettingsNavigationRoute?)
- -> SettingsNavigationController
- {
- let navController = SettingsNavigationController(
- interactorFactory: SettingsInteractorFactory(
- storePaymentManager: storePaymentManager,
- tunnelManager: tunnelManager,
- apiProxy: apiProxy
- )
- )
- 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 makeOutOfTimeViewController() -> OutOfTimeViewController {
- let viewController = OutOfTimeViewController(
- interactor: OutOfTimeInteractor(
- storePaymentManager: storePaymentManager,
- tunnelManager: tunnelManager
- )
- )
- viewController.delegate = self
- return viewController
- }
-
- private func makeTunnelViewController() -> TunnelViewController {
- let interactor = TunnelViewControllerInteractor(tunnelManager: tunnelManager)
- let tunnelViewController = TunnelViewController(interactor: interactor)
- tunnelViewController.shouldShowSelectLocationPicker = { [weak self] in
- self?.showSelectLocationController()
- }
- return tunnelViewController
- }
-
- private func makeSelectLocationController() -> SelectLocationViewController {
- let selectLocationController = SelectLocationViewController()
- selectLocationController.delegate = self
-
- if let cachedRelays = try? relayCacheTracker.getCachedRelays() {
- selectLocationController.setCachedRelays(cachedRelays)
- }
-
- let relayConstraints = tunnelManager.settings.relayConstraints
-
- selectLocationController.setSelectedRelayLocation(
- relayConstraints.location.value,
- 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
- controller.isModalInPresentation = true
- }
-
- controller.completionHandler = { controller in
- TermsOfService.setAgreed()
- completion(controller)
- }
-
- return controller
- }
-
- private func makeRevokedDeviceController() -> RevokedDeviceViewController {
- let controller = RevokedDeviceViewController(
- interactor: RevokedDeviceInteractor(tunnelManager: tunnelManager)
- )
- controller.delegate = self
- return controller
- }
-
- private func makeLoginController() -> LoginViewController {
- let controller = LoginViewController()
- controller.delegate = self
- return controller
- }
-
- private func handleExpiredAccount() {
- guard case let .loggedIn(accountData, _) = tunnelManager.deviceState,
- accountData.expiry <= Date() else { return }
-
- switch UIDevice.current.userInterfaceIdiom {
- case .phone:
- if !rootContainer.viewControllers.contains(where: { $0 is OutOfTimeViewController }) {
- rootContainer.pushViewController(makeOutOfTimeViewController(), animated: false)
- }
- case .pad:
- if !modalRootContainer.viewControllers
- .contains(where: { $0 is OutOfTimeViewController })
- {
- modalRootContainer.pushViewController(
- makeOutOfTimeViewController(),
- animated: false
- )
- presentModalRootContainerIfNeeded(animated: true)
- }
- default:
- return
- }
- }
-
- private func showSplitViewMaster(_ show: Bool, animated: Bool) {
- splitViewController?.preferredDisplayMode = show ? .allVisible : .primaryHidden
- tunnelViewController?.setMainContentHidden(!show, animated: animated)
- }
-
- private func showLoginViewAfterLogout(dismissController: UIViewController?) {
- switch UIDevice.current.userInterfaceIdiom {
- case .phone:
- let loginController = rootContainer.viewControllers.first as? LoginViewController
- loginController?.reset()
-
- rootContainer.popToRootViewController(animated: false)
- dismissController?.dismiss(animated: true)
-
- case .pad:
- let loginController = modalRootContainer.viewControllers.first as? LoginViewController
- loginController?.reset()
-
- let didDismissSourceController = {
- self.presentModalRootContainerIfNeeded(animated: true)
- }
-
- modalRootContainer.popToRootViewController(animated: false)
- showSplitViewMaster(false, animated: true)
-
- if let dismissController = dismissController {
- dismissController.dismiss(animated: true, completion: didDismissSourceController)
- } else {
- didDismissSourceController()
- }
-
- default:
- return
- }
- }
-
- private func dismissOutOfTimeController() {
- switch UIDevice.current.userInterfaceIdiom {
- case .phone:
- var viewControllers = rootContainer.viewControllers
- guard let outOfTimeControllerIndex = viewControllers
- .firstIndex(where: { $0 is OutOfTimeViewController }) else { return }
- viewControllers.remove(at: outOfTimeControllerIndex)
- rootContainer.setViewControllers(viewControllers, animated: true)
- case .pad:
- modalRootContainer.dismiss(animated: true)
- default:
- return
- }
- }
-
- private func showRevokedDeviceView() {
- switch UIDevice.current.userInterfaceIdiom {
- case .phone:
- guard let loginController = rootContainer.viewControllers.first as? LoginViewController
- else {
- return
- }
-
- loginController.reset()
-
- let viewControllers = [
- loginController,
- makeRevokedDeviceController(),
- ]
-
- rootContainer.setViewControllers(viewControllers, animated: true)
-
- case .pad:
- guard let loginController = modalRootContainer.viewControllers
- .first as? LoginViewController
- else {
- return
- }
-
- loginController.reset()
-
- let viewControllers = [
- loginController,
- makeRevokedDeviceController(),
- ]
-
- let didDismissSettings = {
- self.showSplitViewMaster(false, animated: true)
- self.presentModalRootContainerIfNeeded(animated: true)
- }
-
- modalRootContainer.setViewControllers(viewControllers, animated: isModalRootPresented)
-
- if let settingsNavController = settingsNavController {
- settingsNavController.dismiss(animated: true, completion: didDismissSettings)
- } else {
- didDismissSettings()
- }
-
- default:
- fatalError()
- }
- }
-
- // MARK: - LoginViewControllerDelegate
-
- func loginViewController(
- _ controller: LoginViewController,
- shouldHandleLoginAction action: LoginAction,
- completion: @escaping (Result<StoredAccountData?, Error>) -> Void
- ) {
- setEnableSettingsButton(isEnabled: false, from: controller)
-
- tunnelManager.setAccount(action: action.setAccountAction) { result in
- switch result {
- case .success:
- // RootContainer's settings button will be re-enabled in
- // `loginViewControllerDidFinishLogin`
- completion(result)
-
- case let .failure(error):
- // Show device management controller when too many devices detected during log in.
- if case let .useExistingAccount(accountNumber) = action,
- let restError = error as? REST.Error,
- restError.compareErrorCode(.maxDevicesReached)
- {
- self.lastLoginAction = action
-
- let deviceController = DeviceManagementViewController(
- interactor: DeviceManagementInteractor(
- accountNumber: accountNumber,
- devicesProxy: self.devicesProxy
- )
- )
- deviceController.delegate = self
-
- deviceController
- .fetchDevices(animateUpdates: false) { [weak self] operationCompletion in
- controller.rootContainerController?.pushViewController(
- deviceController,
- animated: true
- )
-
- // Return .cancelled to login controller upon success.
- completion(result.flatMap { _ in .failure(OperationError.cancelled) })
-
- self?.setEnableSettingsButton(isEnabled: true, from: controller)
- }
- } else {
- fallthrough
- }
-
- case .failure(OperationError.cancelled):
- self.setEnableSettingsButton(isEnabled: true, from: controller)
- completion(result)
- }
- }
- }
-
- func loginViewControllerDidFinishLogin(_ controller: LoginViewController) {
- lastLoginAction = nil
-
- // Move the settings button back into header bar
- setEnableSettingsButton(isEnabled: true, from: controller)
-
- let relayConstraints = tunnelManager.settings.relayConstraints
- selectLocationViewController?.setSelectedRelayLocation(
- relayConstraints.location.value,
- animated: false,
- scrollPosition: .middle
- )
-
- switch UIDevice.current.userInterfaceIdiom {
- case .phone:
- let tunnelViewController = makeTunnelViewController()
- self.tunnelViewController = tunnelViewController
- var viewControllers = rootContainer.viewControllers
- viewControllers.append(tunnelViewController)
- rootContainer.setViewControllers(viewControllers, animated: true)
- handleExpiredAccount()
-
- case .pad:
- showSplitViewMaster(true, animated: true)
-
- dismissModalRootContainerIfNeeded(animated: true) {
- self.handleExpiredAccount()
- }
-
- default:
- fatalError()
- }
- }
-
- private func setUpOutOfTimeTimer() {
- outOfTimeTimer?.invalidate()
-
- guard case let .loggedIn(accountData, _) = tunnelManager.deviceState,
- accountData.expiry > Date() else { return }
-
- let timer = Timer(
- fire: accountData.expiry,
- interval: 0,
- repeats: false
- ) { [weak self] _ in
- self?.outOfTimeTimerDidFire()
- }
-
- outOfTimeTimer = timer
- RunLoop.main.add(timer, forMode: .common)
- }
-
- @objc func outOfTimeTimerDidFire() {
- handleExpiredAccount()
- }
-
- private func clearOutOfTimeTimer() {
- outOfTimeTimer?.invalidate()
- outOfTimeTimer = nil
- }
-
- private func setEnableSettingsButton(isEnabled: Bool, from viewController: UIViewController?) {
- let containers = [viewController?.rootContainerController, rootContainer].compactMap { $0 }
-
- for container in Set(containers) {
- container.setEnableSettingsButton(isEnabled)
- }
- }
-
- // MARK: - DeviceManagementViewControllerDelegate
-
- func deviceManagementViewControllerDidCancel(_ controller: DeviceManagementViewController) {
- controller.rootContainerController?.popViewController(animated: true)
- }
-
- func deviceManagementViewControllerDidFinish(_ controller: DeviceManagementViewController) {
- let currentRootContainer = controller.rootContainerController
- let loginViewController = currentRootContainer?.viewControllers
- .first as? LoginViewController
-
- currentRootContainer?.popViewController(animated: true) {
- if let lastLoginAction = self.lastLoginAction {
- loginViewController?.start(action: lastLoginAction)
- }
- }
- }
-
- // MARK: - SettingsNavigationControllerDelegate
-
- func settingsNavigationController(
- _ controller: SettingsNavigationController,
- willNavigateTo route: SettingsNavigationRoute
- ) {
- switch route {
- case .root, .account:
- refreshDeviceAndAccountData(forceUpdate: false)
-
- default:
- break
- }
- }
-
- func settingsNavigationController(
- _ controller: SettingsNavigationController,
- didFinishWithReason reason: SettingsDismissReason
- ) {
- if case .userLoggedOut = reason {
- showLoginViewAfterLogout(dismissController: controller)
- } else {
- controller.dismiss(animated: true)
- }
- }
-
- // MARK: - NotificationManagerDelegate
-
- func notificationManagerDidUpdateInAppNotifications(
- _ manager: NotificationManager,
- notifications: [InAppNotificationDescriptor]
- ) {
- tunnelViewController?.notificationController.setNotifications(notifications, animated: true)
- }
-
- // MARK: - SelectLocationViewControllerDelegate
-
- func selectLocationViewController(
- _ controller: SelectLocationViewController,
- didSelectRelayLocation relayLocation: RelayLocation
- ) {
- // Dismiss view controller in modal presentation
- if controller.presentingViewController != nil {
- 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.setRelayConstraints(relayConstraints) {
- self.tunnelManager.startTunnel()
- }
- }
-
- // MARK: - RevokedDeviceViewControllerDelegate
-
- func revokedDeviceControllerDidRequestLogout(_ controller: RevokedDeviceViewController) {
- tunnelManager.unsetAccount { [weak self] in
- self?.showLoginViewAfterLogout(dismissController: nil)
- }
- }
-
- // MARK: - TunnelObserver
-
- func tunnelManagerDidLoadConfiguration(_ manager: TunnelManager) {
- configureScene()
- }
-
- func tunnelManager(_ manager: TunnelManager, didUpdateTunnelStatus tunnelStatus: TunnelStatus) {
- // no-op
- }
-
- func tunnelManager(
- _ manager: TunnelManager,
- didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2
- ) {
- // no-op
- }
-
- func tunnelManager(_ manager: TunnelManager, didUpdateDeviceState deviceState: DeviceState) {
- switch deviceState {
- case let .loggedIn(accountData, _):
- if accountData.expiry > Date(),
- isShowingOutOfTimeView
- {
- dismissOutOfTimeController()
- setUpOutOfTimeTimer()
- }
-
- case .loggedOut:
- resetDeviceAndAccountDataThrottling()
-
- case .revoked:
- resetDeviceAndAccountDataThrottling()
- showRevokedDeviceView()
- }
- }
-
- func tunnelManager(_ manager: TunnelManager, didFailWithError error: Error) {
- // no-op
- }
-
- // MARK: - RelayCacheTrackerObserver
-
- func relayCacheTracker(
- _ tracker: RelayCacheTracker,
- didUpdateCachedRelays cachedRelays: CachedRelays
- ) {
- selectLocationViewController?.setCachedRelays(cachedRelays)
- }
-
- // MARK: - 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 tunnelViewController
- }
-
- func splitViewController(
- _ splitViewController: UISplitViewController,
- separateSecondaryFrom primaryViewController: UIViewController
- ) -> UIViewController? {
- // Dismiss the select location controller when expanding the split view
- if selectLocationViewController?.presentingViewController != nil {
- selectLocationViewController?.dismiss(animated: false)
- }
- return nil
- }
-
// MARK: - SettingsMigrationUIHandler
func showMigrationError(_ error: Error, completionHandler: @escaping () -> Void) {
diff --git a/ios/MullvadVPN/SelectLocationNavigationController.swift b/ios/MullvadVPN/SelectLocationNavigationController.swift
deleted file mode 100644
index a88c87c907..0000000000
--- a/ios/MullvadVPN/SelectLocationNavigationController.swift
+++ /dev/null
@@ -1,30 +0,0 @@
-//
-// SelectLocationNavigationController.swift
-// MullvadVPN
-//
-// Created by pronebird on 22/07/2020.
-// Copyright © 2020 Mullvad VPN AB. All rights reserved.
-//
-
-import Foundation
-import UIKit
-
-class SelectLocationNavigationController: UINavigationController {
- override var childForStatusBarStyle: UIViewController? {
- return topViewController
- }
-
- override var childForStatusBarHidden: UIViewController? {
- return topViewController
- }
-
- init(contentController: SelectLocationViewController) {
- super.init(navigationBarClass: CustomNavigationBar.self, toolbarClass: nil)
-
- viewControllers = [contentController]
- }
-
- required init?(coder aDecoder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-}
diff --git a/ios/MullvadVPN/DNSSettings.swift b/ios/MullvadVPN/SettingsManager/DNSSettings.swift
index ce713d58ab..ce713d58ab 100644
--- a/ios/MullvadVPN/DNSSettings.swift
+++ b/ios/MullvadVPN/SettingsManager/DNSSettings.swift
diff --git a/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift b/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift
index 76b794eba9..de9308c84a 100644
--- a/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift
+++ b/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift
@@ -42,6 +42,11 @@ struct StoredAccountData: Codable, Equatable {
/// Account expiry.
var expiry: Date
+
+ /// Returns `true` if account has expired.
+ var isExpired: Bool {
+ return expiry <= Date()
+ }
}
enum DeviceState: Codable, Equatable {
diff --git a/ios/MullvadVPN/SettingsNavigationController.swift b/ios/MullvadVPN/SettingsNavigationController.swift
deleted file mode 100644
index a8812419e5..0000000000
--- a/ios/MullvadVPN/SettingsNavigationController.swift
+++ /dev/null
@@ -1,168 +0,0 @@
-//
-// SettingsNavigationController.swift
-// MullvadVPN
-//
-// Created by pronebird on 02/07/2020.
-// Copyright © 2020 Mullvad VPN AB. All rights reserved.
-//
-
-import UIKit
-
-enum SettingsNavigationRoute {
- case root
- case account
- case preferences
- case problemReport
-}
-
-enum SettingsDismissReason {
- case none
- case userLoggedOut
-}
-
-protocol SettingsNavigationControllerDelegate: AnyObject {
- func settingsNavigationController(
- _ controller: SettingsNavigationController,
- willNavigateTo route: SettingsNavigationRoute
- )
-
- func settingsNavigationController(
- _ controller: SettingsNavigationController,
- didFinishWithReason reason: SettingsDismissReason
- )
-}
-
-class SettingsNavigationController: UINavigationController, SettingsViewControllerDelegate,
- AccountViewControllerDelegate, UIAdaptivePresentationControllerDelegate,
- UINavigationControllerDelegate
-{
- private let interactorFactory: SettingsInteractorFactory
- private var currentRoutes: [SettingsNavigationRoute] = [.root]
-
- weak var settingsDelegate: SettingsNavigationControllerDelegate?
-
- override var childForStatusBarStyle: UIViewController? {
- return topViewController
- }
-
- override var childForStatusBarHidden: UIViewController? {
- return topViewController
- }
-
- init(interactorFactory: SettingsInteractorFactory) {
- self.interactorFactory = interactorFactory
-
- super.init(navigationBarClass: CustomNavigationBar.self, toolbarClass: nil)
-
- navigationBar.prefersLargeTitles = true
-
- // Navigation controller ignores `prefersLargeTitles` when using `setViewControllers()`.
- pushViewController(makeViewController(for: .root), animated: false)
-
- delegate = self
- }
-
- required init?(coder aDecoder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
- // MARK: - UINavigationControllerDelegate
-
- func navigationController(
- _ navigationController: UINavigationController,
- willShow viewController: UIViewController,
- animated: Bool
- ) {
- let newRoutes = viewControllers.compactMap { route(for: $0) }
-
- if currentRoutes != newRoutes, let nextRoute = newRoutes.last {
- currentRoutes = newRoutes
- settingsDelegate?.settingsNavigationController(self, willNavigateTo: nextRoute)
- }
- }
-
- // MARK: - SettingsViewControllerDelegate
-
- func settingsViewControllerDidFinish(_ controller: SettingsViewController) {
- settingsDelegate?.settingsNavigationController(self, didFinishWithReason: .none)
- }
-
- // MARK: - AccountViewControllerDelegate
-
- func accountViewControllerDidLogout(_ controller: AccountViewController) {
- settingsDelegate?.settingsNavigationController(self, didFinishWithReason: .userLoggedOut)
- }
-
- // MARK: - Navigation
-
- func navigate(to route: SettingsNavigationRoute, animated: Bool) {
- guard route != .root else {
- popToRootViewController(animated: animated)
- return
- }
-
- settingsDelegate?.settingsNavigationController(self, willNavigateTo: route)
-
- let nextViewController = makeViewController(for: route)
-
- if let rootController = viewControllers.first, viewControllers.count > 1 {
- let newChildren = [rootController, nextViewController]
- let newRoutes = newChildren.compactMap { self.route(for: $0) }
-
- currentRoutes = newRoutes
- setViewControllers(newChildren, animated: animated)
- } else {
- currentRoutes.append(route)
- pushViewController(nextViewController, animated: animated)
- }
- }
-
- private func makeViewController(for route: SettingsNavigationRoute) -> UIViewController {
- switch route {
- case .root:
- let controller = SettingsViewController(
- interactor: interactorFactory.makeSettingsInteractor()
- )
- controller.delegate = self
- return controller
-
- case .account:
- let controller = AccountViewController(
- interactor: interactorFactory.makeAccountInteractor()
- )
- controller.delegate = self
- return controller
-
- case .preferences:
- return PreferencesViewController(
- interactor: interactorFactory.makePreferencesInteractor()
- )
-
- case .problemReport:
- return ProblemReportViewController(
- interactor: interactorFactory.makeProblemReportInteractor()
- )
- }
- }
-
- private func route(for viewController: UIViewController) -> SettingsNavigationRoute? {
- switch viewController {
- case is SettingsViewController:
- return .root
- case is AccountViewController:
- return .account
- case is PreferencesViewController:
- return .preferences
- case is ProblemReportViewController:
- return .problemReport
- default:
- return nil
- }
- }
-
- // MARK: - UIAdaptivePresentationControllerDelegate
-
- func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
- settingsDelegate?.settingsNavigationController(self, didFinishWithReason: .none)
- }
-}
diff --git a/ios/MullvadVPN/SimulatorTunnelProvider.swift b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProvider.swift
index bf30237905..bf30237905 100644
--- a/ios/MullvadVPN/SimulatorTunnelProvider.swift
+++ b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProvider.swift
diff --git a/ios/MullvadVPN/SimulatorTunnelProviderHost.swift b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift
index 3841a3bd8b..3841a3bd8b 100644
--- a/ios/MullvadVPN/SimulatorTunnelProviderHost.swift
+++ b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift
diff --git a/ios/MullvadVPN/StringFormatter.swift b/ios/MullvadVPN/StringFormatter.swift
deleted file mode 100644
index 1e4c2ae456..0000000000
--- a/ios/MullvadVPN/StringFormatter.swift
+++ /dev/null
@@ -1,15 +0,0 @@
-//
-// StringFormatter.swift
-// MullvadVPN
-//
-// Created by Andreas Lif on 2022-06-10.
-// Copyright © 2022 Mullvad VPN AB. All rights reserved.
-//
-
-import Foundation
-
-struct StringFormatter {
- static func formattedAccountNumber(from string: String) -> String {
- return string.split(every: 4).joined(separator: " ")
- }
-}
diff --git a/ios/MullvadVPN/Assets.xcassets/AppIcon.appiconset/AppIcon.png b/ios/MullvadVPN/Supporting Files/Assets.xcassets/AppIcon.appiconset/AppIcon.png
index 782f4895f3..782f4895f3 100644
--- a/ios/MullvadVPN/Assets.xcassets/AppIcon.appiconset/AppIcon.png
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/AppIcon.appiconset/AppIcon.png
Binary files differ
diff --git a/ios/MullvadVPN/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/MullvadVPN/Supporting Files/Assets.xcassets/AppIcon.appiconset/Contents.json
index cefcc878e0..cefcc878e0 100644
--- a/ios/MullvadVPN/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/AppIcon.appiconset/Contents.json
diff --git a/ios/MullvadVPN/Assets.xcassets/Contents.json b/ios/MullvadVPN/Supporting Files/Assets.xcassets/Contents.json
index 73c00596a7..73c00596a7 100644
--- a/ios/MullvadVPN/Assets.xcassets/Contents.json
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/Contents.json
diff --git a/ios/MullvadVPN/Assets.xcassets/DangerButton.imageset/Contents.json b/ios/MullvadVPN/Supporting Files/Assets.xcassets/DangerButton.imageset/Contents.json
index 58a332d0b6..58a332d0b6 100644
--- a/ios/MullvadVPN/Assets.xcassets/DangerButton.imageset/Contents.json
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/DangerButton.imageset/Contents.json
diff --git a/ios/MullvadVPN/Assets.xcassets/DangerButton.imageset/DangerButton.pdf b/ios/MullvadVPN/Supporting Files/Assets.xcassets/DangerButton.imageset/DangerButton.pdf
index 7350298c29..7350298c29 100644
--- a/ios/MullvadVPN/Assets.xcassets/DangerButton.imageset/DangerButton.pdf
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/DangerButton.imageset/DangerButton.pdf
Binary files differ
diff --git a/ios/MullvadVPN/Assets.xcassets/DefaultButton.imageset/Contents.json b/ios/MullvadVPN/Supporting Files/Assets.xcassets/DefaultButton.imageset/Contents.json
index 864cb0a750..864cb0a750 100644
--- a/ios/MullvadVPN/Assets.xcassets/DefaultButton.imageset/Contents.json
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/DefaultButton.imageset/Contents.json
diff --git a/ios/MullvadVPN/Assets.xcassets/DefaultButton.imageset/DefaultButton.pdf b/ios/MullvadVPN/Supporting Files/Assets.xcassets/DefaultButton.imageset/DefaultButton.pdf
index 0bf94571f6..0bf94571f6 100644
--- a/ios/MullvadVPN/Assets.xcassets/DefaultButton.imageset/DefaultButton.pdf
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/DefaultButton.imageset/DefaultButton.pdf
Binary files differ
diff --git a/ios/MullvadVPN/Assets.xcassets/IconArrow.imageset/Contents.json b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconArrow.imageset/Contents.json
index 458e0814f2..458e0814f2 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconArrow.imageset/Contents.json
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconArrow.imageset/Contents.json
diff --git a/ios/MullvadVPN/Assets.xcassets/IconArrow.imageset/IconArrow.pdf b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconArrow.imageset/IconArrow.pdf
index 0476e39b57..0476e39b57 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconArrow.imageset/IconArrow.pdf
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconArrow.imageset/IconArrow.pdf
Binary files differ
diff --git a/ios/MullvadVPN/Assets.xcassets/IconBack.imageset/Contents.json b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconBack.imageset/Contents.json
index 5be7d993e8..5be7d993e8 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconBack.imageset/Contents.json
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconBack.imageset/Contents.json
diff --git a/ios/MullvadVPN/Assets.xcassets/IconBack.imageset/IconBack.pdf b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconBack.imageset/IconBack.pdf
index 9b4b57c821..9b4b57c821 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconBack.imageset/IconBack.pdf
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconBack.imageset/IconBack.pdf
Binary files differ
diff --git a/ios/MullvadVPN/Assets.xcassets/IconBackTransitionMask.imageset/Contents.json b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconBackTransitionMask.imageset/Contents.json
index e244194d6e..e244194d6e 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconBackTransitionMask.imageset/Contents.json
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconBackTransitionMask.imageset/Contents.json
diff --git a/ios/MullvadVPN/Assets.xcassets/IconBackTransitionMask.imageset/IconBackTransitionMask.pdf b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconBackTransitionMask.imageset/IconBackTransitionMask.pdf
index c127fe54cf..c127fe54cf 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconBackTransitionMask.imageset/IconBackTransitionMask.pdf
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconBackTransitionMask.imageset/IconBackTransitionMask.pdf
Binary files differ
diff --git a/ios/MullvadVPN/Assets.xcassets/IconChevron.imageset/Contents.json b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconChevron.imageset/Contents.json
index 54dbef6863..54dbef6863 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconChevron.imageset/Contents.json
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconChevron.imageset/Contents.json
diff --git a/ios/MullvadVPN/Assets.xcassets/IconChevron.imageset/IconChevron.pdf b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconChevron.imageset/IconChevron.pdf
index db695a81e0..db695a81e0 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconChevron.imageset/IconChevron.pdf
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconChevron.imageset/IconChevron.pdf
diff --git a/ios/MullvadVPN/Assets.xcassets/IconChevronDown.imageset/Contents.json b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconChevronDown.imageset/Contents.json
index 3171396241..3171396241 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconChevronDown.imageset/Contents.json
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconChevronDown.imageset/Contents.json
diff --git a/ios/MullvadVPN/Assets.xcassets/IconChevronDown.imageset/IconChevronDown.pdf b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconChevronDown.imageset/IconChevronDown.pdf
index 803fe79fbd..803fe79fbd 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconChevronDown.imageset/IconChevronDown.pdf
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconChevronDown.imageset/IconChevronDown.pdf
diff --git a/ios/MullvadVPN/Assets.xcassets/IconChevronUp.imageset/Contents.json b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconChevronUp.imageset/Contents.json
index 1539b29215..1539b29215 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconChevronUp.imageset/Contents.json
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconChevronUp.imageset/Contents.json
diff --git a/ios/MullvadVPN/Assets.xcassets/IconChevronUp.imageset/IconChevronUp.pdf b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconChevronUp.imageset/IconChevronUp.pdf
index d4d1d23918..d4d1d23918 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconChevronUp.imageset/IconChevronUp.pdf
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconChevronUp.imageset/IconChevronUp.pdf
Binary files differ
diff --git a/ios/MullvadVPN/Assets.xcassets/IconClose.imageset/Contents.json b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconClose.imageset/Contents.json
index f5ff7c0c45..f5ff7c0c45 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconClose.imageset/Contents.json
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconClose.imageset/Contents.json
diff --git a/ios/MullvadVPN/Assets.xcassets/IconClose.imageset/IconClose.pdf b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconClose.imageset/IconClose.pdf
index cc53916273..cc53916273 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconClose.imageset/IconClose.pdf
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconClose.imageset/IconClose.pdf
Binary files differ
diff --git a/ios/MullvadVPN/Assets.xcassets/IconCloseSml.imageset/Contents.json b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconCloseSml.imageset/Contents.json
index dc37d59299..dc37d59299 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconCloseSml.imageset/Contents.json
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconCloseSml.imageset/Contents.json
diff --git a/ios/MullvadVPN/Assets.xcassets/IconCloseSml.imageset/IconCloseSml.pdf b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconCloseSml.imageset/IconCloseSml.pdf
index e552786d1c..e552786d1c 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconCloseSml.imageset/IconCloseSml.pdf
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconCloseSml.imageset/IconCloseSml.pdf
Binary files differ
diff --git a/ios/MullvadVPN/Assets.xcassets/IconCopy.imageset/Contents.json b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconCopy.imageset/Contents.json
index 761f256346..761f256346 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconCopy.imageset/Contents.json
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconCopy.imageset/Contents.json
diff --git a/ios/MullvadVPN/Assets.xcassets/IconCopy.imageset/IconCopy.pdf b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconCopy.imageset/IconCopy.pdf
index 47ec193d80..47ec193d80 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconCopy.imageset/IconCopy.pdf
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconCopy.imageset/IconCopy.pdf
Binary files differ
diff --git a/ios/MullvadVPN/Assets.xcassets/IconExtlink.imageset/Contents.json b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconExtlink.imageset/Contents.json
index b95842ff5d..b95842ff5d 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconExtlink.imageset/Contents.json
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconExtlink.imageset/Contents.json
diff --git a/ios/MullvadVPN/Assets.xcassets/IconExtlink.imageset/IconExtlink.pdf b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconExtlink.imageset/IconExtlink.pdf
index 918fac610a..918fac610a 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconExtlink.imageset/IconExtlink.pdf
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconExtlink.imageset/IconExtlink.pdf
Binary files differ
diff --git a/ios/MullvadVPN/Assets.xcassets/IconFail.imageset/Contents.json b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconFail.imageset/Contents.json
index cb95467b59..cb95467b59 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconFail.imageset/Contents.json
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconFail.imageset/Contents.json
diff --git a/ios/MullvadVPN/Assets.xcassets/IconFail.imageset/IconFail.pdf b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconFail.imageset/IconFail.pdf
index 65f239e175..65f239e175 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconFail.imageset/IconFail.pdf
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconFail.imageset/IconFail.pdf
Binary files differ
diff --git a/ios/MullvadVPN/Assets.xcassets/IconObscure.imageset/Contents.json b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconObscure.imageset/Contents.json
index e3f7020b58..e3f7020b58 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconObscure.imageset/Contents.json
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconObscure.imageset/Contents.json
diff --git a/ios/MullvadVPN/Assets.xcassets/IconObscure.imageset/IconObscure.pdf b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconObscure.imageset/IconObscure.pdf
index 813fa38764..813fa38764 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconObscure.imageset/IconObscure.pdf
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconObscure.imageset/IconObscure.pdf
Binary files differ
diff --git a/ios/MullvadVPN/Assets.xcassets/IconReload.imageset/Contents.json b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconReload.imageset/Contents.json
index ff6e723432..ff6e723432 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconReload.imageset/Contents.json
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconReload.imageset/Contents.json
diff --git a/ios/MullvadVPN/Assets.xcassets/IconReload.imageset/IconReload.pdf b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconReload.imageset/IconReload.pdf
index 0cedf546e4..0cedf546e4 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconReload.imageset/IconReload.pdf
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconReload.imageset/IconReload.pdf
diff --git a/ios/MullvadVPN/Assets.xcassets/IconSettings.imageset/Contents.json b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconSettings.imageset/Contents.json
index 82d4cfeb72..82d4cfeb72 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconSettings.imageset/Contents.json
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconSettings.imageset/Contents.json
diff --git a/ios/MullvadVPN/Assets.xcassets/IconSettings.imageset/IconSettings.pdf b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconSettings.imageset/IconSettings.pdf
index 3f2ef14157..3f2ef14157 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconSettings.imageset/IconSettings.pdf
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconSettings.imageset/IconSettings.pdf
Binary files differ
diff --git a/ios/MullvadVPN/Assets.xcassets/IconSpinner.imageset/Contents.json b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconSpinner.imageset/Contents.json
index 6cfa4fdda5..6cfa4fdda5 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconSpinner.imageset/Contents.json
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconSpinner.imageset/Contents.json
diff --git a/ios/MullvadVPN/Assets.xcassets/IconSpinner.imageset/IconSpinner.pdf b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconSpinner.imageset/IconSpinner.pdf
index 95b6d6b73d..95b6d6b73d 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconSpinner.imageset/IconSpinner.pdf
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconSpinner.imageset/IconSpinner.pdf
diff --git a/ios/MullvadVPN/Assets.xcassets/IconSuccess.imageset/Contents.json b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconSuccess.imageset/Contents.json
index a0b7acfdf3..a0b7acfdf3 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconSuccess.imageset/Contents.json
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconSuccess.imageset/Contents.json
diff --git a/ios/MullvadVPN/Assets.xcassets/IconSuccess.imageset/IconSuccess.pdf b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconSuccess.imageset/IconSuccess.pdf
index b1fa595f75..b1fa595f75 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconSuccess.imageset/IconSuccess.pdf
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconSuccess.imageset/IconSuccess.pdf
diff --git a/ios/MullvadVPN/Assets.xcassets/IconTick.imageset/Contents.json b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconTick.imageset/Contents.json
index 9dd11aed54..9dd11aed54 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconTick.imageset/Contents.json
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconTick.imageset/Contents.json
diff --git a/ios/MullvadVPN/Assets.xcassets/IconTick.imageset/IconTick.pdf b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconTick.imageset/IconTick.pdf
index 70c7095ab3..70c7095ab3 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconTick.imageset/IconTick.pdf
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconTick.imageset/IconTick.pdf
diff --git a/ios/MullvadVPN/Assets.xcassets/IconTickSml.imageset/Contents.json b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconTickSml.imageset/Contents.json
index 10a4cd7580..10a4cd7580 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconTickSml.imageset/Contents.json
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconTickSml.imageset/Contents.json
diff --git a/ios/MullvadVPN/Assets.xcassets/IconTickSml.imageset/IconTickSml.pdf b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconTickSml.imageset/IconTickSml.pdf
index 4359e0ae34..4359e0ae34 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconTickSml.imageset/IconTickSml.pdf
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconTickSml.imageset/IconTickSml.pdf
Binary files differ
diff --git a/ios/MullvadVPN/Assets.xcassets/IconUnobscure.imageset/Contents.json b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconUnobscure.imageset/Contents.json
index c69f047004..c69f047004 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconUnobscure.imageset/Contents.json
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconUnobscure.imageset/Contents.json
diff --git a/ios/MullvadVPN/Assets.xcassets/IconUnobscure.imageset/IconUnobscure.pdf b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconUnobscure.imageset/IconUnobscure.pdf
index 77452cf7b2..77452cf7b2 100644
--- a/ios/MullvadVPN/Assets.xcassets/IconUnobscure.imageset/IconUnobscure.pdf
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconUnobscure.imageset/IconUnobscure.pdf
Binary files differ
diff --git a/ios/MullvadVPN/Assets.xcassets/LocationMarkerSecure.imageset/Contents.json b/ios/MullvadVPN/Supporting Files/Assets.xcassets/LocationMarkerSecure.imageset/Contents.json
index 5f66d5e4af..5f66d5e4af 100644
--- a/ios/MullvadVPN/Assets.xcassets/LocationMarkerSecure.imageset/Contents.json
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/LocationMarkerSecure.imageset/Contents.json
diff --git a/ios/MullvadVPN/Assets.xcassets/LocationMarkerSecure.imageset/LocationMarkerSecure.pdf b/ios/MullvadVPN/Supporting Files/Assets.xcassets/LocationMarkerSecure.imageset/LocationMarkerSecure.pdf
index 7da89ac1fd..7da89ac1fd 100644
--- a/ios/MullvadVPN/Assets.xcassets/LocationMarkerSecure.imageset/LocationMarkerSecure.pdf
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/LocationMarkerSecure.imageset/LocationMarkerSecure.pdf
Binary files differ
diff --git a/ios/MullvadVPN/Assets.xcassets/LocationMarkerUnsecure.imageset/Contents.json b/ios/MullvadVPN/Supporting Files/Assets.xcassets/LocationMarkerUnsecure.imageset/Contents.json
index c877c5f83c..c877c5f83c 100644
--- a/ios/MullvadVPN/Assets.xcassets/LocationMarkerUnsecure.imageset/Contents.json
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/LocationMarkerUnsecure.imageset/Contents.json
diff --git a/ios/MullvadVPN/Assets.xcassets/LocationMarkerUnsecure.imageset/LocationMarkerUnsecure.pdf b/ios/MullvadVPN/Supporting Files/Assets.xcassets/LocationMarkerUnsecure.imageset/LocationMarkerUnsecure.pdf
index 0e87b6ec41..0e87b6ec41 100644
--- a/ios/MullvadVPN/Assets.xcassets/LocationMarkerUnsecure.imageset/LocationMarkerUnsecure.pdf
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/LocationMarkerUnsecure.imageset/LocationMarkerUnsecure.pdf
Binary files differ
diff --git a/ios/MullvadVPN/Assets.xcassets/LogoIcon.imageset/Contents.json b/ios/MullvadVPN/Supporting Files/Assets.xcassets/LogoIcon.imageset/Contents.json
index 4199af9556..4199af9556 100644
--- a/ios/MullvadVPN/Assets.xcassets/LogoIcon.imageset/Contents.json
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/LogoIcon.imageset/Contents.json
diff --git a/ios/MullvadVPN/Assets.xcassets/LogoIcon.imageset/LogoIcon.pdf b/ios/MullvadVPN/Supporting Files/Assets.xcassets/LogoIcon.imageset/LogoIcon.pdf
index 1846892f30..1846892f30 100644
--- a/ios/MullvadVPN/Assets.xcassets/LogoIcon.imageset/LogoIcon.pdf
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/LogoIcon.imageset/LogoIcon.pdf
Binary files differ
diff --git a/ios/MullvadVPN/Assets.xcassets/LogoText.imageset/Contents.json b/ios/MullvadVPN/Supporting Files/Assets.xcassets/LogoText.imageset/Contents.json
index 7582073b99..7582073b99 100644
--- a/ios/MullvadVPN/Assets.xcassets/LogoText.imageset/Contents.json
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/LogoText.imageset/Contents.json
diff --git a/ios/MullvadVPN/Assets.xcassets/LogoText.imageset/LogoText.pdf b/ios/MullvadVPN/Supporting Files/Assets.xcassets/LogoText.imageset/LogoText.pdf
index e00c9c6c4d..e00c9c6c4d 100644
--- a/ios/MullvadVPN/Assets.xcassets/LogoText.imageset/LogoText.pdf
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/LogoText.imageset/LogoText.pdf
Binary files differ
diff --git a/ios/MullvadVPN/Assets.xcassets/SuccessButton.imageset/Contents.json b/ios/MullvadVPN/Supporting Files/Assets.xcassets/SuccessButton.imageset/Contents.json
index 1c5bb6e4fc..1c5bb6e4fc 100644
--- a/ios/MullvadVPN/Assets.xcassets/SuccessButton.imageset/Contents.json
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/SuccessButton.imageset/Contents.json
diff --git a/ios/MullvadVPN/Assets.xcassets/SuccessButton.imageset/SuccessButton.pdf b/ios/MullvadVPN/Supporting Files/Assets.xcassets/SuccessButton.imageset/SuccessButton.pdf
index 005ed8e333..005ed8e333 100644
--- a/ios/MullvadVPN/Assets.xcassets/SuccessButton.imageset/SuccessButton.pdf
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/SuccessButton.imageset/SuccessButton.pdf
Binary files differ
diff --git a/ios/MullvadVPN/Assets.xcassets/TranslucentDangerButton.imageset/Contents.json b/ios/MullvadVPN/Supporting Files/Assets.xcassets/TranslucentDangerButton.imageset/Contents.json
index d248ba2cd7..d248ba2cd7 100644
--- a/ios/MullvadVPN/Assets.xcassets/TranslucentDangerButton.imageset/Contents.json
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/TranslucentDangerButton.imageset/Contents.json
diff --git a/ios/MullvadVPN/Assets.xcassets/TranslucentDangerButton.imageset/TranslucentDangerButton.pdf b/ios/MullvadVPN/Supporting Files/Assets.xcassets/TranslucentDangerButton.imageset/TranslucentDangerButton.pdf
index dd63aaa23c..dd63aaa23c 100644
--- a/ios/MullvadVPN/Assets.xcassets/TranslucentDangerButton.imageset/TranslucentDangerButton.pdf
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/TranslucentDangerButton.imageset/TranslucentDangerButton.pdf
Binary files differ
diff --git a/ios/MullvadVPN/Assets.xcassets/TranslucentDangerSplitLeftButton.imageset/Contents.json b/ios/MullvadVPN/Supporting Files/Assets.xcassets/TranslucentDangerSplitLeftButton.imageset/Contents.json
index 3ffcbb4089..3ffcbb4089 100644
--- a/ios/MullvadVPN/Assets.xcassets/TranslucentDangerSplitLeftButton.imageset/Contents.json
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/TranslucentDangerSplitLeftButton.imageset/Contents.json
diff --git a/ios/MullvadVPN/Assets.xcassets/TranslucentDangerSplitLeftButton.imageset/TranslucentDangerSplitLeftButton.pdf b/ios/MullvadVPN/Supporting Files/Assets.xcassets/TranslucentDangerSplitLeftButton.imageset/TranslucentDangerSplitLeftButton.pdf
index 8a9ca176c1..8a9ca176c1 100644
--- a/ios/MullvadVPN/Assets.xcassets/TranslucentDangerSplitLeftButton.imageset/TranslucentDangerSplitLeftButton.pdf
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/TranslucentDangerSplitLeftButton.imageset/TranslucentDangerSplitLeftButton.pdf
Binary files differ
diff --git a/ios/MullvadVPN/Assets.xcassets/TranslucentDangerSplitRightButton.imageset/Contents.json b/ios/MullvadVPN/Supporting Files/Assets.xcassets/TranslucentDangerSplitRightButton.imageset/Contents.json
index 560e715aae..560e715aae 100644
--- a/ios/MullvadVPN/Assets.xcassets/TranslucentDangerSplitRightButton.imageset/Contents.json
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/TranslucentDangerSplitRightButton.imageset/Contents.json
diff --git a/ios/MullvadVPN/Assets.xcassets/TranslucentDangerSplitRightButton.imageset/TranslucentDangerSplitRightButton.pdf b/ios/MullvadVPN/Supporting Files/Assets.xcassets/TranslucentDangerSplitRightButton.imageset/TranslucentDangerSplitRightButton.pdf
index f033dc42f6..f033dc42f6 100644
--- a/ios/MullvadVPN/Assets.xcassets/TranslucentDangerSplitRightButton.imageset/TranslucentDangerSplitRightButton.pdf
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/TranslucentDangerSplitRightButton.imageset/TranslucentDangerSplitRightButton.pdf
Binary files differ
diff --git a/ios/MullvadVPN/Assets.xcassets/TranslucentNeutralButton.imageset/Contents.json b/ios/MullvadVPN/Supporting Files/Assets.xcassets/TranslucentNeutralButton.imageset/Contents.json
index 7d2eefb820..7d2eefb820 100644
--- a/ios/MullvadVPN/Assets.xcassets/TranslucentNeutralButton.imageset/Contents.json
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/TranslucentNeutralButton.imageset/Contents.json
diff --git a/ios/MullvadVPN/Assets.xcassets/TranslucentNeutralButton.imageset/TranslucentNeutralButton.pdf b/ios/MullvadVPN/Supporting Files/Assets.xcassets/TranslucentNeutralButton.imageset/TranslucentNeutralButton.pdf
index fc29439ce1..fc29439ce1 100644
--- a/ios/MullvadVPN/Assets.xcassets/TranslucentNeutralButton.imageset/TranslucentNeutralButton.pdf
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/TranslucentNeutralButton.imageset/TranslucentNeutralButton.pdf
Binary files differ
diff --git a/ios/MullvadVPN/Info.plist b/ios/MullvadVPN/Supporting Files/Info.plist
index 9c9b1744e4..9c9b1744e4 100644
--- a/ios/MullvadVPN/Info.plist
+++ b/ios/MullvadVPN/Supporting Files/Info.plist
diff --git a/ios/MullvadVPN/LaunchScreen.storyboard b/ios/MullvadVPN/Supporting Files/LaunchScreen.storyboard
index a3287d9019..a3287d9019 100644
--- a/ios/MullvadVPN/LaunchScreen.storyboard
+++ b/ios/MullvadVPN/Supporting Files/LaunchScreen.storyboard
diff --git a/ios/MullvadVPN/MullvadVPN.entitlements b/ios/MullvadVPN/Supporting Files/MullvadVPN.entitlements
index 4b3f467000..4b3f467000 100644
--- a/ios/MullvadVPN/MullvadVPN.entitlements
+++ b/ios/MullvadVPN/Supporting Files/MullvadVPN.entitlements
diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift
index 5a81c0def0..1607cd75df 100644
--- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift
+++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift
@@ -354,7 +354,22 @@ final class TunnelManager: StorePaymentObserver {
operationQueue.addOperation(operation)
}
- func setAccount(
+ func setNewAccount(completion: @escaping (Result<StoredAccountData, Error>) -> Void) {
+ setAccount(action: .new) { result in
+ completion(result.map { $0! })
+ }
+ }
+
+ func setExistingAccount(
+ accountNumber: String,
+ completion: @escaping (Result<StoredAccountData, Error>) -> Void
+ ) {
+ setAccount(action: .existing(accountNumber)) { result in
+ completion(result.map { $0! })
+ }
+ }
+
+ private func setAccount(
action: SetAccountAction,
completionHandler: @escaping (Result<StoredAccountData?, Error>) -> Void
) {
diff --git a/ios/MullvadVPN/UIColor+Palette.swift b/ios/MullvadVPN/UI appearance/UIColor+Palette.swift
index d7be5b728d..d7be5b728d 100644
--- a/ios/MullvadVPN/UIColor+Palette.swift
+++ b/ios/MullvadVPN/UI appearance/UIColor+Palette.swift
diff --git a/ios/MullvadVPN/UIMetrics.swift b/ios/MullvadVPN/UI appearance/UIMetrics.swift
index 1e9ee912c1..1e9ee912c1 100644
--- a/ios/MullvadVPN/UIMetrics.swift
+++ b/ios/MullvadVPN/UI appearance/UIMetrics.swift
diff --git a/ios/MullvadVPN/UIPresentationController+Private.swift b/ios/MullvadVPN/UIPresentationController+Private.swift
deleted file mode 100644
index 37f03385b4..0000000000
--- a/ios/MullvadVPN/UIPresentationController+Private.swift
+++ /dev/null
@@ -1,35 +0,0 @@
-//
-// UIPresentationController+Private.swift
-// MullvadVPN
-//
-// Created by pronebird on 31/01/2023.
-// Copyright © 2023 Mullvad VPN AB. All rights reserved.
-//
-
-import UIKit
-
-extension UIPresentationController {
- static let presentationTransitionWillBegin = Notification.Name(
- "UIPresentationControllerPresentationTransitionWillBeginNotification"
- )
-
- static let presentationTransitionDidEndNotification = Notification.Name(
- "UIPresentationControllerPresentationTransitionDidEndNotification"
- )
-
- static let dismissalTransitionWillBeginNotification = Notification.Name(
- "UIPresentationControllerDismissalTransitionWillBeginNotification"
- )
-
- static let dismissalTransitionDidEndNotification = Notification.Name(
- "UIPresentationControllerDismissalTransitionDidEndNotification"
- )
-
- /// Included in `presentationTransitionDidEndNotification` notifications.
- static let presentationTransitionDidEndCompletedUserInfoKey =
- "UIPresentationControllerPresentationTransitionDidEndCompletedKey"
-
- /// Included in `dismissalTransitionDidEndNotification` notifications.
- static let dismissalTransitionDidEndCompletedUserInfoKey =
- "UIPresentationControllerDismissalTransitionDidEndCompletedKey"
-}
diff --git a/ios/MullvadVPN/AccountContentView.swift b/ios/MullvadVPN/View controllers/Account/AccountContentView.swift
index 604ba53ed4..a9e03aa9db 100644
--- a/ios/MullvadVPN/AccountContentView.swift
+++ b/ios/MullvadVPN/View controllers/Account/AccountContentView.swift
@@ -290,7 +290,7 @@ class AccountNumberRow: UIView {
return nil
}
- let formattedString = StringFormatter.formattedAccountNumber(from: accountNumber)
+ let formattedString = accountNumber.formattedAccountNumber
if isObscured {
return String(formattedString.map { ch in
diff --git a/ios/MullvadVPN/AccountInteractor.swift b/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift
index c454181d95..c454181d95 100644
--- a/ios/MullvadVPN/AccountInteractor.swift
+++ b/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift
diff --git a/ios/MullvadVPN/AccountViewController.swift b/ios/MullvadVPN/View controllers/Account/AccountViewController.swift
index 34f16b3233..34f16b3233 100644
--- a/ios/MullvadVPN/AccountViewController.swift
+++ b/ios/MullvadVPN/View controllers/Account/AccountViewController.swift
diff --git a/ios/MullvadVPN/PaymentState.swift b/ios/MullvadVPN/View controllers/Account/PaymentState.swift
index b50263f831..b50263f831 100644
--- a/ios/MullvadVPN/PaymentState.swift
+++ b/ios/MullvadVPN/View controllers/Account/PaymentState.swift
diff --git a/ios/MullvadVPN/ProductState.swift b/ios/MullvadVPN/View controllers/Account/ProductState.swift
index 8188a9b3c8..8188a9b3c8 100644
--- a/ios/MullvadVPN/ProductState.swift
+++ b/ios/MullvadVPN/View controllers/Account/ProductState.swift
diff --git a/ios/MullvadVPN/DeviceManagementContentView.swift b/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementContentView.swift
index 141ec9e007..141ec9e007 100644
--- a/ios/MullvadVPN/DeviceManagementContentView.swift
+++ b/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementContentView.swift
diff --git a/ios/MullvadVPN/DeviceManagementInteractor.swift b/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementInteractor.swift
index 7aa202a2cd..7aa202a2cd 100644
--- a/ios/MullvadVPN/DeviceManagementInteractor.swift
+++ b/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementInteractor.swift
diff --git a/ios/MullvadVPN/DeviceManagementViewController.swift b/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementViewController.swift
index d3b0d006da..d3b0d006da 100644
--- a/ios/MullvadVPN/DeviceManagementViewController.swift
+++ b/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementViewController.swift
diff --git a/ios/MullvadVPN/DeviceRowView.swift b/ios/MullvadVPN/View controllers/DeviceList/DeviceRowView.swift
index ac638f47ab..ac638f47ab 100644
--- a/ios/MullvadVPN/DeviceRowView.swift
+++ b/ios/MullvadVPN/View controllers/DeviceList/DeviceRowView.swift
diff --git a/ios/MullvadVPN/LaunchViewController.swift b/ios/MullvadVPN/View controllers/Launch/LaunchViewController.swift
index 02218244d5..02218244d5 100644
--- a/ios/MullvadVPN/LaunchViewController.swift
+++ b/ios/MullvadVPN/View controllers/Launch/LaunchViewController.swift
diff --git a/ios/MullvadVPN/AccountInputGroupView.swift b/ios/MullvadVPN/View controllers/Login/AccountInputGroupView.swift
index e70be56f96..3086bc7ade 100644
--- a/ios/MullvadVPN/AccountInputGroupView.swift
+++ b/ios/MullvadVPN/View controllers/Login/AccountInputGroupView.swift
@@ -6,23 +6,16 @@
// Copyright © 2019 Mullvad VPN AB. All rights reserved.
//
-import MullvadLogging
import UIKit
-private let accountInputGroupViewAnimationDuration: TimeInterval = 0.25
+private let animationDuration: TimeInterval = 0.25
+private let minimumAccountTokenLength = 10
-protocol AccountInputGroupViewDelegate: AnyObject {
- func accountInputGroupViewShouldRemoveLastUsedAccount(_ view: AccountInputGroupView) -> Bool
- func accountInputGroupViewShouldAttemptLogin(_ view: AccountInputGroupView)
-}
-
-class AccountInputGroupView: UIView {
+final class AccountInputGroupView: UIView {
enum Style {
case normal, error, authenticating
}
- weak var delegate: AccountInputGroupViewDelegate?
-
let sendButton: UIButton = {
let button = UIButton(type: .custom)
button.translatesAutoresizingMaskIntoConstraints = false
@@ -44,12 +37,13 @@ class AccountInputGroupView: UIView {
return privateTextField.parsedToken
}
- let minimumAccountTokenLength = 10
-
var satisfiesMinimumTokenLengthRequirement: Bool {
return privateTextField.parsedToken.count > minimumAccountTokenLength
}
+ var didRemoveLastUsedAccount: (() -> Void)?
+ var didEnterAccount: (() -> Void)?
+
private let privateTextField: AccountTextField = {
let textField = AccountTextField()
textField.font = accountNumberFont()
@@ -328,7 +322,7 @@ class AccountInputGroupView: UIView {
func setLastUsedAccount(_ accountNumber: String?, animated: Bool) {
if let accountNumber = accountNumber {
- let formattedNumber = StringFormatter.formattedAccountNumber(from: accountNumber)
+ let formattedNumber = accountNumber.formattedAccountNumber
lastUsedAccountButton.accessibilityAttributedValue = NSAttributedString(
string: accountNumber,
@@ -382,25 +376,22 @@ class AccountInputGroupView: UIView {
}
@objc private func handleSendButton(_ sender: Any) {
- delegate?.accountInputGroupViewShouldAttemptLogin(self)
+ didEnterAccount?()
}
@objc private func didTapLastUsedAccount() {
- guard let lastUsedAccount = lastUsedAccount else {
- return
- }
+ guard let lastUsedAccount = lastUsedAccount else { return }
setAccount(lastUsedAccount)
privateTextField.resignFirstResponder()
updateLastUsedAccountConstraints(animated: true)
- delegate?.accountInputGroupViewShouldAttemptLogin(self)
+ didEnterAccount?()
}
@objc private func didTapRemoveLastUsedAccount() {
- if delegate?.accountInputGroupViewShouldRemoveLastUsedAccount(self) ?? false {
- setLastUsedAccount(nil, animated: true)
- }
+ didRemoveLastUsedAccount?()
+ setLastUsedAccount(nil, animated: true)
}
// MARK: - Private
@@ -487,7 +478,7 @@ class AccountInputGroupView: UIView {
if animated {
actions()
- UIView.animate(withDuration: accountInputGroupViewAnimationDuration) {
+ UIView.animate(withDuration: animationDuration) {
self.layoutIfNeeded()
}
} else {
@@ -532,7 +523,7 @@ class AccountInputGroupView: UIView {
}
if animated {
- UIView.animate(withDuration: accountInputGroupViewAnimationDuration) {
+ UIView.animate(withDuration: animationDuration) {
actions()
}
} else {
@@ -584,7 +575,7 @@ private class AccountInputBorderLayer: CAShapeLayer {
override class func defaultAction(forKey event: String) -> CAAction? {
if event == "path" {
let action = CABasicAnimation(keyPath: event)
- action.duration = accountInputGroupViewAnimationDuration
+ action.duration = animationDuration
action.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
return action
diff --git a/ios/MullvadVPN/AccountTextField.swift b/ios/MullvadVPN/View controllers/Login/AccountTextField.swift
index 74c6c1c45e..74c6c1c45e 100644
--- a/ios/MullvadVPN/AccountTextField.swift
+++ b/ios/MullvadVPN/View controllers/Login/AccountTextField.swift
diff --git a/ios/MullvadVPN/AccountTokenInput.swift b/ios/MullvadVPN/View controllers/Login/AccountTokenInput.swift
index cccc5a1a75..cccc5a1a75 100644
--- a/ios/MullvadVPN/AccountTokenInput.swift
+++ b/ios/MullvadVPN/View controllers/Login/AccountTokenInput.swift
diff --git a/ios/MullvadVPN/LoginContentView.swift b/ios/MullvadVPN/View controllers/Login/LoginContentView.swift
index 07f63631bb..07f63631bb 100644
--- a/ios/MullvadVPN/LoginContentView.swift
+++ b/ios/MullvadVPN/View controllers/Login/LoginContentView.swift
diff --git a/ios/MullvadVPN/View controllers/Login/LoginInteractor.swift b/ios/MullvadVPN/View controllers/Login/LoginInteractor.swift
new file mode 100644
index 0000000000..cdb09cb7d6
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/Login/LoginInteractor.swift
@@ -0,0 +1,54 @@
+//
+// LoginInteractor.swift
+// MullvadVPN
+//
+// Created by pronebird on 27/01/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadLogging
+
+final class LoginInteractor {
+ private let tunnelManager: TunnelManager
+ private let logger = Logger(label: "LoginInteractor")
+
+ init(tunnelManager: TunnelManager) {
+ self.tunnelManager = tunnelManager
+ }
+
+ func setAccount(accountNumber: String, completion: @escaping (Error?) -> Void) {
+ tunnelManager.setExistingAccount(accountNumber: accountNumber) { result in
+ completion(result.error)
+ }
+ }
+
+ func createAccount(completion: @escaping (Result<String, Error>) -> Void) {
+ tunnelManager.setNewAccount { result in
+ completion(result.map { $0.number })
+ }
+ }
+
+ func getLastUsedAccount() -> String? {
+ do {
+ return try SettingsManager.getLastUsedAccount()
+ } catch {
+ logger.error(
+ error: error,
+ message: "Failed to get last used account."
+ )
+ return nil
+ }
+ }
+
+ func removeLastUsedAccount() {
+ do {
+ try SettingsManager.setLastUsedAccount(nil)
+ } catch {
+ logger.error(
+ error: error,
+ message: "Failed to remove last used account."
+ )
+ }
+ }
+}
diff --git a/ios/MullvadVPN/LoginViewController.swift b/ios/MullvadVPN/View controllers/Login/LoginViewController.swift
index ed113160e9..c18dfe2a97 100644
--- a/ios/MullvadVPN/LoginViewController.swift
+++ b/ios/MullvadVPN/View controllers/Login/LoginViewController.swift
@@ -11,35 +11,27 @@ import MullvadTypes
import Operations
import UIKit
-enum LoginAction {
- case useExistingAccount(String)
- case createAccount
-
- var setAccountAction: SetAccountAction {
- switch self {
- case let .useExistingAccount(accountNumber):
- return .existing(accountNumber)
- case .createAccount:
- return .new
- }
- }
-}
-
enum LoginState {
case `default`
case authenticating(LoginAction)
- case failure(Error)
+ case failure(LoginAction, Error)
case success(LoginAction)
}
-protocol LoginViewControllerDelegate: AnyObject {
- func loginViewController(
- _ controller: LoginViewController,
- shouldHandleLoginAction action: LoginAction,
- completion: @escaping (Result<StoredAccountData?, Error>) -> Void
- )
+enum LoginAction {
+ case useExistingAccount(String)
+ case createAccount
+}
+
+enum EndLoginAction {
+ /// Do nothing.
+ case nothing
- func loginViewControllerDidFinishLogin(_ controller: LoginViewController)
+ /// Set focus on account text field.
+ case activateTextField
+
+ /// Wait for promise before showing login error.
+ case wait(Promise<Void, Error>)
}
class LoginViewController: UIViewController, RootContainment {
@@ -95,7 +87,9 @@ class LoginViewController: UIViewController, RootContainment {
return contentView.accountInputGroup.satisfiesMinimumTokenLengthRequirement
}
- weak var delegate: LoginViewControllerDelegate?
+ private let interactor: LoginInteractor
+
+ var didFinishLogin: ((LoginAction, Error?) -> EndLoginAction)?
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
@@ -109,6 +103,15 @@ class LoginViewController: UIViewController, RootContainment {
return false
}
+ init(interactor: LoginInteractor) {
+ self.interactor = interactor
+ super.init(nibName: nil, bundle: nil)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
override func viewDidLoad() {
super.viewDidLoad()
@@ -121,7 +124,13 @@ class LoginViewController: UIViewController, RootContainment {
])
updateLastUsedAccount()
- contentView.accountInputGroup.delegate = self
+ contentView.accountInputGroup.didRemoveLastUsedAccount = { [weak self] in
+ self?.interactor.removeLastUsedAccount()
+ }
+
+ contentView.accountInputGroup.didEnterAccount = { [weak self] in
+ self?.attemptLogin()
+ }
contentView.accountInputGroup.setOnReturnKey { [weak self] _ in
guard let self = self else { return true }
@@ -174,19 +183,21 @@ class LoginViewController: UIViewController, RootContainment {
func start(action: LoginAction) {
beginLogin(action)
- delegate?
- .loginViewController(self, shouldHandleLoginAction: action) { [weak self] completion in
- switch completion {
- case let .success(accountData):
- if case .createAccount = action {
- self?.contentView.accountInputGroup.setAccount(accountData?.number ?? "")
- }
-
- self?.endLogin(.success(action))
- case let .failure(error):
- self?.endLogin(error.isOperationCancellationError ? .default : .failure(error))
+ switch action {
+ case .createAccount:
+ interactor.createAccount { [weak self] result in
+ if let newAccountNumber = result.value {
+ self?.contentView.accountInputGroup.setAccount(newAccountNumber)
}
+
+ self?.endLogin(action: action, error: result.error)
}
+
+ case let .useExistingAccount(accountNumber):
+ interactor.setAccount(accountNumber: accountNumber) { [weak self] error in
+ self?.endLogin(action: action, error: error)
+ }
+ }
}
func reset() {
@@ -230,16 +241,10 @@ class LoginViewController: UIViewController, RootContainment {
// MARK: - Private
private func updateLastUsedAccount() {
- do {
- let accountNumber = try SettingsManager.getLastUsedAccount()
-
- contentView.accountInputGroup.setLastUsedAccount(accountNumber, animated: false)
- } catch {
- logger.error(
- error: error,
- message: "Failed to update last used account."
- )
- }
+ contentView.accountInputGroup.setLastUsedAccount(
+ interactor.getLastUsedAccount(),
+ animated: false
+ )
}
private func loginStateDidChange() {
@@ -251,16 +256,7 @@ class LoginViewController: UIViewController, RootContainment {
}
private func updateStatusIcon() {
- switch loginState {
- case .failure:
- contentView.statusActivityView.state = .failure
- case .success:
- contentView.statusActivityView.state = .success
- case .authenticating:
- contentView.statusActivityView.state = .activity
- case .default:
- contentView.statusActivityView.state = .hidden
- }
+ contentView.statusActivityView.state = loginState.statusActivityState
}
private func beginLogin(_ action: LoginAction) {
@@ -269,17 +265,22 @@ class LoginViewController: UIViewController, RootContainment {
view.endEditing(true)
}
- private func endLogin(_ nextLoginState: LoginState) {
- let oldLoginState = loginState
+ private func endLogin(action: LoginAction, error: Error?) {
+ let nextLoginState: LoginState = error.map { .failure(action, $0) } ?? .success(action)
- loginState = nextLoginState
+ let endAction = didFinishLogin?(action, error) ?? .nothing
- if case .authenticating(.useExistingAccount) = oldLoginState, case .failure = loginState {
+ switch endAction {
+ case .activateTextField:
contentView.accountInputGroup.textField.becomeFirstResponder()
- } else if case .success = loginState {
- // Navigate to the main view after 1s delay
- DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) {
- self.delegate?.loginViewControllerDidFinishLogin(self)
+ loginState = nextLoginState
+
+ case .nothing:
+ loginState = nextLoginState
+
+ case let .wait(promise):
+ promise.observe { result in
+ self.loginState = result.error.map { .failure(action, $0) } ?? nextLoginState
}
}
}
@@ -390,7 +391,7 @@ private extension LoginState {
)
}
- case let .failure(error):
+ case let .failure(_, error):
return (error as? DisplayError)?.displayErrorDescription ?? error.localizedDescription
case let .success(method):
@@ -412,25 +413,17 @@ private extension LoginState {
}
}
}
-}
-
-// MARK: - AccountInputGroupViewDelegate
-extension LoginViewController: AccountInputGroupViewDelegate {
- func accountInputGroupViewShouldRemoveLastUsedAccount(_ view: AccountInputGroupView) -> Bool {
- do {
- try SettingsManager.setLastUsedAccount(nil)
- return true
- } catch {
- logger.error(
- error: error,
- message: "Failed to remove last used account."
- )
- return false
+ var statusActivityState: StatusActivityView.State {
+ switch self {
+ case .failure:
+ return .failure
+ case .success:
+ return .success
+ case .authenticating:
+ return .activity
+ case .default:
+ return .hidden
}
}
-
- func accountInputGroupViewShouldAttemptLogin(_ view: AccountInputGroupView) {
- attemptLogin()
- }
}
diff --git a/ios/MullvadVPN/OutOfTimeContentView.swift b/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeContentView.swift
index aef959730c..aef959730c 100644
--- a/ios/MullvadVPN/OutOfTimeContentView.swift
+++ b/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeContentView.swift
diff --git a/ios/MullvadVPN/OutOfTimeInteractor.swift b/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeInteractor.swift
index 4166cb1e36..4166cb1e36 100644
--- a/ios/MullvadVPN/OutOfTimeInteractor.swift
+++ b/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeInteractor.swift
diff --git a/ios/MullvadVPN/OutOfTimeViewController.swift b/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeViewController.swift
index 70f7ea57bc..ff1c6ff693 100644
--- a/ios/MullvadVPN/OutOfTimeViewController.swift
+++ b/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeViewController.swift
@@ -195,14 +195,19 @@ class OutOfTimeViewController: UIViewController, RootContainment {
break
default:
- showPaymentErrorAlert(error: paymentFailure.error)
+ showPaymentErrorAlert(error: paymentFailure.error) {
+ self.paymentState = .none
+ }
}
}
paymentState = .none
}
- private func showPaymentErrorAlert(error: StorePaymentManagerError) {
+ private func showPaymentErrorAlert(
+ error: StorePaymentManagerError,
+ completion: @escaping () -> Void
+ ) {
let alertController = UIAlertController(
title: NSLocalizedString(
"CANNOT_COMPLETE_PURCHASE_ALERT_TITLE",
@@ -221,14 +226,19 @@ class OutOfTimeViewController: UIViewController, RootContainment {
tableName: "OutOfTime",
value: "OK",
comment: ""
- ), style: .cancel
+ ),
+ style: .cancel,
+ handler: { _ in completion() }
)
)
alertPresenter.enqueue(alertController, presentingController: self)
}
- private func showRestorePurchasesErrorAlert(error: StorePaymentManagerError) {
+ private func showRestorePurchasesErrorAlert(
+ error: StorePaymentManagerError,
+ completion: @escaping () -> Void
+ ) {
let alertController = UIAlertController(
title: NSLocalizedString(
"RESTORE_PURCHASES_FAILURE_ALERT_TITLE",
@@ -241,12 +251,16 @@ class OutOfTimeViewController: UIViewController, RootContainment {
)
alertController.addAction(
- UIAlertAction(title: NSLocalizedString(
- "RESTORE_PURCHASES_FAILURE_ALERT_OK_ACTION",
- tableName: "OutOfTime",
- value: "OK",
- comment: ""
- ), style: .cancel)
+ UIAlertAction(
+ title: NSLocalizedString(
+ "RESTORE_PURCHASES_FAILURE_ALERT_OK_ACTION",
+ tableName: "OutOfTime",
+ value: "OK",
+ comment: ""
+ ),
+ style: .cancel,
+ handler: { _ in completion() }
+ )
)
alertPresenter.enqueue(alertController, presentingController: self)
@@ -254,9 +268,13 @@ class OutOfTimeViewController: UIViewController, RootContainment {
private func showAlertIfNoTimeAdded(
with response: REST.CreateApplePaymentResponse,
- context: REST.CreateApplePaymentResponse.Context
+ context: REST.CreateApplePaymentResponse.Context,
+ completion: @escaping () -> Void
) {
- guard case .noTimeAdded = response else { return }
+ guard case .noTimeAdded = response else {
+ completion()
+ return
+ }
let alertController = UIAlertController(
title: response.alertTitle(context: context),
@@ -272,7 +290,8 @@ class OutOfTimeViewController: UIViewController, RootContainment {
value: "OK",
comment: ""
),
- style: .cancel
+ style: .cancel,
+ handler: { _ in completion() }
)
)
@@ -306,16 +325,18 @@ class OutOfTimeViewController: UIViewController, RootContainment {
switch result {
case let .success(response):
- self.showAlertIfNoTimeAdded(with: response, context: .restoration)
+ self.showAlertIfNoTimeAdded(with: response, context: .restoration) {
+ self.paymentState = .none
+ }
case let .failure(error as StorePaymentManagerError):
- self.showRestorePurchasesErrorAlert(error: error)
+ self.showRestorePurchasesErrorAlert(error: error) {
+ self.paymentState = .none
+ }
default:
- break
+ self.paymentState = .none
}
-
- self.paymentState = .none
}
}
diff --git a/ios/MullvadVPN/PreferencesCellFactory.swift b/ios/MullvadVPN/View controllers/Preferences/PreferencesCellFactory.swift
index 8f160d3fdc..8f160d3fdc 100644
--- a/ios/MullvadVPN/PreferencesCellFactory.swift
+++ b/ios/MullvadVPN/View controllers/Preferences/PreferencesCellFactory.swift
diff --git a/ios/MullvadVPN/PreferencesDataSource.swift b/ios/MullvadVPN/View controllers/Preferences/PreferencesDataSource.swift
index 20e0bbc892..20e0bbc892 100644
--- a/ios/MullvadVPN/PreferencesDataSource.swift
+++ b/ios/MullvadVPN/View controllers/Preferences/PreferencesDataSource.swift
diff --git a/ios/MullvadVPN/PreferencesDataSourceDelegate.swift b/ios/MullvadVPN/View controllers/Preferences/PreferencesDataSourceDelegate.swift
index 84f18d93e8..84f18d93e8 100644
--- a/ios/MullvadVPN/PreferencesDataSourceDelegate.swift
+++ b/ios/MullvadVPN/View controllers/Preferences/PreferencesDataSourceDelegate.swift
diff --git a/ios/MullvadVPN/PreferencesInteractor.swift b/ios/MullvadVPN/View controllers/Preferences/PreferencesInteractor.swift
index 8029669841..8029669841 100644
--- a/ios/MullvadVPN/PreferencesInteractor.swift
+++ b/ios/MullvadVPN/View controllers/Preferences/PreferencesInteractor.swift
diff --git a/ios/MullvadVPN/PreferencesViewController.swift b/ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift
index e18ea20e7f..e18ea20e7f 100644
--- a/ios/MullvadVPN/PreferencesViewController.swift
+++ b/ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift
diff --git a/ios/MullvadVPN/PreferencesViewModel.swift b/ios/MullvadVPN/View controllers/Preferences/PreferencesViewModel.swift
index 328b809d50..328b809d50 100644
--- a/ios/MullvadVPN/PreferencesViewModel.swift
+++ b/ios/MullvadVPN/View controllers/Preferences/PreferencesViewModel.swift
diff --git a/ios/MullvadVPN/ProblemReportInteractor.swift b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportInteractor.swift
index bac2c375a4..bac2c375a4 100644
--- a/ios/MullvadVPN/ProblemReportInteractor.swift
+++ b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportInteractor.swift
diff --git a/ios/MullvadVPN/ProblemReportReviewViewController.swift b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportReviewViewController.swift
index 4effda0bc5..4effda0bc5 100644
--- a/ios/MullvadVPN/ProblemReportReviewViewController.swift
+++ b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportReviewViewController.swift
diff --git a/ios/MullvadVPN/ProblemReportSubmissionOverlayView.swift b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportSubmissionOverlayView.swift
index 0b5a8b5bf5..0b5a8b5bf5 100644
--- a/ios/MullvadVPN/ProblemReportSubmissionOverlayView.swift
+++ b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportSubmissionOverlayView.swift
diff --git a/ios/MullvadVPN/ProblemReportViewController.swift b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController.swift
index 4a1bd4596b..4a1bd4596b 100644
--- a/ios/MullvadVPN/ProblemReportViewController.swift
+++ b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController.swift
diff --git a/ios/MullvadVPN/RevokedDeviceInteractor.swift b/ios/MullvadVPN/View controllers/RevokedDevice/RevokedDeviceInteractor.swift
index 7ceacb2676..7ceacb2676 100644
--- a/ios/MullvadVPN/RevokedDeviceInteractor.swift
+++ b/ios/MullvadVPN/View controllers/RevokedDevice/RevokedDeviceInteractor.swift
diff --git a/ios/MullvadVPN/RevokedDeviceViewController.swift b/ios/MullvadVPN/View controllers/RevokedDevice/RevokedDeviceViewController.swift
index 2d81f318cb..48fc691579 100644
--- a/ios/MullvadVPN/RevokedDeviceViewController.swift
+++ b/ios/MullvadVPN/View controllers/RevokedDevice/RevokedDeviceViewController.swift
@@ -8,10 +8,6 @@
import UIKit
-protocol RevokedDeviceViewControllerDelegate: AnyObject {
- func revokedDeviceControllerDidRequestLogout(_ controller: RevokedDeviceViewController)
-}
-
class RevokedDeviceViewController: UIViewController, RootContainment {
private lazy var imageView: StatusImageView = {
let statusImageView = StatusImageView(style: .failure)
@@ -80,7 +76,7 @@ class RevokedDeviceViewController: UIViewController, RootContainment {
return button
}()
- weak var delegate: RevokedDeviceViewControllerDelegate?
+ var didFinish: (() -> Void)?
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
@@ -171,7 +167,7 @@ class RevokedDeviceViewController: UIViewController, RootContainment {
@objc private func didTapLogoutButton(_ sender: Any?) {
logoutButton.isEnabled = false
- delegate?.revokedDeviceControllerDidRequestLogout(self)
+ didFinish?()
}
private func updateView(tunnelState: TunnelState) {
diff --git a/ios/MullvadVPN/LocationCellFactory.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationCellFactory.swift
index fa3915f175..fa3915f175 100644
--- a/ios/MullvadVPN/LocationCellFactory.swift
+++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationCellFactory.swift
diff --git a/ios/MullvadVPN/LocationDataSource.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationDataSource.swift
index 62d9b3a4e8..62d9b3a4e8 100644
--- a/ios/MullvadVPN/LocationDataSource.swift
+++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationDataSource.swift
diff --git a/ios/MullvadVPN/SelectLocationCell.swift b/ios/MullvadVPN/View controllers/SelectLocation/SelectLocationCell.swift
index 7622433925..7622433925 100644
--- a/ios/MullvadVPN/SelectLocationCell.swift
+++ b/ios/MullvadVPN/View controllers/SelectLocation/SelectLocationCell.swift
diff --git a/ios/MullvadVPN/SelectLocationHeaderView.swift b/ios/MullvadVPN/View controllers/SelectLocation/SelectLocationHeaderView.swift
index 9d39f73bad..9d39f73bad 100644
--- a/ios/MullvadVPN/SelectLocationHeaderView.swift
+++ b/ios/MullvadVPN/View controllers/SelectLocation/SelectLocationHeaderView.swift
diff --git a/ios/MullvadVPN/SelectLocationViewController.swift b/ios/MullvadVPN/View controllers/SelectLocation/SelectLocationViewController.swift
index 6b27797a07..f021ee6567 100644
--- a/ios/MullvadVPN/SelectLocationViewController.swift
+++ b/ios/MullvadVPN/View controllers/SelectLocation/SelectLocationViewController.swift
@@ -11,14 +11,7 @@ import MullvadTypes
import RelayCache
import UIKit
-protocol SelectLocationViewControllerDelegate: AnyObject {
- func selectLocationViewController(
- _ controller: SelectLocationViewController,
- didSelectRelayLocation relayLocation: RelayLocation
- )
-}
-
-class SelectLocationViewController: UIViewController, UITableViewDelegate {
+final class SelectLocationViewController: UIViewController, UITableViewDelegate {
private var tableView: UITableView?
private let tableHeaderFooterView = SelectLocationHeaderView()
@@ -41,7 +34,9 @@ class SelectLocationViewController: UIViewController, UITableViewDelegate {
return .lightContent
}
- weak var delegate: SelectLocationViewControllerDelegate?
+ var didSelectRelay: ((SelectLocationViewController, RelayLocation) -> Void)?
+ var didFinish: ((SelectLocationViewController) -> Void)?
+
var scrollToSelectedRelayOnViewWillAppear = true
init() {
@@ -64,6 +59,11 @@ class SelectLocationViewController: UIViewController, UITableViewDelegate {
value: "Select Location",
comment: ""
)
+ navigationItem.rightBarButtonItem = UIBarButtonItem(
+ barButtonSystemItem: .done,
+ target: self,
+ action: #selector(handleDone)
+ )
let tableView = UITableView(frame: view.bounds, style: .plain)
tableView.translatesAutoresizingMaskIntoConstraints = false
@@ -79,7 +79,8 @@ class SelectLocationViewController: UIViewController, UITableViewDelegate {
dataSource?.didSelectRelayLocation = { [weak self] location in
guard let self = self else { return }
- self.delegate?.selectLocationViewController(self, didSelectRelayLocation: location)
+
+ self.didSelectRelay?(self, location)
}
view.accessibilityElements = [tableHeaderFooterView, tableView]
@@ -199,9 +200,7 @@ class SelectLocationViewController: UIViewController, UITableViewDelegate {
private func updateTableHeaderTopLayoutMargin() {
// When contained within the navigation controller, we want the distance between
// the navigation title and the table header label to be exactly 24pt.
- if let navigationBar = navigationController?.navigationBar as? CustomNavigationBar,
- !showHeaderViewAtTheBottom
- {
+ if let navigationBar = navigationController?.navigationBar, !showHeaderViewAtTheBottom {
tableHeaderFooterView.topLayoutMarginAdjustmentForNavigationBarTitle = navigationBar
.titleLabelBottomInset
} else {
@@ -223,4 +222,8 @@ class SelectLocationViewController: UIViewController, UITableViewDelegate {
}
view.layoutIfNeeded()
}
+
+ @objc private func handleDone() {
+ didFinish?(self)
+ }
}
diff --git a/ios/MullvadVPN/SettingsAccountCell.swift b/ios/MullvadVPN/View controllers/Settings/SettingsAccountCell.swift
index f0621f31dc..f0621f31dc 100644
--- a/ios/MullvadVPN/SettingsAccountCell.swift
+++ b/ios/MullvadVPN/View controllers/Settings/SettingsAccountCell.swift
diff --git a/ios/MullvadVPN/SettingsAddDNSEntryCell.swift b/ios/MullvadVPN/View controllers/Settings/SettingsAddDNSEntryCell.swift
index 01872824f5..01872824f5 100644
--- a/ios/MullvadVPN/SettingsAddDNSEntryCell.swift
+++ b/ios/MullvadVPN/View controllers/Settings/SettingsAddDNSEntryCell.swift
diff --git a/ios/MullvadVPN/SettingsCell.swift b/ios/MullvadVPN/View controllers/Settings/SettingsCell.swift
index b642549d5a..b642549d5a 100644
--- a/ios/MullvadVPN/SettingsCell.swift
+++ b/ios/MullvadVPN/View controllers/Settings/SettingsCell.swift
diff --git a/ios/MullvadVPN/SettingsCellFactory.swift b/ios/MullvadVPN/View controllers/Settings/SettingsCellFactory.swift
index cd93ceeb28..cd93ceeb28 100644
--- a/ios/MullvadVPN/SettingsCellFactory.swift
+++ b/ios/MullvadVPN/View controllers/Settings/SettingsCellFactory.swift
diff --git a/ios/MullvadVPN/SettingsDNSTextCell.swift b/ios/MullvadVPN/View controllers/Settings/SettingsDNSTextCell.swift
index 83764fe9ee..83764fe9ee 100644
--- a/ios/MullvadVPN/SettingsDNSTextCell.swift
+++ b/ios/MullvadVPN/View controllers/Settings/SettingsDNSTextCell.swift
diff --git a/ios/MullvadVPN/SettingsDataSource.swift b/ios/MullvadVPN/View controllers/Settings/SettingsDataSource.swift
index 96466cf6f2..96466cf6f2 100644
--- a/ios/MullvadVPN/SettingsDataSource.swift
+++ b/ios/MullvadVPN/View controllers/Settings/SettingsDataSource.swift
diff --git a/ios/MullvadVPN/SettingsDataSourceDelegate.swift b/ios/MullvadVPN/View controllers/Settings/SettingsDataSourceDelegate.swift
index 30d7e1629a..30d7e1629a 100644
--- a/ios/MullvadVPN/SettingsDataSourceDelegate.swift
+++ b/ios/MullvadVPN/View controllers/Settings/SettingsDataSourceDelegate.swift
diff --git a/ios/MullvadVPN/SettingsInteractor.swift b/ios/MullvadVPN/View controllers/Settings/SettingsInteractor.swift
index cd5c6e6e68..cd5c6e6e68 100644
--- a/ios/MullvadVPN/SettingsInteractor.swift
+++ b/ios/MullvadVPN/View controllers/Settings/SettingsInteractor.swift
diff --git a/ios/MullvadVPN/SettingsInteractorFactory.swift b/ios/MullvadVPN/View controllers/Settings/SettingsInteractorFactory.swift
index 521e587570..521e587570 100644
--- a/ios/MullvadVPN/SettingsInteractorFactory.swift
+++ b/ios/MullvadVPN/View controllers/Settings/SettingsInteractorFactory.swift
diff --git a/ios/MullvadVPN/SettingsStaticTextFooterView.swift b/ios/MullvadVPN/View controllers/Settings/SettingsStaticTextFooterView.swift
index 700d31dc78..700d31dc78 100644
--- a/ios/MullvadVPN/SettingsStaticTextFooterView.swift
+++ b/ios/MullvadVPN/View controllers/Settings/SettingsStaticTextFooterView.swift
diff --git a/ios/MullvadVPN/SettingsSwitchCell.swift b/ios/MullvadVPN/View controllers/Settings/SettingsSwitchCell.swift
index 5b81c186b4..5b81c186b4 100644
--- a/ios/MullvadVPN/SettingsSwitchCell.swift
+++ b/ios/MullvadVPN/View controllers/Settings/SettingsSwitchCell.swift
diff --git a/ios/MullvadVPN/SettingsViewController.swift b/ios/MullvadVPN/View controllers/Settings/SettingsViewController.swift
index e43c960b56..158c91b7b0 100644
--- a/ios/MullvadVPN/SettingsViewController.swift
+++ b/ios/MullvadVPN/View controllers/Settings/SettingsViewController.swift
@@ -7,16 +7,17 @@
//
import Foundation
-import SafariServices
import UIKit
protocol SettingsViewControllerDelegate: AnyObject {
func settingsViewControllerDidFinish(_ controller: SettingsViewController)
+ func settingsViewController(
+ _ controller: SettingsViewController,
+ didRequestRoutePresentation route: SettingsNavigationRoute
+ )
}
-class SettingsViewController: UITableViewController, SettingsDataSourceDelegate,
- SFSafariViewControllerDelegate
-{
+class SettingsViewController: UITableViewController, SettingsDataSourceDelegate {
weak var delegate: SettingsViewControllerDelegate?
private var dataSource: SettingsDataSource?
private let interactor: SettingsInteractor
@@ -70,29 +71,9 @@ class SettingsViewController: UITableViewController, SettingsDataSourceDelegate,
_ dataSource: SettingsDataSource,
didSelectItem item: SettingsDataSource.Item
) {
- if let route = item.navigationRoute {
- let settingsNavigationController = navigationController as? SettingsNavigationController
+ guard let route = item.navigationRoute else { return }
- settingsNavigationController?.navigate(to: route, animated: true)
- } else if case .faq = item {
- let safariViewController = SFSafariViewController(
- url: ApplicationConfiguration
- .faqAndGuidesURL
- )
- safariViewController.delegate = self
-
- present(safariViewController, animated: true)
- }
- }
-
- // MARK: - SFSafariViewControllerDelegate
-
- func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
- controller.dismiss(animated: true)
- }
-
- func safariViewControllerWillOpenInBrowser(_ controller: SFSafariViewController) {
- controller.dismiss(animated: false)
+ delegate?.settingsViewController(self, didRequestRoutePresentation: route)
}
}
@@ -108,7 +89,7 @@ extension SettingsDataSource.Item {
case .problemReport:
return .problemReport
case .faq:
- return nil
+ return .faq
}
}
}
diff --git a/ios/MullvadVPN/TermsOfServiceContentView.swift b/ios/MullvadVPN/View controllers/TermsOfService/TermsOfServiceContentView.swift
index 58cf8b590e..58cf8b590e 100644
--- a/ios/MullvadVPN/TermsOfServiceContentView.swift
+++ b/ios/MullvadVPN/View controllers/TermsOfService/TermsOfServiceContentView.swift
diff --git a/ios/MullvadVPN/TermsOfServiceViewController.swift b/ios/MullvadVPN/View controllers/TermsOfService/TermsOfServiceViewController.swift
index 3dd909fa06..3dd909fa06 100644
--- a/ios/MullvadVPN/TermsOfServiceViewController.swift
+++ b/ios/MullvadVPN/View controllers/TermsOfService/TermsOfServiceViewController.swift
diff --git a/ios/MullvadVPN/ConnectionPanelView.swift b/ios/MullvadVPN/View controllers/Tunnel/ConnectionPanelView.swift
index 1a491d1667..1a491d1667 100644
--- a/ios/MullvadVPN/ConnectionPanelView.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/ConnectionPanelView.swift
diff --git a/ios/MullvadVPN/CustomOverlayRenderer.swift b/ios/MullvadVPN/View controllers/Tunnel/CustomOverlayRenderer.swift
index 19ca225c93..19ca225c93 100644
--- a/ios/MullvadVPN/CustomOverlayRenderer.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/CustomOverlayRenderer.swift
diff --git a/ios/MullvadVPN/DisconnectSplitButton.swift b/ios/MullvadVPN/View controllers/Tunnel/DisconnectSplitButton.swift
index 59e807cfa0..59e807cfa0 100644
--- a/ios/MullvadVPN/DisconnectSplitButton.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/DisconnectSplitButton.swift
diff --git a/ios/MullvadVPN/GeoJSON.swift b/ios/MullvadVPN/View controllers/Tunnel/GeoJSON.swift
index c33fab801b..c33fab801b 100644
--- a/ios/MullvadVPN/GeoJSON.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/GeoJSON.swift
diff --git a/ios/MullvadVPN/MapViewController.swift b/ios/MullvadVPN/View controllers/Tunnel/MapViewController.swift
index 9a8a434dd3..9a8a434dd3 100644
--- a/ios/MullvadVPN/MapViewController.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/MapViewController.swift
diff --git a/ios/MullvadVPN/TranslucentButtonBlurView.swift b/ios/MullvadVPN/View controllers/Tunnel/TranslucentButtonBlurView.swift
index 496dd1e100..496dd1e100 100644
--- a/ios/MullvadVPN/TranslucentButtonBlurView.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/TranslucentButtonBlurView.swift
diff --git a/ios/MullvadVPN/TunnelControlView.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift
index 8d5e782c95..8d5e782c95 100644
--- a/ios/MullvadVPN/TunnelControlView.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift
diff --git a/ios/MullvadVPN/TunnelViewController.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift
index 4f190995b6..4f190995b6 100644
--- a/ios/MullvadVPN/TunnelViewController.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift
diff --git a/ios/MullvadVPN/TunnelViewControllerInteractor.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewControllerInteractor.swift
index b23bc214f2..b23bc214f2 100644
--- a/ios/MullvadVPN/TunnelViewControllerInteractor.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewControllerInteractor.swift
diff --git a/ios/MullvadVPN/AppButton.swift b/ios/MullvadVPN/Views/AppButton.swift
index 7aa404b7f4..7aa404b7f4 100644
--- a/ios/MullvadVPN/AppButton.swift
+++ b/ios/MullvadVPN/Views/AppButton.swift
diff --git a/ios/MullvadVPN/CustomSwitch.swift b/ios/MullvadVPN/Views/CustomSwitch.swift
index 0172441489..0172441489 100644
--- a/ios/MullvadVPN/CustomSwitch.swift
+++ b/ios/MullvadVPN/Views/CustomSwitch.swift
diff --git a/ios/MullvadVPN/CustomSwitchContainer.swift b/ios/MullvadVPN/Views/CustomSwitchContainer.swift
index 6fdf06a2a3..6fdf06a2a3 100644
--- a/ios/MullvadVPN/CustomSwitchContainer.swift
+++ b/ios/MullvadVPN/Views/CustomSwitchContainer.swift
diff --git a/ios/MullvadVPN/CustomTextField.swift b/ios/MullvadVPN/Views/CustomTextField.swift
index d6813bfcf2..d6813bfcf2 100644
--- a/ios/MullvadVPN/CustomTextField.swift
+++ b/ios/MullvadVPN/Views/CustomTextField.swift
diff --git a/ios/MullvadVPN/CustomTextView.swift b/ios/MullvadVPN/Views/CustomTextView.swift
index d3dffe8e79..d3dffe8e79 100644
--- a/ios/MullvadVPN/CustomTextView.swift
+++ b/ios/MullvadVPN/Views/CustomTextView.swift
diff --git a/ios/MullvadVPN/EmptyTableViewHeaderFooterView.swift b/ios/MullvadVPN/Views/EmptyTableViewHeaderFooterView.swift
index 46b88f78ed..46b88f78ed 100644
--- a/ios/MullvadVPN/EmptyTableViewHeaderFooterView.swift
+++ b/ios/MullvadVPN/Views/EmptyTableViewHeaderFooterView.swift
diff --git a/ios/MullvadVPN/InAppPurchaseButton.swift b/ios/MullvadVPN/Views/InAppPurchaseButton.swift
index a87f277536..a87f277536 100644
--- a/ios/MullvadVPN/InAppPurchaseButton.swift
+++ b/ios/MullvadVPN/Views/InAppPurchaseButton.swift
diff --git a/ios/MullvadVPN/SpinnerActivityIndicatorView.swift b/ios/MullvadVPN/Views/SpinnerActivityIndicatorView.swift
index 344c46e125..344c46e125 100644
--- a/ios/MullvadVPN/SpinnerActivityIndicatorView.swift
+++ b/ios/MullvadVPN/Views/SpinnerActivityIndicatorView.swift
diff --git a/ios/MullvadVPN/StatusActivityView.swift b/ios/MullvadVPN/Views/StatusActivityView.swift
index c44cbf82fb..c44cbf82fb 100644
--- a/ios/MullvadVPN/StatusActivityView.swift
+++ b/ios/MullvadVPN/Views/StatusActivityView.swift
diff --git a/ios/MullvadVPN/StatusImageView.swift b/ios/MullvadVPN/Views/StatusImageView.swift
index af0ef14ae7..af0ef14ae7 100644
--- a/ios/MullvadVPN/StatusImageView.swift
+++ b/ios/MullvadVPN/Views/StatusImageView.swift
diff --git a/ios/MullvadVPN/WireguardKeysContentView.swift b/ios/MullvadVPN/WireguardKeysContentView.swift
deleted file mode 100644
index cc978bd218..0000000000
--- a/ios/MullvadVPN/WireguardKeysContentView.swift
+++ /dev/null
@@ -1,388 +0,0 @@
-//
-// WireguardKeysContentView.swift
-// MullvadVPN
-//
-// Created by pronebird on 05/07/2021.
-// Copyright © 2021 Mullvad VPN AB. All rights reserved.
-//
-
-import UIKit
-
-class WireguardKeysContentView: UIView {
- let regenerateKeyButton: AppButton = {
- let button = AppButton(style: .success)
- button.translatesAutoresizingMaskIntoConstraints = false
- button.setTitle(
- NSLocalizedString(
- "REGENERATE_KEY_BUTTON_TITLE",
- tableName: "WireguardKeys",
- value: "Regenerate key",
- comment: ""
- ),
- for: .normal
- )
- return button
- }()
-
- let verifyKeyButton: AppButton = {
- let button = AppButton(style: .default)
- button.translatesAutoresizingMaskIntoConstraints = false
- button.setTitle(
- NSLocalizedString(
- "VERIFY_KEY_BUTTON_TITLE",
- tableName: "WireguardKeys",
- value: "Verify key",
- comment: ""
- ),
- for: .normal
- )
- return button
- }()
-
- let publicKeyRowView: WireguardKeysPublicKeyRow = {
- let view = WireguardKeysPublicKeyRow()
- view.translatesAutoresizingMaskIntoConstraints = false
- return view
- }()
-
- let creationRowView: WireguardKeysCreationRow = {
- let view = WireguardKeysCreationRow()
- view.translatesAutoresizingMaskIntoConstraints = false
- return view
- }()
-
- lazy var contentStackView: UIStackView = {
- let stackView = UIStackView(arrangedSubviews: [publicKeyRowView, creationRowView])
- stackView.translatesAutoresizingMaskIntoConstraints = false
- stackView.axis = .vertical
- stackView.spacing = UIMetrics.sectionSpacing
- return stackView
- }()
-
- lazy var buttonStackView: UIStackView = {
- let stackView = UIStackView(arrangedSubviews: [regenerateKeyButton, verifyKeyButton])
- stackView.translatesAutoresizingMaskIntoConstraints = false
- stackView.axis = .vertical
- stackView.spacing = UIMetrics.interButtonSpacing
- return stackView
- }()
-
- override init(frame: CGRect) {
- super.init(frame: frame)
-
- layoutMargins = UIMetrics.contentLayoutMargins
-
- addSubview(contentStackView)
- addSubview(buttonStackView)
-
- NSLayoutConstraint.activate([
- contentStackView.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor),
- contentStackView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
- contentStackView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
-
- buttonStackView.topAnchor.constraint(
- greaterThanOrEqualTo: contentStackView.bottomAnchor,
- constant: UIMetrics.sectionSpacing
- ),
- buttonStackView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
- buttonStackView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
- buttonStackView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor),
- ])
- }
-
- required init?(coder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-}
-
-class WireguardKeysPublicKeyRow: UIView {
- var value: String? {
- didSet {
- valueButton.setTitle(value, for: .normal)
- accessibilityValue = value
- }
- }
-
- var status: WireguardKeyStatusView.Status = .default {
- didSet {
- statusView.status = status
- updateAccessibilityLabel()
- }
- }
-
- var actionHandler: (() -> Void)?
-
- private let textLabel: UILabel = {
- let textLabel = UILabel()
- textLabel.translatesAutoresizingMaskIntoConstraints = false
- textLabel.text = NSLocalizedString(
- "PUBLIC_KEY_LABEL",
- tableName: "WireguardKeys",
- value: "Public key",
- comment: ""
- )
- textLabel.font = UIFont.systemFont(ofSize: 14)
- textLabel.textColor = UIColor(white: 1.0, alpha: 0.6)
- return textLabel
- }()
-
- private let valueButton: UIButton = {
- let button = UIButton(type: .system)
- button.translatesAutoresizingMaskIntoConstraints = false
- button.titleLabel?.font = UIFont.systemFont(ofSize: 17)
- button.setTitleColor(.white, for: .normal)
- button.contentHorizontalAlignment = .leading
- button.contentEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 1)
- button.accessibilityHint = NSLocalizedString(
- "PUBLIC_KEY_ACCESSIBILITY_HINT",
- tableName: "WireguardKeys",
- value: "Tap to copy to pasteboard.",
- comment: ""
- )
- return button
- }()
-
- private let statusView: WireguardKeyStatusView = {
- let view = WireguardKeyStatusView()
- view.translatesAutoresizingMaskIntoConstraints = false
- view.setContentHuggingPriority(.defaultHigh, for: .horizontal)
- view.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
- return view
- }()
-
- override init(frame: CGRect) {
- super.init(frame: frame)
-
- [textLabel, valueButton, statusView].forEach { subview in
- addSubview(subview)
- }
-
- NSLayoutConstraint.activate([
- textLabel.topAnchor.constraint(equalTo: topAnchor),
- textLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
- textLabel.trailingAnchor.constraint(
- greaterThanOrEqualTo: statusView.leadingAnchor,
- constant: -8
- ),
-
- statusView.topAnchor.constraint(equalTo: textLabel.topAnchor),
- statusView.bottomAnchor.constraint(equalTo: textLabel.bottomAnchor),
- statusView.trailingAnchor.constraint(equalTo: trailingAnchor),
-
- valueButton.topAnchor.constraint(equalTo: textLabel.bottomAnchor, constant: 8),
- valueButton.leadingAnchor.constraint(equalTo: leadingAnchor),
- valueButton.trailingAnchor.constraint(equalTo: trailingAnchor),
- valueButton.bottomAnchor.constraint(equalTo: bottomAnchor),
- ])
-
- isAccessibilityElement = true
- updateAccessibilityLabel()
-
- let actionName = NSLocalizedString(
- "ACCOUNT_TOKEN_ACCESSIBILITY_ACTION_TITLE",
- tableName: "WireguardKeys",
- value: "Copy account token to pasteboard",
- comment: ""
- )
- accessibilityCustomActions = [
- UIAccessibilityCustomAction(
- name: actionName,
- target: self,
- selector: #selector(performAccessibilityAction)
- ),
- ]
-
- valueButton.addTarget(self, action: #selector(handleTap), for: .touchUpInside)
- }
-
- required init?(coder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
- private func updateAccessibilityLabel() {
- var accessibilityLabelString = textLabel.text ?? ""
-
- if case let .verified(isValid) = status {
- accessibilityLabelString += ", "
-
- if isValid {
- accessibilityLabelString.append(
- NSLocalizedString(
- "KEY_STATUS_VALID",
- tableName: "WireguardKeys",
- value: "Key is valid",
- comment: ""
- )
- )
- } else {
- accessibilityLabelString.append(
- NSLocalizedString(
- "KEY_STATUS_INVALID",
- tableName: "WireguardKeys",
- value: "Key is invalid",
- comment: ""
- )
- )
- }
- }
-
- accessibilityLabel = accessibilityLabelString
- }
-
- @objc private func handleTap() {
- actionHandler?()
- }
-
- @objc private func performAccessibilityAction() {
- actionHandler?()
- }
-}
-
-class WireguardKeysCreationRow: UIView {
- var value: String? {
- didSet {
- accessibilityValue = value
- valueLabel.text = value
- }
- }
-
- private let textLabel: UILabel = {
- let textLabel = UILabel()
- textLabel.translatesAutoresizingMaskIntoConstraints = false
- textLabel.text = NSLocalizedString(
- "KEY_GENERATED_LABEL",
- tableName: "WireguardKeys",
- value: "Key generated",
- comment: ""
- )
- textLabel.font = UIFont.systemFont(ofSize: 14)
- textLabel.textColor = UIColor(white: 1.0, alpha: 0.6)
- return textLabel
- }()
-
- private let valueLabel: UILabel = {
- let valueLabel = UILabel()
- valueLabel.translatesAutoresizingMaskIntoConstraints = false
- valueLabel.font = UIFont.systemFont(ofSize: 17)
- valueLabel.textColor = .white
- return valueLabel
- }()
-
- override init(frame: CGRect) {
- super.init(frame: frame)
-
- addSubview(textLabel)
- addSubview(valueLabel)
-
- NSLayoutConstraint.activate([
- textLabel.topAnchor.constraint(equalTo: topAnchor),
- textLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
- textLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
-
- valueLabel.topAnchor.constraint(equalTo: textLabel.bottomAnchor, constant: 8),
- valueLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
- valueLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
- valueLabel.bottomAnchor.constraint(equalTo: bottomAnchor),
- ])
-
- isAccessibilityElement = true
- accessibilityLabel = textLabel.text
- }
-
- required init?(coder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-}
-
-class WireguardKeyStatusView: UIView {
- enum Status {
- case `default`, verifying, verified(Bool), regenerating
- }
-
- let textLabel: UILabel = {
- let textLabel = UILabel()
- textLabel.translatesAutoresizingMaskIntoConstraints = false
- textLabel.font = UIFont.systemFont(ofSize: 14)
- textLabel.textColor = .successColor
- return textLabel
- }()
-
- let activityIndicator: SpinnerActivityIndicatorView = {
- let activityIndicator = SpinnerActivityIndicatorView(style: .small)
- activityIndicator.translatesAutoresizingMaskIntoConstraints = false
- activityIndicator.tintColor = .white
- return activityIndicator
- }()
-
- lazy var stackView: UIStackView = {
- let stackView = UIStackView(arrangedSubviews: [textLabel, activityIndicator])
- stackView.translatesAutoresizingMaskIntoConstraints = false
- stackView.spacing = 4
- stackView.axis = .horizontal
- stackView.distribution = .equalCentering
- return stackView
- }()
-
- var status: Status = .default {
- didSet {
- updateView()
- }
- }
-
- override init(frame: CGRect) {
- super.init(frame: frame)
-
- addSubview(stackView)
-
- NSLayoutConstraint.activate([
- stackView.topAnchor.constraint(equalTo: topAnchor),
- stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
- stackView.bottomAnchor.constraint(equalTo: bottomAnchor),
- stackView.trailingAnchor.constraint(equalTo: trailingAnchor),
- ])
-
- updateView()
- }
-
- required init?(coder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
- private func updateView() {
- switch status {
- case .default:
- textLabel.isHidden = true
- activityIndicator.stopAnimating()
-
- case .regenerating, .verifying:
- startSpinner()
-
- case let .verified(isValid):
- textLabel.isHidden = false
- activityIndicator.stopAnimating()
-
- if isValid {
- textLabel.text = NSLocalizedString(
- "KEY_STATUS_VALID",
- tableName: "WireguardKeys",
- value: "Key is valid",
- comment: ""
- )
- textLabel.textColor = .successColor
- } else {
- textLabel.text = NSLocalizedString(
- "KEY_STATUS_INVALID",
- tableName: "WireguardKeys",
- value: "Key is invalid",
- comment: ""
- )
- textLabel.textColor = .dangerColor
- }
- }
- }
-
- private func startSpinner() {
- textLabel.isHidden = true
- activityIndicator.startAnimating()
- }
-}
diff --git a/ios/MullvadVPN/NEProviderStopReason+Debug.swift b/ios/PacketTunnel/NEProviderStopReason+Debug.swift
index 35d5339c58..35d5339c58 100644
--- a/ios/MullvadVPN/NEProviderStopReason+Debug.swift
+++ b/ios/PacketTunnel/NEProviderStopReason+Debug.swift