diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2023-04-28 16:47:01 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2023-04-28 16:47:01 +0200 |
| commit | e367a8a011b0875a565f69d8146aca5942e38e16 (patch) | |
| tree | 4b2ab3341dbe97539a0c518f874ae6a2dc7b3c91 | |
| parent | 3595658d8f0f0f6851f113effcab0d2c93e75cb8 (diff) | |
| parent | d288c8742cc36d87edb5f3042130ee2633a20349 (diff) | |
| download | mullvadvpn-e367a8a011b0875a565f69d8146aca5942e38e16.tar.xz mullvadvpn-e367a8a011b0875a565f69d8146aca5942e38e16.zip | |
Merge branch 'handle-account-expiry-transitions'
19 files changed, 205 insertions, 74 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index efb435cd82..d12da06daa 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -244,6 +244,7 @@ 58C3F4F92964B08300D72515 /* MapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C3F4F82964B08300D72515 /* MapViewController.swift */; }; 58C3F4FB296C3AD500D72515 /* SettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C3F4FA296C3AD500D72515 /* SettingsCoordinator.swift */; }; 58C774BE29A7A249003A1A56 /* CustomNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C774BD29A7A249003A1A56 /* CustomNavigationController.swift */; }; + 58C8191829FAA2C400DEB1B4 /* NotificationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C8191729FAA2C400DEB1B4 /* NotificationConfiguration.swift */; }; 58CAF9F82983D36800BE19F7 /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CAF9F72983D36800BE19F7 /* Coordinator.swift */; }; 58CAF9FA2983E0C600BE19F7 /* LoginCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CAF9F92983E0C600BE19F7 /* LoginCoordinator.swift */; }; 58CAFA002983FF0200BE19F7 /* LoginInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CAF9FF2983FF0200BE19F7 /* LoginInteractor.swift */; }; @@ -360,8 +361,8 @@ 58FEEB58260B662E00A621A8 /* AutomaticKeyboardResponder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FEEB57260B662E00A621A8 /* AutomaticKeyboardResponder.swift */; }; 58FF2C03281BDE02009EF542 /* SettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FF2C02281BDE02009EF542 /* SettingsManager.swift */; }; 7A09C98129D99215000C2CAC /* String+FuzzyMatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A09C98029D99215000C2CAC /* String+FuzzyMatch.swift */; }; - 7A7AD28F29DEDB1C00480EF1 /* SettingsContentBlockersHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7AD28E29DEDB1C00480EF1 /* SettingsContentBlockersHeaderView.swift */; }; 7A7AD28D29DC677800480EF1 /* FirstTimeLaunch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7AD28C29DC677800480EF1 /* FirstTimeLaunch.swift */; }; + 7A7AD28F29DEDB1C00480EF1 /* SettingsContentBlockersHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7AD28E29DEDB1C00480EF1 /* SettingsContentBlockersHeaderView.swift */; }; 7A818F1F29F0305800C7F0F4 /* RootConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A818F1E29F0305800C7F0F4 /* RootConfiguration.swift */; }; 7AD2DA1529DC4EB900250737 /* UISearchBar+Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD2DA1429DC4EB900250737 /* UISearchBar+Appearance.swift */; }; 7AF0419E29E957EB00D492DD /* AccountCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF0419D29E957EB00D492DD /* AccountCoordinator.swift */; }; @@ -873,6 +874,7 @@ 58C3F4F82964B08300D72515 /* MapViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewController.swift; sourceTree = "<group>"; }; 58C3F4FA296C3AD500D72515 /* SettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsCoordinator.swift; sourceTree = "<group>"; }; 58C774BD29A7A249003A1A56 /* CustomNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomNavigationController.swift; sourceTree = "<group>"; }; + 58C8191729FAA2C400DEB1B4 /* NotificationConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationConfiguration.swift; sourceTree = "<group>"; }; 58CAF9F72983D36800BE19F7 /* Coordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Coordinator.swift; sourceTree = "<group>"; }; 58CAF9F92983E0C600BE19F7 /* LoginCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginCoordinator.swift; sourceTree = "<group>"; }; 58CAF9FF2983FF0200BE19F7 /* LoginInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginInteractor.swift; sourceTree = "<group>"; }; @@ -949,8 +951,8 @@ 58FEEB57260B662E00A621A8 /* AutomaticKeyboardResponder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutomaticKeyboardResponder.swift; sourceTree = "<group>"; }; 58FF2C02281BDE02009EF542 /* SettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsManager.swift; sourceTree = "<group>"; }; 7A09C98029D99215000C2CAC /* String+FuzzyMatch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+FuzzyMatch.swift"; sourceTree = "<group>"; }; - 7A7AD28E29DEDB1C00480EF1 /* SettingsContentBlockersHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsContentBlockersHeaderView.swift; sourceTree = "<group>"; }; 7A7AD28C29DC677800480EF1 /* FirstTimeLaunch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstTimeLaunch.swift; sourceTree = "<group>"; }; + 7A7AD28E29DEDB1C00480EF1 /* SettingsContentBlockersHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsContentBlockersHeaderView.swift; sourceTree = "<group>"; }; 7A818F1E29F0305800C7F0F4 /* RootConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootConfiguration.swift; sourceTree = "<group>"; }; 7AD2DA1429DC4EB900250737 /* UISearchBar+Appearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UISearchBar+Appearance.swift"; sourceTree = "<group>"; }; 7AF0419D29E957EB00D492DD /* AccountCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountCoordinator.swift; sourceTree = "<group>"; }; @@ -1282,7 +1284,6 @@ 583FE01829C19709006E85F9 /* Settings */ = { isa = PBXGroup; children = ( - 582BB1B2229574F40055B6EF /* SettingsAccountCell.swift */, 5819C2162729595500D6EC38 /* SettingsAddDNSEntryCell.swift */, 582BB1AE229566420055B6EF /* SettingsCell.swift */, 5864AF0029C7879B005B0CD9 /* SettingsCellFactory.swift */, @@ -1582,6 +1583,7 @@ 587B75422669034500DEF7E9 /* Notification Providers */ = { isa = PBXGroup; children = ( + 58C8191729FAA2C400DEB1B4 /* NotificationConfiguration.swift */, 587B75402668FD7700DEF7E9 /* AccountExpirySystemNotificationProvider.swift */, 58607A4C2947287800BC467D /* AccountExpiryInAppNotificationProvider.swift */, F07CFF1F29F2720E008C0343 /* RegisteredDeviceInAppNotification.swift */, @@ -2725,6 +2727,7 @@ 580909D32876D09A0078138D /* RevokedDeviceViewController.swift in Sources */, 5835B7CC233B76CB0096D79F /* TunnelManager.swift in Sources */, 58607A4D2947287800BC467D /* AccountExpiryInAppNotificationProvider.swift in Sources */, + 58C8191829FAA2C400DEB1B4 /* NotificationConfiguration.swift in Sources */, 06410E07292D108E00AFC18C /* SettingsStore.swift in Sources */, 586A950D290125F0007BAF2B /* PresentAlertOperation.swift in Sources */, 7A7AD28F29DEDB1C00480EF1 /* SettingsContentBlockersHeaderView.swift in Sources */, diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index 29db4311c8..640d7fc86c 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -506,13 +506,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD func userNotificationCenter( _ center: UNUserNotificationCenter, willPresent notification: UNNotification, - withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) - -> Void + withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void ) { if #available(iOS 14.0, *) { - completionHandler([.list]) + completionHandler([.list, .banner, .sound]) } else { - completionHandler([]) + completionHandler([.sound, .alert]) } } } diff --git a/ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift b/ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift index 17b02b23cf..b4695072dd 100644 --- a/ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift @@ -616,8 +616,8 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo private func addTunnelObserver() { let tunnelObserver = - TunnelBlockObserver(didUpdateDeviceState: { [weak self] manager, deviceState in - self?.deviceStateDidChange(deviceState) + TunnelBlockObserver(didUpdateDeviceState: { [weak self] manager, deviceState, previousDeviceState in + self?.deviceStateDidChange(deviceState, previousDeviceState: previousDeviceState) }) tunnelManager.addObserver(tunnelObserver) @@ -629,16 +629,28 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo splitViewController.preferredDisplayMode = tunnelManager.deviceState.splitViewMode } - private func deviceStateDidChange(_ deviceState: DeviceState) { + private func deviceStateDidChange(_ deviceState: DeviceState, previousDeviceState: DeviceState) { splitViewController.preferredDisplayMode = deviceState.splitViewMode switch deviceState { case let .loggedIn(accountData, _): - updateOutOfTimeTimer() + updateOutOfTimeTimer(accountData: accountData) updateView(deviceState: deviceState, showDeviceInfo: false) - if !accountData.isExpired { + + // Handle transition to and from expired state. + let accountWasExpired = previousDeviceState.accountData?.isExpired ?? false + switch (accountWasExpired, accountData.isExpired) { + case (true, false): + continueFlow(animated: true) router.dismiss(.outOfTime, animated: true) + + case (false, true): + router.present(.outOfTime, animated: true) + + default: + break } + case .revoked: cancelOutOfTimeTimer() router.present(.revoked, animated: true) @@ -663,13 +675,13 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo // MARK: - Out of time - private func updateOutOfTimeTimer() { + private func updateOutOfTimeTimer(accountData: StoredAccountData) { cancelOutOfTimeTimer() - guard let expiry = tunnelManager.deviceState.accountData?.expiry else { return } + guard !accountData.isExpired else { return } - let timer = Timer(fire: expiry, interval: 0, repeats: false, block: { [weak self] _ in - self?.outOfTimeTimerDidFire() + let timer = Timer(fire: accountData.expiry, interval: 0, repeats: false, block: { [weak self] _ in + self?.router.present(.outOfTime, animated: true) }) RunLoop.main.add(timer, forMode: .common) @@ -677,10 +689,6 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo outOfTimeTimer = timer } - private func outOfTimeTimerDidFire() { - router.present(.outOfTime, animated: true) - } - private func cancelOutOfTimeTimer() { outOfTimeTimer?.invalidate() outOfTimeTimer = nil diff --git a/ios/MullvadVPN/Coordinators/App/TunnelCoordinator.swift b/ios/MullvadVPN/Coordinators/App/TunnelCoordinator.swift index d619f1ebc5..c63b0f80ef 100644 --- a/ios/MullvadVPN/Coordinators/App/TunnelCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/App/TunnelCoordinator.swift @@ -35,7 +35,7 @@ class TunnelCoordinator: Coordinator, NotificationManagerDelegate { func start() { let tunnelObserver = - TunnelBlockObserver(didUpdateDeviceState: { [weak self] _, deviceState in + TunnelBlockObserver(didUpdateDeviceState: { [weak self] _, deviceState, previousDeviceState in self?.updateVisibility(animated: true) }) diff --git a/ios/MullvadVPN/Notifications/Notification Providers/AccountExpiryInAppNotificationProvider.swift b/ios/MullvadVPN/Notifications/Notification Providers/AccountExpiryInAppNotificationProvider.swift index 350aa0dcb0..ddaa37b68b 100644 --- a/ios/MullvadVPN/Notifications/Notification Providers/AccountExpiryInAppNotificationProvider.swift +++ b/ios/MullvadVPN/Notifications/Notification Providers/AccountExpiryInAppNotificationProvider.swift @@ -8,9 +8,6 @@ import Foundation -private let triggerInterval = 3 -private let refreshInterval = 60 - final class AccountExpiryInAppNotificationProvider: NotificationProvider, InAppNotificationProvider { private var accountExpiry: Date? private var tunnelObserver: TunnelBlockObserver? @@ -23,7 +20,7 @@ final class AccountExpiryInAppNotificationProvider: NotificationProvider, InAppN didLoadConfiguration: { [weak self] tunnelManager in self?.invalidate(deviceState: tunnelManager.deviceState) }, - didUpdateDeviceState: { [weak self] tunnelManager, deviceState in + didUpdateDeviceState: { [weak self] tunnelManager, deviceState, previousDeviceState in self?.invalidate(deviceState: deviceState) } ) @@ -89,7 +86,7 @@ final class AccountExpiryInAppNotificationProvider: NotificationProvider, InAppN return Calendar.current.date( byAdding: .day, - value: -triggerInterval, + value: -NotificationConfiguration.closeToExpiryTriggerInterval, to: accountExpiry ) } @@ -120,7 +117,7 @@ final class AccountExpiryInAppNotificationProvider: NotificationProvider, InAppN } timer.schedule( wallDeadline: .now() + fireDate.timeIntervalSince(now), - repeating: .seconds(refreshInterval) + repeating: .seconds(NotificationConfiguration.closeToExpiryInAppNotificationRefreshInterval) ) timer.activate() diff --git a/ios/MullvadVPN/Notifications/Notification Providers/AccountExpirySystemNotificationProvider.swift b/ios/MullvadVPN/Notifications/Notification Providers/AccountExpirySystemNotificationProvider.swift index c32bd24b28..b2edfb49f8 100644 --- a/ios/MullvadVPN/Notifications/Notification Providers/AccountExpirySystemNotificationProvider.swift +++ b/ios/MullvadVPN/Notifications/Notification Providers/AccountExpirySystemNotificationProvider.swift @@ -9,11 +9,7 @@ import Foundation import UserNotifications -private let triggerInterval = 3 - -final class AccountExpirySystemNotificationProvider: NotificationProvider, - SystemNotificationProvider -{ +final class AccountExpirySystemNotificationProvider: NotificationProvider, SystemNotificationProvider { private var accountExpiry: Date? private var tunnelObserver: TunnelBlockObserver? private var defaultActionHandler: (() -> Void)? @@ -25,7 +21,7 @@ final class AccountExpirySystemNotificationProvider: NotificationProvider, didLoadConfiguration: { [weak self] tunnelManager in self?.invalidate(deviceState: tunnelManager.deviceState) }, - didUpdateDeviceState: { [weak self] tunnelManager, deviceState in + didUpdateDeviceState: { [weak self] tunnelManager, deviceState, previousDeviceState in self?.invalidate(deviceState: deviceState) } ) @@ -101,7 +97,7 @@ final class AccountExpirySystemNotificationProvider: NotificationProvider, guard let triggerDate = Calendar.current.date( byAdding: .day, - value: -triggerInterval, + value: -NotificationConfiguration.closeToExpiryTriggerInterval, to: accountExpiry ) else { return nil } diff --git a/ios/MullvadVPN/Notifications/Notification Providers/NotificationConfiguration.swift b/ios/MullvadVPN/Notifications/Notification Providers/NotificationConfiguration.swift new file mode 100644 index 0000000000..98e1913608 --- /dev/null +++ b/ios/MullvadVPN/Notifications/Notification Providers/NotificationConfiguration.swift @@ -0,0 +1,23 @@ +// +// NotificationConfiguration.swift +// MullvadVPN +// +// Created by pronebird on 27/04/2023. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +enum NotificationConfiguration { + /** + Duration measured in days, before the account expiry, when notification is scheduled to remind user to add more + time on account. + */ + static let closeToExpiryTriggerInterval = 3 + + /** + Time interval measured in seconds at which to refresh account expiry in-app notification, which reformats + the duration until account expiry over time. + */ + static let closeToExpiryInAppNotificationRefreshInterval = 60 +} diff --git a/ios/MullvadVPN/Notifications/Notification Providers/RegisteredDeviceInAppNotification.swift b/ios/MullvadVPN/Notifications/Notification Providers/RegisteredDeviceInAppNotification.swift index d468e009c7..ae6007451f 100644 --- a/ios/MullvadVPN/Notifications/Notification Providers/RegisteredDeviceInAppNotification.swift +++ b/ios/MullvadVPN/Notifications/Notification Providers/RegisteredDeviceInAppNotification.swift @@ -1,5 +1,5 @@ // -// AccountCreationInAppNotification.swift +// RegisteredDeviceInAppNotification.swift // MullvadVPN // // Created by Mojgan on 2023-04-21. @@ -10,9 +10,7 @@ import Foundation import UIKit.UIColor import UIKit.UIFont -final class RegisteredDeviceInAppNotification: NotificationProvider, - InAppNotificationProvider -{ +final class RegisteredDeviceInAppNotification: NotificationProvider, InAppNotificationProvider { typealias CompletionHandler = (DeviceState) -> Void // MARK: - private properties @@ -78,13 +76,14 @@ final class RegisteredDeviceInAppNotification: NotificationProvider, } private func addObservers() { - tunnelObserver = TunnelBlockObserver(didUpdateDeviceState: { [weak self] tunnelManager, deviceState in - guard let self = self, - case .loggedIn = deviceState else { return } - self.shouldShowBanner = true - self.deviceState = deviceState - self.invalidate() - }) + tunnelObserver = + TunnelBlockObserver(didUpdateDeviceState: { [weak self] tunnelManager, deviceState, previousDeviceState in + guard let self = self, case .loggedIn = deviceState else { return } + + self.shouldShowBanner = true + self.deviceState = deviceState + self.invalidate() + }) tunnelObserver.flatMap { tunnelManager.addObserver($0) } } } diff --git a/ios/MullvadVPN/Notifications/NotificationManager.swift b/ios/MullvadVPN/Notifications/NotificationManager.swift index 6536ae09fc..68c251e9c7 100644 --- a/ios/MullvadVPN/Notifications/NotificationManager.swift +++ b/ios/MullvadVPN/Notifications/NotificationManager.swift @@ -96,15 +96,12 @@ final class NotificationManager: NotificationProviderDelegate { for newRequest in newSystemNotificationRequests { notificationCenter.add(newRequest) { error in - if let error = error { - self.logger.error( - error: error, - message: """ - Failed to add notification request with identifier \ - \(newRequest.identifier). - """ - ) - } + guard let error = error else { return } + + self.logger.error( + error: error, + message: "Failed to add notification request with identifier \(newRequest.identifier)." + ) } } } diff --git a/ios/MullvadVPN/SceneDelegate.swift b/ios/MullvadVPN/SceneDelegate.swift index a256f1add1..eeadacfc13 100644 --- a/ios/MullvadVPN/SceneDelegate.swift +++ b/ios/MullvadVPN/SceneDelegate.swift @@ -51,7 +51,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, SettingsMigrationUIHand didLoadConfiguration: { [weak self] _ in self?.configureScene() }, - didUpdateDeviceState: { [weak self] _, deviceState in + didUpdateDeviceState: { [weak self] _, deviceState, previousDeviceState in self?.deviceStateDidChange(deviceState) } ) diff --git a/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift b/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift index b9dca0bf21..7ee39c6934 100644 --- a/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift +++ b/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift @@ -85,6 +85,19 @@ enum DeviceState: Codable, Equatable { return nil } } + + /** + Mutates account and device data when in logged in state, otherwise does nothing. + */ + mutating func updateData(_ body: (inout StoredAccountData, inout StoredDeviceData) -> Void) { + switch self { + case var .loggedIn(accountData, deviceData): + body(&accountData, &deviceData) + self = .loggedIn(accountData, deviceData) + case .revoked, .loggedOut: + break + } + } } struct StoredDeviceData: Codable, Equatable { diff --git a/ios/MullvadVPN/TunnelManager/TunnelBlockObserver.swift b/ios/MullvadVPN/TunnelManager/TunnelBlockObserver.swift index 43b74bd33e..90b90b5470 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelBlockObserver.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelBlockObserver.swift @@ -11,7 +11,11 @@ import Foundation final class TunnelBlockObserver: TunnelObserver { typealias DidLoadConfigurationHandler = (TunnelManager) -> Void typealias DidUpdateTunnelStatusHandler = (TunnelManager, TunnelStatus) -> Void - typealias DidUpdateDeviceStateHandler = (TunnelManager, DeviceState) -> Void + typealias DidUpdateDeviceStateHandler = ( + _ tunnelManager: TunnelManager, + _ deviceState: DeviceState, + _ previousDeviceStaate: DeviceState + ) -> Void typealias DidUpdateTunnelSettingsHandler = (TunnelManager, TunnelSettingsV2) -> Void typealias DidFailWithErrorHandler = (TunnelManager, Error) -> Void @@ -43,14 +47,15 @@ final class TunnelBlockObserver: TunnelObserver { didUpdateTunnelStatus?(manager, tunnelStatus) } - func tunnelManager(_ manager: TunnelManager, didUpdateDeviceState deviceState: DeviceState) { - didUpdateDeviceState?(manager, deviceState) - } - func tunnelManager( _ manager: TunnelManager, - didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2 + didUpdateDeviceState deviceState: DeviceState, + previousDeviceState: DeviceState ) { + didUpdateDeviceState?(manager, deviceState, previousDeviceState) + } + + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2) { didUpdateTunnelSettings?(manager, tunnelSettings) } diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift index 8b4bd53c84..9c9190200b 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift @@ -708,6 +708,7 @@ final class TunnelManager: StorePaymentObserver { defer { nslock.unlock() } let shouldCallDelegate = _deviceState != deviceState && _isConfigurationLoaded + let previousDeviceState = _deviceState _deviceState = deviceState @@ -725,7 +726,11 @@ final class TunnelManager: StorePaymentObserver { if shouldCallDelegate { DispatchQueue.main.async { self.observerList.forEach { observer in - observer.tunnelManager(self, didUpdateDeviceState: deviceState) + observer.tunnelManager( + self, + didUpdateDeviceState: deviceState, + previousDeviceState: previousDeviceState + ) } } } @@ -955,6 +960,93 @@ final class TunnelManager: StorePaymentObserver { } } +#if DEBUG + +// MARK: - Simulations + +extension TunnelManager { + enum AccountExpirySimulationOption { + case closeToExpiry + case expired + case active + + fileprivate var date: Date? { + let calendar = Calendar.current + let now = Date() + + switch self { + case .active: + return calendar.date(byAdding: .year, value: 1, to: now) + + case .closeToExpiry: + return calendar.date( + byAdding: DateComponents(day: NotificationConfiguration.closeToExpiryTriggerInterval, second: 5), + to: now + ) + + case .expired: + return calendar.date(byAdding: .minute, value: -1, to: now) + } + } + } + + /** + + This function simulates account state transitions. The change is not permanent and any call to + `updateAccountData()` will overwrite it, but it's usually enough for quick testing. + + It can be invoked somewhere in `initTunnelManagerOperation` (`AppDelegate`) after tunnel manager is fully + initialized. The following code snippet can be used to cycle through various states: + + ``` + func delay(seconds: UInt) async throws { + try await Task.sleep(nanoseconds: UInt64(seconds) * 1_000_000_000) + } + + Task { + print("Wait 5 seconds") + try await delay(seconds: 5) + + print("Simulate active account") + self.tunnelManager.simulateAccountExpiration(option: .active) + try await delay(seconds: 5) + + print("Simulate close to expiry") + self.tunnelManager.simulateAccountExpiration(option: .closeToExpiry) + try await delay(seconds: 10) + + print("Simulate expired account") + self.tunnelManager.simulateAccountExpiration(option: .expired) + try await delay(seconds: 5) + + print("Simulate active account") + self.tunnelManager.simulateAccountExpiration(option: .active) + } + ``` + + Another way to invoke this code is to pause debugger and run it directly: + + ``` + command alias swift expression -l Swift -O -- + + swift import MullvadVPN + swift (UIApplication.shared.delegate as? AppDelegate)?.tunnelManager.simulateAccountExpiration(option: .closeToExpiry) + ``` + + */ + func simulateAccountExpiration(option: AccountExpirySimulationOption) { + scheduleDeviceStateUpdate(taskName: "Simulating account expiry", reconnectTunnel: false) { deviceState in + deviceState.updateData { accountData, deviceData in + guard let date = option.date else { return } + + accountData.expiry = date + } + } + } +} + +#endif + private struct TunnelInteractorProxy: TunnelInteractor { private let tunnelManager: TunnelManager diff --git a/ios/MullvadVPN/TunnelManager/TunnelObserver.swift b/ios/MullvadVPN/TunnelManager/TunnelObserver.swift index 148da699ae..f53461fb70 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelObserver.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelObserver.swift @@ -11,12 +11,11 @@ import Foundation protocol TunnelObserver: AnyObject { func tunnelManagerDidLoadConfiguration(_ manager: TunnelManager) func tunnelManager(_ manager: TunnelManager, didUpdateTunnelStatus tunnelStatus: TunnelStatus) - func tunnelManager(_ manager: TunnelManager, didUpdateDeviceState deviceState: DeviceState) - func tunnelManager( _ manager: TunnelManager, - didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2 + didUpdateDeviceState deviceState: DeviceState, + previousDeviceState: DeviceState ) - + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2) func tunnelManager(_ manager: TunnelManager, didFailWithError error: Error) } diff --git a/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift b/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift index c454181d95..6d8de2958b 100644 --- a/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift +++ b/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift @@ -27,7 +27,7 @@ final class AccountInteractor { self.tunnelManager = tunnelManager let tunnelObserver = - TunnelBlockObserver(didUpdateDeviceState: { [weak self] manager, deviceState in + TunnelBlockObserver(didUpdateDeviceState: { [weak self] manager, deviceState, previousDeviceState in self?.didReceiveDeviceState?(deviceState) }) diff --git a/ios/MullvadVPN/View controllers/Account/AccountViewController.swift b/ios/MullvadVPN/View controllers/Account/AccountViewController.swift index a7c4beb867..9edd3c85fc 100644 --- a/ios/MullvadVPN/View controllers/Account/AccountViewController.swift +++ b/ios/MullvadVPN/View controllers/Account/AccountViewController.swift @@ -102,8 +102,8 @@ class AccountViewController: UIViewController { ) contentView.logoutButton.addTarget(self, action: #selector(doLogout), for: .touchUpInside) - interactor.didReceiveDeviceState = { [weak self] newDeviceState in - self?.updateView(from: newDeviceState) + interactor.didReceiveDeviceState = { [weak self] deviceState in + self?.updateView(from: deviceState) } interactor.didReceivePaymentEvent = { [weak self] event in diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsInteractor.swift b/ios/MullvadVPN/View controllers/Settings/SettingsInteractor.swift index cd5c6e6e68..c695772823 100644 --- a/ios/MullvadVPN/View controllers/Settings/SettingsInteractor.swift +++ b/ios/MullvadVPN/View controllers/Settings/SettingsInteractor.swift @@ -22,7 +22,7 @@ final class SettingsInteractor { self.tunnelManager = tunnelManager let tunnelObserver = - TunnelBlockObserver(didUpdateDeviceState: { [weak self] manager, deviceState in + TunnelBlockObserver(didUpdateDeviceState: { [weak self] manager, deviceState, previousDeviceState in self?.didUpdateDeviceState?(deviceState) }) diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift index 4f190995b6..4ec24fd400 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift @@ -56,7 +56,7 @@ class TunnelViewController: UIViewController, RootContainment { override func viewDidLoad() { super.viewDidLoad() - interactor.didUpdateDeviceState = { [weak self] deviceState in + interactor.didUpdateDeviceState = { [weak self] deviceState, previousDeviceState in self?.setNeedsHeaderBarStyleAppearanceUpdate() } diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelViewControllerInteractor.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewControllerInteractor.swift index b23bc214f2..e0eb61c8fb 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/TunnelViewControllerInteractor.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewControllerInteractor.swift @@ -12,7 +12,7 @@ final class TunnelViewControllerInteractor { private let tunnelManager: TunnelManager private var tunnelObserver: TunnelObserver? - var didUpdateDeviceState: ((DeviceState) -> Void)? + var didUpdateDeviceState: ((_ deviceState: DeviceState, _ previousDeviceState: DeviceState) -> Void)? var didUpdateTunnelStatus: ((TunnelStatus) -> Void)? var tunnelStatus: TunnelStatus { @@ -30,8 +30,8 @@ final class TunnelViewControllerInteractor { didUpdateTunnelStatus: { [weak self] tunnelManager, tunnelStatus in self?.didUpdateTunnelStatus?(tunnelStatus) }, - didUpdateDeviceState: { [weak self] tunnelManager, deviceState in - self?.didUpdateDeviceState?(deviceState) + didUpdateDeviceState: { [weak self] tunnelManager, deviceState, previousDeviceState in + self?.didUpdateDeviceState?(deviceState, previousDeviceState) } ) tunnelManager.addObserver(tunnelObserver) |
