From 30dafb00426443551342f76d2cad9cbc4a2b34c1 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Fri, 22 Dec 2023 23:03:36 +0100 Subject: [PATCH] Adds option + click support for our VPN menu to show some useful debug information --- ...etworkProtectionNavBarPopoverManager.swift | 5 +- .../Menu/StatusBarMenu.swift | 11 ++- .../NetworkProtectionPopover.swift | 16 +++- .../DebugInformationView.swift | 90 +++++++++++++++++++ .../DebugInformationViewModel.swift | 43 +++++++++ .../NetworkProtectionStatusView.swift | 4 + .../NetworkProtectionStatusViewModel.swift | 21 +++++ 7 files changed, 185 insertions(+), 5 deletions(-) create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/DebugInformationView/DebugInformationView.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/DebugInformationView/DebugInformationViewModel.swift diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift index 6d811b056a..4727529c02 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift @@ -16,6 +16,7 @@ // limitations under the License. // +import Combine import Foundation import NetworkProtection import NetworkProtectionIPC @@ -61,7 +62,9 @@ final class NetworkProtectionNavBarPopoverManager { let onboardingStatusPublisher = UserDefaults.netP.networkProtectionOnboardingStatusPublisher _ = VPNSettings(defaults: .netP) - let popover = NetworkProtectionPopover(controller: controller, onboardingStatusPublisher: onboardingStatusPublisher, statusReporter: statusReporter) { + let popover = NetworkProtectionPopover(controller: controller, + onboardingStatusPublisher: onboardingStatusPublisher, + statusReporter: statusReporter) { let menuItems = [ NetworkProtectionStatusView.Model.MenuItem( name: UserText.networkProtectionNavBarStatusMenuVPNSettings, action: { diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Menu/StatusBarMenu.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Menu/StatusBarMenu.swift index 7384586cba..e76ad3b34b 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Menu/StatusBarMenu.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Menu/StatusBarMenu.swift @@ -59,7 +59,10 @@ public final class StatusBarMenu: NSObject { self.statusItem = statusItem self.iconPublisher = NetworkProtectionIconPublisher(statusReporter: statusReporter, iconProvider: iconProvider) - popover = NetworkProtectionPopover(controller: controller, onboardingStatusPublisher: onboardingStatusPublisher, statusReporter: statusReporter, menuItems: menuItems) + popover = NetworkProtectionPopover(controller: controller, + onboardingStatusPublisher: onboardingStatusPublisher, + statusReporter: statusReporter, + menuItems: menuItems) popover.behavior = .transient super.init() @@ -74,6 +77,7 @@ public final class StatusBarMenu: NSObject { @objc private func statusBarButtonTapped() { + let isOptionKeyPressed = NSApp.currentEvent?.modifierFlags.contains(.option) ?? false let isRightClick = NSApp.currentEvent?.type == .rightMouseUp guard !isRightClick else { @@ -81,7 +85,7 @@ public final class StatusBarMenu: NSObject { return } - togglePopover() + togglePopover(isOptionKeyPressed: isOptionKeyPressed) } private func subscribeToIconUpdates() { @@ -95,7 +99,7 @@ public final class StatusBarMenu: NSObject { // MARK: - Popover - private func togglePopover() { + private func togglePopover(isOptionKeyPressed: Bool) { if popover.isShown { popover.close() } else { @@ -103,6 +107,7 @@ public final class StatusBarMenu: NSObject { return } + popover.setShowsDebugInformation(isOptionKeyPressed) popover.show(relativeTo: button.bounds, of: button, preferredEdge: .maxY) } } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionPopover.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionPopover.swift index a4668c3484..691baeb348 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionPopover.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionPopover.swift @@ -17,6 +17,7 @@ // import AppKit +import Combine import Foundation import SwiftUI import NetworkProtection @@ -43,6 +44,7 @@ public final class NetworkProtectionPopover: NSPopover { public typealias MenuItem = NetworkProtectionStatusView.Model.MenuItem + private let debugInformationPublisher = CurrentValueSubject(false) private let statusReporter: NetworkProtectionStatusReporter public required init(controller: TunnelController, @@ -57,7 +59,11 @@ public final class NetworkProtectionPopover: NSPopover { self.animates = false self.behavior = .semitransient - setupContentController(controller: controller, onboardingStatusPublisher: onboardingStatusPublisher, statusReporter: statusReporter, menuItems: menuItems) + setupContentController(controller: controller, + onboardingStatusPublisher: onboardingStatusPublisher, + statusReporter: statusReporter, + debugInformationPublisher: debugInformationPublisher.eraseToAnyPublisher(), + menuItems: menuItems) } required init?(coder: NSCoder) { @@ -67,11 +73,13 @@ public final class NetworkProtectionPopover: NSPopover { private func setupContentController(controller: TunnelController, onboardingStatusPublisher: OnboardingStatusPublisher, statusReporter: NetworkProtectionStatusReporter, + debugInformationPublisher: AnyPublisher, menuItems: @escaping () -> [MenuItem]) { let model = NetworkProtectionStatusView.Model(controller: controller, onboardingStatusPublisher: onboardingStatusPublisher, statusReporter: statusReporter, + debugInformationPublisher: debugInformationPublisher, menuItems: menuItems) let view = NetworkProtectionStatusView(model: model).environment(\.dismiss, { [weak self] in @@ -93,4 +101,10 @@ public final class NetworkProtectionPopover: NSPopover { statusReporter.forceRefresh() super.show(relativeTo: positioningRect, of: positioningView, preferredEdge: preferredEdge) } + + // MARK: - Debug Information + + func setShowsDebugInformation(_ showsDebugInformation: Bool) { + debugInformationPublisher.send(showsDebugInformation) + } } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/DebugInformationView/DebugInformationView.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/DebugInformationView/DebugInformationView.swift new file mode 100644 index 0000000000..34ea0bb2b1 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/DebugInformationView/DebugInformationView.swift @@ -0,0 +1,90 @@ +// +// DebugInformationView.swift +// +// 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. +// + +import SwiftUI +import SwiftUIExtensions +import Combine +import NetworkProtection + +public struct DebugInformationView: View { + + @Environment(\.colorScheme) private var colorScheme + @Environment(\.isEnabled) private var isEnabled + @Environment(\.dismiss) private var dismiss + + // MARK: - Model + + /// The view model that this instance will use. + /// + @ObservedObject var model: DebugInformationViewModel + + // MARK: - Initializers + + public init(model: DebugInformationViewModel) { + self.model = model + } + + // MARK: - View Contents + + public var body: some View { + Group { + VStack(alignment: .leading, spacing: 0) { + informationRow(title: "Bundle Path", details: model.bundlePath) + informationRow(title: "Version", details: model.version) + } + + Divider() + .padding(EdgeInsets(top: 5, leading: 9, bottom: 5, trailing: 9)) + } + } + + // MARK: - Composite Views + + private func informationRow(title: String, details: String) -> some View { + VStack(spacing: 2) { + HStack { + Text(title) + .padding(.leading, 24) + .opacity(0.6) + .font(.system(size: 12, weight: .bold, design: .default)) + .fixedSize() + + Spacer() + } + + HStack { + Text(details) + .makeSelectable() + .multilineText() + .padding(.leading, 24) + .opacity(0.6) + .font(.system(size: 12, weight: .regular, design: .default)) + + Spacer() + } + } + .padding(EdgeInsets(top: 4, leading: 10, bottom: 4, trailing: 9)) + } + + // MARK: - Rows + + private func dividerRow() -> some View { + Divider() + .padding(EdgeInsets(top: 5, leading: 9, bottom: 5, trailing: 9)) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/DebugInformationView/DebugInformationViewModel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/DebugInformationView/DebugInformationViewModel.swift new file mode 100644 index 0000000000..f8433a1c28 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/DebugInformationView/DebugInformationViewModel.swift @@ -0,0 +1,43 @@ +// +// DebugInformationViewModel.swift +// +// 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. +// + +import Combine +import Foundation +import NetworkProtection +import SwiftUI + +@MainActor +public final class DebugInformationViewModel: ObservableObject { + + var bundlePath: String + var version: String + + // MARK: - Initialization & Deinitialization + + public init(bundle: Bundle = .main) { + bundlePath = bundle.bundlePath + + // swiftlint:disable:next force_cast + let shortVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String + + // swiftlint:disable:next force_cast + let buildNumber = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as! String + + version = shortVersion + " (build: " + buildNumber + ")" + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift index ab6d1fcd26..9b6789efb0 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift @@ -65,6 +65,10 @@ public struct NetworkProtectionStatusView: View { TunnelControllerView(model: model.tunnelControllerViewModel) .disabled(model.tunnelControllerViewDisabled) + if model.showDebugInformation { + DebugInformationView(model: DebugInformationViewModel()) + } + bottomMenuView() } .padding(5) diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift index b7d1dc3b86..9cf37ffe37 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift @@ -66,6 +66,15 @@ extension NetworkProtectionStatusView { /// private let statusReporter: NetworkProtectionStatusReporter + /// The debug information publisher + /// + private let debugInformationPublisher: AnyPublisher + + /// Whether we're showing debug information + /// + @Published + var showDebugInformation: Bool + // MARK: - Extra Menu Items public let menuItems: () -> [MenuItem] @@ -90,12 +99,14 @@ extension NetworkProtectionStatusView { public init(controller: TunnelController, onboardingStatusPublisher: OnboardingStatusPublisher, statusReporter: NetworkProtectionStatusReporter, + debugInformationPublisher: AnyPublisher, menuItems: @escaping () -> [MenuItem], runLoopMode: RunLoop.Mode? = nil) { self.tunnelController = controller self.onboardingStatusPublisher = onboardingStatusPublisher self.statusReporter = statusReporter + self.debugInformationPublisher = debugInformationPublisher self.menuItems = menuItems self.runLoopMode = runLoopMode @@ -107,12 +118,14 @@ extension NetworkProtectionStatusView { isHavingConnectivityIssues = statusReporter.connectivityIssuesObserver.recentValue lastTunnelErrorMessage = statusReporter.connectionErrorObserver.recentValue lastControllerErrorMessage = statusReporter.controllerErrorMessageObserver.recentValue + showDebugInformation = false // Particularly useful when unit testing with an initial status of our choosing. subscribeToStatusChanges() subscribeToConnectivityIssues() subscribeToTunnelErrorMessages() subscribeToControllerErrorMessages() + subscribeToDebugInformationChanges() onboardingStatusPublisher .receive(on: DispatchQueue.main) @@ -166,6 +179,14 @@ extension NetworkProtectionStatusView { }.store(in: &cancellables) } + private func subscribeToDebugInformationChanges() { + debugInformationPublisher + .removeDuplicates() + .receive(on: DispatchQueue.main) + .assign(to: \.showDebugInformation, onWeaklyHeld: self) + .store(in: &cancellables) + } + // MARK: - Connection Status: Errors @Published