From 069be15c6a298a59573afba32130f4eee4f21d3d Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Fri, 15 Dec 2023 13:34:45 +0100 Subject: [PATCH 1/9] Move release task to proper section in Code Freeze workflow (#1977) Task/Issue URL: https://app.asana.com/0/1203301625297703/1206152126256397/f --- .github/workflows/code_freeze.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/code_freeze.yml b/.github/workflows/code_freeze.yml index 7883f9200c..bd23a9604b 100644 --- a/.github/workflows/code_freeze.yml +++ b/.github/workflows/code_freeze.yml @@ -58,6 +58,12 @@ jobs: | jq -r .data.new_task.gid)" echo "asana_task_url=https://app.asana.com/0/0/${asana_task_id}/f" >> $GITHUB_OUTPUT + curl -fLSs -X POST "https://app.asana.com/api/1.0/sections/${{ vars.MACOS_APP_DEVELOPMENT_RELEASE_SECTION_ID }}/addTask" \ + -H "Authorization: Bearer ${{ env.ASANA_ACCESS_TOKEN }}" \ + -H "Content-Type: application/json" \ + --output /dev/null \ + -d "{\"data\": {\"task\": \"${asana_task_id}\"}}" + assignee_id="$(curl -fLSs https://raw.githubusercontent.com/duckduckgo/BrowserServicesKit/main/.github/actions/asana-failed-pr-checks/user_ids.json \ | jq -r .${{ github.actor }})" From d45bc8fb653e727bff14f4910d009c6d3254666d Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Fri, 15 Dec 2023 10:29:23 -0300 Subject: [PATCH 2/9] DBP: Send internal user param for dbp waitlist pixels (#1972) --- DuckDuckGo.xcodeproj/project.pbxproj | 8 +++ .../DBP/DataBrokerProtectionAppEvents.swift | 12 ++--- ...okerProtectionExternalWaitlistPixels.swift | 50 +++++++++++++++++++ ...ataBrokerProtectionFeatureVisibility.swift | 2 +- .../NavigationBar/View/MoreOptionsMenu.swift | 2 +- .../WaitlistViewControllerPresenter.swift | 2 +- DuckDuckGo/Waitlist/Waitlist.swift | 4 +- ...tlistTermsAndConditionsActionHandler.swift | 4 +- ...perationPreferredDateCalculatorTests.swift | 2 +- 9 files changed, 68 insertions(+), 18 deletions(-) create mode 100644 DuckDuckGo/DBP/DataBrokerProtectionExternalWaitlistPixels.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index f08acde17e..db86c038c6 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2920,6 +2920,9 @@ B6FA893D269C423100588ECD /* PrivacyDashboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B6FA893C269C423100588ECD /* PrivacyDashboard.storyboard */; }; B6FA893F269C424500588ECD /* PrivacyDashboardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6FA893E269C424500588ECD /* PrivacyDashboardViewController.swift */; }; B6FA8941269C425400588ECD /* PrivacyDashboardPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6FA8940269C425400588ECD /* PrivacyDashboardPopover.swift */; }; + BBDFDC5A2B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBDFDC592B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift */; }; + BBDFDC5C2B2B8D7000F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBDFDC592B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift */; }; + BBDFDC5D2B2B8E2100F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBDFDC592B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift */; }; CB24F70C29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB24F70B29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift */; }; CB24F70D29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB24F70B29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift */; }; CB6BCDF927C6BEFF00CC76DC /* PrivacyFeatures.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB6BCDF827C6BEFF00CC76DC /* PrivacyFeatures.swift */; }; @@ -4177,6 +4180,7 @@ B6FA893C269C423100588ECD /* PrivacyDashboard.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = PrivacyDashboard.storyboard; sourceTree = ""; }; B6FA893E269C424500588ECD /* PrivacyDashboardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardViewController.swift; sourceTree = ""; }; B6FA8940269C425400588ECD /* PrivacyDashboardPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardPopover.swift; sourceTree = ""; }; + BBDFDC592B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionExternalWaitlistPixels.swift; sourceTree = ""; }; CB24F70B29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfigurationURLProvider.swift; sourceTree = ""; }; CB6BCDF827C6BEFF00CC76DC /* PrivacyFeatures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyFeatures.swift; sourceTree = ""; }; CBDD5DE229A67F2700832877 /* MockConfigurationStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockConfigurationStore.swift; sourceTree = ""; }; @@ -4598,6 +4602,7 @@ 31C5FFB82AF64D120008A79F /* DataBrokerProtectionFeatureVisibility.swift */, 3199C6F82AF94F5B002A7BA1 /* DataBrokerProtectionFeatureDisabler.swift */, 3199C6FC2AF97367002A7BA1 /* DataBrokerProtectionAppEvents.swift */, + BBDFDC592B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift */, ); path = DBP; sourceTree = ""; @@ -9202,6 +9207,7 @@ 3706FAFD293F65D500E42796 /* DownloadsPopover.swift in Sources */, 3706FAFE293F65D500E42796 /* SpacerNode.swift in Sources */, 3706FB00293F65D500E42796 /* PasswordManagementCreditCardModel.swift in Sources */, + BBDFDC5D2B2B8E2100F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift in Sources */, 3706FB01293F65D500E42796 /* NSEventExtension.swift in Sources */, 3706FB02293F65D500E42796 /* Onboarding.swift in Sources */, 4B9DB0482A983B24000927DB /* WaitlistRootView.swift in Sources */, @@ -10544,6 +10550,7 @@ 4B957AC42AC7AE700062CA31 /* BWVault.swift in Sources */, 4B957AC52AC7AE700062CA31 /* NSViewExtension.swift in Sources */, 4B957AC62AC7AE700062CA31 /* Preferences.swift in Sources */, + BBDFDC5C2B2B8D7000F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift in Sources */, 4B957AC72AC7AE700062CA31 /* DownloadListViewModel.swift in Sources */, 4B957AC82AC7AE700062CA31 /* BookmarkManagementDetailViewController.swift in Sources */, 4B957AC92AC7AE700062CA31 /* CSVImporter.swift in Sources */, @@ -11032,6 +11039,7 @@ 3158B1502B0BF75200AF130C /* DataBrokerProtectionLoginItemScheduler.swift in Sources */, B684592225C93BE000DC17B6 /* Publisher.asVoid.swift in Sources */, 4B9DB01D2A983B24000927DB /* Waitlist.swift in Sources */, + BBDFDC5A2B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift in Sources */, AAA0CC33252F181A0079BC96 /* NavigationButtonMenuDelegate.swift in Sources */, AAC30A2A268E239100D2D9CD /* CrashReport.swift in Sources */, 1D6A492029CF7A490011DF74 /* NSPopoverExtension.swift in Sources */, diff --git a/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift b/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift index f1ce0ade57..f462671ae7 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift @@ -63,13 +63,9 @@ struct DataBrokerProtectionAppEvents { if DataBrokerProtectionWaitlist().readyToAcceptTermsAndConditions { switch source { case .cardUI: - DailyPixel.fire(pixel: .dataBrokerProtectionWaitlistCardUITapped, - frequency: .dailyAndCount, - includeAppVersionParameter: true) + DataBrokerProtectionExternalWaitlistPixels.fire(pixel: .dataBrokerProtectionWaitlistCardUITapped, frequency: .dailyAndCount) case .localPush: - DailyPixel.fire(pixel: .dataBrokerProtectionWaitlistNotificationTapped, - frequency: .dailyAndCount, - includeAppVersionParameter: true) + DataBrokerProtectionExternalWaitlistPixels.fire(pixel: .dataBrokerProtectionWaitlistNotificationTapped, frequency: .dailyAndCount) } DataBrokerProtectionWaitlistViewControllerPresenter.show() @@ -82,9 +78,7 @@ struct DataBrokerProtectionAppEvents { private func sendActiveDataBrokerProtectionWaitlistUserPixel() { if DefaultDataBrokerProtectionFeatureVisibility().waitlistIsOngoing { - DailyPixel.fire(pixel: .dataBrokerProtectionWaitlistUserActive, - frequency: .dailyOnly, - includeAppVersionParameter: true) + DataBrokerProtectionExternalWaitlistPixels.fire(pixel: .dataBrokerProtectionWaitlistUserActive, frequency: .dailyOnly) } } diff --git a/DuckDuckGo/DBP/DataBrokerProtectionExternalWaitlistPixels.swift b/DuckDuckGo/DBP/DataBrokerProtectionExternalWaitlistPixels.swift new file mode 100644 index 0000000000..3ddbb6e016 --- /dev/null +++ b/DuckDuckGo/DBP/DataBrokerProtectionExternalWaitlistPixels.swift @@ -0,0 +1,50 @@ +// +// DataBrokerProtectionExternalWaitlistPixels.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +struct DataBrokerProtectionExternalWaitlistPixels { + + static var isUserLocaleAllowed: Bool { + var regionCode: String? + if #available(macOS 13, *) { + regionCode = Locale.current.region?.identifier + } else { + regionCode = Locale.current.regionCode + } + +#if DEBUG // Always assume US for debug builds + regionCode = "US" +#endif + + return (regionCode ?? "US") == "US" + } + + static func fire(pixel: Pixel.Event, frequency: DailyPixel.PixelFrequency) { + if Self.isUserLocaleAllowed { + let isInternalUser = NSApp.delegateTyped.internalUserDecider.isInternalUser + DailyPixel.fire(pixel: pixel, + frequency: frequency, + includeAppVersionParameter: true, + withAdditionalParameters: [ + "isInternalUser": isInternalUser.description + ] + ) + } + } +} diff --git a/DuckDuckGo/DBP/DataBrokerProtectionFeatureVisibility.swift b/DuckDuckGo/DBP/DataBrokerProtectionFeatureVisibility.swift index b0cc598478..6a86be92bc 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionFeatureVisibility.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionFeatureVisibility.swift @@ -43,7 +43,7 @@ struct DefaultDataBrokerProtectionFeatureVisibility: DataBrokerProtectionFeature isWaitlistEnabled && isWaitlistBetaActive } - private var isUserLocaleAllowed: Bool { + var isUserLocaleAllowed: Bool { var regionCode: String? if #available(macOS 13, *) { regionCode = Locale.current.region?.identifier diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift index 1776e0aecc..f4d0faec88 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift @@ -340,7 +340,7 @@ final class MoreOptionsMenu: NSMenu { .withImage(NSImage(named: "DBP-Icon")) items.append(dataBrokerProtectionItem) - DailyPixel.fire(pixel: .dataBrokerProtectionWaitlistEntryPointMenuItemDisplayed, frequency: .dailyAndCount, includeAppVersionParameter: true) + DataBrokerProtectionExternalWaitlistPixels.fire(pixel: .dataBrokerProtectionWaitlistEntryPointMenuItemDisplayed, frequency: .dailyAndCount) } else { DefaultDataBrokerProtectionFeatureVisibility().disableAndDeleteForWaitlistUsers() diff --git a/DuckDuckGo/Waitlist/Views/WaitlistViewControllerPresenter.swift b/DuckDuckGo/Waitlist/Views/WaitlistViewControllerPresenter.swift index afee252fc8..72beee02c1 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistViewControllerPresenter.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistViewControllerPresenter.swift @@ -93,7 +93,7 @@ struct DataBrokerProtectionWaitlistViewControllerPresenter: WaitlistViewControll guard let windowController = WindowControllersManager.shared.lastKeyMainWindowController else { return } - DailyPixel.fire(pixel: .dataBrokerProtectionWaitlistIntroDisplayed, frequency: .dailyAndCount, includeAppVersionParameter: true) + DataBrokerProtectionExternalWaitlistPixels.fire(pixel: .dataBrokerProtectionWaitlistIntroDisplayed, frequency: .dailyAndCount) // This is a hack to get around an issue with the waitlist notification screen showing the wrong state while it animates in, and then // jumping to the correct state as soon as the animation is complete. This works around that problem by providing the correct state up front, diff --git a/DuckDuckGo/Waitlist/Waitlist.swift b/DuckDuckGo/Waitlist/Waitlist.swift index 5dfc86eb2b..6400f655a0 100644 --- a/DuckDuckGo/Waitlist/Waitlist.swift +++ b/DuckDuckGo/Waitlist/Waitlist.swift @@ -357,9 +357,7 @@ struct DataBrokerProtectionWaitlist: Waitlist { UserDefaults().setValue(true, forKey: UserDefaultsWrapper.Key.shouldShowDBPWaitlistInvitedCardUI.rawValue) sendInviteCodeAvailableNotification { - DailyPixel.fire(pixel: .dataBrokerProtectionWaitlistNotificationShown, - frequency: .dailyAndCount, - includeAppVersionParameter: true) + DataBrokerProtectionExternalWaitlistPixels.fire(pixel: .dataBrokerProtectionWaitlistNotificationShown, frequency: .dailyAndCount) } } } diff --git a/DuckDuckGo/Waitlist/WaitlistTermsAndConditionsActionHandler.swift b/DuckDuckGo/Waitlist/WaitlistTermsAndConditionsActionHandler.swift index 654f877029..df897b2ab6 100644 --- a/DuckDuckGo/Waitlist/WaitlistTermsAndConditionsActionHandler.swift +++ b/DuckDuckGo/Waitlist/WaitlistTermsAndConditionsActionHandler.swift @@ -57,7 +57,7 @@ struct DataBrokerProtectionWaitlistTermsAndConditionsActionHandler: WaitlistTerm var acceptedTermsAndConditions: Bool func didShow() { - DailyPixel.fire(pixel: .dataBrokerProtectionWaitlistTermsAndConditionsDisplayed, frequency: .dailyAndCount, includeAppVersionParameter: true) + DataBrokerProtectionExternalWaitlistPixels.fire(pixel: .dataBrokerProtectionWaitlistTermsAndConditionsDisplayed, frequency: .dailyAndCount) } mutating func didAccept() { @@ -65,7 +65,7 @@ struct DataBrokerProtectionWaitlistTermsAndConditionsActionHandler: WaitlistTerm // Remove delivered NetP notifications in case the user didn't click them. UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [DataBrokerProtectionWaitlist.notificationIdentifier]) - DailyPixel.fire(pixel: .dataBrokerProtectionWaitlistTermsAndConditionsAccepted, frequency: .dailyAndCount, includeAppVersionParameter: true) + DataBrokerProtectionExternalWaitlistPixels.fire(pixel: .dataBrokerProtectionWaitlistTermsAndConditionsAccepted, frequency: .dailyAndCount) } } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateCalculatorTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateCalculatorTests.swift index c579c9ac55..a3cbcf36f3 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateCalculatorTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateCalculatorTests.swift @@ -476,7 +476,7 @@ final class OperationPreferredDateCalculatorTests: XCTestCase { /* If the time elapsed since the last profile removal exceeds the current date plus maintenance period (expired), we should proceed with scheduling a new opt-out request as the broker has failed to honor the previous one. */ - func testMatchFoundWithExpiredProfileWithRecentDate_thenOptOutDateDoesNotChange() throws { + func skipMatchFoundWithExpiredProfileWithRecentDate_thenOptOutDateDoesNotChange() throws { let expiredDate = Date().addingTimeInterval(-schedulingConfig.maintenanceScan.hoursToSeconds) let expectedOptOutDate = Date() From 3ea54648ce4c3a46e2764776014126d75a822dab Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Fri, 15 Dec 2023 13:30:52 +0000 Subject: [PATCH 3/9] Update embedded files --- .../AppPrivacyConfigurationDataProvider.swift | 4 +- DuckDuckGo/ContentBlocker/macos-config.json | 172 +++++++++++++++++- 2 files changed, 165 insertions(+), 11 deletions(-) diff --git a/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift b/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift index b1ef218592..fb7b34b26e 100644 --- a/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift +++ b/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift @@ -22,8 +22,8 @@ import BrowserServicesKit final class AppPrivacyConfigurationDataProvider: EmbeddedDataProvider { public struct Constants { - public static let embeddedDataETag = "\"119cd38f776d198d0ecd736015643c24\"" - public static let embeddedDataSHA = "cc7c0549233b74a5afa469a358baecc9f56693d3684186ad6856fc4890c5cf96" + public static let embeddedDataETag = "\"ca66d409eb00e5c19f3a0abae449dd1a\"" + public static let embeddedDataSHA = "42f9d3064372bc85ac8ee37afe883ed4741d6a3cfcb9ce927c2f732c3f694140" } var embeddedDataEtag: String { diff --git a/DuckDuckGo/ContentBlocker/macos-config.json b/DuckDuckGo/ContentBlocker/macos-config.json index d932539add..94914fa75a 100644 --- a/DuckDuckGo/ContentBlocker/macos-config.json +++ b/DuckDuckGo/ContentBlocker/macos-config.json @@ -1,6 +1,6 @@ { "readme": "https://github.com/duckduckgo/privacy-configuration", - "version": 1702415315898, + "version": 1702579565498, "features": { "adClickAttribution": { "readme": "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/#3rd-party-tracker-loading-protection", @@ -1038,25 +1038,25 @@ "hash": "d757f6509e9a9a20140c755ed0f21ea2" }, "dbp": { - "state": "disabled", + "state": "enabled", "features": { "waitlistBetaActive": { - "state": "disabled" + "state": "enabled" }, "waitlist": { - "state": "disabled", + "state": "enabled", "rollout": { "steps": [ { - "percent": 1 + "percent": 3 } ] } } }, "exceptions": [], - "minSupportedVersion": "1.66.0", - "hash": "325d5463fd2f2b6c8b9ad9288ae6ceed" + "minSupportedVersion": "1.68.0", + "hash": "784ab72b62adf8b5c07f656f167a28d2" }, "duckPlayer": { "exceptions": [], @@ -1216,6 +1216,10 @@ "selector": ".ad-unit", "type": "hide-empty" }, + { + "selector": ".ad-unit-wrapper", + "type": "hide-empty" + }, { "selector": ".column-ad", "type": "hide-empty" @@ -1304,6 +1308,10 @@ "selector": ".ad-banner-container", "type": "hide-empty" }, + { + "selector": "#banner_ad", + "type": "hide-empty" + }, { "selector": "[class*='bannerAd']", "type": "hide-empty" @@ -1392,6 +1400,10 @@ "selector": "[id*='advert-']", "type": "hide-empty" }, + { + "selector": "[aria-label='advertisement']", + "type": "hide-empty" + }, { "selector": ".ads__inline", "type": "closest-empty" @@ -1640,6 +1652,19 @@ "upgrade to flickr pro to hide these ads" ], "domains": [ + { + "domain": "10minutemail.com", + "rules": [ + { + "selector": "#secondary_ads", + "type": "hide-empty" + }, + { + "selector": "#vi-smartbanner", + "type": "hide" + } + ] + }, { "domain": "3bmeteo.com", "rules": [ @@ -1656,6 +1681,27 @@ } ] }, + { + "domain": "9gag.com", + "rules": [ + { + "selector": ".billboard", + "type": "hide-empty" + }, + { + "selector": ".inline-ad-container", + "type": "hide-empty" + }, + { + "selector": ".salt-section", + "type": "hide-empty" + }, + { + "selector": "#top-adhesion", + "type": "hide-empty" + } + ] + }, { "domain": "abc.es", "rules": [ @@ -1825,6 +1871,19 @@ } ] }, + { + "domain": "businessinsider.com", + "rules": [ + { + "selector": ".in-post-sticky", + "type": "hide-empty" + }, + { + "selector": ".subnav-ad-layout", + "type": "hide-empty" + } + ] + }, { "domain": "carandclassic.com", "rules": [ @@ -1964,6 +2023,23 @@ } ] }, + { + "domain": "drugs.com", + "rules": [ + { + "selector": ".topbanner-wrap", + "type": "hide" + }, + { + "selector": ".display-ad-wrapper", + "type": "hide-empty" + }, + { + "selector": "[id*='ddc-sidebox-ad-stacked-wrap']", + "type": "hide-empty" + } + ] + }, { "domain": "ebay.com", "rules": [ @@ -2210,6 +2286,35 @@ } ] }, + { + "domain": "gbnews.com", + "rules": [ + { + "selector": ".video-inbody", + "type": "hide-empty" + }, + { + "selector": ".ad--billboard", + "type": "hide" + }, + { + "selector": ".ad--placeholder", + "type": "hide" + }, + { + "selector": ".stiky_sky", + "type": "hide" + }, + { + "selector": "[position='sticky_banner']", + "type": "hide" + }, + { + "selector": ".ad-inbody", + "type": "hide" + } + ] + }, { "domain": "getpocket.com", "rules": [ @@ -2426,6 +2531,10 @@ { "selector": ".in-post-sticky", "type": "hide-empty" + }, + { + "selector": ".subnav-ad-layout", + "type": "hide-empty" } ] }, @@ -2780,6 +2889,35 @@ } ] }, + { + "domain": "pcgamesn.com", + "rules": [ + { + "selector": ".static_mpu_wrap", + "type": "hide-empty" + }, + { + "selector": "#nn_astro_wrapper", + "type": "hide-empty" + }, + { + "selector": ".ad-nextpage", + "type": "hide" + }, + { + "selector": ".legion_primiswrapper", + "type": "hide-empty" + }, + { + "selector": ".nn_mobile_mpu_wrapper", + "type": "hide-empty" + }, + { + "selector": ".nn-sticky", + "type": "hide-empty" + } + ] + }, { "domain": "petapixel.com", "rules": [ @@ -3419,6 +3557,10 @@ { "selector": "[data-content='Advertisement']", "type": "hide-empty" + }, + { + "selector": "#YDC-Lead-Stack", + "type": "hide-empty" } ] }, @@ -3536,7 +3678,7 @@ ] }, "state": "enabled", - "hash": "b82b3912ad9e3fea5b8754fdec02bc86" + "hash": "c34713afaf78e090b587a923f132ed56" }, "exceptionHandler": { "exceptions": [ @@ -4614,6 +4756,16 @@ } ] }, + "appboycdn.com": { + "rules": [ + { + "rule": "js.appboycdn.com/web-sdk/3.1/appboy.min.js", + "domains": [ + "edx.org" + ] + } + ] + }, "aticdn.net": { "rules": [ { @@ -5491,6 +5643,7 @@ "domains": [ "doterra.com", "easyjet.com", + "edx.org", "worlddutyfree.com" ] }, @@ -5615,6 +5768,7 @@ { "rule": "pagead2.googlesyndication.com/pagead/js/adsbygoogle.js", "domains": [ + "air-journal.fr", "arcadepunks.com", "daotranslate.com", "drakescans.com", @@ -7355,7 +7509,7 @@ "domain": "sundancecatalog.com" } ], - "hash": "6d895ce49e17ece65475082c7e376325" + "hash": "5cecb6d28193f468b28b9afdaca04da1" }, "trackingCookies1p": { "settings": { From 2d406024a49af76c5f3a68d56cef0a2d7dfd7ad2 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Fri, 15 Dec 2023 13:30:52 +0000 Subject: [PATCH 4/9] Set marketing version to 1.69.0 --- Configuration/Version.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/Version.xcconfig b/Configuration/Version.xcconfig index cfe2587555..a331487eef 100644 --- a/Configuration/Version.xcconfig +++ b/Configuration/Version.xcconfig @@ -1 +1 @@ -MARKETING_VERSION = 1.68.0 +MARKETING_VERSION = 1.69.0 From db4a58d69306288061532b2b02d4f59b6133758b Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Fri, 15 Dec 2023 14:35:30 +0100 Subject: [PATCH 5/9] Remove the reconnect/disconnect logic from the connection tester (#1970) Task/Issue URL: https://app.asana.com/0/0/1206173513538618/f iOS PR: https://github.com/duckduckgo/iOS/pull/2272 BSK PR: https://github.com/duckduckgo/BrowserServicesKit/pull/601 ## Description Removes the disconnect / reconnect logic from the connection tester. --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- .../BothAppTargets/NetworkProtectionNavBarButtonModel.swift | 2 +- DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift | 2 +- LocalPackages/Account/Package.swift | 2 +- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index db86c038c6..3a48341974 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -12672,7 +12672,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 94.0.0; + version = 94.0.1; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9aaa137dac..6be05ad2bf 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "e4f4ae624174c1398d345cfc387db38f8f69986d", - "version" : "94.0.0" + "revision" : "7b0910360d6f700ca9bea5e5374c8e2c9a2da899", + "version" : "94.0.1" } }, { diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift index bce34a70e0..3c9f1fda50 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift @@ -87,7 +87,7 @@ final class NetworkProtectionNavBarButtonModel: NSObject, ObservableObject { statusObserver: ipcClient.connectionStatusObserver, serverInfoObserver: ipcClient.serverInfoObserver, connectionErrorObserver: ipcClient.connectionErrorObserver, - connectivityIssuesObserver: ConnectivityIssueObserverThroughDistributedNotifications(), + connectivityIssuesObserver: DisabledConnectivityIssueObserver(), controllerErrorMessageObserver: ControllerErrorMesssageObserverThroughDistributedNotifications() ) self.iconPublisher = NetworkProtectionIconPublisher(statusReporter: networkProtectionStatusReporter, iconProvider: iconProvider) diff --git a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift index a615e1e0fa..bc3af58b03 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift +++ b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift @@ -105,7 +105,7 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { statusObserver: statusObserver, serverInfoObserver: serverInfoObserver, connectionErrorObserver: errorObserver, - connectivityIssuesObserver: ConnectivityIssueObserverThroughDistributedNotifications(), + connectivityIssuesObserver: DisabledConnectivityIssueObserver(), controllerErrorMessageObserver: ControllerErrorMesssageObserverThroughDistributedNotifications() ) }() diff --git a/LocalPackages/Account/Package.swift b/LocalPackages/Account/Package.swift index 90199085d0..5dbf4f7514 100644 --- a/LocalPackages/Account/Package.swift +++ b/LocalPackages/Account/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["Account"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "94.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "94.0.1"), .package(path: "../Purchase") ], targets: [ diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index dd215d3009..6b6026c209 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: "94.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "94.0.1"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper") diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 7c71ce91bd..c5d829a0c6 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -30,7 +30,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "94.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "94.0.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions") ], From 544e5140021cd2a9866f60f5c630ff784019fe3b Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Fri, 15 Dec 2023 13:36:52 +0000 Subject: [PATCH 6/9] Bump version to 1.69.0 (95) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 60e87698ab..4d07d781a0 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 93 +CURRENT_PROJECT_VERSION = 95 From d6382ce54d50b3a4845b4e20cd1c5ac8132306e6 Mon Sep 17 00:00:00 2001 From: amddg44 Date: Fri, 15 Dec 2023 15:17:00 +0100 Subject: [PATCH 7/9] Updates to Autofill Logins copy (#1924) Task/Issue URL: https://app.asana.com/0/0/1205881550343364/f Tech Design URL: CC: Description: Updates all uses of the word 'Logins' with the word 'Password' --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 8 ++++---- DuckDuckGo/Common/Localizables/UserText.swift | 14 ++++++------- .../Extensions/NSAlert+PasswordManager.swift | 10 +++++----- .../Extensions/UserText+PasswordManager.swift | 20 +++++++++---------- .../PasswordManagementItemListModel.swift | 2 +- .../View/PasswordManager.storyboard | 2 +- LocalPackages/Account/Package.swift | 2 +- .../DataBrokerProtection/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- .../Sources/SyncUI/internal/UserText.swift | 2 +- 11 files changed, 33 insertions(+), 33 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 3a48341974..edfbe159f2 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -12672,7 +12672,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 94.0.1; + version = 94.0.2; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 6be05ad2bf..ba0d558e52 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "7b0910360d6f700ca9bea5e5374c8e2c9a2da899", - "version" : "94.0.1" + "revision" : "861b8a72930f138cd18b6a7722502a8a40375827", + "version" : "94.0.2" } }, { @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/duckduckgo-autofill.git", "state" : { - "revision" : "dbecae0df07650a21b5632a92fa2e498c96af7b5", - "version" : "10.0.1" + "revision" : "5597bc17709c8acf454ecaad4f4082007986242a", + "version" : "10.0.2" } }, { diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 4f2e03e1ba..b7a82331a4 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -372,8 +372,8 @@ struct UserText { static let autofillAddresses = NSLocalizedString("autofill.addresses", value: "Addresses", comment: "Autofill autosaved data type") static let autofillPaymentMethods = NSLocalizedString("autofill.payment-methods", value: "Payment methods", comment: "Autofill autosaved data type") static let autofillAutoLock = NSLocalizedString("autofill.auto-lock", value: "Auto-lock", comment: "Autofill settings section title") - static let autofillLockWhenIdle = NSLocalizedString("autofill.lock-when-idle", value: "Lock Autofill after computer is idle for", comment: "Autofill auto-lock setting") - static let autofillNeverLock = NSLocalizedString("autofill.never-lock", value: "Never lock Autofill", comment: "Autofill auto-lock setting") + static let autofillLockWhenIdle = NSLocalizedString("autofill.lock-when-idle", value: "Lock autofill after computer is idle for", comment: "Autofill auto-lock setting") + static let autofillNeverLock = NSLocalizedString("autofill.never-lock", value: "Never lock autofill", comment: "Autofill auto-lock setting") static let autofillNeverLockWarning = NSLocalizedString("autofill.never-lock-warning", value: "If not locked, anyone with access to your device will be able to use and modify your autofill data. For security purposes, credit card form fill always requires authentication.", comment: "Autofill disabled auto-lock warning") static let autolockLocksFormFill = NSLocalizedString("autofill.autolock-locks-form-filling", value: "Also lock password form fill", comment: "Lock form filling when auto-lock is active text") @@ -384,7 +384,7 @@ struct UserText { static let passwordManagement = NSLocalizedString("passsword.management", value: "Autofill", comment: "Used as title for password management user interface") static let passwordManagementAllItems = NSLocalizedString("passsword.management.all-items", value: "All Items", comment: "Used as title for the Autofill All Items option") - static let passwordManagementLogins = NSLocalizedString("passsword.management.logins", value: "Logins", comment: "Used as title for the Autofill Logins option") + static let passwordManagementLogins = NSLocalizedString("passsword.management.logins", value: "Passwords", comment: "Used as title for the Autofill Logins option") static let passwordManagementIdentities = NSLocalizedString("passsword.management.identities", value: "Identities", comment: "Used as title for the Autofill Identities option") static let passwordManagementCreditCards = NSLocalizedString("passsword.management.credit-cards", value: "Credit Cards", comment: "Used as title for the Autofill Credit Cards option") static let passwordManagementNotes = NSLocalizedString("passsword.management.notes", value: "Notes", comment: "Used as title for the Autofill Notes option") @@ -552,7 +552,7 @@ struct UserText { static let safariPreferences = NSLocalizedString("import.logins.safari.preferences", value: "Preferences", comment: "Title of the Safari Preferences menu (up to and including macOS 12)") static let safariSettings = NSLocalizedString("import.logins.safari.settings", value: "Settings", comment: "Title of the Safari Settings menu (macOS 13 and above)") - static let importLoginsCSV = NSLocalizedString("import.logins.csv.title", value: "CSV Logins File", comment: "Title text for the CSV importer") + static let importLoginsCSV = NSLocalizedString("import.logins.csv.title", value: "CSV Password File", comment: "Title text for the CSV importer") static let importBookmarksHTML = NSLocalizedString("import.bookmarks.html.title", value: "HTML Bookmarks File", comment: "Title text for the HTML Bookmarks importer") static let importBookmarksSelectHTMLFile = NSLocalizedString("import.bookmarks.select-html-file", value: "Select HTML Bookmarks File…", comment: "Button text for selecting HTML Bookmarks file") static let importBookmarksSelectAnotherFile = NSLocalizedString("import.bookmarks.select-another-file", value: "Select Another HTML File…", comment: "Button text for selecting another file") @@ -576,7 +576,7 @@ struct UserText { static func importingFile(validLogins: Int) -> String { let localized = NSLocalizedString("import.logins.csv.valid-logins", - value: "Contains %@ valid logins", + value: "Contains %@ valid passwords", comment: "Displays the number of the logins being imported") return String(format: localized, String(validLogins)) } @@ -639,7 +639,7 @@ struct UserText { static func loginImportSuccessfulCSVImports(totalSuccessfulImports: Int) -> String { let localized = NSLocalizedString("import.logins.csv.successful-imports", - value: "New Logins: %@", + value: "New passwords: %@", comment: "Status text indicating the number of successful CSV login imports") return String(format: localized, String(totalSuccessfulImports)) } @@ -935,7 +935,7 @@ struct UserText { } static func passwordManagerAutosavePopoverText(domain: String) -> String { - let localized = NSLocalizedString("autofill.popover.autosave.text", value: "Login saved for %@", comment: "Text confirming a password has been saved for the %@ domain") + let localized = NSLocalizedString("autofill.popover.autosave.text", value: "Password saved for %@", comment: "Text confirming a password has been saved for the %@ domain") return String(format: localized, domain) } diff --git a/DuckDuckGo/SecureVault/Extensions/NSAlert+PasswordManager.swift b/DuckDuckGo/SecureVault/Extensions/NSAlert+PasswordManager.swift index 7ed65191b9..a496e01b88 100644 --- a/DuckDuckGo/SecureVault/Extensions/NSAlert+PasswordManager.swift +++ b/DuckDuckGo/SecureVault/Extensions/NSAlert+PasswordManager.swift @@ -22,7 +22,7 @@ extension NSAlert { static func passwordManagerConfirmDeleteLogin() -> NSAlert { let alert = NSAlert() - alert.messageText = "Are you sure you want to delete this Login?" + alert.messageText = "Are you sure you want to delete this saved password?" alert.informativeText = "This action cannot be undone." alert.alertStyle = .warning alert.addButton(withTitle: "Delete") @@ -42,8 +42,8 @@ extension NSAlert { static func passwordManagerDuplicateLogin() -> NSAlert { let alert = NSAlert() - alert.messageText = "Duplicate login" - alert.informativeText = "You already have a login for this username and website." + alert.messageText = "Duplicate Password" + alert.informativeText = "You already have a password saved for this username and website." alert.alertStyle = .warning alert.addButton(withTitle: "OK") return alert @@ -51,7 +51,7 @@ extension NSAlert { static func passwordManagerConfirmDeleteCard() -> NSAlert { let alert = NSAlert() - alert.messageText = "Are you sure you want to delete this Payment Method from Autofill?" + alert.messageText = "Are you sure you want to delete this saved credit card?" alert.informativeText = "This action cannot be undone." alert.alertStyle = .warning alert.addButton(withTitle: "Delete") @@ -61,7 +61,7 @@ extension NSAlert { static func passwordManagerConfirmDeleteIdentity() -> NSAlert { let alert = NSAlert() - alert.messageText = "Are you sure you want to delete this Info from Autofill?" + alert.messageText = "Are you sure you want to delete this saved autofill info?" alert.informativeText = "This action cannot be undone." alert.alertStyle = .warning alert.addButton(withTitle: "Delete") diff --git a/DuckDuckGo/SecureVault/Extensions/UserText+PasswordManager.swift b/DuckDuckGo/SecureVault/Extensions/UserText+PasswordManager.swift index 33707e199c..23de1bdfa9 100644 --- a/DuckDuckGo/SecureVault/Extensions/UserText+PasswordManager.swift +++ b/DuckDuckGo/SecureVault/Extensions/UserText+PasswordManager.swift @@ -20,21 +20,21 @@ import Foundation extension UserText { - static let pmSaveCredentialsEditableTitle = NSLocalizedString("pm.save-credentials.editable.title", value: "Save Login?", comment: "Title for the editable Save Credentials popover") - static let pmSaveCredentialsNonEditableTitle = NSLocalizedString("pm.save-credentials.non-editable.title", value: "New Login Saved", comment: "Title for the non-editable Save Credentials popover") + static let pmSaveCredentialsEditableTitle = NSLocalizedString("pm.save-credentials.editable.title", value: "Save password?", comment: "Title for the editable Save Credentials popover") + static let pmSaveCredentialsNonEditableTitle = NSLocalizedString("pm.save-credentials.non-editable.title", value: "New Password Saved", comment: "Title for the non-editable Save Credentials popover") - static let pmEmptyStateDefaultTitle = NSLocalizedString("pm.empty.default.title", value: "No Logins or Payment Methods saved yet", comment: "Label for default empty state title") + static let pmEmptyStateDefaultTitle = NSLocalizedString("pm.empty.default.title", value: "No passwords or credit cards saved yet", comment: "Label for default empty state title") static let pmEmptyStateDefaultDescription = NSLocalizedString("pm.empty.default.description", - value: "If your logins are saved in another browser, you can import them into DuckDuckGo.", + value: "If your passwords are saved in another browser, you can import them into DuckDuckGo.", comment: "Label for default empty state description") - static let pmEmptyStateLoginsTitle = NSLocalizedString("pm.empty.logins.title", value: "No Logins", comment: "Label for logins empty state title") + static let pmEmptyStateLoginsTitle = NSLocalizedString("pm.empty.logins.title", value: "No passwords", comment: "Label for logins empty state title") static let pmEmptyStateIdentitiesTitle = NSLocalizedString("pm.empty.identities.title", value: "No Identities", comment: "Label for identities empty state title") static let pmEmptyStateCardsTitle = NSLocalizedString("pm.empty.cards.title", value: "No Cards", comment: "Label for cards empty state title") static let pmEmptyStateNotesTitle = NSLocalizedString("pm.empty.notes.title", value: "No Notes", comment: "Label for notes empty state title") static let pmNewCard = NSLocalizedString("pm.new.card", value: "Credit Card", comment: "Label for new card title") - static let pmNewLogin = NSLocalizedString("pm.new.login", value: "Login", comment: "Label for new login title") + static let pmNewLogin = NSLocalizedString("pm.new.login", value: "Password", comment: "Label for new login title") static let pmNewIdentity = NSLocalizedString("pm.new.identity", value: "Identity", comment: "Label for new identity title") static let pmNewNote = NSLocalizedString("pm.new.note", value: "Note", comment: "Label for new note title") @@ -102,7 +102,7 @@ extension UserText { static func pmLockScreenDuration(duration: String) -> String { let localized = NSLocalizedString("pm.lock-screen.duration", - value: "Your Autofill info will remain unlocked until your computer is idle for %@.", + value: "Your autofill info will remain unlocked until your computer is idle for %@.", comment: "") return String(format: localized, duration) } @@ -110,10 +110,10 @@ extension UserText { static let pmLockScreenPreferencesLabel = NSLocalizedString("pm.lock-screen.preferences.label", value: "Change in", comment: "Label used for a button that opens preferences") static let pmLockScreenPreferencesLink = NSLocalizedString("pm.lock-screen.preferences.link", value: "Settings", comment: "Label used for a button that opens preferences") - static let pmAutoLockPromptUnlockLogins = NSLocalizedString("pm.lock-screen.prompt.unlock-logins", value: "unlock access to your Autofill info", comment: "Label presented when unlocking Autofill") + static let pmAutoLockPromptUnlockLogins = NSLocalizedString("pm.lock-screen.prompt.unlock-logins", value: "unlock access to your autofill info", comment: "Label presented when unlocking Autofill") static let pmAutoLockPromptExportLogins = NSLocalizedString("pm.lock-screen.prompt.export-logins", value: "export your usernames and passwords", comment: "Label presented when exporting logins") - static let pmAutoLockPromptChangeLoginsSettings = NSLocalizedString("pm.lock-screen.prompt.change-settings", value: "change your Autofill info access settings", comment: "Label presented when changing Auto-Lock settings") - static let pmAutoLockPromptAutofill = NSLocalizedString("pm.lock-screen.prompt.autofill", value: "unlock access to your Autofill info", comment: "Label presented when autofilling credit card information") + static let pmAutoLockPromptChangeLoginsSettings = NSLocalizedString("pm.lock-screen.prompt.change-settings", value: "change your autofill info access settings", comment: "Label presented when changing Auto-Lock settings") + static let pmAutoLockPromptAutofill = NSLocalizedString("pm.lock-screen.prompt.autofill", value: "unlock access to your autofill info", comment: "Label presented when autofilling credit card information") static let autoLockThreshold1Minute = NSLocalizedString("pm.lock-screen.threshold.1-minute", value: "1 minute", comment: "Label used when selecting the Auto-Lock threshold") static let autoLockThreshold5Minutes = NSLocalizedString("pm.lock-screen.threshold.5-minutes", value: "5 minutes", comment: "Label used when selecting the Auto-Lock threshold") diff --git a/DuckDuckGo/SecureVault/Model/PasswordManagementItemListModel.swift b/DuckDuckGo/SecureVault/Model/PasswordManagementItemListModel.swift index a2354bedb7..492bcec5e6 100644 --- a/DuckDuckGo/SecureVault/Model/PasswordManagementItemListModel.swift +++ b/DuckDuckGo/SecureVault/Model/PasswordManagementItemListModel.swift @@ -437,7 +437,7 @@ final class PasswordManagementItemListModel: ObservableObject { var sections = [PasswordManagementListSection]() - if !accounts.isEmpty { sections.append(PasswordManagementListSection(title: "Logins", items: accounts)) } + if !accounts.isEmpty { sections.append(PasswordManagementListSection(title: "Passwords", items: accounts)) } if !cards.isEmpty { sections.append(PasswordManagementListSection(title: "Credit Cards", items: cards)) } if !identities.isEmpty { sections.append(PasswordManagementListSection(title: "Identities", items: identities)) } if !notes.isEmpty { sections.append(PasswordManagementListSection(title: "Notes", items: notes)) } diff --git a/DuckDuckGo/SecureVault/View/PasswordManager.storyboard b/DuckDuckGo/SecureVault/View/PasswordManager.storyboard index 81af455c65..157d696f24 100644 --- a/DuckDuckGo/SecureVault/View/PasswordManager.storyboard +++ b/DuckDuckGo/SecureVault/View/PasswordManager.storyboard @@ -577,7 +577,7 @@ - + diff --git a/LocalPackages/Account/Package.swift b/LocalPackages/Account/Package.swift index 5dbf4f7514..0e357ade93 100644 --- a/LocalPackages/Account/Package.swift +++ b/LocalPackages/Account/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["Account"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "94.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "94.0.2"), .package(path: "../Purchase") ], targets: [ diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 6b6026c209..d5c101fa40 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: "94.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "94.0.2"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper") diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index c5d829a0c6..99ead44dd0 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -30,7 +30,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "94.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "94.0.2"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions") ], diff --git a/LocalPackages/SyncUI/Sources/SyncUI/internal/UserText.swift b/LocalPackages/SyncUI/Sources/SyncUI/internal/UserText.swift index fe4f6c3fef..928aa1123f 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/internal/UserText.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/internal/UserText.swift @@ -143,7 +143,7 @@ enum UserText { static let bookmarksLimitExceededDescription = NSLocalizedString("prefrences.sync.bookmarks-limit-exceeded-description", value: "Bookmark limit exceeded. Delete some to resume syncing.", comment: "Description for sync bookmarks limits exceeded warning") static let credentialsLimitExceededDescription = NSLocalizedString("prefrences.sync.credentials-limit-exceeded-description", value: "Logins limit exceeded. Delete some to resume syncing.", comment: "Description for sync credentials limits exceeded warning") static let bookmarksLimitExceededAction = NSLocalizedString("prefrences.sync.bookmarks-limit-exceeded-action", value: "Manage Bookmarks", comment: "Button title for sync bookmarks limits exceeded warning to manage bookmarks") - static let credentialsLimitExceededAction = NSLocalizedString("prefrences.sync.credentials-limit-exceeded-action", value: "Manage Logins", comment: "Button title for sync credentials limits exceeded warning to manage logins") + static let credentialsLimitExceededAction = NSLocalizedString("prefrences.sync.credentials-limit-exceeded-action", value: "Manage passwords...", comment: "Button title for sync credentials limits exceeded warning to manage logins") static let syncErrorAlertTitle = NSLocalizedString("alert.sync-error", value: "Sync Error", comment: "Title for sync error alert") static let unableToSyncDescription = NSLocalizedString("alert.unable-to-sync-description", value: "Unable to sync.", comment: "Description for unable to sync error") static let unableToGetDevicesDescription = NSLocalizedString("alert.unable-to-get-devices-description", value: "Unable to retrieve the list of connected devices.", comment: "Description for unable to get devices error") From 91324f41a1aea0229d81958a71895c32ab210cb0 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Fri, 15 Dec 2023 10:05:15 -0800 Subject: [PATCH 8/9] Add additional VPN startup pixels (#1975) Task/Issue URL: https://app.asana.com/0/0/1206166619128184/f Tech Design URL: CC: Description: This PR makes a few changes to the VPN startup process: The connection waiter timeout has been increased from 4 seconds to 10, matching Android The system extension activation step now re-throws its error; without this change, the app would continue to try and start up NetP even though the system extension hadn't been activated yet The system extension error pixel is now fired for all error types, not just the unknown activation result type There's a new pixel sent from the start method, which sends any error received; this uses a new enhancement to PixelKit which allows us to provide an Error object, and will correctly pull out underlying error info if it is present --- .../NetworkProtectionPixelEvent.swift | 16 +++++--- .../NetworkProtectionTunnelController.swift | 32 ++++++++++----- .../PixelKit/PixelKit+Parameters.swift | 2 + .../PixelKit/Sources/PixelKit/PixelKit.swift | 40 ++++++++++++++++--- 4 files changed, 70 insertions(+), 20 deletions(-) diff --git a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift index 3186124aea..af341e35be 100644 --- a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift +++ b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift @@ -27,6 +27,8 @@ enum NetworkProtectionPixelEvent: PixelKitEvent { case networkProtectionActiveUser case networkProtectionNewUser + case networkProtectionStartFailed + case networkProtectionEnableAttemptConnecting case networkProtectionEnableAttemptSuccess case networkProtectionEnableAttemptFailure @@ -74,7 +76,7 @@ enum NetworkProtectionPixelEvent: PixelKitEvent { case networkProtectionRekeyCompleted - case networkProtectionSystemExtensionUnknownActivationResult + case networkProtectionSystemExtensionActivationFailure case networkProtectionUnhandledError(function: String, line: Int, error: Error) @@ -90,6 +92,9 @@ enum NetworkProtectionPixelEvent: PixelKitEvent { case .networkProtectionNewUser: return "m_mac_netp_daily_active_u" + case .networkProtectionStartFailed: + return "m_mac_netp_start_failed" + case .networkProtectionEnableAttemptConnecting: return "m_mac_netp_ev_enable_attempt" @@ -201,8 +206,8 @@ enum NetworkProtectionPixelEvent: PixelKitEvent { case .networkProtectionRekeyCompleted: return "m_mac_netp_rekey_completed" - case .networkProtectionSystemExtensionUnknownActivationResult: - return "m_mac_netp_system_extension_unknown_activation_result" + case .networkProtectionSystemExtensionActivationFailure: + return "m_mac_netp_system_extension_activation_failure" case .networkProtectionUnhandledError: return "m_mac_netp_unhandled_error" @@ -279,7 +284,7 @@ enum NetworkProtectionPixelEvent: PixelKitEvent { .networkProtectionWireguardErrorCannotLocateTunnelFileDescriptor, .networkProtectionWireguardErrorInvalidState, .networkProtectionWireguardErrorFailedDNSResolution, - .networkProtectionSystemExtensionUnknownActivationResult, + .networkProtectionSystemExtensionActivationFailure, .networkProtectionActiveUser, .networkProtectionNewUser, .networkProtectionEnableAttemptConnecting, @@ -288,7 +293,8 @@ enum NetworkProtectionPixelEvent: PixelKitEvent { .networkProtectionTunnelFailureDetected, .networkProtectionTunnelFailureRecovered, .networkProtectionLatency, - .networkProtectionLatencyError: + .networkProtectionLatencyError, + .networkProtectionStartFailed: return nil } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift index 815e8e55f6..2c4668f139 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift @@ -81,7 +81,13 @@ final class NetworkProtectionTunnelController: NetworkProtection.TunnelControlle // MARK: - Connection Status - private let statusTransitionAwaiter = ConnectionStatusTransitionAwaiter(statusObserver: ConnectionStatusObserverThroughSession(platformNotificationCenter: NSWorkspace.shared.notificationCenter, platformDidWakeNotification: NSWorkspace.didWakeNotification), transitionTimeout: .seconds(4)) + private let statusTransitionAwaiter = ConnectionStatusTransitionAwaiter( + statusObserver: ConnectionStatusObserverThroughSession( + platformNotificationCenter: NSWorkspace.shared.notificationCenter, + platformDidWakeNotification: NSWorkspace.didWakeNotification + ), + transitionTimeout: .seconds(10) + ) // MARK: - Tunnel Manager @@ -243,6 +249,7 @@ final class NetworkProtectionTunnelController: NetworkProtection.TunnelControlle // kill switch protocolConfiguration.enforceRoutes = settings.enforceRoutes + // this setting breaks Connection Tester protocolConfiguration.includeAllNetworks = settings.includeAllNetworks @@ -277,15 +284,14 @@ final class NetworkProtectionTunnelController: NetworkProtection.TunnelControlle } } - // MARK: - Ensure things are working + // MARK: - Activate System Extension #if NETP_SYSTEM_EXTENSION /// Ensures that the system extension is activated if necessary. /// private func activateSystemExtension(waitingForUserApproval: @escaping () -> Void) async throws { do { - try await networkExtensionController.activateSystemExtension( - waitingForUserApproval: waitingForUserApproval) + try await networkExtensionController.activateSystemExtension(waitingForUserApproval: waitingForUserApproval) } catch { switch error { case OSSystemExtensionError.requestSuperseded: @@ -294,18 +300,20 @@ final class NetworkProtectionTunnelController: NetworkProtection.TunnelControlle controllerErrorStore.lastErrorMessage = UserText.networkProtectionSystemSettings case SystemExtensionRequestError.unknownRequestResult: controllerErrorStore.lastErrorMessage = UserText.networkProtectionUnknownActivationError - - PixelKit.fire( - NetworkProtectionPixelEvent.networkProtectionSystemExtensionUnknownActivationResult, - frequency: .standard, - includeAppVersionParameter: true) case SystemExtensionRequestError.willActivateAfterReboot: controllerErrorStore.lastErrorMessage = UserText.networkProtectionPleaseReboot default: controllerErrorStore.lastErrorMessage = error.localizedDescription } - return + PixelKit.fire( + NetworkProtectionPixelEvent.networkProtectionSystemExtensionActivationFailure, + frequency: .standard, + withError: error, + includeAppVersionParameter: true + ) + + throw error } self.controllerErrorStore.lastErrorMessage = nil @@ -387,6 +395,10 @@ final class NetworkProtectionTunnelController: NetworkProtection.TunnelControlle try await start(tunnelManager) } } catch { + PixelKit.fire( + NetworkProtectionPixelEvent.networkProtectionStartFailed, frequency: .standard, withError: error, includeAppVersionParameter: true + ) + await stop() controllerErrorStore.lastErrorMessage = error.localizedDescription } diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift index 8f2ca1bd8d..cbb4562111 100644 --- a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift +++ b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift @@ -27,10 +27,12 @@ public extension PixelKit { public static let osMajorVersion = "osMajorVersion" public static let errorCode = "e" + public static let errorDomain = "errorDomain" public static let errorDesc = "d" public static let errorCount = "c" public static let errorSource = "error_source" public static let underlyingErrorCode = "ue" + public static let underlyingErrorDomain = "underlyingErrorDomain" public static let underlyingErrorDesc = "ud" public static let underlyingErrorSQLiteCode = "sqlrc" public static let underlyingErrorSQLiteExtendedCode = "sqlerc" diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift index 786c77dc5a..f16a51d9db 100644 --- a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift +++ b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift @@ -121,16 +121,23 @@ public final class PixelKit { private func fire(pixelNamed pixelName: String, frequency: Frequency, - withHeaders headers: [String: String]? = nil, - withAdditionalParameters params: [String: String]? = nil, - allowedQueryReservedCharacters: CharacterSet? = nil, - includeAppVersionParameter: Bool = true, - onComplete: @escaping CompletionBlock = { _, _ in }) { + withHeaders headers: [String: String]?, + withAdditionalParameters params: [String: String]?, + withError error: Error?, + allowedQueryReservedCharacters: CharacterSet?, + includeAppVersionParameter: Bool, + onComplete: @escaping CompletionBlock) { var newParams = params ?? [:] + if includeAppVersionParameter { newParams[Parameters.appVersion] = appVersion } + + if let error { + newParams.appendErrorPixelParams(error: error) + } + #if DEBUG newParams[Parameters.test] = Values.test #endif @@ -203,6 +210,7 @@ public final class PixelKit { frequency: Frequency = .standard, withHeaders headers: [String: String]? = nil, withAdditionalParameters params: [String: String]? = nil, + withError error: Error? = nil, allowedQueryReservedCharacters: CharacterSet? = nil, includeAppVersionParameter: Bool = true, onComplete: @escaping CompletionBlock = { _, _ in }) { @@ -235,6 +243,7 @@ public final class PixelKit { frequency: frequency, withHeaders: headers, withAdditionalParameters: newParams, + withError: error, allowedQueryReservedCharacters: allowedQueryReservedCharacters, includeAppVersionParameter: includeAppVersionParameter, onComplete: onComplete) @@ -244,6 +253,7 @@ public final class PixelKit { frequency: Frequency = .standard, withHeaders headers: [String: String] = [:], withAdditionalParameters parameters: [String: String]? = nil, + withError error: Error? = nil, allowedQueryReservedCharacters: CharacterSet? = nil, includeAppVersionParameter: Bool = true, onComplete: @escaping CompletionBlock = { _, _ in }) { @@ -252,6 +262,7 @@ public final class PixelKit { frequency: frequency, withHeaders: headers, withAdditionalParameters: parameters, + withError: error, allowedQueryReservedCharacters: allowedQueryReservedCharacters, includeAppVersionParameter: includeAppVersionParameter, onComplete: onComplete) @@ -310,3 +321,22 @@ public final class PixelKit { } } + +extension Dictionary where Key == String, Value == String { + + mutating func appendErrorPixelParams(error: Error) { + let nsError = error as NSError + + self[PixelKit.Parameters.errorCode] = "\(nsError.code)" + self[PixelKit.Parameters.errorDomain] = nsError.domain + + if let underlyingError = nsError.userInfo["NSUnderlyingError"] as? NSError { + self[PixelKit.Parameters.underlyingErrorCode] = "\(underlyingError.code)" + self[PixelKit.Parameters.underlyingErrorDomain] = underlyingError.domain + } else if let sqlErrorCode = nsError.userInfo["NSSQLiteErrorDomain"] as? NSNumber { + self[PixelKit.Parameters.underlyingErrorCode] = "\(sqlErrorCode.intValue)" + self[PixelKit.Parameters.underlyingErrorDomain] = "NSSQLiteErrorDomain" + } + } + +} From 2982a234f2081e8ff72494e225aecc42d68e2120 Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Fri, 15 Dec 2023 22:24:23 +0100 Subject: [PATCH 9/9] VPN Geoswitching - initial draft (#1978) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/0/1206183910299715/f Description: This adds an initial implementation of location switching for the VPN. Known issues: Some of the layout / spacing is not quite right The tap area of the items on the VPN Location selection screen is too small The styling of the list sections on the VPN Location screen is not finished. There seems to be a bug where the initial selection is not respected when connecting after selecting There is a momentary delay before the list items load. Needs unit tests (I promise I will add them, they are very similar to the iOS ones so it won’t take long. I just ran out of time before the holidays). --- DuckDuckGo.xcodeproj/project.pbxproj | 48 ++++ DuckDuckGo/Common/Localizables/UserText.swift | 24 ++ .../FeatureFlagging/Model/FeatureFlag.swift | 4 + ...tworkProtectionVPNCountryLabelsModel.swift | 45 ++++ .../VPNLocationPreferenceItem.swift | 67 +++++ .../VPNLocationPreferenceItemModel.swift | 49 ++++ .../VPNLocation/VPNLocationView.swift | 245 ++++++++++++++++++ .../VPNLocation/VPNLocationViewModel.swift | 185 +++++++++++++ .../Model/VPNPreferencesModel.swift | 17 +- .../Preferences/View/PreferencesVPNView.swift | 7 + 10 files changed, 690 insertions(+), 1 deletion(-) create mode 100644 DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/NetworkProtectionVPNCountryLabelsModel.swift create mode 100644 DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationPreferenceItem.swift create mode 100644 DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationPreferenceItemModel.swift create mode 100644 DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationView.swift create mode 100644 DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index edfbe159f2..d4f7e19267 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2950,11 +2950,26 @@ EE7295EB2A545BFC008C0991 /* NetworkProtection in Frameworks */ = {isa = PBXBuildFile; productRef = EE7295EA2A545BFC008C0991 /* NetworkProtection */; }; EE7295ED2A545C0A008C0991 /* NetworkProtection in Frameworks */ = {isa = PBXBuildFile; productRef = EE7295EC2A545C0A008C0991 /* NetworkProtection */; }; EE7295EF2A545C12008C0991 /* NetworkProtection in Frameworks */ = {isa = PBXBuildFile; productRef = EE7295EE2A545C12008C0991 /* NetworkProtection */; }; + EEA3EEB12B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA3EEB02B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift */; }; + EEA3EEB32B24EC0600E8333A /* VPNLocationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA3EEB22B24EC0600E8333A /* VPNLocationViewModel.swift */; }; EEAD7A7A2A1D3E20002A24E7 /* AppLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD7A6E2A1D3E1F002A24E7 /* AppLauncher.swift */; }; EEAD7A7B2A1D3E20002A24E7 /* AppLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD7A6E2A1D3E1F002A24E7 /* AppLauncher.swift */; }; EEAD7A7C2A1D3E20002A24E7 /* AppLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD7A6E2A1D3E1F002A24E7 /* AppLauncher.swift */; }; EEC111E4294D06020086524F /* JSAlert.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = EEC111E3294D06020086524F /* JSAlert.storyboard */; }; EEC111E6294D06290086524F /* JSAlertViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC111E5294D06290086524F /* JSAlertViewModel.swift */; }; + EEC4A65E2B277E8D00F7C0AA /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA3EEB02B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift */; }; + EEC4A65F2B277EE100F7C0AA /* VPNLocationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA3EEB22B24EC0600E8333A /* VPNLocationViewModel.swift */; }; + EEC4A6602B277F0D00F7C0AA /* VPNLocationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA3EEB22B24EC0600E8333A /* VPNLocationViewModel.swift */; }; + EEC4A6612B277F1100F7C0AA /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA3EEB02B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift */; }; + EEC4A6692B2C87D300F7C0AA /* VPNLocationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC4A6682B2C87D300F7C0AA /* VPNLocationView.swift */; }; + EEC4A66A2B2C87D300F7C0AA /* VPNLocationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC4A6682B2C87D300F7C0AA /* VPNLocationView.swift */; }; + EEC4A66B2B2C87D300F7C0AA /* VPNLocationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC4A6682B2C87D300F7C0AA /* VPNLocationView.swift */; }; + EEC4A66D2B2C894F00F7C0AA /* VPNLocationPreferenceItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC4A66C2B2C894F00F7C0AA /* VPNLocationPreferenceItemModel.swift */; }; + EEC4A66E2B2C894F00F7C0AA /* VPNLocationPreferenceItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC4A66C2B2C894F00F7C0AA /* VPNLocationPreferenceItemModel.swift */; }; + EEC4A66F2B2C894F00F7C0AA /* VPNLocationPreferenceItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC4A66C2B2C894F00F7C0AA /* VPNLocationPreferenceItemModel.swift */; }; + EEC4A6712B2C90AB00F7C0AA /* VPNLocationPreferenceItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC4A6702B2C90AB00F7C0AA /* VPNLocationPreferenceItem.swift */; }; + EEC4A6722B2C90AB00F7C0AA /* VPNLocationPreferenceItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC4A6702B2C90AB00F7C0AA /* VPNLocationPreferenceItem.swift */; }; + EEC4A6732B2C90AB00F7C0AA /* VPNLocationPreferenceItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC4A6702B2C90AB00F7C0AA /* VPNLocationPreferenceItem.swift */; }; EEC589D92A4F1CE300BCD60C /* AppLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD7A6E2A1D3E1F002A24E7 /* AppLauncher.swift */; }; EEC589DA2A4F1CE400BCD60C /* AppLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD7A6E2A1D3E1F002A24E7 /* AppLauncher.swift */; }; EEC589DB2A4F1CE700BCD60C /* AppLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD7A6E2A1D3E1F002A24E7 /* AppLauncher.swift */; }; @@ -4197,9 +4212,14 @@ EAE427FF275D47FA00DAC26B /* ClickToLoadModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClickToLoadModel.swift; sourceTree = ""; }; EAFAD6C92728BD1200F9DF00 /* clickToLoad.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = clickToLoad.js; sourceTree = ""; }; EE339227291BDEFD009F62C1 /* JSAlertController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSAlertController.swift; sourceTree = ""; }; + EEA3EEB02B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionVPNCountryLabelsModel.swift; sourceTree = ""; }; + EEA3EEB22B24EC0600E8333A /* VPNLocationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VPNLocationViewModel.swift; sourceTree = ""; }; EEAD7A6E2A1D3E1F002A24E7 /* AppLauncher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppLauncher.swift; sourceTree = ""; }; EEC111E3294D06020086524F /* JSAlert.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = JSAlert.storyboard; sourceTree = ""; }; EEC111E5294D06290086524F /* JSAlertViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSAlertViewModel.swift; sourceTree = ""; }; + EEC4A6682B2C87D300F7C0AA /* VPNLocationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNLocationView.swift; sourceTree = ""; }; + EEC4A66C2B2C894F00F7C0AA /* VPNLocationPreferenceItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNLocationPreferenceItemModel.swift; sourceTree = ""; }; + EEC4A6702B2C90AB00F7C0AA /* VPNLocationPreferenceItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNLocationPreferenceItem.swift; sourceTree = ""; }; EECE10E429DD77E60044D027 /* FeatureFlag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureFlag.swift; sourceTree = ""; }; EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacPacketTunnelProvider.swift; sourceTree = ""; }; EEF53E172950CED5002D78F4 /* JSAlertViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSAlertViewModelTests.swift; sourceTree = ""; }; @@ -5107,6 +5127,7 @@ 4B4D60632A0B29FA00BCD287 /* BothAppTargets */ = { isa = PBXGroup; children = ( + EEA3EEAF2B24EB5100E8333A /* VPNLocation */, 9D9AE8682AA76CDC0026E7DC /* LoginItem+NetworkProtection.swift */, 7BE146062A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift */, 7B05829D2A812AC000AC3F7C /* NetworkProtectionOnboardingMenu.swift */, @@ -7839,6 +7860,18 @@ path = fonts; sourceTree = ""; }; + EEA3EEAF2B24EB5100E8333A /* VPNLocation */ = { + isa = PBXGroup; + children = ( + EEA3EEB22B24EC0600E8333A /* VPNLocationViewModel.swift */, + EEA3EEB02B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift */, + EEC4A6682B2C87D300F7C0AA /* VPNLocationView.swift */, + EEC4A66C2B2C894F00F7C0AA /* VPNLocationPreferenceItemModel.swift */, + EEC4A6702B2C90AB00F7C0AA /* VPNLocationPreferenceItem.swift */, + ); + path = VPNLocation; + sourceTree = ""; + }; EEAEA3F4294D05CF00D04DF3 /* JSAlert */ = { isa = PBXGroup; children = ( @@ -9067,6 +9100,7 @@ 3706FA7F293F65D500E42796 /* TabIndex.swift in Sources */, 3706FA80293F65D500E42796 /* TabLazyLoaderDataSource.swift in Sources */, 3706FA81293F65D500E42796 /* LoginImport.swift in Sources */, + EEC4A66E2B2C894F00F7C0AA /* VPNLocationPreferenceItemModel.swift in Sources */, 3706FA83293F65D500E42796 /* LazyLoadable.swift in Sources */, 3706FA84293F65D500E42796 /* ClickToLoadModel.swift in Sources */, 3706FA85293F65D500E42796 /* KeyedCodingExtension.swift in Sources */, @@ -9165,6 +9199,7 @@ 3706FADC293F65D500E42796 /* FirefoxLoginReader.swift in Sources */, 3706FADD293F65D500E42796 /* AtbParser.swift in Sources */, 3706FADE293F65D500E42796 /* PreferencesDuckPlayerView.swift in Sources */, + EEC4A65E2B277E8D00F7C0AA /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */, B66260E729ACAE4B00E9E3EE /* NavigationHotkeyHandler.swift in Sources */, 3706FADF293F65D500E42796 /* AddFolderModalViewController.swift in Sources */, 3706FAE0293F65D500E42796 /* BookmarkSidebarTreeController.swift in Sources */, @@ -9302,6 +9337,7 @@ 3706FB4D293F65D500E42796 /* GrammarFeaturesManager.swift in Sources */, 3706FB50293F65D500E42796 /* SafariFaviconsReader.swift in Sources */, 3706FB51293F65D500E42796 /* NSScreenExtension.swift in Sources */, + EEC4A66A2B2C87D300F7C0AA /* VPNLocationView.swift in Sources */, 3706FB52293F65D500E42796 /* NSBezierPathExtension.swift in Sources */, 3706FB53293F65D500E42796 /* WebsiteDataStore.swift in Sources */, 3706FB54293F65D500E42796 /* PermissionContextMenu.swift in Sources */, @@ -9391,6 +9427,7 @@ 4B4032852AAAC24400CCA602 /* WaitlistActivationDateStore.swift in Sources */, 3706FB93293F65D500E42796 /* PasteboardFolder.swift in Sources */, 3706FB94293F65D500E42796 /* CookieManagedNotificationView.swift in Sources */, + EEC4A65F2B277EE100F7C0AA /* VPNLocationViewModel.swift in Sources */, 370A34B22AB24E3700C77F7C /* SyncDebugMenu.swift in Sources */, 4B4D60BE2A0C848A00BCD287 /* NetworkProtection+ConvenienceInitializers.swift in Sources */, 3706FB95293F65D500E42796 /* PermissionType.swift in Sources */, @@ -9492,6 +9529,7 @@ 37197EA12942441700394917 /* Tab+UIDelegate.swift in Sources */, 56D6A3D729DB2BAB0055215A /* ContinueSetUpView.swift in Sources */, 3706FBDF293F65D500E42796 /* String+Punycode.swift in Sources */, + EEC4A6722B2C90AB00F7C0AA /* VPNLocationPreferenceItem.swift in Sources */, 3706FBE0293F65D500E42796 /* NSException+Catch.m in Sources */, 3706FBE1293F65D500E42796 /* AppStateRestorationManager.swift in Sources */, 3706FBE2293F65D500E42796 /* ClickToLoadUserScript.swift in Sources */, @@ -10331,6 +10369,7 @@ B68D21D22ACBCA01002DA3C2 /* ContentBlockerRulesManagerMock.swift in Sources */, 4B9579F62AC7AE700062CA31 /* ChromiumFaviconsReader.swift in Sources */, 4B9579F72AC7AE700062CA31 /* SuggestionTableRowView.swift in Sources */, + EEC4A6732B2C90AB00F7C0AA /* VPNLocationPreferenceItem.swift in Sources */, 4B9579F82AC7AE700062CA31 /* DownloadsPreferences.swift in Sources */, 4B9579F92AC7AE700062CA31 /* PasswordManagementItemList.swift in Sources */, 4B9579FA2AC7AE700062CA31 /* Bookmark.swift in Sources */, @@ -10492,6 +10531,7 @@ 4B957A8B2AC7AE700062CA31 /* PasswordManagementListSection.swift in Sources */, 4B957A8C2AC7AE700062CA31 /* FaviconReferenceCache.swift in Sources */, 4B957A8D2AC7AE700062CA31 /* BookmarkTreeController.swift in Sources */, + EEC4A6602B277F0D00F7C0AA /* VPNLocationViewModel.swift in Sources */, 4B957A8E2AC7AE700062CA31 /* FirefoxEncryptionKeyReader.swift in Sources */, 4B957A8F2AC7AE700062CA31 /* EventMapping+NetworkProtectionError.swift in Sources */, 4B957A902AC7AE700062CA31 /* BookmarkManagementSplitViewController.swift in Sources */, @@ -10628,6 +10668,7 @@ 4B957B112AC7AE700062CA31 /* DateExtension.swift in Sources */, 4B957B122AC7AE700062CA31 /* History.xcdatamodeld in Sources */, 4B957B132AC7AE700062CA31 /* PermissionStore.swift in Sources */, + EEC4A6612B277F1100F7C0AA /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */, 4B957B142AC7AE700062CA31 /* PrivacyIconViewModel.swift in Sources */, 4B957B152AC7AE700062CA31 /* ChromiumBookmarksReader.swift in Sources */, 4B957B162AC7AE700062CA31 /* Downloads.xcdatamodeld in Sources */, @@ -10667,6 +10708,7 @@ 4B957B382AC7AE700062CA31 /* ProgressView.swift in Sources */, 7BEC20472B0F505F00243D3E /* BookmarkAddFolderPopoverViewController.swift in Sources */, 4B957B392AC7AE700062CA31 /* StatisticsStore.swift in Sources */, + EEC4A66B2B2C87D300F7C0AA /* VPNLocationView.swift in Sources */, 4B957B3A2AC7AE700062CA31 /* BWInstallationService.swift in Sources */, 4B957B3B2AC7AE700062CA31 /* BookmarksBarPromptPopover.swift in Sources */, 4B957B3C2AC7AE700062CA31 /* NetworkProtectionInvitePresenter.swift in Sources */, @@ -10692,6 +10734,7 @@ 4B957B502AC7AE700062CA31 /* CoreDataStore.swift in Sources */, 4B957B512AC7AE700062CA31 /* BundleExtension.swift in Sources */, 4B957B522AC7AE700062CA31 /* NSOpenPanelExtensions.swift in Sources */, + EEC4A66F2B2C894F00F7C0AA /* VPNLocationPreferenceItemModel.swift in Sources */, 4B957B532AC7AE700062CA31 /* FirePopover.swift in Sources */, 4B957B542AC7AE700062CA31 /* HistoryCoordinator.swift in Sources */, 4B957B552AC7AE700062CA31 /* NetworkProtectionOnboardingMenu.swift in Sources */, @@ -10892,6 +10935,7 @@ 85799C1825DEBB3F0007EC87 /* Logging.swift in Sources */, AAC30A2E268F1EE300D2D9CD /* CrashReportPromptPresenter.swift in Sources */, 1D2DC00629016798008083A1 /* BWCredential.swift in Sources */, + EEA3EEB12B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */, 37AFCE8727DA334800471A10 /* PreferencesRootView.swift in Sources */, B684590825C9027900DC17B6 /* AppStateChangedPublisher.swift in Sources */, 4B92928F26670D1700AD2C21 /* BookmarkTableCellView.swift in Sources */, @@ -10993,6 +11037,7 @@ 37F19A6528E1B3FB00740DC6 /* PreferencesDuckPlayerView.swift in Sources */, 4B9292D22667123700AD2C21 /* AddFolderModalViewController.swift in Sources */, 4B92929E26670D2A00AD2C21 /* BookmarkSidebarTreeController.swift in Sources */, + EEC4A6712B2C90AB00F7C0AA /* VPNLocationPreferenceItem.swift in Sources */, 85589E8727BBB8F20038AD11 /* HomePageFavoritesModel.swift in Sources */, 4BB88B4A25B7B690006F6B06 /* SequenceExtensions.swift in Sources */, B602E7CF2A93A5FF00F12201 /* WKBackForwardListExtension.swift in Sources */, @@ -11087,6 +11132,7 @@ 4BBC16A027C4859400E00A38 /* DeviceAuthenticationService.swift in Sources */, CB24F70C29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift in Sources */, 37CBCA9A2A8966E60050218F /* SyncSettingsAdapter.swift in Sources */, + EEC4A66D2B2C894F00F7C0AA /* VPNLocationPreferenceItemModel.swift in Sources */, 3776582F27F82E62009A6B35 /* AutofillPreferences.swift in Sources */, AAD8078727B3F45600CF7703 /* WebsiteBreakage.swift in Sources */, 7BEC20422B0F505F00243D3E /* BookmarkAddPopoverViewController.swift in Sources */, @@ -11223,6 +11269,7 @@ B6DB3CFB26A17CB800D459B7 /* PermissionModel.swift in Sources */, 4B92929C26670D2A00AD2C21 /* PasteboardFolder.swift in Sources */, 3171D6B82889849F0068632A /* CookieManagedNotificationView.swift in Sources */, + EEA3EEB32B24EC0600E8333A /* VPNLocationViewModel.swift in Sources */, B6106BAB26A7BF1D0013B453 /* PermissionType.swift in Sources */, AAC6881B28626C1900D54247 /* RecentlyClosedWindow.swift in Sources */, 85707F2A276A35FE00DC0649 /* ActionSpeech.swift in Sources */, @@ -11436,6 +11483,7 @@ 4BA1A69B258B076900F6F690 /* FileStore.swift in Sources */, B6A9E47F26146A800067D1B9 /* PixelArguments.swift in Sources */, 37BF3F21286F0A7A00BD9014 /* PinnedTabsViewModel.swift in Sources */, + EEC4A6692B2C87D300F7C0AA /* VPNLocationView.swift in Sources */, AAC5E4D225D6A709007F5990 /* BookmarkList.swift in Sources */, B602E81D2A1E25B1006D261F /* NEOnDemandRuleExtension.swift in Sources */, 4B9292D12667123700AD2C21 /* BookmarkTableRowView.swift in Sources */, diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index b7a82331a4..d4084d6d47 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -302,10 +302,34 @@ struct UserText { // VPN Setting Titles + static let vpnLocationTitle = NSLocalizedString("vpn.location.title", value: "Location", comment: "Location section title in VPN settings") static let vpnGeneralTitle = NSLocalizedString("vpn.general.title", value: "General", comment: "General section title in VPN settings") static let vpnNotificationsSettingsTitle = NSLocalizedString("vpn.notifications.settings.title", value: "Notifications", comment: "Notifications section title in VPN settings") static let vpnAdvancedSettingsTitle = NSLocalizedString("vpn.advanced.settings.title", value: "Advanced", comment: "VPN Advanced section title in VPN settings") + // VPN Location + + static let vpnLocationChangeButtonTitle = NSLocalizedString("vpn.location.change.button.title", value: "Change...", comment: "Title of the VPN location preference change button") + static let vpnLocationListTitle = NSLocalizedString("vpn.location.list.title", value: "VPN Location", comment: "Title of the VPN location list screen") + static let vpnLocationRecommendedSectionTitle = NSLocalizedString("vpn.location.recommended.section.title", value: "Recommended", comment: "Title of the VPN location list recommended section") + static let vpnLocationCustomSectionTitle = NSLocalizedString("vpn.location.custom.section.title", value: "Custom", comment: "Title of the VPN location list custom section") + static let vpnLocationSubmitButtonTitle = NSLocalizedString("vpn.location.submit.button.title", value: "Submit", comment: "Title of the VPN location list submit button") + static let vpnLocationCancelButtonTitle = NSLocalizedString("vpn.location.custom.section.title", value: "Cancel", comment: "Title of the VPN location list cancel button") + static let vpnLocationNearest = NSLocalizedString( + "vpn.location.description.nearest", + value: "Nearest", + comment: "Nearest city setting description") + static let vpnLocationNearestAvailable = NSLocalizedString( + "vpn.location.description.nearest.available", + value: "Nearest Available", + comment: "Nearest available location setting description") + static let vpnLocationNearestAvailableSubtitle = NSLocalizedString("vpn.location.nearest.available.title", value: "Automatically connect to the nearest server we can find.", comment: "Subtitle underneath the nearest available vpn location preference text.") + + static func vpnLocationCountryItemFormattedCitiesCount(_ count: Int) -> String { + let message = NSLocalizedString("network.protection.vpn.location.country.item.formatted.cities.count", value: "%d cities", comment: "Subtitle of countries item when there are multiple cities, example : ") + return String(format: message, count) + } + // VPN Settings static let vpnConnectOnLoginSettingTitle = NSLocalizedString( diff --git a/DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift b/DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift index 8b784addef..66fba3ac63 100644 --- a/DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift +++ b/DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift @@ -25,6 +25,8 @@ public enum FeatureFlag: String { /// Add experimental atb parameter to SERP queries for internal users to display Privacy Reminder /// https://app.asana.com/0/1199230911884351/1205979030848528/f case appendAtbToSerpQueries + + case vpnGeoswitching } extension FeatureFlag: FeatureFlagSourceProviding { @@ -34,6 +36,8 @@ extension FeatureFlag: FeatureFlagSourceProviding { return .internalOnly case .appendAtbToSerpQueries: return .internalOnly + case .vpnGeoswitching: + return .internalOnly } } } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/NetworkProtectionVPNCountryLabelsModel.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/NetworkProtectionVPNCountryLabelsModel.swift new file mode 100644 index 0000000000..12bfce37da --- /dev/null +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/NetworkProtectionVPNCountryLabelsModel.swift @@ -0,0 +1,45 @@ +// +// NetworkProtectionVPNCountryLabelsModel.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#if NETWORK_PROTECTION + +import Foundation +import NetworkProtection + +struct NetworkProtectionVPNCountryLabelsModel { + let emoji: String + let title: String + + init(country: String) { + self.title = Locale.current.localizedString(forRegionCode: country) ?? country.capitalized + self.emoji = Self.flag(country: country) + } + + private static func flag(country: String) -> String { + let flagBase = UnicodeScalar("🇦").value - UnicodeScalar("A").value + + let flag = country + .uppercased() + .unicodeScalars + .compactMap({ UnicodeScalar(flagBase + $0.value)?.description }) + .joined() + return flag + } +} + +#endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationPreferenceItem.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationPreferenceItem.swift new file mode 100644 index 0000000000..ee139d7de2 --- /dev/null +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationPreferenceItem.swift @@ -0,0 +1,67 @@ +// +// NetworkProtectionVPNLocationPreferenceItem.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#if NETWORK_PROTECTION + +import Foundation +import SwiftUI + +struct VPNLocationPreferenceItem: View { + let model: VPNLocationPreferenceItemModel + @State private var isShowingLocationSheet: Bool = false + + var body: some View { + VStack(alignment: .leading) { + HStack(spacing: 10) { + switch model.icon { + case .defaultIcon: + Image(systemName: "location.fill") + .resizable() + .frame(width: 18, height: 18) + case .emoji(let string): + Text(string).font(.system(size: 20)) + } + + VStack(alignment: .leading) { + Text(model.title) + .font(.system(size: 13)) + .foregroundColor(.primary) + if let subtitle = model.subtitle { + Text(subtitle) + .font(.system(size: 11)) + .foregroundColor(.secondary) + } + } + Spacer() + Button(UserText.vpnLocationChangeButtonTitle) { + isShowingLocationSheet = true + } + .sheet(isPresented: $isShowingLocationSheet) { + VPNLocationView(isPresented: $isShowingLocationSheet) + } + } + } + .frame(idealWidth: .infinity, maxWidth: .infinity, alignment: .topLeading) + .padding(10) + .background(Color("BlackWhite1")) + .roundedBorder() + } + +} + +#endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationPreferenceItemModel.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationPreferenceItemModel.swift new file mode 100644 index 0000000000..ea0e505b32 --- /dev/null +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationPreferenceItemModel.swift @@ -0,0 +1,49 @@ +// +// NetworkProtectionLocationSettingsItemModel.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#if NETWORK_PROTECTION + +import Foundation +import NetworkProtection + +struct VPNLocationPreferenceItemModel { + enum LocationIcon { + case defaultIcon + case emoji(String) + } + + let title: String + let subtitle: String? + let icon: LocationIcon + + init(selectedLocation: VPNSettings.SelectedLocation) { + switch selectedLocation { + case .nearest: + title = UserText.vpnLocationNearestAvailable + subtitle = UserText.vpnLocationNearestAvailableSubtitle + icon = .defaultIcon + case .location(let location): + let countryLabelsModel = NetworkProtectionVPNCountryLabelsModel(country: location.country) + title = countryLabelsModel.title + subtitle = selectedLocation.location?.city + icon = .emoji(countryLabelsModel.emoji) + } + } +} + +#endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationView.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationView.swift new file mode 100644 index 0000000000..a19ecca45d --- /dev/null +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationView.swift @@ -0,0 +1,245 @@ +// +// NetworkProtectionVPNLocationView.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#if NETWORK_PROTECTION + +import SwiftUI +import SwiftUIExtensions + +struct VPNLocationView: View { + @StateObject var model = VPNLocationViewModel() + @Binding var isPresented: Bool + + var body: some View { + VStack(alignment: .leading) { + Text(UserText.vpnLocationListTitle) + .font(.system(size: 17, weight: .bold)) + .foregroundColor(.primary) + VStack(alignment: .leading, spacing: 16) { + nearest(isSelected: model.isNearestSelected) + countries() + } + .padding(0) + } + .padding(.horizontal, 56) + .padding(.top, 32) + .padding(.bottom, 20) + .frame(minWidth: 624, maxWidth: .infinity, minHeight: 514, maxHeight: 514, alignment: .top) + Spacer() + Group { + VPNLocationViewButtons( + onDone: { + model.onSubmit() + isPresented = false + }, onCancel: { + isPresented = false + }) + .navigationTitle(UserText.vpnFeedbackFormTitle) + .onAppear { + Task { + await model.onViewAppeared() + } + } + } + .background(Color.secondary.opacity(0.1)) + } + + @ViewBuilder + private func nearest(isSelected: Bool) -> some View { + PreferencePaneSection(vericalPadding: 12) { + Text(UserText.vpnLocationRecommendedSectionTitle) + .font(.system(size: 15)) + .foregroundColor(.primary) + ChecklistItem( + isSelected: isSelected, + action: { + Task { + await model.onNearestItemSelection() + } + }, label: { + Image(systemName: "location.fill") + .resizable() + .frame(width: 18, height: 18) + VStack(alignment: .leading, spacing: 4) { + Text(UserText.vpnLocationNearestAvailable) + .foregroundColor(.primary) + Text(UserText.vpnLocationNearestAvailableSubtitle) + .font(.system(size: 11)) + .foregroundColor(.secondary) + } + } + ) + .frame(idealWidth: .infinity, maxWidth: .infinity) + .padding(10) + .background(Color("BlackWhite1")) + .roundedBorder() + } + } + + @ViewBuilder + private func countries() -> some View { + switch model.state { + case .loading: + EmptyView() + .listRowBackground(Color.clear) + case .loaded(let countryItems): + PreferencePaneSection(vericalPadding: 12) { + Text(UserText.vpnLocationCustomSectionTitle) + .font(.system(size: 15)) + .foregroundColor(.primary) + LazyVStack(alignment: .leading) { + ForEach(countryItems) { item in + CountryItem( + itemModel: item, + action: { + Task { + await model.onCountryItemSelection(id: item.id) + } + }, cityPickerAction: { selection in + Task { + await model.onCountryItemSelection(id: item.id, cityId: selection) + } + }) + .padding(10) + } + } + .roundedBorder() + } + } + } +} + +private struct CountryItem: View { + let itemModel: VPNCountryItemModel + let action: () -> Void + let cityPickerAction: (String?) -> Void + + private var selectedCityItemBinding: Binding { + Binding { + itemModel.selectedCityItem + } set: { city in + cityPickerAction(city.id) + } + } + + init(itemModel: VPNCountryItemModel, action: @escaping () -> Void, cityPickerAction: @escaping (String?) -> Void) { + self.itemModel = itemModel + self.action = action + self.cityPickerAction = cityPickerAction + } + + var body: some View { + ChecklistItem( + isSelected: itemModel.isSelected, + action: action, + label: { + Text(itemModel.emoji) + VStack(alignment: .leading, spacing: 4) { + Text(itemModel.title) + .foregroundColor(.primary) + if let subtitle = itemModel.subtitle { + Text(subtitle) + .foregroundColor(.secondary) + } + } + if itemModel.shouldShowPicker { + Spacer() + Picker("", selection: selectedCityItemBinding) { + Text(itemModel.nearestCityPickerItem.name) + .tag(itemModel.nearestCityPickerItem) + Divider() + ForEach(itemModel.cityPickerItems) { cityItem in + Text(cityItem.name) + .tag(cityItem) + } + } + .pickerStyle(.menu) + .frame(width: 90) + } + } + ) + } +} + +private struct ChecklistItem: View where Content: View { + let isSelected: Bool + let action: () -> Void + @ViewBuilder let label: () -> Content + + var body: some View { + HStack(spacing: 12) { + Image(systemName: "checkmark") + .foregroundColor(Color.accentColor) + .if(!isSelected) { + $0.hidden() + } + label() + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + .contentShape(Rectangle()) + .background(Color("BlackWhite1")) + .onTapGesture { + action() + } + } +} + +private struct VPNLocationViewButtons: View { + let onDone: () -> Void + let onCancel: () -> Void + + var body: some View { + HStack { + Spacer() + button(text: UserText.vpnLocationCancelButtonTitle, action: onCancel) + .keyboardShortcut(.cancelAction) + .buttonStyle(DismissActionButtonStyle()) + + button(text: UserText.vpnLocationSubmitButtonTitle, action: onDone) + .keyboardShortcut(.defaultAction) + .buttonStyle(DefaultActionButtonStyle(enabled: true)) + } + .padding(.vertical, 16) + .padding(.horizontal, 20) + } + + @ViewBuilder + func button(text: String, action: @escaping () -> Void) -> some View { + Button(text) { + action() + } + } + +} + +extension View { + /// Applies the given transform if the given condition evaluates to `true`. + /// - Parameters: + /// - condition: The condition to evaluate. + /// - transform: The transform to apply to the source `View`. + /// - Returns: Either the original `View` or the modified `View` if the condition is `true`. + @ViewBuilder func `if`(_ condition: Bool, transform: (Self) -> Content) -> some View { + if condition { + transform(self) + } else { + self + } + } +} + +#endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift new file mode 100644 index 0000000000..9ad4aba68f --- /dev/null +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift @@ -0,0 +1,185 @@ +// +// NetworkProtectionVPNLocationViewModel.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#if NETWORK_PROTECTION + +import Foundation +import Combine +import NetworkProtection + +final class VPNLocationViewModel: ObservableObject { + private let locationListRepository: NetworkProtectionLocationListRepository + private let settings: VPNSettings + private var selectedLocation: VPNSettings.SelectedLocation + @Published public var state: LoadingState + @Published public var isNearestSelected: Bool + + enum ViewAction { + case cancel + case submit + } + + enum LoadingState { + case loading + case loaded(countryItems: [VPNCountryItemModel]) + + var isLoading: Bool { + switch self { + case .loading: + return true + case .loaded: + return false + } + } + } + + init(locationListRepository: NetworkProtectionLocationListRepository, settings: VPNSettings) { + self.locationListRepository = locationListRepository + self.settings = settings + state = .loading + selectedLocation = settings.selectedLocation + self.isNearestSelected = selectedLocation == .nearest + } + + func onViewAppeared() async { + await reloadList() + } + + func onNearestItemSelection() async { + selectedLocation = .nearest + await reloadList() + } + + func onCountryItemSelection(id: String, cityId: String? = nil) async { + let location = NetworkProtectionSelectedLocation(country: id, city: cityId) + selectedLocation = .location(location) + await reloadList() + } + + func onSubmit() { + settings.selectedLocation = selectedLocation + } + + @MainActor + private func reloadList() async { + guard let list = try? await locationListRepository.fetchLocationList() else { return } + let isNearestSelected = selectedLocation == .nearest + + let countryItems = list.map { currentLocation in + let isCountrySelected: Bool + var cityPickerItems: [CityItem] + let selectedCityItem: CityItem + + switch selectedLocation { + case .location(let location): + isCountrySelected = location.country == currentLocation.country + cityPickerItems = currentLocation.cities.map { currentCity in + let isCitySelected = currentCity.name == location.city + return CityItem(cityName: currentCity.name) + } + selectedCityItem = location.city.flatMap(CityItem.init(cityName:)) ?? .nearest + case .nearest: + isCountrySelected = false + cityPickerItems = currentLocation.cities.map { currentCity in + CityItem(cityName: currentCity.name) + } + selectedCityItem = .nearest + } + + return VPNCountryItemModel( + netPLocation: currentLocation, + isSelected: isCountrySelected, + cityPickerItems: cityPickerItems, + selectedCityItem: selectedCityItem + ) + } + self.isNearestSelected = isNearestSelected + state = .loaded(countryItems: countryItems) + } +} + +private typealias CountryItem = VPNCountryItemModel +private typealias CityItem = VPNCityItemModel + +struct VPNCountryItemModel: Identifiable { + private let labelsModel: NetworkProtectionVPNCountryLabelsModel + + var emoji: String { + labelsModel.emoji + } + var title: String { + labelsModel.title + } + let isSelected: Bool + var id: String + let subtitle: String? + let nearestCityPickerItem: VPNCityItemModel = .nearest + let cityPickerItems: [VPNCityItemModel] + let selectedCityItem: VPNCityItemModel + let shouldShowPicker: Bool + + fileprivate init(netPLocation: NetworkProtectionLocation, isSelected: Bool, cityPickerItems: [VPNCityItemModel], selectedCityItem: VPNCityItemModel) { + self.labelsModel = .init(country: netPLocation.country) + self.isSelected = isSelected + self.id = netPLocation.country + let hasMultipleCities = netPLocation.cities.count > 1 + self.subtitle = hasMultipleCities ? UserText.vpnLocationCountryItemFormattedCitiesCount(netPLocation.cities.count) : nil + self.cityPickerItems = cityPickerItems + self.shouldShowPicker = hasMultipleCities + self.selectedCityItem = selectedCityItem + } +} + +struct VPNCityItemModel: Identifiable, Hashable { + let id: String + let name: String + + fileprivate init(cityName: String) { + self.id = cityName + self.name = cityName + } +} + +extension VPNCityItemModel { + static var nearest: VPNCityItemModel { + Self.init(cityName: UserText.vpnLocationNearest) + } +} + +extension NetworkProtectionLocationListCompositeRepository { + convenience init() { + let settings = VPNSettings(defaults: .netP) + self.init( + environment: settings.selectedEnvironment, + tokenStore: NetworkProtectionKeychainTokenStore(), + errorEvents: .networkProtectionAppDebugEvents + ) + } +} + +extension VPNLocationViewModel { + convenience init() { + let locationListRepository = NetworkProtectionLocationListCompositeRepository() + self.init( + locationListRepository: locationListRepository, + settings: VPNSettings(defaults: .netP) + ) + } +} + +#endif diff --git a/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift b/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift index 453a505449..54c2a5420e 100644 --- a/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift +++ b/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift @@ -23,9 +23,13 @@ import Combine import Foundation import NetworkProtection import NetworkProtectionUI +import BrowserServicesKit final class VPNPreferencesModel: ObservableObject { + let shouldShowLocationItem: Bool + @Published var locationItem: VPNLocationPreferenceItemModel + @Published var alwaysON = true @Published var connectOnLogin: Bool { @@ -66,7 +70,8 @@ final class VPNPreferencesModel: ObservableObject { private var cancellables = Set() init(settings: VPNSettings = .init(defaults: .netP), - defaults: UserDefaults = .netP) { + defaults: UserDefaults = .netP, + featureFlagger: FeatureFlagger = NSApp.delegateTyped.featureFlagger) { self.settings = settings connectOnLogin = settings.connectOnLogin @@ -75,8 +80,11 @@ final class VPNPreferencesModel: ObservableObject { showInMenuBar = settings.showInMenuBar showUninstallVPN = defaults.networkProtectionOnboardingStatus != .default onboardingStatus = defaults.networkProtectionOnboardingStatus + locationItem = VPNLocationPreferenceItemModel(selectedLocation: settings.selectedLocation) + shouldShowLocationItem = featureFlagger.isFeatureOn(.vpnGeoswitching) subscribeToOnboardingStatusChanges(defaults: defaults) + subscribeToLocationSettingChanges() } func subscribeToOnboardingStatusChanges(defaults: UserDefaults) { @@ -85,6 +93,13 @@ final class VPNPreferencesModel: ObservableObject { .store(in: &cancellables) } + func subscribeToLocationSettingChanges() { + settings.selectedLocationPublisher + .map(VPNLocationPreferenceItemModel.init(selectedLocation:)) + .assign(to: \.locationItem, onWeaklyHeld: self) + .store(in: &cancellables) + } + @MainActor func uninstallVPN() async { let response = await uninstallVPNConfirmationAlert().runModal() diff --git a/DuckDuckGo/Preferences/View/PreferencesVPNView.swift b/DuckDuckGo/Preferences/View/PreferencesVPNView.swift index b30d3b8358..7b16780933 100644 --- a/DuckDuckGo/Preferences/View/PreferencesVPNView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesVPNView.swift @@ -33,6 +33,13 @@ extension Preferences { TextMenuTitle(text: UserText.vpn) + if model.shouldShowLocationItem { + PreferencePaneSection { + TextMenuItemHeader(text: UserText.vpnLocationTitle) + VPNLocationPreferenceItem(model: model.locationItem) + } + } + // SECTION: Manage VPN PreferencePaneSection {