From 17bd8823feb47d6572cec48a555e70d9d86adf77 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Sun, 3 Mar 2024 09:13:39 -0800 Subject: [PATCH] Combine notification settings into main settings UI. --- DuckDuckGo.xcodeproj/project.pbxproj | 16 ---- ...orkProtectionConvenienceInitialisers.swift | 8 +- DuckDuckGo/NetworkProtectionStatusView.swift | 3 - ...etworkProtectionVPNNotificationsView.swift | 88 ------------------- ...kProtectionVPNNotificationsViewModel.swift | 79 ----------------- .../NetworkProtectionVPNSettingsView.swift | 51 ++++++++++- ...etworkProtectionVPNSettingsViewModel.swift | 48 +++++++++- 7 files changed, 98 insertions(+), 195 deletions(-) delete mode 100644 DuckDuckGo/NetworkProtectionVPNNotificationsView.swift delete mode 100644 DuckDuckGo/NetworkProtectionVPNNotificationsViewModel.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index b5613b1638..c4bca11254 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -861,9 +861,7 @@ EE8594992A44791C008A6D06 /* NetworkProtectionTunnelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE8594982A44791C008A6D06 /* NetworkProtectionTunnelController.swift */; }; EE8E568A2A56BCE400F11DCA /* NetworkProtection in Frameworks */ = {isa = PBXBuildFile; productRef = EE8E56892A56BCE400F11DCA /* NetworkProtection */; }; EE9D68D12AE00CF300B55EF4 /* NetworkProtectionVPNSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE9D68D02AE00CF300B55EF4 /* NetworkProtectionVPNSettingsView.swift */; }; - EE9D68D52AE1526600B55EF4 /* NetworkProtectionVPNNotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE9D68D42AE1526600B55EF4 /* NetworkProtectionVPNNotificationsView.swift */; }; EE9D68D82AE15AD600B55EF4 /* UIApplicationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE9D68D72AE15AD600B55EF4 /* UIApplicationExtension.swift */; }; - EE9D68DA2AE1659F00B55EF4 /* NetworkProtectionVPNNotificationsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE9D68D92AE1659F00B55EF4 /* NetworkProtectionVPNNotificationsViewModel.swift */; }; EE9D68DC2AE16AE100B55EF4 /* NotificationsAuthorizationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE9D68DB2AE16AE100B55EF4 /* NotificationsAuthorizationController.swift */; }; EE9D68DE2AE2A65600B55EF4 /* UserDefaults+NetworkProtection.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE9D68DD2AE2A65600B55EF4 /* UserDefaults+NetworkProtection.swift */; }; EEC02C142B0519DE0045CE11 /* NetworkProtectionVPNLocationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC02C132B0519DE0045CE11 /* NetworkProtectionVPNLocationViewModel.swift */; }; @@ -2525,9 +2523,7 @@ EE7A92862AC6DE4700832A36 /* NetworkProtectionNotificationIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionNotificationIdentifier.swift; sourceTree = ""; }; EE8594982A44791C008A6D06 /* NetworkProtectionTunnelController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionTunnelController.swift; sourceTree = ""; }; EE9D68D02AE00CF300B55EF4 /* NetworkProtectionVPNSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionVPNSettingsView.swift; sourceTree = ""; }; - EE9D68D42AE1526600B55EF4 /* NetworkProtectionVPNNotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionVPNNotificationsView.swift; sourceTree = ""; }; EE9D68D72AE15AD600B55EF4 /* UIApplicationExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplicationExtension.swift; sourceTree = ""; }; - EE9D68D92AE1659F00B55EF4 /* NetworkProtectionVPNNotificationsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionVPNNotificationsViewModel.swift; sourceTree = ""; }; EE9D68DB2AE16AE100B55EF4 /* NotificationsAuthorizationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsAuthorizationController.swift; sourceTree = ""; }; EE9D68DD2AE2A65600B55EF4 /* UserDefaults+NetworkProtection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+NetworkProtection.swift"; sourceTree = ""; }; EEB8FDB92A990AEE00EBEDCF /* Configuration-Alpha.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Configuration-Alpha.xcconfig"; path = "Configuration/Configuration-Alpha.xcconfig"; sourceTree = ""; }; @@ -4817,21 +4813,11 @@ name = VPNSettings; sourceTree = ""; }; - EE9D68D62AE1527F00B55EF4 /* VPNNotifications */ = { - isa = PBXGroup; - children = ( - EE9D68D42AE1526600B55EF4 /* NetworkProtectionVPNNotificationsView.swift */, - EE9D68D92AE1659F00B55EF4 /* NetworkProtectionVPNNotificationsViewModel.swift */, - ); - name = VPNNotifications; - sourceTree = ""; - }; EECD94B22A28B8580085C66E /* NetworkProtection */ = { isa = PBXGroup; children = ( 4B37E04E2B928C91009E81CA /* Resources */, EE01EB412AFC1DE10096AAC9 /* PreferredLocation */, - EE9D68D62AE1527F00B55EF4 /* VPNNotifications */, EE9D68CF2AE00CE000B55EF4 /* VPNSettings */, EE458D122ABB651500FC651A /* Debug */, EE0153E22A6FE031002A8B26 /* Root */, @@ -6617,7 +6603,6 @@ C1B7B529289420830098FD6A /* RemoteMessaging.xcdatamodeld in Sources */, 986B16C425E92DF0007D23E8 /* BrowsingMenuViewController.swift in Sources */, 988AC355257E47C100793C64 /* RequeryLogic.swift in Sources */, - EE9D68D52AE1526600B55EF4 /* NetworkProtectionVPNNotificationsView.swift in Sources */, 1E4F4A5A297193DE00625985 /* MainViewController+CookiesManaged.swift in Sources */, 8586A10D24CBA7070049720E /* FindInPageActivity.swift in Sources */, 1E1626072968413B0004127F /* ViewExtension.swift in Sources */, @@ -6958,7 +6943,6 @@ F1D796F41E7C2A410019D451 /* BookmarksDelegate.swift in Sources */, D664C7B92B289AA200CBFA76 /* WKUserContentController+Handler.swift in Sources */, C1B7B52428941F2A0098FD6A /* RemoteMessageRequest.swift in Sources */, - EE9D68DA2AE1659F00B55EF4 /* NetworkProtectionVPNNotificationsViewModel.swift in Sources */, 1E8AD1D727C2E24E00ABA377 /* DownloadsListRowViewModel.swift in Sources */, 1E865AF0272042DB001C74F3 /* TextSizeSettingsViewController.swift in Sources */, D6E0C1892B7A2E0D00D5E1E9 /* DesktopDownloadViewModel.swift in Sources */, diff --git a/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift b/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift index 922220f2a8..512852feea 100644 --- a/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift +++ b/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift @@ -76,7 +76,7 @@ extension NetworkProtectionCodeRedemptionCoordinator { } } -extension NetworkProtectionVPNNotificationsViewModel { +extension NetworkProtectionVPNSettingsViewModel { convenience init() { self.init( notificationsAuthorization: NotificationsAuthorizationController(), @@ -85,12 +85,6 @@ extension NetworkProtectionVPNNotificationsViewModel { } } -extension NetworkProtectionVPNSettingsViewModel { - convenience init() { - self.init(settings: VPNSettings(defaults: .networkProtectionGroupDefaults)) - } -} - extension NetworkProtectionLocationListCompositeRepository { convenience init() { let settings = VPNSettings(defaults: .networkProtectionGroupDefaults) diff --git a/DuckDuckGo/NetworkProtectionStatusView.swift b/DuckDuckGo/NetworkProtectionStatusView.swift index 5b434a4326..a27e87a727 100644 --- a/DuckDuckGo/NetworkProtectionStatusView.swift +++ b/DuckDuckGo/NetworkProtectionStatusView.swift @@ -153,9 +153,6 @@ struct NetworkProtectionStatusView: View { NavigationLink(UserText.netPVPNSettingsTitle, destination: NetworkProtectionVPNSettingsView()) .daxBodyRegular() .foregroundColor(.init(designSystemColor: .textPrimary)) - NavigationLink(UserText.netPVPNNotificationsTitle, destination: NetworkProtectionVPNNotificationsView()) - .daxBodyRegular() - .foregroundColor(.init(designSystemColor: .textPrimary)) } header: { Text(UserText.netPStatusViewSettingsSectionTitle).foregroundColor(.init(designSystemColor: .textSecondary)) } footer: { diff --git a/DuckDuckGo/NetworkProtectionVPNNotificationsView.swift b/DuckDuckGo/NetworkProtectionVPNNotificationsView.swift deleted file mode 100644 index e492c73000..0000000000 --- a/DuckDuckGo/NetworkProtectionVPNNotificationsView.swift +++ /dev/null @@ -1,88 +0,0 @@ -// -// NetworkProtectionVPNNotificationsView.swift -// DuckDuckGo -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -#if NETWORK_PROTECTION - -import SwiftUI -import UIKit -import NetworkProtection -import Core - -@available(iOS 15, *) -struct NetworkProtectionVPNNotificationsView: View { - @StateObject var model = NetworkProtectionVPNNotificationsViewModel() - - var body: some View { - List { - switch model.viewKind { - case .loading: - EmptyView() - case .unauthorized: - unauthorizedView - case .authorized: - authorizedView - } - } - .animation(.default, value: model.viewKind) - .applyInsetGroupedListStyle() - .navigationTitle(UserText.netPVPNNotificationsTitle).onAppear { - Task { - await model.onViewAppeared() - } - } - } - - @ViewBuilder - private var unauthorizedView: some View { - Section { - Button(UserText.netPTurnOnNotificationsButtonTitle) { - model.turnOnNotifications() - } - .foregroundColor(.init(designSystemColor: .accent)) - } footer: { - Text(UserText.netPTurnOnNotificationsSectionFooter) - .foregroundColor(.init(designSystemColor: .textSecondary)) - .daxFootnoteRegular() - .padding(.top, 6) - } - .listRowBackground(Color(designSystemColor: .surface)) - } - - @ViewBuilder - private var authorizedView: some View { - Section { - Toggle( - UserText.netPVPNAlertsToggleTitle, - isOn: Binding( - get: { model.alertsEnabled }, - set: model.didToggleAlerts(to:) - ) - ) - .toggleStyle(SwitchToggleStyle(tint: .init(designSystemColor: .accent))) - } footer: { - Text(UserText.netPVPNAlertsToggleSectionFooter) - .foregroundColor(.init(designSystemColor: .textSecondary)) - .daxFootnoteRegular() - .padding(.top, 6) - } - .listRowBackground(Color(designSystemColor: .surface)) - } -} - -#endif diff --git a/DuckDuckGo/NetworkProtectionVPNNotificationsViewModel.swift b/DuckDuckGo/NetworkProtectionVPNNotificationsViewModel.swift deleted file mode 100644 index 096f2194c9..0000000000 --- a/DuckDuckGo/NetworkProtectionVPNNotificationsViewModel.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// NetworkProtectionVPNNotificationsViewModel.swift -// DuckDuckGo -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -#if NETWORK_PROTECTION - -import Combine -import UserNotifications -import NetworkProtection - -enum NetworkProtectionNotificationsViewKind: Equatable { - case loading - case unauthorized - case authorized -} - -final class NetworkProtectionVPNNotificationsViewModel: ObservableObject { - private var notificationsAuthorization: NotificationsAuthorizationControlling - private var settings: VPNSettings - @Published var viewKind: NetworkProtectionNotificationsViewKind = .loading - var alertsEnabled: Bool { - self.settings.notifyStatusChanges - } - - init(notificationsAuthorization: NotificationsAuthorizationControlling, - settings: VPNSettings) { - self.notificationsAuthorization = notificationsAuthorization - self.settings = settings - self.notificationsAuthorization.delegate = self - } - - @MainActor - func onViewAppeared() async { - let status = await notificationsAuthorization.authorizationStatus - updateViewKind(for: status) - } - - func turnOnNotifications() { - notificationsAuthorization.requestAlertAuthorization() - } - - func didToggleAlerts(to enabled: Bool) { - settings.notifyStatusChanges = enabled - } - - private func updateViewKind(for authorizationStatus: UNAuthorizationStatus) { - switch authorizationStatus { - case .notDetermined, .denied: - viewKind = .unauthorized - case .authorized, .ephemeral, .provisional: - viewKind = .authorized - @unknown default: - assertionFailure("Unhandled enum case") - } - } -} - -extension NetworkProtectionVPNNotificationsViewModel: NotificationsPermissionsControllerDelegate { - func authorizationStateDidChange(toStatus status: UNAuthorizationStatus) { - updateViewKind(for: status) - } -} - -#endif diff --git a/DuckDuckGo/NetworkProtectionVPNSettingsView.swift b/DuckDuckGo/NetworkProtectionVPNSettingsView.swift index 439f75b93a..57ec2c4e5b 100644 --- a/DuckDuckGo/NetworkProtectionVPNSettingsView.swift +++ b/DuckDuckGo/NetworkProtectionVPNSettingsView.swift @@ -50,6 +50,13 @@ struct NetworkProtectionVPNSettingsView: View { } } .listRowBackground(Color(designSystemColor: .surface)) + + switch viewModel.viewKind { + case .loading: EmptyView() + case .unauthorized: notificationsUnauthorizedView + case .authorized: notificationAuthorizedView + } + toggleSection( text: UserText.netPExcludeLocalNetworksSettingTitle, footerText: UserText.netPExcludeLocalNetworksSettingFooter @@ -59,6 +66,7 @@ struct NetworkProtectionVPNSettingsView: View { viewModel.toggleExcludeLocalNetworks() } } + Section { HStack(spacing: 16) { Image("Info-Solid-24") @@ -72,7 +80,11 @@ struct NetworkProtectionVPNSettingsView: View { } } .applyInsetGroupedListStyle() - .navigationTitle(UserText.netPVPNSettingsTitle) + .navigationTitle(UserText.netPVPNSettingsTitle).onAppear { + Task { + await viewModel.onViewAppeared() + } + } } @ViewBuilder @@ -98,6 +110,43 @@ struct NetworkProtectionVPNSettingsView: View { } .listRowBackground(Color(designSystemColor: .surface)) } + + @ViewBuilder + private var notificationsUnauthorizedView: some View { + Section { + Button(UserText.netPTurnOnNotificationsButtonTitle) { + viewModel.turnOnNotifications() + } + .foregroundColor(.init(designSystemColor: .accent)) + } footer: { + Text(UserText.netPTurnOnNotificationsSectionFooter) + .foregroundColor(.init(designSystemColor: .textSecondary)) + .daxFootnoteRegular() + .padding(.top, 6) + } + .listRowBackground(Color(designSystemColor: .surface)) + } + + @ViewBuilder + private var notificationAuthorizedView: some View { + Section { + Toggle( + UserText.netPVPNAlertsToggleTitle, + isOn: Binding( + get: { viewModel.alertsEnabled }, + set: viewModel.didToggleAlerts(to:) + ) + ) + .toggleStyle(SwitchToggleStyle(tint: .init(designSystemColor: .accent))) + } footer: { + Text(UserText.netPVPNAlertsToggleSectionFooter) + .foregroundColor(.init(designSystemColor: .textSecondary)) + .daxFootnoteRegular() + .padding(.top, 6) + } + .listRowBackground(Color(designSystemColor: .surface)) + } + } #endif diff --git a/DuckDuckGo/NetworkProtectionVPNSettingsViewModel.swift b/DuckDuckGo/NetworkProtectionVPNSettingsViewModel.swift index b0ac27cf07..0d27f7db64 100644 --- a/DuckDuckGo/NetworkProtectionVPNSettingsViewModel.swift +++ b/DuckDuckGo/NetworkProtectionVPNSettingsViewModel.swift @@ -20,18 +20,33 @@ #if NETWORK_PROTECTION import Foundation +import UserNotifications import NetworkProtection import Combine +enum NetworkProtectionNotificationsViewKind: Equatable { + case loading + case unauthorized + case authorized +} + final class NetworkProtectionVPNSettingsViewModel: ObservableObject { private let settings: VPNSettings private var cancellables: Set = [] + private var notificationsAuthorization: NotificationsAuthorizationControlling + @Published var viewKind: NetworkProtectionNotificationsViewKind = .loading + + var alertsEnabled: Bool { + self.settings.notifyStatusChanges + } + @Published public var preferredLocation: NetworkProtectionLocationSettingsItemModel @Published public var excludeLocalNetworks: Bool = true - init(settings: VPNSettings) { + init(notificationsAuthorization: NotificationsAuthorizationControlling, settings: VPNSettings) { self.settings = settings + self.notificationsAuthorization = notificationsAuthorization self.preferredLocation = NetworkProtectionLocationSettingsItemModel(selectedLocation: settings.selectedLocation) settings.selectedLocationPublisher .receive(on: DispatchQueue.main) @@ -45,6 +60,20 @@ final class NetworkProtectionVPNSettingsViewModel: ObservableObject { .store(in: &cancellables) } + @MainActor + func onViewAppeared() async { + let status = await notificationsAuthorization.authorizationStatus + updateViewKind(for: status) + } + + func turnOnNotifications() { + notificationsAuthorization.requestAlertAuthorization() + } + + func didToggleAlerts(to enabled: Bool) { + settings.notifyStatusChanges = enabled + } + func toggleExcludeLocalNetworks() { settings.excludeLocalNetworks.toggle() } @@ -52,6 +81,17 @@ final class NetworkProtectionVPNSettingsViewModel: ObservableObject { private static func localizedString(forRegionCode: String) -> String { Locale.current.localizedString(forRegionCode: forRegionCode) ?? forRegionCode.capitalized } + + private func updateViewKind(for authorizationStatus: UNAuthorizationStatus) { + switch authorizationStatus { + case .notDetermined, .denied: + viewKind = .unauthorized + case .authorized, .ephemeral, .provisional: + viewKind = .authorized + @unknown default: + assertionFailure("Unhandled enum case") + } + } } struct NetworkProtectionLocationSettingsItemModel { @@ -83,4 +123,10 @@ struct NetworkProtectionLocationSettingsItemModel { } } +extension NetworkProtectionVPNSettingsViewModel: NotificationsPermissionsControllerDelegate { + func authorizationStateDidChange(toStatus status: UNAuthorizationStatus) { + updateViewKind(for: status) + } +} + #endif