diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2023-03-07 15:16:59 +0100 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2023-03-22 16:42:30 +0100 |
| commit | a51757ce590b5063c1c8099b3ed8ea0fa8b3bcdb (patch) | |
| tree | 85935823680100affad563ebeca45a07a71938ee | |
| parent | 1c2c6f58dc1d175d00bea8037ca989ca80b1fcb8 (diff) | |
| download | mullvadvpn-a51757ce590b5063c1c8099b3ed8ea0fa8b3bcdb.tar.xz mullvadvpn-a51757ce590b5063c1c8099b3ed8ea0fa8b3bcdb.zip | |
Add coordinators and app router
Fixes IOS-10
| -rw-r--r-- | ios/MullvadTypes/Promise.swift | 49 | ||||
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 622 | ||||
| -rw-r--r-- | ios/MullvadVPN/AddressCacheTracker/AddressCacheTracker.swift (renamed from ios/MullvadVPN/AddressCacheTracker.swift) | 0 | ||||
| -rw-r--r-- | ios/MullvadVPN/AppDelegate.swift | 2 | ||||
| -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.swift | 25 | ||||
| -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.swift | 719 | ||||
| -rw-r--r-- | ios/MullvadVPN/Coordinators/App/ApplicationRouter.swift | 666 | ||||
| -rw-r--r-- | ios/MullvadVPN/Coordinators/App/LoginCoordinator.swift | 121 | ||||
| -rw-r--r-- | ios/MullvadVPN/Coordinators/App/OutOfTimeCoordinator.swift | 70 | ||||
| -rw-r--r-- | ios/MullvadVPN/Coordinators/App/RevokedCoordinator.swift | 34 | ||||
| -rw-r--r-- | ios/MullvadVPN/Coordinators/App/SelectLocationCoordinator.swift | 82 | ||||
| -rw-r--r-- | ios/MullvadVPN/Coordinators/App/SettingsCoordinator.swift | 193 | ||||
| -rw-r--r-- | ios/MullvadVPN/Coordinators/App/TermsOfServiceCoordinator.swift | 33 | ||||
| -rw-r--r-- | ios/MullvadVPN/Coordinators/App/TunnelCoordinator.swift | 65 | ||||
| -rw-r--r-- | ios/MullvadVPN/Coordinators/Base/Coordinator.swift | 163 | ||||
| -rw-r--r-- | ios/MullvadVPN/Coordinators/Base/ModalPresentationConfiguration.swift | 52 | ||||
| -rw-r--r-- | ios/MullvadVPN/Coordinators/Base/PresentationControllerDismissalInterceptor.swift | 78 | ||||
| -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.swift | 15 | ||||
| -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.swift | 142 | ||||
| -rw-r--r-- | ios/MullvadVPN/Presentation controllers/FormsheetPresentationController.swift | 216 | ||||
| -rw-r--r-- | ios/MullvadVPN/Presentation controllers/SecondaryContextPresentationController.swift | 67 | ||||
| -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.swift | 856 | ||||
| -rw-r--r-- | ios/MullvadVPN/SelectLocationNavigationController.swift | 30 | ||||
| -rw-r--r-- | ios/MullvadVPN/SettingsManager/DNSSettings.swift (renamed from ios/MullvadVPN/DNSSettings.swift) | 0 | ||||
| -rw-r--r-- | ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift | 5 | ||||
| -rw-r--r-- | ios/MullvadVPN/SettingsNavigationController.swift | 168 | ||||
| -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.swift | 15 | ||||
| -rw-r--r-- | ios/MullvadVPN/Supporting Files/Assets.xcassets/AppIcon.appiconset/AppIcon.png (renamed from ios/MullvadVPN/Assets.xcassets/AppIcon.appiconset/AppIcon.png) | bin | 42242 -> 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) | bin | 994 -> 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) | bin | 994 -> 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) | bin | 1103 -> 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) | bin | 1123 -> 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) | bin | 967 -> 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) | bin | 1059 -> 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) | bin | 1128 -> 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) | bin | 1137 -> 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) | bin | 1174 -> 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) | bin | 1124 -> 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) | bin | 1161 -> 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) | bin | 1481 -> 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) | bin | 1399 -> 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) | bin | 1043 -> 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) | bin | 1154 -> 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) | bin | 2405 -> 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) | bin | 2403 -> 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) | bin | 3057 -> 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) | bin | 2742 -> 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) | bin | 995 -> 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) | bin | 998 -> 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) | bin | 980 -> 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) | bin | 980 -> 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) | bin | 998 -> 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.swift | 17 | ||||
| -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.swift | 35 | ||||
| -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.swift | 54 | ||||
| -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.swift | 388 | ||||
| -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 Binary files differindex 782f4895f3..782f4895f3 100644 --- a/ios/MullvadVPN/Assets.xcassets/AppIcon.appiconset/AppIcon.png +++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/AppIcon.appiconset/AppIcon.png 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 Binary files differindex 7350298c29..7350298c29 100644 --- a/ios/MullvadVPN/Assets.xcassets/DangerButton.imageset/DangerButton.pdf +++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/DangerButton.imageset/DangerButton.pdf 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 Binary files differindex 0bf94571f6..0bf94571f6 100644 --- a/ios/MullvadVPN/Assets.xcassets/DefaultButton.imageset/DefaultButton.pdf +++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/DefaultButton.imageset/DefaultButton.pdf 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 Binary files differindex 0476e39b57..0476e39b57 100644 --- a/ios/MullvadVPN/Assets.xcassets/IconArrow.imageset/IconArrow.pdf +++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconArrow.imageset/IconArrow.pdf 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 Binary files differindex 9b4b57c821..9b4b57c821 100644 --- a/ios/MullvadVPN/Assets.xcassets/IconBack.imageset/IconBack.pdf +++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconBack.imageset/IconBack.pdf 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 Binary files differindex c127fe54cf..c127fe54cf 100644 --- a/ios/MullvadVPN/Assets.xcassets/IconBackTransitionMask.imageset/IconBackTransitionMask.pdf +++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconBackTransitionMask.imageset/IconBackTransitionMask.pdf 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 Binary files differindex d4d1d23918..d4d1d23918 100644 --- a/ios/MullvadVPN/Assets.xcassets/IconChevronUp.imageset/IconChevronUp.pdf +++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconChevronUp.imageset/IconChevronUp.pdf 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 Binary files differindex cc53916273..cc53916273 100644 --- a/ios/MullvadVPN/Assets.xcassets/IconClose.imageset/IconClose.pdf +++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconClose.imageset/IconClose.pdf 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 Binary files differindex e552786d1c..e552786d1c 100644 --- a/ios/MullvadVPN/Assets.xcassets/IconCloseSml.imageset/IconCloseSml.pdf +++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconCloseSml.imageset/IconCloseSml.pdf 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 Binary files differindex 47ec193d80..47ec193d80 100644 --- a/ios/MullvadVPN/Assets.xcassets/IconCopy.imageset/IconCopy.pdf +++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconCopy.imageset/IconCopy.pdf 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 Binary files differindex 918fac610a..918fac610a 100644 --- a/ios/MullvadVPN/Assets.xcassets/IconExtlink.imageset/IconExtlink.pdf +++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconExtlink.imageset/IconExtlink.pdf 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 Binary files differindex 65f239e175..65f239e175 100644 --- a/ios/MullvadVPN/Assets.xcassets/IconFail.imageset/IconFail.pdf +++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconFail.imageset/IconFail.pdf 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 Binary files differindex 813fa38764..813fa38764 100644 --- a/ios/MullvadVPN/Assets.xcassets/IconObscure.imageset/IconObscure.pdf +++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconObscure.imageset/IconObscure.pdf 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 Binary files differindex 3f2ef14157..3f2ef14157 100644 --- a/ios/MullvadVPN/Assets.xcassets/IconSettings.imageset/IconSettings.pdf +++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconSettings.imageset/IconSettings.pdf 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 Binary files differindex 4359e0ae34..4359e0ae34 100644 --- a/ios/MullvadVPN/Assets.xcassets/IconTickSml.imageset/IconTickSml.pdf +++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconTickSml.imageset/IconTickSml.pdf 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 Binary files differindex 77452cf7b2..77452cf7b2 100644 --- a/ios/MullvadVPN/Assets.xcassets/IconUnobscure.imageset/IconUnobscure.pdf +++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconUnobscure.imageset/IconUnobscure.pdf 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 Binary files differindex 7da89ac1fd..7da89ac1fd 100644 --- a/ios/MullvadVPN/Assets.xcassets/LocationMarkerSecure.imageset/LocationMarkerSecure.pdf +++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/LocationMarkerSecure.imageset/LocationMarkerSecure.pdf 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 Binary files differindex 0e87b6ec41..0e87b6ec41 100644 --- a/ios/MullvadVPN/Assets.xcassets/LocationMarkerUnsecure.imageset/LocationMarkerUnsecure.pdf +++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/LocationMarkerUnsecure.imageset/LocationMarkerUnsecure.pdf 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 Binary files differindex 1846892f30..1846892f30 100644 --- a/ios/MullvadVPN/Assets.xcassets/LogoIcon.imageset/LogoIcon.pdf +++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/LogoIcon.imageset/LogoIcon.pdf 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 Binary files differindex e00c9c6c4d..e00c9c6c4d 100644 --- a/ios/MullvadVPN/Assets.xcassets/LogoText.imageset/LogoText.pdf +++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/LogoText.imageset/LogoText.pdf 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 Binary files differindex 005ed8e333..005ed8e333 100644 --- a/ios/MullvadVPN/Assets.xcassets/SuccessButton.imageset/SuccessButton.pdf +++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/SuccessButton.imageset/SuccessButton.pdf 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 Binary files differindex dd63aaa23c..dd63aaa23c 100644 --- a/ios/MullvadVPN/Assets.xcassets/TranslucentDangerButton.imageset/TranslucentDangerButton.pdf +++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/TranslucentDangerButton.imageset/TranslucentDangerButton.pdf 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 Binary files differindex 8a9ca176c1..8a9ca176c1 100644 --- a/ios/MullvadVPN/Assets.xcassets/TranslucentDangerSplitLeftButton.imageset/TranslucentDangerSplitLeftButton.pdf +++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/TranslucentDangerSplitLeftButton.imageset/TranslucentDangerSplitLeftButton.pdf 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 Binary files differindex f033dc42f6..f033dc42f6 100644 --- a/ios/MullvadVPN/Assets.xcassets/TranslucentDangerSplitRightButton.imageset/TranslucentDangerSplitRightButton.pdf +++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/TranslucentDangerSplitRightButton.imageset/TranslucentDangerSplitRightButton.pdf 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 Binary files differindex fc29439ce1..fc29439ce1 100644 --- a/ios/MullvadVPN/Assets.xcassets/TranslucentNeutralButton.imageset/TranslucentNeutralButton.pdf +++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/TranslucentNeutralButton.imageset/TranslucentNeutralButton.pdf 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 |
