diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2022-11-03 11:14:18 +0100 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2022-11-03 11:14:18 +0100 |
| commit | 4e3ce788f6e75a11ff6d22140adda6fdb1f0d716 (patch) | |
| tree | a99b8c166b03a1cb7cba448d28388d2141f264c5 | |
| parent | b8523b04e98e444f199ab3718adb48f8faf7bd55 (diff) | |
| parent | 19aa248e6d5fd58ba224bfd2ed1db8214f19f67b (diff) | |
| download | mullvadvpn-4e3ce788f6e75a11ff6d22140adda6fdb1f0d716.tar.xz mullvadvpn-4e3ce788f6e75a11ff6d22140adda6fdb1f0d716.zip | |
Merge branch 'add-settings-interactors'
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 20 | ||||
| -rw-r--r-- | ios/MullvadVPN/AccountInteractor.swift | 79 | ||||
| -rw-r--r-- | ios/MullvadVPN/AccountViewController.swift | 126 | ||||
| -rw-r--r-- | ios/MullvadVPN/PreferencesInteractor.swift | 32 | ||||
| -rw-r--r-- | ios/MullvadVPN/PreferencesViewController.swift | 43 | ||||
| -rw-r--r-- | ios/MullvadVPN/ProblemReportInteractor.swift | 64 | ||||
| -rw-r--r-- | ios/MullvadVPN/ProblemReportViewController.swift | 50 | ||||
| -rw-r--r-- | ios/MullvadVPN/SceneDelegate.swift | 8 | ||||
| -rw-r--r-- | ios/MullvadVPN/SettingsDataSource.swift | 40 | ||||
| -rw-r--r-- | ios/MullvadVPN/SettingsInteractor.swift | 33 | ||||
| -rw-r--r-- | ios/MullvadVPN/SettingsInteractorFactory.swift | 45 | ||||
| -rw-r--r-- | ios/MullvadVPN/SettingsNavigationController.swift | 29 | ||||
| -rw-r--r-- | ios/MullvadVPN/SettingsViewController.swift | 6 |
13 files changed, 400 insertions, 175 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 73b7b77a3e..b8d6b404c8 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -137,6 +137,8 @@ 585E820327F3285E00939F0E /* SendStoreReceiptOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585E820227F3285E00939F0E /* SendStoreReceiptOperation.swift */; }; 5862805422428EF100F5A6E1 /* TranslucentButtonBlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.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 */; }; @@ -150,6 +152,7 @@ 586A9516290133ED007BAF2B /* AnyIPAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584D26BE270C550B004EA533 /* AnyIPAddress.swift */; }; 586A95172901344A007BAF2B /* IPAddress+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06AC115628F848D00037AF9A /* IPAddress+Codable.swift */; }; 586E54FB27A2DF6D0029B88B /* SendTunnelProviderMessageOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586E54FA27A2DF6D0029B88B /* SendTunnelProviderMessageOperation.swift */; }; + 5871167F2910035700D41AAC /* PreferencesInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5871167E2910035700D41AAC /* PreferencesInteractor.swift */; }; 5871FB96254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5871FB95254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift */; }; 5871FBA0254C26C00051A0A4 /* NSRegularExpression+IPAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5871FB9F254C26BF0051A0A4 /* NSRegularExpression+IPAddress.swift */; }; 5872631B283F6EAB00E14ADF /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 5872631A283F6EAB00E14ADF /* Intents.intentdefinition */; }; @@ -159,6 +162,8 @@ 587425C12299833500CA2045 /* RootContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587425C02299833500CA2045 /* RootContainerViewController.swift */; }; 5875960A26F371FC00BF6711 /* Tunnel+Messaging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5875960926F371FC00BF6711 /* Tunnel+Messaging.swift */; }; 5877D70F282137E8002FCFC7 /* SettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FF2C02281BDE02009EF542 /* SettingsManager.swift */; }; + 5878A26F2907E7E00096FC88 /* ProblemReportInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5878A26E2907E7E00096FC88 /* ProblemReportInteractor.swift */; }; + 5878A27129091CF20096FC88 /* AccountInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5878A27029091CF20096FC88 /* AccountInteractor.swift */; }; 5878A27329091D6D0096FC88 /* TunnelBlockObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5878A27229091D6D0096FC88 /* TunnelBlockObserver.swift */; }; 5878A27529093A310096FC88 /* StorePaymentEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5878A27429093A310096FC88 /* StorePaymentEvent.swift */; }; 5878A27729093A4F0096FC88 /* StorePaymentBlockObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5878A27629093A4F0096FC88 /* StorePaymentBlockObserver.swift */; }; @@ -625,6 +630,8 @@ 5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslucentButtonBlurView.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>"; }; + 58677711290976FB006F721F /* SettingsInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInteractor.swift; sourceTree = "<group>"; }; 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>"; }; @@ -632,6 +639,7 @@ 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>"; }; + 5871167E2910035700D41AAC /* PreferencesInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesInteractor.swift; sourceTree = "<group>"; }; 5871FB95254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsolidatedApplicationLog.swift; sourceTree = "<group>"; }; 5871FB9F254C26BF0051A0A4 /* NSRegularExpression+IPAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSRegularExpression+IPAddress.swift"; sourceTree = "<group>"; }; 5872631A283F6EAB00E14ADF /* Intents.intentdefinition */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; path = Intents.intentdefinition; sourceTree = "<group>"; }; @@ -642,6 +650,8 @@ 5875960926F371FC00BF6711 /* Tunnel+Messaging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Tunnel+Messaging.swift"; sourceTree = "<group>"; }; 58781CC822AE7CA8009B9D8E /* RelayConstraints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayConstraints.swift; sourceTree = "<group>"; }; 58781CD422AFBA39009B9D8E /* RelaySelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelector.swift; sourceTree = "<group>"; }; + 5878A26E2907E7E00096FC88 /* ProblemReportInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProblemReportInteractor.swift; sourceTree = "<group>"; }; + 5878A27029091CF20096FC88 /* AccountInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountInteractor.swift; sourceTree = "<group>"; }; 5878A27229091D6D0096FC88 /* TunnelBlockObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelBlockObserver.swift; sourceTree = "<group>"; }; 5878A27429093A310096FC88 /* StorePaymentEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorePaymentEvent.swift; sourceTree = "<group>"; }; 5878A27629093A4F0096FC88 /* StorePaymentBlockObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorePaymentBlockObserver.swift; sourceTree = "<group>"; }; @@ -1223,6 +1233,7 @@ 58C3A4B122456F1A00340BDB /* AccountInputGroupView.swift */, 58CCA01D2242787B004F3011 /* AccountTextField.swift */, 582AE30F2440A6CA00E6733A /* AccountTokenInput.swift */, + 5878A27029091CF20096FC88 /* AccountInteractor.swift */, 58CCA01722426713004F3011 /* AccountViewController.swift */, 5867771329097BCD006F721F /* PaymentState.swift */, 5867771529097C5B006F721F /* ProductState.swift */, @@ -1288,11 +1299,13 @@ 584D26C3270C855A004EA533 /* PreferencesDataSource.swift */, 587EB6732714520600123C75 /* PreferencesDataSourceDelegate.swift */, 58ACF6482655365700ACE4B7 /* PreferencesViewController.swift */, + 5871167E2910035700D41AAC /* PreferencesInteractor.swift */, 587EB671271451E300123C75 /* PreferencesViewModel.swift */, 58F8AC0D25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift */, 58EF580A25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift */, 58293FAC2510CA58005D0BB5 /* ProblemReportViewController.swift */, 585DA87526B0249A00B8C587 /* RelayCache */, + 5878A26E2907E7E00096FC88 /* ProblemReportInteractor.swift */, 06FAE67828F83CA50033DD93 /* RESTCreateApplePaymentResponse+Localization.swift */, 58F1311427E0B2AB007AC5BC /* Result+Extensions.swift */, 580909D22876D09A0078138D /* RevokedDeviceViewController.swift */, @@ -1311,9 +1324,11 @@ 584D26C5270C8741004EA533 /* SettingsDNSTextCell.swift */, 580F8B88281A79A7002E0998 /* SettingsManager */, 58E6771E24ADFE7800AA26E7 /* SettingsNavigationController.swift */, + 5867770F290975E8006F721F /* SettingsInteractorFactory.swift */, 584D26C1270C8542004EA533 /* SettingsStaticTextFooterView.swift */, 58ACF64A26553C3F00ACE4B7 /* SettingsSwitchCell.swift */, 58CCA01122424D11004F3011 /* SettingsViewController.swift */, + 58677711290976FB006F721F /* SettingsInteractor.swift */, 75FD0C2028B108570021E33E /* ShortcutsDataSource.swift */, 75FD0C2228B109860021E33E /* ShortcutsDataSourceDelegate.swift */, 753D6C0B28B4BF3E0052D9E1 /* ShortcutsManager.swift */, @@ -2081,6 +2096,7 @@ 5867770E29096984006F721F /* OutOfTimeInteractor.swift in Sources */, 5872631B283F6EAB00E14ADF /* Intents.intentdefinition in Sources */, 58F8AC0E25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift in Sources */, + 5878A27129091CF20096FC88 /* AccountInteractor.swift in Sources */, 58CCA010224249A1004F3011 /* ConnectViewController.swift in Sources */, 5893716A28817A45004EE76C /* DeviceManagementViewController.swift in Sources */, 58BFA5C622A7C97F00A6173D /* RelayCacheTracker.swift in Sources */, @@ -2148,7 +2164,9 @@ 587EB66A270EFACB00123C75 /* CharacterSet+IPAddress.swift in Sources */, 5888AD83227B11080051EB06 /* SelectLocationCell.swift in Sources */, 5891BF1C25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift in Sources */, + 5878A26F2907E7E00096FC88 /* ProblemReportInteractor.swift in Sources */, 5807E2C02432038B00F5FF30 /* String+Split.swift in Sources */, + 58677712290976FB006F721F /* SettingsInteractor.swift in Sources */, 58CE5E66224146200008646E /* LoginViewController.swift in Sources */, 58EF580B25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift in Sources */, 58FD5BE724192A2C00112C88 /* StoreReceipt.swift in Sources */, @@ -2168,6 +2186,7 @@ 58EE2E3A272FF814003BFF93 /* SettingsDataSource.swift in Sources */, 58421032282E42B000F24E46 /* UpdateDeviceDataOperation.swift in Sources */, 5878A27D2909657C0096FC88 /* RevokedDeviceInteractor.swift in Sources */, + 58677710290975E9006F721F /* SettingsInteractorFactory.swift in Sources */, 5872631D283F755900E14ADF /* IntentHandlers.swift in Sources */, 58CCA01222424D11004F3011 /* SettingsViewController.swift in Sources */, 580F8B8628197958002E0998 /* DNSSettings.swift in Sources */, @@ -2190,6 +2209,7 @@ 58293FB725138B88005D0BB5 /* CustomNavigationController.swift in Sources */, 587425C12299833500CA2045 /* RootContainerViewController.swift in Sources */, 5896AE84246D5889005B36CB /* CustomDateComponentsFormatting.swift in Sources */, + 5871167F2910035700D41AAC /* PreferencesInteractor.swift in Sources */, 587AD7C623421D7000E93A53 /* TunnelSettingsV1.swift in Sources */, 58E20771274672CA00DE5D77 /* LaunchViewController.swift in Sources */, 584D26C4270C855B004EA533 /* PreferencesDataSource.swift in Sources */, diff --git a/ios/MullvadVPN/AccountInteractor.swift b/ios/MullvadVPN/AccountInteractor.swift new file mode 100644 index 0000000000..298f73f0b5 --- /dev/null +++ b/ios/MullvadVPN/AccountInteractor.swift @@ -0,0 +1,79 @@ +// +// AccountInteractor.swift +// MullvadVPN +// +// Created by pronebird on 26/10/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import MullvadREST +import MullvadTypes +import Operations +import StoreKit + +final class AccountInteractor { + private let storePaymentManager: StorePaymentManager + private let tunnelManager: TunnelManager + + var didReceivePaymentEvent: ((StorePaymentEvent) -> Void)? + var didReceiveDeviceState: ((DeviceState) -> Void)? + + private var tunnelObserver: TunnelObserver? + private var paymentObserver: StorePaymentObserver? + + init(storePaymentManager: StorePaymentManager, tunnelManager: TunnelManager) { + self.storePaymentManager = storePaymentManager + self.tunnelManager = tunnelManager + + let tunnelObserver = + TunnelBlockObserver(didUpdateDeviceState: { [weak self] manager, deviceState in + self?.didReceiveDeviceState?(deviceState) + }) + + let paymentObserver = StorePaymentBlockObserver { [weak self] manager, event in + self?.didReceivePaymentEvent?(event) + } + + tunnelManager.addObserver(tunnelObserver) + storePaymentManager.addPaymentObserver(paymentObserver) + + self.tunnelObserver = tunnelObserver + self.paymentObserver = paymentObserver + } + + var deviceState: DeviceState { + return tunnelManager.deviceState + } + + func logout(_ completion: @escaping () -> Void) { + tunnelManager.unsetAccount(completionHandler: completion) + } + + func addPayment(_ payment: SKPayment, for accountNumber: String) { + storePaymentManager.addPayment(payment, for: accountNumber) + } + + func restorePurchases( + for accountNumber: String, + completionHandler: @escaping (OperationCompletion< + REST.CreateApplePaymentResponse, + StorePaymentManagerError + >) -> Void + ) -> Cancellable { + return storePaymentManager.restorePurchases( + for: accountNumber, + completionHandler: completionHandler + ) + } + + func requestProducts( + with productIdentifiers: Set<StoreSubscription>, + completionHandler: @escaping (OperationCompletion<SKProductsResponse, Swift.Error>) -> Void + ) -> Cancellable { + return storePaymentManager.requestProducts( + with: productIdentifiers, + completionHandler: completionHandler + ) + } +} diff --git a/ios/MullvadVPN/AccountViewController.swift b/ios/MullvadVPN/AccountViewController.swift index 83ae252191..9aa1abd460 100644 --- a/ios/MullvadVPN/AccountViewController.swift +++ b/ios/MullvadVPN/AccountViewController.swift @@ -8,6 +8,7 @@ import MullvadLogging import MullvadREST +import MullvadTypes import Operations import StoreKit import UIKit @@ -16,7 +17,8 @@ protocol AccountViewControllerDelegate: AnyObject { func accountViewControllerDidLogout(_ controller: AccountViewController) } -class AccountViewController: UIViewController, StorePaymentObserver, TunnelObserver { +class AccountViewController: UIViewController { + private let interactor: AccountInteractor private let alertPresenter = AlertPresenter() private let contentView: AccountContentView = { @@ -30,6 +32,16 @@ class AccountViewController: UIViewController, StorePaymentObserver, TunnelObser weak var delegate: AccountViewControllerDelegate? + init(interactor: AccountInteractor) { + self.interactor = interactor + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + // MARK: - View lifecycle override var preferredStatusBarStyle: UIStatusBarStyle { @@ -83,10 +95,15 @@ class AccountViewController: UIViewController, StorePaymentObserver, TunnelObser ) contentView.logoutButton.addTarget(self, action: #selector(doLogout), for: .touchUpInside) - StorePaymentManager.shared.addPaymentObserver(self) - TunnelManager.shared.addObserver(self) + interactor.didReceiveDeviceState = { [weak self] newDeviceState in + self?.updateView(from: newDeviceState) + } + + interactor.didReceivePaymentEvent = { [weak self] event in + self?.didReceivePaymentEvent(event) + } - updateView(from: TunnelManager.shared.deviceState) + updateView(from: interactor.deviceState) applyViewState(animated: false) if StorePaymentManager.canMakePayments { @@ -96,20 +113,19 @@ class AccountViewController: UIViewController, StorePaymentObserver, TunnelObser } } - // MARK: - Private methods + // MARK: - Private private func requestStoreProducts() { let productKind = StoreSubscription.thirtyDays setProductState(.fetching(productKind), animated: true) - _ = StorePaymentManager.shared - .requestProducts(with: [productKind]) { [weak self] completion in - let productState: ProductState = completion.value?.products.first - .map { .received($0) } ?? .failed + _ = interactor.requestProducts(with: [productKind]) { [weak self] completion in + let productState: ProductState = completion.value?.products.first + .map { .received($0) } ?? .failed - self?.setProductState(productState, animated: true) - } + self?.setProductState(productState, animated: true) + } } private func setPaymentState(_ newState: PaymentState, animated: Bool) { @@ -158,6 +174,27 @@ class AccountViewController: UIViewController, StorePaymentObserver, TunnelObser navigationItem.setHidesBackButton(!isInteractionEnabled, animated: animated) } + private func didReceivePaymentEvent(_ event: StorePaymentEvent) { + guard case let .makingPayment(payment) = paymentState, + payment == event.payment else { return } + + switch event { + case let .finished(completion): + showTimeAddedConfirmationAlert(with: completion.serverResponse, context: .purchase) + + case let .failure(paymentFailure): + switch paymentFailure.error { + case .storePayment(SKError.paymentCancelled): + break + + default: + showPaymentErrorAlert(error: paymentFailure.error) + } + } + + setPaymentState(.none, animated: true) + } + private func showPaymentErrorAlert(error: StorePaymentManagerError) { let alertController = UIAlertController( title: NSLocalizedString( @@ -295,7 +332,7 @@ class AccountViewController: UIViewController, StorePaymentObserver, TunnelObser ) alertPresenter.enqueue(alertController, presentingController: self) { - TunnelManager.shared.unsetAccount { + self.interactor.logout { DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { alertController.dismiss(animated: true) { self.delegate?.accountViewControllerDidLogout(self) @@ -305,59 +342,6 @@ class AccountViewController: UIViewController, StorePaymentObserver, TunnelObser } } - // MARK: - TunnelObserver - - func tunnelManagerDidLoadConfiguration(_ manager: TunnelManager) { - // no-op - } - - func tunnelManager(_ manager: TunnelManager, didUpdateTunnelStatus tunnelStatus: TunnelStatus) { - // no-op - } - - func tunnelManager(_ manager: TunnelManager, didFailWithError error: Error) { - // no-op - } - - func tunnelManager( - _ manager: TunnelManager, - didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2 - ) { - // no-op - } - - func tunnelManager(_ manager: TunnelManager, didUpdateDeviceState deviceState: DeviceState) { - updateView(from: deviceState) - } - - // MARK: - StorePaymentObserver - - func storePaymentManager( - _ manager: StorePaymentManager, - didReceiveEvent event: StorePaymentEvent - ) { - guard case let .makingPayment(payment) = paymentState, - payment == event.payment else { return } - - switch event { - case let .finished(paymentCompletion): - showTimeAddedConfirmationAlert( - with: paymentCompletion.serverResponse, - context: .purchase - ) - - case let .failure(paymentFailure): - switch paymentFailure.error { - case .storePayment(SKError.paymentCancelled): - break - default: - showPaymentErrorAlert(error: paymentFailure.error) - } - } - - setPaymentState(.none, animated: true) - } - // MARK: - Actions @objc private func doLogout() { @@ -369,7 +353,7 @@ class AccountViewController: UIViewController, StorePaymentObserver, TunnelObser } private func copyAccountToken() { - guard let accountData = TunnelManager.shared.deviceState.accountData else { + guard let accountData = interactor.deviceState.accountData else { return } @@ -378,25 +362,27 @@ class AccountViewController: UIViewController, StorePaymentObserver, TunnelObser @objc private func doPurchase() { guard case let .received(product) = productState, - let accountData = TunnelManager.shared.deviceState.accountData + let accountData = interactor.deviceState.accountData else { return } let payment = SKPayment(product: product) - StorePaymentManager.shared.addPayment(payment, for: accountData.number) + interactor.addPayment(payment, for: accountData.number) setPaymentState(.makingPayment(payment), animated: true) } @objc private func restorePurchases() { - guard let accountData = TunnelManager.shared.deviceState.accountData else { + guard let accountData = interactor.deviceState.accountData else { return } setPaymentState(.restoringPurchases, animated: true) - _ = StorePaymentManager.shared.restorePurchases(for: accountData.number) { completion in + _ = interactor.restorePurchases(for: accountData.number) { [weak self] completion in + guard let self = self else { return } + switch completion { case let .success(response): self.showTimeAddedConfirmationAlert(with: response, context: .restoration) diff --git a/ios/MullvadVPN/PreferencesInteractor.swift b/ios/MullvadVPN/PreferencesInteractor.swift new file mode 100644 index 0000000000..8029669841 --- /dev/null +++ b/ios/MullvadVPN/PreferencesInteractor.swift @@ -0,0 +1,32 @@ +// +// PreferencesInteractor.swift +// MullvadVPN +// +// Created by pronebird on 31/10/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +final class PreferencesInteractor { + private let tunnelManager: TunnelManager + private var tunnelObserver: TunnelObserver? + + var dnsSettingsDidChange: ((DNSSettings) -> Void)? + + init(tunnelManager: TunnelManager) { + self.tunnelManager = tunnelManager + tunnelObserver = + TunnelBlockObserver(didUpdateTunnelSettings: { [weak self] manager, newSettings in + self?.dnsSettingsDidChange?(newSettings.dnsSettings) + }) + } + + var dnsSettings: DNSSettings { + return tunnelManager.settings.dnsSettings + } + + func setDNSSettings(_ newDNSSettings: DNSSettings, completion: (() -> Void)? = nil) { + tunnelManager.setDNSSettings(newDNSSettings, completionHandler: completion) + } +} diff --git a/ios/MullvadVPN/PreferencesViewController.swift b/ios/MullvadVPN/PreferencesViewController.swift index 5d70790569..ea85b62b0f 100644 --- a/ios/MullvadVPN/PreferencesViewController.swift +++ b/ios/MullvadVPN/PreferencesViewController.swift @@ -6,19 +6,18 @@ // Copyright © 2021 Mullvad VPN AB. All rights reserved. // -import MullvadLogging import UIKit -class PreferencesViewController: UITableViewController, PreferencesDataSourceDelegate, - TunnelObserver -{ +class PreferencesViewController: UITableViewController, PreferencesDataSourceDelegate { + private let interactor: PreferencesInteractor private let dataSource = PreferencesDataSource() override var preferredStatusBarStyle: UIStatusBarStyle { return .lightContent } - init() { + init(interactor: PreferencesInteractor) { + self.interactor = interactor super.init(style: .grouped) } @@ -45,8 +44,11 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel ) navigationItem.rightBarButtonItem = editButtonItem - TunnelManager.shared.addObserver(self) - dataSource.update(from: TunnelManager.shared.settings.dnsSettings) + interactor.dnsSettingsDidChange = { [weak self] newDNSSettings in + self?.dataSource.update(from: newDNSSettings) + } + + dataSource.update(from: interactor.dnsSettings) } override func setEditing(_ editing: Bool, animated: Bool) { @@ -68,31 +70,6 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel ) { let dnsSettings = dataModel.asDNSSettings() - TunnelManager.shared.setDNSSettings(dnsSettings) - } - - // MARK: - TunnelObserver - - func tunnelManagerDidLoadConfiguration(_ manager: TunnelManager) { - // no-op - } - - func tunnelManager(_ manager: TunnelManager, didUpdateTunnelStatus tunnelStatus: TunnelStatus) { - // no-op - } - - func tunnelManager(_ manager: TunnelManager, didFailWithError error: Error) { - // no-op - } - - func tunnelManager( - _ manager: TunnelManager, - didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2 - ) { - dataSource.update(from: tunnelSettings.dnsSettings) - } - - func tunnelManager(_ manager: TunnelManager, didUpdateDeviceState deviceState: DeviceState) { - // no-op + interactor.setDNSSettings(dnsSettings) } } diff --git a/ios/MullvadVPN/ProblemReportInteractor.swift b/ios/MullvadVPN/ProblemReportInteractor.swift new file mode 100644 index 0000000000..744eedcbac --- /dev/null +++ b/ios/MullvadVPN/ProblemReportInteractor.swift @@ -0,0 +1,64 @@ +// +// ProblemReportInteractor.swift +// MullvadVPN +// +// Created by pronebird on 25/10/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import MullvadREST +import MullvadTypes +import Operations + +final class ProblemReportInteractor { + private let apiProxy: REST.APIProxy + private let tunnelManager: TunnelManager + + private lazy var consolidatedLog: ConsolidatedApplicationLog = { + let securityGroupIdentifier = ApplicationConfiguration.securityGroupIdentifier + + // TODO: make sure we redact old tokens + + let redactStrings = [tunnelManager.deviceState.accountData?.number].compactMap { $0 } + + let report = ConsolidatedApplicationLog( + redactCustomStrings: redactStrings, + redactContainerPathsForSecurityGroupIdentifiers: [securityGroupIdentifier] + ) + + report.addLogFiles(fileURLs: ApplicationConfiguration.logFileURLs, includeLogBackups: true) + + return report + }() + + init(apiProxy: REST.APIProxy, tunnelManager: TunnelManager) { + self.apiProxy = apiProxy + self.tunnelManager = tunnelManager + } + + func sendReport( + email: String, + message: String, + completion: @escaping (OperationCompletion<Void, REST.Error>) -> Void + ) -> Cancellable { + let request = REST.ProblemReportRequest( + address: email, + message: message, + log: consolidatedLog.string, + metadata: consolidatedLog.metadata.reduce(into: [:]) { output, entry in + output[entry.key.rawValue] = entry.value + } + ) + + return apiProxy.sendProblemReport( + request, + retryStrategy: .default, + completionHandler: completion + ) + } + + var reportString: String { + return consolidatedLog.string + } +} diff --git a/ios/MullvadVPN/ProblemReportViewController.swift b/ios/MullvadVPN/ProblemReportViewController.swift index 5a543b5746..622cc1600c 100644 --- a/ios/MullvadVPN/ProblemReportViewController.swift +++ b/ios/MullvadVPN/ProblemReportViewController.swift @@ -7,33 +7,16 @@ // import MullvadREST +import MullvadTypes import Operations import UIKit class ProblemReportViewController: UIViewController, UITextFieldDelegate, ConditionalNavigation { - private let apiProxy = REST.ProxyFactory.shared.createAPIProxy() + private let interactor: ProblemReportInteractor private var textViewKeyboardResponder: AutomaticKeyboardResponder? private var scrollViewKeyboardResponder: AutomaticKeyboardResponder? - private lazy var consolidatedLog: ConsolidatedApplicationLog = { - let securityGroupIdentifier = ApplicationConfiguration.securityGroupIdentifier - - // TODO: make sure we redact old tokens - - let redactStrings = [TunnelManager.shared.deviceState.accountData?.number] - .compactMap { $0 } - - let report = ConsolidatedApplicationLog( - redactCustomStrings: redactStrings, - redactContainerPathsForSecurityGroupIdentifiers: [securityGroupIdentifier] - ) - - report.addLogFiles(fileURLs: ApplicationConfiguration.logFileURLs, includeLogBackups: true) - - return report - }() - /// Scroll view private lazy var scrollView: UIScrollView = { let scrollView = UIScrollView() @@ -204,6 +187,16 @@ class ProblemReportViewController: UIViewController, UITextFieldDelegate, Condit return false } + init(interactor: ProblemReportInteractor) { + 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() @@ -283,7 +276,7 @@ class ProblemReportViewController: UIViewController, UITextFieldDelegate, Condit @objc func handleViewLogsButtonTap() { let reviewController = ProblemReportReviewViewController( - reportString: consolidatedLog.string + reportString: interactor.reportString ) let navigationController = UINavigationController(rootViewController: reviewController) @@ -681,21 +674,12 @@ class ProblemReportViewController: UIViewController, UITextFieldDelegate, Condit private func sendProblemReport() { let viewModel = Self.persistentViewModel - let log = consolidatedLog.string - let metadata = consolidatedLog.metadata.reduce(into: [:]) { output, entry in - output[entry.key.rawValue] = entry.value - } - - let request = REST.ProblemReportRequest( - address: viewModel.email, - message: viewModel.message, - log: log, - metadata: metadata - ) - willSendProblemReport() - _ = apiProxy.sendProblemReport(request, retryStrategy: .default) { completion in + _ = interactor.sendReport( + email: viewModel.email, + message: viewModel.message + ) { completion in self.didSendProblemReport(viewModel: viewModel, completion: completion) } } diff --git a/ios/MullvadVPN/SceneDelegate.swift b/ios/MullvadVPN/SceneDelegate.swift index 00ac40f33f..6016af5792 100644 --- a/ios/MullvadVPN/SceneDelegate.swift +++ b/ios/MullvadVPN/SceneDelegate.swift @@ -345,7 +345,13 @@ extension SceneDelegate { private func makeSettingsNavigationController(route: SettingsNavigationRoute?) -> SettingsNavigationController { - let navController = SettingsNavigationController() + let navController = SettingsNavigationController( + interactorFactory: SettingsInteractorFactory( + storePaymentManager: .shared, + tunnelManager: .shared, + apiProxy: REST.ProxyFactory.shared.createAPIProxy() + ) + ) navController.settingsDelegate = self if UIDevice.current.userInterfaceIdiom == .pad { diff --git a/ios/MullvadVPN/SettingsDataSource.swift b/ios/MullvadVPN/SettingsDataSource.swift index 0f0f9b6cd2..ed4bf23c3d 100644 --- a/ios/MullvadVPN/SettingsDataSource.swift +++ b/ios/MullvadVPN/SettingsDataSource.swift @@ -8,7 +8,7 @@ import UIKit -class SettingsDataSource: NSObject, TunnelObserver, UITableViewDataSource, UITableViewDelegate { +final class SettingsDataSource: NSObject, UITableViewDataSource, UITableViewDelegate { private enum CellReuseIdentifiers: String, CaseIterable { case accountCell case basicCell @@ -50,6 +50,7 @@ class SettingsDataSource: NSObject, TunnelObserver, UITableViewDataSource, UITab } private var snapshot = DataSourceSnapshot<Section, Item>() + private let interactor: SettingsInteractor private var storedAccountData: StoredAccountData? weak var delegate: SettingsDataSourceDelegate? @@ -63,11 +64,15 @@ class SettingsDataSource: NSObject, TunnelObserver, UITableViewDataSource, UITab } } - override init() { + init(interactor: SettingsInteractor) { + self.interactor = interactor + super.init() - TunnelManager.shared.addObserver(self) - storedAccountData = TunnelManager.shared.deviceState.accountData + interactor.didUpdateDeviceState = { [weak self] deviceState in + self?.didUpdateDeviceState(deviceState) + } + storedAccountData = interactor.deviceState.accountData updateDataSnapshot() } @@ -91,7 +96,7 @@ class SettingsDataSource: NSObject, TunnelObserver, UITableViewDataSource, UITab private func updateDataSnapshot() { var newSnapshot = DataSourceSnapshot<Section, Item>() - if TunnelManager.shared.deviceState.isLoggedIn { + if interactor.deviceState.isLoggedIn { newSnapshot.appendSections([.main]) newSnapshot.appendItems([.account, .preferences, .shortcuts], in: .main) } @@ -130,7 +135,7 @@ class SettingsDataSource: NSObject, TunnelObserver, UITableViewDataSource, UITab value: "Account", comment: "" ) - cell.accountExpiryDate = TunnelManager.shared.deviceState.accountData?.expiry + cell.accountExpiryDate = interactor.deviceState.accountData?.expiry cell.accessibilityIdentifier = "AccountCell" cell.disclosureType = .chevron @@ -259,21 +264,9 @@ class SettingsDataSource: NSObject, TunnelObserver, UITableViewDataSource, UITab return 0 } - // MARK: - TunnelObserver - - func tunnelManagerDidLoadConfiguration(_ manager: TunnelManager) { - // no-op - } - - func tunnelManager(_ manager: TunnelManager, didFailWithError error: Error) { - // no-op - } - - func tunnelManager(_ manager: TunnelManager, didUpdateTunnelStatus tunnelStatus: TunnelStatus) { - // no-op - } + // MARK: - Private - func tunnelManager(_ manager: TunnelManager, didUpdateDeviceState deviceState: DeviceState) { + private func didUpdateDeviceState(_ deviceState: DeviceState) { let newAccountData = deviceState.accountData let oldAccountData = storedAccountData @@ -295,11 +288,4 @@ class SettingsDataSource: NSObject, TunnelObserver, UITableViewDataSource, UITab updateDataSnapshot() tableView?.reloadData() } - - func tunnelManager( - _ manager: TunnelManager, - didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2 - ) { - // no-op - } } diff --git a/ios/MullvadVPN/SettingsInteractor.swift b/ios/MullvadVPN/SettingsInteractor.swift new file mode 100644 index 0000000000..cd5c6e6e68 --- /dev/null +++ b/ios/MullvadVPN/SettingsInteractor.swift @@ -0,0 +1,33 @@ +// +// SettingsInteractor.swift +// MullvadVPN +// +// Created by pronebird on 26/10/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +final class SettingsInteractor { + private let tunnelManager: TunnelManager + private var tunnelObserver: TunnelObserver? + + var didUpdateDeviceState: ((DeviceState) -> Void)? + + var deviceState: DeviceState { + return tunnelManager.deviceState + } + + init(tunnelManager: TunnelManager) { + self.tunnelManager = tunnelManager + + let tunnelObserver = + TunnelBlockObserver(didUpdateDeviceState: { [weak self] manager, deviceState in + self?.didUpdateDeviceState?(deviceState) + }) + + tunnelManager.addObserver(tunnelObserver) + + self.tunnelObserver = tunnelObserver + } +} diff --git a/ios/MullvadVPN/SettingsInteractorFactory.swift b/ios/MullvadVPN/SettingsInteractorFactory.swift new file mode 100644 index 0000000000..521e587570 --- /dev/null +++ b/ios/MullvadVPN/SettingsInteractorFactory.swift @@ -0,0 +1,45 @@ +// +// SettingsInteractorFactory.swift +// MullvadVPN +// +// Created by pronebird on 26/10/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import MullvadREST + +final class SettingsInteractorFactory { + private let storePaymentManager: StorePaymentManager + private let tunnelManager: TunnelManager + private let apiProxy: REST.APIProxy + + init( + storePaymentManager: StorePaymentManager, + tunnelManager: TunnelManager, + apiProxy: REST.APIProxy + ) { + self.storePaymentManager = storePaymentManager + self.tunnelManager = tunnelManager + self.apiProxy = apiProxy + } + + func makeAccountInteractor() -> AccountInteractor { + return AccountInteractor( + storePaymentManager: storePaymentManager, + tunnelManager: tunnelManager + ) + } + + func makePreferencesInteractor() -> PreferencesInteractor { + return PreferencesInteractor(tunnelManager: tunnelManager) + } + + func makeProblemReportInteractor() -> ProblemReportInteractor { + return ProblemReportInteractor(apiProxy: apiProxy, tunnelManager: tunnelManager) + } + + func makeSettingsInteractor() -> SettingsInteractor { + return SettingsInteractor(tunnelManager: tunnelManager) + } +} diff --git a/ios/MullvadVPN/SettingsNavigationController.swift b/ios/MullvadVPN/SettingsNavigationController.swift index 58993fc6c4..6656eb2acb 100644 --- a/ios/MullvadVPN/SettingsNavigationController.swift +++ b/ios/MullvadVPN/SettingsNavigationController.swift @@ -6,7 +6,6 @@ // Copyright © 2020 Mullvad VPN AB. All rights reserved. // -import Foundation import UIKit enum SettingsNavigationRoute { @@ -37,6 +36,8 @@ protocol SettingsNavigationControllerDelegate: AnyObject { class SettingsNavigationController: CustomNavigationController, SettingsViewControllerDelegate, AccountViewControllerDelegate, UIAdaptivePresentationControllerDelegate { + private let interactorFactory: SettingsInteractorFactory + weak var settingsDelegate: SettingsNavigationControllerDelegate? override var childForStatusBarStyle: UIViewController? { @@ -47,7 +48,9 @@ class SettingsNavigationController: CustomNavigationController, SettingsViewCont return topViewController } - init() { + init(interactorFactory: SettingsInteractorFactory) { + self.interactorFactory = interactorFactory + super.init(navigationBarClass: CustomNavigationBar.self, toolbarClass: nil) navigationBar.prefersLargeTitles = true @@ -57,7 +60,7 @@ class SettingsNavigationController: CustomNavigationController, SettingsViewCont } required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) + fatalError("init(coder:) has not been implemented") } override func willPop(navigationItem: UINavigationItem) { @@ -102,23 +105,31 @@ class SettingsNavigationController: CustomNavigationController, SettingsViewCont private func makeViewController(for route: SettingsNavigationRoute) -> UIViewController { switch route { case .root: - let settingsController = SettingsViewController() - settingsController.delegate = self - return settingsController + let controller = SettingsViewController( + interactor: interactorFactory.makeSettingsInteractor() + ) + controller.delegate = self + return controller case .account: - let controller = AccountViewController() + let controller = AccountViewController( + interactor: interactorFactory.makeAccountInteractor() + ) controller.delegate = self return controller case .preferences: - return PreferencesViewController() + return PreferencesViewController( + interactor: interactorFactory.makePreferencesInteractor() + ) case .shortcuts: return ShortcutsViewController() case .problemReport: - return ProblemReportViewController() + return ProblemReportViewController( + interactor: interactorFactory.makeProblemReportInteractor() + ) } } diff --git a/ios/MullvadVPN/SettingsViewController.swift b/ios/MullvadVPN/SettingsViewController.swift index 0048c2eed3..3f5ce245df 100644 --- a/ios/MullvadVPN/SettingsViewController.swift +++ b/ios/MullvadVPN/SettingsViewController.swift @@ -23,9 +23,11 @@ class SettingsViewController: UITableViewController, SettingsDataSourceDelegate, return .lightContent } - private let dataSource = SettingsDataSource() + private let dataSource: SettingsDataSource + + init(interactor: SettingsInteractor) { + dataSource = SettingsDataSource(interactor: interactor) - init() { super.init(style: .grouped) } |
