From 78ae7851923e590cfacb0aaabfe0bbde941ad15d Mon Sep 17 00:00:00 2001 From: Anka Date: Tue, 4 Feb 2025 18:09:14 +0000 Subject: [PATCH 01/22] Set marketing version to 1.124.1 --- Configuration/Version.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/Version.xcconfig b/Configuration/Version.xcconfig index d442b28834..1e0c11cef5 100644 --- a/Configuration/Version.xcconfig +++ b/Configuration/Version.xcconfig @@ -1 +1 @@ -MARKETING_VERSION = 1.124.0 +MARKETING_VERSION = 1.124.1 From 92ff7f7043fe3614226080293d759fc75bb866da Mon Sep 17 00:00:00 2001 From: Anka Date: Tue, 4 Feb 2025 18:09:16 +0000 Subject: [PATCH 02/22] Bump version to 1.124.1 (355) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 28aca0de0b..1f0b98eb22 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 353 +CURRENT_PROJECT_VERSION = 355 From 2172b2696b4fc640b7a040ba60a567b9d2ad4fe9 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Tue, 4 Feb 2025 19:24:34 +0100 Subject: [PATCH 03/22] Fix counting blocked trackers and add pixel for reporting NTP exceptions (#3825) Task/Issue URL: https://app.asana.com/0/1201091620985789/1209319157623402 Description: This change adds a pixel for New Tab Page exceptions and also fixes counting blocked trackers in Recent Activity widget on the NTP. --- DuckDuckGo-macOS.xcodeproj/project.pbxproj | 6 ++ .../NewTabPageConfigurationErrorHandler.swift | 37 ++++++++++ .../Features/RecentActivityProvider.swift | 14 ++-- .../NewTabPageActionsManagerExtension.swift | 3 +- DuckDuckGo/Statistics/NewTabPagePixel.swift | 5 ++ .../NewTabPageConfigurationClient.swift | 18 +++-- .../NewTabPageDataModel+Configuration.swift | 4 ++ ...gNewTabPageConfigurationErrorHandler.swift | 43 +++++++++++ .../NewTabPageConfigurationClientTests.swift | 23 +++++- UITests/BookmarksAndFavoritesTests.swift | 8 +-- UITests/UI Tests.xctestplan | 1 + .../RecentActivityProviderTests.swift | 72 +++++++++++++++++++ 12 files changed, 214 insertions(+), 20 deletions(-) create mode 100644 DuckDuckGo/NewTabPage/Features/NewTabPageConfigurationErrorHandler.swift create mode 100644 LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageConfigurationErrorHandler.swift diff --git a/DuckDuckGo-macOS.xcodeproj/project.pbxproj b/DuckDuckGo-macOS.xcodeproj/project.pbxproj index 9aba612c48..a6caa97379 100644 --- a/DuckDuckGo-macOS.xcodeproj/project.pbxproj +++ b/DuckDuckGo-macOS.xcodeproj/project.pbxproj @@ -1099,6 +1099,8 @@ 370C230F2C76A3D600A80A3E /* SettingsGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 370C23042C76A31F00A80A3E /* SettingsGrid.swift */; }; 370C23112C7747E200A80A3E /* ImageProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 370C23102C7747E200A80A3E /* ImageProcessor.swift */; }; 370C23122C7747E200A80A3E /* ImageProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 370C23102C7747E200A80A3E /* ImageProcessor.swift */; }; + 370DB6E22D521D130055D988 /* NewTabPageConfigurationErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 370DB6E12D521D0A0055D988 /* NewTabPageConfigurationErrorHandler.swift */; }; + 370DB6E32D521D130055D988 /* NewTabPageConfigurationErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 370DB6E12D521D0A0055D988 /* NewTabPageConfigurationErrorHandler.swift */; }; 370E70A32D4691560077D4F3 /* RecentActivityProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 370E70A22D46914A0077D4F3 /* RecentActivityProviderTests.swift */; }; 370E70A42D4691560077D4F3 /* RecentActivityProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 370E70A22D46914A0077D4F3 /* RecentActivityProviderTests.swift */; }; 371209212C232E3F003ADF3D /* RemoteMessagingClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371209202C232E3F003ADF3D /* RemoteMessagingClient.swift */; }; @@ -3767,6 +3769,7 @@ 370C23082C76A39900A80A3E /* BackgroundCategoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundCategoryView.swift; sourceTree = ""; }; 370C230A2C76A3BC00A80A3E /* BackgroundThumbnailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundThumbnailView.swift; sourceTree = ""; }; 370C23102C7747E200A80A3E /* ImageProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageProcessor.swift; sourceTree = ""; }; + 370DB6E12D521D0A0055D988 /* NewTabPageConfigurationErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageConfigurationErrorHandler.swift; sourceTree = ""; }; 370E70A22D46914A0077D4F3 /* RecentActivityProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentActivityProviderTests.swift; sourceTree = ""; }; 371209202C232E3F003ADF3D /* RemoteMessagingClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteMessagingClient.swift; sourceTree = ""; }; 371209292C2333A0003ADF3D /* RemoteMessagingDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteMessagingDatabase.swift; sourceTree = ""; }; @@ -6238,6 +6241,7 @@ 379E4D632D439F9A00A901B1 /* Features */ = { isa = PBXGroup; children = ( + 370DB6E12D521D0A0055D988 /* NewTabPageConfigurationErrorHandler.swift */, 376E8C192D41186200D5D2EC /* DefaultRecentActivityActionsHandler.swift */, 37F9AEB02D1316FE007FD19B /* DefaultHomePageSettingsModelNavigator+NewTabPageLinkOpening.swift */, 3758A38B2D108229001CEAA1 /* NewTabPageFreemiumDBPBannerProvider.swift */, @@ -11618,6 +11622,7 @@ F1D042A22BFBB4DD00A31506 /* DataBrokerProtectionSettings+Environment.swift in Sources */, 370270BD2C78B6D3002E44E4 /* NewTabBackgroundPixel.swift in Sources */, BB7BA64C2D11FC170020B47E /* TabBarRemoteMessagePresenting.swift in Sources */, + 370DB6E32D521D130055D988 /* NewTabPageConfigurationErrorHandler.swift in Sources */, 3706FAA3293F65D500E42796 /* CrashReportPromptViewController.swift in Sources */, BD88A83F2C4F3E4300460A26 /* FeedbackCategoryProviding.swift in Sources */, 3706FAA4293F65D500E42796 /* ContextMenuManager.swift in Sources */, @@ -13774,6 +13779,7 @@ 316C48F02CC2B232000B08C1 /* AIChatPreferencesStorage.swift in Sources */, B6830961274CDE99004B46BB /* FireproofDomainsContainer.swift in Sources */, 85B49AFA2CD1B7C5007FAA2A /* SystemInfo.swift in Sources */, + 370DB6E22D521D130055D988 /* NewTabPageConfigurationErrorHandler.swift in Sources */, B687B7CC2947A1E9001DEA6F /* ExternalAppSchemeHandler.swift in Sources */, B65536AE2685E17200085A79 /* GeolocationService.swift in Sources */, 4B02198925E05FAC00ED7DEA /* FireproofingURLExtensions.swift in Sources */, diff --git a/DuckDuckGo/NewTabPage/Features/NewTabPageConfigurationErrorHandler.swift b/DuckDuckGo/NewTabPage/Features/NewTabPageConfigurationErrorHandler.swift new file mode 100644 index 0000000000..9622f89475 --- /dev/null +++ b/DuckDuckGo/NewTabPage/Features/NewTabPageConfigurationErrorHandler.swift @@ -0,0 +1,37 @@ +// +// NewTabPageConfigurationErrorHandler.swift +// +// 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. +// 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 Common +import NewTabPage +import PixelKit + +final class NewTabPageConfigurationErrorHandler: EventMapping { + + init() { + super.init { event, _, _, _ in + switch event { + case .newTabPageError(let message): + PixelKit.fire(DebugEvent(NewTabPagePixel.newTabPageExceptionReported(message: message)), frequency: .dailyAndStandard) + } + } + } + + override init(mapping: @escaping EventMapping.Mapping) { + fatalError("Use init()") + } +} diff --git a/DuckDuckGo/NewTabPage/Features/RecentActivityProvider.swift b/DuckDuckGo/NewTabPage/Features/RecentActivityProvider.swift index ac0b2b04ab..f484a8f66d 100644 --- a/DuckDuckGo/NewTabPage/Features/RecentActivityProvider.swift +++ b/DuckDuckGo/NewTabPage/Features/RecentActivityProvider.swift @@ -106,8 +106,6 @@ final class RecentActivityProvider: NewTabPageRecentActivityProviding { var activityItems = [DomainActivityRef]() var activityItemsByDomain = [String: DomainActivityRef]() - let oneWeekAgo = Date.weekAgo - browsingHistory .filter(\.isValidForRecentActivity) .sorted(by: { $0.lastVisit > $1.lastVisit }) @@ -129,7 +127,7 @@ final class RecentActivityProvider: NewTabPageRecentActivityProviding { return newItemRef }() - activityItem?.activity.addBlockedEntities(historyEntry.blockedTrackingEntities) + activityItem?.activity.addBlockedEntities(from: historyEntry) activityItem?.activity.addPage(fromHistory: historyEntry, dateFormatter: Self.relativeTime) } @@ -192,16 +190,14 @@ extension NewTabPageDataModel.DomainActivity { favicon: favicon, favorite: urlFavoriteStatusProvider.isUrlFavorited(url: rootURL), trackersFound: historyEntry.trackersFound, - trackingStatus: .init( - totalCount: Int64(historyEntry.numberOfTrackersBlocked), - trackerCompanies: historyEntry.blockedTrackingEntities.map(NewTabPageDataModel.TrackingStatus.TrackerCompany.init) - ), + trackingStatus: .init(totalCount: 0, trackerCompanies: []), // keep this empty because it's updated separately history: [] ) } - mutating func addBlockedEntities(_ entities: Set) { - let trackerCompanies = Set(entities.map(NewTabPageDataModel.TrackingStatus.TrackerCompany.init)) + mutating func addBlockedEntities(from entry: HistoryEntry) { + let trackerCompanies = Set(entry.blockedTrackingEntities.filter({ !$0.isEmpty }).map(NewTabPageDataModel.TrackingStatus.TrackerCompany.init)) + trackingStatus.totalCount += Int64(entry.numberOfTrackersBlocked) trackingStatus.trackerCompanies = Array(Set(trackingStatus.trackerCompanies).union(trackerCompanies)) } diff --git a/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift b/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift index 9470584981..40a4c50fd2 100644 --- a/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift +++ b/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift @@ -68,7 +68,8 @@ extension NewTabPageActionsManager { sectionsAvailabilityProvider: NewTabPageModeDecider(), sectionsVisibilityProvider: appearancePreferences, customBackgroundProvider: customizationProvider, - linkOpener: DefaultHomePageSettingsModelNavigator() + linkOpener: DefaultHomePageSettingsModelNavigator(), + eventMapper: NewTabPageConfigurationErrorHandler() ), NewTabPageCustomBackgroundClient(model: customizationProvider), NewTabPageRMFClient(remoteMessageProvider: activeRemoteMessageModel), diff --git a/DuckDuckGo/Statistics/NewTabPagePixel.swift b/DuckDuckGo/Statistics/NewTabPagePixel.swift index f45e212d91..7d443b3657 100644 --- a/DuckDuckGo/Statistics/NewTabPagePixel.swift +++ b/DuckDuckGo/Statistics/NewTabPagePixel.swift @@ -138,6 +138,8 @@ enum NewTabPagePixel: PixelKitEventV2 { */ case privacyStatsDatabaseError + case newTabPageExceptionReported(message: String) + var name: String { switch self { case .newTabPageShown: return "m_mac_newtab_shown" @@ -148,6 +150,7 @@ enum NewTabPagePixel: PixelKitEventV2 { case .blockedTrackingAttemptsShowMore: return "m_mac_new-tab-page_blocked-tracking-attempts_show-more" case .privacyStatsCouldNotLoadDatabase: return "new-tab-page_privacy-stats_could-not-load-database" case .privacyStatsDatabaseError: return "new-tab-page_privacy-stats_database_error" + case .newTabPageExceptionReported: return "new-tab-page_exception-reported" } } @@ -165,6 +168,8 @@ enum NewTabPagePixel: PixelKitEventV2 { parameters["blocked-tracking-attempts"] = String(privacyStats) } return parameters + case .newTabPageExceptionReported(let message): + return [PixelKit.Parameters.assertionMessage: message] case .favoriteSectionHidden, .recentActivitySectionHidden, .blockedTrackingAttemptsSectionHidden, diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/Configuration/NewTabPageConfigurationClient.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/Configuration/NewTabPageConfigurationClient.swift index f4aef15f16..0f9014d3cd 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/Configuration/NewTabPageConfigurationClient.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/Configuration/NewTabPageConfigurationClient.swift @@ -42,6 +42,10 @@ public protocol NewTabPageLinkOpening { func openLink(_ target: NewTabPageDataModel.OpenAction.Target) async } +public enum NewTabPageConfigurationEvent: Equatable { + case newTabPageError(message: String) +} + public final class NewTabPageConfigurationClient: NewTabPageUserScriptClient { private var cancellables = Set() @@ -50,19 +54,22 @@ public final class NewTabPageConfigurationClient: NewTabPageUserScriptClient { private let customBackgroundProvider: NewTabPageCustomBackgroundProviding private let contextMenuPresenter: NewTabPageContextMenuPresenting private let linkOpener: NewTabPageLinkOpening + private let eventMapper: EventMapping? public init( sectionsAvailabilityProvider: NewTabPageSectionsAvailabilityProviding, sectionsVisibilityProvider: NewTabPageSectionsVisibilityProviding, customBackgroundProvider: NewTabPageCustomBackgroundProviding, contextMenuPresenter: NewTabPageContextMenuPresenting = DefaultNewTabPageContextMenuPresenter(), - linkOpener: NewTabPageLinkOpening + linkOpener: NewTabPageLinkOpening, + eventMapper: EventMapping? ) { self.sectionsAvailabilityProvider = sectionsAvailabilityProvider self.sectionsVisibilityProvider = sectionsVisibilityProvider self.customBackgroundProvider = customBackgroundProvider self.contextMenuPresenter = contextMenuPresenter self.linkOpener = linkOpener + self.eventMapper = eventMapper super.init() Publishers.Merge3( @@ -237,10 +244,11 @@ public final class NewTabPageConfigurationClient: NewTabPageUserScriptClient { } private func reportException(params: Any, original: WKScriptMessage) async throws -> Encodable? { - guard let params = params as? [String: String] else { return nil } - let message = params["message"] ?? "" - let id = params["id"] ?? "" - Logger.general.error("New Tab Page error: \("\(id): \(message)", privacy: .public)") + guard let exception: NewTabPageDataModel.Exception = DecodableHelper.decode(from: params) else { + return nil + } + eventMapper?.fire(.newTabPageError(message: exception.message)) + Logger.general.error("New Tab Page error: \("\(exception.message)", privacy: .public)") return nil } } diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/Configuration/NewTabPageDataModel+Configuration.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/Configuration/NewTabPageDataModel+Configuration.swift index 21ead15b96..0468d41793 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/Configuration/NewTabPageDataModel+Configuration.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/Configuration/NewTabPageDataModel+Configuration.swift @@ -43,6 +43,10 @@ extension NewTabPageDataModel { } } + struct Exception: Codable, Equatable { + let message: String + } + struct NewTabPageConfiguration: Encodable { var widgets: [Widget] var widgetConfigs: [WidgetConfig] diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageConfigurationErrorHandler.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageConfigurationErrorHandler.swift new file mode 100644 index 0000000000..84175dd651 --- /dev/null +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageConfigurationErrorHandler.swift @@ -0,0 +1,43 @@ +// +// CapturingNewTabPageConfigurationErrorHandler.swift +// +// 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. +// 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 Common +import NewTabPage + +final class CapturingNewTabPageConfigurationEventHandler: EventMapping { + var events: [NewTabPageConfigurationEvent] = [] + + init() { + let localEvents = PassthroughSubject() + super.init { event, _, _, _ in + localEvents.send(event) + } + + cancellable = localEvents + .sink { [weak self] value in + self?.events.append(value) + } + } + + override init(mapping: @escaping EventMapping.Mapping) { + fatalError("Use init()") + } + + private var cancellable: AnyCancellable? +} diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageConfigurationClientTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageConfigurationClientTests.swift index 503d11a787..53339d92cc 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageConfigurationClientTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageConfigurationClientTests.swift @@ -28,18 +28,21 @@ final class NewTabPageConfigurationClientTests: XCTestCase { private var contextMenuPresenter: CapturingNewTabPageContextMenuPresenter! private var userScript: NewTabPageUserScript! private var messageHelper: MessageHelper! + private var eventMapper: CapturingNewTabPageConfigurationEventHandler! override func setUpWithError() throws { try super.setUpWithError() sectionsAvailabilityProvider = MockNewTabPageSectionsAvailabilityProvider() sectionsVisibilityProvider = MockNewTabPageSectionsVisibilityProvider() contextMenuPresenter = CapturingNewTabPageContextMenuPresenter() + eventMapper = CapturingNewTabPageConfigurationEventHandler() client = NewTabPageConfigurationClient( sectionsAvailabilityProvider: sectionsAvailabilityProvider, sectionsVisibilityProvider: sectionsVisibilityProvider, customBackgroundProvider: CapturingNewTabPageCustomBackgroundProvider(), contextMenuPresenter: contextMenuPresenter, - linkOpener: CapturingNewTabPageLinkOpener() + linkOpener: CapturingNewTabPageLinkOpener(), + eventMapper: eventMapper ) userScript = NewTabPageUserScript() @@ -159,4 +162,22 @@ final class NewTabPageConfigurationClientTests: XCTestCase { XCTAssertEqual(sectionsVisibilityProvider.isPrivacyStatsVisible, false) XCTAssertEqual(sectionsVisibilityProvider.isRecentActivityVisible, true) } + + // MARK: - reportInitException + + func testThatReportInitExceptionForwardsEventToTheMapper() async throws { + let exception = NewTabPageDataModel.Exception(message: "sample message") + try await messageHelper.handleMessageExpectingNilResponse(named: .reportInitException, parameters: exception) + + XCTAssertEqual(eventMapper.events, [.newTabPageError(message: "sample message")]) + } + + // MARK: - reportPageException + + func testThatReportPageExceptionForwardsEventToTheMapper() async throws { + let exception = NewTabPageDataModel.Exception(message: "sample message") + try await messageHelper.handleMessageExpectingNilResponse(named: .reportPageException, parameters: exception) + + XCTAssertEqual(eventMapper.events, [.newTabPageError(message: "sample message")]) + } } diff --git a/UITests/BookmarksAndFavoritesTests.swift b/UITests/BookmarksAndFavoritesTests.swift index a7ad55d948..846a6ac08f 100644 --- a/UITests/BookmarksAndFavoritesTests.swift +++ b/UITests/BookmarksAndFavoritesTests.swift @@ -82,7 +82,7 @@ class BookmarksAndFavoritesTests: UITestCase { contextualMenuRemoveBookmarkFromFavoritesMenuItem = app.menuItems["ContextualMenu.removeBookmarkFromFavoritesMenuItem"] defaultBookmarkDialogButton = app.buttons["BookmarkDialogButtonsView.defaultButton"] defaultBookmarkOtherButton = app.buttons["BookmarkDialogButtonsView.otherButton"] - favoriteGridAddFavoriteButton = app.staticTexts["HomePage.Models.FavoriteModel.addButton"] + favoriteGridAddFavoriteButton = app.buttons["Add Favorite"] favoriteThisPageMenuItem = app.menuItems["MainMenu.favoriteThisPage"] manageBookmarksMenuItem = app.menuItems["MainMenu.manageBookmarksMenuItem"] openBookmarksMenuItem = app.menuItems["MoreOptionsMenu.openBookmarks"] @@ -297,7 +297,7 @@ class BookmarksAndFavoritesTests: UITestCase { let urlForAddFavoriteDialog = try XCTUnwrap(urlForBookmarksBar, "Couldn't unwrap page url") app.typeText("\(pageTitleForAddFavoriteDialog)\t") app.typeURL(urlForAddFavoriteDialog) - let newFavorite = app.otherElements.staticTexts[pageTitleForAddFavoriteDialog] + let newFavorite = app.links[pageTitleForAddFavoriteDialog] XCTAssertTrue( newFavorite.waitForExistence(timeout: UITests.Timeouts.elementExistence), @@ -433,7 +433,7 @@ class BookmarksAndFavoritesTests: UITestCase { toggleBookmarksBarShowFavoritesOn() let unwrappedPageTitle = try XCTUnwrap(pageTitle, "It wasn't possible to unwrap pageTitle") - let firstFavoriteInGridMatchingTitle = app.staticTexts["HomePage.Models.FavoriteModel.\(unwrappedPageTitle)"].firstMatch + let firstFavoriteInGridMatchingTitle = app.staticTexts[unwrappedPageTitle].firstMatch XCTAssertTrue( firstFavoriteInGridMatchingTitle.waitForExistence(timeout: UITests.Timeouts.elementExistence), @@ -545,7 +545,7 @@ class BookmarksAndFavoritesTests: UITestCase { app.typeKey("n", modifierFlags: .command) // New window let unwrappedPageTitle = try XCTUnwrap(pageTitle, "It wasn't possible to unwrap pageTitle") - let firstFavoriteInGridMatchingTitle = app.staticTexts["HomePage.Models.FavoriteModel.\(unwrappedPageTitle)"].firstMatch + let firstFavoriteInGridMatchingTitle = app.links[unwrappedPageTitle].firstMatch XCTAssertTrue( firstFavoriteInGridMatchingTitle.waitForExistence(timeout: UITests.Timeouts.elementExistence), "The favorited item in the grid did not become available in a reasonable timeframe." diff --git a/UITests/UI Tests.xctestplan b/UITests/UI Tests.xctestplan index 56c2819cab..5e5cc8fe3d 100644 --- a/UITests/UI Tests.xctestplan +++ b/UITests/UI Tests.xctestplan @@ -38,6 +38,7 @@ "skippedTests" : [ "BookmarksAndFavoritesTests\/test_bookmarks_canBeAddedTo_byClickingBookmarksButtonInAddressBar()", "BookmarksAndFavoritesTests\/test_favorites_canBeAddedTo_byClickingAddFavoriteInAddBookmarkPopover()", + "BookmarksAndFavoritesTests\/test_favorites_canBeRemovedFromNewTabViaContextClick()", "PermissionsTests" ], "target" : { diff --git a/UnitTests/NewTabPage/RecentActivityProviderTests.swift b/UnitTests/NewTabPage/RecentActivityProviderTests.swift index 5c3644616e..fe77e567d3 100644 --- a/UnitTests/NewTabPage/RecentActivityProviderTests.swift +++ b/UnitTests/NewTabPage/RecentActivityProviderTests.swift @@ -166,6 +166,78 @@ final class RecentActivityProviderTests: XCTestCase { ) } + func testThatHistoryEntryDisplaysSumOfBlockedTrackersForVisitsToAllURLsOfTheSameDomain() throws { + let uuid = UUID() + let url = try XCTUnwrap("https://example.com".url) + let date = Date() + + historyCoordinator.history = [ + .make(identifier: uuid, url: url.appending("index1.html"), lastVisit: date, numberOfTrackersBlocked: 1, blockedTrackingEntities: ["a"]), + .make(identifier: uuid, url: url.appending("index2.html"), lastVisit: date.addingTimeInterval(-1), numberOfTrackersBlocked: 2, blockedTrackingEntities: ["b"]), + .make(identifier: uuid, url: url.appending("index3.html"), lastVisit: date.addingTimeInterval(-2), numberOfTrackersBlocked: 4, blockedTrackingEntities: ["c", "d"]) + ] + + XCTAssertEqual( + provider.refreshActivity(), + [ + .init( + id: uuid.uuidString, + title: "example.com", + url: "https://example.com", + etldPlusOne: "example.com", + favicon: .init(maxAvailableSize: 32, src: try XCTUnwrap(URL.duckFavicon(for: url)?.absoluteString)), + favorite: false, + trackersFound: false, + trackingStatus: .init( + totalCount: 7, + trackerCompanies: [ + .init(displayName: "a"), .init(displayName: "b"), .init(displayName: "c"), .init(displayName: "d") + ] + ), + history: [ + .init(relativeTime: UserText.justNow, title: "/index1.html", url: "https://example.com/index1.html"), + .init(relativeTime: UserText.justNow, title: "/index2.html", url: "https://example.com/index2.html"), + .init(relativeTime: UserText.justNow, title: "/index3.html", url: "https://example.com/index3.html") + ] + ) + ] + ) + } + + func testThatHistoryEntryFiltersOutEmptyTrackerCompanies() throws { + let uuid = UUID() + let url = try XCTUnwrap("https://example.com".url) + let date = Date() + + historyCoordinator.history = [ + .make(identifier: uuid, url: url.appending("index1.html"), lastVisit: date, numberOfTrackersBlocked: 10, blockedTrackingEntities: ["", "a"]) + ] + + XCTAssertEqual( + provider.refreshActivity(), + [ + .init( + id: uuid.uuidString, + title: "example.com", + url: "https://example.com", + etldPlusOne: "example.com", + favicon: .init(maxAvailableSize: 32, src: try XCTUnwrap(URL.duckFavicon(for: url)?.absoluteString)), + favorite: false, + trackersFound: false, + trackingStatus: .init( + totalCount: 10, + trackerCompanies: [ + .init(displayName: "a") + ] + ), + history: [ + .init(relativeTime: UserText.justNow, title: "/index1.html", url: "https://example.com/index1.html"), + ] + ) + ] + ) + } + func testWhenHistoryEntryHasVisitsToTwoDifferentDomainsThenActivityHasTwoEntries() throws { let uuid = UUID() let url1 = try XCTUnwrap("https://example.com".url) From a3247beefa536c5677ef1b9dac7c825b94648886 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Tue, 4 Feb 2025 20:22:36 +0100 Subject: [PATCH 04/22] Hotfixes a VPN issue introduces in 1.124.0 that caused the connection to be lost (#3826) Task/Issue URL: https://app.asana.com/0/1207603085593419/1209318940389812 ## Description Hotfixes a VPN issue introduces in 1.124.0 that caused the connection to be lost --- .../BothAppTargets/NetworkProtectionTunnelController.swift | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift index 8c1f9a6915..2f2a5401e8 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift @@ -382,7 +382,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr public var session: NETunnelProviderSession? { get async { - guard let manager = internalManager, + guard let manager = await manager, let session = manager.connection as? NETunnelProviderSession else { // The active connection is not running, so there's no session, this is acceptable @@ -596,10 +596,6 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr ) } - if await isConnected { - await stop() - } - // Always keep the first error message shown, as it's the more actionable one. if controllerErrorStore.lastErrorMessage == nil { controllerErrorStore.lastErrorMessage = error.localizedDescription From 78d074e6a66a8f3970cb6075a561967cc2d89ab1 Mon Sep 17 00:00:00 2001 From: Anka Date: Tue, 4 Feb 2025 20:40:21 +0000 Subject: [PATCH 05/22] Bump version to 1.125.0 (356) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 1f0b98eb22..e9047b164c 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 355 +CURRENT_PROJECT_VERSION = 356 From 9df11fb529b6ac52b7421ad564e18bf89051f5e9 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Tue, 4 Feb 2025 21:54:23 +0100 Subject: [PATCH 06/22] Update VPN Status View submenu to show exclusions (#3818) Task/Issue URL: https://app.asana.com/0/0/1209150257377771/f ## Description Changes in this PR: - Re-organizes the Status View submenu - Updates titles to remove the "..." suffix - Adds exclusions to the VPN Status View submenu ## Translations Translation Job is [here](https://dashboard.smartling.com/app/projects/f767cb116/account-jobs/f767cb116:kcvr3syqw1nh). --- DuckDuckGo-macOS.xcodeproj/project.pbxproj | 6 + .../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 .../UserText+NetworkProtection.swift | 37 ++- DuckDuckGo/Localizable.xcstrings | 300 +++++++++++++++++ .../MainWindow/MainViewController.swift | 5 +- ...etworkProtectionNavBarPopoverManager.swift | 123 +++++-- .../BothAppTargets/VPNUIPresenting.swift | 60 ++++ .../BothAppTargets/VPNURLEventHandler.swift | 4 + .../Model/VPNPreferencesModel.swift | 22 +- .../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 + .../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 | 89 ++++-- DuckDuckGoVPN/Localizable.xcstrings | 302 +++++++++++++++++- DuckDuckGoVPN/UserText.swift | 38 ++- .../SwiftUI/MenuItemButton.swift | 10 +- .../NetworkProtectionStatusView.swift | 16 +- .../NetworkProtectionStatusViewModel.swift | 18 +- .../VPNAppLauncher/VPNAppLaunchCommand.swift | 6 + 30 files changed, 1051 insertions(+), 96 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 DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNUIPresenting.swift 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-macOS.xcodeproj/project.pbxproj b/DuckDuckGo-macOS.xcodeproj/project.pbxproj index 9aba612c48..207ce93a07 100644 --- a/DuckDuckGo-macOS.xcodeproj/project.pbxproj +++ b/DuckDuckGo-macOS.xcodeproj/project.pbxproj @@ -1907,6 +1907,8 @@ 7B934C412A866DD400FC8F9C /* UserDefaults+NetworkProtectionShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B934C402A866DD400FC8F9C /* UserDefaults+NetworkProtectionShared.swift */; }; 7B93A68B2D4A5AF200E9FFC1 /* ExcludedAppsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93A68A2D4A5AEC00E9FFC1 /* ExcludedAppsModel.swift */; }; 7B93A68C2D4A5AF200E9FFC1 /* ExcludedAppsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93A68A2D4A5AEC00E9FFC1 /* ExcludedAppsModel.swift */; }; + 7B969B3D2D52A81D004AE4E8 /* VPNUIPresenting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B969B3C2D52A818004AE4E8 /* VPNUIPresenting.swift */; }; + 7B969B3E2D52A81D004AE4E8 /* VPNUIPresenting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B969B3C2D52A818004AE4E8 /* VPNUIPresenting.swift */; }; 7B97CD592B7E0B57004FEF43 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7B97CD582B7E0B57004FEF43 /* NetworkProtectionProxy */; }; 7B97CD5B2B7E0B85004FEF43 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 7B97CD5A2B7E0B85004FEF43 /* Common */; }; 7B97CD5C2B7E0BBB004FEF43 /* UserDefaultsWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85C6A29525CC1FFD00EEB5F1 /* UserDefaultsWrapper.swift */; }; @@ -4290,6 +4292,7 @@ 7B934C3D2A866CFF00FC8F9C /* NetworkProtectionOnboardingMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionOnboardingMenu.swift; sourceTree = ""; }; 7B934C402A866DD400FC8F9C /* UserDefaults+NetworkProtectionShared.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UserDefaults+NetworkProtectionShared.swift"; sourceTree = ""; }; 7B93A68A2D4A5AEC00E9FFC1 /* ExcludedAppsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExcludedAppsModel.swift; sourceTree = ""; }; + 7B969B3C2D52A818004AE4E8 /* VPNUIPresenting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNUIPresenting.swift; sourceTree = ""; }; 7BA7CC0B2AD11D1E0042E5CE /* DuckDuckGoVPNAppStore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DuckDuckGoVPNAppStore.xcconfig; sourceTree = ""; }; 7BA7CC0C2AD11D1E0042E5CE /* DuckDuckGoVPN.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DuckDuckGoVPN.xcconfig; sourceTree = ""; }; 7BA7CC0E2AD11DC80042E5CE /* DuckDuckGoVPNAppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DuckDuckGoVPNAppDelegate.swift; sourceTree = ""; }; @@ -6633,6 +6636,7 @@ B6F1B02D2BCE6B47005E863C /* TunnelControllerProvider.swift */, 4BE3A6C02C16BEB1003FC378 /* VPNRedditSessionWorkaround.swift */, 7B60B0002C514541008E32A3 /* VPNUIActionHandler.swift */, + 7B969B3C2D52A818004AE4E8 /* VPNUIPresenting.swift */, 7B60AFFC2C514260008E32A3 /* VPNURLEventHandler.swift */, ); path = BothAppTargets; @@ -12329,6 +12333,7 @@ 317307262CD248DB00C492AB /* AutofillToolbarOnboardingPopover.swift in Sources */, 9F6434622BEC82B700D2D8A0 /* AttributionPixelHandler.swift in Sources */, 843AD3DC2CD389CC00163067 /* XMLNodeExtension.swift in Sources */, + 7B969B3E2D52A81D004AE4E8 /* VPNUIPresenting.swift in Sources */, 3706FC32293F65D500E42796 /* MoreOptionsMenu.swift in Sources */, 3706FC34293F65D500E42796 /* PermissionAuthorizationViewController.swift in Sources */, 3706FC35293F65D500E42796 /* BookmarkNode.swift in Sources */, @@ -13744,6 +13749,7 @@ 37DF370A2CF38CD7005ED34B /* PrivacyStatsDatabase.swift in Sources */, 4B4D60C22A0C849000BCD287 /* EventMapping+NetworkProtectionError.swift in Sources */, 4B9292D02667123700AD2C21 /* BookmarkManagementSplitViewController.swift in Sources */, + 7B969B3D2D52A81D004AE4E8 /* VPNUIPresenting.swift in Sources */, 31F2D1FF2AF026D800BF0144 /* WaitlistTermsAndConditionsActionHandler.swift in Sources */, B6B140882ABDBCC1004F8E85 /* HoverTrackingArea.swift in Sources */, 84F1C8CF2C7705B500716446 /* BookmarksBarMenuPopover.swift in Sources */, 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/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 6de97c1737..5c98bd4375 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -72417,6 +72417,306 @@ } } }, + "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" : { + "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)" + } + } + } + }, + "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" : { + "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)" + } + } + } + }, + "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" : { + "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" : "ЧаВо и поддержка" + } + } + } + }, + "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" : { + "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" : "Отправить отзыв" + } + } + } + }, + "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" : { + "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" + } + } + } + }, "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/MainWindow/MainViewController.swift b/DuckDuckGo/MainWindow/MainViewController.swift index 64057aba22..0ff9c259d5 100644 --- a/DuckDuckGo/MainWindow/MainViewController.swift +++ b/DuckDuckGo/MainWindow/MainViewController.swift @@ -78,7 +78,7 @@ final class MainViewController: NSViewController { tabBarViewController = TabBarViewController.create(tabCollectionViewModel: tabCollectionViewModel, activeRemoteMessageModel: NSApp.delegateTyped.activeRemoteMessageModel) bookmarksBarVisibilityManager = BookmarksBarVisibilityManager(selectedTabPublisher: tabCollectionViewModel.$selectedTabViewModel.eraseToAnyPublisher()) - let networkProtectionPopoverManager: NetPPopoverManager = { + let networkProtectionPopoverManager: NetPPopoverManager = { @MainActor in #if DEBUG guard case .normal = NSApp.runType else { return NetPPopoverManagerMock() @@ -93,7 +93,8 @@ final class MainViewController: NSViewController { return NetworkProtectionNavBarPopoverManager( ipcClient: vpnXPCClient, - vpnUninstaller: vpnUninstaller) + vpnUninstaller: vpnUninstaller, + vpnUIPresenting: WindowControllersManager.shared) }() let networkProtectionStatusReporter: NetworkProtectionStatusReporter = { var connectivityIssuesObserver: ConnectivityIssueObserver! diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift index 1165d2afce..b7f94740ef 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift @@ -56,23 +56,30 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { private var networkProtectionPopover: NetworkProtectionPopover? let ipcClient: NetworkProtectionIPCClient let vpnUninstaller: VPNUninstalling + private let vpnUIPresenting: VPNUIPresenting + private let proxySettings: TransparentProxySettings @Published private var siteInfo: ActiveSiteInfo? private let activeSitePublisher: ActiveSiteInfoPublisher + private let featureFlagger = NSApp.delegateTyped.featureFlagger private var cancellables = Set() init(ipcClient: VPNControllerXPCClient, - vpnUninstaller: VPNUninstalling) { + vpnUninstaller: VPNUninstalling, + vpnUIPresenting: VPNUIPresenting, + proxySettings: TransparentProxySettings = .init(defaults: .netP)) { self.ipcClient = ipcClient self.vpnUninstaller = vpnUninstaller + self.vpnUIPresenting = vpnUIPresenting + self.proxySettings = proxySettings let activeDomainPublisher = ActiveDomainPublisher(windowControllersManager: .shared) activeSitePublisher = ActiveSiteInfoPublisher( activeDomainPublisher: activeDomainPublisher.eraseToAnyPublisher(), - proxySettings: TransparentProxySettings(defaults: .netP)) + proxySettings: proxySettings) subscribeToCurrentSitePublisher() } @@ -87,6 +94,78 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { networkProtectionPopover?.isShown ?? false } + @MainActor + func manageExcludedApps() { + vpnUIPresenting.showVPNAppExclusions() + } + + @MainActor + func manageExcludedSites() { + vpnUIPresenting.showVPNDomainExclusions() + } + + private func statusViewSubmenu() -> [StatusBarMenu.MenuItem] { + let appLauncher = AppLauncher(appBundleURL: Bundle.main.bundleURL) + 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(icon: Image(.settings16), title: UserText.vpnStatusViewVPNSettingsMenuItemTitle, action: { + try? await appLauncher.launchApp(withCommand: VPNAppLaunchCommand.showSettings) + })) + } + + menuItems.append(contentsOf: [ + .text(icon: Image(.window16), title: excludedAppsTitle, action: { [weak self] in + self?.manageExcludedApps() + }), + .text(icon: Image(.globe16), title: excludedWebsitesTitle, action: { [weak self] in + self?.manageExcludedSites() + }), + .divider(), + .text(icon: Image(.help16), title: UserText.vpnStatusViewFAQMenuItemTitle, action: { + try? await appLauncher.launchApp(withCommand: VPNAppLaunchCommand.showFAQ) + }), + .text(icon: Image(.support16), 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,9 +186,7 @@ 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) let connectionStatusPublisher = CurrentValuePublisher( @@ -129,36 +206,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 +224,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/VPNUIPresenting.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNUIPresenting.swift new file mode 100644 index 0000000000..16c3ea2cfa --- /dev/null +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNUIPresenting.swift @@ -0,0 +1,60 @@ +// +// VPNUIPresenting.swift +// +// 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. +// 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. +// + +protocol VPNUIPresenting { + @MainActor + func showVPNAppExclusions() + + @MainActor + func showVPNDomainExclusions() +} + +extension WindowControllersManager: VPNUIPresenting { + + @MainActor + func showVPNAppExclusions() { + showPreferencesTab(withSelectedPane: .vpn) + + 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) + } + + @MainActor + func showVPNDomainExclusions() { + showPreferencesTab(withSelectedPane: .vpn) + + 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) + } +} diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNURLEventHandler.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNURLEventHandler.swift index 70fff17588..064b20bd8f 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: + windowControllerManager.showVPNAppExclusions() + case VPNAppLaunchCommand.manageExcludedDomains.launchURL: + windowControllerManager.showVPNDomainExclusions() case VPNAppLaunchCommand.showStatus.launchURL: await showStatus() case VPNAppLaunchCommand.showSettings.launchURL: 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/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 zcmeHNc|25Y-zSu&MT?ZAPH7{|9A<`Tk$uV1Sdxk~#>iM^EHjxbAxhoSLUs`niQH7O zS5&wuODSs+EmF!7lJcB0I5Sl5^LhVx-{<*b&L8HQ?{!_j?Of-4eqZ0&K-AQh1Lc)a zC=kE^bQ=d00Bqk5pw*lh6gL`)K>^U3lp|C!#Y9IPnu0F0HWi+OKAoUxO$wQAOPQQ8 zpwJF8>;VNFUTG3B0u-TB_|Cd_;OCl zT|VyTl(Zz2k2H7Dl6zK*jr|vEX$o4Z&emGx!mzMiwAi?HwvdpSMs*2}x;g}y!?;#%Ite@`6eJ5LkP#JHshKTJAgz1vgMR#b9WaBgJ)!8K2u z+Wtz&r_3W@fbOv(r!crRx_rU+g$%O?5<_B3_7#s4zZZ+L#^%fC#4Rsvwdm>;PEd^! zlUP};o+rt-o$zy;m`{0;=xPzZ(mm^a506>Ofx70gvU-bw>M&PSLYTh`zH~HOk7^UR zh2mQ0{gWyl{H;|muyFCl!@EXwC5`cslKX7-sYt6wlo>9qR2C=Aq0O38xwZjtQh7BV zCoEpU=bVw@JYE(*?pXQ6Hh%EjEFfW?>VVu0&5MLJ8dbCvs+vY;W2s}*?U$M57P z;4-`Sp2SFi>LS#-`wsVn<^n>eRK*(ka*uB(@(ua@xV&l=Kko8Ete@g#p<4mL?WjC{ zS^z(0{*^=Xiv&&EC8PWS;W@^D%$3sqJ%0Y4`=xx*XRQ<>uD^)dHRqu!Xe?_6$e@-5ElI>D_8X_9IhYBg zFK7rT2nbEuXuPJ*0=vvmN3r&a{mwxYcKM~VF9YQ@Ba?Suj;(8ZzwQ0vLDA7ICy&?N ziz3BgoD2jvt`0jatF72`lh+;5zjMi6Jh*811OFGD|{E$v+_4taIIxeWp>h$iQ3ITVve? zhThpLZ`Siu=UqF)h+%|d)kk&B*TshJ>$1-YEQfIA6N=;^x1ia$-$% z@->edd;@gCZ=NSt^>yK^_Eg}izE%~7ZmN)|@T$m)u-;@X<7@siV(iK+wfjW>lzYaF zSBQx&MkHO*{tM~$nJdzwk}T3#CVerl^OhJ@5-Sg!PQ7@YVbC2~zj>KfLvl`<l$xq?&trsX+PR|*FL=_bAkckK)(97q8u}!6q~eCb3Jou6GE~=T0hnQ z?W}V`V?u>uSlRd1Uk)-8(+g#+an`qunW|sIR$4tiAgAY?wLiF7ig~Nx+Nj%C*{@|E zA{#a>uNT{j?vk&zX4!QJEU+juld>QeKLSHyS3Zr|A5#)TjhTZ(Q;i(1+2=cMa0s_& z+8f%N-nP0e)V!g2`7P_(@b1-vww~g}u_`IaiJhrZb=T^<8=~uQ^;U_o$t`K$b-4*7cMp#H^WXQQoJl{y)n4xNP?EtX!@3$0um)^hdlYnQ0UsVBPS6dS8w8XP)Okx{dX+-on7&BCv- zIWNBYjDhwJ@&omUHm9}DY6U;sqVH3+xvDr~M}&3e$BfWShm4b%;Tbh~`g!_vC!H`FseSL;pL$rPkw=YZ_yP zO?{MFwffsVI%kqoJMGGkl%F#iGO|fkH1gf57#9||d+Rwget)&Kc`Y=*b^wb!G-o*SU?K@{YR!*R*1K-9&GK8(o-W~a=^yvJFu$H)sX5XvA|i65p~tRQ2`U>9*FXB&+Klta!kk?*f-qiIq2CtdSk&>iCo%> zruW&|4oh!IA2EGlSNu3Xwmf%)M#v7&!%=(a1arbe0{zYjx?@#CbzA7Rf;JB?pLomo z&2;;qfwNNE>b41O^YV~uX1ct1;eGJu*ICz(*B`Gr(OTNG(_)-BRQ9nqudP^XNGoXg z(6I3E{9a~@SCU0G%c}2vNy_&a{r4NbZJ-ZlHTh;f%N?xXf6ZUXPi+d#c^)u2XL!|^ zWjs-Z*s-t9;+k>(_5L4gUnc%6>kX1PYrk4rxxYKD{e7{_tMs=HWgqIvjpXR^_=doS z!Lf?5>>K9AxtE8kTsvI5Pq!~^V=iPeH{p!L8p^(gj-EEHOQnr>eKmCyNKQ;QPRVY^ zD$5amynYvzFsDW^u6t|M_LeWf_tD{o&#sQyr5$x1Fw<8LzJ<%V{W)$t{|5eD?p15K zV>@2nczV$F;5TpMIoVQo$U~(C^PXO!UJ6^@i9UNIvwLNUsB1TExWD&h6@O&b!M=RA zYvsxBO7}!w|MVj6#-+k1MK9gTergXG7RDOhZTj*2nd7S#@2U>-9_R8FXP@BjTSwbQ z(J%W7+~3H5?+x*%2>1kSnf-ZzmWbZxzTS{myGzDL)!qg~cgTr=A_Hrrx6Als^yQ77 zt_;(@Ry;U%>P5xl)U~^Vbn=tYzBMC11}u6N|G8QJuDPb?k@KULP2L-6cP+lze55p2 zMOPN>6dh0Vri^`v+}So>L|6vmhV~D74rP@pIndAEz3tP2lf?cQvHGVldidL#XDV?c z6=RJ9(qm5tqBCmpYADtQCAWMQjEuVADMyC3iP+p9va9=)Df#J>IDWok!SbDFrS!y} zQl6X*kBO66T&Gd5Y^icdEiyWMJa+f#9m1+rNBGRvcn7aV#rRfxjhMlz@VKduQE?OloS1PH$>LSI82}_!zr>3WW&QNuek- z+sTd)8o^yBN&#s<+3n*Ltcbt|0JNTlhC0ceVhg~%stI5Pn_+LpBCFuu)$FJX)V5me zwsD}48Pi)+9K3N7(s83RUD(a;#0K{Fi8*$oJJIc$LiXH9Gl}iRol(fLUkC1F8cI07Kx|fR|$l1TKV(f?N?m zwq3-#6AFqGkHsh}bFGBShJ|vNvI_@}Ku8>rJp=@ih#`X3=CYaYMm@oOHx=JD(QbAGLaw4c-XwK4*8^J}uEA zmEzKd{s(>y5v%_if(@tkA0yZZHTw4i8(9kTO!fspBncUFMi3K0P-GJqdHx;7*#Xlr zyFXDD_I;b+ED9Jb!dXC`D9959d7>as6y%A5JW-G*3i3ojo+!u@1^*vKLGTZ%AovGa zaHcK@P8S9zB|)|VX8{86Buo${|Ktt+2fY@^{xCHpxjV8)6RZdRI3=Js!j?EnE&IGo>BAay8iPWET(T3N zLW!^r%m6?(yM#jFE)a)I!vX74ZJ}@h5KaRo(-UE9(3$Y|Uy^X2VQV_&hSP)snR3d> zUxUOTInfVK-^_BO*r70h0tPj~C*i*U9tX>S5I%Xqln=fD+9afaCqSW2hm;h4S*3)7 zqL{Wyk)QxQN;4oVbS|brc)TL?)Xso#zpf(8w50?pK__q8MkSCiGYEpR@=RNhLOnBj z0;Gmcw*_JplxM^RVic8rg$Of(faF;v==H=M7l^@QIdL)ENK_|^8wx%PdYaHl*z8Vs xrbq*uskYJ+#{q3Zr$c7}c~78Csh$-0wy~ckk{g44BSDN3hJf0zL2I}6zW@T!KivQT literal 0 HcmV?d00001 diff --git a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift index 46028204d7..bd8418a05d 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift +++ b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift @@ -17,22 +17,24 @@ // import AppLauncher +import BrowserServicesKit import Cocoa import Combine -import BrowserServicesKit import Common import Configuration +import FeatureFlags import LoginItems import Networking 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 { @@ -138,7 +140,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 +321,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(icon: Image(.settings16), title: UserText.vpnStatusViewVPNSettingsMenuItemTitle, action: { + try? await appLauncher.launchApp(withCommand: VPNAppLaunchCommand.showSettings) + })) + } + + menuItems.append(contentsOf: [ + .text(icon: Image(.window16), title: excludedAppsTitle, action: { [weak self] in + try? await self?.appLauncher.launchApp(withCommand: VPNAppLaunchCommand.manageExcludedApps) + }), + .text(icon: Image(.globe16), title: excludedWebsitesTitle, action: { [weak self] in + try? await self?.appLauncher.launchApp(withCommand: VPNAppLaunchCommand.manageExcludedDomains) + }), + .divider(), + .text(icon: Image(.help16), title: UserText.vpnStatusViewFAQMenuItemTitle, action: { [weak self] in + try? await self?.appLauncher.launchApp(withCommand: VPNAppLaunchCommand.showFAQ) + }), + .text(icon: Image(.support16), 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 +398,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..c8002e3487 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,306 @@ } } } + }, + "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" : { + "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)" + } + } + } + }, + "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" : { + "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)" + } + } + } + }, + "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" : { + "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" : "ЧаВо и поддержка" + } + } + } + }, + "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" : { + "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" : "Отправить отзыв" + } + } + } + }, + "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" : { + "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" + } + } + } } }, "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/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 22562a36dd..77acac244e 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 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 90225f3822..f579612487 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift @@ -17,12 +17,12 @@ // import Combine +import Common 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 +34,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(), icon: Image? = nil, 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..937d5a8e0c 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://excluded-apps" + case .manageExcludedDomains: + return "networkprotection://excluded-domains" case .shareFeedback: return "networkprotection://share-feedback" case .showFAQ: From 3afff2143aa3e64361dc27bf54080a8790a85388 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Tue, 4 Feb 2025 22:12:03 +0100 Subject: [PATCH 07/22] Add daily pixel for WebKit terminations (#3827) Task/Issue URL: https://app.asana.com/0/69071770703008/1209322185454642 Description: This change introduces a daily pixel for WebKit terminations (alongside the existing standard pixel reported for every tab crash). --- DuckDuckGo/Tab/Model/Tab.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift index ce3b3fe13c..02485acef3 100644 --- a/DuckDuckGo/Tab/Model/Tab.swift +++ b/DuckDuckGo/Tab/Model/Tab.swift @@ -1317,7 +1317,7 @@ extension Tab/*: NavigationResponder*/ { // to be moved to Tab+Navigation.swift let additionalParameters = await SystemInfo.pixelParameters() #endif - PixelKit.fire(DebugEvent(GeneralPixel.webKitDidTerminate, error: error), withAdditionalParameters: additionalParameters) + PixelKit.fire(DebugEvent(GeneralPixel.webKitDidTerminate, error: error), frequency: .dailyAndStandard, withAdditionalParameters: additionalParameters) } } From c751b212b427207ca407f9128b57809c6e417710 Mon Sep 17 00:00:00 2001 From: Anka Date: Wed, 5 Feb 2025 07:30:05 +0000 Subject: [PATCH 08/22] Bump version to 1.125.0 (357) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index e9047b164c..ec68a8b19e 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 356 +CURRENT_PROJECT_VERSION = 357 From 563abb92c473329571b8e361c9b8877edffd14cf Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 5 Feb 2025 10:18:56 +0100 Subject: [PATCH 09/22] Fire privacyFeedHistoryLinkOpened pixel from HTML NTP (#3830) Task/Issue URL: https://app.asana.com/0/72649045549333/1209326060139784 --- .../HomePage/View/HomePageViewController.swift | 2 +- .../DefaultRecentActivityActionsHandler.swift | 3 +++ DuckDuckGo/Statistics/GeneralPixel.swift | 6 ------ DuckDuckGo/Statistics/NewTabPagePixel.swift | 14 ++++++++++++++ 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/DuckDuckGo/HomePage/View/HomePageViewController.swift b/DuckDuckGo/HomePage/View/HomePageViewController.swift index 3882315491..777b25d9a4 100644 --- a/DuckDuckGo/HomePage/View/HomePageViewController.swift +++ b/DuckDuckGo/HomePage/View/HomePageViewController.swift @@ -162,7 +162,7 @@ final class HomePageViewController: NSViewController { func createRecentlyVisitedModel() -> HomePage.Models.RecentlyVisitedModel { return .init { [weak self] url in - PixelKit.fire(GeneralPixel.privacyFeedHistoryLinkOpened, frequency: .dailyAndCount) + PixelKit.fire(NewTabPagePixel.privacyFeedHistoryLinkOpened, frequency: .dailyAndCount) self?.openUrl(url) } } diff --git a/DuckDuckGo/NewTabPage/Features/DefaultRecentActivityActionsHandler.swift b/DuckDuckGo/NewTabPage/Features/DefaultRecentActivityActionsHandler.swift index 603069974f..0009795516 100644 --- a/DuckDuckGo/NewTabPage/Features/DefaultRecentActivityActionsHandler.swift +++ b/DuckDuckGo/NewTabPage/Features/DefaultRecentActivityActionsHandler.swift @@ -20,6 +20,7 @@ import Combine import Common import Foundation import NewTabPage +import PixelKit protocol URLFireproofStatusProviding: AnyObject { func isDomainFireproof(forURL url: URL) -> Bool @@ -165,6 +166,8 @@ final class DefaultRecentActivityActionsHandler: RecentActivityActionsHandling { return } + PixelKit.fire(NewTabPagePixel.privacyFeedHistoryLinkOpened, frequency: .dailyAndCount) + if target == .newWindow || NSApplication.shared.isCommandPressed && NSApplication.shared.isOptionPressed { WindowsManager.openNewWindow(with: url, source: .bookmark, isBurner: tabCollectionViewModel.isBurner) } else if target == .newTab || NSApplication.shared.isCommandPressed && NSApplication.shared.isShiftPressed { diff --git a/DuckDuckGo/Statistics/GeneralPixel.swift b/DuckDuckGo/Statistics/GeneralPixel.swift index 7cdd6e68d2..77898e6344 100644 --- a/DuckDuckGo/Statistics/GeneralPixel.swift +++ b/DuckDuckGo/Statistics/GeneralPixel.swift @@ -145,9 +145,6 @@ enum GeneralPixel: PixelKitEventV2 { case duckPlayerYouTubeOverlayNavigationClosed case duckPlayerYouTubeNavigationIdle30 - // Temporary Home Page Pixels - case privacyFeedHistoryLinkOpened - // Dashboard case dashboardProtectionAllowlistAdd(triggerOrigin: String?) case dashboardProtectionAllowlistRemove(triggerOrigin: String?) @@ -703,9 +700,6 @@ enum GeneralPixel: PixelKitEventV2 { case .duckPlayerYouTubeNavigationIdle30: return "duckplayer_youtube_overlay_idle-30" - case .privacyFeedHistoryLinkOpened: - return "privacy_feed_history_link_opened" - case .dashboardProtectionAllowlistAdd: return "mp_wla" case .dashboardProtectionAllowlistRemove: diff --git a/DuckDuckGo/Statistics/NewTabPagePixel.swift b/DuckDuckGo/Statistics/NewTabPagePixel.swift index 7d443b3657..46c57ef0ad 100644 --- a/DuckDuckGo/Statistics/NewTabPagePixel.swift +++ b/DuckDuckGo/Statistics/NewTabPagePixel.swift @@ -52,6 +52,18 @@ enum NewTabPagePixel: PixelKitEventV2 { */ case favoriteSectionHidden + /** + * Event Trigger: A link in Privacy Feed (a.k.a. Recent Activity) is activated. + * + * > Related links: + * [Privacy Triage](https://app.asana.com/0/69071770703008/1209316863206567) + * + * Anomaly Investigation: + * - Anomaly in this pixel may mean an increase/drop in app use. + * - This pixel is fired from `DefaultRecentActivityActionsHandler` when handling `open` JS message. + */ + case privacyFeedHistoryLinkOpened + /** * Event Trigger: Recent Activity section on NTP is hidden. * @@ -144,6 +156,7 @@ enum NewTabPagePixel: PixelKitEventV2 { switch self { case .newTabPageShown: return "m_mac_newtab_shown" case .favoriteSectionHidden: return "m_mac_favorite-section-hidden" + case .privacyFeedHistoryLinkOpened: return "m_mac_privacy_feed_history_link_opened" case .recentActivitySectionHidden: return "m_mac_recent-activity-section-hidden" case .blockedTrackingAttemptsSectionHidden: return "m_mac_blocked-tracking-attempts-section-hidden" case .blockedTrackingAttemptsShowLess: return "m_mac_new-tab-page_blocked-tracking-attempts_show-less" @@ -175,6 +188,7 @@ enum NewTabPagePixel: PixelKitEventV2 { .blockedTrackingAttemptsSectionHidden, .blockedTrackingAttemptsShowLess, .blockedTrackingAttemptsShowMore, + .privacyFeedHistoryLinkOpened, .privacyStatsCouldNotLoadDatabase, .privacyStatsDatabaseError: return nil From 107fb078962b4f6e58aaca86391c16002a01dab0 Mon Sep 17 00:00:00 2001 From: Sabrina Tardio <44158575+SabrinaTardio@users.noreply.github.com> Date: Wed, 5 Feb 2025 12:23:06 +0100 Subject: [PATCH 10/22] fix override removal (#3828) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1204186595873227/1209321233961841 **Description**: Fix a bug where the feature flag override “remove override” submenu does not appear --- .../FeatureFlagOverridesMenu.swift | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/DuckDuckGo/InternalUserDecider/FeatureFlagOverridesMenu.swift b/DuckDuckGo/InternalUserDecider/FeatureFlagOverridesMenu.swift index 0a39162593..669d7170d1 100644 --- a/DuckDuckGo/InternalUserDecider/FeatureFlagOverridesMenu.swift +++ b/DuckDuckGo/InternalUserDecider/FeatureFlagOverridesMenu.swift @@ -112,14 +112,16 @@ final class FeatureFlagOverridesMenu: NSMenu { private func updateFeatureFlagItem(_ item: NSMenuItem, flag: FeatureFlag) { let override = featureFlagger.localOverrides?.override(for: flag) + let submenu = NSMenu() + submenu.addItem(removeOverrideSubmenuItem(for: flag)) item.state = override == true ? .on : .off - item.submenu = override != nil ? cohortSubmenu(for: flag) : nil + item.submenu = override != nil ? submenu : nil } private func updateExperimentFeatureItem(_ item: NSMenuItem, flag: FeatureFlag) { let override = featureFlagger.localOverrides?.experimentOverride(for: flag) item.state = override != nil ? .on : .off - item.submenu = override != nil ? cohortSubmenu(for: flag) : cohortSubmenu(for: flag) + item.submenu = cohortSubmenu(for: flag) } // MARK: - Actions @@ -177,17 +179,23 @@ final class FeatureFlagOverridesMenu: NSMenu { submenu.addItem(NSMenuItem.separator()) // "Remove Override" only if an override exists + let removeOverrideItem = removeOverrideSubmenuItem(for: flag) + removeOverrideItem.isHidden = currentOverride == nil + + submenu.addItem(removeOverrideItem) + + return submenu + } + + private func removeOverrideSubmenuItem(for flag: FeatureFlag) -> NSMenuItem { let removeOverrideItem = NSMenuItem( title: "Remove Override", action: #selector(resetOverride(_:)), target: self ) removeOverrideItem.representedObject = flag - removeOverrideItem.isHidden = currentOverride == nil - - submenu.addItem(removeOverrideItem) - - return submenu + removeOverrideItem.isHidden = featureFlagger.localOverrides?.override(for: flag) == nil + return removeOverrideItem } private func cohorts(for featureFlag: Flag) -> [any FeatureFlagCohortDescribing] { From 2b5e47d2161c3da7919309ed61d790e17d9e6a2a Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 5 Feb 2025 14:33:15 +0100 Subject: [PATCH 11/22] Deduplicate entries in History Menu (#3831) Task/Issue URL: https://app.asana.com/0/0/1209328755192084/f Description: This change updates History Menu to only display one item per URL per day, which is in line with other mainstream browsers. This change remains behind historyView feature flag and will only be made available when Full History View is released. The grouping logic was extracted into HistoryGroupingProvider class where it's made dependent on the feature flag state. Current behavior remains unchanged, as evidenced by added unit tests. --- DuckDuckGo-macOS.xcodeproj/project.pbxproj | 12 + .../Services/HistoryGroupingProvider.swift | 89 ++++ .../Menus/CleanThisHistoryMenuItem.swift | 21 +- DuckDuckGo/Menus/HistoryMenu.swift | 46 +- DuckDuckGo/Menus/MainMenuActions.swift | 2 +- .../HistoryGroupingProviderTests.swift | 483 ++++++++++++++++++ 6 files changed, 609 insertions(+), 44 deletions(-) create mode 100644 DuckDuckGo/History/Services/HistoryGroupingProvider.swift create mode 100644 UnitTests/History/Services/HistoryGroupingProviderTests.swift diff --git a/DuckDuckGo-macOS.xcodeproj/project.pbxproj b/DuckDuckGo-macOS.xcodeproj/project.pbxproj index 26e1fe4dc9..db76786806 100644 --- a/DuckDuckGo-macOS.xcodeproj/project.pbxproj +++ b/DuckDuckGo-macOS.xcodeproj/project.pbxproj @@ -1175,6 +1175,10 @@ 37445F9A2A1566420029F789 /* SyncDataProviders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37445F982A1566420029F789 /* SyncDataProviders.swift */; }; 37445F9C2A1569F00029F789 /* SyncBookmarksAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37445F9B2A1569F00029F789 /* SyncBookmarksAdapter.swift */; }; 37445F9D2A1569F00029F789 /* SyncBookmarksAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37445F9B2A1569F00029F789 /* SyncBookmarksAdapter.swift */; }; + 3745DE052D536DCF00024FC8 /* HistoryGroupingProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3745DE042D536DCA00024FC8 /* HistoryGroupingProvider.swift */; }; + 3745DE062D536DCF00024FC8 /* HistoryGroupingProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3745DE042D536DCA00024FC8 /* HistoryGroupingProvider.swift */; }; + 3745DE082D536EF900024FC8 /* HistoryGroupingProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3745DE072D536EF000024FC8 /* HistoryGroupingProviderTests.swift */; }; + 3745DE092D536EF900024FC8 /* HistoryGroupingProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3745DE072D536EF000024FC8 /* HistoryGroupingProviderTests.swift */; }; 37479F152891BC8300302FE2 /* TabCollectionViewModelTests+WithoutPinnedTabsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37479F142891BC8300302FE2 /* TabCollectionViewModelTests+WithoutPinnedTabsManager.swift */; }; 374EF08329B751FC003D2E87 /* RecentlyClosedCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374EF08229B751FC003D2E87 /* RecentlyClosedCoordinatorTests.swift */; }; 374EF08429B7575B003D2E87 /* RecentlyClosedCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374EF08229B751FC003D2E87 /* RecentlyClosedCoordinatorTests.swift */; }; @@ -3804,6 +3808,8 @@ 374286242CC593F900E66323 /* HomePageSettingsVisibilityModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePageSettingsVisibilityModelTests.swift; sourceTree = ""; }; 37445F982A1566420029F789 /* SyncDataProviders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncDataProviders.swift; sourceTree = ""; }; 37445F9B2A1569F00029F789 /* SyncBookmarksAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncBookmarksAdapter.swift; sourceTree = ""; }; + 3745DE042D536DCA00024FC8 /* HistoryGroupingProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryGroupingProvider.swift; sourceTree = ""; }; + 3745DE072D536EF000024FC8 /* HistoryGroupingProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryGroupingProviderTests.swift; sourceTree = ""; }; 37479F142891BC8300302FE2 /* TabCollectionViewModelTests+WithoutPinnedTabsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TabCollectionViewModelTests+WithoutPinnedTabsManager.swift"; sourceTree = ""; }; 374EF08229B751FC003D2E87 /* RecentlyClosedCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentlyClosedCoordinatorTests.swift; sourceTree = ""; }; 374EFDF22D01C99700B30939 /* ActiveRemoteMessageModel+NewTabPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ActiveRemoteMessageModel+NewTabPage.swift"; sourceTree = ""; }; @@ -9235,6 +9241,7 @@ AAE75278263B046100B973F8 /* History.xcdatamodeld */, AAE7527B263B056C00B973F8 /* EncryptedHistoryStore.swift */, 85D0327A2B8E3D090041D1FB /* HistoryCoordinatorExtension.swift */, + 3745DE042D536DCA00024FC8 /* HistoryGroupingProvider.swift */, ); path = Services; sourceTree = ""; @@ -9270,6 +9277,7 @@ AAEC74B02642C48B00C2EFBC /* Services */ = { isa = PBXGroup; children = ( + 3745DE072D536EF000024FC8 /* HistoryGroupingProviderTests.swift */, AAEC74B52642CC6A00C2EFBC /* HistoryStoringMock.swift */, AAEC74B72642E43800C2EFBC /* HistoryStoreTests.swift */, ); @@ -11707,6 +11715,7 @@ 1D0DE9422C3BB9CC0037ABC2 /* ReleaseNotesParser.swift in Sources */, EED4D3D92C874AE200C79EEA /* PopoverInfoViewController.swift in Sources */, 3707C722294B5D2900682A9F /* WKWebViewExtension.swift in Sources */, + 3745DE052D536DCF00024FC8 /* HistoryGroupingProvider.swift in Sources */, 3706FAD9293F65D500E42796 /* FirefoxFaviconsReader.swift in Sources */, 3706FADB293F65D500E42796 /* ContentBlockingRulesUpdateObserver.swift in Sources */, 3706FADC293F65D500E42796 /* FirefoxLoginReader.swift in Sources */, @@ -12737,6 +12746,7 @@ 56A054002C1AEFA1007D8FAB /* OnboardingManagerTests.swift in Sources */, 3706FE36293F661700E42796 /* FirefoxFaviconsReaderTests.swift in Sources */, 3706FE37293F661700E42796 /* TabCollectionViewModelTests+WithoutPinnedTabsManager.swift in Sources */, + 3745DE092D536EF900024FC8 /* HistoryGroupingProviderTests.swift in Sources */, 3706FE38293F661700E42796 /* SuggestionContainerTests.swift in Sources */, 3706FE39293F661700E42796 /* TabTests.swift in Sources */, 9FAD623E2BD09DE5007F3A65 /* WebsiteInfoTests.swift in Sources */, @@ -13510,6 +13520,7 @@ B684592225C93BE000DC17B6 /* Publisher.asVoid.swift in Sources */, 4B9DB01D2A983B24000927DB /* Waitlist.swift in Sources */, BB9BDD4A2D09BAA80069E9EF /* TabBarRemoteMessageViewModel.swift in Sources */, + 3745DE062D536DCF00024FC8 /* HistoryGroupingProvider.swift in Sources */, AAA0CC33252F181A0079BC96 /* NavigationButtonMenuDelegate.swift in Sources */, 1DA84D2F2C11989D0011C80F /* Update.swift in Sources */, AAC30A2A268E239100D2D9CD /* CrashReport.swift in Sources */, @@ -14505,6 +14516,7 @@ 5677A93D2C98414900DA7B0A /* ContextualOnboardingStateMachineTests.swift in Sources */, AA652CCE25DD9071009059CC /* BookmarkListTests.swift in Sources */, 859E7D6D274548F2009C2B69 /* BookmarksExporterTests.swift in Sources */, + 3745DE082D536EF900024FC8 /* HistoryGroupingProviderTests.swift in Sources */, B6A5A2A825BAA35500AA7ADA /* WindowManagerStateRestorationTests.swift in Sources */, B6AE39F129373AF200C37AA4 /* EmptyAttributionRulesProver.swift in Sources */, 4BB99D1126FE1A84001E4761 /* SafariBookmarksReaderTests.swift in Sources */, diff --git a/DuckDuckGo/History/Services/HistoryGroupingProvider.swift b/DuckDuckGo/History/Services/HistoryGroupingProvider.swift new file mode 100644 index 0000000000..5846754b31 --- /dev/null +++ b/DuckDuckGo/History/Services/HistoryGroupingProvider.swift @@ -0,0 +1,89 @@ +// +// HistoryGroupingProvider.swift +// +// 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. +// 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 BrowserServicesKit +import Common +import FeatureFlags +import Foundation +import History +import os.log + +/** + * This protocol describes a data source (history provider) for `HistoryGroupingProvider`. + */ +protocol HistoryGroupingDataSource: AnyObject { + var history: BrowsingHistory? { get } +} + +extension HistoryCoordinator: HistoryGroupingDataSource {} + +/** + * This class is responsible for grouping history visits for History Menu. + * + * When `historyView` feature flag is enabled, visits are deduplicated + * to only have the latest visit per URL per day. + */ +final class HistoryGroupingProvider { + private let featureFlagger: FeatureFlagger + private(set) weak var dataSource: HistoryGroupingDataSource? + + init(dataSource: HistoryGroupingDataSource, featureFlagger: FeatureFlagger = NSApp.delegateTyped.featureFlagger) { + self.featureFlagger = featureFlagger + self.dataSource = dataSource + } + + /** + * Returns visits for a given day. + */ + func getRecentVisits(maxCount: Int) -> [Visit] { + removeDuplicatesIfNeeded(from: getSortedArrayOfVisits()) + .prefix(maxCount) + .filter { Calendar.current.isDateInToday($0.date) } + } + + /** + * Returns history visits bucketed per day. + */ + func getVisitGroupings() -> [HistoryMenu.HistoryGrouping] { + Dictionary(grouping: getSortedArrayOfVisits(), by: \.date.startOfDay) + .map { date, sortedVisits in + HistoryMenu.HistoryGrouping(date: date, visits: removeDuplicatesIfNeeded(from: sortedVisits)) + } + .sorted { $0.date > $1.date } + } + + private func removeDuplicatesIfNeeded(from sortedVisits: [Visit]) -> [Visit] { + // History View introduces visits deduplication to declutter history. + // We don't want to release it before History View, so we're + // only deduplicating visits if the feature flag is on. + guard featureFlagger.isFeatureOn(.historyView) else { + return sortedVisits + } + // It's so simple because visits array is sorted by timestamp, so removing duplicates would + // remove items with older timestamps (as proven by unit tests). + return sortedVisits.removingDuplicates(byKey: \.historyEntry?.url) + } + + private func getSortedArrayOfVisits() -> [Visit] { + guard let history = dataSource?.history else { + Logger.general.error("HistoryCoordinator: No history available") + return [] + } + return history.flatMap(\.visits).sorted { $0.date > $1.date } + } +} diff --git a/DuckDuckGo/Menus/CleanThisHistoryMenuItem.swift b/DuckDuckGo/Menus/CleanThisHistoryMenuItem.swift index a2c274cf69..192a3717ef 100644 --- a/DuckDuckGo/Menus/CleanThisHistoryMenuItem.swift +++ b/DuckDuckGo/Menus/CleanThisHistoryMenuItem.swift @@ -17,6 +17,7 @@ // import AppKit +import BrowserServicesKit import Foundation import History @@ -67,10 +68,26 @@ final class ClearThisHistoryMenuItem: NSMenuItem { } // Getting visits for the whole menu section in order to perform burning - func getVisits() -> [Visit] { - return menu?.items.compactMap({ menuItem in + func getVisits(featureFlagger: FeatureFlagger) -> [Visit] { + let visits = menu?.items.compactMap({ menuItem in return (menuItem as? VisitMenuItem)?.visit }) ?? [] + + guard featureFlagger.isFeatureOn(.historyView) else { + // historyView feature flag enables history items deduplication + // where menu items don't represent all visits on a given day. + // If disabled, we're free to just return visits represented by + // menu items. + return visits + } + + // If historyView flag is enabled, we have to find all visits from a given day, + // by querying their respective history entries. + let allVisitsForGivenDay = visits.flatMap { visit in + let date = visit.date + return visit.historyEntry?.visits.filter { $0.date.isSameDay(date) } ?? [] + } + return allVisitsForGivenDay } } diff --git a/DuckDuckGo/Menus/HistoryMenu.swift b/DuckDuckGo/Menus/HistoryMenu.swift index 5b02786c87..f133e1de4b 100644 --- a/DuckDuckGo/Menus/HistoryMenu.swift +++ b/DuckDuckGo/Menus/HistoryMenu.swift @@ -38,13 +38,13 @@ final class HistoryMenu: NSMenu { .withAccessibilityIdentifier("HistoryMenu.clearAllHistory") private let clearAllHistorySeparator = NSMenuItem.separator() - private let historyCoordinator: HistoryCoordinating + private let historyGroupingProvider: HistoryGroupingProvider @MainActor private let reopenMenuItemKeyEquivalentManager = ReopenMenuItemKeyEquivalentManager() @MainActor - init(historyCoordinator: HistoryCoordinating = HistoryCoordinator.shared) { - self.historyCoordinator = historyCoordinator + init(historyGroupingProvider: HistoryGroupingProvider = .init(dataSource: HistoryCoordinator.shared)) { + self.historyGroupingProvider = historyGroupingProvider super.init(title: UserText.mainMenuHistory) @@ -131,7 +131,7 @@ final class HistoryMenu: NSMenu { private func addRecentlyVisited() { recentlyVisitedMenuItems = [recentlyVisitedHeaderMenuItem] - let recentVisits = historyCoordinator.getRecentVisits(maxCount: 14) + let recentVisits = historyGroupingProvider.getRecentVisits(maxCount: 14) for (index, visit) in zip( recentVisits.indices, recentVisits ) { @@ -154,7 +154,7 @@ final class HistoryMenu: NSMenu { private var historyGroupingsMenuItems = [NSMenuItem]() private func addHistoryGroupings() { - let groupings = historyCoordinator.getVisitGroupings() + let groupings = historyGroupingProvider.getVisitGroupings() var firstWeek = [HistoryGrouping](), older = [HistoryGrouping]() groupings.forEach { grouping in if grouping.date > Date.weekAgo.startOfDay { @@ -340,39 +340,3 @@ private extension NSApplication { } } - -private extension HistoryCoordinating { - - func getSortedArrayOfVisits() -> [Visit] { - guard let history = history else { - Logger.general.error("HistoryCoordinator: No history available") - return [] - } - - return Array(history - .flatMap { entry in - Array(entry.visits) - } - .sorted(by: { (visit1, visit2) in - visit1.date > visit2.date - })) - } - - func getRecentVisits(maxCount: Int) -> [Visit] { - return Array(getSortedArrayOfVisits() - .prefix(maxCount) - .filter { NSCalendar.current.isDateInToday($0.date) } - ) - } - - func getVisitGroupings() -> [HistoryMenu.HistoryGrouping] { - return Dictionary(grouping: getSortedArrayOfVisits()) { visit in - return visit.date.startOfDay - } .map { - return HistoryMenu.HistoryGrouping(date: $0.key, visits: $0.value) - } .sorted(by: { (grouping1, grouping2) in - grouping1.date > grouping2.date - }) - } - -} diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index 1bbf27bb9a..cefe87a170 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -584,7 +584,7 @@ extension MainViewController { let dateString = sender.dateString let isToday = sender.isToday - let visits = sender.getVisits() + let visits = sender.getVisits(featureFlagger: featureFlagger) let alert = NSAlert.clearHistoryAndDataAlert(dateString: dateString) alert.beginSheetModal(for: window, completionHandler: { response in guard case .alertFirstButtonReturn = response else { diff --git a/UnitTests/History/Services/HistoryGroupingProviderTests.swift b/UnitTests/History/Services/HistoryGroupingProviderTests.swift new file mode 100644 index 0000000000..4499fd299c --- /dev/null +++ b/UnitTests/History/Services/HistoryGroupingProviderTests.swift @@ -0,0 +1,483 @@ +// +// HistoryGroupingProviderTests.swift +// +// 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. +// 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 Common +import History +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +final class MockHistoryGroupingDataSource: HistoryGroupingDataSource { + var history: BrowsingHistory? = [] +} + +final class HistoryGroupingProviderTests: XCTestCase { + private var dataSource: MockHistoryGroupingDataSource! + private var featureFlagger: MockFeatureFlagger! + private var provider: HistoryGroupingProvider! + + override func setUp() async throws { + dataSource = MockHistoryGroupingDataSource() + featureFlagger = MockFeatureFlagger() + provider = HistoryGroupingProvider(dataSource: dataSource, featureFlagger: featureFlagger) + } + + // MARK: - getRecentVisits with deduplication + + func testWhenHistoryViewIsEnabledThenRecentVisitsAreDeduplicatedLeavingMostRecentVisit() throws { + featureFlagger.isFeatureOn = true + + let date = Date.noonToday + dataSource.history = [ + .make(url: "https://example.com".url!, visits: [ + Visit(date: date.addingTimeInterval(-1)), + Visit(date: date.addingTimeInterval(-2)), + Visit(date: date.addingTimeInterval(-3)) + ]) + ] + + let recentVisits = provider.getRecentVisits(maxCount: 100) + let firstRecentVisit = try XCTUnwrap(recentVisits[safe: 0]) + XCTAssertEqual(recentVisits.count, 1) + XCTAssertEqual(firstRecentVisit.date, date.addingTimeInterval(-1)) + } + + func testWhenHistoryViewIsEnabledThenRecentVisitsAreSortedByMostRecentVisit() throws { + featureFlagger.isFeatureOn = true + + let date = Date.noonToday + dataSource.history = [ + .make(url: "https://example.com".url!, visits: [ + Visit(date: date.addingTimeInterval(-3)), + Visit(date: date.addingTimeInterval(-5)), + Visit(date: date.addingTimeInterval(-10)) + ]), + .make(url: "https://example.com/index2.html".url!, visits: [ + Visit(date: date.addingTimeInterval(-1)), + Visit(date: date.addingTimeInterval(-20)) + ]) + ] + + let recentVisits = provider.getRecentVisits(maxCount: 100) + let firstRecentVisit = try XCTUnwrap(recentVisits[safe: 0]) + let secondRecentVisit = try XCTUnwrap(recentVisits[safe: 1]) + XCTAssertEqual(recentVisits.count, 2) + XCTAssertEqual(firstRecentVisit.date, date.addingTimeInterval(-1)) + XCTAssertEqual(secondRecentVisit.date, date.addingTimeInterval(-3)) + } + + func testWhenHistoryViewIsEnabledThenRecentVisitsAreLimitedToMaxCount() throws { + featureFlagger.isFeatureOn = true + + let date = Date.noonToday + dataSource.history = [ + .make(url: "https://example.com".url!, visits: [ + Visit(date: date.addingTimeInterval(-3)), + ]), + .make(url: "https://example.com/index2.html".url!, visits: [ + Visit(date: date.addingTimeInterval(-1)), + ]), + .make(url: "https://example.com/index3.html".url!, visits: [ + Visit(date: date.addingTimeInterval(-10)), + ]), + .make(url: "https://example.com/index4.html".url!, visits: [ + Visit(date: date.addingTimeInterval(-20)), + ]), + .make(url: "https://example.com/index5.html".url!, visits: [ + Visit(date: date.addingTimeInterval(-6)), + ]) + ] + + let recentVisits = provider.getRecentVisits(maxCount: 3) + XCTAssertEqual(recentVisits.count, 3) + XCTAssertEqual(recentVisits.map(\.date), [ + date.addingTimeInterval(-1), + date.addingTimeInterval(-3), + date.addingTimeInterval(-6) + ]) + } + + func testWhenHistoryViewIsEnabledThenRecentVisitsAreLimitedToCurrentDay() throws { + featureFlagger.isFeatureOn = true + + let date = Date.noonToday + dataSource.history = [ + .make(url: "https://example.com".url!, visits: [ + Visit(date: date.addingTimeInterval(-3)), + ]), + .make(url: "https://example.com/index2.html".url!, visits: [ + Visit(date: date.daysAgo(1)), + ]), + .make(url: "https://example.com/index3.html".url!, visits: [ + Visit(date: date.addingTimeInterval(-10)), + ]), + .make(url: "https://example.com/index4.html".url!, visits: [ + Visit(date: date.addingTimeInterval(-20*3600)), + ]), + .make(url: "https://example.com/index5.html".url!, visits: [ + Visit(date: date.addingTimeInterval(-6)), + ]) + ] + + let recentVisits = provider.getRecentVisits(maxCount: 100) + XCTAssertEqual(recentVisits.count, 3) + XCTAssertEqual(recentVisits.map(\.date), [ + date.addingTimeInterval(-3), + date.addingTimeInterval(-6), + date.addingTimeInterval(-10) + ]) + } + + // MARK: - getRecentVisits without deduplication + + func testWhenHistoryViewIsDisabledThenRecentVisitsAreNotDeduplicated() throws { + featureFlagger.isFeatureOn = false + + let date = Date.noonToday + dataSource.history = [ + .make(url: "https://example.com".url!, visits: [ + Visit(date: date.addingTimeInterval(-1)), + Visit(date: date.addingTimeInterval(-2)), + Visit(date: date.addingTimeInterval(-3)) + ]) + ] + + let recentVisits = provider.getRecentVisits(maxCount: 100) + XCTAssertEqual(recentVisits.count, 3) + XCTAssertEqual(recentVisits.map(\.date), [ + date.addingTimeInterval(-1), + date.addingTimeInterval(-2), + date.addingTimeInterval(-3) + ]) + } + + func testWhenHistoryViewIsDisabledThenRecentVisitsAreSortedByMostRecentVisit() throws { + featureFlagger.isFeatureOn = false + + let date = Date.noonToday + dataSource.history = [ + .make(url: "https://example.com".url!, visits: [ + Visit(date: date.addingTimeInterval(-3)), + Visit(date: date.addingTimeInterval(-5)), + Visit(date: date.addingTimeInterval(-10)) + ]), + .make(url: "https://example.com/index2.html".url!, visits: [ + Visit(date: date.addingTimeInterval(-1)), + Visit(date: date.addingTimeInterval(-20)) + ]) + ] + + let recentVisits = provider.getRecentVisits(maxCount: 100) + XCTAssertEqual(recentVisits.count, 5) + XCTAssertEqual(recentVisits.map(\.historyEntry?.url), [ + "https://example.com/index2.html".url!, + "https://example.com".url!, + "https://example.com".url!, + "https://example.com".url!, + "https://example.com/index2.html".url! + ]) + XCTAssertEqual(recentVisits.map(\.date), [ + date.addingTimeInterval(-1), + date.addingTimeInterval(-3), + date.addingTimeInterval(-5), + date.addingTimeInterval(-10), + date.addingTimeInterval(-20) + ]) + } + + func testWhenHistoryViewIsDisabledThenRecentVisitsAreLimitedToMaxCount() throws { + featureFlagger.isFeatureOn = false + + let date = Date.noonToday + dataSource.history = [ + .make(url: "https://example.com".url!, visits: [ + Visit(date: date.addingTimeInterval(-3)), + Visit(date: date.addingTimeInterval(-5)), + Visit(date: date.addingTimeInterval(-10)) + ]), + .make(url: "https://example.com/index2.html".url!, visits: [ + Visit(date: date.addingTimeInterval(-1)), + Visit(date: date.addingTimeInterval(-20)) + ]) + ] + + let recentVisits = provider.getRecentVisits(maxCount: 3) + XCTAssertEqual(recentVisits.count, 3) + XCTAssertEqual(recentVisits.map(\.date), [ + date.addingTimeInterval(-1), + date.addingTimeInterval(-3), + date.addingTimeInterval(-5) + ]) + } + + func testWhenHistoryViewIsDisabledThenRecentVisitsAreLimitedToCurrentDay() throws { + featureFlagger.isFeatureOn = false + + let date = Date.noonToday + dataSource.history = [ + .make(url: "https://example.com".url!, visits: [ + Visit(date: date.addingTimeInterval(-3)), + Visit(date: date.addingTimeInterval(-5000)), + Visit(date: date.addingTimeInterval(-20*3600)) + ]), + .make(url: "https://example.com/index2.html".url!, visits: [ + Visit(date: date.addingTimeInterval(-3000)), + Visit(date: date.daysAgo(1)) + ]) + ] + + let recentVisits = provider.getRecentVisits(maxCount: 100) + XCTAssertEqual(recentVisits.count, 3) + XCTAssertEqual(recentVisits.map(\.date), [ + date.addingTimeInterval(-3), + date.addingTimeInterval(-3000), + date.addingTimeInterval(-5000) + ]) + } + + // MARK: - getVisitGroupings with deduplication + + func testWhenHistoryViewIsEnabledThenVisitGroupingsAreDeduplicated() throws { + featureFlagger.isFeatureOn = true + + let date = Date.noonToday + dataSource.history = [ + .make(url: "https://example.com".url!, visits: [ + Visit(date: date.addingTimeInterval(-3)), + Visit(date: date.addingTimeInterval(-100)), + Visit(date: date.daysAgo(1)), + Visit(date: date.daysAgo(1).addingTimeInterval(-100)), + Visit(date: date.daysAgo(2).addingTimeInterval(-1)), + Visit(date: date.daysAgo(2).addingTimeInterval(-100)), + Visit(date: date.daysAgo(4)), + Visit(date: date.daysAgo(4).addingTimeInterval(-100)), + Visit(date: date.daysAgo(5).addingTimeInterval(-1)), + Visit(date: date.daysAgo(5).addingTimeInterval(-100)) + ]), + .make(url: "https://example.com/index2.html".url!, visits: [ + Visit(date: date.addingTimeInterval(-3000)), + Visit(date: date.addingTimeInterval(-5000)), + Visit(date: date.daysAgo(1).addingTimeInterval(-1)), + Visit(date: date.daysAgo(1).addingTimeInterval(-5000)), + Visit(date: date.daysAgo(2)), + Visit(date: date.daysAgo(2).addingTimeInterval(-5000)), + Visit(date: date.daysAgo(3)), + Visit(date: date.daysAgo(3).addingTimeInterval(-5000)), + Visit(date: date.daysAgo(5)), + Visit(date: date.daysAgo(5).addingTimeInterval(-5000)) + ]) + ] + + let groupings = provider.getVisitGroupings() + + XCTAssertEqual(groupings.count, 6) + XCTAssertEqual(groupings.map { $0.visits.map(\.date) }, [ + [ + date.addingTimeInterval(-3), + date.addingTimeInterval(-3000) + ], + [ + date.daysAgo(1), + date.daysAgo(1).addingTimeInterval(-1) + ], + [ + date.daysAgo(2), + date.daysAgo(2).addingTimeInterval(-1) + ], + [ + date.daysAgo(3) + ], + [ + date.daysAgo(4) + ], + [ + date.daysAgo(5), + date.daysAgo(5).addingTimeInterval(-1) + ] + ]) + XCTAssertEqual(groupings.map { $0.visits.map(\.historyEntry?.url) }, [ + [ + "https://example.com".url!, + "https://example.com/index2.html".url! + ], + [ + "https://example.com".url!, + "https://example.com/index2.html".url! + ], + [ + "https://example.com/index2.html".url!, + "https://example.com".url! + ], + [ + "https://example.com/index2.html".url! + ], + [ + "https://example.com".url!, + ], + [ + "https://example.com/index2.html".url!, + "https://example.com".url! + ] + ]) + } + + // MARK: - getVisitGroupings without deduplication + + func testWhenHistoryViewIsDisabledThenVisitGroupingsAreNotDeduplicated() throws { + featureFlagger.isFeatureOn = false + + let date = Date.noonToday + dataSource.history = [ + .make(url: "https://example.com".url!, visits: [ + Visit(date: date.addingTimeInterval(-3)), + Visit(date: date.addingTimeInterval(-100)), + Visit(date: date.daysAgo(1)), + Visit(date: date.daysAgo(1).addingTimeInterval(-100)), + Visit(date: date.daysAgo(2).addingTimeInterval(-1)), + Visit(date: date.daysAgo(2).addingTimeInterval(-100)), + Visit(date: date.daysAgo(4)), + Visit(date: date.daysAgo(4).addingTimeInterval(-100)), + Visit(date: date.daysAgo(5).addingTimeInterval(-1)), + Visit(date: date.daysAgo(5).addingTimeInterval(-100)) + ]), + .make(url: "https://example.com/index2.html".url!, visits: [ + Visit(date: date.addingTimeInterval(-3000)), + Visit(date: date.addingTimeInterval(-5000)), + Visit(date: date.daysAgo(1).addingTimeInterval(-1)), + Visit(date: date.daysAgo(1).addingTimeInterval(-5000)), + Visit(date: date.daysAgo(2)), + Visit(date: date.daysAgo(2).addingTimeInterval(-5000)), + Visit(date: date.daysAgo(3)), + Visit(date: date.daysAgo(3).addingTimeInterval(-5000)), + Visit(date: date.daysAgo(5)), + Visit(date: date.daysAgo(5).addingTimeInterval(-5000)) + ]) + ] + + let groupings = provider.getVisitGroupings() + + XCTAssertEqual(groupings.count, 6) + XCTAssertEqual(groupings.map { $0.visits.map(\.date) }, [ + [ + date.addingTimeInterval(-3), + date.addingTimeInterval(-100), + date.addingTimeInterval(-3000), + date.addingTimeInterval(-5000) + ], + [ + date.daysAgo(1), + date.daysAgo(1).addingTimeInterval(-1), + date.daysAgo(1).addingTimeInterval(-100), + date.daysAgo(1).addingTimeInterval(-5000) + ], + [ + date.daysAgo(2), + date.daysAgo(2).addingTimeInterval(-1), + date.daysAgo(2).addingTimeInterval(-100), + date.daysAgo(2).addingTimeInterval(-5000) + ], + [ + date.daysAgo(3), + date.daysAgo(3).addingTimeInterval(-5000) + ], + [ + date.daysAgo(4), + date.daysAgo(4).addingTimeInterval(-100) + ], + [ + date.daysAgo(5), + date.daysAgo(5).addingTimeInterval(-1), + date.daysAgo(5).addingTimeInterval(-100), + date.daysAgo(5).addingTimeInterval(-5000) + ] + ]) + XCTAssertEqual(groupings.map { $0.visits.map(\.historyEntry?.url) }, [ + [ + "https://example.com".url!, + "https://example.com".url!, + "https://example.com/index2.html".url!, + "https://example.com/index2.html".url! + ], + [ + "https://example.com".url!, + "https://example.com/index2.html".url!, + "https://example.com".url!, + "https://example.com/index2.html".url! + ], + [ + "https://example.com/index2.html".url!, + "https://example.com".url!, + "https://example.com".url!, + "https://example.com/index2.html".url! + ], + [ + "https://example.com/index2.html".url!, + "https://example.com/index2.html".url! + ], + [ + "https://example.com".url!, + "https://example.com".url! + ], + [ + "https://example.com/index2.html".url!, + "https://example.com".url!, + "https://example.com".url!, + "https://example.com/index2.html".url! + ] + ]) + } +} + +private extension Date { + /// Useful for date calculations to ensure we're not going into a previous day + /// when removing a small time interval. + static var noonToday: Date { + Date.startOfDayTomorrow.addingTimeInterval(-12*3600) + } +} + +private extension HistoryEntry { + static func make( + identifier: UUID = UUID(), + url: URL, + title: String? = nil, + failedToLoad: Bool = false, + numberOfTotalVisits: Int = 1, + lastVisit: Date = Date(), + visits: Set, + numberOfTrackersBlocked: Int = 0, + blockedTrackingEntities: Set = [], + trackersFound: Bool = false + ) -> HistoryEntry { + let entry = HistoryEntry( + identifier: identifier, + url: url, + title: title, + failedToLoad: failedToLoad, + numberOfTotalVisits: numberOfTotalVisits, + lastVisit: lastVisit, + visits: [], + numberOfTrackersBlocked: numberOfTrackersBlocked, + blockedTrackingEntities: blockedTrackingEntities, + trackersFound: trackersFound + ) + entry.visits = Set(visits.map { + Visit(date: $0.date, identifier: entry.url, historyEntry: entry) + }) + return entry + } +} From 5f82d5ee132524bec505b643a8f6fb920dd085b1 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 5 Feb 2025 17:41:42 +0100 Subject: [PATCH 12/22] Add History Debug Menu on macOS and display only 1 week of history when history view is enabled (#3833) Task/Issue URL: https://app.asana.com/0/72649045549333/1209328755192085 Description: This change updates History Menu when historyView feature flag is enabled, to only keep history items for last 7 days, and offer opening Full History View in order to view all history. For testing purposes, History Debug Menu was added, where history can be populated with visits to fake websites. --- DuckDuckGo-macOS.xcodeproj/project.pbxproj | 8 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../History/Services/HistoryDebugMenu.swift | 113 ++++++++++++++++++ DuckDuckGo/Menus/HistoryMenu.swift | 32 +++-- DuckDuckGo/Menus/MainMenu.swift | 2 + .../DataBrokerProtection/Package.swift | 2 +- LocalPackages/FeatureFlags/Package.swift | 2 +- LocalPackages/HistoryView/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/NewTabPage/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- .../UserScriptActionsManager/Package.swift | 2 +- LocalPackages/WebKitExtensions/Package.swift | 2 +- .../Model/HistoryCoordinatingMock.swift | 2 +- 14 files changed, 155 insertions(+), 22 deletions(-) create mode 100644 DuckDuckGo/History/Services/HistoryDebugMenu.swift diff --git a/DuckDuckGo-macOS.xcodeproj/project.pbxproj b/DuckDuckGo-macOS.xcodeproj/project.pbxproj index db76786806..40822b172d 100644 --- a/DuckDuckGo-macOS.xcodeproj/project.pbxproj +++ b/DuckDuckGo-macOS.xcodeproj/project.pbxproj @@ -1179,6 +1179,8 @@ 3745DE062D536DCF00024FC8 /* HistoryGroupingProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3745DE042D536DCA00024FC8 /* HistoryGroupingProvider.swift */; }; 3745DE082D536EF900024FC8 /* HistoryGroupingProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3745DE072D536EF000024FC8 /* HistoryGroupingProviderTests.swift */; }; 3745DE092D536EF900024FC8 /* HistoryGroupingProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3745DE072D536EF000024FC8 /* HistoryGroupingProviderTests.swift */; }; + 3745DE0B2D53969300024FC8 /* HistoryDebugMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3745DE0A2D53969000024FC8 /* HistoryDebugMenu.swift */; }; + 3745DE0C2D53969300024FC8 /* HistoryDebugMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3745DE0A2D53969000024FC8 /* HistoryDebugMenu.swift */; }; 37479F152891BC8300302FE2 /* TabCollectionViewModelTests+WithoutPinnedTabsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37479F142891BC8300302FE2 /* TabCollectionViewModelTests+WithoutPinnedTabsManager.swift */; }; 374EF08329B751FC003D2E87 /* RecentlyClosedCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374EF08229B751FC003D2E87 /* RecentlyClosedCoordinatorTests.swift */; }; 374EF08429B7575B003D2E87 /* RecentlyClosedCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374EF08229B751FC003D2E87 /* RecentlyClosedCoordinatorTests.swift */; }; @@ -3810,6 +3812,7 @@ 37445F9B2A1569F00029F789 /* SyncBookmarksAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncBookmarksAdapter.swift; sourceTree = ""; }; 3745DE042D536DCA00024FC8 /* HistoryGroupingProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryGroupingProvider.swift; sourceTree = ""; }; 3745DE072D536EF000024FC8 /* HistoryGroupingProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryGroupingProviderTests.swift; sourceTree = ""; }; + 3745DE0A2D53969000024FC8 /* HistoryDebugMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryDebugMenu.swift; sourceTree = ""; }; 37479F142891BC8300302FE2 /* TabCollectionViewModelTests+WithoutPinnedTabsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TabCollectionViewModelTests+WithoutPinnedTabsManager.swift"; sourceTree = ""; }; 374EF08229B751FC003D2E87 /* RecentlyClosedCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentlyClosedCoordinatorTests.swift; sourceTree = ""; }; 374EFDF22D01C99700B30939 /* ActiveRemoteMessageModel+NewTabPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ActiveRemoteMessageModel+NewTabPage.swift"; sourceTree = ""; }; @@ -9238,6 +9241,7 @@ AAE75276263B038A00B973F8 /* Services */ = { isa = PBXGroup; children = ( + 3745DE0A2D53969000024FC8 /* HistoryDebugMenu.swift */, AAE75278263B046100B973F8 /* History.xcdatamodeld */, AAE7527B263B056C00B973F8 /* EncryptedHistoryStore.swift */, 85D0327A2B8E3D090041D1FB /* HistoryCoordinatorExtension.swift */, @@ -11591,6 +11595,7 @@ 37197EAB2942443D00394917 /* WebViewContainerView.swift in Sources */, 84F1C8DF2C774D4200716446 /* NSTableViewExtension.swift in Sources */, 3706FA88293F65D500E42796 /* Logger+Multiple.swift in Sources */, + 3745DE0C2D53969300024FC8 /* HistoryDebugMenu.swift in Sources */, 3706FA89293F65D500E42796 /* CrashReportPromptPresenter.swift in Sources */, 3706FA8B293F65D500E42796 /* PreferencesRootView.swift in Sources */, 31521AC12CC013AD00248E6F /* AIChatMenuVisibilityConfigurable.swift in Sources */, @@ -13779,6 +13784,7 @@ AAC6881B28626C1900D54247 /* RecentlyClosedWindow.swift in Sources */, 85707F2A276A35FE00DC0649 /* ActionSpeech.swift in Sources */, B6BE9FAA293F7955006363C6 /* ModalSheetCancellable.swift in Sources */, + 3745DE0B2D53969300024FC8 /* HistoryDebugMenu.swift in Sources */, B6830963274CDEC7004B46BB /* FireproofDomainsStore.swift in Sources */, F188268D2BBF01C300D9AC4F /* PixelDataModel.xcdatamodeld in Sources */, 7B430EA12A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift in Sources */, @@ -15678,7 +15684,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 235.2.0; + version = 235.3.0; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo-macOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo-macOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8541ac95eb..c2b45fe176 100644 --- a/DuckDuckGo-macOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo-macOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "744895ff98a5f2213333be0170e495b1e85034d0", - "version" : "235.2.0" + "revision" : "f3a9e71bb3be7dc53a74e2750d4282150e62798e", + "version" : "235.3.0" } }, { diff --git a/DuckDuckGo/History/Services/HistoryDebugMenu.swift b/DuckDuckGo/History/Services/HistoryDebugMenu.swift new file mode 100644 index 0000000000..3f9b84759d --- /dev/null +++ b/DuckDuckGo/History/Services/HistoryDebugMenu.swift @@ -0,0 +1,113 @@ +// +// HistoryDebugMenu.swift +// +// 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. +// 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 AppKit +import History + +final class HistoryDebugMenu: NSMenu { + + let historyCoordinator: HistoryCoordinating + + private let environmentMenu = NSMenu() + + init(historyCoordinator: HistoryCoordinating = HistoryCoordinator.shared) { + self.historyCoordinator = historyCoordinator + super.init(title: "") + + buildItems { + NSMenuItem( + title: "Add 10 history visits each day (10 domains)", + action: #selector(populateFakeHistory), + target: self, + representedObject: (10, FakeURLsPool.random10Domains) + ) + NSMenuItem( + title: "Populate 100 history visits each day (10 domains)", + action: #selector(populateFakeHistory), + target: self, + representedObject: (100, FakeURLsPool.random10Domains) + ) + NSMenuItem( + title: "Populate 100 history visits each day (200 domains – SLOW!)", + action: #selector(populateFakeHistory), + target: self, + representedObject: (100, FakeURLsPool.random200Domains) + ) + } + } + + required init(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc func populateFakeHistory(_ sender: NSMenuItem) { + guard let (maxVisitsPerDay, pool) = sender.representedObject as? (Int, FakeURLsPool) else { + return + } + Task.detached { + self.populateHistory(maxVisitsPerDay, pool.urls) + } + } + + private func populateHistory(_ maxVisitsPerDay: Int, _ urls: [URL]) { + var date = Date() + let endDate = Date.monthAgo + + var visitsPerDay = 0 + + while date > endDate { + guard let url = urls.randomElement() else { + continue + } + let visitDate = Date(timeIntervalSince1970: TimeInterval.random(in: date.startOfDay.timeIntervalSince1970..= maxVisitsPerDay { + date = date.daysAgo(1) + visitsPerDay = 0 + } + } + } + + enum FakeURLsPool { + case random10Domains + case random200Domains + + var urls: [URL] { + switch self { + case .random10Domains: + Self.fakeURLs10Domains + case .random200Domains: + Self.fakeURLs200Domains + } + } + + private static let fakeURLs10Domains: [URL] = generateFakeURLs(numberOfDomains: 10) + private static let fakeURLs200Domains: [URL] = generateFakeURLs(numberOfDomains: 200) + + private static func generateFakeURLs(numberOfDomains: Int) -> [URL] { + (0.. Date.weekAgo.startOfDay { firstWeek.append(grouping) - } else { + } else if !featureFlagger.isFeatureOn(.historyView) { older.append(grouping) } } @@ -269,7 +274,14 @@ final class HistoryMenu: NSMenu { // MARK: - Clear All History - private func addClearAllHistoryOnTheBottom() { + private func addClearAllAndShowHistoryOnTheBottom() { + if featureFlagger.isFeatureOn(.historyView) { + if showHistorySeparator.menu != nil { + removeItem(showHistorySeparator) + } + addItem(showHistorySeparator) + addItem(showHistoryMenuItem) + } if clearAllHistorySeparator.menu != nil { removeItem(clearAllHistorySeparator) } diff --git a/DuckDuckGo/Menus/MainMenu.swift b/DuckDuckGo/Menus/MainMenu.swift index ccc6539b1f..b688975e1f 100644 --- a/DuckDuckGo/Menus/MainMenu.swift +++ b/DuckDuckGo/Menus/MainMenu.swift @@ -659,6 +659,8 @@ final class MainMenu: NSMenu { NSMenuItem(title: "Shift New Tab daily impression", action: #selector(MainViewController.debugShiftNewTabOpeningDate)) NSMenuItem(title: "Shift \(AppearancePreferences.Constants.dismissNextStepsCardsAfterDays) days", action: #selector(MainViewController.debugShiftNewTabOpeningDateNtimes)) } + NSMenuItem(title: "History") + .submenu(HistoryDebugMenu()) NSMenuItem(title: "Reset Data") { NSMenuItem(title: "Reset Default Browser Prompt", action: #selector(MainViewController.resetDefaultBrowserPrompt)) NSMenuItem(title: "Reset Default Grammar Checks", action: #selector(MainViewController.resetDefaultGrammarChecks)) diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 830e92113e..f5d0ee95ae 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -29,7 +29,7 @@ let package = Package( targets: ["DataBrokerProtection"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "235.2.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "235.3.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../AppKitExtensions"), .package(path: "../XPCHelper"), diff --git a/LocalPackages/FeatureFlags/Package.swift b/LocalPackages/FeatureFlags/Package.swift index 4e4c268a74..bcc61de04f 100644 --- a/LocalPackages/FeatureFlags/Package.swift +++ b/LocalPackages/FeatureFlags/Package.swift @@ -32,7 +32,7 @@ let package = Package( targets: ["FeatureFlags"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "235.2.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "235.3.0"), ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. diff --git a/LocalPackages/HistoryView/Package.swift b/LocalPackages/HistoryView/Package.swift index f74adf87a4..0de44ecc63 100644 --- a/LocalPackages/HistoryView/Package.swift +++ b/LocalPackages/HistoryView/Package.swift @@ -32,7 +32,7 @@ let package = Package( targets: ["HistoryView"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "235.2.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "235.3.0"), .package(path: "../WebKitExtensions"), .package(path: "../UserScriptActionsManager"), .package(path: "../Utilities"), diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 5e798f9193..31c6c62b33 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -33,7 +33,7 @@ let package = Package( .library(name: "VPNAppLauncher", targets: ["VPNAppLauncher"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "235.2.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "235.3.0"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.3"), .package(path: "../AppLauncher"), .package(path: "../UDSHelper"), diff --git a/LocalPackages/NewTabPage/Package.swift b/LocalPackages/NewTabPage/Package.swift index 2d47eae307..48e6b34a86 100644 --- a/LocalPackages/NewTabPage/Package.swift +++ b/LocalPackages/NewTabPage/Package.swift @@ -32,7 +32,7 @@ let package = Package( targets: ["NewTabPage"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "235.2.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "235.3.0"), .package(path: "../WebKitExtensions"), .package(path: "../UserScriptActionsManager"), .package(path: "../Utilities"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 7b3ea07626..16721fa24a 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -13,7 +13,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "235.2.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "235.3.0"), .package(path: "../PreferencesUI-macOS"), .package(path: "../SwiftUIExtensions"), .package(path: "../FeatureFlags") diff --git a/LocalPackages/UserScriptActionsManager/Package.swift b/LocalPackages/UserScriptActionsManager/Package.swift index 1c0d399af1..95529fdf6d 100644 --- a/LocalPackages/UserScriptActionsManager/Package.swift +++ b/LocalPackages/UserScriptActionsManager/Package.swift @@ -31,7 +31,7 @@ let package = Package( targets: ["UserScriptActionsManager"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "235.2.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "235.3.0"), ], targets: [ .target( diff --git a/LocalPackages/WebKitExtensions/Package.swift b/LocalPackages/WebKitExtensions/Package.swift index 28a344be14..b67527b3cd 100644 --- a/LocalPackages/WebKitExtensions/Package.swift +++ b/LocalPackages/WebKitExtensions/Package.swift @@ -32,7 +32,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "235.2.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "235.3.0"), .package(path: "../AppKitExtensions") ], targets: [ diff --git a/UnitTests/History/Model/HistoryCoordinatingMock.swift b/UnitTests/History/Model/HistoryCoordinatingMock.swift index d16af67900..0e4b3ab94e 100644 --- a/UnitTests/History/Model/HistoryCoordinatingMock.swift +++ b/UnitTests/History/Model/HistoryCoordinatingMock.swift @@ -35,7 +35,7 @@ final class HistoryCoordinatingMock: HistoryCoordinating { var addVisitCalled = false var visit: Visit? - func addVisit(of url: URL) -> Visit? { + func addVisit(of url: URL, at date: Date) -> Visit? { addVisitCalled = true return visit } From e247f0884e447021837a6076dffde2ba187a5bb1 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Wed, 5 Feb 2025 12:00:36 -0800 Subject: [PATCH 13/22] Disable failing integration test (#3836) Task/Issue URL: https://app.asana.com/0/1199333091098016/1209334737154919 Tech Design URL: CC: **Description**: This PR disables a failing integration test. --- .../xcshareddata/xcschemes/macOS Browser App Store.xcscheme | 3 +++ .../xcshareddata/xcschemes/macOS Browser.xcscheme | 3 +++ .../Configurations/ConfigurationManagerIntegrationTests.swift | 1 + 3 files changed, 7 insertions(+) diff --git a/DuckDuckGo-macOS.xcodeproj/xcshareddata/xcschemes/macOS Browser App Store.xcscheme b/DuckDuckGo-macOS.xcodeproj/xcshareddata/xcschemes/macOS Browser App Store.xcscheme index 97eb66154a..0ebec12642 100644 --- a/DuckDuckGo-macOS.xcodeproj/xcshareddata/xcschemes/macOS Browser App Store.xcscheme +++ b/DuckDuckGo-macOS.xcodeproj/xcshareddata/xcschemes/macOS Browser App Store.xcscheme @@ -145,6 +145,9 @@ + + diff --git a/DuckDuckGo-macOS.xcodeproj/xcshareddata/xcschemes/macOS Browser.xcscheme b/DuckDuckGo-macOS.xcodeproj/xcshareddata/xcschemes/macOS Browser.xcscheme index 53ca4be731..850015cc8d 100644 --- a/DuckDuckGo-macOS.xcodeproj/xcshareddata/xcschemes/macOS Browser.xcscheme +++ b/DuckDuckGo-macOS.xcodeproj/xcshareddata/xcschemes/macOS Browser.xcscheme @@ -180,6 +180,9 @@ + + diff --git a/IntegrationTests/Configurations/ConfigurationManagerIntegrationTests.swift b/IntegrationTests/Configurations/ConfigurationManagerIntegrationTests.swift index 8505861f54..9e65c911b9 100644 --- a/IntegrationTests/Configurations/ConfigurationManagerIntegrationTests.swift +++ b/IntegrationTests/Configurations/ConfigurationManagerIntegrationTests.swift @@ -35,6 +35,7 @@ final class ConfigurationManagerIntegrationTests: XCTestCase { configManager = nil } + // Test temporarily disabled due to failure func testTdsAreFetchedFromURLBasedOnPrivacyConfigExperiment() async { // GIVEN await configManager.refreshNow() From 5c0a0376692c190c9f1d1138bc463a6b8c84fea7 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Wed, 5 Feb 2025 12:00:36 -0800 Subject: [PATCH 14/22] Disable failing integration test (#3836) Task/Issue URL: https://app.asana.com/0/1199333091098016/1209334737154919 Tech Design URL: CC: **Description**: This PR disables a failing integration test. --- .../xcshareddata/xcschemes/macOS Browser App Store.xcscheme | 3 +++ .../xcshareddata/xcschemes/macOS Browser.xcscheme | 3 +++ .../Configurations/ConfigurationManagerIntegrationTests.swift | 1 + 3 files changed, 7 insertions(+) diff --git a/DuckDuckGo-macOS.xcodeproj/xcshareddata/xcschemes/macOS Browser App Store.xcscheme b/DuckDuckGo-macOS.xcodeproj/xcshareddata/xcschemes/macOS Browser App Store.xcscheme index 97eb66154a..0ebec12642 100644 --- a/DuckDuckGo-macOS.xcodeproj/xcshareddata/xcschemes/macOS Browser App Store.xcscheme +++ b/DuckDuckGo-macOS.xcodeproj/xcshareddata/xcschemes/macOS Browser App Store.xcscheme @@ -145,6 +145,9 @@ + + diff --git a/DuckDuckGo-macOS.xcodeproj/xcshareddata/xcschemes/macOS Browser.xcscheme b/DuckDuckGo-macOS.xcodeproj/xcshareddata/xcschemes/macOS Browser.xcscheme index 53ca4be731..850015cc8d 100644 --- a/DuckDuckGo-macOS.xcodeproj/xcshareddata/xcschemes/macOS Browser.xcscheme +++ b/DuckDuckGo-macOS.xcodeproj/xcshareddata/xcschemes/macOS Browser.xcscheme @@ -180,6 +180,9 @@ + + diff --git a/IntegrationTests/Configurations/ConfigurationManagerIntegrationTests.swift b/IntegrationTests/Configurations/ConfigurationManagerIntegrationTests.swift index 8505861f54..9e65c911b9 100644 --- a/IntegrationTests/Configurations/ConfigurationManagerIntegrationTests.swift +++ b/IntegrationTests/Configurations/ConfigurationManagerIntegrationTests.swift @@ -35,6 +35,7 @@ final class ConfigurationManagerIntegrationTests: XCTestCase { configManager = nil } + // Test temporarily disabled due to failure func testTdsAreFetchedFromURLBasedOnPrivacyConfigExperiment() async { // GIVEN await configManager.refreshNow() From 9bb0049a4af8d1e884ff4dbd9ba386aee75d8902 Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Thu, 6 Feb 2025 18:16:05 +1100 Subject: [PATCH 15/22] Malicious Site Protection - Update BSK Ref (#3750) Task/Issue URL: https://app.asana.com/0/1206329551987282/1209158005098852/f Tech Design URL: https://app.asana.com/0/1206329551987282/1209133564224796/f **Description**: Integrate Malicious Site Protection changes as part of the iOS work. During iOS Ship Review we changed copy title for settings. I will double check if these changes needs to be integrated in this PR. **Optional E2E tests**: - [] Run PIR E2E tests Check this to run the Personal Information Removal end to end tests. If updating CCF, or any PIR related code, tick this. **Steps to test this PR**: 1. Ensure CI is green *Before Merging* * [ ] Update BSK Reference. * [x] Merge String Localizations (TBC). **Definition of Done**: * [x] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --- DuckDuckGo-macOS.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 12 ++++++------ DuckDuckGo/Common/Localizables/UserText.swift | 2 +- DuckDuckGo/Localizable.xcstrings | 18 +++++++++--------- .../MaliciousSiteProtectionManager.swift | 4 +++- .../DataBrokerProtection/Package.swift | 2 +- LocalPackages/FeatureFlags/Package.swift | 2 +- LocalPackages/HistoryView/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/NewTabPage/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- .../UserScriptActionsManager/Package.swift | 2 +- LocalPackages/WebKitExtensions/Package.swift | 2 +- .../Mocks/MaliciousSiteProtectionMocks.swift | 3 +-- 14 files changed, 29 insertions(+), 28 deletions(-) diff --git a/DuckDuckGo-macOS.xcodeproj/project.pbxproj b/DuckDuckGo-macOS.xcodeproj/project.pbxproj index 40822b172d..b3ee835263 100644 --- a/DuckDuckGo-macOS.xcodeproj/project.pbxproj +++ b/DuckDuckGo-macOS.xcodeproj/project.pbxproj @@ -15684,7 +15684,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 235.3.0; + version = 236.0.0; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo-macOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo-macOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index c2b45fe176..17f8aa4566 100644 --- a/DuckDuckGo-macOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo-macOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "f3a9e71bb3be7dc53a74e2750d4282150e62798e", - "version" : "235.3.0" + "revision" : "1169c5565a1d6e3091e93448a28485a736dfa6d4", + "version" : "236.0.0" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "revision" : "4a6bca2433aec573369546291bb6c53addef0a5f", - "version" : "7.15.0" + "revision" : "7a37fdc86198b0447e9ff17dbf494171bfaddc33", + "version" : "7.16.0" } }, { @@ -113,8 +113,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/privacy-dashboard", "state" : { - "revision" : "c52bd5d851b1f8f0482e82b8720852670f525497", - "version" : "8.1.0" + "revision" : "099f7ed5faac946e4d80746703aaaf87fdfbee09", + "version" : "8.3.0" } }, { diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index f3e4c94430..29233f5466 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -448,7 +448,7 @@ struct UserText { static let downloadsOpenPopupOnCompletion = NSLocalizedString("downloads.open.on.completion", value: "Automatically open the Downloads panel when downloads complete", comment: "Checkbox to open a Download Manager popover when downloads are completed") - static let maliciousSiteDetectionHeader = NSLocalizedString("phishing-detection.enabled.header", value: "Malicious Site Protection", comment: "Header for phishing site protection section in the settings page") + static let maliciousSiteDetectionHeader = NSLocalizedString("phishing-detection.enabled.header", value: "Site Safety Warnings", comment: "Header for phishing site protection section in the settings page") static let maliciousSiteDetectionIsEnabled = NSLocalizedString("phishing-detection.enabled.checkbox", value: "Warn me on sites flagged for phishing or malware.", comment: "Checkbox that enables or disables the phishing and malware detection feature in the browser") static let maliciousDetectionEnabledWarning = NSLocalizedString("phishing-detection.enabled.warning", value: "Disabling this feature can put your personal information at risk.", comment: "A description box to warn users away from disabling the phishing and malware protection feature") diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 5c98bd4375..584e1c701e 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -47692,55 +47692,55 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Schutz vor bösartigen Websites" + "value" : "Sicherheitswarnungen der Website" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "Malicious Site Protection" + "value" : "Site Safety Warnings" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Protección contra sitios maliciosos" + "value" : "Advertencias de seguridad del sitio" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Protection contre les sites malveillants" + "value" : "Avertissements de sécurité du site" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Protezione dai siti dannosi" + "value" : "Avvisi di sicurezza del sito" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Bescherming tegen schadelijke sites" + "value" : "Waarschuwingen over de veiligheid van de website" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Ochrona przed złośliwymi witrynami" + "value" : "Ostrzeżenia dotyczące bezpieczeństwa witryny" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Proteção contra sites maliciosos" + "value" : "Avisos de segurança do site" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Защита от вредоносных сайтов" + "value" : "Предупреждения о безопасности на сайте" } } } diff --git a/DuckDuckGo/MaliciousSiteProtection/MaliciousSiteProtectionManager.swift b/DuckDuckGo/MaliciousSiteProtection/MaliciousSiteProtectionManager.swift index 18e277a0d6..64fe2abc61 100644 --- a/DuckDuckGo/MaliciousSiteProtection/MaliciousSiteProtectionManager.swift +++ b/DuckDuckGo/MaliciousSiteProtection/MaliciousSiteProtectionManager.swift @@ -137,7 +137,7 @@ public class MaliciousSiteProtectionManager: MaliciousSiteDetecting { let apiEnvironment = apiEnvironment ?? MaliciousSiteDetector.APIEnvironment.production self.detector = detector ?? MaliciousSiteDetector(apiEnvironment: apiEnvironment, service: apiService, dataManager: dataManager, eventMapping: Self.debugEvents) - self.updateManager = MaliciousSiteProtection.UpdateManager(apiEnvironment: apiEnvironment, service: apiService, dataManager: dataManager, updateIntervalProvider: updateIntervalProvider ?? Self.updateInterval) + self.updateManager = MaliciousSiteProtection.UpdateManager(apiEnvironment: apiEnvironment, service: apiService, dataManager: dataManager, eventMapping: Self.debugEvents, updateIntervalProvider: updateIntervalProvider ?? Self.updateInterval) self.detectionPreferences = detectionPreferences self.setupBindings() @@ -151,6 +151,8 @@ public class MaliciousSiteProtectionManager: MaliciousSiteDetecting { .settingToggled, .matchesApiTimeout: PixelKit.fire(event) + case .failedToDownloadInitialDataSets: + PixelKit.fire(DebugEvent(event), frequency: .dailyAndCount) case .matchesApiFailure(let error): Logger.maliciousSiteProtection.error("Error fetching matches from API: \(error)") } diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index f5d0ee95ae..8083b7c093 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -29,7 +29,7 @@ let package = Package( targets: ["DataBrokerProtection"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "235.3.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "236.0.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../AppKitExtensions"), .package(path: "../XPCHelper"), diff --git a/LocalPackages/FeatureFlags/Package.swift b/LocalPackages/FeatureFlags/Package.swift index bcc61de04f..95ecd9f224 100644 --- a/LocalPackages/FeatureFlags/Package.swift +++ b/LocalPackages/FeatureFlags/Package.swift @@ -32,7 +32,7 @@ let package = Package( targets: ["FeatureFlags"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "235.3.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "236.0.0"), ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. diff --git a/LocalPackages/HistoryView/Package.swift b/LocalPackages/HistoryView/Package.swift index 0de44ecc63..2a3a54cdea 100644 --- a/LocalPackages/HistoryView/Package.swift +++ b/LocalPackages/HistoryView/Package.swift @@ -32,7 +32,7 @@ let package = Package( targets: ["HistoryView"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "235.3.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "236.0.0"), .package(path: "../WebKitExtensions"), .package(path: "../UserScriptActionsManager"), .package(path: "../Utilities"), diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 31c6c62b33..6a3b56c730 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -33,7 +33,7 @@ let package = Package( .library(name: "VPNAppLauncher", targets: ["VPNAppLauncher"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "235.3.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "236.0.0"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.3"), .package(path: "../AppLauncher"), .package(path: "../UDSHelper"), diff --git a/LocalPackages/NewTabPage/Package.swift b/LocalPackages/NewTabPage/Package.swift index 48e6b34a86..3e5b9d5929 100644 --- a/LocalPackages/NewTabPage/Package.swift +++ b/LocalPackages/NewTabPage/Package.swift @@ -32,7 +32,7 @@ let package = Package( targets: ["NewTabPage"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "235.3.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "236.0.0"), .package(path: "../WebKitExtensions"), .package(path: "../UserScriptActionsManager"), .package(path: "../Utilities"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 16721fa24a..ab3ac1438f 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -13,7 +13,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "235.3.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "236.0.0"), .package(path: "../PreferencesUI-macOS"), .package(path: "../SwiftUIExtensions"), .package(path: "../FeatureFlags") diff --git a/LocalPackages/UserScriptActionsManager/Package.swift b/LocalPackages/UserScriptActionsManager/Package.swift index 95529fdf6d..a20fdedb24 100644 --- a/LocalPackages/UserScriptActionsManager/Package.swift +++ b/LocalPackages/UserScriptActionsManager/Package.swift @@ -31,7 +31,7 @@ let package = Package( targets: ["UserScriptActionsManager"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "235.3.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "236.0.0"), ], targets: [ .target( diff --git a/LocalPackages/WebKitExtensions/Package.swift b/LocalPackages/WebKitExtensions/Package.swift index b67527b3cd..34e25d329d 100644 --- a/LocalPackages/WebKitExtensions/Package.swift +++ b/LocalPackages/WebKitExtensions/Package.swift @@ -32,7 +32,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "235.3.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "236.0.0"), .package(path: "../AppKitExtensions") ], targets: [ diff --git a/UnitTests/MaliciousSiteProtection/Mocks/MaliciousSiteProtectionMocks.swift b/UnitTests/MaliciousSiteProtection/Mocks/MaliciousSiteProtectionMocks.swift index 67da850a6d..1aa36cdb2c 100644 --- a/UnitTests/MaliciousSiteProtection/Mocks/MaliciousSiteProtectionMocks.swift +++ b/UnitTests/MaliciousSiteProtection/Mocks/MaliciousSiteProtectionMocks.swift @@ -70,10 +70,9 @@ class MockMaliciousSiteFileStore: MaliciousSiteProtection.FileStoring { var didWriteToDisk: Bool = false var didReadFromDisk: Bool = false - func write(data: Data, to filename: String) -> Bool { + func write(data: Data, to filename: String) throws { didWriteToDisk = true storage[filename] = data - return true } func read(from filename: String) -> Data? { From 7e64567f684ca696b217114a008cc51be46147b1 Mon Sep 17 00:00:00 2001 From: Anka Date: Thu, 6 Feb 2025 08:55:49 +0000 Subject: [PATCH 16/22] Bump version to 1.125.0 (358) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index ec68a8b19e..c64e7473c5 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 357 +CURRENT_PROJECT_VERSION = 358 From ecc894256b4a5a7418955b05368150359fcb9d5c Mon Sep 17 00:00:00 2001 From: Anka Date: Thu, 6 Feb 2025 09:20:20 +0000 Subject: [PATCH 17/22] Bump version to 1.125.0 (359) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index c64e7473c5..db47ca1149 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 358 +CURRENT_PROJECT_VERSION = 359 From 6a28610b51b95d5e908b15418baabbf691865a1b Mon Sep 17 00:00:00 2001 From: Brian Hall Date: Thu, 6 Feb 2025 14:56:59 +0100 Subject: [PATCH 18/22] Fix PeopleWhiz selector (#3839) Task/Issue URL: https://app.asana.com/0/1206873150423133/1209329833070078 Tech Design URL: CC: **Description**: Fixes the PeopleWhiz selector which was appending extra text to the name and causing matches to fail. **Optional E2E tests**: - [x] Run PIR E2E tests Check this to run the Personal Information Removal end to end tests. If updating CCF, or any PIR related code, tick this. **Steps to test this PR**: 1. Load into macOS, test in the debugger **Definition of Done**: * [x] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --- .../DataBrokerProtection/Resources/JSON/peoplewhiz.com.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhiz.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhiz.com.json index b07a9c6ce8..8295522ff0 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhiz.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhiz.com.json @@ -21,7 +21,7 @@ "noResultsSelector": "div[class^=ResultNotFound__NoResults]", "profile": { "name": { - "selector": "[class^='ResultsTable__Name-sc']" + "selector": ".//div[contains(@class, 'ResultsTable__Name')]/text()" }, "age": { "selector": "[class^='ResultsTable__Age-sc']" @@ -62,7 +62,7 @@ "selector": "[class^='ResultsTable__Record-sc']", "profile": { "name": { - "selector": "[class^='ResultsTable__Name-sc']" + "selector": ".//div[contains(@class, 'ResultsTable__Name')]/text()" }, "age": { "selector": "[class^='ResultsTable__Age-sc']" From 012d1065afc3fe8b6e5dd67b529b33b4677d5d1a Mon Sep 17 00:00:00 2001 From: Sabrina Tardio <44158575+SabrinaTardio@users.noreply.github.com> Date: Thu, 6 Feb 2025 16:06:09 +0100 Subject: [PATCH 19/22] reset experiment (#3832) Task/Issue URL: https://app.asana.com/0/1204186595873227/1209311973888722 **Description**: Restart onboarding experiment with contextual onboarding --- DuckDuckGo/Statistics/Experiment/PixelExperiment.swift | 4 ++-- DuckDuckGo/Tab/Model/Tab.swift | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo/Statistics/Experiment/PixelExperiment.swift b/DuckDuckGo/Statistics/Experiment/PixelExperiment.swift index 50c04dfd8c..2e92735157 100644 --- a/DuckDuckGo/Statistics/Experiment/PixelExperiment.swift +++ b/DuckDuckGo/Statistics/Experiment/PixelExperiment.swift @@ -60,8 +60,8 @@ enum PixelExperiment: String, CaseIterable { // These are the variants. Rename or add/remove them as needed. If you change the string value // remember to keep it clear for privacy triage. - case control = "oe" - case newOnboarding = "of" + case control = "og" + case newOnboarding = "oh" } // These functions contain the business logic for determining if the pixel should be fired or not. diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift index 02485acef3..1ac06086b4 100644 --- a/DuckDuckGo/Tab/Model/Tab.swift +++ b/DuckDuckGo/Tab/Model/Tab.swift @@ -808,7 +808,12 @@ protocol NewWindowPolicyDecisionMaker { return } #endif - setContent(PixelExperiment.cohort == .newOnboarding ? .onboarding : .onboardingDeprecated) + if PixelExperiment.cohort == .newOnboarding { + Application.appDelegate.onboardingStateMachine.state = .notStarted + setContent(.onboarding) + } else { + setContent(.onboardingDeprecated) + } } @MainActor(unsafe) From ab05170a00a13703997d83bb004c33c6caea4f4e Mon Sep 17 00:00:00 2001 From: Elle Sullivan Date: Thu, 6 Feb 2025 15:31:27 +0000 Subject: [PATCH 20/22] Add new empty PIR package to aid migration (#3835) Task/Issue URL: https://app.asana.com/0/1187352151074490/1209334604294772 Tech Design URL: CC: **Description**: Adds an empty and inert package for now to aid migration. See TD for details **Optional E2E tests**: - [x] Run PIR E2E tests Check this to run the Personal Information Removal end to end tests. If updating CCF, or any PIR related code, tick this. **Steps to test this PR**: 1. Test macOS still compiles and runs **Definition of Done**: * [x] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --------- Co-authored-by: Pete Smith <5278441+aataraxiaa@users.noreply.github.com> --- DuckDuckGo-macOS.xcodeproj/project.pbxproj | 2 + .../DataBrokerProtection/Package.swift | 2 + .../contents.xcworkspacedata | 7 ++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 +++ .../xcschemes/macOS PIR Tests.xcscheme | 53 ++++++++++++++ .../DataBrokerProtectionShared/Package.swift | 70 +++++++++++++++++++ .../DataBrokerProtectionBlank.swift | 19 +++++ .../Tests/.swiftlint.yml | 17 +++++ .../DataBrokerProtectionBlankTests.swift | 19 +++++ 9 files changed, 197 insertions(+) create mode 100644 LocalPackages/DataBrokerProtectionShared/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata create mode 100644 LocalPackages/DataBrokerProtectionShared/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 LocalPackages/DataBrokerProtectionShared/.swiftpm/xcode/xcshareddata/xcschemes/macOS PIR Tests.xcscheme create mode 100644 LocalPackages/DataBrokerProtectionShared/Package.swift create mode 100644 LocalPackages/DataBrokerProtectionShared/Sources/DataBrokerProtectionShared/DataBrokerProtectionBlank.swift create mode 100644 LocalPackages/DataBrokerProtectionShared/Tests/.swiftlint.yml create mode 100644 LocalPackages/DataBrokerProtectionShared/Tests/DataBrokerProtectionSharedTests/DataBrokerProtectionBlankTests.swift diff --git a/DuckDuckGo-macOS.xcodeproj/project.pbxproj b/DuckDuckGo-macOS.xcodeproj/project.pbxproj index b3ee835263..cc020075fb 100644 --- a/DuckDuckGo-macOS.xcodeproj/project.pbxproj +++ b/DuckDuckGo-macOS.xcodeproj/project.pbxproj @@ -4469,6 +4469,7 @@ 9D84E3F42CD4E6660046CD8B /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9D84E43C2CD4E66F0046CD8B /* DBPE2ETests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DBPE2ETests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 9D8FA00B2AC5BDCE005DD0D0 /* LoginItem+DataBrokerProtection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LoginItem+DataBrokerProtection.swift"; sourceTree = ""; }; + 9D9150F32D53F1F1005B5833 /* DataBrokerProtectionShared */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = DataBrokerProtectionShared; sourceTree = ""; }; 9D9AE8682AA76CDC0026E7DC /* LoginItem+NetworkProtection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LoginItem+NetworkProtection.swift"; sourceTree = ""; }; 9D9AE86A2AA76CF90026E7DC /* LoginItemsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginItemsManager.swift; sourceTree = ""; }; 9D9AE8D12AAA39A70026E7DC /* DuckDuckGo Personal Information Removal.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "DuckDuckGo Personal Information Removal.app"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -6220,6 +6221,7 @@ 9D9DE5712C63A96400D20B15 /* AppKitExtensions */, 7B9167A82C09E88800322310 /* AppLauncher */, 378E279D2970217400FCADA2 /* BuildToolPlugins */, + 9D9150F32D53F1F1005B5833 /* DataBrokerProtectionShared */, 3192A2702A4C4E330084EA89 /* DataBrokerProtection */, 7B8FDD1E2CDD877000720907 /* FeatureFlags */, 9DF2DB592C73B52F0025F43C /* Freemium */, diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 8083b7c093..a91f76a058 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -30,6 +30,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "236.0.0"), + .package(path: "../DataBrokerProtectionShared"), .package(path: "../SwiftUIExtensions"), .package(path: "../AppKitExtensions"), .package(path: "../XPCHelper"), @@ -40,6 +41,7 @@ let package = Package( name: "DataBrokerProtection", dependencies: [ .product(name: "BrowserServicesKit", package: "BrowserServicesKit"), + .product(name: "DataBrokerProtectionShared", package: "DataBrokerProtectionShared"), .product(name: "SwiftUIExtensions", package: "SwiftUIExtensions"), .product(name: "AppKitExtensions", package: "AppKitExtensions"), .byName(name: "XPCHelper"), diff --git a/LocalPackages/DataBrokerProtectionShared/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/LocalPackages/DataBrokerProtectionShared/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..919434a625 --- /dev/null +++ b/LocalPackages/DataBrokerProtectionShared/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/LocalPackages/DataBrokerProtectionShared/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/LocalPackages/DataBrokerProtectionShared/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/LocalPackages/DataBrokerProtectionShared/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/LocalPackages/DataBrokerProtectionShared/.swiftpm/xcode/xcshareddata/xcschemes/macOS PIR Tests.xcscheme b/LocalPackages/DataBrokerProtectionShared/.swiftpm/xcode/xcshareddata/xcschemes/macOS PIR Tests.xcscheme new file mode 100644 index 0000000000..5ba440572a --- /dev/null +++ b/LocalPackages/DataBrokerProtectionShared/.swiftpm/xcode/xcshareddata/xcschemes/macOS PIR Tests.xcscheme @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/LocalPackages/DataBrokerProtectionShared/Package.swift b/LocalPackages/DataBrokerProtectionShared/Package.swift new file mode 100644 index 0000000000..f804fc6bf7 --- /dev/null +++ b/LocalPackages/DataBrokerProtectionShared/Package.swift @@ -0,0 +1,70 @@ +// swift-tools-version: 5.7 +// The swift-tools-version declares the minimum version of Swift required to build this package. +// +// Package.swift +// +// 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. +// 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 PackageDescription + +let package = Package( + name: "DataBrokerProtectionShared", + platforms: [ .macOS("11.4") ], + products: [ + .library( + name: "DataBrokerProtectionShared", + targets: ["DataBrokerProtectionShared"]) + ], + dependencies: [ + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "236.0.0"), + .package(path: "../SwiftUIExtensions"), + .package(path: "../AppKitExtensions"), + .package(path: "../XPCHelper"), + .package(path: "../Freemium"), + ], + targets: [ + .target( + name: "DataBrokerProtectionShared", + dependencies: [ + .product(name: "BrowserServicesKit", package: "BrowserServicesKit"), + .product(name: "SwiftUIExtensions", package: "SwiftUIExtensions"), + .product(name: "AppKitExtensions", package: "AppKitExtensions"), + .byName(name: "XPCHelper"), + .product(name: "PixelKit", package: "BrowserServicesKit"), + .product(name: "Configuration", package: "BrowserServicesKit"), + .product(name: "Persistence", package: "BrowserServicesKit"), + .product(name: "Freemium", package: "Freemium"), + ], + resources: [.copy("Resources")], + swiftSettings: [ + .define("DEBUG", .when(configuration: .debug)) + ] + ), + .testTarget( + name: "DataBrokerProtectionSharedTests", + dependencies: [ + "DataBrokerProtectionShared", + "BrowserServicesKit", + "Freemium", + .product(name: "PersistenceTestingUtils", package: "BrowserServicesKit"), + .product(name: "SubscriptionTestingUtilities", package: "BrowserServicesKit"), + ], + resources: [ + .copy("Resources") + ] + ) + ] +) diff --git a/LocalPackages/DataBrokerProtectionShared/Sources/DataBrokerProtectionShared/DataBrokerProtectionBlank.swift b/LocalPackages/DataBrokerProtectionShared/Sources/DataBrokerProtectionShared/DataBrokerProtectionBlank.swift new file mode 100644 index 0000000000..058d5a708e --- /dev/null +++ b/LocalPackages/DataBrokerProtectionShared/Sources/DataBrokerProtectionShared/DataBrokerProtectionBlank.swift @@ -0,0 +1,19 @@ +// +// DataBrokerProtectionBlank.swift +// +// 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. +// 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 Foundation diff --git a/LocalPackages/DataBrokerProtectionShared/Tests/.swiftlint.yml b/LocalPackages/DataBrokerProtectionShared/Tests/.swiftlint.yml new file mode 100644 index 0000000000..bf8a5655d9 --- /dev/null +++ b/LocalPackages/DataBrokerProtectionShared/Tests/.swiftlint.yml @@ -0,0 +1,17 @@ +disabled_rules: + - file_length + - unused_closure_parameter + - type_name + - force_cast + - force_try + - function_body_length + - cyclomatic_complexity + - identifier_name + - blanket_disable_command + - type_body_length + - explicit_non_final_class + - enforce_os_log_wrapper + +large_tuple: + warning: 6 + error: 10 diff --git a/LocalPackages/DataBrokerProtectionShared/Tests/DataBrokerProtectionSharedTests/DataBrokerProtectionBlankTests.swift b/LocalPackages/DataBrokerProtectionShared/Tests/DataBrokerProtectionSharedTests/DataBrokerProtectionBlankTests.swift new file mode 100644 index 0000000000..4806322f53 --- /dev/null +++ b/LocalPackages/DataBrokerProtectionShared/Tests/DataBrokerProtectionSharedTests/DataBrokerProtectionBlankTests.swift @@ -0,0 +1,19 @@ +// +// DataBrokerProtectionBlankTests.swift +// +// 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. +// 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 Foundation From 347b6828a5d299b3262ec733393d5dde928dac08 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Fri, 7 Feb 2025 10:18:46 +0100 Subject: [PATCH 21/22] Exclude child binaries (#3824) Task/Issue URL: https://app.asana.com/0/1206580121312550/1209309062840626/f ## Description See [Figma](https://www.figma.com/design/y7g8d3Nuefhfedq4638Rhu/VPN%3A-Domain-and-App-exclusions-on-Windows?node-id=134-18153&p=f&m=dev) for reference (keep in mind this is a Windows Figma, and there's no macOS one). Changes: - When a routing-rule is applied to an app through the VPN, its embedded binaries will be subjected to the same rules. --- .../BothAppTargets/VPNURLEventHandler.swift | 10 ++ .../ExcludedApps/ExcludedAppsModel.swift | 2 +- .../AppInfoRetriever/AppInfoRetriever.swift | 112 +++++++++++++++++- .../NetworkProtectionMac/Package.swift | 2 + .../Exclusions/AppRoutingRulesManager.swift | 73 ++++++++++++ .../Provider/TransparentProxyProvider.swift | 6 +- 6 files changed, 199 insertions(+), 6 deletions(-) create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Exclusions/AppRoutingRulesManager.swift diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNURLEventHandler.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNURLEventHandler.swift index 064b20bd8f..e63932f4f2 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNURLEventHandler.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNURLEventHandler.swift @@ -90,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/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 4124a2fe65..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,6 +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), @@ -94,4 +170,32 @@ public class AppInfoRetriever: AppInfoRetrieveing { } return nil } + + // 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 + + guard let enumerator = fileManager.enumerator(at: bundleURL, + includingPropertiesForKeys: nil, + options: [.skipsHiddenFiles], + errorHandler: nil) else { + return [] + } + + 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) + } + } + + return Set(bundleIDs) + } } diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 6a3b56c730..67057ff5ae 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: "236.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..e850c25da5 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Exclusions/AppRoutingRulesManager.swift @@ -0,0 +1,73 @@ +// +// AppRoutingRulesManager.swift +// +// 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. +// 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. +/// +/// 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 AppRoutingRulesManager { + + private let appInfoRetriever: AppInfoRetrieving + private(set) var rules: VPNAppRoutingRules + private var cancellables = Set() + + init(settings: TransparentProxySettings, + appInfoRetriever: AppInfoRetrieving = AppInfoRetriever()) { + + self.appInfoRetriever = appInfoRetriever + self.rules = Self.expandAppRoutingRules(settings.appRoutingRules, appInfoRetriever: appInfoRetriever) + + subscribeToAppRoutingRulesChanges(settings) + } + + static func expandAppRoutingRules(_ rules: VPNAppRoutingRules, + appInfoRetriever: AppInfoRetrieving) -> VPNAppRoutingRules { + + var expandedRules = rules + + for (bundleID, rule) in rules { + guard let bundleURL = appInfoRetriever.getAppURL(bundleID: bundleID) else { + continue + } + + let embeddedAppBundleIDs = appInfoRetriever.findEmbeddedBundleIDs(in: bundleURL) + + for childBundleID in embeddedAppBundleIDs { + expandedRules[childBundleID] = rule + } + } + + return expandedRules + } + + private func subscribeToAppRoutingRulesChanges(_ settings: TransparentProxySettings) { + settings.appRoutingRulesPublisher + .receive(on: DispatchQueue.main) + .map { [appInfoRetriever] rules in + return Self.expandAppRoutingRules(rules, appInfoRetriever: appInfoRetriever) + } + .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 077c3b2727..92d1ceada2 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 appRoutingRulesManager: AppRoutingRulesManager 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 + appRoutingRulesManager = AppRoutingRulesManager(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 appRoutingRulesManager.rules[appIdentifier] { case .none: if let hostname = flow.remoteHostname, isExcludedDomain(hostname) { From bb86771e20ef7a4f31ab616ab7f5aecd498f7e7a Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Fri, 7 Feb 2025 11:23:26 +0100 Subject: [PATCH 22/22] VPN is sometimes stopped twice (#3829) Task/Issue URL: https://app.asana.com/0/1207603085593419/1209322889682604/f iOS PR: https://github.com/duckduckgo/iOS/pull/3928 BSK PR: https://github.com/duckduckgo/BrowserServicesKit/pull/1213 **Description**: Fixes an issue where the tunnel is being stopped twice. --- DuckDuckGo-macOS.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 8 ++++---- .../Experiment/PixelExperiment.swift | 2 +- .../DataBrokerProtection/Package.swift | 2 +- .../DataBrokerProtectionShared/Package.swift | 2 +- LocalPackages/FeatureFlags/Package.swift | 2 +- LocalPackages/HistoryView/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/NewTabPage/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- .../UserScriptActionsManager/Package.swift | 2 +- LocalPackages/WebKitExtensions/Package.swift | 2 +- ...ptionPagesUseSubscriptionFeatureTests.swift | 18 +++++++++--------- 13 files changed, 24 insertions(+), 24 deletions(-) diff --git a/DuckDuckGo-macOS.xcodeproj/project.pbxproj b/DuckDuckGo-macOS.xcodeproj/project.pbxproj index cc020075fb..9399123b5b 100644 --- a/DuckDuckGo-macOS.xcodeproj/project.pbxproj +++ b/DuckDuckGo-macOS.xcodeproj/project.pbxproj @@ -15686,7 +15686,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 236.0.0; + version = 236.0.1; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo-macOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo-macOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 17f8aa4566..1f575d93a9 100644 --- a/DuckDuckGo-macOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo-macOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "1169c5565a1d6e3091e93448a28485a736dfa6d4", - "version" : "236.0.0" + "revision" : "ab64a6616c7b726a55b6c67c0da421c636db1224", + "version" : "236.0.1" } }, { @@ -158,8 +158,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-crypto.git", "state" : { - "revision" : "ff0f781cf7c6a22d52957e50b104f5768b50c779", - "version" : "3.10.0" + "revision" : "f2f3774fd116a305136b6866e5e7cb7dff39d8f2", + "version" : "3.10.1" } }, { diff --git a/DuckDuckGo/Statistics/Experiment/PixelExperiment.swift b/DuckDuckGo/Statistics/Experiment/PixelExperiment.swift index 2e92735157..dc1c05b1c3 100644 --- a/DuckDuckGo/Statistics/Experiment/PixelExperiment.swift +++ b/DuckDuckGo/Statistics/Experiment/PixelExperiment.swift @@ -287,7 +287,7 @@ final internal class PixelExperimentLogic { DispatchQueue.main.async { let now = self.now() - if now >= AppDelegate.firstLaunchDate.adding(.days(21)) && now <= AppDelegate.firstLaunchDate.adding(.days(27)) { + if now >= AppDelegate.firstLaunchDate.addingTimeInterval(.days(21)) && now <= AppDelegate.firstLaunchDate.addingTimeInterval(.days(27)) { PixelKit.fire(GeneralPixel.serpDay21to27(cohort: cohort.rawValue), frequency: .legacyInitial, includeAppVersionParameter: false) } } diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index a91f76a058..a81b00cea4 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -29,7 +29,7 @@ let package = Package( targets: ["DataBrokerProtection"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "236.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "236.0.1"), .package(path: "../DataBrokerProtectionShared"), .package(path: "../SwiftUIExtensions"), .package(path: "../AppKitExtensions"), diff --git a/LocalPackages/DataBrokerProtectionShared/Package.swift b/LocalPackages/DataBrokerProtectionShared/Package.swift index f804fc6bf7..727eca6597 100644 --- a/LocalPackages/DataBrokerProtectionShared/Package.swift +++ b/LocalPackages/DataBrokerProtectionShared/Package.swift @@ -29,7 +29,7 @@ let package = Package( targets: ["DataBrokerProtectionShared"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "236.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "236.0.1"), .package(path: "../SwiftUIExtensions"), .package(path: "../AppKitExtensions"), .package(path: "../XPCHelper"), diff --git a/LocalPackages/FeatureFlags/Package.swift b/LocalPackages/FeatureFlags/Package.swift index 95ecd9f224..2a7642fe18 100644 --- a/LocalPackages/FeatureFlags/Package.swift +++ b/LocalPackages/FeatureFlags/Package.swift @@ -32,7 +32,7 @@ let package = Package( targets: ["FeatureFlags"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "236.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "236.0.1"), ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. diff --git a/LocalPackages/HistoryView/Package.swift b/LocalPackages/HistoryView/Package.swift index 2a3a54cdea..69037aa789 100644 --- a/LocalPackages/HistoryView/Package.swift +++ b/LocalPackages/HistoryView/Package.swift @@ -32,7 +32,7 @@ let package = Package( targets: ["HistoryView"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "236.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "236.0.1"), .package(path: "../WebKitExtensions"), .package(path: "../UserScriptActionsManager"), .package(path: "../Utilities"), diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 67057ff5ae..7c0f269380 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -33,7 +33,7 @@ let package = Package( .library(name: "VPNAppLauncher", targets: ["VPNAppLauncher"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "236.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "236.0.1"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.3"), .package(path: "../AppInfoRetriever"), .package(path: "../AppLauncher"), diff --git a/LocalPackages/NewTabPage/Package.swift b/LocalPackages/NewTabPage/Package.swift index 3e5b9d5929..e38eb09218 100644 --- a/LocalPackages/NewTabPage/Package.swift +++ b/LocalPackages/NewTabPage/Package.swift @@ -32,7 +32,7 @@ let package = Package( targets: ["NewTabPage"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "236.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "236.0.1"), .package(path: "../WebKitExtensions"), .package(path: "../UserScriptActionsManager"), .package(path: "../Utilities"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index ab3ac1438f..2097829049 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -13,7 +13,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "236.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "236.0.1"), .package(path: "../PreferencesUI-macOS"), .package(path: "../SwiftUIExtensions"), .package(path: "../FeatureFlags") diff --git a/LocalPackages/UserScriptActionsManager/Package.swift b/LocalPackages/UserScriptActionsManager/Package.swift index a20fdedb24..6af1e94405 100644 --- a/LocalPackages/UserScriptActionsManager/Package.swift +++ b/LocalPackages/UserScriptActionsManager/Package.swift @@ -31,7 +31,7 @@ let package = Package( targets: ["UserScriptActionsManager"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "236.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "236.0.1"), ], targets: [ .target( diff --git a/LocalPackages/WebKitExtensions/Package.swift b/LocalPackages/WebKitExtensions/Package.swift index 34e25d329d..de580b0dba 100644 --- a/LocalPackages/WebKitExtensions/Package.swift +++ b/LocalPackages/WebKitExtensions/Package.swift @@ -32,7 +32,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "236.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "236.0.1"), .package(path: "../AppKitExtensions") ], targets: [ diff --git a/UnitTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift b/UnitTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift index f72b2cd866..375c9b8737 100644 --- a/UnitTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift +++ b/UnitTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift @@ -426,7 +426,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { storePurchaseManager.purchaseSubscriptionResult = .success(Constants.mostRecentTransactionJWS) subscriptionService.confirmPurchaseResult = .success(ConfirmPurchaseResponse(email: Constants.email, entitlements: Constants.entitlements, - subscription: SubscriptionMockFactory.subscription)) + subscription: SubscriptionMockFactory.appleSubscription)) authService.storeLoginResult = .success(StoreLoginResponse(authToken: Constants.authToken, email: Constants.email, externalID: Constants.externalID, @@ -470,7 +470,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { storePurchaseManager.purchaseSubscriptionResult = .success(Constants.mostRecentTransactionJWS) subscriptionService.confirmPurchaseResult = .success(ConfirmPurchaseResponse(email: Constants.email, entitlements: Constants.entitlements, - subscription: SubscriptionMockFactory.subscription)) + subscription: SubscriptionMockFactory.appleSubscription)) authService.storeLoginResult = .success(StoreLoginResponse(authToken: Constants.authToken, email: Constants.email, externalID: Constants.externalID, @@ -516,7 +516,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { storePurchaseManager.purchaseSubscriptionResult = .success(Constants.mostRecentTransactionJWS) subscriptionService.confirmPurchaseResult = .success(ConfirmPurchaseResponse(email: Constants.email, entitlements: Constants.entitlements, - subscription: SubscriptionMockFactory.subscription)) + subscription: SubscriptionMockFactory.appleSubscription)) // When let subscriptionSelectedParams = ["id": "some-subscription-id"] @@ -547,7 +547,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { storePurchaseManager.purchaseSubscriptionResult = .success(Constants.mostRecentTransactionJWS) subscriptionService.confirmPurchaseResult = .success(ConfirmPurchaseResponse(email: Constants.email, entitlements: Constants.entitlements, - subscription: SubscriptionMockFactory.subscription)) + subscription: SubscriptionMockFactory.appleSubscription)) // When let subscriptionSelectedParams = ["id": "some-subscription-id"] @@ -609,7 +609,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { status: "authenticated")) authService.getAccessTokenResult = .success(AccessTokenResponse(accessToken: Constants.accessToken)) authService.validateTokenResult = .success(Constants.validateTokenResponse) - subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.subscription) + subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.appleSubscription) // When let subscriptionSelectedParams = ["id": "some-subscription-id"] @@ -1009,7 +1009,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { storePurchaseManager.purchaseSubscriptionResult = .success(Constants.mostRecentTransactionJWS) subscriptionService.confirmPurchaseResult = .success(ConfirmPurchaseResponse(email: Constants.email, entitlements: Constants.entitlements, - subscription: SubscriptionMockFactory.subscription)) + subscription: SubscriptionMockFactory.appleSubscription)) authService.storeLoginResult = .success(StoreLoginResponse(authToken: Constants.authToken, email: Constants.email, externalID: Constants.externalID, @@ -1048,7 +1048,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { storePurchaseManager.purchaseSubscriptionResult = .success(Constants.mostRecentTransactionJWS) subscriptionService.confirmPurchaseResult = .success(ConfirmPurchaseResponse(email: Constants.email, entitlements: Constants.entitlements, - subscription: SubscriptionMockFactory.subscription)) + subscription: SubscriptionMockFactory.appleSubscription)) authService.storeLoginResult = .success(StoreLoginResponse(authToken: Constants.authToken, email: Constants.email, externalID: Constants.externalID, @@ -1088,7 +1088,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { storePurchaseManager.purchaseSubscriptionResult = .success(Constants.mostRecentTransactionJWS) subscriptionService.confirmPurchaseResult = .success(ConfirmPurchaseResponse(email: Constants.email, entitlements: Constants.entitlements, - subscription: SubscriptionMockFactory.subscription)) + subscription: SubscriptionMockFactory.appleSubscription)) authService.storeLoginResult = .success(StoreLoginResponse(authToken: Constants.authToken, email: Constants.email, externalID: Constants.externalID, @@ -1131,7 +1131,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { storePurchaseManager.purchaseSubscriptionResult = .success(Constants.mostRecentTransactionJWS) subscriptionService.confirmPurchaseResult = .success(ConfirmPurchaseResponse(email: Constants.email, entitlements: Constants.entitlements, - subscription: SubscriptionMockFactory.subscription)) + subscription: SubscriptionMockFactory.appleSubscription)) mockFreemiumDBPUserStateManager.didPostFirstProfileSavedNotification = false feature.with(broker: broker)