diff --git a/ios/.swiftlint.yml b/ios/.swiftlint.yml index b5634621a301..9e7704961d8c 100644 --- a/ios/.swiftlint.yml +++ b/ios/.swiftlint.yml @@ -35,7 +35,8 @@ line_length: type_name: min_length: 4 max_length: - error: 50 + warning: 50 + error: 60 excluded: iPhone # excluded via string allowed_symbols: ["_"] # these are allowed in type names identifier_name: @@ -47,3 +48,7 @@ identifier_name: - URL - GlobalAPIKey reporter: "xcode" +nesting: + type_level: + warning: 2 + error: 4 diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 27f16941746a..6842e3d2363c 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -4077,7 +4077,6 @@ 58B465702A98C53300467203 /* RequestExecutorTests.swift in Sources */, A917352129FAAA5200D5DCFD /* TransportStrategyTests.swift in Sources */, 58FBFBE9291622580020E046 /* ExponentialBackoffTests.swift in Sources */, - 58FBFBF1291630700020E046 /* DurationTests.swift in Sources */, 58BDEB9D2A98F69E00F578F2 /* MemoryCache.swift in Sources */, 58BDEB9B2A98F58600F578F2 /* TimeServerProxy.swift in Sources */, 58BDEB992A98F4ED00F578F2 /* AnyTransport.swift in Sources */, diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index 4c99cfd85765..8c1487f91683 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -49,25 +49,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - configureLogging() - - logger = Logger(label: "AppDelegate") - let containerURL = ApplicationConfiguration.containerURL + configureLogging() + addressCache = REST.AddressCache(canWriteToCache: true, cacheDirectory: containerURL) addressCache.loadFromFile() - proxyFactory = REST.ProxyFactory.makeProxyFactory( - transportProvider: REST.AnyTransportProvider { [weak self] in - return self?.transportMonitor.makeTransport() - }, - addressCache: addressCache - ) - - apiProxy = proxyFactory.createAPIProxy() - accountsProxy = proxyFactory.createAccountsProxy() - devicesProxy = proxyFactory.createDevicesProxy() + setUpProxies(containerURL: containerURL) let relayCache = RelayCache(cacheDirectory: containerURL) relayCacheTracker = RelayCacheTracker(relayCache: relayCache, application: application, apiProxy: apiProxy) @@ -93,6 +82,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD relayConstraintsObserver = TunnelBlockObserver(didUpdateTunnelSettings: { _, settings in constraintsUpdater.onNewConstraints?(settings.relayConstraints) }) + tunnelManager.addObserver(relayConstraintsObserver) storePaymentManager = StorePaymentManager( application: application, @@ -110,15 +100,41 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD shadowsocksCache: shadowsocksCache, constraintsUpdater: constraintsUpdater ) + setUpTransportMonitor(transportProvider: transportProvider) + setUpSimulatorHost(transportProvider: transportProvider) - tunnelManager.addObserver(relayConstraintsObserver) + registerBackgroundTasks() + setupPaymentHandler() + setupNotifications() + addApplicationNotifications(application: application) + + startInitialization(application: application) + + return true + } + + private func setUpProxies(containerURL: URL) { + proxyFactory = REST.ProxyFactory.makeProxyFactory( + transportProvider: REST.AnyTransportProvider { [weak self] in + return self?.transportMonitor.makeTransport() + }, + addressCache: addressCache + ) + + apiProxy = proxyFactory.createAPIProxy() + accountsProxy = proxyFactory.createAccountsProxy() + devicesProxy = proxyFactory.createDevicesProxy() + } + private func setUpTransportMonitor(transportProvider: TransportProvider) { transportMonitor = TransportMonitor( tunnelManager: tunnelManager, tunnelStore: tunnelStore, transportProvider: transportProvider ) + } + private func setUpSimulatorHost(transportProvider: TransportProvider) { #if targetEnvironment(simulator) // Configure mock tunnel provider on simulator simulatorTunnelProviderHost = SimulatorTunnelProviderHost( @@ -127,15 +143,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD ) SimulatorTunnelProvider.shared.delegate = simulatorTunnelProviderHost #endif - - registerBackgroundTasks() - setupPaymentHandler() - setupNotifications() - addApplicationNotifications(application: application) - - startInitialization(application: application) - - return true } // MARK: - UISceneSession lifecycle @@ -318,6 +325,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD loggerBuilder.addOSLogOutput(subsystem: ApplicationTarget.mainApp.bundleIdentifier) #endif loggerBuilder.install() + + logger = Logger(label: "AppDelegate") } private func addApplicationNotifications(application: UIApplication) { @@ -360,8 +369,26 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD private func startInitialization(application: UIApplication) { let wipeSettingsOperation = getWipeSettingsOperation() + let loadTunnelStoreOperation = getLoadTunnelStoreOperation() + let migrateSettingsOperation = getMigrateSettingsOperation(application: application) + let initTunnelManagerOperation = getInitTunnelManagerOperation() + + migrateSettingsOperation.addDependencies([wipeSettingsOperation, loadTunnelStoreOperation]) + initTunnelManagerOperation.addDependency(migrateSettingsOperation) + + operationQueue.addOperations( + [ + wipeSettingsOperation, + loadTunnelStoreOperation, + migrateSettingsOperation, + initTunnelManagerOperation, + ], + waitUntilFinished: false + ) + } - let loadTunnelStoreOperation = AsyncBlockOperation(dispatchQueue: .main) { [self] finish in + private func getLoadTunnelStoreOperation() -> AsyncBlockOperation { + AsyncBlockOperation(dispatchQueue: .main) { [self] finish in tunnelStore.loadPersistentTunnels { [self] error in if let error { logger.error( @@ -372,8 +399,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD finish(nil) } } + } - let migrateSettingsOperation = AsyncBlockOperation(dispatchQueue: .main) { [self] finish in + private func getMigrateSettingsOperation(application: UIApplication) -> AsyncBlockOperation { + AsyncBlockOperation(dispatchQueue: .main) { [self] finish in migrationManager .migrateSettings(store: SettingsManager.store, proxyFactory: proxyFactory) { [self] migrationResult in switch migrationResult { @@ -387,8 +416,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD finish(nil) case let .failure(error): - let migrationUIHandler = application.connectedScenes.first { $0 is SettingsMigrationUIHandler } - as? SettingsMigrationUIHandler + let migrationUIHandler = application.connectedScenes + .first { $0 is SettingsMigrationUIHandler } as? SettingsMigrationUIHandler if let migrationUIHandler { migrationUIHandler.showMigrationError(error) { @@ -400,12 +429,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } } } + } - migrateSettingsOperation.addDependencies([wipeSettingsOperation, loadTunnelStoreOperation]) - - let initTunnelManagerOperation = AsyncBlockOperation(dispatchQueue: .main) { finish in + private func getInitTunnelManagerOperation() -> AsyncBlockOperation { + AsyncBlockOperation(dispatchQueue: .main) { finish in self.tunnelManager.loadConfiguration { error in - // TODO: avoid throwing fatal error and show the problem report UI instead. if let error { fatalError(error.localizedDescription) } @@ -418,17 +446,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD finish(nil) } } - initTunnelManagerOperation.addDependency(migrateSettingsOperation) - - operationQueue.addOperations( - [ - wipeSettingsOperation, - loadTunnelStoreOperation, - migrateSettingsOperation, - initTunnelManagerOperation, - ], - waitUntilFinished: false - ) } /// Returns an operation that acts on two conditions: @@ -492,4 +509,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD ) { completionHandler([.list, .banner, .sound]) } + + // swiftlint:disable:next file_length } diff --git a/ios/MullvadVPN/Classes/AutomaticKeyboardResponder.swift b/ios/MullvadVPN/Classes/AutomaticKeyboardResponder.swift index 99db0e966ebe..fa91118ba9b9 100644 --- a/ios/MullvadVPN/Classes/AutomaticKeyboardResponder.swift +++ b/ios/MullvadVPN/Classes/AutomaticKeyboardResponder.swift @@ -21,7 +21,9 @@ class AutomaticKeyboardResponder { init(targetView: T, handler: @escaping (T, CGFloat) -> Void) { self.targetView = targetView self.handler = { view, adjustment in - handler(view as! T, adjustment) + if let view = view as? T { + handler(view, adjustment) + } } NotificationCenter.default.addObserver( @@ -112,7 +114,7 @@ class AutomaticKeyboardResponder { \.frame, options: [.new], changeHandler: { [weak self] _, _ in - guard let self, + guard let self = self, let keyboardFrameValue = lastKeyboardRect else { return } adjustContentInsets(convertedKeyboardFrameEnd: keyboardFrameValue) diff --git a/ios/MullvadVPN/Classes/ConsolidatedApplicationLog.swift b/ios/MullvadVPN/Classes/ConsolidatedApplicationLog.swift index 22b9ec86f0bc..f6f5f0956fcb 100644 --- a/ios/MullvadVPN/Classes/ConsolidatedApplicationLog.swift +++ b/ios/MullvadVPN/Classes/ConsolidatedApplicationLog.swift @@ -176,6 +176,7 @@ class ConsolidatedApplicationLog: TextOutputStreamable { private static func redactAccountNumber(string: String) -> String { redact( + // swiftlint:disable:next force_try regularExpression: try! NSRegularExpression(pattern: #"\d{16}"#), string: string, replacementString: kRedactedAccountPlaceholder diff --git a/ios/MullvadVPN/Classes/InputTextFormatter.swift b/ios/MullvadVPN/Classes/InputTextFormatter.swift index acb95e7960e7..c9a0cb88c59e 100644 --- a/ios/MullvadVPN/Classes/InputTextFormatter.swift +++ b/ios/MullvadVPN/Classes/InputTextFormatter.swift @@ -75,8 +75,7 @@ class InputTextFormatter: NSObject, UITextFieldDelegate, UITextPasteDelegate { // Since removing separator alone makes no sense, this computation extends the string range // to include the digit preceding a separator. - if replacementString.isEmpty, emptySelection, - !formattedString.isEmpty { + if replacementString.isEmpty, emptySelection, !formattedString.isEmpty { let precedingDigitIndex = formattedString .prefix(through: stringRange.lowerBound) .lastIndex { isAllowed($0) } ?? formattedString.startIndex @@ -85,19 +84,13 @@ class InputTextFormatter: NSObject, UITextFieldDelegate, UITextPasteDelegate { } // Replace the given range within a formatted string - let newString = formattedString.replacingCharacters( - in: stringRange, - with: replacementString - ) + let newString = formattedString.replacingCharacters(in: stringRange, with: replacementString) // Number of digits within a string var numDigits = 0 // Insertion location within the input string - let insertionLocation = formattedString.distance( - from: formattedString.startIndex, - to: stringRange.lowerBound - ) + let insertionLocation = formattedString.distance(from: formattedString.startIndex, to: stringRange.lowerBound) // Original caret location based on insertion location + number of characters added let originalCaretPosition = insertionLocation + replacementString.count diff --git a/ios/MullvadVPN/Containers/Root/RootContainerViewController.swift b/ios/MullvadVPN/Containers/Root/RootContainerViewController.swift index afb28ce12499..2e825c8b2f75 100644 --- a/ios/MullvadVPN/Containers/Root/RootContainerViewController.swift +++ b/ios/MullvadVPN/Containers/Root/RootContainerViewController.swift @@ -484,54 +484,141 @@ class RootContainerViewController: UIViewController { // hide in-App notificationBanner when the container decides to keep it invisible isNavigationBarHidden = (targetViewController as? RootContainment)?.prefersNotificationBarHidden ?? false - 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, - sourceViewController != targetViewController { - self.endChildControllerTransition(targetViewController) - } - - if let sourceViewController, - sourceViewController != targetViewController { - self.endChildControllerTransition(sourceViewController) - } - } + configureViewControllers( + viewControllersToAdd: viewControllersToAdd, + newViewControllers: newViewControllers, + targetViewController: targetViewController, + viewControllersToRemove: viewControllersToRemove + ) + + beginTransition( + shouldHandleAppearanceEvents: shouldHandleAppearanceEvents, + targetViewController: targetViewController, + shouldAnimate: shouldAnimate, + sourceViewController: sourceViewController + ) + + let finishTransition = { [weak self] in + self?.onTransitionEnd( + shouldHandleAppearanceEvents: shouldHandleAppearanceEvents, + sourceViewController: sourceViewController, + targetViewController: targetViewController, + viewControllersToAdd: viewControllersToAdd, + viewControllersToRemove: viewControllersToRemove + ) + + completion?() + } - // Notify the added controllers that they finished a transition into the container - for child in viewControllersToAdd { - child.didMove(toParent: self) + let alongSideAnimations = { [weak self] in + guard let self = self else { return } + + updateHeaderBarStyleFromChildPreferences(animated: shouldAnimate) + updateHeaderBarHiddenFromChildPreferences(animated: shouldAnimate) + updateNotificationBarHiddenFromChildPreferences() + updateDeviceInfoBarHiddenFromChildPreferences() + } + + if shouldAnimate { + CATransaction.begin() + CATransaction.setCompletionBlock { + finishTransition() } - // Remove the controllers that transitioned out of the container - // The call to removeFromParent() automatically calls child.didMove() - for child in viewControllersToRemove { - child.view.removeFromSuperview() - child.removeFromParent() + animateTransition( + sourceViewController: sourceViewController, + newViewControllers: newViewControllers, + targetViewController: targetViewController, + isUnwinding: isUnwinding, + alongSideAnimations: alongSideAnimations + ) + + CATransaction.commit() + } else { + alongSideAnimations() + finishTransition() + } + } + + private func animateTransition( + sourceViewController: UIViewController?, + newViewControllers: [UIViewController], + targetViewController: UIViewController?, + isUnwinding: Bool, + alongSideAnimations: () -> Void + ) { + let transition = CATransition() + transition.duration = 0.35 + transition.type = .push + + // Pick the animation movement direction + let sourceIndex = sourceViewController.flatMap { newViewControllers.firstIndex(of: $0) } + let targetIndex = targetViewController.flatMap { newViewControllers.firstIndex(of: $0) } + + switch (sourceIndex, targetIndex) { + case let (.some(lhs), .some(rhs)): + transition.subtype = lhs > rhs ? .fromLeft : .fromRight + case (.none, .some): + transition.subtype = isUnwinding ? .fromLeft : .fromRight + default: + transition.subtype = .fromRight + } + + transitionContainer.layer.add(transition, forKey: "transition") + alongSideAnimations() + } + + private func onTransitionEnd( + shouldHandleAppearanceEvents: Bool, + sourceViewController: UIViewController?, + targetViewController: UIViewController?, + viewControllersToAdd: [UIViewController], + viewControllersToRemove: [UIViewController] + ) { + /* + 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, + sourceViewController != targetViewController { + self.endChildControllerTransition(targetViewController) } - // Remove the source controller from view hierarchy - if sourceViewController != targetViewController { - sourceViewController?.view.removeFromSuperview() + if let sourceViewController, + sourceViewController != targetViewController { + self.endChildControllerTransition(sourceViewController) } + } - self.updateInterfaceOrientation(attemptRotateToDeviceOrientation: true) - self.updateAccessibilityElementsAndNotifyScreenChange() + // Notify the added controllers that they finished a transition into the container + for child in viewControllersToAdd { + child.didMove(toParent: self) + } - completion?() + // Remove the controllers that transitioned out of the container + // The call to removeFromParent() automatically calls child.didMove() + for child in viewControllersToRemove { + child.view.removeFromSuperview() + child.removeFromParent() } - let alongSideAnimations = { - self.updateHeaderBarStyleFromChildPreferences(animated: shouldAnimate) - self.updateHeaderBarHiddenFromChildPreferences(animated: shouldAnimate) - self.updateNotificationBarHiddenFromChildPreferences() - self.updateDeviceInfoBarHiddenFromChildPreferences() + // Remove the source controller from view hierarchy + if sourceViewController != targetViewController { + sourceViewController?.view.removeFromSuperview() } + self.updateInterfaceOrientation(attemptRotateToDeviceOrientation: true) + self.updateAccessibilityElementsAndNotifyScreenChange() + } + + private func configureViewControllers( + viewControllersToAdd: [UIViewController], + newViewControllers: [UIViewController], + targetViewController: UIViewController?, + viewControllersToRemove: [UIViewController] + ) { // Add new child controllers. The call to addChild() automatically calls child.willMove() // Children have to be registered in the container for Storyboard unwind segues to function // properly, however the child controller views don't have to be added immediately, and @@ -558,8 +645,14 @@ class RootContainerViewController: UIViewController { } viewControllers = newViewControllers + } - // Begin appearance transition + private func beginTransition( + shouldHandleAppearanceEvents: Bool, + targetViewController: UIViewController?, + shouldAnimate: Bool, + sourceViewController: UIViewController? + ) { if shouldHandleAppearanceEvents { if let sourceViewController, sourceViewController != targetViewController { @@ -579,38 +672,6 @@ class RootContainerViewController: UIViewController { } setNeedsStatusBarAppearanceUpdate() } - - if shouldAnimate { - CATransaction.begin() - CATransaction.setCompletionBlock { - finishTransition() - } - - let transition = CATransition() - transition.duration = 0.35 - transition.type = .push - - // Pick the animation movement direction - let sourceIndex = sourceViewController.flatMap { newViewControllers.firstIndex(of: $0) } - let targetIndex = targetViewController.flatMap { newViewControllers.firstIndex(of: $0) } - - switch (sourceIndex, targetIndex) { - case let (.some(lhs), .some(rhs)): - transition.subtype = lhs > rhs ? .fromLeft : .fromRight - case (.none, .some): - transition.subtype = isUnwinding ? .fromLeft : .fromRight - default: - transition.subtype = .fromRight - } - - transitionContainer.layer.add(transition, forKey: "transition") - alongSideAnimations() - - CATransaction.commit() - } else { - alongSideAnimations() - finishTransition() - } } private func addChildView(_ childView: UIView) { @@ -840,4 +901,6 @@ extension RootContainerViewController { presentationContainerAccountButton?.isHidden = !configuration.showsAccountButton headerBarView.update(configuration: configuration) } + + // swiftlint:disable:next file_length } diff --git a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift index 2c4a0cacab15..08d98f1d15d4 100644 --- a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift @@ -572,7 +572,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo ) coordinator.didFinishPayment = { [weak self] _ in - guard let self else { return } + guard let self = self else { return } if shouldDismissOutOfTime() { router.dismiss(.outOfTime, animated: true) @@ -597,7 +597,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo ) coordinator.didFinish = { [weak self] _ in - guard let self else { return } + guard let self = self else { return } appPreferences.isShownOnboarding = true router.dismiss(.welcome, animated: false) continueFlow(animated: false) @@ -979,4 +979,6 @@ fileprivate extension AppPreferencesDataSource { mutating func markChangeLogSeen() { self.lastSeenChangeLogVersion = Bundle.main.shortVersion } + + // swiftlint:disable:next file_length } diff --git a/ios/MullvadVPN/Coordinators/LoginCoordinator.swift b/ios/MullvadVPN/Coordinators/LoginCoordinator.swift index 444913b059c0..fa9f9adc2dad 100644 --- a/ios/MullvadVPN/Coordinators/LoginCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/LoginCoordinator.swift @@ -108,7 +108,7 @@ final class LoginCoordinator: Coordinator, DeviceManagementViewControllerDelegat let controller = DeviceManagementViewController(interactor: interactor) controller.delegate = self controller.fetchDevices(animateUpdates: false) { [weak self] result in - guard let self else { return } + guard let self = self else { return } switch result { case .success: diff --git a/ios/MullvadVPN/Coordinators/WelcomeCoordinator.swift b/ios/MullvadVPN/Coordinators/WelcomeCoordinator.swift index 33a09b41718e..dde31b4166b6 100644 --- a/ios/MullvadVPN/Coordinators/WelcomeCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/WelcomeCoordinator.swift @@ -146,7 +146,7 @@ extension WelcomeCoordinator: WelcomeViewControllerDelegate { ) coordinator.didCancel = { [weak self] coordinator in - guard let self else { return } + guard let self = self else { return } navigationController.popViewController(animated: true) coordinator.removeFromParent() } diff --git a/ios/MullvadVPN/Extensions/NSRegularExpression+IPAddress.swift b/ios/MullvadVPN/Extensions/NSRegularExpression+IPAddress.swift index 7cb5ae90da59..155de03d0a38 100644 --- a/ios/MullvadVPN/Extensions/NSRegularExpression+IPAddress.swift +++ b/ios/MullvadVPN/Extensions/NSRegularExpression+IPAddress.swift @@ -22,6 +22,7 @@ extension NSRegularExpression { return try! NSRegularExpression(pattern: pattern, options: [.allowCommentsAndWhitespace]) } + // swiftlint:disable line_length static var ipv6RegularExpression: NSRegularExpression { // Regular expression obtained from: // https://stackoverflow.com/a/17871737 @@ -49,4 +50,5 @@ extension NSRegularExpression { // swiftlint:disable:next force_try return try! NSRegularExpression(pattern: pattern, options: [.allowCommentsAndWhitespace]) } + // swiftlint:enable line_length } diff --git a/ios/MullvadVPN/Notifications/Notification Providers/RegisteredDeviceInAppNotificationProvider.swift b/ios/MullvadVPN/Notifications/Notification Providers/RegisteredDeviceInAppNotificationProvider.swift index b9f2bb630740..785975bfee79 100644 --- a/ios/MullvadVPN/Notifications/Notification Providers/RegisteredDeviceInAppNotificationProvider.swift +++ b/ios/MullvadVPN/Notifications/Notification Providers/RegisteredDeviceInAppNotificationProvider.swift @@ -58,7 +58,7 @@ final class RegisteredDeviceInAppNotificationProvider: NotificationProvider, action: .init( image: .init(named: "IconCloseSml"), handler: { [weak self] in - guard let self else { return } + guard let self = self else { return } isNewDeviceRegistered = false sendAction() invalidate() diff --git a/ios/MullvadVPN/Notifications/NotificationManager.swift b/ios/MullvadVPN/Notifications/NotificationManager.swift index 11da16407f34..3f722a628028 100644 --- a/ios/MullvadVPN/Notifications/NotificationManager.swift +++ b/ios/MullvadVPN/Notifications/NotificationManager.swift @@ -207,7 +207,10 @@ final class NotificationManager: NotificationProviderDelegate { } } - // Invalidate in-app notification + invalidateInAppNotification(notificationProvider) + } + + private func invalidateInAppNotification(_ notificationProvider: NotificationProvider) { if let notificationProvider = notificationProvider as? InAppNotificationProvider { var newNotificationDescriptors = inAppNotificationDescriptors diff --git a/ios/MullvadVPN/Presentation controllers/FormsheetPresentationController.swift b/ios/MullvadVPN/Presentation controllers/FormsheetPresentationController.swift index 5bee1b2156b3..2b2b1cfe8965 100644 --- a/ios/MullvadVPN/Presentation controllers/FormsheetPresentationController.swift +++ b/ios/MullvadVPN/Presentation controllers/FormsheetPresentationController.swift @@ -177,7 +177,7 @@ class FormSheetPresentationController: UIPresentationController { keyboardResponder = AutomaticKeyboardResponder( targetView: presentedView, handler: { [weak self] view, adjustment in - guard let self, + guard let self = self, let containerView, !isInFullScreenPresentation else { return } let frame = view.frame diff --git a/ios/MullvadVPN/SceneDelegate.swift b/ios/MullvadVPN/SceneDelegate.swift index 4720556fe4b2..c1e9b91f9c4a 100644 --- a/ios/MullvadVPN/SceneDelegate.swift +++ b/ios/MullvadVPN/SceneDelegate.swift @@ -27,6 +27,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, SettingsMigrationUIHand private var tunnelObserver: TunnelObserver? private var appDelegate: AppDelegate { + // swiftlint:disable:next force_cast UIApplication.shared.delegate as! AppDelegate } diff --git a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProvider.swift b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProvider.swift index 637e0ad5f34d..a2eb3d5f4314 100644 --- a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProvider.swift +++ b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProvider.swift @@ -423,4 +423,5 @@ final class SimulatorTunnelProviderManager: NSObject, VPNTunnelProviderManagerPr } } +// swiftlint:disable:next file_length #endif diff --git a/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift b/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift index 5c0d4f83a8a7..da232dcde48f 100644 --- a/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift +++ b/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift @@ -47,23 +47,7 @@ class MapConnectionStatusOperation: AsyncOperation { switch connectionStatus { case .connecting: - switch tunnelState { - case .connecting: - break - - default: - interactor.updateTunnelStatus { tunnelStatus in - tunnelStatus.state = .connecting(nil) - } - } - - fetchTunnelStatus(tunnel: tunnel) { packetTunnelStatus in - if packetTunnelStatus.isNetworkReachable { - return packetTunnelStatus.tunnelRelay.map { .connecting($0) } - } else { - return .waitingForConnectivity(.noConnection) - } - } + handleConnectingState(tunnelState, tunnel) return case .reasserting: @@ -87,36 +71,10 @@ class MapConnectionStatusOperation: AsyncOperation { return case .disconnected: - switch tunnelState { - case .pendingReconnect: - logger.debug("Ignore disconnected state when pending reconnect.") - - case .disconnecting(.reconnect): - logger.debug("Restart the tunnel on disconnect.") - interactor.updateTunnelStatus { tunnelStatus in - tunnelStatus = TunnelStatus() - tunnelStatus.state = .pendingReconnect - } - interactor.startTunnel() - - default: - setTunnelDisconnectedStatus() - } + handleDisconnectedState(tunnelState) case .disconnecting: - switch tunnelState { - case .disconnecting: - break - default: - interactor.updateTunnelStatus { tunnelStatus in - let packetTunnelStatus = tunnelStatus.packetTunnelStatus - - tunnelStatus = TunnelStatus() - tunnelStatus.state = packetTunnelStatus.isNetworkReachable - ? .disconnecting(.nothing) - : .waitingForConnectivity(.noNetwork) - } - } + handleDisconnectionState(tunnelState) case .invalid: setTunnelDisconnectedStatus() @@ -132,6 +90,60 @@ class MapConnectionStatusOperation: AsyncOperation { request?.cancel() } + private func handleConnectingState(_ tunnelState: TunnelState, _ tunnel: Tunnel) { + switch tunnelState { + case .connecting: + break + + default: + interactor.updateTunnelStatus { tunnelStatus in + tunnelStatus.state = .connecting(nil) + } + } + + fetchTunnelStatus(tunnel: tunnel) { packetTunnelStatus in + if packetTunnelStatus.isNetworkReachable { + return packetTunnelStatus.tunnelRelay.map { .connecting($0) } + } else { + return .waitingForConnectivity(.noConnection) + } + } + } + + private func handleDisconnectionState(_ tunnelState: TunnelState) { + switch tunnelState { + case .disconnecting: + break + default: + interactor.updateTunnelStatus { tunnelStatus in + let packetTunnelStatus = tunnelStatus.packetTunnelStatus + + tunnelStatus = TunnelStatus() + tunnelStatus.state = packetTunnelStatus.isNetworkReachable + ? .disconnecting(.nothing) + : .waitingForConnectivity(.noNetwork) + } + } + } + + private func handleDisconnectedState(_ tunnelState: TunnelState) { + switch tunnelState { + case .pendingReconnect: + logger.debug("Ignore disconnected state when pending reconnect.") + + case .disconnecting(.reconnect): + logger.debug("Restart the tunnel on disconnect.") + interactor.updateTunnelStatus { tunnelStatus in + tunnelStatus = TunnelStatus() + tunnelStatus.state = .pendingReconnect + } + interactor.startTunnel() + + default: + setTunnelDisconnectedStatus() + } + } + private func setTunnelDisconnectedStatus() { interactor.updateTunnelStatus { tunnelStatus in tunnelStatus = TunnelStatus() diff --git a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift index 108b6be81bc9..080fdc36ab9e 100644 --- a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift +++ b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift @@ -216,10 +216,7 @@ class SetAccountOperation: ResultOperation { let result = result.inspectError { error in guard !error.isOperationCancellationError else { return } - logger.error( - error: error, - message: "Failed to create new account." - ) + logger.error(error: error, message: "Failed to create new account.") }.map { newAccountData -> StoredAccountData in logger.debug("Created new account.") @@ -322,10 +319,7 @@ class SetAccountOperation: ResultOperation { dispatchQueue.async { [self] in // Ignore error but log it. if let error { - logger.error( - error: error, - message: "Failed to remove VPN configuration." - ) + logger.error(error: error, message: "Failed to remove VPN configuration.") } interactor.setTunnel(nil, shouldRefreshTunnelState: false) @@ -338,11 +332,7 @@ class SetAccountOperation: ResultOperation { /// Create new private key and create new device via API. private func createDevice(accountNumber: String, completion: @escaping (Result) -> Void) { let privateKey = PrivateKey() - - let request = REST.CreateDeviceRequest( - publicKey: privateKey.publicKey, - hijackDNS: false - ) + let request = REST.CreateDeviceRequest(publicKey: privateKey.publicKey, hijackDNS: false) logger.debug("Create device...") diff --git a/ios/MullvadVPN/TunnelManager/Tunnel.swift b/ios/MullvadVPN/TunnelManager/Tunnel.swift index fa830f1298d5..5a9990c8862d 100644 --- a/ios/MullvadVPN/TunnelManager/Tunnel.swift +++ b/ios/MullvadVPN/TunnelManager/Tunnel.swift @@ -108,9 +108,9 @@ final class Tunnel: Equatable { } func sendProviderMessage(_ messageData: Data, responseHandler: ((Data?) -> Void)?) throws { - let session = tunnelProvider.connection as! VPNTunnelProviderSessionProtocol + let session = tunnelProvider.connection as? VPNTunnelProviderSessionProtocol - try session.sendProviderMessage(messageData, responseHandler: responseHandler) + try session?.sendProviderMessage(messageData, responseHandler: responseHandler) } func setConfiguration(_ configuration: TunnelConfiguration) { diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift index 905aec844540..ee091f865552 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift @@ -506,7 +506,7 @@ final class TunnelManager: StorePaymentObserver { operation.completionQueue = .main operation.completionHandler = { [weak self] result in - guard let self else { return } + guard let self = self else { return } updatePrivateKeyRotationTimer() @@ -755,7 +755,6 @@ final class TunnelManager: StorePaymentObserver { } case .invalid: - // TODO: handle invalid account in some way? break } } diff --git a/ios/MullvadVPN/UI appearance/UIMetrics.swift b/ios/MullvadVPN/UI appearance/UIMetrics.swift index d3145a821fc3..b257d5d46a8d 100644 --- a/ios/MullvadVPN/UI appearance/UIMetrics.swift +++ b/ios/MullvadVPN/UI appearance/UIMetrics.swift @@ -53,6 +53,20 @@ enum UIMetrics { static let textFieldContentInsets = UIEdgeInsets(top: 8, left: 24, bottom: 8, right: 24) static let textFieldNonEditingContentInsetLeft: CGFloat = 40 } + + /// Group of constants related to in-app notifications banner. + enum InAppBannerNotification { + /// Layout margins for contents presented within the banner. + static let layoutMargins = NSDirectionalEdgeInsets(top: 16, leading: 24, bottom: 16, trailing: 24) + + /// Size of little round severity indicator. + static let indicatorSize = CGSize(width: 12, height: 12) + } + + enum DisconnectSplitButton { + static let secondaryButtonPhone = CGSize(width: 42, height: 42) + static let secondaryButtonPad = CGSize(width: 52, height: 52) + } } extension UIMetrics { @@ -83,15 +97,6 @@ extension UIMetrics { /// Common cell indentation width static let cellIndentationWidth: CGFloat = 16 - /// Group of constants related to in-app notifications banner. - enum InAppBannerNotification { - /// Layout margins for contents presented within the banner. - static let layoutMargins = NSDirectionalEdgeInsets(top: 16, leading: 24, bottom: 16, trailing: 24) - - /// Size of little round severity indicator. - static let indicatorSize = CGSize(width: 12, height: 12) - } - /// Spacing used in stack views of buttons static let interButtonSpacing: CGFloat = 16 diff --git a/ios/MullvadVPN/View controllers/Account/AccountViewController.swift b/ios/MullvadVPN/View controllers/Account/AccountViewController.swift index 4c50c91110c9..8878f6ae302d 100644 --- a/ios/MullvadVPN/View controllers/Account/AccountViewController.swift +++ b/ios/MullvadVPN/View controllers/Account/AccountViewController.swift @@ -267,7 +267,7 @@ class AccountViewController: UIViewController { setPaymentState(.restoringPurchases, animated: true) _ = interactor.restorePurchases(for: accountData.number) { [weak self] completion in - guard let self else { return } + guard let self = self else { return } switch completion { case let .success(response): diff --git a/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementContentView.swift b/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementContentView.swift index aec42d87fde3..0df7733b8087 100644 --- a/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementContentView.swift +++ b/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementContentView.swift @@ -195,21 +195,7 @@ class DeviceManagementContentView: UIView { difference.forEach { change in switch change { case let .insert(offset, model, _): - let view = DeviceRowView(viewModel: model) - - view.isHidden = true - view.alpha = 0 - - view.deleteHandler = { [weak self] _ in - view.showsActivityIndicator = true - - self?.handleDeviceDeletion?(view.viewModel) { - view.showsActivityIndicator = false - } - } - - viewsToAdd.append((view, offset)) - + viewsToAdd.append((createDeviceRowView(from: model), offset)) case let .remove(offset, _, _): viewsToRemove.append(deviceStackView.arrangedSubviews[offset]) } @@ -226,41 +212,58 @@ class DeviceManagementContentView: UIView { } } - let showHideViews = { - viewsToRemove.forEach { view in - view.alpha = 0 - view.isHidden = true - } - - viewsToAdd.forEach { item in - item.view.alpha = 1 - item.view.isHidden = false - } - } - - let removeViews = { - viewsToRemove.forEach { view in - view.removeFromSuperview() - } - } - if animated { UIView.animate( withDuration: 0.25, delay: 0, options: [.curveEaseInOut], animations: { [weak self] in - showHideViews() + self?.showHideViews(viewsToAdd: viewsToAdd, viewsToRemove: viewsToRemove) self?.deviceStackView.layoutIfNeeded() }, - completion: { _ in - removeViews() + completion: { [weak self] _ in + self?.removeViews(viewsToRemove: viewsToRemove) } ) } else { - showHideViews() - removeViews() + showHideViews(viewsToAdd: viewsToAdd, viewsToRemove: viewsToRemove) + removeViews(viewsToRemove: viewsToRemove) + } + } + + private func showHideViews(viewsToAdd: [(view: UIView, offset: Int)], viewsToRemove: [UIView]) { + viewsToRemove.forEach { view in + view.alpha = 0 + view.isHidden = true + } + + viewsToAdd.forEach { item in + item.view.alpha = 1 + item.view.isHidden = false + } + } + + private func removeViews(viewsToRemove: [UIView]) { + viewsToRemove.forEach { view in + view.removeFromSuperview() + } + } + + private func createDeviceRowView(from model: DeviceViewModel) -> DeviceRowView { + let view = DeviceRowView(viewModel: model) + + view.isHidden = true + view.alpha = 0 + + view.deleteHandler = { [weak self] _ in + view.showsActivityIndicator = true + + self?.handleDeviceDeletion?(view.viewModel) { + view.showsActivityIndicator = false + } } + + return view } private func updateView() { diff --git a/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementViewController.swift b/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementViewController.swift index fa8e1f0945dc..cd9b7c34bd17 100644 --- a/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementViewController.swift +++ b/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementViewController.swift @@ -89,7 +89,7 @@ class DeviceManagementViewController: UIViewController, RootContainment { completionHandler: ((Result) -> Void)? = nil ) { interactor.getDevices { [weak self] result in - guard let self else { return } + guard let self = self else { return } if let devices = result.value { setDevices(devices, animated: animateUpdates) @@ -224,7 +224,7 @@ class DeviceManagementViewController: UIViewController, RootContainment { private func deleteDevice(identifier: String, completionHandler: @escaping (Error?) -> Void) { interactor.deleteDevice(identifier) { [weak self] completion in - guard let self else { return } + guard let self = self else { return } switch completion { case .success: diff --git a/ios/MullvadVPN/View controllers/Login/AccountInputGroupView.swift b/ios/MullvadVPN/View controllers/Login/AccountInputGroupView.swift index 2a34644b6ddc..e5eb0ddb7794 100644 --- a/ios/MullvadVPN/View controllers/Login/AccountInputGroupView.swift +++ b/ios/MullvadVPN/View controllers/Login/AccountInputGroupView.swift @@ -552,4 +552,6 @@ private class AccountInputBorderLayer: CAShapeLayer { } return super.defaultAction(forKey: event) } + + // swiftlint:disable:next file_length } diff --git a/ios/MullvadVPN/View controllers/Login/LoginContentView.swift b/ios/MullvadVPN/View controllers/Login/LoginContentView.swift index d730bdb0c90d..5bcce9b6b56f 100644 --- a/ios/MullvadVPN/View controllers/Login/LoginContentView.swift +++ b/ios/MullvadVPN/View controllers/Login/LoginContentView.swift @@ -133,84 +133,43 @@ class LoginContentView: UIView { footerContainer.addSubview(footerLabel) footerContainer.addSubview(createAccountButton) - addSubview(contentContainer) - addSubview(footerContainer) - let contentContainerBottomConstraint = bottomAnchor .constraint(equalTo: contentContainer.bottomAnchor) self.contentContainerBottomConstraint = contentContainerBottomConstraint - NSLayoutConstraint.activate([ - contentContainer.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor), - contentContainer.leadingAnchor.constraint(equalTo: leadingAnchor), - contentContainer.trailingAnchor.constraint(equalTo: trailingAnchor), - contentContainerBottomConstraint, + addConstrainedSubviews([contentContainer, footerContainer]) { + contentContainer.pinEdges(PinnableEdges([.top(0)]), to: safeAreaLayoutGuide) + contentContainer.pinEdgesToSuperview(PinnableEdges([.leading(0), .trailing(0)])) + contentContainerBottomConstraint - footerContainer.leadingAnchor.constraint(equalTo: leadingAnchor), - footerContainer.trailingAnchor.constraint(equalTo: trailingAnchor), - footerContainer.bottomAnchor.constraint(equalTo: bottomAnchor), + footerContainer.pinEdgesToSuperview(.all().excluding(.top)) + footerLabel.pinEdges(.all().excluding(.bottom), to: footerContainer.layoutMarginsGuide) - footerLabel.topAnchor.constraint(equalTo: footerContainer.layoutMarginsGuide.topAnchor), - footerLabel.leadingAnchor - .constraint(equalTo: footerContainer.layoutMarginsGuide.leadingAnchor), - footerLabel.trailingAnchor - .constraint(equalTo: footerContainer.layoutMarginsGuide.trailingAnchor), + createAccountButton.topAnchor.constraint(equalToSystemSpacingBelow: footerLabel.bottomAnchor, multiplier: 1) + createAccountButton.pinEdges(.all().excluding(.top), to: footerContainer.layoutMarginsGuide) - createAccountButton.topAnchor.constraint( - equalToSystemSpacingBelow: footerLabel.bottomAnchor, - multiplier: 1 - ), - createAccountButton.leadingAnchor - .constraint(equalTo: footerContainer.layoutMarginsGuide.leadingAnchor), - createAccountButton.trailingAnchor - .constraint(equalTo: footerContainer.layoutMarginsGuide.trailingAnchor), - createAccountButton.bottomAnchor - .constraint(equalTo: footerContainer.layoutMarginsGuide.bottomAnchor), - - statusActivityView.centerXAnchor.constraint(equalTo: contentContainer.centerXAnchor), - formContainer.topAnchor.constraint( - equalTo: statusActivityView.bottomAnchor, - constant: 30 - ), - formContainer.centerYAnchor.constraint( - equalTo: contentContainer.centerYAnchor, - constant: -20 - ), - formContainer.leadingAnchor.constraint(equalTo: contentContainer.leadingAnchor), - formContainer.trailingAnchor.constraint(equalTo: contentContainer.trailingAnchor), - formContainer.bottomAnchor.constraint(equalTo: accountInputGroupWrapper.bottomAnchor), - - titleLabel.topAnchor.constraint(equalTo: formContainer.topAnchor), - titleLabel.leadingAnchor - .constraint(equalTo: formContainer.layoutMarginsGuide.leadingAnchor), - titleLabel.trailingAnchor - .constraint(equalTo: formContainer.layoutMarginsGuide.trailingAnchor), - - messageLabel.topAnchor.constraint( - equalToSystemSpacingBelow: titleLabel.bottomAnchor, - multiplier: 1 - ), - messageLabel.leadingAnchor - .constraint(equalTo: formContainer.layoutMarginsGuide.leadingAnchor), - messageLabel.trailingAnchor - .constraint(equalTo: formContainer.layoutMarginsGuide.trailingAnchor), + statusActivityView.centerXAnchor.constraint(equalTo: contentContainer.centerXAnchor) + + formContainer.topAnchor.constraint(equalTo: statusActivityView.bottomAnchor, constant: 30) + formContainer.centerYAnchor.constraint(equalTo: contentContainer.centerYAnchor, constant: -20) + formContainer.pinEdges(PinnableEdges([.leading(0), .trailing(0)]), to: contentContainer) + formContainer.pinEdges(PinnableEdges([.bottom(0)]), to: accountInputGroupWrapper) + + titleLabel.pinEdges(.all().excluding(.bottom), to: formContainer.layoutMarginsGuide) + + messageLabel.topAnchor.constraint(equalToSystemSpacingBelow: titleLabel.bottomAnchor, multiplier: 1) + messageLabel.pinEdges(PinnableEdges([.leading(0), .trailing(0)]), to: formContainer.layoutMarginsGuide) accountInputGroupWrapper.topAnchor.constraint( equalToSystemSpacingBelow: messageLabel.bottomAnchor, multiplier: 1 - ), - accountInputGroupWrapper.leadingAnchor - .constraint(equalTo: formContainer.layoutMarginsGuide.leadingAnchor), - accountInputGroupWrapper.trailingAnchor - .constraint(equalTo: formContainer.layoutMarginsGuide.trailingAnchor), - accountInputGroupWrapper.heightAnchor - .constraint(equalTo: accountInputGroup.contentView.heightAnchor), - - accountInputGroup.topAnchor.constraint(equalTo: accountInputGroupWrapper.topAnchor), - accountInputGroup.leadingAnchor - .constraint(equalTo: accountInputGroupWrapper.leadingAnchor), - accountInputGroup.trailingAnchor - .constraint(equalTo: accountInputGroupWrapper.trailingAnchor), - ]) + ) + accountInputGroupWrapper.pinEdges( + PinnableEdges([.leading(0), .trailing(0)]), + to: formContainer.layoutMarginsGuide + ) + accountInputGroupWrapper.heightAnchor.constraint(equalTo: accountInputGroup.contentView.heightAnchor) + accountInputGroup.pinEdges(.all().excluding(.bottom), to: accountInputGroupWrapper) + } } } diff --git a/ios/MullvadVPN/View controllers/Login/LoginViewController.swift b/ios/MullvadVPN/View controllers/Login/LoginViewController.swift index e7261079607e..a9b51f302158 100644 --- a/ios/MullvadVPN/View controllers/Login/LoginViewController.swift +++ b/ios/MullvadVPN/View controllers/Login/LoginViewController.swift @@ -141,7 +141,7 @@ class LoginViewController: UIViewController, RootContainment { } contentView.accountInputGroup.setOnReturnKey { [weak self] _ in - guard let self else { return true } + guard let self = self else { return true } return attemptLogin() } @@ -433,4 +433,6 @@ private extension LoginState { return .hidden } } + + // swiftlint:disable:next file_length } diff --git a/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeViewController.swift b/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeViewController.swift index 651886277f06..06b3dedb0bb3 100644 --- a/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeViewController.swift +++ b/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeViewController.swift @@ -156,7 +156,10 @@ class OutOfTimeViewController: UIViewController, RootContainment { NSLocalizedString( "OUT_OF_TIME_BODY_CONNECTED", tableName: "OutOfTime", - value: "You have no more VPN time left on this account. To add more, you will need to disconnect and access the Internet with an unsecure connection.", + value: """ + You have no more VPN time left on this account. To add more, you will need to \ + disconnect and access the Internet with an unsecure connection. + """, comment: "" ) ) @@ -165,7 +168,10 @@ class OutOfTimeViewController: UIViewController, RootContainment { NSLocalizedString( "OUT_OF_TIME_BODY_DISCONNECTED", tableName: "OutOfTime", - value: "You have no more VPN time left on this account. Either buy credit on our website or make an in-app purchase via the **Add 30 days time** button below.", + value: """ + You have no more VPN time left on this account. Either buy credit on our website \ + or make an in-app purchase via the **Add 30 days time** button below. + """, comment: "" ) ) diff --git a/ios/MullvadVPN/View controllers/Preferences/PreferencesDataSource.swift b/ios/MullvadVPN/View controllers/Preferences/PreferencesDataSource.swift index 7aa4c82acc4c..d4b048a04599 100644 --- a/ios/MullvadVPN/View controllers/Preferences/PreferencesDataSource.swift +++ b/ios/MullvadVPN/View controllers/Preferences/PreferencesDataSource.swift @@ -890,7 +890,7 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< header.titleLabel.text = title header.accessibilityCustomActionName = title header.didCollapseHandler = { [weak self] header in - guard let self else { return } + guard let self = self else { return } var snapshot = snapshot() if header.isExpanded { @@ -918,7 +918,7 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< header.titleLabel.text = title header.accessibilityCustomActionName = title header.didCollapseHandler = { [weak self] header in - guard let self else { return } + guard let self = self else { return } var snapshot = snapshot() if header.isExpanded { diff --git a/ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift b/ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift index 5c23cf4dcfdc..ef05999fe4bb 100644 --- a/ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift +++ b/ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift @@ -117,6 +117,7 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel interactor.setDNSSettings(dnsSettings) } + // swiftlint:disable:next function_body_length func preferencesDataSource( _ dataSource: PreferencesDataSource, showInfo item: PreferencesDataSource.InfoButtonItem? @@ -128,7 +129,11 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel message = NSLocalizedString( "PREFERENCES_CONTENT_BLOCKERS_GENERAL", tableName: "ContentBlockers", - value: "When this feature is enabled it stops the device from contacting certain domains or websites known for distributing ads, malware, trackers and more. This might cause issues on certain websites, services, and programs.", + value: """ + When this feature is enabled it stops the device from contacting certain \ + domains or websites known for distributing ads, malware, trackers and more. \ + This might cause issues on certain websites, services, and programs. + """, comment: "" ) @@ -136,7 +141,10 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel message = NSLocalizedString( "PREFERENCES_CONTENT_BLOCKERS_MALWARE", tableName: "ContentBlockers", - value: "Warning: The malware blocker is not an anti-virus and should not be treated as such, this is just an extra layer of protection.", + value: """ + Warning: The malware blocker is not an anti-virus and should not \ + be treated as such, this is just an extra layer of protection. + """, comment: "" ) @@ -163,12 +171,15 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel ) #if DEBUG - case .wireGuardObfuscation: message = NSLocalizedString( "PREFERENCES_WIRE_GUARD_OBFUSCATION_GENERAL", tableName: "WireGuardObfuscation", - value: "Obfuscation hides the WireGuard traffic inside another protocol. It can be used to help circumvent censorship and other types of filtering, where a plain WireGuard connect would be blocked.", + value: """ + Obfuscation hides the WireGuard traffic inside another protocol. \ + It can be used to help circumvent censorship and other types of filtering, \ + where a plain WireGuard connect would be blocked. + """, comment: "" ) @@ -180,6 +191,7 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel comment: "" ) #endif + default: assertionFailure("No matching InfoButtonItem") } diff --git a/ios/MullvadVPN/View controllers/Preferences/PreferencesViewModel.swift b/ios/MullvadVPN/View controllers/Preferences/PreferencesViewModel.swift index 091be496a535..85a381606a94 100644 --- a/ios/MullvadVPN/View controllers/Preferences/PreferencesViewModel.swift +++ b/ios/MullvadVPN/View controllers/Preferences/PreferencesViewModel.swift @@ -53,7 +53,8 @@ enum CustomDNSPrecondition { "CUSTOM_DNS_NO_DNS_ENTRIES_EDITING_OFF_FOOTNOTE", tableName: "Preferences", value: "Tap **Edit** to add at least one DNS server.", - comment: "Foot note displayed if there are no DNS entries, but table view is not in editing mode." + comment: + "Foot note displayed if there are no DNS entries, but table view is not in editing mode." ), options: MarkdownStylingOptions(font: preferredFont) ) @@ -65,7 +66,10 @@ enum CustomDNSPrecondition { "CUSTOM_DNS_DISABLE_CONTENT_BLOCKERS_FOOTNOTE", tableName: "Preferences", value: "Disable all content blockers (under VPN settings) to activate this setting.", - comment: "Foot note displayed when custom DNS cannot be enabled, because content blockers should be disabled first." + comment: """ + Foot note displayed when custom DNS cannot be enabled, because content blockers should be \ + disabled first. + """ ), attributes: [.font: preferredFont] ) diff --git a/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportInteractor.swift b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportInteractor.swift index 28aed41d7709..b1bb49ccb2b8 100644 --- a/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportInteractor.swift +++ b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportInteractor.swift @@ -17,9 +17,6 @@ final class ProblemReportInteractor { 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( diff --git a/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController.swift b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController.swift index 93cfbde3eb5e..0a633d114369 100644 --- a/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController.swift +++ b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController.swift @@ -43,7 +43,10 @@ final class ProblemReportViewController: UIViewController, UITextFieldDelegate { textLabel.text = NSLocalizedString( "SUBHEAD_LABEL", tableName: "ProblemReport", - value: "To help you more effectively, your app’s log file will be attached to this message. Your data will remain secure and private, as it is anonymised before being sent over an encrypted channel.", + value: """ + To help you more effectively, your app’s log file will be attached to this message. \ + Your data will remain secure and private, as it is anonymised before being sent over an encrypted channel. + """, comment: "" ) return textLabel @@ -82,7 +85,9 @@ final class ProblemReportViewController: UIViewController, UITextFieldDelegate { textView.placeholder = NSLocalizedString( "DESCRIPTION_TEXTVIEW_PLACEHOLDER", tableName: "ProblemReport", - value: "To assist you better, please write in English or Swedish and include which country you are connecting from.", + value: """ + To assist you better, please write in English or Swedish and include which country you are connecting from. + """, comment: "" ) textView.contentInsetAdjustmentBehavior = .never @@ -221,19 +226,8 @@ final class ProblemReportViewController: UIViewController, UITextFieldDelegate { messageTextView.setContentHuggingPriority(.defaultLow, for: .vertical) messageTextView.setContentCompressionResistancePriority(.defaultLow, for: .vertical) - textFieldsHolder.addSubview(emailTextField) - textFieldsHolder.addSubview(messagePlaceholder) - textFieldsHolder.addSubview(messageTextView) - - view.addSubview(scrollView) - scrollView.addSubview(containerView) - containerView.addSubview(subheaderLabel) - containerView.addSubview(textFieldsHolder) - containerView.addSubview(buttonsStackView) - addConstraints() registerForNotifications() - loadPersistentViewModel() } @@ -339,88 +333,55 @@ final class ProblemReportViewController: UIViewController, UITextFieldDelegate { } private func addConstraints() { - activeMessageTextViewConstraints = [ - messageTextView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), - messageTextView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - messageTextView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - messageTextView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - ] - - inactiveMessageTextViewConstraints = [ - messageTextView.topAnchor.constraint( - equalTo: emailTextField.bottomAnchor, - constant: 12 - ), - messageTextView.leadingAnchor.constraint(equalTo: textFieldsHolder.leadingAnchor), - messageTextView.trailingAnchor.constraint(equalTo: textFieldsHolder.trailingAnchor), - messageTextView.bottomAnchor.constraint(equalTo: textFieldsHolder.bottomAnchor), - ] - - var constraints = [ - subheaderLabel.topAnchor - .constraint(equalTo: containerView.layoutMarginsGuide.topAnchor), - subheaderLabel.leadingAnchor - .constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor), - subheaderLabel.trailingAnchor - .constraint(equalTo: containerView.layoutMarginsGuide.trailingAnchor), - - textFieldsHolder.topAnchor.constraint( - equalTo: subheaderLabel.bottomAnchor, - constant: 24 - ), - textFieldsHolder.leadingAnchor - .constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor), - textFieldsHolder.trailingAnchor - .constraint(equalTo: containerView.layoutMarginsGuide.trailingAnchor), - - buttonsStackView.topAnchor.constraint( - equalTo: textFieldsHolder.bottomAnchor, - constant: 18 - ), - buttonsStackView.leadingAnchor - .constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor), - buttonsStackView.trailingAnchor - .constraint(equalTo: containerView.layoutMarginsGuide.trailingAnchor), - buttonsStackView.bottomAnchor - .constraint(equalTo: containerView.layoutMarginsGuide.bottomAnchor), - - emailTextField.topAnchor.constraint(equalTo: textFieldsHolder.topAnchor), - emailTextField.leadingAnchor.constraint(equalTo: textFieldsHolder.leadingAnchor), - emailTextField.trailingAnchor.constraint(equalTo: textFieldsHolder.trailingAnchor), - - messagePlaceholder.topAnchor.constraint( - equalTo: emailTextField.bottomAnchor, - constant: 12 - ), - messagePlaceholder.leadingAnchor.constraint(equalTo: textFieldsHolder.leadingAnchor), - messagePlaceholder.trailingAnchor.constraint(equalTo: textFieldsHolder.trailingAnchor), - messagePlaceholder.bottomAnchor.constraint(equalTo: textFieldsHolder.bottomAnchor), - messagePlaceholder.heightAnchor.constraint(equalTo: messageTextView.heightAnchor), - - scrollView.frameLayoutGuide.topAnchor.constraint(equalTo: view.topAnchor), - scrollView.frameLayoutGuide.bottomAnchor.constraint(equalTo: view.bottomAnchor), - scrollView.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.leadingAnchor), - scrollView.frameLayoutGuide.trailingAnchor.constraint(equalTo: view.trailingAnchor), - - scrollView.contentLayoutGuide.topAnchor.constraint(equalTo: containerView.topAnchor), - scrollView.contentLayoutGuide.bottomAnchor - .constraint(equalTo: containerView.bottomAnchor), - scrollView.contentLayoutGuide.leadingAnchor - .constraint(equalTo: containerView.leadingAnchor), - scrollView.contentLayoutGuide.trailingAnchor - .constraint(equalTo: containerView.trailingAnchor), - - scrollView.contentLayoutGuide.widthAnchor - .constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor), - scrollView.contentLayoutGuide.heightAnchor - .constraint(greaterThanOrEqualTo: scrollView.safeAreaLayoutGuide.heightAnchor), + activeMessageTextViewConstraints = + messageTextView.pinEdgesToSuperview(.all().excluding(.top)) + + messageTextView.pinEdges(PinnableEdges([.top(0)]), to: view.safeAreaLayoutGuide) + + inactiveMessageTextViewConstraints = + messageTextView.pinEdges(.all().excluding(.top), to: textFieldsHolder) + + [messageTextView.topAnchor.constraint(equalTo: emailTextField.bottomAnchor, constant: 12)] + + textFieldsHolder.addSubview(emailTextField) + textFieldsHolder.addSubview(messagePlaceholder) + textFieldsHolder.addSubview(messageTextView) + + scrollView.addSubview(containerView) + containerView.addSubview(subheaderLabel) + containerView.addSubview(textFieldsHolder) + containerView.addSubview(buttonsStackView) + + view.addConstrainedSubviews([scrollView]) { + inactiveMessageTextViewConstraints + + subheaderLabel.pinEdges(.all().excluding(.bottom), to: containerView.layoutMarginsGuide) - messageTextView.heightAnchor.constraint(greaterThanOrEqualToConstant: 150), - ] + textFieldsHolder.pinEdges(PinnableEdges([.leading(0), .trailing(0)]), to: containerView.layoutMarginsGuide) + textFieldsHolder.topAnchor.constraint(equalTo: subheaderLabel.bottomAnchor, constant: 24) - constraints.append(contentsOf: inactiveMessageTextViewConstraints) + buttonsStackView.pinEdges(.all().excluding(.top), to: containerView.layoutMarginsGuide) + buttonsStackView.topAnchor.constraint(equalTo: textFieldsHolder.bottomAnchor, constant: 18) - NSLayoutConstraint.activate(constraints) + emailTextField.pinEdges(.all().excluding(.bottom), to: textFieldsHolder) + + messagePlaceholder.pinEdges(.all().excluding(.top), to: textFieldsHolder) + messagePlaceholder.topAnchor.constraint(equalTo: emailTextField.bottomAnchor, constant: 12) + messagePlaceholder.heightAnchor.constraint(equalTo: messageTextView.heightAnchor) + + scrollView.frameLayoutGuide.topAnchor.constraint(equalTo: view.topAnchor) + scrollView.frameLayoutGuide.bottomAnchor.constraint(equalTo: view.bottomAnchor) + scrollView.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.leadingAnchor) + scrollView.frameLayoutGuide.trailingAnchor.constraint(equalTo: view.trailingAnchor) + + scrollView.contentLayoutGuide.topAnchor.constraint(equalTo: containerView.topAnchor) + scrollView.contentLayoutGuide.bottomAnchor.constraint(equalTo: containerView.bottomAnchor) + scrollView.contentLayoutGuide.leadingAnchor.constraint(equalTo: containerView.leadingAnchor) + scrollView.contentLayoutGuide.trailingAnchor.constraint(equalTo: containerView.trailingAnchor) + scrollView.contentLayoutGuide.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor) + scrollView.contentLayoutGuide.heightAnchor + .constraint(greaterThanOrEqualTo: scrollView.safeAreaLayoutGuide.heightAnchor) + + messageTextView.heightAnchor.constraint(greaterThanOrEqualToConstant: 150) + } } private func setDescriptionFieldExpanded(_ isExpanded: Bool) { @@ -453,14 +414,14 @@ final class ProblemReportViewController: UIViewController, UITextFieldDelegate { self.messageTextView.roundCorners = false self.view.layoutIfNeeded() - }) { _ in + }, completion: { _ in self.isMessageTextViewExpanded = true self.textViewKeyboardResponder?.updateContentInsets() // Tell accessibility engine to scan the new layout UIAccessibility.post(notification: .layoutChanged, argument: nil) - } + }) } else { // Re-enable the large title @@ -476,7 +437,7 @@ final class ProblemReportViewController: UIViewController, UITextFieldDelegate { self.messageTextView.roundCorners = true self.view.layoutIfNeeded() - }) { _ in + }, completion: { _ in // Revert the content adjustment behavior self.messageTextView.contentInsetAdjustmentBehavior = .never @@ -487,7 +448,7 @@ final class ProblemReportViewController: UIViewController, UITextFieldDelegate { // Tell accessibility engine to scan the new layout UIAccessibility.post(notification: .layoutChanged, argument: nil) - } + }) } } @@ -504,7 +465,10 @@ final class ProblemReportViewController: UIViewController, UITextFieldDelegate { let message = NSLocalizedString( "EMPTY_EMAIL_ALERT_MESSAGE", tableName: "ProblemReport", - value: "You are about to send the problem report without a way for us to get back to you. If you want an answer to your report you will have to enter an email address.", + value: """ + You are about to send the problem report without a way for us to get back to you. \ + If you want an answer to your report you will have to enter an email address. + """, comment: "" ) @@ -718,4 +682,6 @@ final class ProblemReportViewController: UIViewController, UITextFieldDelegate { messageTextView.becomeFirstResponder() return false } + + // swiftlint:disable:next file_length } diff --git a/ios/MullvadVPN/View controllers/SelectLocation/LocationDataSource.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationDataSource.swift index 8f07d64f717c..ee5efa3d2497 100644 --- a/ios/MullvadVPN/View controllers/SelectLocation/LocationDataSource.swift +++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationDataSource.swift @@ -75,14 +75,11 @@ final class LocationDataSource: UITableViewDiffableDataSource Node { + let node: Node + + switch ascendantOrSelf { + case .country: + node = Node( + type: .country, + location: ascendantOrSelf, + displayName: serverLocation.country, + showsChildren: wasShowingChildren, + isActive: true, + children: [] + ) + rootNode.addChild(node) + + case let .city(countryCode, _): + node = Node( + type: .city, + location: ascendantOrSelf, + displayName: serverLocation.city, + showsChildren: wasShowingChildren, + isActive: true, + children: [] + ) + nodeByLocation[.country(countryCode)]!.addChild(node) + + case let .hostname(countryCode, cityCode, _): + node = Node( + type: .relay, + location: ascendantOrSelf, + displayName: relay.hostname, + showsChildren: false, + isActive: relay.active, + children: [] + ) + nodeByLocation[.city(countryCode, cityCode)]!.addChild(node) + } + + return node + } + private func updateDataSnapshot( with locations: [RelayLocation], reloadExisting: Bool = false, @@ -518,4 +531,6 @@ private extension [RelayLocation] { locations.contains(location) }) } + + // swiftlint:disable:next file_length } diff --git a/ios/MullvadVPN/View controllers/SelectLocation/SelectLocationCell.swift b/ios/MullvadVPN/View controllers/SelectLocation/SelectLocationCell.swift index 89a44fa2f1ba..cb63f1428d3d 100644 --- a/ios/MullvadVPN/View controllers/SelectLocation/SelectLocationCell.swift +++ b/ios/MullvadVPN/View controllers/SelectLocation/SelectLocationCell.swift @@ -114,11 +114,7 @@ class SelectLocationCell: UITableViewCell { collapseButton.accessibilityIdentifier = "CollapseButton" collapseButton.isAccessibilityElement = false collapseButton.tintColor = .white - collapseButton.addTarget( - self, - action: #selector(handleCollapseButton(_:)), - for: .touchUpInside - ) + collapseButton.addTarget(self, action: #selector(handleCollapseButton(_:)), for: .touchUpInside) [locationLabel, tickImageView, statusIndicator, collapseButton].forEach { subview in subview.translatesAutoresizingMaskIntoConstraints = false @@ -132,8 +128,7 @@ class SelectLocationCell: UITableViewCell { setLayoutMargins() NSLayoutConstraint.activate([ - tickImageView.leadingAnchor - .constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor), + tickImageView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor), tickImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), statusIndicator.widthAnchor.constraint(equalToConstant: kRelayIndicatorSize), @@ -148,8 +143,7 @@ class SelectLocationCell: UITableViewCell { locationLabel.trailingAnchor.constraint(lessThanOrEqualTo: collapseButton.leadingAnchor) .withPriority(.defaultHigh), locationLabel.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor), - locationLabel.bottomAnchor - .constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor), + locationLabel.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor), collapseButton.widthAnchor .constraint( diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsCell.swift b/ios/MullvadVPN/View controllers/Settings/SettingsCell.swift index a6333462ee22..7cd88c8e81fc 100644 --- a/ios/MullvadVPN/View controllers/Settings/SettingsCell.swift +++ b/ios/MullvadVPN/View controllers/Settings/SettingsCell.swift @@ -80,11 +80,7 @@ class SettingsCell: UITableViewCell { contentView.backgroundColor = .clear infoButton.isHidden = true - infoButton.addTarget( - self, - action: #selector(handleInfoButton(_:)), - for: .touchUpInside - ) + infoButton.addTarget(self, action: #selector(handleInfoButton(_:)), for: .touchUpInside) titleLabel.translatesAutoresizingMaskIntoConstraints = false titleLabel.font = UIFont.systemFont(ofSize: 17) diff --git a/ios/MullvadVPN/View controllers/TermsOfService/TermsOfServiceContentView.swift b/ios/MullvadVPN/View controllers/TermsOfService/TermsOfServiceContentView.swift index 11423c91e0ac..ec4541dabf87 100644 --- a/ios/MullvadVPN/View controllers/TermsOfService/TermsOfServiceContentView.swift +++ b/ios/MullvadVPN/View controllers/TermsOfService/TermsOfServiceContentView.swift @@ -37,9 +37,11 @@ class TermsOfServiceContentView: UIView { "PRIVACY_NOTICE_BODY", tableName: "TermsOfService", value: """ - You have a right to privacy. That’s why we never store activity logs, don’t ask for personal information, and encourage anonymous payments. + You have a right to privacy. That’s why we never store activity logs, don’t ask for personal \ + information, and encourage anonymous payments. - In some situations, as outlined in our privacy policy, we might process personal data that you choose to send, for example if you email us. + In some situations, as outlined in our privacy policy, we might process personal data that you \ + choose to send, for example if you email us. We strongly believe in retaining as little data as possible because we want you to remain anonymous. """, diff --git a/ios/MullvadVPN/View controllers/Tunnel/ConnectionPanelView.swift b/ios/MullvadVPN/View controllers/Tunnel/ConnectionPanelView.swift index 30a0b7170ef7..65cae0c148a5 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/ConnectionPanelView.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/ConnectionPanelView.swift @@ -75,7 +75,7 @@ class ConnectionPanelView: UIView { inAddressRow.translatesAutoresizingMaskIntoConstraints = false outAddressRow.translatesAutoresizingMaskIntoConstraints = false - // TODO: Unhide it when we have out address + // Remove this line when we have out address outAddressRow.isHidden = true inAddressRow.title = NSLocalizedString( diff --git a/ios/MullvadVPN/View controllers/Tunnel/DisconnectSplitButton.swift b/ios/MullvadVPN/View controllers/Tunnel/DisconnectSplitButton.swift index 59e807cfa04e..8baa2eaac71c 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/DisconnectSplitButton.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/DisconnectSplitButton.swift @@ -11,12 +11,11 @@ import UIKit class DisconnectSplitButton: UIView { private var secondaryButtonSize: CGSize { - // TODO: make it less hardcoded switch traitCollection.userInterfaceIdiom { case .phone: - return CGSize(width: 42, height: 42) + return UIMetrics.DisconnectSplitButton.secondaryButtonPhone case .pad: - return CGSize(width: 52, height: 52) + return UIMetrics.DisconnectSplitButton.secondaryButtonPad default: return .zero } diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift index 9c1103325d36..2c26c6a07ceb 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift @@ -649,4 +649,6 @@ private extension TunnelState { return [] } } + + // swiftlint:disable:next file_length } diff --git a/ios/MullvadVPN/Views/AppButton.swift b/ios/MullvadVPN/Views/AppButton.swift index 2fc3603e2212..48bbd20ac873 100644 --- a/ios/MullvadVPN/Views/AppButton.swift +++ b/ios/MullvadVPN/Views/AppButton.swift @@ -286,6 +286,7 @@ class CustomButton: UIButton { } } + // swiftlint:disable:next cyclomatic_complexity private func computeLayout(forContentRect contentRect: CGRect) -> (CGRect, CGRect) { var imageRect = super.imageRect(forContentRect: contentRect) var titleRect = super.titleRect(forContentRect: contentRect) diff --git a/ios/MullvadVPNTests/DeviceCheckOperationTests.swift b/ios/MullvadVPNTests/DeviceCheckOperationTests.swift index cefd20ae45b4..887194d3d9eb 100644 --- a/ios/MullvadVPNTests/DeviceCheckOperationTests.swift +++ b/ios/MullvadVPNTests/DeviceCheckOperationTests.swift @@ -567,4 +567,6 @@ private extension AccountVerdict { } return false } + + // swiftlint:disable:next file_length } diff --git a/ios/Operations/AsyncOperation.swift b/ios/Operations/AsyncOperation.swift index ea8edb24c485..7deccd316a8a 100644 --- a/ios/Operations/AsyncOperation.swift +++ b/ios/Operations/AsyncOperation.swift @@ -423,4 +423,6 @@ extension OperationBlockObserverSupport where Self: AsyncOperation { public func addBlockObserver(_ observer: OperationBlockObserver) { addObserver(observer) } + + // swiftlint:disable:next file_length } diff --git a/ios/PacketTunnel/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider.swift index f06f1c0b4e1c..ba924aadc0c0 100644 --- a/ios/PacketTunnel/PacketTunnelProvider.swift +++ b/ios/PacketTunnel/PacketTunnelProvider.swift @@ -125,18 +125,6 @@ class PacketTunnelProvider: NEPacketTunnelProvider { } override init() { - var loggerBuilder = LoggerBuilder() - let pid = ProcessInfo.processInfo.processIdentifier - loggerBuilder.metadata["pid"] = .string("\(pid)") - loggerBuilder.addFileOutput(fileURL: ApplicationConfiguration.logFileURL(for: .packetTunnel)) - #if DEBUG - loggerBuilder.addOSLogOutput(subsystem: ApplicationTarget.packetTunnel.bundleIdentifier) - #endif - loggerBuilder.install() - - providerLogger = Logger(label: "PacketTunnelProvider") - tunnelLogger = Logger(label: "WireGuard") - let containerURL = ApplicationConfiguration.containerURL let addressCache = REST.AddressCache(canWriteToCache: false, cacheDirectory: containerURL) addressCache.loadFromFile() @@ -166,8 +154,13 @@ class PacketTunnelProvider: NEPacketTunnelProvider { accountsProxy = proxyFactory.createAccountsProxy() devicesProxy = proxyFactory.createDevicesProxy() + providerLogger = Logger(label: "PacketTunnelProvider") + tunnelLogger = Logger(label: "WireGuard") + super.init() + configureLogging() + adapter = WireGuardAdapter( with: self, shouldHandleReasserting: false, @@ -287,10 +280,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { do { message = try TunnelProviderMessage(messageData: messageData) } catch { - self.providerLogger.error( - error: error, - message: "Failed to decode the app message." - ) + self.providerLogger.error(error: error, message: "Failed to decode the app message.") completionHandler?(nil) return @@ -302,9 +292,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { case let .reconnectTunnel(appSelectorResult): self.providerLogger.debug("Reconnecting the tunnel...") - let nextRelay: NextRelay = (appSelectorResult ?? self.selectorResult) - .map { .set($0) } ?? .automatic - + let nextRelay: NextRelay = (appSelectorResult ?? self.selectorResult).map { .set($0) } ?? .automatic self.reconnectTunnel(to: nextRelay, shouldStopTunnelMonitor: true) completionHandler?(nil) @@ -419,6 +407,20 @@ class PacketTunnelProvider: NEPacketTunnelProvider { // MARK: - Private + private func configureLogging() { + var loggerBuilder = LoggerBuilder() + let pid = ProcessInfo.processInfo.processIdentifier + + loggerBuilder.metadata["pid"] = .string("\(pid)") + loggerBuilder.addFileOutput(fileURL: ApplicationConfiguration.logFileURL(for: .packetTunnel)) + + #if DEBUG + loggerBuilder.addOSLogOutput(subsystem: ApplicationTarget.packetTunnel.bundleIdentifier) + #endif + + loggerBuilder.install() + } + private func startTunnelReconnectionTimer(reconnectionDelay: Duration) { dispatchPrecondition(condition: .onQueue(dispatchQueue)) @@ -763,4 +765,6 @@ extension PacketTunnelErrorWrapper { return nil } } + + // swiftlint:disable:next file_length } diff --git a/ios/PacketTunnelCore/Pinger/Pinger.swift b/ios/PacketTunnelCore/Pinger/Pinger.swift index 8f220f38b66b..0d754f3cfade 100644 --- a/ios/PacketTunnelCore/Pinger/Pinger.swift +++ b/ios/PacketTunnelCore/Pinger/Pinger.swift @@ -408,4 +408,6 @@ private extension IPv4Header { var isIPv4Version: Bool { (versionAndHeaderLength & 0xF0) == 0x40 } + + // swiftlint:disable:next file_length } diff --git a/ios/PacketTunnelCore/TunnelMonitor/TunnelMonitor.swift b/ios/PacketTunnelCore/TunnelMonitor/TunnelMonitor.swift index fc116fd7e1f4..239a6b4ecdc4 100644 --- a/ios/PacketTunnelCore/TunnelMonitor/TunnelMonitor.swift +++ b/ios/PacketTunnelCore/TunnelMonitor/TunnelMonitor.swift @@ -101,65 +101,12 @@ public final class TunnelMonitor: TunnelMonitorProtocol { func evaluateConnection(now: Date, pingTimeout: Duration) -> ConnectionEvaluation { switch connectionState { case .connecting: - if now.timeIntervalSince(timeoutReference) >= pingTimeout { - return .pingTimeout - } - - guard let lastRequestDate = pingStats.lastRequestDate else { - return .sendInitialPing - } - - if now.timeIntervalSince(lastRequestDate) >= pingDelay { - return .sendNextPing - } - + return handleConnectingState(now: now) case .connected: - if now.timeIntervalSince(timeoutReference) >= pingTimeout, !isHeartbeatSuspended { - return .pingTimeout - } - - guard let lastRequestDate = pingStats.lastRequestDate else { - return .sendInitialPing - } - - let timeSinceLastPing = now.timeIntervalSince(lastRequestDate) - if let lastReplyDate = pingStats.lastReplyDate, - lastRequestDate.timeIntervalSince(lastReplyDate) >= heartbeatReplyTimeout, - timeSinceLastPing >= pingDelay, !isHeartbeatSuspended { - return .retryHeartbeatPing - } - - guard let lastSeenRx, let lastSeenTx else { return .ok } - - let rxTimeElapsed = now.timeIntervalSince(lastSeenRx) - let txTimeElapsed = now.timeIntervalSince(lastSeenTx) - - if timeSinceLastPing >= heartbeatPingInterval { - // Send heartbeat if traffic is flowing. - if rxTimeElapsed <= trafficFlowTimeout || txTimeElapsed <= trafficFlowTimeout { - return .sendHeartbeatPing - } - - if !isHeartbeatSuspended { - return .suspendHeartbeat - } - } - - if timeSinceLastPing >= pingDelay { - if txTimeElapsed >= trafficTimeout || rxTimeElapsed >= trafficTimeout { - return .trafficTimeout - } - - if lastSeenTx > lastSeenRx, rxTimeElapsed >= inboundTrafficTimeout { - return .inboundTrafficTimeout - } - } - + return handleConnectedState(now: now, pingTimeout: pingTimeout) default: - break + return .ok } - - return .ok } func getPingTimeout() -> Duration { @@ -206,6 +153,67 @@ public final class TunnelMonitor: TunnelMonitorProtocol { return pingTimestamp } + + private func handleConnectingState(now: Date) -> ConnectionEvaluation { + if now.timeIntervalSince(timeoutReference) >= pingTimeout { + return .pingTimeout + } + + guard let lastRequestDate = pingStats.lastRequestDate else { + return .sendInitialPing + } + + if now.timeIntervalSince(lastRequestDate) >= pingDelay { + return .sendNextPing + } + + return .ok + } + + private func handleConnectedState(now: Date, pingTimeout: Duration) -> ConnectionEvaluation { + if now.timeIntervalSince(timeoutReference) >= pingTimeout, !isHeartbeatSuspended { + return .pingTimeout + } + + guard let lastRequestDate = pingStats.lastRequestDate else { + return .sendInitialPing + } + + let timeSinceLastPing = now.timeIntervalSince(lastRequestDate) + if let lastReplyDate = pingStats.lastReplyDate, + lastRequestDate.timeIntervalSince(lastReplyDate) >= heartbeatReplyTimeout, + timeSinceLastPing >= pingDelay, !isHeartbeatSuspended { + return .retryHeartbeatPing + } + + guard let lastSeenRx, let lastSeenTx else { return .ok } + + let rxTimeElapsed = now.timeIntervalSince(lastSeenRx) + let txTimeElapsed = now.timeIntervalSince(lastSeenTx) + + if timeSinceLastPing >= heartbeatPingInterval { + // Send heartbeat if traffic is flowing. + if rxTimeElapsed <= trafficFlowTimeout || txTimeElapsed <= trafficFlowTimeout { + return .sendHeartbeatPing + } + + if !isHeartbeatSuspended { + return .suspendHeartbeat + } + } + + if timeSinceLastPing >= pingDelay { + if txTimeElapsed >= trafficTimeout || rxTimeElapsed >= trafficTimeout { + return .trafficTimeout + } + + if lastSeenTx > lastSeenRx, rxTimeElapsed >= inboundTrafficTimeout { + return .inboundTrafficTimeout + } + } + + return .ok + } } /// Ping statistics. @@ -262,7 +270,7 @@ public final class TunnelMonitor: TunnelMonitorProtocol { self.pinger = pinger self.pinger.onReply = { [weak self] reply in - guard let self else { return } + guard let self = self else { return } switch reply { case let .success(sender, sequenceNumber): @@ -635,4 +643,6 @@ public final class TunnelMonitor: TunnelMonitorProtocol { return nil } } + + // swiftlint:disable:next file_length } diff --git a/ios/PacketTunnelCoreTests/Mocks/MockPinger.swift b/ios/PacketTunnelCoreTests/Mocks/MockPinger.swift index 1ff173b8863b..971d81ec1b28 100644 --- a/ios/PacketTunnelCoreTests/Mocks/MockPinger.swift +++ b/ios/PacketTunnelCoreTests/Mocks/MockPinger.swift @@ -60,7 +60,7 @@ class MockPinger: PingerProtocol { switch decideOutcome(address, nextSequenceId) { case let .sendReply(reply, delay): DispatchQueue.main.asyncAfter(wallDeadline: .now() + delay) { [weak self] in - guard let self else { return } + guard let self = self else { return } networkStatsReporting.reportBytesReceived(UInt64(icmpPacketSize)) diff --git a/ios/PacketTunnelCoreTests/TunnelMonitorTests.swift b/ios/PacketTunnelCoreTests/TunnelMonitorTests.swift index e85a50617c8d..4677b042ad37 100644 --- a/ios/PacketTunnelCoreTests/TunnelMonitorTests.swift +++ b/ios/PacketTunnelCoreTests/TunnelMonitorTests.swift @@ -118,7 +118,7 @@ final class TunnelMonitorTests: XCTestCase { } case .connectionEstablished: - XCTFail() + XCTFail("Connection should fail.") case .networkReachabilityChanged: break