From 7af4b7c8423ecfde2630b1f3086ec6ba17b88879 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Mon, 3 Feb 2025 15:35:27 +0100 Subject: [PATCH 01/12] Completed some changes to update the VPN status view submenu --- .../UserText+NetworkProtection.swift | 37 +++++- DuckDuckGo/Localizable.xcstrings | 60 ++++++++++ ...etworkProtectionNavBarPopoverManager.swift | 113 +++++++++++++----- .../BothAppTargets/VPNURLEventHandler.swift | 14 +++ .../Model/VPNPreferencesModel.swift | 22 +--- .../View/WindowControllersManager.swift | 26 ++++ DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift | 84 ++++++++++--- DuckDuckGoVPN/Localizable.xcstrings | 62 +++++++++- DuckDuckGoVPN/UserText.swift | 38 +++++- .../UserText+NetworkProtectionUI.swift | 1 + .../Resources/Localizable.xcstrings | 12 ++ .../NetworkProtectionStatusView.swift | 16 ++- .../NetworkProtectionStatusViewModel.swift | 19 +-- .../VPNAppLauncher/VPNAppLaunchCommand.swift | 6 + 14 files changed, 426 insertions(+), 84 deletions(-) diff --git a/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift b/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift index bdfa7f5298..8df2f7a3cd 100644 --- a/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift +++ b/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift @@ -40,13 +40,48 @@ extension UserText { static let networkProtectionInviteSuccessMessage = NSLocalizedString("network.protection.invite.success.title", value: "DuckDuckGo's VPN secures all of your device's Internet traffic anytime, anywhere.", comment: "Message for the VPN invite success view") - // MARK: - Navigation Bar Status View + // MARK: - VPN Status View submenu (legacy) static let networkProtectionNavBarStatusViewSendFeedback = NSLocalizedString("network.protection.navbar.status.view.send.feedback", value: "Send Feedback…", comment: "Menu item for 'Send Feedback' in the VPN status view that's shown in the navigation bar") static let networkProtectionNavBarStatusViewVPNSettings = NSLocalizedString("network.protection.navbar.status.view.vpn.settings", value: "VPN Settings…", comment: "The status menu 'VPN Settings' menu item") static let networkProtectionNavBarStatusViewFAQ = NSLocalizedString("network.protection.navbar.status.view.faq", value: "FAQs and Support…", comment: "The status menu 'FAQ' menu item") + + // MARK: - VPN Status View submenu + + static let vpnStatusViewVPNSettingsMenuItemTitle = NSLocalizedString( + "vpn.status-view.vpn-settings.menu-item.title", + value: "VPN Settings", + comment: "The VPN status view's 'VPN Settings' menu item for our main app. The number shown is how many Apps are excluded.") + + static func vpnStatusViewExcludedAppsMenuItemTitle(_ count: Int) -> String { + let message = NSLocalizedString( + "vpn.status-view.excluded-apps.menu-item.title", + value: "Excluded Apps (%d)", + comment: "The VPN status view's 'Excluded Apps' menu item for our main app. The number shown is how many Apps are excluded.") + + return String(format: message, count) + } + + static func vpnStatusViewExcludedDomainsMenuItemTitle(_ count: Int) -> String { + let message = NSLocalizedString( + "vpn.status-view.excluded-domains.menu-item.title", + value: "Excluded Websites (%d)", + comment: "The VPN status view's 'Excluded Websites' menu item for our main app. The number shown is how many websites are excluded.") + + return String(format: message, count) + } + + static let vpnStatusViewSendFeedbackMenuItemTitle = NSLocalizedString( + "vpn.status-view.send-feedback.menu-item.title", + value: "Send Feedback", + comment: "The VPN status view's 'Send Feedback' menu item for our main app") + + static let vpnStatusViewFAQMenuItemTitle = NSLocalizedString( + "vpn.status-view.faq.menu-item.title", + value: "FAQs and Support", + comment: "The VPN status view's 'FAQ' menu item for our main app") } extension UserText { diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 746c69b930..6bc11dfdcd 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -72298,6 +72298,66 @@ } } }, + "vpn.status-view.excluded-apps.menu-item.title" : { + "comment" : "The VPN status view's 'Excluded Apps' menu item for our main app. The number shown is how many Apps are excluded.", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Excluded Apps (%d)" + } + } + } + }, + "vpn.status-view.excluded-domains.menu-item.title" : { + "comment" : "The VPN status view's 'Excluded Websites' menu item for our main app. The number shown is how many websites are excluded.", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Excluded Websites (%d)" + } + } + } + }, + "vpn.status-view.faq.menu-item.title" : { + "comment" : "The VPN status view's 'FAQ' menu item for our main app", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "FAQs and Support" + } + } + } + }, + "vpn.status-view.send-feedback.menu-item.title" : { + "comment" : "The VPN status view's 'Send Feedback' menu item for our main app", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Send Feedback" + } + } + } + }, + "vpn.status-view.vpn-settings.menu-item.title" : { + "comment" : "The VPN status view's 'VPN Settings' menu item for our main app. The number shown is how many Apps are excluded.", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "VPN Settings" + } + } + } + }, "vpn.uninstall.alert.informative.text" : { "comment" : "Informative text for the alert that comes up when the user decides to uninstall our VPN", "extractionState" : "extracted_with_value", diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift index 1165d2afce..17611aae38 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift @@ -60,6 +60,7 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { @Published private var siteInfo: ActiveSiteInfo? private let activeSitePublisher: ActiveSiteInfoPublisher + private let featureFlagger = NSApp.delegateTyped.featureFlagger private var cancellables = Set() init(ipcClient: VPNControllerXPCClient, @@ -87,6 +88,79 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { networkProtectionPopover?.isShown ?? false } + @MainActor + func manageExcludedApps() { + WindowControllersManager.shared.showVPNAppExclusions() + } + + @MainActor + func manageExcludedSites() { + WindowControllersManager.shared.showVPNDomainExclusions() + } + + private func statusViewSubmenu() -> [StatusBarMenu.MenuItem] { + let appLauncher = AppLauncher(appBundleURL: Bundle.main.bundleURL) + let proxySettings = TransparentProxySettings(defaults: .netP) + let excludedAppsTitle = UserText.vpnStatusViewExcludedAppsMenuItemTitle(proxySettings.excludedApps.count) + let excludedWebsitesTitle = UserText.vpnStatusViewExcludedDomainsMenuItemTitle(proxySettings.excludedDomains.count) + + var menuItems = [StatusBarMenu.MenuItem]() + + if UserDefaults.netP.networkProtectionOnboardingStatus == .completed { + menuItems.append( + .text(title: UserText.networkProtectionNavBarStatusViewVPNSettings, action: { + try? await appLauncher.launchApp(withCommand: VPNAppLaunchCommand.showSettings) + })) + } + + menuItems.append(contentsOf: [ + .text(title: excludedAppsTitle, action: { [weak self] in + self?.manageExcludedApps() + }), + .text(title: excludedWebsitesTitle, action: { [weak self] in + self?.manageExcludedSites() + }), + .divider(), + .text(title: UserText.vpnStatusViewFAQMenuItemTitle, action: { + try? await appLauncher.launchApp(withCommand: VPNAppLaunchCommand.showFAQ) + }), + .text(title: UserText.vpnStatusViewSendFeedbackMenuItemTitle, action: { + try? await appLauncher.launchApp(withCommand: VPNAppLaunchCommand.shareFeedback) + }) + ]) + + return menuItems + } + + /// Only used if the .networkProtectionAppExclusions feature flag is disabled + /// + private func legacyStatusViewSubmenu() -> [StatusBarMenu.MenuItem] { + let appLauncher = AppLauncher(appBundleURL: Bundle.main.bundleURL) + + if UserDefaults.netP.networkProtectionOnboardingStatus == .completed { + return [ + .text(title: UserText.networkProtectionNavBarStatusViewVPNSettings, action: { + try? await appLauncher.launchApp(withCommand: VPNAppLaunchCommand.showSettings) + }), + .text(title: UserText.networkProtectionNavBarStatusViewFAQ, action: { + try? await appLauncher.launchApp(withCommand: VPNAppLaunchCommand.showFAQ) + }), + .text(title: UserText.networkProtectionNavBarStatusViewSendFeedback, action: { + try? await appLauncher.launchApp(withCommand: VPNAppLaunchCommand.shareFeedback) + }) + ] + } else { + return [ + .text(title: UserText.networkProtectionNavBarStatusViewFAQ, action: { + try? await appLauncher.launchApp(withCommand: VPNAppLaunchCommand.showFAQ) + }), + .text(title: UserText.networkProtectionNavBarStatusViewSendFeedback, action: { + try? await appLauncher.launchApp(withCommand: VPNAppLaunchCommand.shareFeedback) + }) + ] + } + } + func show(positionedBelow view: NSView, withDelegate delegate: NSPopoverDelegate) -> NSPopover { /// Since the favicon doesn't have a publisher we force refreshing here @@ -107,7 +181,6 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { ) let onboardingStatusPublisher = UserDefaults.netP.networkProtectionOnboardingStatusPublisher - let appLauncher = AppLauncher(appBundleURL: Bundle.main.bundleURL) let vpnURLEventHandler = VPNURLEventHandler() let proxySettings = TransparentProxySettings(defaults: .netP) let uiActionHandler = VPNUIActionHandler(vpnURLEventHandler: vpnURLEventHandler, proxySettings: proxySettings) @@ -129,36 +202,15 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { onboardingStatusPublisher: onboardingStatusPublisher, statusReporter: statusReporter, uiActionHandler: uiActionHandler, - menuItems: { - if UserDefaults.netP.networkProtectionOnboardingStatus == .completed { - return [ - NetworkProtectionStatusView.Model.MenuItem( - name: UserText.networkProtectionNavBarStatusViewVPNSettings, action: { - try? await appLauncher.launchApp(withCommand: VPNAppLaunchCommand.showSettings) - }), - NetworkProtectionStatusView.Model.MenuItem( - name: UserText.networkProtectionNavBarStatusViewFAQ, action: { - try? await appLauncher.launchApp(withCommand: VPNAppLaunchCommand.showFAQ) - }), - NetworkProtectionStatusView.Model.MenuItem( - name: UserText.networkProtectionNavBarStatusViewSendFeedback, - action: { - try? await appLauncher.launchApp(withCommand: VPNAppLaunchCommand.shareFeedback) - }) - ] - } else { - return [ - NetworkProtectionStatusView.Model.MenuItem( - name: UserText.networkProtectionNavBarStatusViewFAQ, action: { - try? await appLauncher.launchApp(withCommand: VPNAppLaunchCommand.showFAQ) - }), - NetworkProtectionStatusView.Model.MenuItem( - name: UserText.networkProtectionNavBarStatusViewSendFeedback, - action: { - try? await appLauncher.launchApp(withCommand: VPNAppLaunchCommand.shareFeedback) - }) - ] + menuItems: { [weak self] in + + guard let self else { return [] } + + guard featureFlagger.isFeatureOn(.networkProtectionAppExclusions) else { + return legacyStatusViewSubmenu() } + + return statusViewSubmenu() }, agentLoginItem: LoginItem.vpnMenu, isMenuBarStatusView: false, @@ -168,7 +220,6 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { _ = try? await self?.vpnUninstaller.uninstall(removeSystemExtension: true) }) - let featureFlagger = NSApp.delegateTyped.featureFlagger let tipsFeatureFlagInitialValue = featureFlagger.isFeatureOn(.networkProtectionUserTips) let tipsFeatureFlagPublisher: CurrentValuePublisher diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNURLEventHandler.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNURLEventHandler.swift index 70fff17588..0596d8e6c9 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNURLEventHandler.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNURLEventHandler.swift @@ -33,6 +33,10 @@ final class VPNURLEventHandler { /// func handle(_ url: URL) async { switch url { + case VPNAppLaunchCommand.manageExcludedApps.launchURL: + showVPNAppExclusions() + case VPNAppLaunchCommand.manageExcludedDomains.launchURL: + showVPNDomainExclusions() case VPNAppLaunchCommand.showStatus.launchURL: await showStatus() case VPNAppLaunchCommand.showSettings.launchURL: @@ -86,6 +90,16 @@ final class VPNURLEventHandler { PixelKit.fire(PrivacyProPixel.privacyProOfferScreenImpression) } + func showVPNAppExclusions() { + windowControllerManager.showPreferencesTab(withSelectedPane: .vpn) + windowControllerManager.showVPNAppExclusions() + } + + func showVPNDomainExclusions() { + windowControllerManager.showPreferencesTab(withSelectedPane: .vpn) + windowControllerManager.showVPNDomainExclusions() + } + #if !APPSTORE && !DEBUG func moveAppToApplicationsFolder() { // this should be run after NSApplication.shared is set diff --git a/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift b/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift index 9e74dea8b3..a51fab0d4b 100644 --- a/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift +++ b/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift @@ -275,30 +275,12 @@ final class VPNPreferencesModel: ObservableObject { @MainActor func manageExcludedApps() { - let windowController = ExcludedAppsViewController.create().wrappedInWindowController() - - guard let window = windowController.window, - let parentWindowController = WindowControllersManager.shared.lastKeyMainWindowController - else { - assertionFailure("DataClearingPreferences: Failed to present ExcludedAppsViewController") - return - } - - parentWindowController.window?.beginSheet(window) + WindowControllersManager.shared.showVPNAppExclusions() } @MainActor func manageExcludedSites() { - let windowController = ExcludedDomainsViewController.create().wrappedInWindowController() - - guard let window = windowController.window, - let parentWindowController = WindowControllersManager.shared.lastKeyMainWindowController - else { - assertionFailure("DataClearingPreferences: Failed to present ExcludedDomainsViewController") - return - } - - parentWindowController.window?.beginSheet(window) + WindowControllersManager.shared.showVPNDomainExclusions() } } diff --git a/DuckDuckGo/Windows/View/WindowControllersManager.swift b/DuckDuckGo/Windows/View/WindowControllersManager.swift index 3dd89570de..703435a927 100644 --- a/DuckDuckGo/Windows/View/WindowControllersManager.swift +++ b/DuckDuckGo/Windows/View/WindowControllersManager.swift @@ -342,6 +342,32 @@ extension WindowControllersManager { parentWindowController.window?.beginSheet(locationsFormWindow) } + func showVPNAppExclusions() { + let windowController = ExcludedAppsViewController.create().wrappedInWindowController() + + guard let window = windowController.window, + let parentWindowController = WindowControllersManager.shared.lastKeyMainWindowController + else { + assertionFailure("Failed to present ExcludedAppsViewController") + return + } + + parentWindowController.window?.beginSheet(window) + } + + func showVPNDomainExclusions() { + let windowController = ExcludedDomainsViewController.create().wrappedInWindowController() + + guard let window = windowController.window, + let parentWindowController = WindowControllersManager.shared.lastKeyMainWindowController + else { + assertionFailure("Failed to present ExcludedDomainsViewController") + return + } + + parentWindowController.window?.beginSheet(window) + } + @discardableResult func openNewWindow(with tabCollectionViewModel: TabCollectionViewModel? = nil, burnerMode: BurnerMode = .regular, diff --git a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift index 46028204d7..dd835f117a 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift +++ b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift @@ -17,11 +17,12 @@ // import AppLauncher +import BrowserServicesKit import Cocoa import Combine -import BrowserServicesKit import Common import Configuration +import FeatureFlags import LoginItems import Networking import NetworkExtension @@ -138,7 +139,12 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { private lazy var featureFlagger = DefaultFeatureFlagger( internalUserDecider: privacyConfigurationManager.internalUserDecider, privacyConfigManager: privacyConfigurationManager, - experimentManager: nil) + localOverrides: FeatureFlagLocalOverrides( + keyValueStore: UserDefaults.appConfiguration, + actionHandler: FeatureFlagOverridesPublishingHandler() + ), + experimentManager: nil, + for: FeatureFlag.self) public init(accountManager: AccountManager, accessTokenStorage: SubscriptionTokenKeychainStorage, @@ -314,6 +320,57 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { makeStatusBarMenu() }() + private func statusViewSubmenu() -> [StatusBarMenu.MenuItem] { + let appLauncher = AppLauncher(appBundleURL: Bundle.main.bundleURL) + let proxySettings = TransparentProxySettings(defaults: .netP) + let excludedAppsTitle = UserText.vpnStatusViewExcludedAppsMenuItemTitle(proxySettings.excludedApps.count) + let excludedWebsitesTitle = UserText.vpnStatusViewExcludedDomainsMenuItemTitle(proxySettings.excludedDomains.count) + + var menuItems = [StatusBarMenu.MenuItem]() + + if UserDefaults.netP.networkProtectionOnboardingStatus == .completed { + menuItems.append( + .text(title: UserText.vpnStatusViewVPNSettingsMenuItemTitle, action: { + try? await appLauncher.launchApp(withCommand: VPNAppLaunchCommand.showSettings) + })) + } + + menuItems.append(contentsOf: [ + .text(title: excludedAppsTitle, action: { [weak self] in + try? await self?.appLauncher.launchApp(withCommand: VPNAppLaunchCommand.manageExcludedApps) + }), + .text(title: excludedWebsitesTitle, action: { [weak self] in + try? await self?.appLauncher.launchApp(withCommand: VPNAppLaunchCommand.manageExcludedDomains) + }), + .divider(), + .text(title: UserText.vpnStatusViewFAQMenuItemTitle, action: { [weak self] in + try? await self?.appLauncher.launchApp(withCommand: VPNAppLaunchCommand.showFAQ) + }), + .text(title: UserText.vpnStatusViewSendFeedbackMenuItemTitle, action: { [weak self] in + try? await self?.appLauncher.launchApp(withCommand: VPNAppLaunchCommand.shareFeedback) + }) + ]) + + return menuItems + } + + private func legacyStatusViewSubmenu() -> [StatusBarMenu.MenuItem] { + [ + .text(title: UserText.networkProtectionStatusMenuVPNSettings, action: { [weak self] in + try? await self?.appLauncher.launchApp(withCommand: VPNAppLaunchCommand.showSettings) + }), + .text(title: UserText.networkProtectionStatusMenuFAQ, action: { [weak self] in + try? await self?.appLauncher.launchApp(withCommand: VPNAppLaunchCommand.showFAQ) + }), + .text(title: UserText.networkProtectionStatusMenuSendFeedback, action: { [weak self] in + try? await self?.appLauncher.launchApp(withCommand: VPNAppLaunchCommand.shareFeedback) + }), + .text(title: UserText.networkProtectionStatusMenuOpenDuckDuckGo, action: { [weak self] in + try? await self?.appLauncher.launchApp(withCommand: VPNAppLaunchCommand.justOpen) + }), + ] + } + @MainActor private func makeStatusBarMenu() -> StatusBarMenu { #if DEBUG @@ -340,21 +397,14 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { controller: tunnelController, iconProvider: iconProvider, uiActionHandler: uiActionHandler, - menuItems: { - [ - StatusBarMenu.MenuItem(name: UserText.networkProtectionStatusMenuVPNSettings, action: { [weak self] in - try? await self?.appLauncher.launchApp(withCommand: VPNAppLaunchCommand.showSettings) - }), - StatusBarMenu.MenuItem(name: UserText.networkProtectionStatusMenuFAQ, action: { [weak self] in - try? await self?.appLauncher.launchApp(withCommand: VPNAppLaunchCommand.showFAQ) - }), - StatusBarMenu.MenuItem(name: UserText.networkProtectionStatusMenuSendFeedback, action: { [weak self] in - try? await self?.appLauncher.launchApp(withCommand: VPNAppLaunchCommand.shareFeedback) - }), - StatusBarMenu.MenuItem(name: UserText.networkProtectionStatusMenuOpenDuckDuckGo, action: { [weak self] in - try? await self?.appLauncher.launchApp(withCommand: VPNAppLaunchCommand.justOpen) - }), - ] + menuItems: { [weak self] in + guard let self else { return [] } + + guard featureFlagger.isFeatureOn(.networkProtectionAppExclusions) else { + return legacyStatusViewSubmenu() + } + + return statusViewSubmenu() }, agentLoginItem: nil, isMenuBarStatusView: true, diff --git a/DuckDuckGoVPN/Localizable.xcstrings b/DuckDuckGoVPN/Localizable.xcstrings index 43636be9c4..0370a7ff97 100644 --- a/DuckDuckGoVPN/Localizable.xcstrings +++ b/DuckDuckGoVPN/Localizable.xcstrings @@ -243,7 +243,7 @@ }, "network.protection.status.menu.share.feedback" : { "comment" : "The status menu 'Share VPN Feedback' menu item", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -726,6 +726,66 @@ } } } + }, + "vpn.status-view.excluded-apps.menu-item.title" : { + "comment" : "The VPN status view's 'Excluded Apps' menu item for our status menu app. The number shown is how many Apps are excluded.", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Excluded Apps (%d)" + } + } + } + }, + "vpn.status-view.excluded-domains.menu-item.title" : { + "comment" : "The VPN status view's 'Excluded Websites' menu item for our status menu app. The number shown is how many websites are excluded.", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Excluded Websites (%d)" + } + } + } + }, + "vpn.status-view.faq.menu-item.title" : { + "comment" : "The VPN status view's 'FAQ' menu item for our status menu app", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "FAQs and Support" + } + } + } + }, + "vpn.status-view.send-feedback.menu-item.title" : { + "comment" : "The VPN status view's 'Send Feedback' menu item for our status menu app", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Send Feedback" + } + } + } + }, + "vpn.status-view.vpn-settings.menu-item.title" : { + "comment" : "The VPN status view's 'VPN Settings' menu item for our status menu app. The number shown is how many Apps are excluded.", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "VPN Settings" + } + } + } } }, "version" : "1.0" diff --git a/DuckDuckGoVPN/UserText.swift b/DuckDuckGoVPN/UserText.swift index 0e7d32e691..17d985634a 100644 --- a/DuckDuckGoVPN/UserText.swift +++ b/DuckDuckGoVPN/UserText.swift @@ -19,11 +19,45 @@ import Foundation final class UserText { - // MARK: - Status Menu + // MARK: - VPN Status View submenu (legacy) static let networkProtectionStatusMenuVPNSettings = NSLocalizedString("network.protection.status.menu.vpn.settings", value: "VPN Settings…", comment: "The status menu 'VPN Settings' menu item") static let networkProtectionStatusMenuFAQ = NSLocalizedString("network.protection.status.menu.faq", value: "FAQs and Support…", comment: "The status menu 'FAQ' menu item") static let networkProtectionStatusMenuOpenDuckDuckGo = NSLocalizedString("network.protection.status.menu.vpn.open-duckduckgo", value: "Open DuckDuckGo…", comment: "The status menu 'Open DuckDuckGo' menu item") - static let networkProtectionStatusMenuShareFeedback = NSLocalizedString("network.protection.status.menu.share.feedback", value: "Share VPN Feedback…", comment: "The status menu 'Share VPN Feedback' menu item") static let networkProtectionStatusMenuSendFeedback = NSLocalizedString("network.protection.status.menu.send.feedback", value: "Send Feedback…", comment: "The status menu 'Send Feedback' menu item") + + // MARK: - VPN Status View submenu + + static let vpnStatusViewVPNSettingsMenuItemTitle = NSLocalizedString( + "vpn.status-view.vpn-settings.menu-item.title", + value: "VPN Settings", + comment: "The VPN status view's 'VPN Settings' menu item for our status menu app. The number shown is how many Apps are excluded.") + + static func vpnStatusViewExcludedAppsMenuItemTitle(_ count: Int) -> String { + let message = NSLocalizedString( + "vpn.status-view.excluded-apps.menu-item.title", + value: "Excluded Apps (%d)", + comment: "The VPN status view's 'Excluded Apps' menu item for our status menu app. The number shown is how many Apps are excluded.") + + return String(format: message, count) + } + + static func vpnStatusViewExcludedDomainsMenuItemTitle(_ count: Int) -> String { + let message = NSLocalizedString( + "vpn.status-view.excluded-domains.menu-item.title", + value: "Excluded Websites (%d)", + comment: "The VPN status view's 'Excluded Websites' menu item for our status menu app. The number shown is how many websites are excluded.") + + return String(format: message, count) + } + + static let vpnStatusViewFAQMenuItemTitle = NSLocalizedString( + "vpn.status-view.faq.menu-item.title", + value: "FAQs and Support", + comment: "The VPN status view's 'FAQ' menu item for our status menu app") + + static let vpnStatusViewSendFeedbackMenuItemTitle = NSLocalizedString( + "vpn.status-view.send-feedback.menu-item.title", + value: "Send Feedback", + comment: "The VPN status view's 'Send Feedback' menu item for our status menu app") } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserText+NetworkProtectionUI.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserText+NetworkProtectionUI.swift index c6653d5667..764e9f0b05 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserText+NetworkProtectionUI.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserText+NetworkProtectionUI.swift @@ -34,6 +34,7 @@ final class UserText { static let vpnDnsServer = NSLocalizedString("network.protection.vpn.dns-server", bundle: Bundle.module, value: "DNS Server", comment: "Title for the DNS server section in the VPN status view") static let vpnDataVolume = NSLocalizedString("network.protection.vpn.data-volume", bundle: Bundle.module, value: "Data Volume", comment: "Title for the data volume section in the VPN status view") static let vpnSendFeedback = NSLocalizedString("network.protection.vpn.send-feedback", bundle: Bundle.module, value: "Send Feedback…", comment: "Action button title for the Send feedback option") + static let vpnSendFeedbackMenuItemTitle = NSLocalizedString("vpn.send-feedback.menu.item.title", bundle: Bundle.module, value: "Send Feedback", comment: "The VPN status view's 'Send Feedback' menu item for our status bar app") static let vpnOperationNotPermittedMessage = NSLocalizedString("network.protection.vpn.failure.operation-not-permitted", bundle: Bundle.module, value: "Unable to connect due to an unexpected error. Restarting your Mac can usually fix the issue.", comment: "Error message for the Operation not permitted error") static let vpnLoginItemVersionMismatchedMessage = NSLocalizedString("network.protection.vpn.failure.login-item-version-mismatched", bundle: Bundle.module, value: "Unable to connect due to versioning conflict. If you have multiple versions of the browser installed, remove all but the most recent version of DuckDuckGo and restart your Mac.", comment: "Error message for the Login item version mismatched error") static let vpnRegisteredServerFetchingFailedMessage = NSLocalizedString("network.protection.vpn.failure.registered-server-fetching-failed", bundle: Bundle.module, value: "Unable to connect. Double check your internet connection. Make sure other software or services aren't blocking DuckDuckGo VPN servers.", comment: "Error message for the Failed to fetch registered server error") diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Localizable.xcstrings b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Localizable.xcstrings index 9c2a701b7d..073d3f21b3 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Localizable.xcstrings +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Localizable.xcstrings @@ -3718,6 +3718,18 @@ } } } + }, + "vpn.send-feedback.menu.item.title" : { + "comment" : "The VPN status view's 'Send Feedback' menu item for our status bar app", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Send Feedback" + } + } + } } }, "version" : "1.0" diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift index 22562a36dd..ea892a7b1e 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift @@ -82,11 +82,17 @@ public struct NetworkProtectionStatusView: View { private func bottomMenuView() -> some View { VStack(spacing: 0) { - ForEach(model.menuItems(), id: \.name) { menuItem in - MenuItemButton(title: menuItem.name, textColor: Color(.defaultText)) { - await menuItem.action() - dismiss() - }.applyMenuAttributes() + ForEach(model.menuItems(), id: \.uuid) { item in + switch item { + case .divider: + Divider() + .padding(EdgeInsets(top: 5, leading: 9, bottom: 5, trailing: 9)) + case .text(_, let title, let action): + MenuItemButton(title: title, textColor: Color(.defaultText)) { + await action() + dismiss() + }.applyMenuAttributes() + } } } } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift index 90225f3822..1202ad9cad 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift @@ -17,12 +17,13 @@ // import Combine +import Common +import BrowserServicesKit import LoginItems import NetworkExtension import NetworkProtection import ServiceManagement import SwiftUI -import Common /// This view can be shown from any location where we want the user to be able to interact with VPN. /// This view shows status information about the VPN, and offers a chance to toggle it ON and OFF. @@ -34,13 +35,17 @@ extension NetworkProtectionStatusView { @MainActor public final class Model: ObservableObject { - public struct MenuItem { - let name: String - let action: () async -> Void + public enum MenuItem { + case divider(uuid: UUID = UUID()) + case text(uuid: UUID = UUID(), title: String, action: () async -> Void) - public init(name: String, action: @escaping () async -> Void) { - self.name = name - self.action = action + public var uuid: UUID { + switch self { + case .divider(let uuid): + return uuid + case .text(let uuid, _, _): + return uuid + } } } diff --git a/LocalPackages/NetworkProtectionMac/Sources/VPNAppLauncher/VPNAppLaunchCommand.swift b/LocalPackages/NetworkProtectionMac/Sources/VPNAppLauncher/VPNAppLaunchCommand.swift index 81e01787e6..1a50656848 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/VPNAppLauncher/VPNAppLaunchCommand.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/VPNAppLauncher/VPNAppLaunchCommand.swift @@ -21,6 +21,8 @@ import Foundation public enum VPNAppLaunchCommand: Codable, AppLaunchCommand { case justOpen + case manageExcludedApps + case manageExcludedDomains case shareFeedback case showFAQ case showStatus @@ -33,6 +35,10 @@ public enum VPNAppLaunchCommand: Codable, AppLaunchCommand { switch self { case .justOpen: return "networkprotection://just-open" + case .manageExcludedApps: + return "networkprotection://share-feedback" + case .manageExcludedDomains: + return "networkprotection://share-feedback" case .shareFeedback: return "networkprotection://share-feedback" case .showFAQ: From fa57423ca0df5d28fc3c7ce45ec59aaedbd4dc50 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Mon, 3 Feb 2025 15:51:34 +0100 Subject: [PATCH 02/12] Removes an unused string --- .../Extensions/UserText+NetworkProtectionUI.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserText+NetworkProtectionUI.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserText+NetworkProtectionUI.swift index 764e9f0b05..c6653d5667 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserText+NetworkProtectionUI.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserText+NetworkProtectionUI.swift @@ -34,7 +34,6 @@ final class UserText { static let vpnDnsServer = NSLocalizedString("network.protection.vpn.dns-server", bundle: Bundle.module, value: "DNS Server", comment: "Title for the DNS server section in the VPN status view") static let vpnDataVolume = NSLocalizedString("network.protection.vpn.data-volume", bundle: Bundle.module, value: "Data Volume", comment: "Title for the data volume section in the VPN status view") static let vpnSendFeedback = NSLocalizedString("network.protection.vpn.send-feedback", bundle: Bundle.module, value: "Send Feedback…", comment: "Action button title for the Send feedback option") - static let vpnSendFeedbackMenuItemTitle = NSLocalizedString("vpn.send-feedback.menu.item.title", bundle: Bundle.module, value: "Send Feedback", comment: "The VPN status view's 'Send Feedback' menu item for our status bar app") static let vpnOperationNotPermittedMessage = NSLocalizedString("network.protection.vpn.failure.operation-not-permitted", bundle: Bundle.module, value: "Unable to connect due to an unexpected error. Restarting your Mac can usually fix the issue.", comment: "Error message for the Operation not permitted error") static let vpnLoginItemVersionMismatchedMessage = NSLocalizedString("network.protection.vpn.failure.login-item-version-mismatched", bundle: Bundle.module, value: "Unable to connect due to versioning conflict. If you have multiple versions of the browser installed, remove all but the most recent version of DuckDuckGo and restart your Mac.", comment: "Error message for the Login item version mismatched error") static let vpnRegisteredServerFetchingFailedMessage = NSLocalizedString("network.protection.vpn.failure.registered-server-fetching-failed", bundle: Bundle.module, value: "Unable to connect. Double check your internet connection. Make sure other software or services aren't blocking DuckDuckGo VPN servers.", comment: "Error message for the Failed to fetch registered server error") From b8ff3e79719923f25f08379837eaef97219b8672 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Mon, 3 Feb 2025 17:46:35 +0100 Subject: [PATCH 03/12] Adds icons to the new VPN submenu --- .../Images/Help-16.imageset/Contents.json | 15 +++++++++++++++ .../Images/Help-16.imageset/Help-16.pdf | Bin 0 -> 4118 bytes .../Images/Support-16.imageset/Contents.json | 15 +++++++++++++++ .../Images/Support-16.imageset/Support-16.pdf | Bin 0 -> 11047 bytes .../NetworkProtectionNavBarPopoverManager.swift | 10 +++++----- .../Assets.xcassets/Icons/Contents.json | 6 ++++++ .../Icons/Globe-16.imageset/Contents.json | 15 +++++++++++++++ .../Icons/Globe-16.imageset/Globe-16.pdf | Bin 0 -> 4364 bytes .../Icons/Help-16.imageset/Contents.json | 15 +++++++++++++++ .../Icons/Help-16.imageset/Help-16.pdf | Bin 0 -> 4118 bytes .../Icons/Settings-16.imageset/Contents.json | 15 +++++++++++++++ .../Icons/Settings-16.imageset/Settings-16.pdf | Bin 0 -> 12281 bytes .../Icons/Support-16.imageset/Contents.json | 15 +++++++++++++++ .../Icons/Support-16.imageset/Support-16.pdf | Bin 0 -> 11047 bytes .../Icons/Window-16.imageset/Contents.json | 15 +++++++++++++++ .../Icons/Window-16.imageset/Window-16.pdf | Bin 0 -> 11182 bytes DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift | 15 ++++++++------- .../SwiftUI/MenuItemButton.swift | 10 +++++----- .../NetworkProtectionStatusView.swift | 4 ++-- .../NetworkProtectionStatusViewModel.swift | 4 ++-- 20 files changed, 133 insertions(+), 21 deletions(-) create mode 100644 DuckDuckGo/Assets.xcassets/Images/Help-16.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/Help-16.imageset/Help-16.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/Support-16.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/Support-16.imageset/Support-16.pdf create mode 100644 DuckDuckGoVPN/Assets.xcassets/Icons/Contents.json create mode 100644 DuckDuckGoVPN/Assets.xcassets/Icons/Globe-16.imageset/Contents.json create mode 100644 DuckDuckGoVPN/Assets.xcassets/Icons/Globe-16.imageset/Globe-16.pdf create mode 100644 DuckDuckGoVPN/Assets.xcassets/Icons/Help-16.imageset/Contents.json create mode 100644 DuckDuckGoVPN/Assets.xcassets/Icons/Help-16.imageset/Help-16.pdf create mode 100644 DuckDuckGoVPN/Assets.xcassets/Icons/Settings-16.imageset/Contents.json create mode 100644 DuckDuckGoVPN/Assets.xcassets/Icons/Settings-16.imageset/Settings-16.pdf create mode 100644 DuckDuckGoVPN/Assets.xcassets/Icons/Support-16.imageset/Contents.json create mode 100644 DuckDuckGoVPN/Assets.xcassets/Icons/Support-16.imageset/Support-16.pdf create mode 100644 DuckDuckGoVPN/Assets.xcassets/Icons/Window-16.imageset/Contents.json create mode 100644 DuckDuckGoVPN/Assets.xcassets/Icons/Window-16.imageset/Window-16.pdf diff --git a/DuckDuckGo/Assets.xcassets/Images/Help-16.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Help-16.imageset/Contents.json new file mode 100644 index 0000000000..d4a43f732c --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Help-16.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Help-16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Help-16.imageset/Help-16.pdf b/DuckDuckGo/Assets.xcassets/Images/Help-16.imageset/Help-16.pdf new file mode 100644 index 0000000000000000000000000000000000000000..0fdcbfabd1534feefa01cc49061b4fe0d314348b GIT binary patch literal 4118 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 NSp~45pw=br{{#B88LI#Q literal 0 HcmV?d00001 diff --git a/DuckDuckGo/Assets.xcassets/Images/Support-16.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Support-16.imageset/Contents.json new file mode 100644 index 0000000000..e348d8ee4f --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Support-16.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Support-16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Support-16.imageset/Support-16.pdf b/DuckDuckGo/Assets.xcassets/Images/Support-16.imageset/Support-16.pdf new file mode 100644 index 0000000000000000000000000000000000000000..ed941cca0588e77ae50f35dfe2d1475a062d0a10 GIT binary patch literal 11047 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 literal 0 HcmV?d00001 diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift index 17611aae38..ab924a4444 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift @@ -108,23 +108,23 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { if UserDefaults.netP.networkProtectionOnboardingStatus == .completed { menuItems.append( - .text(title: UserText.networkProtectionNavBarStatusViewVPNSettings, action: { + .text(icon: Image(.settings16), title: UserText.vpnStatusViewVPNSettingsMenuItemTitle, action: { try? await appLauncher.launchApp(withCommand: VPNAppLaunchCommand.showSettings) })) } menuItems.append(contentsOf: [ - .text(title: excludedAppsTitle, action: { [weak self] in + .text(icon: Image(.window16), title: excludedAppsTitle, action: { [weak self] in self?.manageExcludedApps() }), - .text(title: excludedWebsitesTitle, action: { [weak self] in + .text(icon: Image(.globe16), title: excludedWebsitesTitle, action: { [weak self] in self?.manageExcludedSites() }), .divider(), - .text(title: UserText.vpnStatusViewFAQMenuItemTitle, action: { + .text(icon: Image(.help16), title: UserText.vpnStatusViewFAQMenuItemTitle, action: { try? await appLauncher.launchApp(withCommand: VPNAppLaunchCommand.showFAQ) }), - .text(title: UserText.vpnStatusViewSendFeedbackMenuItemTitle, action: { + .text(icon: Image(.support16), title: UserText.vpnStatusViewSendFeedbackMenuItemTitle, action: { try? await appLauncher.launchApp(withCommand: VPNAppLaunchCommand.shareFeedback) }) ]) diff --git a/DuckDuckGoVPN/Assets.xcassets/Icons/Contents.json b/DuckDuckGoVPN/Assets.xcassets/Icons/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/DuckDuckGoVPN/Assets.xcassets/Icons/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGoVPN/Assets.xcassets/Icons/Globe-16.imageset/Contents.json b/DuckDuckGoVPN/Assets.xcassets/Icons/Globe-16.imageset/Contents.json new file mode 100644 index 0000000000..8fafea4f8d --- /dev/null +++ b/DuckDuckGoVPN/Assets.xcassets/Icons/Globe-16.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Globe-16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/DuckDuckGoVPN/Assets.xcassets/Icons/Globe-16.imageset/Globe-16.pdf b/DuckDuckGoVPN/Assets.xcassets/Icons/Globe-16.imageset/Globe-16.pdf new file mode 100644 index 0000000000000000000000000000000000000000..e61c83ff097aeed61d92ce72ad3c0da86a9f4ef8 GIT binary patch literal 4364 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 DNdvHg literal 0 HcmV?d00001 diff --git a/DuckDuckGoVPN/Assets.xcassets/Icons/Help-16.imageset/Contents.json b/DuckDuckGoVPN/Assets.xcassets/Icons/Help-16.imageset/Contents.json new file mode 100644 index 0000000000..d4a43f732c --- /dev/null +++ b/DuckDuckGoVPN/Assets.xcassets/Icons/Help-16.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Help-16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/DuckDuckGoVPN/Assets.xcassets/Icons/Help-16.imageset/Help-16.pdf b/DuckDuckGoVPN/Assets.xcassets/Icons/Help-16.imageset/Help-16.pdf new file mode 100644 index 0000000000000000000000000000000000000000..0fdcbfabd1534feefa01cc49061b4fe0d314348b GIT binary patch literal 4118 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 NSp~45pw=br{{#B88LI#Q literal 0 HcmV?d00001 diff --git a/DuckDuckGoVPN/Assets.xcassets/Icons/Settings-16.imageset/Contents.json b/DuckDuckGoVPN/Assets.xcassets/Icons/Settings-16.imageset/Contents.json new file mode 100644 index 0000000000..d09602778e --- /dev/null +++ b/DuckDuckGoVPN/Assets.xcassets/Icons/Settings-16.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Settings-16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/DuckDuckGoVPN/Assets.xcassets/Icons/Settings-16.imageset/Settings-16.pdf b/DuckDuckGoVPN/Assets.xcassets/Icons/Settings-16.imageset/Settings-16.pdf new file mode 100644 index 0000000000000000000000000000000000000000..cd3cfeb69a6e91f667b9454e238d4fee3deeb6b8 GIT binary patch literal 12281 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 IlvR`a4~s)Y=l}o! literal 0 HcmV?d00001 diff --git a/DuckDuckGoVPN/Assets.xcassets/Icons/Support-16.imageset/Contents.json b/DuckDuckGoVPN/Assets.xcassets/Icons/Support-16.imageset/Contents.json new file mode 100644 index 0000000000..e348d8ee4f --- /dev/null +++ b/DuckDuckGoVPN/Assets.xcassets/Icons/Support-16.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Support-16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/DuckDuckGoVPN/Assets.xcassets/Icons/Support-16.imageset/Support-16.pdf b/DuckDuckGoVPN/Assets.xcassets/Icons/Support-16.imageset/Support-16.pdf new file mode 100644 index 0000000000000000000000000000000000000000..ed941cca0588e77ae50f35dfe2d1475a062d0a10 GIT binary patch literal 11047 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 literal 0 HcmV?d00001 diff --git a/DuckDuckGoVPN/Assets.xcassets/Icons/Window-16.imageset/Contents.json b/DuckDuckGoVPN/Assets.xcassets/Icons/Window-16.imageset/Contents.json new file mode 100644 index 0000000000..b0d5f1693f --- /dev/null +++ b/DuckDuckGoVPN/Assets.xcassets/Icons/Window-16.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Window-16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/DuckDuckGoVPN/Assets.xcassets/Icons/Window-16.imageset/Window-16.pdf b/DuckDuckGoVPN/Assets.xcassets/Icons/Window-16.imageset/Window-16.pdf new file mode 100644 index 0000000000000000000000000000000000000000..25f59a05681ae3d5961b86c67b7b69fdbe3edf43 GIT binary patch literal 11182 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 literal 0 HcmV?d00001 diff --git a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift index dd835f117a..bd8418a05d 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift +++ b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift @@ -29,11 +29,12 @@ import NetworkExtension import NetworkProtection import NetworkProtectionProxy import NetworkProtectionUI -import ServiceManagement +import os.log import PixelKit +import ServiceManagement import Subscription +import SwiftUICore import VPNAppLauncher -import os.log @objc(Application) final class DuckDuckGoVPNApplication: NSApplication { @@ -330,23 +331,23 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { if UserDefaults.netP.networkProtectionOnboardingStatus == .completed { menuItems.append( - .text(title: UserText.vpnStatusViewVPNSettingsMenuItemTitle, action: { + .text(icon: Image(.settings16), title: UserText.vpnStatusViewVPNSettingsMenuItemTitle, action: { try? await appLauncher.launchApp(withCommand: VPNAppLaunchCommand.showSettings) })) } menuItems.append(contentsOf: [ - .text(title: excludedAppsTitle, action: { [weak self] in + .text(icon: Image(.window16), title: excludedAppsTitle, action: { [weak self] in try? await self?.appLauncher.launchApp(withCommand: VPNAppLaunchCommand.manageExcludedApps) }), - .text(title: excludedWebsitesTitle, action: { [weak self] in + .text(icon: Image(.globe16), title: excludedWebsitesTitle, action: { [weak self] in try? await self?.appLauncher.launchApp(withCommand: VPNAppLaunchCommand.manageExcludedDomains) }), .divider(), - .text(title: UserText.vpnStatusViewFAQMenuItemTitle, action: { [weak self] in + .text(icon: Image(.help16), title: UserText.vpnStatusViewFAQMenuItemTitle, action: { [weak self] in try? await self?.appLauncher.launchApp(withCommand: VPNAppLaunchCommand.showFAQ) }), - .text(title: UserText.vpnStatusViewSendFeedbackMenuItemTitle, action: { [weak self] in + .text(icon: Image(.support16), title: UserText.vpnStatusViewSendFeedbackMenuItemTitle, action: { [weak self] in try? await self?.appLauncher.launchApp(withCommand: VPNAppLaunchCommand.shareFeedback) }) ]) diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/SwiftUI/MenuItemButton.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/SwiftUI/MenuItemButton.swift index e8270c4a9f..939871b195 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/SwiftUI/MenuItemButton.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/SwiftUI/MenuItemButton.swift @@ -21,7 +21,7 @@ import SwiftUI struct MenuItemButton: View { @Environment(\.colorScheme) private var colorScheme - private let iconName: NetworkProtectionAsset? + private let icon: Image? private let title: String private let detailTitle: String? private let textColor: Color @@ -32,8 +32,8 @@ struct MenuItemButton: View { @State private var isHovered = false @State private var animatingTap = false - init(iconName: NetworkProtectionAsset? = nil, title: String, detailTitle: String? = nil, textColor: Color, action: @escaping () async -> Void) { - self.iconName = iconName + init(icon: Image? = nil, title: String, detailTitle: String? = nil, textColor: Color, action: @escaping () async -> Void) { + self.icon = icon self.title = title self.detailTitle = detailTitle self.textColor = textColor @@ -45,8 +45,8 @@ struct MenuItemButton: View { buttonTapped() }) { HStack { - if let iconName { - Image(iconName) + if let icon { + icon .foregroundColor(isHovered ? .white : textColor) } Text(title) diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift index ea892a7b1e..77acac244e 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift @@ -87,8 +87,8 @@ public struct NetworkProtectionStatusView: View { case .divider: Divider() .padding(EdgeInsets(top: 5, leading: 9, bottom: 5, trailing: 9)) - case .text(_, let title, let action): - MenuItemButton(title: title, textColor: Color(.defaultText)) { + case .text(_, let icon, let title, let action): + MenuItemButton(icon: icon, title: title, textColor: Color(.defaultText)) { await action() dismiss() }.applyMenuAttributes() diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift index 1202ad9cad..da501a999c 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift @@ -37,13 +37,13 @@ extension NetworkProtectionStatusView { public enum MenuItem { case divider(uuid: UUID = UUID()) - case text(uuid: UUID = UUID(), title: String, action: () async -> Void) + case text(uuid: UUID = UUID(), icon: Image? = nil, title: String, action: () async -> Void) public var uuid: UUID { switch self { case .divider(let uuid): return uuid - case .text(let uuid, _, _): + case .text(let uuid, _, _, _): return uuid } } From 20d03a4ec9c169606ee50dc063ed5810475a5af6 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Tue, 4 Feb 2025 13:50:23 +0100 Subject: [PATCH 04/12] Expands app exclusions to included embedded binaries --- .../AppInfoRetriever/AppInfoRetriever.swift | 29 +++++++++++++ .../NetworkProtectionMac/Package.swift | 2 + .../Exclusions/AppRoutingRulesManager.swift | 41 +++++++++++++++++++ .../Provider/TransparentProxyProvider.swift | 6 ++- .../Resources/Localizable.xcstrings | 12 ------ 5 files changed, 77 insertions(+), 13 deletions(-) create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Exclusions/AppRoutingRulesManager.swift diff --git a/LocalPackages/AppInfoRetriever/Sources/AppInfoRetriever/AppInfoRetriever.swift b/LocalPackages/AppInfoRetriever/Sources/AppInfoRetriever/AppInfoRetriever.swift index 4124a2fe65..cc0400aef7 100644 --- a/LocalPackages/AppInfoRetriever/Sources/AppInfoRetriever/AppInfoRetriever.swift +++ b/LocalPackages/AppInfoRetriever/Sources/AppInfoRetriever/AppInfoRetriever.swift @@ -86,6 +86,10 @@ public class AppInfoRetriever: AppInfoRetrieveing { return nil } + public func getAppURL(bundleID: String) -> URL? { + NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID) + } + public func getBundleID(appURL: URL) -> String? { let infoPlistURL = appURL.appendingPathComponent("Contents/Info.plist") if let plist = NSDictionary(contentsOf: infoPlistURL), @@ -94,4 +98,29 @@ public class AppInfoRetriever: AppInfoRetrieveing { } return nil } + + // MARK: - Embedded Bundle IDs + + public func findEmbeddedBundleIDs(in bundleURL: URL) -> Set { + var bundleIDs: [String] = [] + let fileManager = FileManager.default + + guard let enumerator = fileManager.enumerator(at: bundleURL, + includingPropertiesForKeys: nil, + options: [.skipsHiddenFiles], + errorHandler: nil) else { + return [] + } + + for case let fileURL as URL in enumerator { + if fileURL.pathExtension == "app" { + let embeddedBundle = Bundle(url: fileURL) + if let bundleID = embeddedBundle?.bundleIdentifier { + bundleIDs.append(bundleID) + } + } + } + + return Set(bundleIDs) + } } diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index af1544bb6f..a7f8a05504 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -35,6 +35,7 @@ let package = Package( dependencies: [ .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "234.0.0"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.3"), + .package(path: "../AppInfoRetriever"), .package(path: "../AppLauncher"), .package(path: "../UDSHelper"), .package(path: "../XPCHelper"), @@ -62,6 +63,7 @@ let package = Package( .target( name: "NetworkProtectionProxy", dependencies: [ + "AppInfoRetriever", .product(name: "NetworkProtection", package: "BrowserServicesKit"), .product(name: "PixelKit", package: "BrowserServicesKit"), ], diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Exclusions/AppRoutingRulesManager.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Exclusions/AppRoutingRulesManager.swift new file mode 100644 index 0000000000..08e411ead1 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Exclusions/AppRoutingRulesManager.swift @@ -0,0 +1,41 @@ +// +// ExclusionsManager.swift +// NetworkProtectionMac +// +// Created by ddg on 2/4/25. +// + +import AppInfoRetriever + +/// Manages App routing rules. +/// +/// This manager expands the routing rules stored in the Proxy settings to include the bundleIDs +/// of all embedded binaries. This is useful because when blocking or excluding an app the user +/// likely expects the rule to extend to all child processes. +/// +final class AppExclusionsManager { + + let appRoutingRules: VPNAppRoutingRules + private let settings: TransparentProxySettings + + init(settings: TransparentProxySettings) { + self.settings = settings + + let appInfoRetriever = AppInfoRetriever() + var expandedRules = settings.appRoutingRules + + for (bundleID, rule) in settings.appRoutingRules { + guard let bundleURL = appInfoRetriever.getAppURL(bundleID: bundleID) else { + continue + } + + let embeddedAppBundleIDs = appInfoRetriever.findEmbeddedBundleIDs(in: bundleURL) + + for childBundleID in embeddedAppBundleIDs { + expandedRules[childBundleID] = rule + } + } + + self.appRoutingRules = expandedRules + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Provider/TransparentProxyProvider.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Provider/TransparentProxyProvider.swift index 077c3b2727..efe9a5cec8 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Provider/TransparentProxyProvider.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Provider/TransparentProxyProvider.swift @@ -16,6 +16,7 @@ // limitations under the License. // +import AppInfoRetriever import Combine import Foundation import NetworkExtension @@ -91,6 +92,7 @@ open class TransparentProxyProvider: NETransparentProxyProvider { @MainActor public var isRunning = false + private let appExclusionsManager: AppExclusionsManager private let logger: Logger private let appMessageHandler: TransparentProxyAppMessageHandler private let eventHandler: TransparentProxyProviderEventHandler @@ -108,6 +110,8 @@ open class TransparentProxyProvider: NETransparentProxyProvider { self.settings = settings self.eventHandler = eventHandler + appExclusionsManager = AppExclusionsManager(settings: settings) + super.init() subscribeToSettings() @@ -445,7 +449,7 @@ open class TransparentProxyProvider: NETransparentProxyProvider { private func path(for flow: NEAppProxyFlow) -> FlowPath { let appIdentifier = flow.metaData.sourceAppSigningIdentifier - switch settings.appRoutingRules[appIdentifier] { + switch appExclusionsManager.appRoutingRules[appIdentifier] { case .none: if let hostname = flow.remoteHostname, isExcludedDomain(hostname) { diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Localizable.xcstrings b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Localizable.xcstrings index 073d3f21b3..9c2a701b7d 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Localizable.xcstrings +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Localizable.xcstrings @@ -3718,18 +3718,6 @@ } } } - }, - "vpn.send-feedback.menu.item.title" : { - "comment" : "The VPN status view's 'Send Feedback' menu item for our status bar app", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Send Feedback" - } - } - } } }, "version" : "1.0" From c74c4d34e9c25d1c58621f14c5dd1fec9bc88bac Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Tue, 4 Feb 2025 14:12:54 +0100 Subject: [PATCH 05/12] Updates translations --- DuckDuckGo/Localizable.xcstrings | 240 ++++++++++++++++++ DuckDuckGoVPN/Localizable.xcstrings | 240 ++++++++++++++++++ .../Resources/Localizable.xcstrings | 12 - 3 files changed, 480 insertions(+), 12 deletions(-) diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 6bc11dfdcd..1878c19a5e 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -72302,11 +72302,59 @@ "comment" : "The VPN status view's 'Excluded Apps' menu item for our main app. The number shown is how many Apps are excluded.", "extractionState" : "extracted_with_value", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ausgeschlossene Apps (%d)" + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "Excluded Apps (%d)" } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aplicaciones excluidas (%d)" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Applications exclues (%d)" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "App escluse (%d)" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uitgesloten apps (%d)" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wykluczone aplikacje (%d)" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aplicações excluídas (%d)" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Исключенные приложения (%d)" + } } } }, @@ -72314,11 +72362,59 @@ "comment" : "The VPN status view's 'Excluded Websites' menu item for our main app. The number shown is how many websites are excluded.", "extractionState" : "extracted_with_value", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ausgeschlossene Websites (%d)" + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "Excluded Websites (%d)" } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sitios web excluidos (%d)" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sites Web exclus (%d)" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Siti esclusi (%d)" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uitgesloten websites (%d)" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wykluczone witryny internetowe (%d)" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Websites excluídos (%d)" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Исключенные сайты (%d)" + } } } }, @@ -72326,11 +72422,59 @@ "comment" : "The VPN status view's 'FAQ' menu item for our main app", "extractionState" : "extracted_with_value", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "F&A und Support" + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "FAQs and Support" } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Preguntas frecuentes y asistencia" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "FAQ et assistance" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Domande frequenti e assistenza" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Veelgestelde vragen en ondersteuning" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Często zadawane pytania i pomoc techniczna" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Perguntas Frequentes e Apoio Técnico" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "ЧаВо и поддержка" + } } } }, @@ -72338,11 +72482,59 @@ "comment" : "The VPN status view's 'Send Feedback' menu item for our main app", "extractionState" : "extracted_with_value", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rückmeldung senden" + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "Send Feedback" } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviar comentarios" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Envoyer vos remarques" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Invia feedback" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verstuur feedback" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wyślij opinię" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviar comentário" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отправить отзыв" + } } } }, @@ -72350,11 +72542,59 @@ "comment" : "The VPN status view's 'VPN Settings' menu item for our main app. The number shown is how many Apps are excluded.", "extractionState" : "extracted_with_value", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "VPN-Einstellungen" + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "VPN Settings" } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de VPN" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Paramètres VPN" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Impostazioni VPN" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "VPN-instellingen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ustawienia VPN" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Definições da VPN" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Настройки VPN" + } } } }, diff --git a/DuckDuckGoVPN/Localizable.xcstrings b/DuckDuckGoVPN/Localizable.xcstrings index 0370a7ff97..c8002e3487 100644 --- a/DuckDuckGoVPN/Localizable.xcstrings +++ b/DuckDuckGoVPN/Localizable.xcstrings @@ -731,11 +731,59 @@ "comment" : "The VPN status view's 'Excluded Apps' menu item for our status menu app. The number shown is how many Apps are excluded.", "extractionState" : "extracted_with_value", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ausgeschlossene Apps (%d)" + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "Excluded Apps (%d)" } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aplicaciones excluidas (%d)" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Applications exclues (%d)" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "App escluse (%d)" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uitgesloten apps (%d)" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wykluczone aplikacje (%d)" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aplicações excluídas (%d)" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Исключенные приложения (%d)" + } } } }, @@ -743,11 +791,59 @@ "comment" : "The VPN status view's 'Excluded Websites' menu item for our status menu app. The number shown is how many websites are excluded.", "extractionState" : "extracted_with_value", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ausgeschlossene Websites (%d)" + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "Excluded Websites (%d)" } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sitios web excluidos (%d)" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sites Web exclus (%d)" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Siti esclusi (%d)" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uitgesloten websites (%d)" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wykluczone witryny internetowe (%d)" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Websites excluídos (%d)" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Исключенные сайты (%d)" + } } } }, @@ -755,11 +851,59 @@ "comment" : "The VPN status view's 'FAQ' menu item for our status menu app", "extractionState" : "extracted_with_value", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "F&A und Support" + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "FAQs and Support" } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Preguntas frecuentes y asistencia" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "FAQ et assistance" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Domande frequenti e assistenza" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Veelgestelde vragen en ondersteuning" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Często zadawane pytania i pomoc techniczna" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Perguntas Frequentes e Apoio Técnico" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "ЧаВо и поддержка" + } } } }, @@ -767,11 +911,59 @@ "comment" : "The VPN status view's 'Send Feedback' menu item for our status menu app", "extractionState" : "extracted_with_value", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rückmeldung senden" + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "Send Feedback" } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviar comentarios" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Envoyer vos remarques" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Invia feedback" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verstuur feedback" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wyślij opinię" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviar comentário" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отправить отзыв" + } } } }, @@ -779,11 +971,59 @@ "comment" : "The VPN status view's 'VPN Settings' menu item for our status menu app. The number shown is how many Apps are excluded.", "extractionState" : "extracted_with_value", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "VPN-Einstellungen" + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "VPN Settings" } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuración de VPN" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Paramètres VPN" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Impostazioni VPN" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "VPN-instellingen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ustawienia VPN" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Definições da VPN" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Настройки VPN" + } } } } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Localizable.xcstrings b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Localizable.xcstrings index 073d3f21b3..9c2a701b7d 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Localizable.xcstrings +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Localizable.xcstrings @@ -3718,18 +3718,6 @@ } } } - }, - "vpn.send-feedback.menu.item.title" : { - "comment" : "The VPN status view's 'Send Feedback' menu item for our status bar app", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Send Feedback" - } - } - } } }, "version" : "1.0" From 6708eb5681a850e9ffb943d2023054a58a484821 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Tue, 4 Feb 2025 14:17:13 +0100 Subject: [PATCH 06/12] Fixes a small bug --- .../Sources/VPNAppLauncher/VPNAppLaunchCommand.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LocalPackages/NetworkProtectionMac/Sources/VPNAppLauncher/VPNAppLaunchCommand.swift b/LocalPackages/NetworkProtectionMac/Sources/VPNAppLauncher/VPNAppLaunchCommand.swift index 1a50656848..937d5a8e0c 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/VPNAppLauncher/VPNAppLaunchCommand.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/VPNAppLauncher/VPNAppLaunchCommand.swift @@ -36,9 +36,9 @@ public enum VPNAppLaunchCommand: Codable, AppLaunchCommand { case .justOpen: return "networkprotection://just-open" case .manageExcludedApps: - return "networkprotection://share-feedback" + return "networkprotection://excluded-apps" case .manageExcludedDomains: - return "networkprotection://share-feedback" + return "networkprotection://excluded-domains" case .shareFeedback: return "networkprotection://share-feedback" case .showFAQ: From 119fe366624f72db54b31266c87b2df884e7348c Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Tue, 4 Feb 2025 14:40:29 +0100 Subject: [PATCH 07/12] Exclusions now encompass embedded apps --- .../Exclusions/AppRoutingRulesManager.swift | 46 +++++++++++++++---- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Exclusions/AppRoutingRulesManager.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Exclusions/AppRoutingRulesManager.swift index 08e411ead1..e90854a71a 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Exclusions/AppRoutingRulesManager.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Exclusions/AppRoutingRulesManager.swift @@ -1,11 +1,24 @@ // -// ExclusionsManager.swift -// NetworkProtectionMac +// AppExclusionsManager.swift // -// Created by ddg on 2/4/25. +// Copyright © 2024 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 AppInfoRetriever +import Foundation +import Combine /// Manages App routing rules. /// @@ -15,16 +28,21 @@ import AppInfoRetriever /// final class AppExclusionsManager { - let appRoutingRules: VPNAppRoutingRules - private let settings: TransparentProxySettings + private(set) var appRoutingRules: VPNAppRoutingRules + private var cancellables = Set() init(settings: TransparentProxySettings) { - self.settings = settings + self.appRoutingRules = Self.expandAppRoutingRules(settings.appRoutingRules) + + subscribeToAppRoutingRulesChanges(settings) + } + + static func expandAppRoutingRules(_ rules: VPNAppRoutingRules) -> VPNAppRoutingRules { let appInfoRetriever = AppInfoRetriever() - var expandedRules = settings.appRoutingRules + var expandedRules = rules - for (bundleID, rule) in settings.appRoutingRules { + for (bundleID, rule) in rules { guard let bundleURL = appInfoRetriever.getAppURL(bundleID: bundleID) else { continue } @@ -36,6 +54,16 @@ final class AppExclusionsManager { } } - self.appRoutingRules = expandedRules + return expandedRules + } + + private func subscribeToAppRoutingRulesChanges(_ settings: TransparentProxySettings) { + settings.appRoutingRulesPublisher + .receive(on: DispatchQueue.main) + .map { rules in + return Self.expandAppRoutingRules(rules) + } + .assign(to: \.appRoutingRules, onWeaklyHeld: self) + .store(in: &cancellables) } } From 726de5288a6303db23b9194ca836f2b757a871d5 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Tue, 4 Feb 2025 15:01:17 +0100 Subject: [PATCH 08/12] Removed an unnecessary import that was causing trouble in automated tests --- .../Views/StatusView/NetworkProtectionStatusViewModel.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift index da501a999c..f579612487 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift @@ -18,7 +18,6 @@ import Combine import Common -import BrowserServicesKit import LoginItems import NetworkExtension import NetworkProtection From 2e4924ebf847355da9ff7d8188eb7d04c2ab380b Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Tue, 4 Feb 2025 15:33:43 +0100 Subject: [PATCH 09/12] Removes an unnecessary import --- .../Views/StatusView/NetworkProtectionStatusViewModel.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift index da501a999c..f579612487 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift @@ -18,7 +18,6 @@ import Combine import Common -import BrowserServicesKit import LoginItems import NetworkExtension import NetworkProtection From caf2181e749afe55ed6a8fbc6f12c49dd4f5e3cb Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Thu, 6 Feb 2025 10:05:20 +0100 Subject: [PATCH 10/12] Cleans up the code a bit --- .../Exclusions/AppRoutingRulesManager.swift | 12 ++++++------ .../Provider/TransparentProxyProvider.swift | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Exclusions/AppRoutingRulesManager.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Exclusions/AppRoutingRulesManager.swift index e90854a71a..b462d75206 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Exclusions/AppRoutingRulesManager.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Exclusions/AppRoutingRulesManager.swift @@ -1,7 +1,7 @@ // -// AppExclusionsManager.swift +// AppRoutingRulesManager.swift // -// Copyright © 2024 DuckDuckGo. All rights reserved. +// Copyright © 2025 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. @@ -26,13 +26,13 @@ import Combine /// of all embedded binaries. This is useful because when blocking or excluding an app the user /// likely expects the rule to extend to all child processes. /// -final class AppExclusionsManager { +final class AppRoutingRulesManager { - private(set) var appRoutingRules: VPNAppRoutingRules + private(set) var rules: VPNAppRoutingRules private var cancellables = Set() init(settings: TransparentProxySettings) { - self.appRoutingRules = Self.expandAppRoutingRules(settings.appRoutingRules) + self.rules = Self.expandAppRoutingRules(settings.appRoutingRules) subscribeToAppRoutingRulesChanges(settings) } @@ -63,7 +63,7 @@ final class AppExclusionsManager { .map { rules in return Self.expandAppRoutingRules(rules) } - .assign(to: \.appRoutingRules, onWeaklyHeld: self) + .assign(to: \.rules, onWeaklyHeld: self) .store(in: &cancellables) } } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Provider/TransparentProxyProvider.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Provider/TransparentProxyProvider.swift index efe9a5cec8..92d1ceada2 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Provider/TransparentProxyProvider.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Provider/TransparentProxyProvider.swift @@ -92,7 +92,7 @@ open class TransparentProxyProvider: NETransparentProxyProvider { @MainActor public var isRunning = false - private let appExclusionsManager: AppExclusionsManager + private let appRoutingRulesManager: AppRoutingRulesManager private let logger: Logger private let appMessageHandler: TransparentProxyAppMessageHandler private let eventHandler: TransparentProxyProviderEventHandler @@ -110,7 +110,7 @@ open class TransparentProxyProvider: NETransparentProxyProvider { self.settings = settings self.eventHandler = eventHandler - appExclusionsManager = AppExclusionsManager(settings: settings) + appRoutingRulesManager = AppRoutingRulesManager(settings: settings) super.init() @@ -449,7 +449,7 @@ open class TransparentProxyProvider: NETransparentProxyProvider { private func path(for flow: NEAppProxyFlow) -> FlowPath { let appIdentifier = flow.metaData.sourceAppSigningIdentifier - switch appExclusionsManager.appRoutingRules[appIdentifier] { + switch appRoutingRulesManager.rules[appIdentifier] { case .none: if let hostname = flow.remoteHostname, isExcludedDomain(hostname) { From 9922c5087f336264196d7687a28e130fe3549b31 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Thu, 6 Feb 2025 17:51:59 +0100 Subject: [PATCH 11/12] Changes a line --- .../Sources/AppInfoRetriever/AppInfoRetriever.swift | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/LocalPackages/AppInfoRetriever/Sources/AppInfoRetriever/AppInfoRetriever.swift b/LocalPackages/AppInfoRetriever/Sources/AppInfoRetriever/AppInfoRetriever.swift index cc0400aef7..e221960967 100644 --- a/LocalPackages/AppInfoRetriever/Sources/AppInfoRetriever/AppInfoRetriever.swift +++ b/LocalPackages/AppInfoRetriever/Sources/AppInfoRetriever/AppInfoRetriever.swift @@ -112,12 +112,10 @@ public class AppInfoRetriever: AppInfoRetrieveing { return [] } - for case let fileURL as URL in enumerator { - if fileURL.pathExtension == "app" { - let embeddedBundle = Bundle(url: fileURL) - if let bundleID = embeddedBundle?.bundleIdentifier { - bundleIDs.append(bundleID) - } + for case let fileURL as URL in enumerator where fileURL.pathExtension == "app" { + let embeddedBundle = Bundle(url: fileURL) + if let bundleID = embeddedBundle?.bundleIdentifier { + bundleIDs.append(bundleID) } } From a6d0ad1e032b66d2d517a56b8cf08d33eb6d7a44 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Thu, 6 Feb 2025 18:05:20 +0100 Subject: [PATCH 12/12] Adds documentation and improves some of the code by making proper use of injection --- .../ExcludedApps/ExcludedAppsModel.swift | 2 +- .../AppInfoRetriever/AppInfoRetriever.swift | 85 ++++++++++++++++++- .../Exclusions/AppRoutingRulesManager.swift | 16 ++-- 3 files changed, 92 insertions(+), 11 deletions(-) diff --git a/DuckDuckGo/NetworkProtection/ExcludedApps/ExcludedAppsModel.swift b/DuckDuckGo/NetworkProtection/ExcludedApps/ExcludedAppsModel.swift index b487386cde..9f10f89c2c 100644 --- a/DuckDuckGo/NetworkProtection/ExcludedApps/ExcludedAppsModel.swift +++ b/DuckDuckGo/NetworkProtection/ExcludedApps/ExcludedAppsModel.swift @@ -31,7 +31,7 @@ protocol ExcludedAppsModel { } final class DefaultExcludedAppsModel { - private let appInfoRetriever: AppInfoRetrieveing = AppInfoRetriever() + private let appInfoRetriever: AppInfoRetrieving = AppInfoRetriever() let proxySettings = TransparentProxySettings(defaults: .netP) private let pixelKit: PixelFiring? diff --git a/LocalPackages/AppInfoRetriever/Sources/AppInfoRetriever/AppInfoRetriever.swift b/LocalPackages/AppInfoRetriever/Sources/AppInfoRetriever/AppInfoRetriever.swift index e221960967..c356cc86af 100644 --- a/LocalPackages/AppInfoRetriever/Sources/AppInfoRetriever/AppInfoRetriever.swift +++ b/LocalPackages/AppInfoRetriever/Sources/AppInfoRetriever/AppInfoRetriever.swift @@ -19,24 +19,71 @@ import AppKit import Foundation -public protocol AppInfoRetrieveing { +/// Protocol to provide a mechanism to query information about installed Applications. +/// +public protocol AppInfoRetrieving { - /// Provides a structure featuring commonly-used app info. + /// Provides a structure featuring commonly-used app info given the Application's bundleID. /// - /// It's also possible to retrieve the individual information directly by calling other methods in this class. + /// - Parameters: + /// - bundleID: the bundleID of the target Application. /// func getAppInfo(bundleID: String) -> AppInfo? + + /// Provides a structure featuring commonly-used app info, given the Application's URL. + /// + /// - Parameters: + /// - appURL: the URL where the target Application is installed. + /// func getAppInfo(appURL: URL) -> AppInfo? + + /// Obtains the icon for a specified application. + /// + /// - Parameters: + /// - bundleID: the bundleID of the target Application. + /// func getAppIcon(bundleID: String) -> NSImage? + + /// Obtains the URL for a specified application. + /// + /// - Parameters: + /// - bundleID: the bundleID of the target Application. + /// + func getAppURL(bundleID: String) -> URL? + + /// Obtains the visible name for a specified application. + /// + /// - Parameters: + /// - bundleID: the bundleID of the target Application. + /// func getAppName(bundleID: String) -> String? + + /// Obtains the bundleID for a specified application. + /// + /// - Parameters: + /// - appURL: the URL where the target Application is installed. + /// func getBundleID(appURL: URL) -> String? + /// Obtains the bundleIDs for all Applications embedded within a speciried application. + /// + /// - Parameters: + /// - bundleURL: the URL where the parent Application is installed. + /// + func findEmbeddedBundleIDs(in bundleURL: URL) -> Set } -public class AppInfoRetriever: AppInfoRetrieveing { +/// Provides a mechanism to query information about installed Applications. +/// +public class AppInfoRetriever: AppInfoRetrieving { public init() {} + /// Provides a structure featuring commonly-used app info given the Application's bundleID. + /// + /// - Parameters: + /// - bundleID: the bundleID of the target Application. + /// public func getAppInfo(bundleID: String) -> AppInfo? { guard let appName = getAppName(bundleID: bundleID) else { return nil @@ -46,6 +93,11 @@ public class AppInfoRetriever: AppInfoRetrieveing { return AppInfo(bundleID: bundleID, name: appName, icon: appIcon) } + /// Provides a structure featuring commonly-used app info, given the Application's URL. + /// + /// - Parameters: + /// - appURL: the URL where the target Application is installed. + /// public func getAppInfo(appURL: URL) -> AppInfo? { guard let bundleID = getBundleID(appURL: appURL) else { return nil @@ -54,6 +106,11 @@ public class AppInfoRetriever: AppInfoRetrieveing { return getAppInfo(bundleID: bundleID) } + /// Obtains the icon for a specified application. + /// + /// - Parameters: + /// - bundleID: the bundleID of the target Application. + /// public func getAppIcon(bundleID: String) -> NSImage? { guard let appURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID) else { return nil @@ -72,6 +129,11 @@ public class AppInfoRetriever: AppInfoRetrieveing { return NSImage(contentsOf: iconURL) } + /// Obtains the visible name for a specified application. + /// + /// - Parameters: + /// - bundleID: the bundleID of the target Application. + /// public func getAppName(bundleID: String) -> String? { if let appURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID) { // Try reading from Info.plist @@ -86,10 +148,20 @@ public class AppInfoRetriever: AppInfoRetrieveing { return nil } + /// Obtains the URL for a specified application. + /// + /// - Parameters: + /// - bundleID: the bundleID of the target Application. + /// public func getAppURL(bundleID: String) -> URL? { NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID) } + /// Obtains the bundleID for a specified application. + /// + /// - Parameters: + /// - appURL: the URL where the target Application is installed. + /// public func getBundleID(appURL: URL) -> String? { let infoPlistURL = appURL.appendingPathComponent("Contents/Info.plist") if let plist = NSDictionary(contentsOf: infoPlistURL), @@ -101,6 +173,11 @@ public class AppInfoRetriever: AppInfoRetrieveing { // MARK: - Embedded Bundle IDs + /// Obtains the bundleIDs for all Applications embedded within a speciried application. + /// + /// - Parameters: + /// - bundleURL: the URL where the parent Application is installed. + /// public func findEmbeddedBundleIDs(in bundleURL: URL) -> Set { var bundleIDs: [String] = [] let fileManager = FileManager.default diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Exclusions/AppRoutingRulesManager.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Exclusions/AppRoutingRulesManager.swift index b462d75206..e850c25da5 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Exclusions/AppRoutingRulesManager.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Exclusions/AppRoutingRulesManager.swift @@ -28,18 +28,22 @@ import Combine /// final class AppRoutingRulesManager { + private let appInfoRetriever: AppInfoRetrieving private(set) var rules: VPNAppRoutingRules private var cancellables = Set() - init(settings: TransparentProxySettings) { - self.rules = Self.expandAppRoutingRules(settings.appRoutingRules) + init(settings: TransparentProxySettings, + appInfoRetriever: AppInfoRetrieving = AppInfoRetriever()) { + + self.appInfoRetriever = appInfoRetriever + self.rules = Self.expandAppRoutingRules(settings.appRoutingRules, appInfoRetriever: appInfoRetriever) subscribeToAppRoutingRulesChanges(settings) } - static func expandAppRoutingRules(_ rules: VPNAppRoutingRules) -> VPNAppRoutingRules { + static func expandAppRoutingRules(_ rules: VPNAppRoutingRules, + appInfoRetriever: AppInfoRetrieving) -> VPNAppRoutingRules { - let appInfoRetriever = AppInfoRetriever() var expandedRules = rules for (bundleID, rule) in rules { @@ -60,8 +64,8 @@ final class AppRoutingRulesManager { private func subscribeToAppRoutingRulesChanges(_ settings: TransparentProxySettings) { settings.appRoutingRulesPublisher .receive(on: DispatchQueue.main) - .map { rules in - return Self.expandAppRoutingRules(rules) + .map { [appInfoRetriever] rules in + return Self.expandAppRoutingRules(rules, appInfoRetriever: appInfoRetriever) } .assign(to: \.rules, onWeaklyHeld: self) .store(in: &cancellables)