diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2022-12-12 15:52:58 +0100 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2022-12-12 15:52:58 +0100 |
| commit | 41d084b2d94ad68390b60a5f200f001500c352d4 (patch) | |
| tree | 87e7ace83ce32705e94a3b5b16cd41fdca6869f8 | |
| parent | fe9fe00c2630efa49d2007dc5c38584aab81dabb (diff) | |
| parent | 37645072fe534baa798a448aceb131802f911231 (diff) | |
| download | mullvadvpn-41d084b2d94ad68390b60a5f200f001500c352d4.tar.xz mullvadvpn-41d084b2d94ad68390b60a5f200f001500c352d4.zip | |
Merge branch 'refactor-notifications'
14 files changed, 294 insertions, 146 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 9c8eb65127..5661dcee7c 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -250,6 +250,12 @@ 58ACF64D26567A5000ACE4B7 /* CustomSwitch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ACF64C26567A4F00ACE4B7 /* CustomSwitch.swift */; }; 58ACF64F26567A7100ACE4B7 /* CustomSwitchContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ACF64E26567A7100ACE4B7 /* CustomSwitchContainer.swift */; }; 58B0A2A8238EE68200BC001D /* RelaySelectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584B26F3237434D00073B10E /* RelaySelectorTests.swift */; }; + 58B26E1E2943514300D5980C /* InAppNotificationDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B26E1D2943514300D5980C /* InAppNotificationDescriptor.swift */; }; + 58B26E22294351EA00D5980C /* InAppNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B26E21294351EA00D5980C /* InAppNotificationProvider.swift */; }; + 58B26E242943520C00D5980C /* NotificationProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B26E232943520C00D5980C /* NotificationProviderProtocol.swift */; }; + 58B26E262943522400D5980C /* NotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B26E252943522400D5980C /* NotificationProvider.swift */; }; + 58B26E282943527300D5980C /* SystemNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B26E272943527300D5980C /* SystemNotificationProvider.swift */; }; + 58B26E2A2943545A00D5980C /* NotificationManagerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B26E292943545A00D5980C /* NotificationManagerDelegate.swift */; }; 58B3F30F2742708B00A2DD38 /* HeaderBarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B3F30E2742708B00A2DD38 /* HeaderBarButton.swift */; }; 58B43C1925F77DB60002C8C3 /* ConnectContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B43C1825F77DB60002C8C3 /* ConnectContentView.swift */; }; 58B93A1326C3F13600A55733 /* TunnelState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B93A1226C3F13600A55733 /* TunnelState.swift */; }; @@ -747,6 +753,12 @@ 58AEEF642344A36000C9BBD5 /* KeychainError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainError.swift; sourceTree = "<group>"; }; 58B0A2A0238EE67E00BC001D /* MullvadVPNTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MullvadVPNTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 58B0A2A4238EE67E00BC001D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; + 58B26E1D2943514300D5980C /* InAppNotificationDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppNotificationDescriptor.swift; sourceTree = "<group>"; }; + 58B26E21294351EA00D5980C /* InAppNotificationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppNotificationProvider.swift; sourceTree = "<group>"; }; + 58B26E232943520C00D5980C /* NotificationProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationProviderProtocol.swift; sourceTree = "<group>"; }; + 58B26E252943522400D5980C /* NotificationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationProvider.swift; sourceTree = "<group>"; }; + 58B26E272943527300D5980C /* SystemNotificationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemNotificationProvider.swift; sourceTree = "<group>"; }; + 58B26E292943545A00D5980C /* NotificationManagerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManagerDelegate.swift; sourceTree = "<group>"; }; 58B3F30E2742708B00A2DD38 /* HeaderBarButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderBarButton.swift; sourceTree = "<group>"; }; 58B43C1825F77DB60002C8C3 /* ConnectContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectContentView.swift; sourceTree = "<group>"; }; 58B93A1226C3F13600A55733 /* TunnelState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelState.swift; sourceTree = "<group>"; }; @@ -1164,13 +1176,13 @@ path = Operations; sourceTree = "<group>"; }; - 587B75422669034500DEF7E9 /* Notifications */ = { + 587B75422669034500DEF7E9 /* Notification Providers */ = { isa = PBXGroup; children = ( 587B75402668FD7700DEF7E9 /* AccountExpiryNotificationProvider.swift */, 58A94AE326CFD945001CB97C /* TunnelStatusNotificationProvider.swift */, ); - path = Notifications; + path = "Notification Providers"; sourceTree = "<group>"; }; 5898D28A29017BD400EB5EBA /* TunnelProviderMessaging */ = { @@ -1235,6 +1247,32 @@ path = MullvadVPNTests; sourceTree = "<group>"; }; + 58B26E1F2943516500D5980C /* Notifications */ = { + isa = PBXGroup; + children = ( + 58B26E20294351A600D5980C /* UI */, + 587B75422669034500DEF7E9 /* Notification Providers */, + 58B26E232943520C00D5980C /* NotificationProviderProtocol.swift */, + 58B26E252943522400D5980C /* NotificationProvider.swift */, + 58B26E1D2943514300D5980C /* InAppNotificationDescriptor.swift */, + 58B26E21294351EA00D5980C /* InAppNotificationProvider.swift */, + 58B26E272943527300D5980C /* SystemNotificationProvider.swift */, + 587B7535266528A200DEF7E9 /* NotificationManager.swift */, + 58B26E292943545A00D5980C /* NotificationManagerDelegate.swift */, + ); + path = Notifications; + sourceTree = "<group>"; + }; + 58B26E20294351A600D5980C /* UI */ = { + isa = PBXGroup; + children = ( + 587B753C2666468F00DEF7E9 /* NotificationController.swift */, + 587B753E2668E5A700DEF7E9 /* NotificationContainerView.swift */, + 587B753A2666467500DEF7E9 /* NotificationBannerView.swift */, + ); + path = UI; + sourceTree = "<group>"; + }; 58CE5E57224146200008646E = { isa = PBXGroup; children = ( @@ -1337,11 +1375,7 @@ 5866F39B2243B82D00168AE5 /* MullvadVPN.entitlements */, 58906DDF2445C7A5002F0673 /* NEProviderStopReason+Debug.swift */, 5811DE4F239014550011EB53 /* NEVPNStatus+Debug.swift */, - 587B753A2666467500DEF7E9 /* NotificationBannerView.swift */, - 587B753E2668E5A700DEF7E9 /* NotificationContainerView.swift */, - 587B753C2666468F00DEF7E9 /* NotificationController.swift */, - 587B7535266528A200DEF7E9 /* NotificationManager.swift */, - 587B75422669034500DEF7E9 /* Notifications */, + 58B26E1F2943516500D5980C /* Notifications */, 584EBDBC2747C98F00A0C9FD /* NSAttributedString+Markdown.swift */, 587D9675288989DB00CD8F1C /* NSLayoutConstraint+Helpers.swift */, 5871FB9F254C26BF0051A0A4 /* NSRegularExpression+IPAddress.swift */, @@ -2178,6 +2212,7 @@ 587B753B2666467500DEF7E9 /* NotificationBannerView.swift in Sources */, 58B993B12608A34500BA7811 /* LoginContentView.swift in Sources */, 5878A27529093A310096FC88 /* StorePaymentEvent.swift in Sources */, + 58B26E2A2943545A00D5980C /* NotificationManagerDelegate.swift in Sources */, 58E6771F24ADFE7800AA26E7 /* SettingsNavigationController.swift in Sources */, 58A1AA8C23F5584C009F7EA6 /* ConnectionPanelView.swift in Sources */, 5878A27B2909649A0096FC88 /* CustomOverlayRenderer.swift in Sources */, @@ -2189,6 +2224,7 @@ 5878A27129091CF20096FC88 /* AccountInteractor.swift in Sources */, 068CE5742927B7A400A068BB /* Migration.swift in Sources */, 58CCA010224249A1004F3011 /* ConnectViewController.swift in Sources */, + 58B26E22294351EA00D5980C /* InAppNotificationProvider.swift in Sources */, 5893716A28817A45004EE76C /* DeviceManagementViewController.swift in Sources */, 58BFA5C622A7C97F00A6173D /* RelayCacheTracker.swift in Sources */, E158B360285381C60002F069 /* StringFormatter.swift in Sources */, @@ -2256,6 +2292,7 @@ 5878A26F2907E7E00096FC88 /* ProblemReportInteractor.swift in Sources */, 58E11188292FA11F009FCA84 /* SettingsMigrationUIHandler.swift in Sources */, 5807E2C02432038B00F5FF30 /* String+Split.swift in Sources */, + 58B26E242943520C00D5980C /* NotificationProviderProtocol.swift in Sources */, 58677712290976FB006F721F /* SettingsInteractor.swift in Sources */, 58CE5E66224146200008646E /* LoginViewController.swift in Sources */, 58EF580B25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift in Sources */, @@ -2265,6 +2302,7 @@ 06410E07292D108E00AFC18C /* SettingsStore.swift in Sources */, 586A950D290125F0007BAF2B /* PresentAlertOperation.swift in Sources */, 58B93A1326C3F13600A55733 /* TunnelState.swift in Sources */, + 58B26E262943522400D5980C /* NotificationProvider.swift in Sources */, 58FEEB46260A028D00A621A8 /* GeoJSON.swift in Sources */, 58CE5E64224146200008646E /* AppDelegate.swift in Sources */, 5878A27329091D6D0096FC88 /* TunnelBlockObserver.swift in Sources */, @@ -2273,10 +2311,12 @@ 58ACF64F26567A7100ACE4B7 /* CustomSwitchContainer.swift in Sources */, 5857F24324C8662600CF6F47 /* SelectLocationHeaderView.swift in Sources */, 58EE2E3A272FF814003BFF93 /* SettingsDataSource.swift in Sources */, + 58B26E1E2943514300D5980C /* InAppNotificationDescriptor.swift in Sources */, 58421032282E42B000F24E46 /* UpdateDeviceDataOperation.swift in Sources */, 06410E04292D0F7100AFC18C /* SettingsParser.swift in Sources */, 5878A27D2909657C0096FC88 /* RevokedDeviceInteractor.swift in Sources */, 58677710290975E9006F721F /* SettingsInteractorFactory.swift in Sources */, + 58B26E282943527300D5980C /* SystemNotificationProvider.swift in Sources */, 58CCA01222424D11004F3011 /* SettingsViewController.swift in Sources */, 06410DFE292CE18F00AFC18C /* KeychainSettingsStore.swift in Sources */, 580F8B8628197958002E0998 /* DNSSettings.swift in Sources */, diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index eec04a0851..7f2cb40392 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -389,8 +389,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } private func setupNotificationHandler() { + let accountExpiryProvider = AccountExpiryNotificationProvider(tunnelManager: tunnelManager) + + accountExpiryProvider.defaultActionHandler = { + let sceneDelegate = UIApplication.shared.connectedScenes + .first?.delegate as? SceneDelegate + + sceneDelegate?.showUserAccount() + } + NotificationManager.shared.notificationProviders = [ - AccountExpiryNotificationProvider(tunnelManager: tunnelManager), + accountExpiryProvider, TunnelStatusNotificationProvider(tunnelManager: tunnelManager), ] UNUserNotificationCenter.current().delegate = self @@ -416,14 +425,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD withCompletionHandler completionHandler: @escaping () -> Void ) { let blockOperation = AsyncBlockOperation(dispatchQueue: .main) { - if response.notification.request.identifier == accountExpiryNotificationIdentifier, - response.actionIdentifier == UNNotificationDefaultActionIdentifier - { - let sceneDelegate = UIApplication.shared.connectedScenes - .first?.delegate as? SceneDelegate - - sceneDelegate?.showUserAccount() - } + NotificationManager.shared.handleSystemNotificationResponse(response) completionHandler() } diff --git a/ios/MullvadVPN/Notifications/InAppNotificationDescriptor.swift b/ios/MullvadVPN/Notifications/InAppNotificationDescriptor.swift new file mode 100644 index 0000000000..7d16791fc7 --- /dev/null +++ b/ios/MullvadVPN/Notifications/InAppNotificationDescriptor.swift @@ -0,0 +1,28 @@ +// +// InAppNotificationDescriptor.swift +// MullvadVPN +// +// Created by pronebird on 09/12/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +/// Struct describing in-app notification. +struct InAppNotificationDescriptor: Equatable { + /// Notification identifier. + var identifier: String + + /// Notification banner style. + var style: NotificationBannerStyle + + /// Notification title. + var title: String + + /// Notification body. + var body: String +} + +enum NotificationBannerStyle { + case success, warning, error +} diff --git a/ios/MullvadVPN/Notifications/InAppNotificationProvider.swift b/ios/MullvadVPN/Notifications/InAppNotificationProvider.swift new file mode 100644 index 0000000000..8410aef8b3 --- /dev/null +++ b/ios/MullvadVPN/Notifications/InAppNotificationProvider.swift @@ -0,0 +1,15 @@ +// +// InAppNotificationProvider.swift +// MullvadVPN +// +// Created by pronebird on 09/12/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +/// Protocol describing in-app notification provider. +protocol InAppNotificationProvider: NotificationProviderProtocol { + /// In-app notification descriptor. + var notificationDescriptor: InAppNotificationDescriptor? { get } +} diff --git a/ios/MullvadVPN/Notifications/AccountExpiryNotificationProvider.swift b/ios/MullvadVPN/Notifications/Notification Providers/AccountExpiryNotificationProvider.swift index fc6a579661..80b7a06428 100644 --- a/ios/MullvadVPN/Notifications/AccountExpiryNotificationProvider.swift +++ b/ios/MullvadVPN/Notifications/Notification Providers/AccountExpiryNotificationProvider.swift @@ -9,19 +9,19 @@ import Foundation import UserNotifications -let accountExpiryNotificationIdentifier = "net.mullvad.MullvadVPN.AccountExpiryNotification" private let defaultTriggerInterval = 3 -class AccountExpiryNotificationProvider: NotificationProvider, SystemNotificationProvider, +final class AccountExpiryNotificationProvider: NotificationProvider, SystemNotificationProvider, InAppNotificationProvider, TunnelObserver { - private var accountExpiry: Date? - /// Interval prior to expiry used to calculate when to trigger notifications. private let triggerInterval: Int + private var accountExpiry: Date? + + var defaultActionHandler: (() -> Void)? override var identifier: String { - return accountExpiryNotificationIdentifier + return "net.mullvad.MullvadVPN.AccountExpiryNotification" } init(tunnelManager: TunnelManager, triggerInterval: Int = defaultTriggerInterval) { @@ -85,7 +85,7 @@ class AccountExpiryNotificationProvider: NotificationProvider, SystemNotificatio content.sound = UNNotificationSound.default return UNNotificationRequest( - identifier: accountExpiryNotificationIdentifier, + identifier: identifier, content: content, trigger: trigger ) @@ -101,6 +101,18 @@ class AccountExpiryNotificationProvider: NotificationProvider, SystemNotificatio return shouldRemovePendingOrDeliveredRequests } + func handleResponse(_ response: UNNotificationResponse) -> Bool { + guard response.notification.request.identifier == identifier else { + return false + } + + if response.actionIdentifier == UNNotificationDefaultActionIdentifier { + defaultActionHandler?() + } + + return true + } + // MARK: - InAppNotificationProvider var notificationDescriptor: InAppNotificationDescriptor? { @@ -170,6 +182,6 @@ class AccountExpiryNotificationProvider: NotificationProvider, SystemNotificatio } func tunnelManager(_ manager: TunnelManager, didUpdateDeviceState deviceState: DeviceState) { - invalidate(deviceState: manager.deviceState) + invalidate(deviceState: deviceState) } } diff --git a/ios/MullvadVPN/Notifications/TunnelStatusNotificationProvider.swift b/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift index 81c2709267..df59cc1433 100644 --- a/ios/MullvadVPN/Notifications/TunnelStatusNotificationProvider.swift +++ b/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift @@ -8,7 +8,7 @@ import Foundation -class TunnelStatusNotificationProvider: NotificationProvider, InAppNotificationProvider, +final class TunnelStatusNotificationProvider: NotificationProvider, InAppNotificationProvider, TunnelObserver { private var isWaitingForConnectivity = false diff --git a/ios/MullvadVPN/NotificationManager.swift b/ios/MullvadVPN/Notifications/NotificationManager.swift index ca29eccabf..7cf48066cb 100644 --- a/ios/MullvadVPN/NotificationManager.swift +++ b/ios/MullvadVPN/Notifications/NotificationManager.swift @@ -8,86 +8,36 @@ import Foundation import MullvadLogging -import UIKit import UserNotifications -protocol NotificationManagerDelegate: AnyObject { - func notificationManagerDidUpdateInAppNotifications( - _ manager: NotificationManager, - notifications: [InAppNotificationDescriptor] - ) -} - -private protocol NotificationProviderDelegate: AnyObject { - func notificationProviderDidInvalidate(_ notificationProvider: NotificationProvider) -} - -class NotificationProvider { - fileprivate weak var delegate: NotificationProviderDelegate? - - var identifier: String { - return "default" - } - - func invalidate() { - let executor = { - self.delegate?.notificationProviderDidInvalidate(self) - return - } - - if Thread.isMainThread { - executor() - } else { - DispatchQueue.main.async(execute: executor) - } - } -} - -protocol SystemNotificationProvider { - /// Unique provider identifier used to identify requests - var identifier: String { get } - - /// Notification request if available, otherwise `nil` - var notificationRequest: UNNotificationRequest? { get } - - /// Whether any pending requests should be removed - var shouldRemovePendingRequests: Bool { get } - - /// Whether any delivered requests should be removed - var shouldRemoveDeliveredRequests: Bool { get } -} +final class NotificationManager: NotificationProviderDelegate { + private lazy var logger = Logger(label: "NotificationManager") + private var _notificationProviders: [NotificationProvider] = [] + private var inAppNotificationDescriptors: [InAppNotificationDescriptor] = [] -protocol InAppNotificationProvider { - /// Unique provider identifier used to identify notifications - var identifier: String { get } + var notificationProviders: [NotificationProvider] { + set(newNotificationProviders) { + dispatchPrecondition(condition: .onQueue(.main)) - /// In-app notification descriptor - var notificationDescriptor: InAppNotificationDescriptor? { get } -} + for oldNotificationProvider in _notificationProviders { + oldNotificationProvider.delegate = nil + } -class NotificationManager: NotificationProviderDelegate { - private lazy var logger = Logger(label: "NotificationManager") + for newNotificationProvider in newNotificationProviders { + newNotificationProvider.delegate = self + } - var notificationProviders: [NotificationProvider] = [] { - willSet { - assert(Thread.isMainThread) + _notificationProviders = newNotificationProviders } - - didSet { - for notificationProvider in notificationProviders { - notificationProvider.delegate = self - } + get { + return _notificationProviders } } - private var inAppNotificationDescriptors: [InAppNotificationDescriptor] = [] - weak var delegate: NotificationManagerDelegate? { - willSet { - assert(Thread.isMainThread) - } - didSet { + dispatchPrecondition(condition: .onQueue(.main)) + // Pump in-app notifications when changing delegate. if !inAppNotificationDescriptors.isEmpty { delegate?.notificationManagerDidUpdateInAppNotifications( @@ -103,7 +53,7 @@ class NotificationManager: NotificationProviderDelegate { private init() {} func updateNotifications() { - assert(Thread.isMainThread) + dispatchPrecondition(condition: .onQueue(.main)) var newSystemNotificationRequests = [UNNotificationRequest]() var newInAppNotificationDescriptors = [InAppNotificationDescriptor]() @@ -133,10 +83,13 @@ class NotificationManager: NotificationProviderDelegate { } let notificationCenter = UNUserNotificationCenter.current() - notificationCenter - .removePendingNotificationRequests(withIdentifiers: pendingRequestIdentifiersToRemove) - notificationCenter - .removeDeliveredNotifications(withIdentifiers: deliveredRequestIdentifiersToRemove) + + notificationCenter.removePendingNotificationRequests( + withIdentifiers: pendingRequestIdentifiersToRemove + ) + notificationCenter.removeDeliveredNotifications( + withIdentifiers: deliveredRequestIdentifiersToRemove + ) requestNotificationPermissions { granted in guard granted else { return } @@ -146,7 +99,10 @@ class NotificationManager: NotificationProviderDelegate { if let error = error { self.logger.error( error: error, - message: "Failed to add notification request with identifier \(newRequest.identifier)." + message: """ + Failed to add notification request with identifier \ + \(newRequest.identifier). + """ ) } } @@ -161,6 +117,14 @@ class NotificationManager: NotificationProviderDelegate { ) } + func handleSystemNotificationResponse(_ response: UNNotificationResponse) { + for case let notificationProvider as SystemNotificationProvider in notificationProviders { + if notificationProvider.handleResponse(response) { + return + } + } + } + // MARK: - Private private func requestNotificationPermissions(completion: @escaping (Bool) -> Void) { @@ -170,16 +134,17 @@ class NotificationManager: NotificationProviderDelegate { userNotificationCenter.getNotificationSettings { notificationSettings in switch notificationSettings.authorizationStatus { case .notDetermined: - userNotificationCenter - .requestAuthorization(options: authorizationOptions) { granted, error in - if let error = error { - self.logger.error( - error: error, - message: "Failed to obtain user notifications authorization" - ) - } - completion(granted) + userNotificationCenter.requestAuthorization( + options: authorizationOptions + ) { granted, error in + if let error = error { + self.logger.error( + error: error, + message: "Failed to obtain user notifications authorization" + ) } + completion(granted) + } case .authorized, .provisional: completion(true) @@ -203,19 +168,15 @@ class NotificationManager: NotificationProviderDelegate { let notificationCenter = UNUserNotificationCenter.current() if notificationProvider.shouldRemovePendingRequests { - notificationCenter - .removePendingNotificationRequests(withIdentifiers: [ - notificationProvider - .identifier, - ]) + notificationCenter.removePendingNotificationRequests(withIdentifiers: [ + notificationProvider.identifier, + ]) } if notificationProvider.shouldRemoveDeliveredRequests { - notificationCenter - .removeDeliveredNotifications(withIdentifiers: [ - notificationProvider - .identifier, - ]) + notificationCenter.removeDeliveredNotifications(withIdentifiers: [ + notificationProvider.identifier, + ]) } if let request = notificationProvider.notificationRequest { @@ -224,10 +185,12 @@ class NotificationManager: NotificationProviderDelegate { notificationCenter.add(request) { error in if let error = error { - self.logger - .error( - "Failed to add notification request with identifier \(request.identifier). Error: \(error.localizedDescription)" - ) + self.logger.error( + """ + Failed to add notification request with identifier \ + \(request.identifier). Error: \(error.localizedDescription) + """ + ) } } } diff --git a/ios/MullvadVPN/Notifications/NotificationManagerDelegate.swift b/ios/MullvadVPN/Notifications/NotificationManagerDelegate.swift new file mode 100644 index 0000000000..818d7deaad --- /dev/null +++ b/ios/MullvadVPN/Notifications/NotificationManagerDelegate.swift @@ -0,0 +1,16 @@ +// +// NotificationManagerDelegate.swift +// MullvadVPN +// +// Created by pronebird on 09/12/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +protocol NotificationManagerDelegate: AnyObject { + func notificationManagerDidUpdateInAppNotifications( + _ manager: NotificationManager, + notifications: [InAppNotificationDescriptor] + ) +} diff --git a/ios/MullvadVPN/Notifications/NotificationProvider.swift b/ios/MullvadVPN/Notifications/NotificationProvider.swift new file mode 100644 index 0000000000..67299226c5 --- /dev/null +++ b/ios/MullvadVPN/Notifications/NotificationProvider.swift @@ -0,0 +1,36 @@ +// +// NotificationProvider.swift +// MullvadVPN +// +// Created by pronebird on 09/12/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +/// Notification provider delegate primarily used by `NotificationManager`. +protocol NotificationProviderDelegate: AnyObject { + func notificationProviderDidInvalidate(_ notificationProvider: NotificationProvider) +} + +/// Base class for all notification providers. +class NotificationProvider: NotificationProviderProtocol { + weak var delegate: NotificationProviderDelegate? + + var identifier: String { + return "default" + } + + func invalidate() { + let executor = { + self.delegate?.notificationProviderDidInvalidate(self) + return + } + + if Thread.isMainThread { + executor() + } else { + DispatchQueue.main.async(execute: executor) + } + } +} diff --git a/ios/MullvadVPN/Notifications/NotificationProviderProtocol.swift b/ios/MullvadVPN/Notifications/NotificationProviderProtocol.swift new file mode 100644 index 0000000000..ee512e3961 --- /dev/null +++ b/ios/MullvadVPN/Notifications/NotificationProviderProtocol.swift @@ -0,0 +1,19 @@ +// +// NotificationProviderProtocol.swift +// MullvadVPN +// +// Created by pronebird on 09/12/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +/// Base protocol for notification providers. +protocol NotificationProviderProtocol { + /// Unique provider identifier used to identify notification providers and notifications + /// produced by them. + var identifier: String { get } + + /// Tell notifcation manager to update the associated notification. + func invalidate() +} diff --git a/ios/MullvadVPN/Notifications/SystemNotificationProvider.swift b/ios/MullvadVPN/Notifications/SystemNotificationProvider.swift new file mode 100644 index 0000000000..f346e3e230 --- /dev/null +++ b/ios/MullvadVPN/Notifications/SystemNotificationProvider.swift @@ -0,0 +1,26 @@ +// +// SystemNotificationProvider.swift +// MullvadVPN +// +// Created by pronebird on 09/12/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import UserNotifications + +/// Protocol describing a system notification provider. +protocol SystemNotificationProvider: NotificationProviderProtocol { + /// Notification request if available, otherwise `nil`. + var notificationRequest: UNNotificationRequest? { get } + + /// Whether any pending requests should be removed. + var shouldRemovePendingRequests: Bool { get } + + /// Whether any delivered requests should be removed. + var shouldRemoveDeliveredRequests: Bool { get } + + /// Handle system notification response. + /// Return `true` if response was handled by this provider, otherwise `false`. + func handleResponse(_ response: UNNotificationResponse) -> Bool +} diff --git a/ios/MullvadVPN/NotificationBannerView.swift b/ios/MullvadVPN/Notifications/UI/NotificationBannerView.swift index 325230d8a6..858d8a7f6b 100644 --- a/ios/MullvadVPN/NotificationBannerView.swift +++ b/ios/MullvadVPN/Notifications/UI/NotificationBannerView.swift @@ -8,22 +8,7 @@ import UIKit -enum NotificationBannerStyle { - case success, warning, error - - fileprivate var color: UIColor { - switch self { - case .success: - return UIColor.InAppNotificationBanner.successIndicatorColor - case .warning: - return UIColor.InAppNotificationBanner.warningIndicatorColor - case .error: - return UIColor.InAppNotificationBanner.errorIndicatorColor - } - } -} - -class NotificationBannerView: UIView { +final class NotificationBannerView: UIView { private static let indicatorViewSize = CGSize(width: 12, height: 12) private let backgroundView: UIVisualEffectView = { @@ -145,3 +130,16 @@ class NotificationBannerView: UIView { fatalError("init(coder:) has not been implemented") } } + +private extension NotificationBannerStyle { + var color: UIColor { + switch self { + case .success: + return UIColor.InAppNotificationBanner.successIndicatorColor + case .warning: + return UIColor.InAppNotificationBanner.warningIndicatorColor + case .error: + return UIColor.InAppNotificationBanner.errorIndicatorColor + } + } +} diff --git a/ios/MullvadVPN/NotificationContainerView.swift b/ios/MullvadVPN/Notifications/UI/NotificationContainerView.swift index 1a4ee8a5d5..8f77accff6 100644 --- a/ios/MullvadVPN/NotificationContainerView.swift +++ b/ios/MullvadVPN/Notifications/UI/NotificationContainerView.swift @@ -8,7 +8,7 @@ import UIKit -class NotificationContainerView: UIView { +final class NotificationContainerView: UIView { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { let hitView = super.hitTest(point, with: event) diff --git a/ios/MullvadVPN/NotificationController.swift b/ios/MullvadVPN/Notifications/UI/NotificationController.swift index 8d07dfefb9..aa325332ed 100644 --- a/ios/MullvadVPN/NotificationController.swift +++ b/ios/MullvadVPN/Notifications/UI/NotificationController.swift @@ -8,14 +8,7 @@ import UIKit -struct InAppNotificationDescriptor: Equatable { - var identifier: String - var style: NotificationBannerStyle - var title: String - var body: String -} - -class NotificationController: UIViewController { +final class NotificationController: UIViewController { let bannerView: NotificationBannerView = { let bannerView = NotificationBannerView() bannerView.translatesAutoresizingMaskIntoConstraints = false |
