From bc46afa70ac29d9ccd0fcc980cadd4b2f8188341 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Mon, 12 Feb 2024 16:42:14 +0100 Subject: [PATCH 01/44] Fix tag usage in tag_and_merge workflow --- .github/workflows/tag_and_merge.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tag_and_merge.yml b/.github/workflows/tag_and_merge.yml index 8798981f8b..4004a55ac0 100644 --- a/.github/workflows/tag_and_merge.yml +++ b/.github/workflows/tag_and_merge.yml @@ -84,10 +84,11 @@ jobs: if: always() env: GH_TOKEN: ${{ github.token }} + TAG: ${{ steps.create-tag.outputs.tag }} run: | - echo "TAG=${{ steps.create-tag.outputs.tag }}" >> $GITHUB_ENV + echo "TAG=$TAG" >> $GITHUB_ENV echo "WORKFLOW_URL=https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> $GITHUB_ENV - echo "DMG_URL=${{ vars.DMG_URL_ROOT }}duckduckgo-${{ steps.create-tag.outputs.tag }}.dmg" >> $GITHUB_ENV + echo "DMG_URL=${{ vars.DMG_URL_ROOT }}duckduckgo-${TAG//-/.}.dmg" >> $GITHUB_ENV echo "RELEASE_URL=https://github.com/${{ github.repository }}/releases/tag/${{ steps.create-tag.outputs.tag }}" >> $GITHUB_ENV if [[ ${{ steps.create-tag.outputs.tag-created }} == "false" ]]; then last_release_tag=$(gh api /repos/${{ github.repository }}/releases/latest --jq '.tag_name') From 9c7f9bb2b2bfa082a4ff412704a398d938d037c2 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Wed, 14 Feb 2024 19:23:34 +0000 Subject: [PATCH 02/44] Add DBP to the common config file (#2203) Task/Issue URL: https://app.asana.com/0/1203581873609357/1206213121818078/f **Description**: Enable DBP in the App Store release config --- Configuration/Common.xcconfig | 2 +- DuckDuckGo/Waitlist/Waitlist.swift | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Configuration/Common.xcconfig b/Configuration/Common.xcconfig index e83abad09c..04c51ab42e 100644 --- a/Configuration/Common.xcconfig +++ b/Configuration/Common.xcconfig @@ -21,7 +21,7 @@ COMBINE_HIDPI_IMAGES = YES DEVELOPMENT_TEAM = HKE973VLUW DEVELOPMENT_TEAM[config=CI][sdk=*] = -FEATURE_FLAGS = FEEDBACK +FEATURE_FLAGS = FEEDBACK DBP GCC_PREPROCESSOR_DEFINITIONS[config=CI][arch=*][sdk=*] = DEBUG=1 CI=1 $(inherited) GCC_PREPROCESSOR_DEFINITIONS[config=Debug][arch=*][sdk=*] = DEBUG=1 $(inherited) diff --git a/DuckDuckGo/Waitlist/Waitlist.swift b/DuckDuckGo/Waitlist/Waitlist.swift index 6400f655a0..31331e3b63 100644 --- a/DuckDuckGo/Waitlist/Waitlist.swift +++ b/DuckDuckGo/Waitlist/Waitlist.swift @@ -357,7 +357,9 @@ struct DataBrokerProtectionWaitlist: Waitlist { UserDefaults().setValue(true, forKey: UserDefaultsWrapper.Key.shouldShowDBPWaitlistInvitedCardUI.rawValue) sendInviteCodeAvailableNotification { - DataBrokerProtectionExternalWaitlistPixels.fire(pixel: .dataBrokerProtectionWaitlistNotificationShown, frequency: .dailyAndCount) + DispatchQueue.main.async { + DataBrokerProtectionExternalWaitlistPixels.fire(pixel: .dataBrokerProtectionWaitlistNotificationShown, frequency: .dailyAndCount) + } } } } From 4209093d9d90450d85bfc1d76002503c7eec688e Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Thu, 15 Feb 2024 09:18:26 -0800 Subject: [PATCH 03/44] NetP App Store waitlist tweaks (#2197) (#2204) Task/Issue URL: https://app.asana.com/0/1199230911884351/1206604571588642/f Tech Design URL: CC: @Bunn **Description**: This PR cherry picks a recent NetP changes and turns NetP on in release builds. --- Configuration/AppStore.xcconfig | 6 ++-- Configuration/Common.xcconfig | 2 +- .../Tests/UnitTestsAppStore.xcconfig | 2 +- .../NetworkProtectionAppEvents.swift | 2 +- .../NetworkProtectionNavBarButtonModel.swift | 2 ++ .../NetworkProtectionFeatureVisibility.swift | 29 ++++++++++++++++++- .../HomePage/ContinueSetUpModelTests.swift | 6 ++++ 7 files changed, 42 insertions(+), 7 deletions(-) diff --git a/Configuration/AppStore.xcconfig b/Configuration/AppStore.xcconfig index af55b7fa5d..c2ae87c9b5 100644 --- a/Configuration/AppStore.xcconfig +++ b/Configuration/AppStore.xcconfig @@ -31,9 +31,9 @@ GCC_PREPROCESSOR_DEFINITIONS[config=Review][arch=*][sdk=*] = APPSTORE=1 REVIEW=1 MACOSX_DEPLOYMENT_TARGET = 12.3 SWIFT_ACTIVE_COMPILATION_CONDITIONS[arch=*][sdk=*] = APPSTORE $(FEATURE_FLAGS) -SWIFT_ACTIVE_COMPILATION_CONDITIONS[config=CI][arch=*][sdk=*] = APPSTORE DEBUG NETWORK_PROTECTION DBP CI $(FEATURE_FLAGS) -SWIFT_ACTIVE_COMPILATION_CONDITIONS[config=Debug][arch=*][sdk=*] = APPSTORE DEBUG NETWORK_PROTECTION DBP $(FEATURE_FLAGS) -SWIFT_ACTIVE_COMPILATION_CONDITIONS[config=Review][arch=*][sdk=*] = APPSTORE REVIEW NETWORK_PROTECTION DBP $(FEATURE_FLAGS) +SWIFT_ACTIVE_COMPILATION_CONDITIONS[config=CI][arch=*][sdk=*] = APPSTORE DEBUG CI $(FEATURE_FLAGS) +SWIFT_ACTIVE_COMPILATION_CONDITIONS[config=Debug][arch=*][sdk=*] = APPSTORE DEBUG $(FEATURE_FLAGS) +SWIFT_ACTIVE_COMPILATION_CONDITIONS[config=Review][arch=*][sdk=*] = APPSTORE REVIEW $(FEATURE_FLAGS) NETP_BASE_APP_GROUP = $(DEVELOPMENT_TEAM).com.duckduckgo.macos.browser.network-protection NETP_APP_GROUP[config=CI][sdk=macos*] = $(NETP_BASE_APP_GROUP).debug diff --git a/Configuration/Common.xcconfig b/Configuration/Common.xcconfig index 04c51ab42e..5c38d4e8e4 100644 --- a/Configuration/Common.xcconfig +++ b/Configuration/Common.xcconfig @@ -21,7 +21,7 @@ COMBINE_HIDPI_IMAGES = YES DEVELOPMENT_TEAM = HKE973VLUW DEVELOPMENT_TEAM[config=CI][sdk=*] = -FEATURE_FLAGS = FEEDBACK DBP +FEATURE_FLAGS = FEEDBACK DBP NETWORK_PROTECTION GCC_PREPROCESSOR_DEFINITIONS[config=CI][arch=*][sdk=*] = DEBUG=1 CI=1 $(inherited) GCC_PREPROCESSOR_DEFINITIONS[config=Debug][arch=*][sdk=*] = DEBUG=1 $(inherited) diff --git a/Configuration/Tests/UnitTestsAppStore.xcconfig b/Configuration/Tests/UnitTestsAppStore.xcconfig index fb90843360..a885a868ed 100644 --- a/Configuration/Tests/UnitTestsAppStore.xcconfig +++ b/Configuration/Tests/UnitTestsAppStore.xcconfig @@ -16,7 +16,7 @@ #include "UnitTests.xcconfig" #include "../AppStore.xcconfig" -FEATURE_FLAGS = FEEDBACK +FEATURE_FLAGS = FEEDBACK NETWORK_PROTECTION DBP PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.mobile.ios.DuckDuckGoTests diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift index a1e96fa4cf..981e4155b8 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift @@ -57,7 +57,7 @@ final class NetworkProtectionAppEvents { func applicationDidFinishLaunching() { let loginItemsManager = LoginItemsManager() - Task { + Task { @MainActor in await removeLegacyLoginItemAndVPNConfiguration() migrateNetworkProtectionAuthTokenToSharedKeychainIfNecessary() diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift index df8396f857..df0b97b8ac 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift @@ -209,6 +209,7 @@ final class NetworkProtectionNavBarButtonModel: NSObject, ObservableObject { let isWaitlistUser = waitlist.waitlistStorage.isWaitlistUser let hasAuthToken = NetworkProtectionKeychainTokenStore().isFeatureActivated +#if !APPSTORE // If the user hasn't signed up to the waitlist or doesn't have an auth token through some other method, then show them the badged icon // to get their attention and encourage them to sign up. Also avoid showing the button is the user has opened the waitlist UI but // dismissed it. @@ -216,6 +217,7 @@ final class NetworkProtectionNavBarButtonModel: NSObject, ObservableObject { showButton = true return } +#endif } guard !isPinned, diff --git a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift b/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift index 370acfc068..729e2e7114 100644 --- a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift +++ b/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift @@ -65,7 +65,30 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { /// /// Once the waitlist beta has ended, we can trigger a remote change that removes the user's auth token and turn off the waitlist flag, hiding Network Protection from the user. func isNetworkProtectionVisible() -> Bool { - isEasterEggUser || waitlistIsOngoing + #if APPSTORE + return isEasterEggUser || (isUserLocaleAllowed && waitlistIsOngoing) + #else + return isEasterEggUser || waitlistIsOngoing + #endif + } + + var isUserLocaleAllowed: Bool { + var regionCode: String? + if #available(macOS 13, *) { + regionCode = Locale.current.region?.identifier + } else { + regionCode = Locale.current.regionCode + } + + if isInternalUser { + regionCode = "US" + } + + #if DEBUG // Always assume US for debug builds + regionCode = "US" + #endif + + return (regionCode ?? "US") == "US" } /// Whether the user is fully onboarded @@ -129,6 +152,10 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { } } + private var isInternalUser: Bool { + NSApp.delegateTyped.internalUserDecider.isInternalUser + } + func disableForAllUsers() { Task { await featureDisabler.disable(keepAuthToken: false, uninstallSystemExtension: false) diff --git a/UnitTests/HomePage/ContinueSetUpModelTests.swift b/UnitTests/HomePage/ContinueSetUpModelTests.swift index e37b1fff44..e9411db6e1 100644 --- a/UnitTests/HomePage/ContinueSetUpModelTests.swift +++ b/UnitTests/HomePage/ContinueSetUpModelTests.swift @@ -109,6 +109,8 @@ final class ContinueSetUpModelTests: XCTestCase { dataBrokerProtectionRemoteMessaging: MockDataBrokerProtectionRemoteMessaging(), dataBrokerProtectionUserDefaults: userDefaults ) +#else + let messaging = HomePageRemoteMessaging.defaultMessaging() #endif vm = HomePage.Models.ContinueSetUpModel( @@ -543,6 +545,8 @@ final class ContinueSetUpModelTests: XCTestCase { dataBrokerProtectionRemoteMessaging: MockDataBrokerProtectionRemoteMessaging(), dataBrokerProtectionUserDefaults: userDefaults ) +#else + return HomePageRemoteMessaging.defaultMessaging() #endif } @@ -587,6 +591,8 @@ extension HomePage.Models.ContinueSetUpModel { dataBrokerProtectionRemoteMessaging: MockDataBrokerProtectionRemoteMessaging(), dataBrokerProtectionUserDefaults: appGroupUserDefaults ) +#else + let messaging = HomePageRemoteMessaging.defaultMessaging() #endif return HomePage.Models.ContinueSetUpModel( From fe829f797481c36eb669dbc6c3bbe7d61e8428b2 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Mon, 12 Feb 2024 21:18:27 +0100 Subject: [PATCH 04/44] Only report success to Asana when build_notarized is run for the official release (#2194) Task/Issue URL: https://app.asana.com/0/1203301625297703/1206584139104169/f --- .github/workflows/build_notarized.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build_notarized.yml b/.github/workflows/build_notarized.yml index 281a8e77a4..010df3bcc6 100644 --- a/.github/workflows/build_notarized.yml +++ b/.github/workflows/build_notarized.yml @@ -211,6 +211,7 @@ jobs: aws s3 cp ${{ github.workspace }}/release/${{ steps.set-outputs.outputs.dsym-name }} ${{ env.DSYM_S3_PATH }} - name: Report success + if: ${{ env.upload-to == 's3' }} uses: ./.github/actions/asana-log-message env: DSYM_S3_PATH: ${{ steps.upload-dsyms-to-s3.outputs.dsym-s3-path }} @@ -298,6 +299,7 @@ jobs: s3://${{ env.RELEASE_BUCKET_NAME }}/${{ env.RELEASE_BUCKET_PREFIX }}/ - name: Report success + if: ${{ env.upload-to == 's3' }} uses: ./.github/actions/asana-log-message env: DMG_URL: ${{ vars.DMG_URL_ROOT }}${{ steps.create-dmg.outputs.dmg }} From 7ba7857002770165cf275a2f3ff82429e38091f0 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 15 Feb 2024 18:55:40 +0100 Subject: [PATCH 05/44] Add missing secrets to Bump Internal Release workflow --- .github/workflows/bump_internal_release.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/bump_internal_release.yml b/.github/workflows/bump_internal_release.yml index 223162dbc6..58210bd698 100644 --- a/.github/workflows/bump_internal_release.yml +++ b/.github/workflows/bump_internal_release.yml @@ -119,7 +119,9 @@ jobs: MM_HANDLES_BASE64: ${{ secrets.MM_HANDLES_BASE64 }} MM_WEBHOOK_URL: ${{ secrets.MM_WEBHOOK_URL }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_ACCESS_KEY_ID_RELEASE_S3: ${{ secrets.AWS_ACCESS_KEY_ID_RELEASE_S3 }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_SECRET_ACCESS_KEY_RELEASE_S3: ${{ secrets.AWS_SECRET_ACCESS_KEY_RELEASE_S3 }} MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} SSH_PRIVATE_KEY_FASTLANE_MATCH: ${{ secrets.SSH_PRIVATE_KEY_FASTLANE_MATCH }} From d50217109a0e2d51940a21784f630f93c5eca182 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Thu, 15 Feb 2024 18:22:27 +0000 Subject: [PATCH 06/44] Update embedded files --- .../AppPrivacyConfigurationDataProvider.swift | 4 +- .../AppTrackerDataSetProvider.swift | 4 +- DuckDuckGo/ContentBlocker/macos-config.json | 67 +- DuckDuckGo/ContentBlocker/trackerData.json | 2211 +++++++++++------ 4 files changed, 1551 insertions(+), 735 deletions(-) diff --git a/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift b/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift index a8052699b3..c7da20370a 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 = "\"80ac4a1898be3d2e52a76d841bdd21b5\"" - public static let embeddedDataSHA = "c61d9c08e6b54aabddb6a5043bf3dcd940afe996dca0c1f7f1e105156e77a9b8" + public static let embeddedDataETag = "\"4df1da8b0cb81f8545df267fabe244c5\"" + public static let embeddedDataSHA = "22717483451eb903be6194c06300fb77f6c18d7eb060a68ea1d44f19ad07a5a6" } var embeddedDataEtag: String { diff --git a/DuckDuckGo/ContentBlocker/AppTrackerDataSetProvider.swift b/DuckDuckGo/ContentBlocker/AppTrackerDataSetProvider.swift index e7557059a5..8088be236e 100644 --- a/DuckDuckGo/ContentBlocker/AppTrackerDataSetProvider.swift +++ b/DuckDuckGo/ContentBlocker/AppTrackerDataSetProvider.swift @@ -22,8 +22,8 @@ import BrowserServicesKit final class AppTrackerDataSetProvider: EmbeddedDataProvider { public struct Constants { - public static let embeddedDataETag = "\"144361b3801e3d4c33c5aff8d8de3c6b\"" - public static let embeddedDataSHA = "0cf5a43c234d54c3168cc28a65c19b0c5804c15e87aae3e8368d2b2f775a1a8b" + public static let embeddedDataETag = "\"0b6a7a2629abc170a505b92aebd67017\"" + public static let embeddedDataSHA = "32cd805f6be415e77affdf51929494c7add6363234cef58ea8b53ca3a08c86d4" } var embeddedDataEtag: String { diff --git a/DuckDuckGo/ContentBlocker/macos-config.json b/DuckDuckGo/ContentBlocker/macos-config.json index 478a07e8ea..57a7a46e91 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": 1707497831317, + "version": 1708001825742, "features": { "adClickAttribution": { "readme": "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/#3rd-party-tracker-loading-protection", @@ -1068,6 +1068,14 @@ { "domain": "zalando.fr", "reason": "https://github.com/duckduckgo/privacy-configuration/issues/667" + }, + { + "domain": "www.canva.com", + "reason": "https://github.com/duckduckgo/privacy-configuration/issues/1818" + }, + { + "domain": "53.com", + "reason": "https://github.com/duckduckgo/privacy-configuration/issues/1824" } ], "webViewDefault": [ @@ -1091,7 +1099,7 @@ }, "exceptions": [], "state": "enabled", - "hash": "96ca986dfe68dbb73a36dd7f3adbb53e" + "hash": "d135871f701e3aa7e1052427ee721f31" }, "dbp": { "state": "enabled", @@ -1105,6 +1113,9 @@ "steps": [ { "percent": 10 + }, + { + "percent": 50 } ] } @@ -1112,7 +1123,7 @@ }, "exceptions": [], "minSupportedVersion": "1.70.0", - "hash": "c90b300b1e5ab20c29a726c8de9fd2da" + "hash": "b405d032a1519de3059db2e833e81c9a" }, "duckPlayer": { "exceptions": [], @@ -4337,9 +4348,6 @@ { "domain": "tirerack.com" }, - { - "domain": "sephora.com" - }, { "domain": "earth.google.com" }, @@ -4362,7 +4370,7 @@ "privacy-test-pages.site" ] }, - "hash": "0e4353dbfbf35914b784ef12e0073447" + "hash": "1a1373bcf16647d63220659fce650a83" }, "harmfulApis": { "settings": { @@ -4874,6 +4882,16 @@ } ] }, + "adlightning.com": { + "rules": [ + { + "rule": "publisher.adlightning.com/user-api/session/", + "domains": [ + "boltive.com" + ] + } + ] + }, "ads-twitter.com": { "rules": [ { @@ -6122,6 +6140,7 @@ "rule": "googletagmanager.com/gtag/js", "domains": [ "abril.com.br", + "algomalegalclinic.com", "cosmicbook.news", "thesimsresource.com", "tradersync.com" @@ -7080,6 +7099,16 @@ } ] }, + "pubnation.com": { + "rules": [ + { + "rule": "scripts.pubnation.com/tags/", + "domains": [ + "n4g.com" + ] + } + ] + }, "qualtrics.com": { "rules": [ { @@ -7339,6 +7368,16 @@ } ] }, + "sumo.com": { + "rules": [ + { + "rule": "load.sumo.com", + "domains": [ + "glennbeck.com" + ] + } + ] + }, "taboola.com": { "rules": [ { @@ -7765,7 +7804,7 @@ "domain": "sundancecatalog.com" } ], - "hash": "f5cba883ae4a666f7b6135d3406c2797" + "hash": "faa9240f18945b1bb354bcb8b6f7483a" }, "trackingCookies1p": { "settings": { @@ -8152,10 +8191,20 @@ "value": "enabled" } ] + }, + { + "domain": "myhome.experian.co.uk", + "patchSettings": [ + { + "op": "replace", + "path": "/messageHandlers/state", + "value": "enabled" + } + ] } ] }, - "hash": "1a7df2f0fbf4e5838e7c4b21627d2086" + "hash": "151d7ee40451c4aac4badfcc829ea0b5" }, "windowsPermissionUsage": { "exceptions": [], diff --git a/DuckDuckGo/ContentBlocker/trackerData.json b/DuckDuckGo/ContentBlocker/trackerData.json index 0d4648236e..2e842e28c9 100644 --- a/DuckDuckGo/ContentBlocker/trackerData.json +++ b/DuckDuckGo/ContentBlocker/trackerData.json @@ -1,6 +1,6 @@ { "_builtWith": { - "tracker-radar": "56bc133a7354c326d8afcb10b905e6cf865390022e9f2fc69045315332db9afd-4013b4e91930c643394cb31c6c745356f133b04f", + "tracker-radar": "9b6a3c1e62c8a97f63db77d2aef4f185129f05aad0f770425bcb54cbf8e0db84-4013b4e91930c643394cb31c6c745356f133b04f", "tracker-surrogates": "ba0d8cefe4432723ec75b998241efd2454dff35a" }, "readme": "https://github.com/duckduckgo/tracker-blocklists", @@ -418,7 +418,20 @@ "fingerprinting": 1, "cookies": 0, "categories": [], - "default": "block" + "default": "ignore", + "rules": [ + { + "rule": "a2z\\.com\\/resource\\/00000179-3cdd-d40f-a779-bedf7f820000\\/styleguide\\/All\\.min\\.7cb0b3550a4bbcbe1dbaee0522794cc9\\.gz\\.js", + "fingerprinting": 1, + "cookies": 0 + }, + { + "rule": "a2z\\.com\\/x\\.png", + "fingerprinting": 0, + "cookies": 0, + "comment": "pixel" + } + ] }, "aamsitecertifier.com": { "domain": "aamsitecertifier.com", @@ -451,7 +464,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -462,7 +475,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -508,7 +521,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -2396,7 +2409,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -2407,7 +2420,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -2451,7 +2464,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -2462,7 +2475,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -2577,7 +2590,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -2711,7 +2724,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -2722,7 +2735,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -3041,7 +3054,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -3168,7 +3181,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -3292,7 +3305,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -3676,7 +3689,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -3687,7 +3700,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -3698,7 +3711,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -3709,7 +3722,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -3775,7 +3788,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -3827,7 +3840,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -3838,7 +3851,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -3975,7 +3988,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -4326,7 +4339,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -4361,7 +4374,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -4807,7 +4820,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -4818,7 +4831,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -5020,7 +5033,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -5062,7 +5075,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -5085,7 +5098,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -5120,7 +5133,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -5149,7 +5162,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -5437,7 +5450,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -5554,7 +5567,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -5565,7 +5578,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -5576,7 +5589,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -5587,7 +5600,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -5624,7 +5637,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -5679,7 +5692,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -6215,7 +6228,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -6369,7 +6382,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -6685,7 +6698,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -6696,7 +6709,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -6816,7 +6829,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -7012,7 +7025,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -7048,7 +7061,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -7059,7 +7072,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -7070,7 +7083,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -7653,7 +7666,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -7664,7 +7677,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -7675,7 +7688,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -7756,7 +7769,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -7879,7 +7892,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -7890,7 +7903,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -7968,7 +7981,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -7979,7 +7992,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -8094,7 +8107,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -8245,7 +8258,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -8256,7 +8269,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -8747,7 +8760,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -8758,7 +8771,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -8799,7 +8812,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -9215,7 +9228,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -9820,7 +9833,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -9831,7 +9844,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -9842,7 +9855,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -9882,7 +9895,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -9924,7 +9937,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -9978,7 +9991,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -10046,7 +10059,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -10057,7 +10070,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -10105,7 +10118,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -10231,7 +10244,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -10391,7 +10404,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -10402,7 +10415,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -10413,7 +10426,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -10440,7 +10453,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -10490,7 +10503,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -10537,7 +10550,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -11015,7 +11028,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -11055,7 +11068,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -11791,7 +11804,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -11880,7 +11893,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -11891,7 +11904,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -12113,7 +12126,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -12153,7 +12166,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -12164,7 +12177,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -12175,7 +12188,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -12186,7 +12199,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -12512,7 +12525,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -12523,7 +12536,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -12534,7 +12547,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -14442,7 +14455,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -15127,7 +15140,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -15202,7 +15215,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -15253,7 +15266,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -16112,7 +16125,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -16123,7 +16136,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -16152,7 +16165,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -16371,7 +16384,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -16680,7 +16693,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -16691,7 +16704,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -16876,7 +16889,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -16925,7 +16938,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -17199,7 +17212,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -17210,7 +17223,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -17773,7 +17786,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -18121,7 +18134,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -18260,7 +18273,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -18619,7 +18632,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -18830,7 +18843,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -20007,7 +20020,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -20228,7 +20241,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -20239,7 +20252,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -20250,7 +20263,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -20390,7 +20403,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -20877,7 +20890,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -20906,7 +20919,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -20917,7 +20930,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -20928,7 +20941,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -20992,7 +21005,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -21072,7 +21085,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -21120,7 +21133,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -21131,7 +21144,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -21171,7 +21184,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -21182,7 +21195,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -21217,7 +21230,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -21228,7 +21241,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -21361,7 +21374,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -21445,7 +21458,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -21916,7 +21929,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -21927,7 +21940,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -21938,7 +21951,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22008,7 +22021,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22019,7 +22032,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22172,7 +22185,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22183,7 +22196,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22225,7 +22238,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22236,7 +22249,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22247,7 +22260,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22517,7 +22530,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22528,7 +22541,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22589,7 +22602,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22647,7 +22660,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22658,7 +22671,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22758,7 +22771,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22781,7 +22794,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22792,7 +22805,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23149,7 +23162,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23253,7 +23266,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23389,7 +23402,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23464,7 +23477,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23475,7 +23488,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23519,7 +23532,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23530,7 +23543,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23541,7 +23554,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23552,7 +23565,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23632,7 +23645,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23665,7 +23678,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23716,7 +23729,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23852,7 +23865,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23981,7 +23994,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23992,7 +24005,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -24239,7 +24252,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -24250,7 +24263,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -24261,7 +24274,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -24542,7 +24555,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -24577,7 +24590,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -24618,7 +24631,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -24734,7 +24747,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -24745,7 +24758,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -24774,7 +24787,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -24870,7 +24883,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -24927,7 +24940,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -24938,7 +24951,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25059,7 +25072,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25216,7 +25229,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25242,7 +25255,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25253,7 +25266,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25316,7 +25329,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25327,7 +25340,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25338,7 +25351,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25569,7 +25582,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25596,7 +25609,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25607,7 +25620,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25618,7 +25631,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25648,7 +25661,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25659,7 +25672,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25687,7 +25700,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25698,7 +25711,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25771,7 +25784,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25819,7 +25832,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25830,7 +25843,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25841,7 +25854,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25852,7 +25865,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25863,7 +25876,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25874,7 +25887,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25946,7 +25959,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25957,7 +25970,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -26011,7 +26024,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -26213,7 +26226,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -26521,7 +26534,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -26532,7 +26545,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -26543,7 +26556,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -26794,7 +26807,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -26805,7 +26818,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -27223,7 +27236,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -28080,7 +28093,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -28210,7 +28223,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -28221,7 +28234,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -28354,7 +28367,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -28437,7 +28450,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -28448,7 +28461,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -28809,7 +28822,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -29116,7 +29129,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -29127,7 +29140,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -29253,7 +29266,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -29264,7 +29277,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -31006,7 +31019,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -31311,7 +31324,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32471,7 +32484,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32482,7 +32495,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32493,7 +32506,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32504,7 +32517,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32515,7 +32528,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32526,7 +32539,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32537,7 +32550,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32548,7 +32561,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32559,7 +32572,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32570,7 +32583,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32581,7 +32594,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32592,7 +32605,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32603,7 +32616,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32614,7 +32627,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32625,7 +32638,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32636,7 +32649,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32647,7 +32660,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32658,7 +32671,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32669,7 +32682,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32680,7 +32693,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "ambientdusk.com": { + "domain": "ambientdusk.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32691,7 +32715,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "ambrosialsummit.com": { + "domain": "ambrosialsummit.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32702,7 +32737,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32713,7 +32748,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32724,7 +32759,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "analyzecorona.com": { + "domain": "analyzecorona.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32735,7 +32781,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32746,7 +32792,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32757,7 +32803,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32768,7 +32814,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32779,7 +32825,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32790,7 +32836,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32801,7 +32847,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32812,7 +32858,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32823,7 +32869,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32834,7 +32880,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32845,7 +32891,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32856,7 +32902,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32867,7 +32913,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32878,7 +32924,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32889,7 +32935,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32900,7 +32946,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32911,7 +32957,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32922,7 +32968,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32933,7 +32979,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32944,7 +32990,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32955,7 +33001,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32966,7 +33012,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32977,7 +33023,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32988,7 +33034,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32999,7 +33045,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33010,7 +33056,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33021,7 +33067,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33032,7 +33078,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33043,7 +33089,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33054,7 +33100,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33065,7 +33111,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33076,7 +33122,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33087,7 +33133,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33098,7 +33144,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33109,7 +33155,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33120,7 +33166,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33131,7 +33177,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33142,7 +33188,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33153,7 +33199,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33164,7 +33210,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33175,7 +33221,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33186,7 +33232,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33197,7 +33243,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33208,7 +33254,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33219,7 +33265,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33230,7 +33276,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33241,7 +33287,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33252,7 +33298,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33263,7 +33309,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33274,7 +33320,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33285,7 +33331,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33296,7 +33342,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33307,7 +33353,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33318,7 +33364,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33329,7 +33375,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33340,7 +33386,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33351,7 +33397,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33362,7 +33408,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33373,7 +33419,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "celestialquasar.com": { + "domain": "celestialquasar.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33384,7 +33441,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33395,7 +33452,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33406,7 +33463,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33417,7 +33474,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33428,7 +33485,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33439,7 +33496,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33450,7 +33507,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33461,7 +33518,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33472,7 +33529,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33483,7 +33540,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33494,7 +33551,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33505,7 +33562,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33516,7 +33573,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33527,7 +33584,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33538,7 +33595,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33549,7 +33606,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33560,7 +33617,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33571,7 +33628,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33582,7 +33639,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33593,7 +33650,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33604,7 +33661,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33615,7 +33672,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33626,7 +33683,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33637,7 +33694,40 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "coordinatedcoat.com": { + "domain": "coordinatedcoat.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "copycarpenter.com": { + "domain": "copycarpenter.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "cosmicsculptor.com": { + "domain": "cosmicsculptor.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33648,7 +33738,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33659,7 +33749,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33670,7 +33760,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33681,7 +33771,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33692,7 +33782,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33703,7 +33793,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33714,7 +33804,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33725,7 +33815,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33736,7 +33826,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33747,7 +33837,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33758,7 +33848,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33769,7 +33859,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33780,7 +33870,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33791,7 +33881,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33802,7 +33892,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33813,7 +33903,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33824,7 +33914,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33835,7 +33925,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33846,7 +33936,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33857,7 +33947,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33868,7 +33958,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33879,7 +33969,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33890,7 +33980,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33901,7 +33991,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33912,7 +34002,29 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "deliciousducks.com": { + "domain": "deliciousducks.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "dependenttrip.com": { + "domain": "dependenttrip.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33923,7 +34035,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33934,7 +34046,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33945,7 +34057,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33956,7 +34068,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33967,7 +34079,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33978,7 +34090,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33989,7 +34101,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34000,7 +34112,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34011,7 +34123,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34022,7 +34134,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34033,7 +34145,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "effervescentvista.com": { + "domain": "effervescentvista.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34044,7 +34167,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34055,7 +34178,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34066,7 +34189,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "enchantingmystique.com": { + "domain": "enchantingmystique.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34077,7 +34211,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34088,7 +34222,29 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "enigmaticcanyon.com": { + "domain": "enigmaticcanyon.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "enigmaticvoyage.com": { + "domain": "enigmaticvoyage.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34099,7 +34255,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34110,7 +34266,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34121,7 +34277,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34132,7 +34288,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "evasivejar.com": { + "domain": "evasivejar.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34143,7 +34310,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34154,7 +34321,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34165,7 +34332,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34176,7 +34343,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34187,7 +34354,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34198,7 +34365,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34209,7 +34376,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34220,7 +34387,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34231,7 +34398,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34242,7 +34409,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34253,7 +34420,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34264,7 +34431,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34275,7 +34442,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34286,7 +34453,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34297,7 +34464,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34308,7 +34475,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34319,7 +34486,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34330,7 +34497,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34341,7 +34508,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34352,7 +34519,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34363,7 +34530,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34374,7 +34541,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34385,7 +34552,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34396,7 +34563,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34407,7 +34574,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34418,7 +34585,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34429,7 +34596,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34440,7 +34607,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34451,7 +34618,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34462,7 +34629,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "friendlycrayon.com": { + "domain": "friendlycrayon.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34473,7 +34651,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34484,7 +34662,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34495,7 +34673,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34506,7 +34684,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34517,7 +34695,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34528,7 +34706,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34539,7 +34717,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34550,7 +34728,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34561,7 +34739,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34572,7 +34750,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34583,7 +34761,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34594,7 +34772,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34605,7 +34783,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34616,7 +34794,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "gracefulmilk.com": { + "domain": "gracefulmilk.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34627,7 +34816,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34638,7 +34827,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34649,7 +34838,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34660,7 +34849,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34671,7 +34860,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34682,7 +34871,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34693,7 +34882,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34704,7 +34893,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34715,7 +34904,29 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "halcyoncanyon.com": { + "domain": "halcyoncanyon.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "halcyonsculpture.com": { + "domain": "halcyonsculpture.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34726,7 +34937,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34737,7 +34948,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34748,7 +34959,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34759,7 +34970,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34770,7 +34981,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34781,7 +34992,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34792,7 +35003,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34803,7 +35014,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34814,7 +35025,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34825,7 +35036,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34836,7 +35047,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34847,7 +35058,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34858,7 +35069,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34869,7 +35080,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34880,7 +35091,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34891,7 +35102,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34902,7 +35113,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34913,7 +35124,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34924,7 +35135,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34935,7 +35146,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34946,7 +35157,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34957,7 +35168,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34968,7 +35179,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34979,7 +35190,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34990,7 +35201,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35001,7 +35212,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35012,7 +35223,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35023,7 +35234,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35034,7 +35245,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35045,7 +35256,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35056,7 +35267,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35067,7 +35278,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35078,7 +35289,40 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "jubilantcascade.com": { + "domain": "jubilantcascade.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "jubilantglimmer.com": { + "domain": "jubilantglimmer.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "jubilantwhisper.com": { + "domain": "jubilantwhisper.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35089,7 +35333,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35100,7 +35344,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35111,7 +35355,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35122,7 +35366,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35133,7 +35377,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35144,7 +35388,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35155,7 +35399,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35166,7 +35410,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35177,7 +35421,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35188,7 +35432,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35199,7 +35443,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35210,7 +35454,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "loadsurprise.com": { + "domain": "loadsurprise.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35221,7 +35476,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35232,7 +35487,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35243,7 +35498,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35254,7 +35509,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35265,7 +35520,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "luminouscatalyst.com": { + "domain": "luminouscatalyst.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35276,7 +35542,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "lustroushaven.com": { + "domain": "lustroushaven.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35287,7 +35564,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35298,7 +35575,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35309,7 +35586,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35320,7 +35597,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35331,7 +35608,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35342,7 +35619,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35353,7 +35630,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35364,7 +35641,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35375,7 +35652,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35386,7 +35663,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35397,7 +35674,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35408,7 +35685,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35419,7 +35696,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35430,7 +35707,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35441,7 +35718,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35452,7 +35729,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35463,7 +35740,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35474,7 +35751,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35485,7 +35762,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35496,7 +35773,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35507,7 +35784,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "mysticalagoon.com": { + "domain": "mysticalagoon.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35518,7 +35806,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35529,7 +35817,29 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "nebulacrescent.com": { + "domain": "nebulacrescent.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "nebulajubilee.com": { + "domain": "nebulajubilee.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35540,7 +35850,40 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "nebulousgarden.com": { + "domain": "nebulousgarden.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "nebulousquasar.com": { + "domain": "nebulousquasar.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "nebulousripple.com": { + "domain": "nebulousripple.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35551,7 +35894,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "niftyhospital.com": { + "domain": "niftyhospital.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35562,7 +35916,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35573,7 +35927,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35584,7 +35938,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35595,7 +35949,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35606,7 +35960,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35617,7 +35971,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35628,7 +35982,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35639,7 +35993,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35650,7 +36004,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35661,7 +36015,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35672,7 +36026,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35683,7 +36037,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35694,7 +36048,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35705,7 +36059,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35716,7 +36070,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35727,7 +36081,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "parallelbulb.com": { + "domain": "parallelbulb.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35738,7 +36103,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35749,7 +36114,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35760,7 +36125,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35771,7 +36136,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "piquantvortex.com": { + "domain": "piquantvortex.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35782,7 +36158,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35793,7 +36169,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35804,7 +36180,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35815,7 +36191,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35826,7 +36202,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35837,7 +36213,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "pointlessprofit.com": { + "domain": "pointlessprofit.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35848,7 +36235,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35859,7 +36246,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35870,7 +36257,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35881,7 +36268,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35892,7 +36279,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35903,7 +36290,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35914,7 +36301,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35925,7 +36312,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35936,7 +36323,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35947,7 +36334,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35958,7 +36345,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35969,7 +36356,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35980,7 +36367,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35991,7 +36378,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36002,7 +36389,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "radiantlullaby.com": { + "domain": "radiantlullaby.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36013,7 +36411,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36024,7 +36422,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36035,7 +36433,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36046,7 +36444,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36057,7 +36455,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36068,7 +36466,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36079,7 +36477,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36090,7 +36488,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36101,7 +36499,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36112,7 +36510,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36123,7 +36521,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "reconditeprison.com": { + "domain": "reconditeprison.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36134,7 +36543,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36145,7 +36554,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36156,7 +36565,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36167,7 +36576,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36178,7 +36587,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36189,7 +36598,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "resplendentecho.com": { + "domain": "resplendentecho.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36200,7 +36620,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36211,7 +36631,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36222,7 +36642,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36233,7 +36653,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36244,7 +36664,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36255,7 +36675,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36266,7 +36686,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36277,7 +36697,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36288,7 +36708,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36299,7 +36719,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36310,7 +36730,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36321,7 +36741,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36332,7 +36752,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "scaredslip.com": { + "domain": "scaredslip.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36343,7 +36774,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36354,7 +36785,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36365,7 +36796,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36376,7 +36807,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36387,7 +36818,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36398,7 +36829,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36409,7 +36840,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36420,7 +36851,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36431,7 +36862,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36442,7 +36873,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36453,7 +36884,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36464,7 +36895,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36475,7 +36906,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36486,7 +36917,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36497,7 +36928,29 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "seraphicjubilee.com": { + "domain": "seraphicjubilee.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "serenepebble.com": { + "domain": "serenepebble.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36508,7 +36961,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36519,7 +36972,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36530,7 +36983,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36541,7 +36994,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36552,7 +37005,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36563,7 +37016,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36574,7 +37027,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36585,7 +37038,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36596,7 +37049,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36607,7 +37060,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36618,7 +37071,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36629,7 +37082,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36640,7 +37093,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36651,7 +37104,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36662,7 +37115,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36673,7 +37126,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36684,7 +37137,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36695,7 +37148,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36706,7 +37159,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36717,7 +37170,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36728,7 +37181,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36739,7 +37192,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36750,7 +37203,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36761,7 +37214,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36772,7 +37225,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36783,7 +37236,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "soggyzoo.com": { + "domain": "soggyzoo.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36794,7 +37258,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36805,7 +37269,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36816,7 +37280,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36827,7 +37291,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36838,7 +37302,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "soresidewalk.com": { + "domain": "soresidewalk.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36849,7 +37324,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36860,7 +37335,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36871,7 +37346,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36882,7 +37357,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36893,7 +37368,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36904,7 +37379,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36915,7 +37390,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36926,7 +37401,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36937,7 +37412,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36948,7 +37423,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36959,7 +37434,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36970,7 +37445,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36981,7 +37456,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36992,7 +37467,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37003,7 +37478,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37014,7 +37489,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37025,7 +37500,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37036,7 +37511,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37047,7 +37522,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37058,7 +37533,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37069,7 +37544,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37080,7 +37555,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37091,7 +37566,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37102,7 +37577,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37113,7 +37588,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37124,7 +37599,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37135,7 +37610,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37146,7 +37621,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37157,7 +37632,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37168,7 +37643,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37179,7 +37654,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37190,7 +37665,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "stripedbat.com": { + "domain": "stripedbat.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37201,7 +37687,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37212,7 +37698,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37223,7 +37709,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37234,7 +37720,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37245,7 +37731,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37256,7 +37742,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37267,7 +37753,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37278,7 +37764,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37289,7 +37775,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37300,7 +37786,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37311,7 +37797,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37322,7 +37808,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37333,7 +37819,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37344,7 +37830,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37355,7 +37841,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37366,7 +37852,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37377,7 +37863,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37388,7 +37874,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37399,7 +37885,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37410,7 +37896,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37421,7 +37907,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37432,7 +37918,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37443,7 +37929,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "thingstaste.com": { + "domain": "thingstaste.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37454,7 +37951,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37465,7 +37962,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37476,7 +37973,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37487,7 +37984,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37498,7 +37995,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37509,7 +38006,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37520,7 +38017,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "tranquilcan.com": { + "domain": "tranquilcan.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37531,7 +38039,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "tranquilplume.com": { + "domain": "tranquilplume.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37542,7 +38061,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37553,7 +38072,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37564,7 +38083,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37575,7 +38094,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37586,7 +38105,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37597,7 +38116,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37608,7 +38127,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37619,7 +38138,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37630,7 +38149,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37641,7 +38160,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37652,7 +38171,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37663,7 +38182,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37674,7 +38193,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37685,7 +38204,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37696,7 +38215,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37707,7 +38226,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37718,7 +38237,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37729,7 +38248,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37740,7 +38259,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37751,7 +38270,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37762,7 +38281,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37773,7 +38292,40 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "vanishmemory.com": { + "domain": "vanishmemory.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "velvetquasar.com": { + "domain": "velvetquasar.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "venomousvessel.com": { + "domain": "venomousvessel.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37784,7 +38336,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37795,7 +38347,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "verdantloom.com": { + "domain": "verdantloom.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37806,7 +38369,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "vibrantgale.com": { + "domain": "vibrantgale.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37817,7 +38391,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "vibranttalisman.com": { + "domain": "vibranttalisman.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37828,7 +38413,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "vividmeadow.com": { + "domain": "vividmeadow.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37839,7 +38435,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37850,7 +38446,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37861,7 +38457,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37872,7 +38468,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "whimsicalcanyon.com": { + "domain": "whimsicalcanyon.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37883,7 +38490,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37894,7 +38501,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "whisperingquasar.com": { + "domain": "whisperingquasar.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37905,7 +38523,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37916,7 +38534,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37927,7 +38545,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "wistfulwaste.com": { + "domain": "wistfulwaste.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37938,7 +38567,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "wretchedfloor.com": { + "domain": "wretchedfloor.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37949,7 +38589,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "zephyrlabyrinth.com": { + "domain": "zephyrlabyrinth.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37960,7 +38611,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37971,7 +38622,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -47997,11 +48648,14 @@ "aliveachiever.com", "alluringbucket.com", "aloofvest.com", + "ambientdusk.com", "ambiguousafternoon.com", "ambiguousdinosaurs.com", + "ambrosialsummit.com", "amethystzenith.com", "amuckafternoon.com", "amusedbucket.com", + "analyzecorona.com", "ancientact.com", "annoyedairport.com", "annoyingacoustics.com", @@ -48083,6 +48737,7 @@ "cautiouscredit.com", "cavecurtain.com", "ceciliavenus.com", + "celestialquasar.com", "celestialspectra.com", "chalkoil.com", "changeablecats.com", @@ -48116,6 +48771,9 @@ "confusedcart.com", "consciouscheese.com", "consciousdirt.com", + "coordinatedcoat.com", + "copycarpenter.com", + "cosmicsculptor.com", "courageousbaby.com", "coverapparatus.com", "cozyhillside.com", @@ -48152,6 +48810,8 @@ "deerbeginner.com", "defeatedbadge.com", "delicatecascade.com", + "deliciousducks.com", + "dependenttrip.com", "detailedkitten.com", "detectdiscovery.com", "devilishdinner.com", @@ -48170,18 +48830,23 @@ "dreamycanyon.com", "dustydime.com", "dustyhammer.com", + "effervescentvista.com", "elasticchange.com", "elderlybean.com", "eminentbubble.com", + "enchantingmystique.com", "encouragingthread.com", "endurablebulb.com", "energeticladybug.com", + "enigmaticcanyon.com", + "enigmaticvoyage.com", "enormousearth.com", "entertainskin.com", "enviousshape.com", "equablekettle.com", "ethereallagoon.com", "evanescentedge.com", + "evasivejar.com", "eventexistence.com", "exampleshake.com", "excitingtub.com", @@ -48225,6 +48890,7 @@ "franticroof.com", "freezingbuilding.com", "frequentflesh.com", + "friendlycrayon.com", "friendwool.com", "fronttoad.com", "fumblingform.com", @@ -48246,6 +48912,7 @@ "gloriousbeef.com", "gondolagnome.com", "gorgeousedge.com", + "gracefulmilk.com", "grainmass.com", "grandfatherguitar.com", "grayoranges.com", @@ -48258,6 +48925,8 @@ "guiltlessbasketball.com", "gulliblegrip.com", "gustygrandmother.com", + "halcyoncanyon.com", + "halcyonsculpture.com", "hallowedinvention.com", "haltingbadge.com", "haltingdivision.com", @@ -48299,6 +48968,9 @@ "internalsink.com", "j93557g.com", "jubilantcanyon.com", + "jubilantcascade.com", + "jubilantglimmer.com", + "jubilantwhisper.com", "kaputquill.com", "knitstamp.com", "knottyswing.com", @@ -48314,6 +48986,7 @@ "livelyreward.com", "livingsleet.com", "lizardslaugh.com", + "loadsurprise.com", "lonelyflavor.com", "longingtrees.com", "looseloaf.com", @@ -48321,8 +48994,10 @@ "losslace.com", "lovelydrum.com", "ludicrousarch.com", + "luminouscatalyst.com", "lumpylumber.com", "lunchroomlock.com", + "lustroushaven.com", "maddeningpowder.com", "maliciousmusic.com", "marketspiders.com", @@ -48352,12 +49027,19 @@ "mundanenail.com", "mushywaste.com", "muteknife.com", + "mysticalagoon.com", "naivestatement.com", "nappyattack.com", "neatshade.com", + "nebulacrescent.com", + "nebulajubilee.com", "nebulousamusement.com", + "nebulousgarden.com", + "nebulousquasar.com", + "nebulousripple.com", "needlessnorth.com", "nervoussummer.com", + "niftyhospital.com", "nightwound.com", "nondescriptcrowd.com", "nondescriptnote.com", @@ -48380,12 +49062,14 @@ "panickycurtain.com", "panickypancake.com", "panoramicplane.com", + "parallelbulb.com", "parchedsofa.com", "parentpicture.com", "partplanes.com", "passivepolo.com", "peacefullimit.com", "petiteumbrella.com", + "piquantvortex.com", "placidactivity.com", "placidperson.com", "planebasin.com", @@ -48397,6 +49081,7 @@ "poeticpackage.com", "pointdigestion.com", "pointlesspocket.com", + "pointlessprofit.com", "politeplanes.com", "politicalporter.com", "possibleboats.com", @@ -48424,6 +49109,7 @@ "quizzicalzephyr.com", "rabbitbreath.com", "rabbitrifle.com", + "radiantlullaby.com", "radiateprose.com", "railwaygiraffe.com", "railwayreason.com", @@ -48440,6 +49126,7 @@ "rebelswing.com", "receptivereaction.com", "recessrain.com", + "reconditeprison.com", "reconditerake.com", "reconditerespect.com", "reflectivestatement.com", @@ -48451,6 +49138,7 @@ "resonantbrush.com", "resonantrock.com", "respectrain.com", + "resplendentecho.com", "restrainstorm.com", "restructureinvention.com", "retrievemint.com", @@ -48474,6 +49162,7 @@ "savoryorange.com", "scarceshock.com", "scaredcomfort.com", + "scaredslip.com", "scaredsnake.com", "scaredsnakes.com", "scaredsong.com", @@ -48499,6 +49188,8 @@ "selectivesummer.com", "selfishsnake.com", "separatesort.com", + "seraphicjubilee.com", + "serenepebble.com", "serioussuit.com", "serpentshampoo.com", "settleshoes.com", @@ -48538,12 +49229,14 @@ "smoggysongs.com", "sneakwind.com", "soggysponge.com", + "soggyzoo.com", "solarislabyrinth.com", "somberscarecrow.com", "sombersticks.com", "songsterritory.com", "soothingglade.com", "sordidsmile.com", + "soresidewalk.com", "soretrain.com", "sortsail.com", "sortsummer.com", @@ -48590,6 +49283,7 @@ "stretchsister.com", "stretchsneeze.com", "stretchsquirrel.com", + "stripedbat.com", "strivesidewalk.com", "strivesquirrel.com", "strokesystem.com", @@ -48626,6 +49320,7 @@ "tendertest.com", "terriblethumb.com", "terrifictooth.com", + "thingstaste.com", "thinkitten.com", "thirdrespect.com", "thomastorch.com", @@ -48636,7 +49331,9 @@ "tiredthroat.com", "tiresomethunder.com", "tradetooth.com", + "tranquilcan.com", "tranquilcanyon.com", + "tranquilplume.com", "tremendousearthquake.com", "tremendousplastic.com", "tritebadge.com", @@ -48665,12 +49362,19 @@ "unwieldyimpulse.com", "unwieldyplastic.com", "uselesslumber.com", + "vanishmemory.com", + "velvetquasar.com", "vengefulgrass.com", + "venomousvessel.com", "venusgloria.com", "verdantanswer.com", + "verdantloom.com", "verseballs.com", + "vibrantgale.com", "vibranthaven.com", + "vibranttalisman.com", "virtualvincent.com", + "vividmeadow.com", "volatileprofit.com", "volatilevessel.com", "voraciousgrip.com", @@ -48679,18 +49383,23 @@ "warmquiver.com", "wearbasin.com", "wellgroomedhydrant.com", + "whimsicalcanyon.com", "whimsicalgrove.com", "whisperingcascade.com", + "whisperingquasar.com", "whisperingsummit.com", "whispermeeting.com", "wildcommittee.com", + "wistfulwaste.com", "workoperation.com", + "wretchedfloor.com", "wrongwound.com", + "zephyrlabyrinth.com", "zestycrime.com", "zipperxray.com", "zlp6s.pw" ], - "prevalence": 0.0151, + "prevalence": 0.0146, "displayName": "Admiral" } }, @@ -49436,11 +50145,14 @@ "aliveachiever.com": "Leven Labs, Inc. DBA Admiral", "alluringbucket.com": "Leven Labs, Inc. DBA Admiral", "aloofvest.com": "Leven Labs, Inc. DBA Admiral", + "ambientdusk.com": "Leven Labs, Inc. DBA Admiral", "ambiguousafternoon.com": "Leven Labs, Inc. DBA Admiral", "ambiguousdinosaurs.com": "Leven Labs, Inc. DBA Admiral", + "ambrosialsummit.com": "Leven Labs, Inc. DBA Admiral", "amethystzenith.com": "Leven Labs, Inc. DBA Admiral", "amuckafternoon.com": "Leven Labs, Inc. DBA Admiral", "amusedbucket.com": "Leven Labs, Inc. DBA Admiral", + "analyzecorona.com": "Leven Labs, Inc. DBA Admiral", "ancientact.com": "Leven Labs, Inc. DBA Admiral", "annoyedairport.com": "Leven Labs, Inc. DBA Admiral", "annoyingacoustics.com": "Leven Labs, Inc. DBA Admiral", @@ -49522,6 +50234,7 @@ "cautiouscredit.com": "Leven Labs, Inc. DBA Admiral", "cavecurtain.com": "Leven Labs, Inc. DBA Admiral", "ceciliavenus.com": "Leven Labs, Inc. DBA Admiral", + "celestialquasar.com": "Leven Labs, Inc. DBA Admiral", "celestialspectra.com": "Leven Labs, Inc. DBA Admiral", "chalkoil.com": "Leven Labs, Inc. DBA Admiral", "changeablecats.com": "Leven Labs, Inc. DBA Admiral", @@ -49555,6 +50268,9 @@ "confusedcart.com": "Leven Labs, Inc. DBA Admiral", "consciouscheese.com": "Leven Labs, Inc. DBA Admiral", "consciousdirt.com": "Leven Labs, Inc. DBA Admiral", + "coordinatedcoat.com": "Leven Labs, Inc. DBA Admiral", + "copycarpenter.com": "Leven Labs, Inc. DBA Admiral", + "cosmicsculptor.com": "Leven Labs, Inc. DBA Admiral", "courageousbaby.com": "Leven Labs, Inc. DBA Admiral", "coverapparatus.com": "Leven Labs, Inc. DBA Admiral", "cozyhillside.com": "Leven Labs, Inc. DBA Admiral", @@ -49591,6 +50307,8 @@ "deerbeginner.com": "Leven Labs, Inc. DBA Admiral", "defeatedbadge.com": "Leven Labs, Inc. DBA Admiral", "delicatecascade.com": "Leven Labs, Inc. DBA Admiral", + "deliciousducks.com": "Leven Labs, Inc. DBA Admiral", + "dependenttrip.com": "Leven Labs, Inc. DBA Admiral", "detailedkitten.com": "Leven Labs, Inc. DBA Admiral", "detectdiscovery.com": "Leven Labs, Inc. DBA Admiral", "devilishdinner.com": "Leven Labs, Inc. DBA Admiral", @@ -49609,18 +50327,23 @@ "dreamycanyon.com": "Leven Labs, Inc. DBA Admiral", "dustydime.com": "Leven Labs, Inc. DBA Admiral", "dustyhammer.com": "Leven Labs, Inc. DBA Admiral", + "effervescentvista.com": "Leven Labs, Inc. DBA Admiral", "elasticchange.com": "Leven Labs, Inc. DBA Admiral", "elderlybean.com": "Leven Labs, Inc. DBA Admiral", "eminentbubble.com": "Leven Labs, Inc. DBA Admiral", + "enchantingmystique.com": "Leven Labs, Inc. DBA Admiral", "encouragingthread.com": "Leven Labs, Inc. DBA Admiral", "endurablebulb.com": "Leven Labs, Inc. DBA Admiral", "energeticladybug.com": "Leven Labs, Inc. DBA Admiral", + "enigmaticcanyon.com": "Leven Labs, Inc. DBA Admiral", + "enigmaticvoyage.com": "Leven Labs, Inc. DBA Admiral", "enormousearth.com": "Leven Labs, Inc. DBA Admiral", "entertainskin.com": "Leven Labs, Inc. DBA Admiral", "enviousshape.com": "Leven Labs, Inc. DBA Admiral", "equablekettle.com": "Leven Labs, Inc. DBA Admiral", "ethereallagoon.com": "Leven Labs, Inc. DBA Admiral", "evanescentedge.com": "Leven Labs, Inc. DBA Admiral", + "evasivejar.com": "Leven Labs, Inc. DBA Admiral", "eventexistence.com": "Leven Labs, Inc. DBA Admiral", "exampleshake.com": "Leven Labs, Inc. DBA Admiral", "excitingtub.com": "Leven Labs, Inc. DBA Admiral", @@ -49664,6 +50387,7 @@ "franticroof.com": "Leven Labs, Inc. DBA Admiral", "freezingbuilding.com": "Leven Labs, Inc. DBA Admiral", "frequentflesh.com": "Leven Labs, Inc. DBA Admiral", + "friendlycrayon.com": "Leven Labs, Inc. DBA Admiral", "friendwool.com": "Leven Labs, Inc. DBA Admiral", "fronttoad.com": "Leven Labs, Inc. DBA Admiral", "fumblingform.com": "Leven Labs, Inc. DBA Admiral", @@ -49685,6 +50409,7 @@ "gloriousbeef.com": "Leven Labs, Inc. DBA Admiral", "gondolagnome.com": "Leven Labs, Inc. DBA Admiral", "gorgeousedge.com": "Leven Labs, Inc. DBA Admiral", + "gracefulmilk.com": "Leven Labs, Inc. DBA Admiral", "grainmass.com": "Leven Labs, Inc. DBA Admiral", "grandfatherguitar.com": "Leven Labs, Inc. DBA Admiral", "grayoranges.com": "Leven Labs, Inc. DBA Admiral", @@ -49697,6 +50422,8 @@ "guiltlessbasketball.com": "Leven Labs, Inc. DBA Admiral", "gulliblegrip.com": "Leven Labs, Inc. DBA Admiral", "gustygrandmother.com": "Leven Labs, Inc. DBA Admiral", + "halcyoncanyon.com": "Leven Labs, Inc. DBA Admiral", + "halcyonsculpture.com": "Leven Labs, Inc. DBA Admiral", "hallowedinvention.com": "Leven Labs, Inc. DBA Admiral", "haltingbadge.com": "Leven Labs, Inc. DBA Admiral", "haltingdivision.com": "Leven Labs, Inc. DBA Admiral", @@ -49738,6 +50465,9 @@ "internalsink.com": "Leven Labs, Inc. DBA Admiral", "j93557g.com": "Leven Labs, Inc. DBA Admiral", "jubilantcanyon.com": "Leven Labs, Inc. DBA Admiral", + "jubilantcascade.com": "Leven Labs, Inc. DBA Admiral", + "jubilantglimmer.com": "Leven Labs, Inc. DBA Admiral", + "jubilantwhisper.com": "Leven Labs, Inc. DBA Admiral", "kaputquill.com": "Leven Labs, Inc. DBA Admiral", "knitstamp.com": "Leven Labs, Inc. DBA Admiral", "knottyswing.com": "Leven Labs, Inc. DBA Admiral", @@ -49753,6 +50483,7 @@ "livelyreward.com": "Leven Labs, Inc. DBA Admiral", "livingsleet.com": "Leven Labs, Inc. DBA Admiral", "lizardslaugh.com": "Leven Labs, Inc. DBA Admiral", + "loadsurprise.com": "Leven Labs, Inc. DBA Admiral", "lonelyflavor.com": "Leven Labs, Inc. DBA Admiral", "longingtrees.com": "Leven Labs, Inc. DBA Admiral", "looseloaf.com": "Leven Labs, Inc. DBA Admiral", @@ -49760,8 +50491,10 @@ "losslace.com": "Leven Labs, Inc. DBA Admiral", "lovelydrum.com": "Leven Labs, Inc. DBA Admiral", "ludicrousarch.com": "Leven Labs, Inc. DBA Admiral", + "luminouscatalyst.com": "Leven Labs, Inc. DBA Admiral", "lumpylumber.com": "Leven Labs, Inc. DBA Admiral", "lunchroomlock.com": "Leven Labs, Inc. DBA Admiral", + "lustroushaven.com": "Leven Labs, Inc. DBA Admiral", "maddeningpowder.com": "Leven Labs, Inc. DBA Admiral", "maliciousmusic.com": "Leven Labs, Inc. DBA Admiral", "marketspiders.com": "Leven Labs, Inc. DBA Admiral", @@ -49791,12 +50524,19 @@ "mundanenail.com": "Leven Labs, Inc. DBA Admiral", "mushywaste.com": "Leven Labs, Inc. DBA Admiral", "muteknife.com": "Leven Labs, Inc. DBA Admiral", + "mysticalagoon.com": "Leven Labs, Inc. DBA Admiral", "naivestatement.com": "Leven Labs, Inc. DBA Admiral", "nappyattack.com": "Leven Labs, Inc. DBA Admiral", "neatshade.com": "Leven Labs, Inc. DBA Admiral", + "nebulacrescent.com": "Leven Labs, Inc. DBA Admiral", + "nebulajubilee.com": "Leven Labs, Inc. DBA Admiral", "nebulousamusement.com": "Leven Labs, Inc. DBA Admiral", + "nebulousgarden.com": "Leven Labs, Inc. DBA Admiral", + "nebulousquasar.com": "Leven Labs, Inc. DBA Admiral", + "nebulousripple.com": "Leven Labs, Inc. DBA Admiral", "needlessnorth.com": "Leven Labs, Inc. DBA Admiral", "nervoussummer.com": "Leven Labs, Inc. DBA Admiral", + "niftyhospital.com": "Leven Labs, Inc. DBA Admiral", "nightwound.com": "Leven Labs, Inc. DBA Admiral", "nondescriptcrowd.com": "Leven Labs, Inc. DBA Admiral", "nondescriptnote.com": "Leven Labs, Inc. DBA Admiral", @@ -49819,12 +50559,14 @@ "panickycurtain.com": "Leven Labs, Inc. DBA Admiral", "panickypancake.com": "Leven Labs, Inc. DBA Admiral", "panoramicplane.com": "Leven Labs, Inc. DBA Admiral", + "parallelbulb.com": "Leven Labs, Inc. DBA Admiral", "parchedsofa.com": "Leven Labs, Inc. DBA Admiral", "parentpicture.com": "Leven Labs, Inc. DBA Admiral", "partplanes.com": "Leven Labs, Inc. DBA Admiral", "passivepolo.com": "Leven Labs, Inc. DBA Admiral", "peacefullimit.com": "Leven Labs, Inc. DBA Admiral", "petiteumbrella.com": "Leven Labs, Inc. DBA Admiral", + "piquantvortex.com": "Leven Labs, Inc. DBA Admiral", "placidactivity.com": "Leven Labs, Inc. DBA Admiral", "placidperson.com": "Leven Labs, Inc. DBA Admiral", "planebasin.com": "Leven Labs, Inc. DBA Admiral", @@ -49836,6 +50578,7 @@ "poeticpackage.com": "Leven Labs, Inc. DBA Admiral", "pointdigestion.com": "Leven Labs, Inc. DBA Admiral", "pointlesspocket.com": "Leven Labs, Inc. DBA Admiral", + "pointlessprofit.com": "Leven Labs, Inc. DBA Admiral", "politeplanes.com": "Leven Labs, Inc. DBA Admiral", "politicalporter.com": "Leven Labs, Inc. DBA Admiral", "possibleboats.com": "Leven Labs, Inc. DBA Admiral", @@ -49863,6 +50606,7 @@ "quizzicalzephyr.com": "Leven Labs, Inc. DBA Admiral", "rabbitbreath.com": "Leven Labs, Inc. DBA Admiral", "rabbitrifle.com": "Leven Labs, Inc. DBA Admiral", + "radiantlullaby.com": "Leven Labs, Inc. DBA Admiral", "radiateprose.com": "Leven Labs, Inc. DBA Admiral", "railwaygiraffe.com": "Leven Labs, Inc. DBA Admiral", "railwayreason.com": "Leven Labs, Inc. DBA Admiral", @@ -49879,6 +50623,7 @@ "rebelswing.com": "Leven Labs, Inc. DBA Admiral", "receptivereaction.com": "Leven Labs, Inc. DBA Admiral", "recessrain.com": "Leven Labs, Inc. DBA Admiral", + "reconditeprison.com": "Leven Labs, Inc. DBA Admiral", "reconditerake.com": "Leven Labs, Inc. DBA Admiral", "reconditerespect.com": "Leven Labs, Inc. DBA Admiral", "reflectivestatement.com": "Leven Labs, Inc. DBA Admiral", @@ -49890,6 +50635,7 @@ "resonantbrush.com": "Leven Labs, Inc. DBA Admiral", "resonantrock.com": "Leven Labs, Inc. DBA Admiral", "respectrain.com": "Leven Labs, Inc. DBA Admiral", + "resplendentecho.com": "Leven Labs, Inc. DBA Admiral", "restrainstorm.com": "Leven Labs, Inc. DBA Admiral", "restructureinvention.com": "Leven Labs, Inc. DBA Admiral", "retrievemint.com": "Leven Labs, Inc. DBA Admiral", @@ -49913,6 +50659,7 @@ "savoryorange.com": "Leven Labs, Inc. DBA Admiral", "scarceshock.com": "Leven Labs, Inc. DBA Admiral", "scaredcomfort.com": "Leven Labs, Inc. DBA Admiral", + "scaredslip.com": "Leven Labs, Inc. DBA Admiral", "scaredsnake.com": "Leven Labs, Inc. DBA Admiral", "scaredsnakes.com": "Leven Labs, Inc. DBA Admiral", "scaredsong.com": "Leven Labs, Inc. DBA Admiral", @@ -49938,6 +50685,8 @@ "selectivesummer.com": "Leven Labs, Inc. DBA Admiral", "selfishsnake.com": "Leven Labs, Inc. DBA Admiral", "separatesort.com": "Leven Labs, Inc. DBA Admiral", + "seraphicjubilee.com": "Leven Labs, Inc. DBA Admiral", + "serenepebble.com": "Leven Labs, Inc. DBA Admiral", "serioussuit.com": "Leven Labs, Inc. DBA Admiral", "serpentshampoo.com": "Leven Labs, Inc. DBA Admiral", "settleshoes.com": "Leven Labs, Inc. DBA Admiral", @@ -49977,12 +50726,14 @@ "smoggysongs.com": "Leven Labs, Inc. DBA Admiral", "sneakwind.com": "Leven Labs, Inc. DBA Admiral", "soggysponge.com": "Leven Labs, Inc. DBA Admiral", + "soggyzoo.com": "Leven Labs, Inc. DBA Admiral", "solarislabyrinth.com": "Leven Labs, Inc. DBA Admiral", "somberscarecrow.com": "Leven Labs, Inc. DBA Admiral", "sombersticks.com": "Leven Labs, Inc. DBA Admiral", "songsterritory.com": "Leven Labs, Inc. DBA Admiral", "soothingglade.com": "Leven Labs, Inc. DBA Admiral", "sordidsmile.com": "Leven Labs, Inc. DBA Admiral", + "soresidewalk.com": "Leven Labs, Inc. DBA Admiral", "soretrain.com": "Leven Labs, Inc. DBA Admiral", "sortsail.com": "Leven Labs, Inc. DBA Admiral", "sortsummer.com": "Leven Labs, Inc. DBA Admiral", @@ -50029,6 +50780,7 @@ "stretchsister.com": "Leven Labs, Inc. DBA Admiral", "stretchsneeze.com": "Leven Labs, Inc. DBA Admiral", "stretchsquirrel.com": "Leven Labs, Inc. DBA Admiral", + "stripedbat.com": "Leven Labs, Inc. DBA Admiral", "strivesidewalk.com": "Leven Labs, Inc. DBA Admiral", "strivesquirrel.com": "Leven Labs, Inc. DBA Admiral", "strokesystem.com": "Leven Labs, Inc. DBA Admiral", @@ -50065,6 +50817,7 @@ "tendertest.com": "Leven Labs, Inc. DBA Admiral", "terriblethumb.com": "Leven Labs, Inc. DBA Admiral", "terrifictooth.com": "Leven Labs, Inc. DBA Admiral", + "thingstaste.com": "Leven Labs, Inc. DBA Admiral", "thinkitten.com": "Leven Labs, Inc. DBA Admiral", "thirdrespect.com": "Leven Labs, Inc. DBA Admiral", "thomastorch.com": "Leven Labs, Inc. DBA Admiral", @@ -50075,7 +50828,9 @@ "tiredthroat.com": "Leven Labs, Inc. DBA Admiral", "tiresomethunder.com": "Leven Labs, Inc. DBA Admiral", "tradetooth.com": "Leven Labs, Inc. DBA Admiral", + "tranquilcan.com": "Leven Labs, Inc. DBA Admiral", "tranquilcanyon.com": "Leven Labs, Inc. DBA Admiral", + "tranquilplume.com": "Leven Labs, Inc. DBA Admiral", "tremendousearthquake.com": "Leven Labs, Inc. DBA Admiral", "tremendousplastic.com": "Leven Labs, Inc. DBA Admiral", "tritebadge.com": "Leven Labs, Inc. DBA Admiral", @@ -50104,12 +50859,19 @@ "unwieldyimpulse.com": "Leven Labs, Inc. DBA Admiral", "unwieldyplastic.com": "Leven Labs, Inc. DBA Admiral", "uselesslumber.com": "Leven Labs, Inc. DBA Admiral", + "vanishmemory.com": "Leven Labs, Inc. DBA Admiral", + "velvetquasar.com": "Leven Labs, Inc. DBA Admiral", "vengefulgrass.com": "Leven Labs, Inc. DBA Admiral", + "venomousvessel.com": "Leven Labs, Inc. DBA Admiral", "venusgloria.com": "Leven Labs, Inc. DBA Admiral", "verdantanswer.com": "Leven Labs, Inc. DBA Admiral", + "verdantloom.com": "Leven Labs, Inc. DBA Admiral", "verseballs.com": "Leven Labs, Inc. DBA Admiral", + "vibrantgale.com": "Leven Labs, Inc. DBA Admiral", "vibranthaven.com": "Leven Labs, Inc. DBA Admiral", + "vibranttalisman.com": "Leven Labs, Inc. DBA Admiral", "virtualvincent.com": "Leven Labs, Inc. DBA Admiral", + "vividmeadow.com": "Leven Labs, Inc. DBA Admiral", "volatileprofit.com": "Leven Labs, Inc. DBA Admiral", "volatilevessel.com": "Leven Labs, Inc. DBA Admiral", "voraciousgrip.com": "Leven Labs, Inc. DBA Admiral", @@ -50118,13 +50880,18 @@ "warmquiver.com": "Leven Labs, Inc. DBA Admiral", "wearbasin.com": "Leven Labs, Inc. DBA Admiral", "wellgroomedhydrant.com": "Leven Labs, Inc. DBA Admiral", + "whimsicalcanyon.com": "Leven Labs, Inc. DBA Admiral", "whimsicalgrove.com": "Leven Labs, Inc. DBA Admiral", "whisperingcascade.com": "Leven Labs, Inc. DBA Admiral", + "whisperingquasar.com": "Leven Labs, Inc. DBA Admiral", "whisperingsummit.com": "Leven Labs, Inc. DBA Admiral", "whispermeeting.com": "Leven Labs, Inc. DBA Admiral", "wildcommittee.com": "Leven Labs, Inc. DBA Admiral", + "wistfulwaste.com": "Leven Labs, Inc. DBA Admiral", "workoperation.com": "Leven Labs, Inc. DBA Admiral", + "wretchedfloor.com": "Leven Labs, Inc. DBA Admiral", "wrongwound.com": "Leven Labs, Inc. DBA Admiral", + "zephyrlabyrinth.com": "Leven Labs, Inc. DBA Admiral", "zestycrime.com": "Leven Labs, Inc. DBA Admiral", "zipperxray.com": "Leven Labs, Inc. DBA Admiral", "zlp6s.pw": "Leven Labs, Inc. DBA Admiral", From e132b43d4ddfc3b94fd26ebdea0263eeccd46618 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Thu, 15 Feb 2024 18:29:15 +0000 Subject: [PATCH 07/44] Bump version to 1.75.0 (119) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 321e492669..9a2efd023d 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 118 +CURRENT_PROJECT_VERSION = 119 From 1a88f9e6e4e6e456817863cc37353c890e448869 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Fri, 16 Feb 2024 10:23:19 +0100 Subject: [PATCH 08/44] Generate appcast2.xml and upload files to S3 in a CI workflow (#2184) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/0/1206459737646671/f Description: This change adds new workflow called sparkle_internal.yml that should be triggered manually after smoke testing. It takes Asana release task URL and the tag to be published, and generates new appcast file and uploads missing files to S3. The workflow fetches DMG for the release from S3 (based on the assumption that Code Freeze and/or Bump Internal Release uploaded the DMG there – this was introduced in #2165). appcastManager and upload_to_s3 scripts were updated for running in CI (support non-interactive shell). Additionally, appcastManager generates a list of added/updated files. Completion is reported to the Automation subtask and also to the release task. Upon successful completion, the release DRI is assigned a task to verify that the update worked, with instructions on how to revert the appcast in case it's broken. If the workflow fails, the user is assigned a task with instructions for generating appcast and uploading to S3 locally. --- .github/actions/asana-add-comment/action.yml | 78 +++++++ .../templates/appcast-failed.yml | 9 + .../templates/debug-symbols-uploaded.yml | 8 + .../templates/dmg-uploaded.yml | 8 + .../templates/internal-release-complete.yml | 8 + .../internal-release-ready-merge-failed.yml | 17 ++ .../internal-release-ready-tag-failed.yml | 16 ++ .../templates/internal-release-ready.yml | 14 ++ .../validate-check-for-updates-internal.yml | 11 + .../templates/appcast-failed.yml | 37 +++ .../validate-check-for-updates-internal.yml | 41 ++++ .github/actions/asana-log-message/action.yml | 48 +--- .github/actions/asana-upload/action.yml | 2 +- .github/workflows/build_appstore.yml | 2 +- .github/workflows/build_notarized.yml | 5 +- .github/workflows/bump_internal_release.yml | 4 +- .github/workflows/code_freeze.yml | 4 +- .github/workflows/pr.yml | 14 +- .github/workflows/publish_dmg_release.yml | 210 ++++++++++++++++++ scripts/appcast_manager/appcastManager.swift | 56 ++++- scripts/extract_release_notes.sh | 30 +++ scripts/upload_to_s3/upload_to_s3.sh | 63 ++++-- 22 files changed, 599 insertions(+), 86 deletions(-) create mode 100644 .github/actions/asana-add-comment/action.yml create mode 100644 .github/actions/asana-add-comment/templates/appcast-failed.yml create mode 100644 .github/actions/asana-add-comment/templates/debug-symbols-uploaded.yml create mode 100644 .github/actions/asana-add-comment/templates/dmg-uploaded.yml create mode 100644 .github/actions/asana-add-comment/templates/internal-release-complete.yml create mode 100644 .github/actions/asana-add-comment/templates/internal-release-ready-merge-failed.yml create mode 100644 .github/actions/asana-add-comment/templates/internal-release-ready-tag-failed.yml create mode 100644 .github/actions/asana-add-comment/templates/internal-release-ready.yml create mode 100644 .github/actions/asana-add-comment/templates/validate-check-for-updates-internal.yml create mode 100644 .github/actions/asana-create-action-item/templates/appcast-failed.yml create mode 100644 .github/actions/asana-create-action-item/templates/validate-check-for-updates-internal.yml create mode 100644 .github/workflows/publish_dmg_release.yml create mode 100755 scripts/extract_release_notes.sh diff --git a/.github/actions/asana-add-comment/action.yml b/.github/actions/asana-add-comment/action.yml new file mode 100644 index 0000000000..73f442c5be --- /dev/null +++ b/.github/actions/asana-add-comment/action.yml @@ -0,0 +1,78 @@ +name: Add a Comment to Asana Task +description: Adds a comment to the Asana task. +inputs: + access-token: + description: "Asana access token" + required: true + type: string + task-url: + description: "Task URL" + required: false + type: string + task-id: + description: "Task ID" + required: false + type: string + comment: + description: "Comment to add to the Asana task" + required: false + type: string + template-name: + description: | + Name of a template file (without extension) for the comment, relative to 'templates' subdirectory of the action. + The file is processed by envsubst before being sent to Asana. + required: false + type: string +runs: + using: "composite" + steps: + - id: extract-task-id + if: ${{ inputs.task-url }} + uses: ./.github/actions/asana-extract-task-id + with: + task-url: ${{ inputs.task-url }} + access-token: ${{ inputs.access-token }} + + - id: process-template-payload + if: ${{ inputs.template-name }} + shell: bash + env: + TEMPLATE_PATH: ${{ github.action_path }}/templates/${{ inputs.template-name }}.yml + run: | + if [ ! -f $TEMPLATE_PATH ]; then + echo "::error::Template file not found at $TEMPLATE_PATH" + exit 1 + fi + + # Process the template file with envsubst, turn into JSON, remove leading spaces and newlines on non-empty lines, and compact the JSON + payload="$(envsubst < $TEMPLATE_PATH | yq -o=j | sed -E 's/\\n( *)([^\\n])/\2/g' | jq -c)" + echo "payload-base64=$(base64 <<< $payload)" >> $GITHUB_OUTPUT + + - id: process-comment-payload + if: ${{ inputs.comment }} + shell: bash + env: + COMMENT: ${{ inputs.comment }} + WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + payload="{ \"data\": { \"text\": \"${COMMENT}\n\nWorkflow URL: ${WORKFLOW_URL}\" } }" + echo "payload-base64=$(base64 <<< $payload)" >> $GITHUB_OUTPUT + + - id: add-comment + shell: bash + env: + ASANA_ACCESS_TOKEN: ${{ inputs.access-token }} + TASK_ID: ${{ inputs.task-id || steps.extract-task-id.outputs.task-id }} + PAYLOAD_BASE64: ${{ steps.process-template-payload.outputs.payload-base64 || steps.process-comment-payload.outputs.payload-base64 }} + run: | + return_code=$(curl -fLSs "https://app.asana.com/api/1.0/tasks/${TASK_ID}/stories" \ + -H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}" \ + -H 'content-type: application/json' \ + --write-out '%{http_code}' \ + --output /dev/null \ + -d "$(base64 -d <<< $PAYLOAD_BASE64)") + + if [ $return_code -ne 201 ]; then + echo "::error::Failed to add a comment to the Asana task" + exit 1 + fi diff --git a/.github/actions/asana-add-comment/templates/appcast-failed.yml b/.github/actions/asana-add-comment/templates/appcast-failed.yml new file mode 100644 index 0000000000..b20642af47 --- /dev/null +++ b/.github/actions/asana-add-comment/templates/appcast-failed.yml @@ -0,0 +1,9 @@ +data: + html_text: | + +

[ACTION NEEDED] Publishing ${TAG} internal release to Sparkle failed

+ , please proceed with generating appcast2.xml and uploading files to S3 from your local machine, according to instructions. + + + 🔗 Workflow URL: ${WORKFLOW_URL}. + \ No newline at end of file diff --git a/.github/actions/asana-add-comment/templates/debug-symbols-uploaded.yml b/.github/actions/asana-add-comment/templates/debug-symbols-uploaded.yml new file mode 100644 index 0000000000..dd02a3b80a --- /dev/null +++ b/.github/actions/asana-add-comment/templates/debug-symbols-uploaded.yml @@ -0,0 +1,8 @@ +data: + html_text: | + + 🐛 Debug symbols archive for ${TAG} build is uploaded to ${DSYM_S3_PATH}. + + + 🔗 Workflow URL: ${WORKFLOW_URL}. + diff --git a/.github/actions/asana-add-comment/templates/dmg-uploaded.yml b/.github/actions/asana-add-comment/templates/dmg-uploaded.yml new file mode 100644 index 0000000000..a5bc0978de --- /dev/null +++ b/.github/actions/asana-add-comment/templates/dmg-uploaded.yml @@ -0,0 +1,8 @@ +data: + html_text: | + + 📥 DMG for ${TAG} is available from ${DMG_URL}. + + + 🔗 Workflow URL: ${WORKFLOW_URL}. + diff --git a/.github/actions/asana-add-comment/templates/internal-release-complete.yml b/.github/actions/asana-add-comment/templates/internal-release-complete.yml new file mode 100644 index 0000000000..3f6773c3d2 --- /dev/null +++ b/.github/actions/asana-add-comment/templates/internal-release-complete.yml @@ -0,0 +1,8 @@ +data: + html_text: | + + Build ${TAG} is now available for internal testing through Sparkle and TestFlight. + + + 📥 DMG download link + diff --git a/.github/actions/asana-add-comment/templates/internal-release-ready-merge-failed.yml b/.github/actions/asana-add-comment/templates/internal-release-ready-merge-failed.yml new file mode 100644 index 0000000000..733d97340b --- /dev/null +++ b/.github/actions/asana-add-comment/templates/internal-release-ready-merge-failed.yml @@ -0,0 +1,17 @@ +data: + # yq -o=j | sed -E 's/\\n( *)([^\\n])/\2/g' + html_text: | + +

[ACTION NEEDED] Internal release build ${TAG} ready

+ + + 🔗 Workflow URL: ${WORKFLOW_URL}. + diff --git a/.github/actions/asana-add-comment/templates/internal-release-ready-tag-failed.yml b/.github/actions/asana-add-comment/templates/internal-release-ready-tag-failed.yml new file mode 100644 index 0000000000..f98641fa62 --- /dev/null +++ b/.github/actions/asana-add-comment/templates/internal-release-ready-tag-failed.yml @@ -0,0 +1,16 @@ +data: + html_text: | + +

[ACTION NEEDED] Internal release build ${TAG} ready

+
    +
  • 📥 DMG is available from ${DMG_URL}.
  • +
  • ❗️ Tagging repository failed.
  • +
  • ⚠️ GitHub release creation was skipped.
  • +
  • ⚠️ Merging ${BRANCH} to ${BASE_BRANCH} was skipped.
  • +
+ + , please proceed with manual tagging and merging according to instructions. + + + 🔗 Workflow URL: ${WORKFLOW_URL}. + diff --git a/.github/actions/asana-add-comment/templates/internal-release-ready.yml b/.github/actions/asana-add-comment/templates/internal-release-ready.yml new file mode 100644 index 0000000000..d089b0d551 --- /dev/null +++ b/.github/actions/asana-add-comment/templates/internal-release-ready.yml @@ -0,0 +1,14 @@ +data: + # yq -o=j | sed -E 's/\\n( *)([^\\n])/\2/g' + html_text: | + +

Internal release build ${TAG} ready ✅

+
    +
  • 📥 DMG is available from ${DMG_URL}.
  • +
  • 🏷️ Repository is tagged with ${TAG} tag.
  • +
  • 🚢 GitHub ${TAG} pre-release is created.
  • +
  • 🔱 ${BRANCH} branch has been successfully merged to ${BASE_BRANCH}.
  • +
+ + 🔗 Workflow URL: ${WORKFLOW_URL}. + diff --git a/.github/actions/asana-add-comment/templates/validate-check-for-updates-internal.yml b/.github/actions/asana-add-comment/templates/validate-check-for-updates-internal.yml new file mode 100644 index 0000000000..f76a5cc72d --- /dev/null +++ b/.github/actions/asana-add-comment/templates/validate-check-for-updates-internal.yml @@ -0,0 +1,11 @@ +data: + html_text: | + +

Build ${TAG} is available for internal testing through Sparkle 🚀

+ + + 🔗 Workflow URL: ${WORKFLOW_URL}. + diff --git a/.github/actions/asana-create-action-item/templates/appcast-failed.yml b/.github/actions/asana-create-action-item/templates/appcast-failed.yml new file mode 100644 index 0000000000..668083791a --- /dev/null +++ b/.github/actions/asana-create-action-item/templates/appcast-failed.yml @@ -0,0 +1,37 @@ +data: + name: Generate appcast2.xml for ${TAG} internal release and upload assets to S3 + assignee: "${ASSIGNEE_ID}" + html_notes: | + + Publishing internal release ${TAG} failed in CI. Please follow the steps to generate the appcast file and upload files to S3 from your local machine. + +
    +
  1. Download the DMG for ${TAG} release.
  2. +
  3. Create a new file called release-notes.txt on your disk. +
      +
    • Add each release note as a separate line and don't add bullet points (•) – the script will add them automatically.
    • +
  4. +
  5. Run appcastManager: +
      +
    • ./scripts/appcast_manager/appcastManager.swift --release-to-internal-channel --dmg ~/Downloads/${DMG_NAME} --release-notes release-notes.txt
    • +
  6. +
  7. Verify that the new build is in the appcast file with the following internal channel tag: +
      +
    • <sparkle:channel>internal-channel</sparkle:channel>
    • +
  8. +
  9. Run upload_to_s3.sh script: +
      +
    • ./scripts/upload_to_s3/upload_to_s3.sh --run
    • +
  10. +
+ When done, please verify that "Check for Updates" works correctly: +
    +
  1. Launch a debug version of the app with an old version number.
  2. +
  3. Identify as an internal user in the app.
  4. +
  5. Go to Main Menu → DuckDuckGo → Check for Updates...
  6. +
  7. Verify that you're being offered to update to ${TAG}.
  8. +
  9. Verify that the update works.
  10. +
+ + 🔗 Workflow URL: ${WORKFLOW_URL}. + diff --git a/.github/actions/asana-create-action-item/templates/validate-check-for-updates-internal.yml b/.github/actions/asana-create-action-item/templates/validate-check-for-updates-internal.yml new file mode 100644 index 0000000000..61d5b86207 --- /dev/null +++ b/.github/actions/asana-create-action-item/templates/validate-check-for-updates-internal.yml @@ -0,0 +1,41 @@ +data: + name: Validate that 'Check For Updates' upgrades to ${TAG} for internal users + assignee: "${ASSIGNEE_ID}" + html_notes: | + +

Build ${TAG} has been released internally via Sparkle 🎉

+ Please verify that "Check for Updates" works correctly: +
    +
  1. Launch a debug version of the app with an old version number.
  2. +
  3. Identify as an internal user in the app.
  4. +
  5. Go to Main Menu → DuckDuckGo → Check for Updates...
  6. +
  7. Verify that you're being offered to update to ${TAG}.
  8. +
  9. Verify that the update works.
  10. +
+

🚨In case "Check for Updates" is broken

+ You can restore previous version of the appcast2.xml: +
    +
  1. Download the ${OLD_APPCAST_NAME} file attached to this task.
  2. +
  3. Log in to AWS session: +
      +
    • aws --profile ddg-macos sso login
    • +
  4. +
  5. Overwrite appcast2.xml with the old version: +
      +
    • aws --profile ddg-macos s3 cp ${OLD_APPCAST_NAME} s3://${RELEASE_BUCKET_NAME}/${RELEASE_BUCKET_PREFIX}/appcast2.xml --acl public-read
    • +
  6. +
+ +
+

Summary of automated changes

+

Changes to appcast2.xml

+ See the attached ${APPCAST_PATCH_NAME} file. +

Release notes

+ See the attached ${RELEASE_NOTES_FILE} file for release notes extracted automatically from the release task description. +

List of files uploaded to S3

+
    + ${FILES_UPLOADED} +
+ + 🔗 Workflow URL: ${WORKFLOW_URL}. + diff --git a/.github/actions/asana-log-message/action.yml b/.github/actions/asana-log-message/action.yml index be0d695ce0..966d7a4605 100644 --- a/.github/actions/asana-log-message/action.yml +++ b/.github/actions/asana-log-message/action.yml @@ -47,46 +47,10 @@ runs: exit 1 fi - - id: process-template-payload - if: ${{ inputs.template-name }} - shell: bash - env: - TEMPLATE_PATH: ${{ github.action_path }}/templates/${{ inputs.template-name }}.yml - run: | - if [ ! -f $TEMPLATE_PATH ]; then - echo "::error::Template file not found at $TEMPLATE_PATH" - exit 1 - fi - - # Process the template file with envsubst, turn into JSON, remove leading spaces and newlines on non-empty lines, and compact the JSON - payload="$(envsubst < $TEMPLATE_PATH | yq -o=j | sed -E 's/\\n( *)([^\\n])/\2/g' | jq -c)" - echo "payload-base64=$(base64 <<< $payload)" >> $GITHUB_OUTPUT - - - id: process-comment-payload - if: ${{ inputs.comment }} - shell: bash - env: - COMMENT: ${{ inputs.comment }} - WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} - run: | - payload="{ \"data\": { \"text\": \"${COMMENT}\n\nWorkflow URL: ${WORKFLOW_URL}\" } }" - echo "payload-base64=$(base64 <<< $payload)" >> $GITHUB_OUTPUT - - id: add-comment - shell: bash - env: - ASANA_ACCESS_TOKEN: ${{ inputs.access-token }} - TASK_ID: ${{ steps.get-automation-subtask.outputs.automation-task-id }} - PAYLOAD_BASE64: ${{ steps.process-template-payload.outputs.payload-base64 || steps.process-comment-payload.outputs.payload-base64 }} - run: | - return_code=$(curl -fLSs "https://app.asana.com/api/1.0/tasks/${TASK_ID}/stories" \ - -H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}" \ - -H 'content-type: application/json' \ - --write-out '%{http_code}' \ - --output /dev/null \ - -d "$(base64 -d <<< $PAYLOAD_BASE64)") - - if [ $return_code -ne 201 ]; then - echo "::error::Failed to add a comment to the Asana task" - exit 1 - fi + uses: ./.github/actions/asana-add-comment + with: + access-token: ${{ inputs.access-token }} + task-id: ${{ steps.get-automation-subtask.outputs.automation-task-id }} + comment: ${{ inputs.comment }} + template-name: ${{ inputs.template-name }} diff --git a/.github/actions/asana-upload/action.yml b/.github/actions/asana-upload/action.yml index d54584236a..405216a35f 100644 --- a/.github/actions/asana-upload/action.yml +++ b/.github/actions/asana-upload/action.yml @@ -17,7 +17,7 @@ runs: using: "composite" steps: - run: | - curl -s "https://app.asana.com/api/1.0/tasks/${{ inputs.task-id }}/attachments" \ + curl -fLSs "https://app.asana.com/api/1.0/tasks/${{ inputs.task-id }}/attachments" \ -H "Authorization: Bearer ${{ inputs.access-token }}" \ --form "file=@${{ inputs.file-name }}" shell: bash diff --git a/.github/workflows/build_appstore.yml b/.github/workflows/build_appstore.yml index fc5108c64a..7b0ccd1868 100644 --- a/.github/workflows/build_appstore.yml +++ b/.github/workflows/build_appstore.yml @@ -82,7 +82,7 @@ jobs: ${{ secrets.SSH_PRIVATE_KEY_FASTLANE_MATCH }} - name: Check out the code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive ref: ${{ inputs.branch || github.ref_name }} diff --git a/.github/workflows/build_notarized.yml b/.github/workflows/build_notarized.yml index 010df3bcc6..533f3fc3f3 100644 --- a/.github/workflows/build_notarized.yml +++ b/.github/workflows/build_notarized.yml @@ -108,7 +108,7 @@ jobs: steps: - name: Check out the code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive ref: ${{ env.branch }} @@ -296,7 +296,8 @@ jobs: run: | aws s3 cp \ ${{ github.workspace }}/${{ steps.create-dmg.outputs.dmg }} \ - s3://${{ env.RELEASE_BUCKET_NAME }}/${{ env.RELEASE_BUCKET_PREFIX }}/ + s3://${{ env.RELEASE_BUCKET_NAME }}/${{ env.RELEASE_BUCKET_PREFIX }}/ \\ + --acl public-read - name: Report success if: ${{ env.upload-to == 's3' }} diff --git a/.github/workflows/bump_internal_release.yml b/.github/workflows/bump_internal_release.yml index 58210bd698..8df401cd82 100644 --- a/.github/workflows/bump_internal_release.yml +++ b/.github/workflows/bump_internal_release.yml @@ -31,7 +31,7 @@ jobs: esac - name: Check out the code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive @@ -71,7 +71,7 @@ jobs: steps: - name: Check out the code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive ref: ${{ github.ref_name }} diff --git a/.github/workflows/code_freeze.yml b/.github/workflows/code_freeze.yml index d4f475d290..9f99e65808 100644 --- a/.github/workflows/code_freeze.yml +++ b/.github/workflows/code_freeze.yml @@ -26,7 +26,7 @@ jobs: fi - name: Check out the code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive @@ -95,7 +95,7 @@ jobs: steps: - name: Check out the code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive ref: ${{ needs.create_release_branch.outputs.release_branch_name }} diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 5b3ad38fbb..bb7a657499 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: SwiftLint uses: docker://norionomura/swiftlint:0.54.0 with: @@ -37,11 +37,11 @@ jobs: steps: - name: Check out the code if: github.event_name == 'pull_request' || github.event_name == 'push' - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Check out the code if: github.event_name != 'pull_request' && github.event_name != 'push' - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ inputs.branch || github.ref_name }} @@ -88,13 +88,13 @@ jobs: steps: - name: Check out the code if: github.event_name == 'pull_request' || github.event_name == 'push' - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive - name: Check out the code if: github.event_name != 'pull_request' && github.event_name != 'push' - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive ref: ${{ inputs.branch || github.ref_name }} @@ -265,7 +265,7 @@ jobs: steps: - name: Check out the code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive @@ -331,7 +331,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-node@v3 with: node-version: 16 diff --git a/.github/workflows/publish_dmg_release.yml b/.github/workflows/publish_dmg_release.yml new file mode 100644 index 0000000000..41cb12e4af --- /dev/null +++ b/.github/workflows/publish_dmg_release.yml @@ -0,0 +1,210 @@ +name: Publish DMG Release + +on: + workflow_dispatch: + inputs: + asana-task-url: + description: "Asana release task URL" + required: true + type: string + tag: + description: "Tag to publish" + required: true + type: string + +jobs: + + publish-to-sparkle: + + name: Publish internal release to Sparkle + + runs-on: macos-13-xlarge + timeout-minutes: 10 + + env: + SPARKLE_DIR: ${{ github.workspace }}/sparkle-updates + + steps: + + - name: Verify the tag + id: verify-tag + env: + tag: ${{ github.event.inputs.tag }} + run: | + tag_regex='^[0-9]+\.[0-9]+\.[0-9]+-[0-9]+$' + + if [[ ! "$tag" =~ $tag_regex ]]; then + echo "::error::The provided tag ($tag) has incorrect format (attempted to match ${tag_regex})." + exit 1 + fi + echo "tag-in-filename=${tag//-/.}" >> $GITHUB_OUTPUT + + - name: Check out the code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up Sparkle tools + env: + SPARKLE_URL: https://github.com/sparkle-project/Sparkle/releases/download/${{ vars.SPARKLE_VERSION }}/Sparkle-${{ vars.SPARKLE_VERSION }}.tar.xz + run: | + curl -fLSs $SPARKLE_URL | tar xJ bin + echo "${{ github.workspace }}/bin" >> $GITHUB_PATH + + - name: Fetch DMG + id: fetch-dmg + env: + DMG_NAME: duckduckgo-${{ steps.verify-tag.outputs.tag-in-filename }}.dmg + run: | + DMG_URL="${{ vars.DMG_URL_ROOT }}${DMG_NAME}" + curl -fLSs -o "$DMG_NAME" "$DMG_URL" + echo "dmg-name=$DMG_NAME" >> $GITHUB_OUTPUT + echo "dmg-path=$DMG_NAME" >> $GITHUB_OUTPUT + + - name: Extract Asana Task ID + id: task-id + uses: ./.github/actions/asana-extract-task-id + with: + task-url: ${{ github.event.inputs.asana-task-url }} + + - name: Fetch release notes + env: + TASK_ID: ${{ steps.task-id.outputs.task-id }} + ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }} + run: | + curl -fLSs "https://app.asana.com/api/1.0/tasks/${TASK_ID}?opt_fields=notes" \ + -H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}" \ + | jq -r .data.notes \ + | ./scripts/extract_release_notes.sh > release_notes.txt + echo "RELEASE_NOTES_FILE=release_notes.txt" >> $GITHUB_ENV + + - name: Select Xcode + run: sudo xcode-select -s /Applications/Xcode_$(<.xcode-version).app/Contents/Developer + + - name: Generate appcast + id: appcast + env: + DMG_PATH: ${{ steps.fetch-dmg.outputs.dmg-path }} + SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }} + run: | + echo -n "$SPARKLE_PRIVATE_KEY" > sparkle_private_key + chmod 600 sparkle_private_key + + ./scripts/appcast_manager/appcastManager.swift \ + --release-to-internal-channel \ + --dmg ${DMG_PATH} \ + --release-notes release_notes.txt \ + --key sparkle_private_key + + appcast_patch_name="appcast2-${{ steps.verify-tag.outputs.tag-in-filename }}.patch" + mv -f ${{ env.SPARKLE_DIR }}/appcast_diff.txt ${{ env.SPARKLE_DIR }}/${appcast_patch_name} + echo "appcast-patch-name=${appcast_patch_name}" >> $GITHUB_OUTPUT + + - name: Upload appcast diff artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.appcast.outputs.appcast-patch-name }} + path: ${{ env.SPARKLE_DIR }}/${{ steps.appcast.outputs.appcast-patch-name }} + + - name: Upload to S3 + id: upload + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: ${{ vars.AWS_DEFAULT_REGION }} + run: | + # Back up existing appcast2.xml + OLD_APPCAST_NAME=appcast2_old.xml + echo "OLD_APPCAST_NAME=${OLD_APPCAST_NAME}" >> $GITHUB_ENV + curl -fLSs "${{ vars.DMG_URL_ROOT }}appcast2.xml" --output "${OLD_APPCAST_NAME}" + + # Upload files to S3 + ./scripts/upload_to_s3/upload_to_s3.sh --run --force + + if [[ -f "${{ env.SPARKLE_DIR }}/uploaded_files_list.txt" ]]; then + echo "FILES_UPLOADED=$(awk '{ print "
  • "$1"
  • "; }' < ${{ env.SPARKLE_DIR }}/uploaded_files_list.txt | tr '\n' ' ')" >> $GITHUB_ENV + else + echo "FILES_UPLOADED='No files uploaded.'" >> $GITHUB_ENV + fi + + - name: Set common environment variables + if: always() + env: + DMG_NAME: ${{ steps.fetch-dmg.outputs.dmg-name }} + run: | + echo "APPCAST_PATCH_NAME=${{ steps.appcast.outputs.appcast-patch-name }}" >> $GITHUB_ENV + echo "DMG_NAME=${DMG_NAME}" >> $GITHUB_ENV + echo "DMG_URL=${{ vars.DMG_URL_ROOT }}${DMG_NAME}" >> $GITHUB_ENV + echo "RELEASE_BUCKET_NAME=${{ vars.RELEASE_BUCKET_NAME }}" >> $GITHUB_ENV + echo "RELEASE_BUCKET_PREFIX=${{ vars.RELEASE_BUCKET_PREFIX }}" >> $GITHUB_ENV + echo "RELEASE_TASK_ID=${{ steps.task-id.outputs.task-id }}" >> $GITHUB_ENV + echo "TAG=${{ github.event.inputs.tag }}" >> $GITHUB_ENV + echo "WORKFLOW_URL=https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> $GITHUB_ENV + + - name: Set up Asana templates + if: always() + id: asana-templates + run: | + if [[ ${{ steps.upload.outcome }} == "success" ]]; then + echo "task-template=validate-check-for-updates-internal" >> $GITHUB_OUTPUT + echo "comment-template=validate-check-for-updates-internal" >> $GITHUB_OUTPUT + echo "release-task-comment-template=internal-release-complete" >> $GITHUB_OUTPUT + else + echo "task-template=appcast-failed" >> $GITHUB_OUTPUT + echo "comment-template=appcast-failed" >> $GITHUB_OUTPUT + fi + + - name: Create Asana task + id: create-task + if: always() + uses: ./.github/actions/asana-create-action-item + with: + access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} + release-task-url: ${{ github.event.inputs.asana-task-url }} + template-name: ${{ steps.asana-templates.outputs.task-template }} + + - name: Upload patch to the Asana task + id: upload-patch + if: success() + uses: ./.github/actions/asana-upload + with: + access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} + file-name: ${{ env.SPARKLE_DIR }}/${{ steps.appcast.outputs.appcast-patch-name }} + task-id: ${{ steps.create-task.outputs.new-task-id }} + + - name: Upload old appcast file to the Asana task + id: upload-old-appcast + if: success() + uses: ./.github/actions/asana-upload + with: + access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} + file-name: ${{ env.OLD_APPCAST_NAME }} + task-id: ${{ steps.create-task.outputs.new-task-id }} + + - name: Upload release notes to the Asana task + id: upload-release-notes + if: success() + uses: ./.github/actions/asana-upload + with: + access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} + file-name: ${{ env.RELEASE_NOTES_FILE }} + task-id: ${{ steps.create-task.outputs.new-task-id }} + + - name: Report status + if: always() + uses: ./.github/actions/asana-log-message + env: + ASSIGNEE_ID: ${{ steps.create-task.outputs.assignee-id }} + TASK_ID: ${{ steps.create-task.outputs.new-task-id }} + with: + access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} + task-url: ${{ github.event.inputs.asana-task-url }} + template-name: ${{ steps.asana-templates.outputs.comment-template }} + + - name: Add a comment to the release task + if: success() + uses: ./.github/actions/asana-add-comment + with: + access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} + task-url: ${{ github.event.inputs.asana-task-url }} + template-name: internal-release-complete diff --git a/scripts/appcast_manager/appcastManager.swift b/scripts/appcast_manager/appcastManager.swift index cf059f2cb0..c3906a3cae 100755 --- a/scripts/appcast_manager/appcastManager.swift +++ b/scripts/appcast_manager/appcastManager.swift @@ -11,9 +11,10 @@ signal(SIGINT) { _ in exit(1) } +let isCI = ProcessInfo.processInfo.environment["CI"] != nil let appcastURLString = "https://staticcdn.duckduckgo.com/macos-desktop-browser/appcast2.xml" let appcastURL = URL(string: appcastURLString)! -let tmpDir = NSString(string: "~/Developer").expandingTildeInPath +let tmpDir = isCI ? "." : NSString(string: "~/Developer").expandingTildeInPath let tmpDirURL = URL(fileURLWithPath: tmpDir, isDirectory: true) let specificDir = tmpDirURL.appendingPathComponent("sparkle-updates") let appcastFilePath = specificDir.appendingPathComponent("appcast2.xml") @@ -76,9 +77,9 @@ NAME appcastManager – automation of appcast file management SYNOPSIS - appcastManager --release-to-internal-channel --dmg --release-notes - appcastManager --release-to-public-channel --version [--release-notes ] - appcastManager --release-hotfix-to-public-channel --dmg --release-notes + appcastManager --release-to-internal-channel --dmg --release-notes [--key ] + appcastManager --release-to-public-channel --version [--release-notes ] [--key ] + appcastManager --release-hotfix-to-public-channel --dmg --release-notes [--key ] appcastManager --help DESCRIPTION @@ -112,10 +113,14 @@ case .releaseToInternalChannel, .releaseHotfixToPublicChannel: print("Missing required parameters") exit(1) } + let keyFile = readKeyFileArgument() print("➡️ Action: Add to internal channel") print("➡️ DMG Path: \(dmgPath)") print("➡️ Release Notes Path: \(releaseNotesPath)") + if isCI, let keyFile { + print("➡️ Key file: \(keyFile)") + } performCommonChecksAndOperations() @@ -132,9 +137,9 @@ case .releaseToInternalChannel, .releaseHotfixToPublicChannel: // Differentiate between the two actions if arguments.action == .releaseToInternalChannel { - runGenerateAppcast(with: versionNumber, channel: "internal-channel") + runGenerateAppcast(with: versionNumber, channel: "internal-channel", keyFile: keyFile) } else { - runGenerateAppcast(with: versionNumber) + runGenerateAppcast(with: versionNumber, keyFile: keyFile) } case .releaseToPublicChannel: @@ -142,11 +147,15 @@ case .releaseToPublicChannel: print("Missing required version parameter for action '--release-to-public-channel'") exit(1) } + let keyFile = readKeyFileArgument() let versionNumber = extractVersionNumber(from: versionIdentifier) - print("Action: Release to public channel") - print("Version: \(versionIdentifier)") + print("➡️ Action: Release to public channel") + print("➡️ Version: \(versionIdentifier)") + if isCI, let keyFile { + print("➡️ Key file: \(keyFile)") + } performCommonChecksAndOperations() @@ -171,11 +180,25 @@ case .releaseToPublicChannel: } print("⚠️ Version \(versionIdentifier) removed from the appcast.") - runGenerateAppcast(with: versionNumber, rolloutInterval: "43200") + runGenerateAppcast(with: versionNumber, rolloutInterval: "43200", keyFile: keyFile) } // MARK: - Common +func readKeyFileArgument() -> String? { + let keyFile: String? = arguments.parameters["--key"] + + if isCI { + print("Running in CI mode") + guard keyFile != nil else { + print("Missing required key parameter for CI") + exit(1) + } + } + + return keyFile +} + func extractVersionNumber(from versionIdentifier: String) -> String { let components = versionIdentifier.components(separatedBy: ".") guard components.count == 4 else { @@ -251,6 +274,10 @@ extension DateFormatter { // MARK: - Verification of the signing keys func verifySigningKeys() -> Bool { + if isCI { + print("Running in CI mode. Skipping verification of signing keys.") + return true + } let publicKeyOutput = shell("generate_keys", "-p").trimmingCharacters(in: .whitespacesAndNewlines) let desiredPublicKey = "ZaO/DNMzMPBldh40b5xVrpNBmqRkuGY0BNRCUng2qRo=" @@ -703,7 +730,7 @@ func writeAppcastContent(_ content: String, to filePath: URL) { // MARK: - Generating of New Appcast -func runGenerateAppcast(with versionNumber: String, channel: String? = nil, rolloutInterval: String? = nil) { +func runGenerateAppcast(with versionNumber: String, channel: String? = nil, rolloutInterval: String? = nil, keyFile: String? = nil) { // Check if backup file already exists and remove it if FileManager.default.fileExists(atPath: backupFileURL.path) { do { @@ -730,6 +757,9 @@ func runGenerateAppcast(with versionNumber: String, channel: String? = nil, roll commandComponents.append("--versions \(versionNumber)") commandComponents.append("--maximum-versions \(maximumVersions)") commandComponents.append("--maximum-deltas \(maximumDeltas)") + if let keyFile { + commandComponents.append("--ed-key-file \(keyFile)") + } if let channel = channel { commandComponents.append("--channel \(channel)") @@ -779,8 +809,10 @@ func runGenerateAppcast(with versionNumber: String, channel: String? = nil, roll moveFiles(from: specificDir.appendingPathComponent("old_updates"), to: specificDir) print("Old update files moved back to \(specificDir.path)") - // Open specific directory in Finder - shell("open", specificDir.path) + if !isCI { + // Open specific directory in Finder + shell("open", specificDir.path) + } } func moveFiles(from sourceDir: URL, to destinationDir: URL) { diff --git a/scripts/extract_release_notes.sh b/scripts/extract_release_notes.sh new file mode 100755 index 0000000000..fba7600977 --- /dev/null +++ b/scripts/extract_release_notes.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# +# This script extracts release notes from Asana release task description. +# +# Usage: +# cat release_task_description.txt | ./extract_release_notes.sh +# + +notes_start="release notes:" +notes_end="this release includes:" +is_release_notes=0 +has_release_notes=0 + +while read -r line +do + if [[ $(tr '[:upper:]' '[:lower:]' <<< "$line") == "$notes_start" ]]; then + is_release_notes=1 + elif [[ $(tr '[:upper:]' '[:lower:]' <<< "$line") == "$notes_end" ]]; then + exit 0 + elif [[ $is_release_notes -eq 1 && -n "$line" ]]; then + has_release_notes=1 + echo "$line" + fi +done + +if [[ $has_release_notes -eq 0 ]]; then + exit 1 +fi + +exit 0 diff --git a/scripts/upload_to_s3/upload_to_s3.sh b/scripts/upload_to_s3/upload_to_s3.sh index e8250295dd..ac7979f4c2 100755 --- a/scripts/upload_to_s3/upload_to_s3.sh +++ b/scripts/upload_to_s3/upload_to_s3.sh @@ -4,11 +4,18 @@ S3_PATH="s3://ddg-staticcdn/macos-desktop-browser/" # Defaults -DIRECTORY="$HOME/Developer/sparkle-updates" -PROFILE="ddg-macos" +if [[ -n "$CI" ]]; then + AWS="aws" + DIRECTORY="sparkle-updates" +else + AWS="aws --profile ddg-macos" + DIRECTORY="$HOME/Developer/sparkle-updates" +fi + DEBUG=0 OVERWRITE_DMG_VERSION="" RUN_COMMAND=0 +FORCE=0 # Print the usage function print_usage() { @@ -17,7 +24,7 @@ NAME upload_to_s3.sh – automation tool for uploading files to AWS S3 for macOS Desktop Browser SYNOPSIS - $0 --run [--directory directory_path] [--overwrite-duckduckgo-dmg version] [--debug] + $0 --run [--directory directory_path] [--overwrite-duckduckgo-dmg version] [--debug] [--force] $0 --help DESCRIPTION @@ -35,6 +42,9 @@ DESCRIPTION --debug In debug mode, no 'aws cp' commands will be executed; they will only be printed to stdout. + --force + Forces the upload process to continue without asking for confirmation. + --help Displays this help message. @@ -62,13 +72,11 @@ function check_aws_installed() { # Check if there‘s a valid token function check_and_login_aws_sso() { - SSO_ACCOUNT_PROFILE=$(aws sts get-caller-identity --query "Account" --profile $PROFILE) - - if [ ${#SSO_ACCOUNT_PROFILE} -eq 14 ]; then + if $AWS sts get-caller-identity --query "Account" >/dev/null 2>&1; then echo "Session is still valid" else echo "Session has expired" - aws sso login --profile $PROFILE + $AWS sso login fi } @@ -105,6 +113,7 @@ while [[ "$#" -gt 0 ]]; do --debug) DEBUG=1 ;; --help) print_usage; exit 0 ;; # Display the help and exit immediately. --run) RUN_COMMAND=1 ;; + --force) FORCE=1 ;; *) echo "Unknown parameter passed: $1"; print_usage; exit 1 ;; # Display the help and exit with error. esac shift @@ -115,8 +124,10 @@ if [[ $RUN_COMMAND -eq 0 ]]; then exit 0 fi -# Perform AWS login if needed -check_and_login_aws_sso +if [[ -z "$CI" ]]; then + # When not in CI, perform AWS login if needed + check_and_login_aws_sso +fi # Ensure appcast2.xml exists if [[ ! -f "$DIRECTORY/appcast2.xml" ]]; then @@ -138,9 +149,9 @@ for FILENAME in $FILES_TO_UPLOAD; do fi # Check if the file exists on S3 - AWS_CMD="aws --profile $PROFILE s3 ls ${S3_PATH}${FILENAME}" + AWS_CMD="$AWS s3 ls ${S3_PATH}${FILENAME}" echo "Checking S3 for ${S3_PATH}${FILENAME}..." - if ! aws --profile "$PROFILE" s3 ls "${S3_PATH}${FILENAME}" > /dev/null 2>&1; then + if ! $AWS s3 ls "${S3_PATH}${FILENAME}" > /dev/null 2>&1; then echo "$FILENAME not found on S3. Marking for upload." MISSING_FILES+=("$FILENAME") else @@ -169,23 +180,41 @@ if [[ ${#MISSING_FILES[@]} -gt 0 ]] || [[ -n "$OVERWRITE_DMG_VERSION" ]]; then echo "The file duckduckgo-$OVERWRITE_DMG_VERSION.dmg will be used to overwrite duckduckgo.dmg on S3." fi - read -p "Do you wish to continue? (y/n) " -n 1 -r - echo - if [[ ! $REPLY =~ ^[Yy]$ ]]; then - exit 1 + if [[ $FORCE -eq 0 ]]; then + read -p "Do you wish to continue? (y/n) " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + exit 1 + fi fi fi # Upload each missing file for FILE in "${MISSING_FILES[@]}"; do - AWS_CMD="aws --profile $PROFILE s3 cp \"${DIRECTORY}/${FILE}\" ${S3_PATH}${FILE} --acl public-read" + AWS_CMD="$AWS s3 cp \"${DIRECTORY}/${FILE}\" ${S3_PATH}${FILE} --acl public-read" execute_aws "$AWS_CMD" || exit 1 done # If the overwrite flag was set, overwrite the primary dmg if [[ -n "$OVERWRITE_DMG_VERSION" ]]; then - AWS_CMD="aws --profile $PROFILE s3 cp \"${DIRECTORY}/duckduckgo-$OVERWRITE_DMG_VERSION.dmg\" ${S3_PATH}duckduckgo.dmg --acl public-read" + AWS_CMD="$AWS s3 cp \"${DIRECTORY}/duckduckgo-$OVERWRITE_DMG_VERSION.dmg\" ${S3_PATH}duckduckgo.dmg --acl public-read" execute_aws "$AWS_CMD" || exit 1 fi +if [[ -n "$CI" ]]; then + # Store the list of uploaded files in a file + TMP_FILE="$(mktemp)" + for FILE in "${MISSING_FILES[@]}"; do + echo "$FILE" >> "$TMP_FILE" + done + if [[ -n "$OVERWRITE_DMG_VERSION" ]]; then + echo "duckduckgo.dmg" >> "$TMP_FILE" + fi + + FILES_LIST_FILE="${DIRECTORY}/uploaded_files_list.txt" + rm -f "$FILES_LIST_FILE" + sort -f < "$TMP_FILE" > "$FILES_LIST_FILE" + rm -f "$TMP_FILE" +fi + echo "Upload complete!" From 76d0032799e9c6c746b4ae555f8d1ce20aceceb7 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Fri, 16 Feb 2024 10:52:26 +0100 Subject: [PATCH 09/44] Handle Asana app board automation and tagging for the release (#2206) Task/Issue URL: https://app.asana.com/0/0/1206459737646674/f Description: This change adds a shell script that takes Asana release task ID and the release version, and populates "This release includes" in task description with the list of tasks included in the release (based on git log), tags the release task and tasks included in the release (creating the tag as needed), and moves all tasks to Validation section. The script is called by Code Freeze and Bump Internal Release workflows. Additionally, updating embedded files step has been removed from Bump Internal Release workflow because we don't need to update the files so frequently and once a week is enough, also to limit introducing potential bugs by embedding broken data. --- .github/workflows/bump_internal_release.yml | 47 ++--- .github/workflows/code_freeze.yml | 13 ++ .github/workflows/publish_dmg_release.yml | 2 +- .github/workflows/sparkle_internal.yml | 210 ++++++++++++++++++++ scripts/extract_release_notes.sh | 2 +- scripts/update_asana_for_release.sh | 181 +++++++++++++++++ 6 files changed, 427 insertions(+), 28 deletions(-) create mode 100644 .github/workflows/sparkle_internal.yml create mode 100755 scripts/update_asana_for_release.sh diff --git a/.github/workflows/bump_internal_release.yml b/.github/workflows/bump_internal_release.yml index 8df401cd82..49b67e1bd2 100644 --- a/.github/workflows/bump_internal_release.yml +++ b/.github/workflows/bump_internal_release.yml @@ -14,11 +14,11 @@ on: jobs: - update_embedded_files: + assert_release_branch: - name: Update Embedded Files + name: Assert Release Branch - runs-on: macos-13-xlarge + runs-on: ubuntu-latest timeout-minutes: 10 steps: @@ -30,32 +30,11 @@ jobs: *) echo "👎 Not a release branch"; exit 1 ;; esac - - name: Check out the code - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Select Xcode - run: sudo xcode-select -s /Applications/Xcode_$(<.xcode-version).app/Contents/Developer - - - name: Prepare fastlane - run: bundle install - - - name: Update embedded files - env: - APPLE_API_KEY_BASE64: ${{ secrets.APPLE_API_KEY_BASE64 }} - APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} - APPLE_API_KEY_ISSUER: ${{ secrets.APPLE_API_KEY_ISSUER }} - run: | - git config --global user.name "Dax the Duck" - git config --global user.email "dax@duckduckgo.com" - bundle exec fastlane update_embedded_files - run_tests: name: Run Tests - needs: update_embedded_files + needs: assert_release_branch uses: ./.github/workflows/pr.yml secrets: ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }} @@ -73,8 +52,9 @@ jobs: - name: Check out the code uses: actions/checkout@v4 with: - submodules: recursive + fetch-depth: 0 # Fetch all history and tags in order to extract Asana task URLs from git log ref: ${{ github.ref_name }} + submodules: recursive - name: Select Xcode run: sudo xcode-select -s /Applications/Xcode_$(<.xcode-version).app/Contents/Developer @@ -92,6 +72,21 @@ jobs: git config --global user.email "dax@duckduckgo.com" bundle exec fastlane bump_internal_release update_embedded_files:false + - name: Extract Asana Task ID + id: task-id + uses: ./.github/actions/asana-extract-task-id + with: + task-url: ${{ github.event.inputs.asana-task-url }} + + - name: Update Asana tasks for the release + env: + ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }} + GH_TOKEN: ${{ github.token }} + BRANCH: ${{ github.ref_name }} + run: | + version="$(cut -d '/' -f 2 <<< "$BRANCH")" + ./scripts/update_asana_for_release.sh ${{ steps.task-id.outputs.task-id }} "${version}" ${{ vars.MACOS_APP_BOARD_VALIDATION_SECTION_ID }} + prepare_release: name: Prepare Release needs: increment_build_number diff --git a/.github/workflows/code_freeze.yml b/.github/workflows/code_freeze.yml index 9f99e65808..6806f405c0 100644 --- a/.github/workflows/code_freeze.yml +++ b/.github/workflows/code_freeze.yml @@ -28,6 +28,7 @@ jobs: - name: Check out the code uses: actions/checkout@v4 with: + fetch-depth: 0 # Fetch all history and tags in order to extract Asana task URLs from git log submodules: recursive - name: Prepare fastlane @@ -56,6 +57,8 @@ jobs: -H "Content-Type: application/json" \ -d "{ \"data\": { \"name\": \"$task_name\" }}" \ | jq -r .data.new_task.gid)" + echo "marketing_version=${version}" >> $GITHUB_OUTPUT + echo "asana_task_id=${asana_task_id}" >> $GITHUB_OUTPUT 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" \ @@ -73,6 +76,16 @@ jobs: --output /dev/null \ -d "{ \"data\": { \"assignee\": \"$assignee_id\" }}" + - name: Update Asana tasks for the release + env: + ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }} + GH_TOKEN: ${{ github.token }} + run: | + ./scripts/update_asana_for_release.sh \ + ${{ steps.create_release_task.outputs.asana_task_id }} \ + ${{ steps.create_release_task.outputs.marketing_version }} \ + ${{ vars.MACOS_APP_BOARD_VALIDATION_SECTION_ID }} + run_tests: name: Run Tests diff --git a/.github/workflows/publish_dmg_release.yml b/.github/workflows/publish_dmg_release.yml index 41cb12e4af..86c787cb1d 100644 --- a/.github/workflows/publish_dmg_release.yml +++ b/.github/workflows/publish_dmg_release.yml @@ -43,7 +43,7 @@ jobs: uses: actions/checkout@v4 with: submodules: recursive - + - name: Set up Sparkle tools env: SPARKLE_URL: https://github.com/sparkle-project/Sparkle/releases/download/${{ vars.SPARKLE_VERSION }}/Sparkle-${{ vars.SPARKLE_VERSION }}.tar.xz diff --git a/.github/workflows/sparkle_internal.yml b/.github/workflows/sparkle_internal.yml new file mode 100644 index 0000000000..bfc7293c92 --- /dev/null +++ b/.github/workflows/sparkle_internal.yml @@ -0,0 +1,210 @@ +name: Publish Internal Release to Sparkle + +on: + workflow_dispatch: + inputs: + asana-task-url: + description: "Asana release task URL" + required: true + type: string + tag: + description: "Tag to publish" + required: true + type: string + +jobs: + + publish-to-sparkle: + + name: Publish internal release to Sparkle + + runs-on: macos-13-xlarge + timeout-minutes: 10 + + env: + SPARKLE_DIR: ${{ github.workspace }}/sparkle-updates + + steps: + + - name: Verify the tag + id: verify-tag + env: + tag: ${{ github.event.inputs.tag }} + run: | + tag_regex='^[0-9]+\.[0-9]+\.[0-9]+-[0-9]+$' + + if [[ ! "$tag" =~ $tag_regex ]]; then + echo "::error::The provided tag ($tag) has incorrect format (attempted to match ${tag_regex})." + exit 1 + fi + echo "tag-in-filename=${tag//-/.}" >> $GITHUB_OUTPUT + + - name: Check out the code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up Sparkle tools + env: + SPARKLE_URL: https://github.com/sparkle-project/Sparkle/releases/download/${{ vars.SPARKLE_VERSION }}/Sparkle-${{ vars.SPARKLE_VERSION }}.tar.xz + run: | + curl -fLSs $SPARKLE_URL | tar xJ bin + echo "${{ github.workspace }}/bin" >> $GITHUB_PATH + + - name: Fetch DMG + id: fetch-dmg + env: + DMG_NAME: duckduckgo-${{ steps.verify-tag.outputs.tag-in-filename }}.dmg + run: | + DMG_URL="${{ vars.DMG_URL_ROOT }}${DMG_NAME}" + curl -fLSs -o "$DMG_NAME" "$DMG_URL" + echo "dmg-name=$DMG_NAME" >> $GITHUB_OUTPUT + echo "dmg-path=$DMG_NAME" >> $GITHUB_OUTPUT + + - name: Extract Asana Task ID + id: task-id + uses: ./.github/actions/asana-extract-task-id + with: + task-url: ${{ github.event.inputs.asana-task-url }} + + - name: Fetch release notes + env: + TASK_ID: ${{ steps.task-id.outputs.task-id }} + ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }} + run: | + curl -fLSs "https://app.asana.com/api/1.0/tasks/${TASK_ID}?opt_fields=notes" \ + -H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}" \ + | jq -r .data.notes \ + | ./scripts/extract_release_notes.sh > release_notes.txt + echo "RELEASE_NOTES_FILE=release_notes.txt" >> $GITHUB_ENV + + - name: Select Xcode + run: sudo xcode-select -s /Applications/Xcode_$(<.xcode-version).app/Contents/Developer + + - name: Generate appcast + id: appcast + env: + DMG_PATH: ${{ steps.fetch-dmg.outputs.dmg-path }} + SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }} + run: | + echo -n "$SPARKLE_PRIVATE_KEY" > sparkle_private_key + chmod 600 sparkle_private_key + + ./scripts/appcast_manager/appcastManager.swift \ + --release-to-internal-channel \ + --dmg ${DMG_PATH} \ + --release-notes release_notes.txt \ + --key sparkle_private_key + + appcast_patch_name="appcast2-${{ steps.verify-tag.outputs.tag-in-filename }}.patch" + mv -f ${{ env.SPARKLE_DIR }}/appcast_diff.txt ${{ env.SPARKLE_DIR }}/${appcast_patch_name} + echo "appcast-patch-name=${appcast_patch_name}" >> $GITHUB_OUTPUT + + - name: Upload appcast diff artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.appcast.outputs.appcast-patch-name }} + path: ${{ env.SPARKLE_DIR }}/${{ steps.appcast.outputs.appcast-patch-name }} + + - name: Upload to S3 + id: upload + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: ${{ vars.AWS_DEFAULT_REGION }} + run: | + # Back up existing appcast2.xml + OLD_APPCAST_NAME=appcast2_old.xml + echo "OLD_APPCAST_NAME=${OLD_APPCAST_NAME}" >> $GITHUB_ENV + curl -fLSs "${{ vars.DMG_URL_ROOT }}appcast2.xml" --output "${OLD_APPCAST_NAME}" + + # Upload files to S3 + ./scripts/upload_to_s3/upload_to_s3.sh --run --force + + if [[ -f "${{ env.SPARKLE_DIR }}/uploaded_files_list.txt" ]]; then + echo "FILES_UPLOADED=$(awk '{ print "
  • "$1"
  • "; }' < ${{ env.SPARKLE_DIR }}/uploaded_files_list.txt | tr '\n' ' ')" >> $GITHUB_ENV + else + echo "FILES_UPLOADED='No files uploaded.'" >> $GITHUB_ENV + fi + + - name: Set common environment variables + if: always() + env: + DMG_NAME: ${{ steps.fetch-dmg.outputs.dmg-name }} + run: | + echo "APPCAST_PATCH_NAME=${{ steps.appcast.outputs.appcast-patch-name }}" >> $GITHUB_ENV + echo "DMG_NAME=${DMG_NAME}" >> $GITHUB_ENV + echo "DMG_URL=${{ vars.DMG_URL_ROOT }}${DMG_NAME}" >> $GITHUB_ENV + echo "RELEASE_BUCKET_NAME=${{ vars.RELEASE_BUCKET_NAME }}" >> $GITHUB_ENV + echo "RELEASE_BUCKET_PREFIX=${{ vars.RELEASE_BUCKET_PREFIX }}" >> $GITHUB_ENV + echo "RELEASE_TASK_ID=${{ steps.task-id.outputs.task-id }}" >> $GITHUB_ENV + echo "TAG=${{ github.event.inputs.tag }}" >> $GITHUB_ENV + echo "WORKFLOW_URL=https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> $GITHUB_ENV + + - name: Set up Asana templates + if: always() + id: asana-templates + run: | + if [[ ${{ steps.upload.outcome }} == "success" ]]; then + echo "task-template=validate-check-for-updates-internal" >> $GITHUB_OUTPUT + echo "comment-template=validate-check-for-updates-internal" >> $GITHUB_OUTPUT + echo "release-task-comment-template=internal-release-complete" >> $GITHUB_OUTPUT + else + echo "task-template=appcast-failed" >> $GITHUB_OUTPUT + echo "comment-template=appcast-failed" >> $GITHUB_OUTPUT + fi + + - name: Create Asana task + id: create-task + if: always() + uses: ./.github/actions/asana-create-action-item + with: + access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} + release-task-url: ${{ github.event.inputs.asana-task-url }} + template-name: ${{ steps.asana-templates.outputs.task-template }} + + - name: Upload patch to the Asana task + id: upload-patch + if: success() + uses: ./.github/actions/asana-upload + with: + access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} + file-name: ${{ env.SPARKLE_DIR }}/${{ steps.appcast.outputs.appcast-patch-name }} + task-id: ${{ steps.create-task.outputs.new-task-id }} + + - name: Upload old appcast file to the Asana task + id: upload-old-appcast + if: success() + uses: ./.github/actions/asana-upload + with: + access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} + file-name: ${{ env.OLD_APPCAST_NAME }} + task-id: ${{ steps.create-task.outputs.new-task-id }} + + - name: Upload release notes to the Asana task + id: upload-release-notes + if: success() + uses: ./.github/actions/asana-upload + with: + access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} + file-name: ${{ env.RELEASE_NOTES_FILE }} + task-id: ${{ steps.create-task.outputs.new-task-id }} + + - name: Report status + if: always() + uses: ./.github/actions/asana-log-message + env: + ASSIGNEE_ID: ${{ steps.create-task.outputs.assignee-id }} + TASK_ID: ${{ steps.create-task.outputs.new-task-id }} + with: + access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} + task-url: ${{ github.event.inputs.asana-task-url }} + template-name: ${{ steps.asana-templates.outputs.comment-template }} + + - name: Add a comment to the release task + if: success() + uses: ./.github/actions/asana-add-comment + with: + access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} + task-url: ${{ github.event.inputs.asana-task-url }} + template-name: internal-release-complete diff --git a/scripts/extract_release_notes.sh b/scripts/extract_release_notes.sh index fba7600977..175cbf5ee0 100755 --- a/scripts/extract_release_notes.sh +++ b/scripts/extract_release_notes.sh @@ -6,7 +6,7 @@ # cat release_task_description.txt | ./extract_release_notes.sh # -notes_start="release notes:" +notes_start="release notes" notes_end="this release includes:" is_release_notes=0 has_release_notes=0 diff --git a/scripts/update_asana_for_release.sh b/scripts/update_asana_for_release.sh new file mode 100755 index 0000000000..6f70c994e7 --- /dev/null +++ b/scripts/update_asana_for_release.sh @@ -0,0 +1,181 @@ +#!/bin/bash +# +# This scripts updates Asana tasks related to the release: +# - Updates "This release includes:" section of the release task with the list +# of Asana tasks linked in git commit messages since the last official release tag. +# - Moves all tasks (including the release task itself) to the Validation section +# in macOS App Board project. +# - Tags all tasks with the release tag (creating the tag as needed). +# +# Note: this script is intended to be run in CI environment and should not +# be run locally as part of the release process. +# +# Usage: +# ./update_asana_for_release.sh +# + +set -e -o pipefail + +workspace_id="137249556945" +asana_api_url="https://app.asana.com/api/1.0" +task_url_regex='^https://app.asana.com/[0-9]/[0-9]*/([0-9]*)/f$' +cwd="$(dirname "${BASH_SOURCE[0]}")" + +find_task_urls_in_git_log() { + git fetch -q --tags + last_release_tag="$(gh api /repos/duckduckgo/macos-browser/releases/latest --jq .tag_name)" + + # 1. Fetch all commit messages since the last release tag + # 2. Extract Asana task URLs from the commit messages + # (Use -A 1 to handle cases where URL is on the next line after "Task/Issue URL:") + # 3. Print the last space-separated field ($NF) of each line + # 4. Filter only Asana URLs + # 5. Remove duplicates + git log "${last_release_tag}"..HEAD \ + | grep -A 1 'Task.*URL' \ + | awk '{ print $NF; }' \ + | grep app\.asana\.com \ + | uniq +} + +fetch_current_release_notes() { + local release_task_id="$1" + curl -fLSs "${asana_api_url}/tasks/${release_task_id}?opt_fields=notes" \ + -H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}" \ + | jq -r .data.notes \ + | "${cwd}"/extract_release_notes.sh +} + +get_task_id() { + local url="$1" + if [[ "$url" =~ ${task_url_regex} ]]; then + echo "${BASH_REMATCH[1]}" + fi +} + +construct_task_description() { + local escaped_release_note + printf '%s' "Note: This task's description is managed automatically.\n" + printf '%s' 'Only the Release notes section below should be modified manually.\n' + printf '%s' 'Please do not adjust formatting.

    Release notes

    ' + if [[ -n "${release_notes[*]}" ]]; then + printf '%s' '
      ' + for release_note in "${release_notes[@]}"; do + escaped_release_note="$(sed -e 's/&/\&/g' -e 's//\>/g' <<< "${release_note}")" + printf '%s' "
    • ${escaped_release_note}
    • " + done + printf '%s' '
    ' + fi + + printf '%s' '

    This release includes:

    ' + + if [[ -n "${task_ids[*]}" ]]; then + printf '%s' '' + fi + + printf '%s' '' +} + +update_task_description() { + local html_notes="$1" + local request_payload="{\"data\":{\"html_notes\":\"${html_notes}\"}}" + + curl -fLSs -X PUT "${asana_api_url}/tasks/${release_task_id}?opt_fields=permalink_url" \ + -H 'Content-Type: application/json' \ + -H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}" \ + -d "$request_payload" | jq -r .data.permalink_url +} + +move_tasks_to_section() { + local section_id="$1" + shift + local task_ids=("$@") + + for task_id in "${task_ids[@]}"; do + curl -fLSs "${asana_api_url}/sections/${section_id}/addTask" \ + -H 'Content-Type: application/json' \ + -H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}" \ + --output /dev/null \ + -d "{\"data\": {\"task\": \"${task_id}\"}}" + done +} + +find_or_create_asana_release_tag() { + local marketing_version="$1" + local tag_name="macos-app-release-${marketing_version}" + local tag_id + + tag_id="$(curl -fLSs "${asana_api_url}/tasks/${release_task_id}/tags?opt_fields=name" \ + -H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}" \ + | jq -r ".data[] | select(.name==\"${tag_name}\").gid")" + + if [[ -z "$tag_id" ]]; then + tag_id=$(curl -fLSs "${asana_api_url}/workspaces/${workspace_id}/tags?opt_fields=gid" \ + -H 'Content-Type: application/json' \ + -H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}" \ + -d "{\"data\": {\"name\": \"${tag_name}\"}}" | jq -r .data.gid) + fi + + echo "$tag_id" +} + +tag_tasks() { + local tag_id="$1" + shift + local task_ids=("$@") + + for task_id in "${task_ids[@]}"; do + curl -fLSs "${asana_api_url}/tasks/${task_id}/addTag" \ + -H 'Content-Type: application/json' \ + -H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}" \ + --output /dev/null \ + -d "{\"data\": {\"tag\": \"${tag_id}\"}}" + done +} + +main() { + local release_task_id="$1" + local marketing_version="$2" + local validation_section_id="$3" + + if [[ -z "$release_task_id" ]]; then + echo "Usage: $0 " + exit 1 + fi + + # 1. Fetch task URLs from git commit messages + task_ids=() + while read -r line; do + task_ids+=("$(get_task_id "$line")") + done <<< "$(find_task_urls_in_git_log)" + + # 2. Fetch current release notes from Asana release task. + release_notes=() + while read -r line; do + release_notes+=("$line") + done <<< "$(fetch_current_release_notes "${release_task_id}")" + + # 3. Construct new release task description + local html_notes + html_notes="$(construct_task_description)" + + # 4. Update release task description + update_task_description "$html_notes" + + # 5. Move all tasks (including release task itself) to the validation section + task_ids+=("${release_task_id}") + move_tasks_to_section "$validation_section_id" "${task_ids[@]}" + + # 6. Get the existing Asana tag for the release, or create a new one. + local tag_id + tag_id=$(find_or_create_asana_release_tag "$marketing_version") + + # 7. Tag all tasks with the release tag + tag_tasks "$tag_id" "${task_ids[@]}" +} + +main "$@" \ No newline at end of file From 3e586d09c881fb84f4acbb9adfaccea68d6774c3 Mon Sep 17 00:00:00 2001 From: Pete Smith Date: Fri, 16 Feb 2024 12:06:29 +0000 Subject: [PATCH 10/44] Consolidate Letter Icon Creation in Autofill, Favorites and Recently Visited UI (#2201) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1177771139624306/1205337057550165/f **Description**: When we don’t have a cached Favicon, we display letters in place of the Favicon. We currently do this in several places - the Autofill UI, Favorites UI, and Recently Visited Sites. However, currently we lack consistency in that two letters are displayed in Autofill, while one letter is displayed in Favorites and Recently Visited Sites. This PR aims to address this by consolidating the method used to display these letters. --- .../Common/View/SwiftUI/FaviconView.swift | 19 ++++++------------ .../View/SwiftUI/LoginFaviconView.swift | 3 ++- .../HomePage/View/RecentlyVisitedView.swift | 8 ++++++-- .../SwiftUIExtensions/LetterIconView.swift | 20 +++++++++++++------ 4 files changed, 28 insertions(+), 22 deletions(-) diff --git a/DuckDuckGo/Common/View/SwiftUI/FaviconView.swift b/DuckDuckGo/Common/View/SwiftUI/FaviconView.swift index 8249a26e71..9bf9832bd8 100644 --- a/DuckDuckGo/Common/View/SwiftUI/FaviconView.swift +++ b/DuckDuckGo/Common/View/SwiftUI/FaviconView.swift @@ -26,6 +26,7 @@ struct FaviconView: View { let url: URL? let size: CGFloat let onFaviconMissing: (() -> Void)? + private var letterPaddingModifier: CGFloat var domain: String { url?.host ?? "" @@ -34,9 +35,12 @@ struct FaviconView: View { @State var image: NSImage? @State private var timer = Timer.publish(every: 0.1, tolerance: 0, on: .main, in: .default, options: nil).autoconnect() - init(url: URL?, size: CGFloat = 32, onFaviconMissing: (() -> Void)? = nil) { + /// Initializes a `FaviconView` + /// Note: The `letterPaddingModifier` parameter is only used when a `LetterIconView` is displayed instead of a Favicon image + init(url: URL?, size: CGFloat = 32, letterPaddingModifier: CGFloat = 0.33, onFaviconMissing: (() -> Void)? = nil) { self.url = url self.size = size + self.letterPaddingModifier = letterPaddingModifier self.onFaviconMissing = onFaviconMissing } @@ -73,18 +77,7 @@ struct FaviconView: View { timer.upstream.connect().cancel() } } else { - - ZStack { - let eTLDplus1 = ContentBlocking.shared.tld.eTLDplus1(domain) ?? domain - Rectangle() - .foregroundColor(Color.forString(eTLDplus1)) - Text(String(eTLDplus1.capitalized.first ?? "?")) - .font(.title) - .foregroundColor(Color.white) - } - .frame(width: size, height: size) - .cornerRadius(4.0) - + LetterIconView(title: ContentBlocking.shared.tld.eTLDplus1(domain) ?? domain, size: size, paddingModifier: letterPaddingModifier) } }.onAppear { refreshImage() diff --git a/DuckDuckGo/Common/View/SwiftUI/LoginFaviconView.swift b/DuckDuckGo/Common/View/SwiftUI/LoginFaviconView.swift index a2c5557f6e..f7532478c1 100644 --- a/DuckDuckGo/Common/View/SwiftUI/LoginFaviconView.swift +++ b/DuckDuckGo/Common/View/SwiftUI/LoginFaviconView.swift @@ -35,7 +35,8 @@ struct LoginFaviconView: View { .cornerRadius(4.0) .padding(.leading, 6) } else { - LetterIconView(title: generatedIconLetters) + LetterIconView(title: generatedIconLetters, font: .system(size: 32, weight: .semibold)) + .padding(.leading, 8) } } } diff --git a/DuckDuckGo/HomePage/View/RecentlyVisitedView.swift b/DuckDuckGo/HomePage/View/RecentlyVisitedView.swift index c57c95da96..d81e82722b 100644 --- a/DuckDuckGo/HomePage/View/RecentlyVisitedView.swift +++ b/DuckDuckGo/HomePage/View/RecentlyVisitedView.swift @@ -362,6 +362,10 @@ struct SiteIconAndConnector: View { @State var isHovering = false + private var favicon: FaviconView { + FaviconView(url: site.url, size: 22, letterPaddingModifier: 0.22) + } + var body: some View { VStack(spacing: 0) { if site.isRealDomain { @@ -383,7 +387,7 @@ struct SiteIconAndConnector: View { RoundedRectangle(cornerRadius: 6) .fill(isHovering ? mouseOverColor : backgroundColor) - FaviconView(url: site.url, size: 22) + favicon } .link { self.isHovering = $0 @@ -405,7 +409,7 @@ struct SiteIconAndConnector: View { RoundedRectangle(cornerRadius: 6) .fill(backgroundColor) - FaviconView(url: site.url, size: 22) + favicon } .frame(width: 32, height: 32) } diff --git a/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/LetterIconView.swift b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/LetterIconView.swift index 84cdc2a6ed..f0e70db952 100644 --- a/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/LetterIconView.swift +++ b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/LetterIconView.swift @@ -24,7 +24,8 @@ public struct LetterIconView: View { public var size: CGFloat public var prefferedFirstCharacters: String? public var characterCount: Int - private var padding: CGFloat = 0.33 + private var paddingModifier: CGFloat + private var font: Font private static let wwwPreffix = "www." private var characters: String { @@ -35,11 +36,20 @@ public struct LetterIconView: View { return String(title.replacingOccurrences(of: Self.wwwPreffix, with: "").prefix(characterCount)) } - public init(title: String, size: CGFloat = 32, prefferedFirstCharacters: String? = nil, characterCount: Int = 2) { + /// Initializes a `LetterIconView` + /// Note: The `paddingModifier`parameter is used to calculate the inner frame width/height using `size - (size * paddingModifier)` + public init(title: String, + size: CGFloat = 32, + prefferedFirstCharacters: String? = nil, + characterCount: Int = 2, + font: Font = .title, + paddingModifier: CGFloat = 0.33) { self.title = title self.size = size self.prefferedFirstCharacters = prefferedFirstCharacters self.characterCount = characterCount + self.font = font + self.paddingModifier = paddingModifier } public var body: some View { @@ -47,13 +57,11 @@ public struct LetterIconView: View { RoundedRectangle(cornerRadius: size * 0.125) .foregroundColor(Color.forString(title)) .frame(width: size, height: size) - Text(characters.capitalized(with: .current)) - .frame(width: size - (size * padding), height: size - (size * padding)) + .frame(width: size - (size * paddingModifier), height: size - (size * paddingModifier)) .foregroundColor(.white) .minimumScaleFactor(0.01) - .font(.system(size: size, weight: .semibold)) + .font(font) } - .padding(.leading, 8) } } From e5355f9dfd923c52e8549e8859c37e64da352b91 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Fri, 16 Feb 2024 13:39:49 +0000 Subject: [PATCH 11/44] Remove unnecessary pixel (#2214) Task/Issue URL: https://app.asana.com/0/1177771139624306/1206612719423754/f **Description**: Remove unnecessary pixel for bitwarden --- DuckDuckGo/PasswordManager/Bitwarden/Model/BWManager.swift | 1 - DuckDuckGo/Statistics/PixelEvent.swift | 3 --- DuckDuckGo/Statistics/PixelParameters.swift | 1 - 3 files changed, 5 deletions(-) diff --git a/DuckDuckGo/PasswordManager/Bitwarden/Model/BWManager.swift b/DuckDuckGo/PasswordManager/Bitwarden/Model/BWManager.swift index 7e1255168d..24be234ef2 100644 --- a/DuckDuckGo/PasswordManager/Bitwarden/Model/BWManager.swift +++ b/DuckDuckGo/PasswordManager/Bitwarden/Model/BWManager.swift @@ -232,7 +232,6 @@ final class BWManager: BWManagement, ObservableObject { switch error { case "cannot-decrypt": logOrAssertionFailure("BWManagement: Bitwarden error - cannot decrypt") - Pixel.fire(.debug(event: .bitwardenRespondedCannotDecrypt)) if Pixel.Event.Repetition(key: "bitwardenRespondedCannotDecryptUnique", update: false) != .repetitive { Pixel.fire(.debug(event: .bitwardenRespondedCannotDecryptUnique())) diff --git a/DuckDuckGo/Statistics/PixelEvent.swift b/DuckDuckGo/Statistics/PixelEvent.swift index e15e0c4066..c4de970179 100644 --- a/DuckDuckGo/Statistics/PixelEvent.swift +++ b/DuckDuckGo/Statistics/PixelEvent.swift @@ -288,7 +288,6 @@ extension Pixel { case removedInvalidBookmarkManagedObjects case bitwardenNotResponding - case bitwardenRespondedCannotDecrypt case bitwardenRespondedCannotDecryptUnique(repetition: Repetition = .init(key: "bitwardenRespondedCannotDecryptUnique")) case bitwardenHandshakeFailed case bitwardenDecryptionOfSharedKeyFailed @@ -734,8 +733,6 @@ extension Pixel.Event.Debug { case .bitwardenNotResponding: return "bitwarden_not_responding" - case .bitwardenRespondedCannotDecrypt: - return "bitwarden_responded_cannot_decrypt" case .bitwardenRespondedCannotDecryptUnique: return "bitwarden_responded_cannot_decrypt_unique" case .bitwardenHandshakeFailed: diff --git a/DuckDuckGo/Statistics/PixelParameters.swift b/DuckDuckGo/Statistics/PixelParameters.swift index f5aa69b6c2..60ff916e27 100644 --- a/DuckDuckGo/Statistics/PixelParameters.swift +++ b/DuckDuckGo/Statistics/PixelParameters.swift @@ -224,7 +224,6 @@ extension Pixel.Event.Debug { .webKitDidTerminate, .removedInvalidBookmarkManagedObjects, .bitwardenNotResponding, - .bitwardenRespondedCannotDecrypt, .bitwardenRespondedCannotDecryptUnique, .bitwardenHandshakeFailed, .bitwardenDecryptionOfSharedKeyFailed, From a54bb937f6bd0f5b3ba9ae161ca613d54812ee8d Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Fri, 16 Feb 2024 13:22:30 -0300 Subject: [PATCH 12/44] DBP: Delete profile data and broker data (#2213) --- .../Storage/DataBrokerProtectionDatabaseProvider.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionDatabaseProvider.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionDatabaseProvider.swift index 384157de75..ab59f166cc 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionDatabaseProvider.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionDatabaseProvider.swift @@ -320,7 +320,8 @@ final class DefaultDataBrokerProtectionDatabaseProvider: GRDBSecureStorageDataba } func deleteProfileData() throws { - try db.write { db in + try db.writeWithoutTransaction { db in + try db.execute(sql: "PRAGMA foreign_keys = OFF;") try OptOutDB .deleteAll(db) try ScanDB @@ -331,6 +332,11 @@ final class DefaultDataBrokerProtectionDatabaseProvider: GRDBSecureStorageDataba .deleteAll(db) try PhoneDB .deleteAll(db) + try ProfileDB + .deleteAll(db) + try BrokerDB + .deleteAll(db) + try db.execute(sql: "PRAGMA foreign_keys = ON;") } } From a002b1736a7ad0d84731752d26996a3e88d12890 Mon Sep 17 00:00:00 2001 From: Sabrina Tardio <44158575+SabrinaTardio@users.noreply.github.com> Date: Fri, 16 Feb 2024 19:01:51 +0100 Subject: [PATCH 13/44] removes plurals and fix some strings issues (#2215) Task/Issue URL: https://app.asana.com/0/1199230911884351/1206591469977954/f **Description**: Changes strings to remove need for pluralisation, makes some subscription and NetP strings not localisable, fixes some comment, uses attributed strings for unsupported devices. --- DuckDuckGo/Common/Localizables/UserText.swift | 83 +++++---- .../HomePage/View/RecentlyVisitedView.swift | 4 +- DuckDuckGo/Localizable.xcstrings | 166 +++--------------- .../View/PreferencesAboutView.swift | 58 +++--- .../View/RecentlyClosedMenu.swift | 13 +- .../WaitlistTermsAndConditionsView.swift | 126 ++++++------- 6 files changed, 176 insertions(+), 274 deletions(-) diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index e590978dc5..985fd7fcaa 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -284,19 +284,16 @@ struct UserText { static let selectedDomainsDescription = NSLocalizedString("fire.selected-domains.description", value: "Clear data only for selected domains", comment: "Description of the 'Current Window' configuration option for the fire button") static let selectSiteToClear = NSLocalizedString("fire.select-site-to-clear", value: "Select a site to clear its data.", comment: "Info label in the fire button popover") static func activeTabsInfo(tabs: Int, sites: Int) -> String { - let siteString = sites == 1 ? "site" : "sites" - let tabsString = tabs == 1 ? "tab" : "tabs" let localized = NSLocalizedString("fire.active-tabs-info", - value: "Close %d active %@ and clear all browsing history and cookies (%d %@).", + value: "Close active tabs (%d) and clear all browsing history and cookies (sites: %d).", comment: "Info in the Fire Button popover") - return String(format: localized, tabs, tabsString, sites, siteString) + return String(format: localized, tabs, sites) } static func oneTabInfo(sites: Int) -> String { - let siteString = sites == 1 ? "site" : "sites" let localized = NSLocalizedString("fire.one-tab-info", - value: "Close this tab and clear its browsing history and cookies (%d %@).", + value: "Close this tab and clear its browsing history and cookies (sites: %d).", comment: "Info in the Fire Button popover") - return String(format: localized, sites, siteString) + return String(format: localized, sites) } static let fireDialogDetails = NSLocalizedString("fire.dialog.details", value: "Details", comment: "Button to show more details") static let fireDialogWindowWillClose = NSLocalizedString("fire.dialog.window-will-close", value: "Current window will close", comment: "Warning label shown in an expanded view of the fire popover") @@ -444,8 +441,8 @@ struct UserText { static let emailOptionsMenuItem = NSLocalizedString("email.optionsMenu", value: "Email Protection", comment: "Menu item email feature") static let emailOptionsMenuCreateAddressSubItem = NSLocalizedString("email.optionsMenu.createAddress", value: "Generate Private Duck Address", comment: "Create an email alias sub menu item") static let emailOptionsMenuTurnOffSubItem = NSLocalizedString("email.optionsMenu.turnOff", value: "Disable Email Protection Autofill", comment: "Disable email sub menu item") - static let emailOptionsMenuTurnOnSubItem = NSLocalizedString("email.optionsMenu.turnOn", value: "Enable Email Protection", comment: "Enable email sub menu item") - static let privateEmailCopiedToClipboard = NSLocalizedString("email.copied", value: "New address copied to your clipboard", comment: "Private email address was copied to clipboard message") + static let emailOptionsMenuTurnOnSubItem = NSLocalizedString("email.optionsMenu.turnOn", value: "Enable Email Protection", comment: "Sub menu item to enable Email Protection") + static let privateEmailCopiedToClipboard = NSLocalizedString("email.copied", value: "New address copied to your clipboard", comment: "Notification that the Private email address was copied to clipboard after the user generated a new address") static let emailOptionsMenuManageAccountSubItem = NSLocalizedString("email.optionsMenu.manageAccount", value: "Manage Account", comment: "Manage private email account sub menu item") static let newFolder = NSLocalizedString("folder.optionsMenu.newFolder", value: "New Folder", comment: "Option for creating a new folder") @@ -456,7 +453,7 @@ struct UserText { static let updateBookmark = NSLocalizedString("bookmark.update", value: "Update Bookmark", comment: "Option for updating a bookmark") - static let failedToOpenExternally = NSLocalizedString("open.externally.failed", value: "The app required to open that link can’t be found", comment: "’Link’ is link on a website") + static let failedToOpenExternally = NSLocalizedString("open.externally.failed", value: "The app required to open that link can’t be found", comment: "’Link’ is link on a website, it couldn't be opened due to the required app not being found") // MARK: Permission static let devicePermissionAuthorizationFormat = NSLocalizedString("permission.authorization.format", @@ -493,7 +490,7 @@ struct UserText { static let permissionGeolocationServicesDisabled = NSLocalizedString("permission.disabled.system", value: "System location services are disabled", comment: "Geolocation Services are disabled in System Preferences") static let permissionOpenSystemSettings = NSLocalizedString("permission.open.settings", value: "Open System Settings", comment: "Open System Settings (to re-enable permission for the App) (macOS 13 and above)") - static let permissionPopupTitle = NSLocalizedString("permission.popup.title", value: "Blocked Pop-ups", comment: "List of blocked popups Title") + static let permissionPopupTitle = NSLocalizedString("permission.popup.title", value: "Blocked Pop-ups", comment: "Title of a popup that has a list of blocked popups") static let permissionPopupOpenFormat = NSLocalizedString("permission.popup.open.format", value: "%@", comment: "Open %@ URL Pop-up") static let permissionExternalSchemeOpenFormat = NSLocalizedString("permission.externalScheme.open.format", value: "Open %@", comment: "Open %@ App Name") @@ -571,15 +568,15 @@ struct UserText { static let privacySimplified = NSLocalizedString("preferences.about.privacy-simplified", value: "Privacy, simplified.", comment: "About screen") static let aboutUnsupportedDeviceInfo1 = NSLocalizedString("preferences.about.unsupported-device-info1", value: "DuckDuckGo is no longer providing browser updates for your version of macOS.", comment: "") static func aboutUnsupportedDeviceInfo2(version: String) -> String { - let localized = NSLocalizedString("preferences.about.unsupported-device-info2", value: "Please update to macOS %@ or later to use the most recent version", comment: "Link to the about page") + let localized = NSLocalizedString("preferences.about.unsupported-device-info2", value: "Please update to macOS %@ or later to use the most recent version of DuckDuckGo. You can also keep using your current version of the browser, but it will not receive further updates.", comment: "Copy in section that tells the user to update their macOS version since their current version is unsupported") return String(format: localized, version) } - static let aboutUnsupportedDeviceInfo2Part1 = NSLocalizedString("preferences.about.unsupported-device-info2-part1", value: "Please", comment: "Second paragraph of unsupported device info - sentence part 1") + static let aboutUnsupportedDeviceInfo2Part1 = "Please" static func aboutUnsupportedDeviceInfo2Part2(version: String) -> String { - return String(format: NSLocalizedString("preferences.about.unsupported-device-info2-part2", value: "update to macOS %@", comment: "Second paragraph of unsupported device info - sentence part 2 (underlined)"), version) + return String(format: "update to macOS %@", version) } - static let aboutUnsupportedDeviceInfo2Part3 = NSLocalizedString("preferences.about.unsupported-device-info2-part3", value: "or later to use the most recent version", comment: "Second paragraph of unsupported device info - sentence part 3") - static let aboutUnsupportedDeviceInfo2Part4 = NSLocalizedString("preferences.about.unsupported-device-info2-part4", value: "of DuckDuckGo. You can also keep using your current version of the browser, but it will not receive further updates.", comment: "Second paragraph of unsupported device info - sentence part 4") + static let aboutUnsupportedDeviceInfo2Part3 = "or later to use the most recent version" + static let aboutUnsupportedDeviceInfo2Part4 = "of DuckDuckGo. You can also keep using your current version of the browser, but it will not receive further updates." static let unsupportedDeviceInfoAlertHeader = NSLocalizedString("unsupported.device.info.alert.header", value: "Your version of macOS is no longer supported.", comment: "") @@ -590,10 +587,10 @@ struct UserText { static let sendFeedback = NSLocalizedString("preferences.about.send-feedback", value: "Send Feedback", comment: "Feedback button in the about preferences page") - static let feedbackDisclaimer = NSLocalizedString("feedback.disclaimer", value: "Reports sent to DuckDuckGo are 100% anonymous and only include your message, the DuckDuckGo app version, and your macOS version.", comment: "Disclaimer in breakage form") + static let feedbackDisclaimer = NSLocalizedString("feedback.disclaimer", value: "Reports sent to DuckDuckGo are 100% anonymous and only include your message, the DuckDuckGo app version, and your macOS version.", comment: "Disclaimer in breakage form - a form that users can submit to say that a website is not working properly in DuckDuckGo") - static let feedbackBugDescription = NSLocalizedString("feedback.bug.description", value: "Please describe the problem in as much detail as possible:", comment: "Label in the feedback form") - static let feedbackFeatureRequestDescription = NSLocalizedString("feedback.feature.request.description", value: "What feature would you like to see?", comment: "Label in the feedback form") + static let feedbackBugDescription = NSLocalizedString("feedback.bug.description", value: "Please describe the problem in as much detail as possible:", comment: "Label in the feedback form that users can submit to say that a website is not working properly in DuckDuckGo") + static let feedbackFeatureRequestDescription = NSLocalizedString("feedback.feature.request.description", value: "What feature would you like to see?", comment: "Label in the feedback form for feature requests.") static let feedbackOtherDescription = NSLocalizedString("feedback.other.description", value: "Please give us your feedback:", comment: "Label in the feedback form") static func versionLabel(version: String, build: String) -> String { @@ -697,7 +694,7 @@ struct UserText { static func importingBookmarks(_ numberOfBookmarks: Int?) -> String { if let numberOfBookmarks, numberOfBookmarks > 0 { - let localized = NSLocalizedString("import.bookmarks.number.progress.text", value: "Importing %d bookmarks…", comment: "Operation progress info message about %d number of bookmarks being imported") + let localized = NSLocalizedString("import.bookmarks.number.progress.text", value: "Importing bookmarks (%d)…", comment: "Operation progress info message about %d number of bookmarks being imported") return String(format: localized, numberOfBookmarks) } else { return NSLocalizedString("import.bookmarks.indefinite.progress.text", value: "Importing bookmarks…", comment: "Operation progress info message about indefinite number of bookmarks being imported") @@ -706,7 +703,7 @@ struct UserText { static func importingPasswords(_ numberOfPasswords: Int?) -> String { if let numberOfPasswords, numberOfPasswords > 0 { - let localized = NSLocalizedString("import.passwords.number.progress.text", value: "Importing %d passwords…", comment: "Operation progress info message about %d number of passwords being imported") + let localized = NSLocalizedString("import.passwords.number.progress.text", value: "Importing passwords (%d)…", comment: "Operation progress info message about %d number of passwords being imported") return String(format: localized, numberOfPasswords) } else { return NSLocalizedString("import.passwords.indefinite.progress.text", value: "Importing passwords…", comment: "Operation progress info message about indefinite number of passwords being imported") @@ -723,7 +720,7 @@ struct UserText { static func homePageProtectionSummaryMessage(numberOfTrackersBlocked: Int) -> String { let localized = NSLocalizedString("home.page.protection.summary.message", value: "%@ tracking attempts blocked", - comment: "") + comment: "The number of tracking attempts blocked in the last 7 days, shown on a new tab, translate as: Tracking attempts blocked: %@") return String(format: localized, NumberFormatter.localizedString(from: NSNumber(value: numberOfTrackersBlocked), number: .decimal)) } static let homePageProtectionDurationInfo = NSLocalizedString("home.page.protection.duration", value: "PAST 7 DAYS", comment: "Past 7 days in uppercase.") @@ -740,18 +737,17 @@ struct UserText { static func tooltipClearHistoryAndData(domain: String) -> String { let localized = NSLocalizedString("tooltip.clearHistoryAndData", value: "Clear browsing history and data for %@", - comment: "Tooltip for burn button") + comment: "Tooltip for burn button where %@ is the domain") return String(format: localized, domain) } static func tooltipClearHistory(domain: String) -> String { let localized = NSLocalizedString("tooltip.clearHistory", value: "Clear browsing history for %@", - comment: "Tooltip for burn button") + comment: "Tooltip for burn button where %@ is the domain") return String(format: localized, domain) } - static let recentlyClosedMenuItemSuffixOne = NSLocalizedString("one.more.tab", value: " (and 1 more tab)", comment: "suffix of string in Recently Closed menu") - static let recentlyClosedMenuItemSuffixMultiple = NSLocalizedString("n.more.tabs", value: " (and %d more tabs)", comment: "suffix of string in Recently Closed menu") + static let recentlyClosedWindowMenuItem = NSLocalizedString("n.more.tabs", value: "Window with multiple tabs (%d)", comment: "String in Recently Closed menu item for recently closed browser window and number of tabs contained in the closed window") static let reopenLastClosedTab = NSLocalizedString("reopen.last.closed.tab", value: "Reopen Last Closed Tab", comment: "") static let reopenLastClosedWindow = NSLocalizedString("reopen.last.closed.window", value: "Reopen Last Closed Window", comment: "") @@ -795,18 +791,18 @@ struct UserText { // MARK: - Bitwarden static let passwordManager = NSLocalizedString("password.manager", value: "Password Manager", comment: "Section header") - static let bitwardenPreferencesUnableToConnect = NSLocalizedString("bitwarden.preferences.unable-to-connect", value: "Unable to find or connect to Bitwarden", comment: "") + static let bitwardenPreferencesUnableToConnect = NSLocalizedString("bitwarden.preferences.unable-to-connect", value: "Unable to find or connect to Bitwarden", comment: "Dialog telling the user Bitwarden (a password manager) is not available") static let bitwardenPreferencesCompleteSetup = NSLocalizedString("bitwarden.preferences.complete-setup", value: "Complete Setup…", comment: "") static let bitwardenPreferencesOpenBitwarden = NSLocalizedString("bitwarden.preferences.open-bitwarden", value: "Open Bitwarden", comment: "") - static let bitwardenPreferencesUnlock = NSLocalizedString("bitwarden.preferences.unlock", value: "Unlock Bitwarden", comment: "") - static let bitwardenPreferencesRun = NSLocalizedString("bitwarden.preferences.run", value: "Bitwarden app not running", comment: "") + static let bitwardenPreferencesUnlock = NSLocalizedString("bitwarden.preferences.unlock", value: "Unlock Bitwarden", comment: "Asks the user to unlock the password manager Bitwarden") + static let bitwardenPreferencesRun = NSLocalizedString("bitwarden.preferences.run", value: "Bitwarden app not running", comment: "Warns user that the password manager Bitwarden app is not running") static let bitwardenError = NSLocalizedString("bitwarden.error", value: "Unable to find or connect to Bitwarden", comment: "") static let bitwardenNotInstalled = NSLocalizedString("bitwarden.not.installed", value: "Bitwarden app is not installed", comment: "") static let bitwardenOldVersion = NSLocalizedString("bitwarden.old.version", value: "Please update Bitwarden to the latest version", comment: "") static let bitwardenIntegrationNotApproved = NSLocalizedString("bitwarden.integration.not.approved", value: "Integration with DuckDuckGo is not approved in Bitwarden app", comment: "") static let bitwardenMissingHandshake = NSLocalizedString("bitwarden.missing.handshake", value: "Missing handshake", comment: "") static let bitwardenWaitingForHandshake = NSLocalizedString("bitwarden.waiting.for.handshake", value: "Waiting for the handshake approval in Bitwarden app", comment: "") - static let bitwardenCantAccessContainer = NSLocalizedString("bitwarden.cant.access.container", value: "DuckDuckGo needs permission to access Bitwarden. You can grant DuckDuckGo Full Disk Access in System Settings, or switch back to the built-in password manager.", comment: "") + static let bitwardenCantAccessContainer = NSLocalizedString("bitwarden.cant.access.container", value: "DuckDuckGo needs permission to access Bitwarden. You can grant DuckDuckGo Full Disk Access in System Settings, or switch back to the built-in password manager.", comment: "Requests user Full Disk access in order to access password manager Birwarden") static let bitwardenHanshakeNotApproved = NSLocalizedString("bitwarden.handshake.not.approved", value: "Handshake not approved in Bitwarden app", comment: "") static let bitwardenConnecting = NSLocalizedString("bitwarden.connecting", value: "Connecting to Bitwarden", comment: "") static let bitwardenWaitingForStatusResponse = NSLocalizedString("bitwarden.waiting.for.status.response", value: "Waiting for the status response from Bitwarden", comment: "") @@ -834,7 +830,7 @@ struct UserText { static let bitwardenIntegrationCompleteInfo = NSLocalizedString("bitwarden.integration.complete.info", value: "You are now using Bitwarden as your password manager.", comment: "Setup of the integration with Bitwarden app") static let bitwardenCommunicationInfo = NSLocalizedString("bitwarden.connect.communication-info", value: "All communication between Bitwarden and DuckDuckGo is encrypted and the data never leaves your device.", comment: "") - static let bitwardenHistoryInfo = NSLocalizedString("bitwarden.connect.history-info", value: "Bitwarden will have access to your browsing history.", comment: "") + static let bitwardenHistoryInfo = NSLocalizedString("bitwarden.connect.history-info", value: "Bitwarden will have access to your browsing history.", comment: "Warn users that the password Manager Bitwarden will have access to their browsing history") static let showAutofillShortcut = NSLocalizedString("pinning.show-autofill-shortcut", value: "Show Autofill Shortcut", comment: "Menu item for showing the autofill shortcut") static let hideAutofillShortcut = NSLocalizedString("pinning.hide-autofill-shortcut", value: "Hide Autofill Shortcut", comment: "Menu item for hiding the autofill shortcut") @@ -931,7 +927,7 @@ struct UserText { return String(format: localized, domain) } - static let noAccessToDownloadsFolderHeader = NSLocalizedString("no.access.to.downloads.folder.header", value: "DuckDuckGo needs permission to access your Downloads folder", comment: "Header of the alert dialog informing user about failed download") + static let noAccessToDownloadsFolderHeader = NSLocalizedString("no.access.to.downloads.folder.header", value: "DuckDuckGo needs permission to access your Downloads folder", comment: "Header of the alert dialog warning the user they need to give the browser permission to access the Downloads folder") private static let noAccessToDownloadsFolderLegacy = NSLocalizedString("no.access.to.downloads.folder.legacy", value: "Grant access in Security & Privacy preferences in System Settings.", comment: "Alert presented to user if the app doesn't have rights to access Downloads folder. This is used for macOS version 12 and below") private static let noAccessToDownloadsFolderModern = NSLocalizedString("no.access.to.downloads.folder.modern", value: "Grant access in Privacy & Security preferences in System Settings.", comment: "Alert presented to user if the app doesn't have rights to access Downloads folder. This is used for macOS version 13 and above") @@ -1058,11 +1054,24 @@ struct UserText { } #if SUBSCRIPTION - static let subscriptionOptionsMenuItem = NSLocalizedString("subscription.menu.item", value: "Privacy Pro", comment: "Title for Subscription item in the options menu") - static let subscription = NSLocalizedString("preferences.subscription", value: "Privacy Pro", comment: "Show subscription preferences") + // Key: "subscription.menu.item" + // Comment: "Title for Subscription item in the options menu" + static let subscriptionOptionsMenuItem = "Privacy Pro" - static let purchasingSubscriptionTitle = NSLocalizedString("subscription.progress.view.purchasing.subscription", value: "Purchase in progress...", comment: "Progress view title when starting the purchase") - static let restoringSubscriptionTitle = NSLocalizedString("subscription.progress.view.restoring.subscription", value: "Restoring subscription...", comment: "Progress view title when restoring past subscription purchase") - static let completingPurchaseTitle = NSLocalizedString("subscription.progress.view.completing.purchase", value: "Completing purchase...", comment: "Progress view title when completing the purchase") + // Key: "preferences.subscription" + // Comment: "Show subscription preferences" + static let subscription = "Privacy Pro" + + // Key: "subscription.progress.view.purchasing.subscription" + // Comment: "Progress view title when starting the purchase" + static let purchasingSubscriptionTitle = "Purchase in progress..." + + // Key: "subscription.progress.view.restoring.subscription" + // Comment: "Progress view title when restoring past subscription purchase" + static let restoringSubscriptionTitle = "Restoring subscription..." + + // Key: "subscription.progress.view.completing.purchase" + // Comment: "Progress view title when completing the purchase" + static let completingPurchaseTitle = "Completing purchase..." #endif } diff --git a/DuckDuckGo/HomePage/View/RecentlyVisitedView.swift b/DuckDuckGo/HomePage/View/RecentlyVisitedView.swift index d81e82722b..c7e865ac7a 100644 --- a/DuckDuckGo/HomePage/View/RecentlyVisitedView.swift +++ b/DuckDuckGo/HomePage/View/RecentlyVisitedView.swift @@ -444,9 +444,9 @@ struct SiteTrackerSummary: View { Group { Group { if #available(macOS 12, *) { - Text("**\(site.numberOfTrackersBlocked)** tracking attempts blocked") + Text("**\(site.numberOfTrackersBlocked)** tracking attempts blocked", comment: "The number of tracking attempts blocked in the last 7 days, shown on a new tab, translate as: Tracking attempts blocked: %@") } else { - Text("\(site.numberOfTrackersBlocked) tracking attempts blocked") + Text("\(site.numberOfTrackersBlocked) tracking attempts blocked", comment: "The number of tracking attempts blocked in the last 7 days, shown on a new tab, translate as: Tracking attempts blocked: %@") } } .visibility(site.blockedEntities.isEmpty ? .gone : .visible) diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index d53f2f6d1c..d98a50d6fd 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -32,7 +32,7 @@ }, "**%lld** tracking attempts blocked" : { - + "comment" : "The number of tracking attempts blocked in the last 7 days, shown on a new tab, translate as: Tracking attempts blocked: %@" }, "%@ does not support storing passwords" : { "comment" : "Data Import disabled checkbox message about a browser (%@) not supporting storing passwords" @@ -41,7 +41,7 @@ }, "%lld tracking attempts blocked" : { - + "comment" : "The number of tracking attempts blocked in the last 7 days, shown on a new tab, translate as: Tracking attempts blocked: %@" }, "• Because data broker sites often have multi-step processes required to have information removed, and because they regularly update their databases with new personal information, this authorization includes ongoing action on your behalf solely to perform the service." : { @@ -1001,6 +1001,7 @@ } }, "bitwarden.cant.access.container" : { + "comment" : "Requests user Full Disk access in order to access password manager Birwarden", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1034,6 +1035,7 @@ } }, "bitwarden.connect.history-info" : { + "comment" : "Warn users that the password Manager Bitwarden will have access to their browsing history", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1227,6 +1229,7 @@ } }, "bitwarden.preferences.run" : { + "comment" : "Warns user that the password manager Bitwarden app is not running", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1238,6 +1241,7 @@ } }, "bitwarden.preferences.unable-to-connect" : { + "comment" : "Dialog telling the user Bitwarden (a password manager) is not available", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1249,6 +1253,7 @@ } }, "bitwarden.preferences.unlock" : { + "comment" : "Asks the user to unlock the password manager Bitwarden", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -2808,7 +2813,7 @@ } }, "email.copied" : { - "comment" : "Private email address was copied to clipboard message", + "comment" : "Notification that the Private email address was copied to clipboard after the user generated a new address", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -2868,7 +2873,7 @@ } }, "email.optionsMenu.turnOn" : { - "comment" : "Enable email sub menu item", + "comment" : "Sub menu item to enable Email Protection", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -3033,7 +3038,7 @@ } }, "feedback.bug.description" : { - "comment" : "Label in the feedback form", + "comment" : "Label in the feedback form that users can submit to say that a website is not working properly in DuckDuckGo", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -3045,7 +3050,7 @@ } }, "feedback.disclaimer" : { - "comment" : "Disclaimer in breakage form", + "comment" : "Disclaimer in breakage form - a form that users can submit to say that a website is not working properly in DuckDuckGo", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -3057,7 +3062,7 @@ } }, "feedback.feature.request.description" : { - "comment" : "Label in the feedback form", + "comment" : "Label in the feedback form for feature requests.", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -3123,7 +3128,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "Close %1$d active %2$@ and clear all browsing history and cookies (%3$d %4$@)." + "value" : "Close active tabs (%1$d) and clear all browsing history and cookies (sites: %2$d)." } } } @@ -3363,7 +3368,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "Close this tab and clear its browsing history and cookies (%1$d %2$@)." + "value" : "Close this tab and clear its browsing history and cookies (sites: %d)." } } } @@ -3823,6 +3828,7 @@ } }, "home.page.protection.summary.message" : { + "comment" : "The number of tracking attempts blocked in the last 7 days, shown on a new tab, translate as: Tracking attempts blocked: %@", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -3891,7 +3897,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "Importing %d bookmarks…" + "value" : "Importing bookmarks (%d)…" } } } @@ -4431,7 +4437,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "Importing %d passwords…" + "value" : "Importing passwords (%d)…" } } } @@ -5076,13 +5082,13 @@ } }, "n.more.tabs" : { - "comment" : "suffix of string in Recently Closed menu", + "comment" : "String in Recently Closed menu item for recently closed browser window and number of tabs contained in the closed window", "extractionState" : "extracted_with_value", "localizations" : { "en" : { "stringUnit" : { "state" : "new", - "value" : " (and %d more tabs)" + "value" : "Window with multiple tabs (%d)" } } } @@ -5475,7 +5481,7 @@ } }, "no.access.to.downloads.folder.header" : { - "comment" : "Header of the alert dialog informing user about failed download", + "comment" : "Header of the alert dialog warning the user they need to give the browser permission to access the Downloads folder", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -5690,18 +5696,6 @@ } } }, - "one.more.tab" : { - "comment" : "suffix of string in Recently Closed menu", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : " (and 1 more tab)" - } - } - } - }, "Only Show on New Tab" : { "comment" : "Preference for only showing the bookmarks bar on new tab" }, @@ -5766,7 +5760,7 @@ } }, "open.externally.failed" : { - "comment" : "’Link’ is link on a website", + "comment" : "’Link’ is link on a website, it couldn't be opened due to the required app not being found", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -6545,7 +6539,7 @@ } }, "permission.popup.title" : { - "comment" : "List of blocked popups Title", + "comment" : "Title of a popup that has a list of blocked popups", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -7866,61 +7860,13 @@ } }, "preferences.about.unsupported-device-info2" : { - "comment" : "Link to the about page", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Please update to macOS %@ or later to use the most recent version" - } - } - } - }, - "preferences.about.unsupported-device-info2-part1" : { - "comment" : "Second paragraph of unsupported device info - sentence part 1", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Please" - } - } - } - }, - "preferences.about.unsupported-device-info2-part2" : { - "comment" : "Second paragraph of unsupported device info - sentence part 2 (underlined)", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "update to macOS %@" - } - } - } - }, - "preferences.about.unsupported-device-info2-part3" : { - "comment" : "Second paragraph of unsupported device info - sentence part 3", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "or later to use the most recent version" - } - } - } - }, - "preferences.about.unsupported-device-info2-part4" : { - "comment" : "Second paragraph of unsupported device info - sentence part 4", + "comment" : "Copy in section that tells the user to update their macOS version since their current version is unsupported", "extractionState" : "extracted_with_value", "localizations" : { "en" : { "stringUnit" : { "state" : "new", - "value" : "of DuckDuckGo. You can also keep using your current version of the browser, but it will not receive further updates." + "value" : "Please update to macOS %@ or later to use the most recent version of DuckDuckGo. You can also keep using your current version of the browser, but it will not receive further updates." } } } @@ -8188,18 +8134,6 @@ } } }, - "preferences.subscription" : { - "comment" : "Show subscription preferences", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Privacy Pro" - } - } - } - }, "preferences.sync" : { "comment" : "Show sync preferences", "extractionState" : "extracted_with_value", @@ -8692,54 +8626,6 @@ } } }, - "subscription.menu.item" : { - "comment" : "Title for Subscription item in the options menu", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Privacy Pro" - } - } - } - }, - "subscription.progress.view.completing.purchase" : { - "comment" : "Progress view title when completing the purchase", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Completing purchase..." - } - } - } - }, - "subscription.progress.view.purchasing.subscription" : { - "comment" : "Progress view title when starting the purchase", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Purchase in progress..." - } - } - } - }, - "subscription.progress.view.restoring.subscription" : { - "comment" : "Progress view title when restoring past subscription purchase", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Restoring subscription..." - } - } - } - }, "tab.bookmarks.title" : { "comment" : "Tab bookmarks title", "extractionState" : "extracted_with_value", @@ -8948,7 +8834,7 @@ } }, "tooltip.clearHistory" : { - "comment" : "Tooltip for burn button", + "comment" : "Tooltip for burn button where %@ is the domain", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -8960,7 +8846,7 @@ } }, "tooltip.clearHistoryAndData" : { - "comment" : "Tooltip for burn button", + "comment" : "Tooltip for burn button where %@ is the domain", "extractionState" : "extracted_with_value", "localizations" : { "en" : { diff --git a/DuckDuckGo/Preferences/View/PreferencesAboutView.swift b/DuckDuckGo/Preferences/View/PreferencesAboutView.swift index 30b4febb5f..96315f1cc0 100644 --- a/DuckDuckGo/Preferences/View/PreferencesAboutView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesAboutView.swift @@ -131,26 +131,11 @@ extension Preferences { let narrowContentView = Text(combinedText) let wideContentView: some View = VStack(alignment: .leading, spacing: 0) { - HStack(alignment: .center, spacing: 0) { - Text(UserText.aboutUnsupportedDeviceInfo2Part1 + " ") - Button(action: { - NSWorkspace.shared.open(Self.softwareUpdateURL) - }) { - Text(UserText.aboutUnsupportedDeviceInfo2Part2(version: versionString) + " ") - .foregroundColor(Color.blue) - .underline() - } - .buttonStyle(PlainButtonStyle()) - .onHover { hovering in - if hovering { - NSCursor.pointingHand.set() - } else { - NSCursor.arrow.set() - } - } - Text(UserText.aboutUnsupportedDeviceInfo2Part3) + if #available(macOS 12.0, *) { + Text(aboutUnsupportedDeviceInfo2Attributed) + } else { + aboutUnsupportedDeviceInfo2DeprecatedView() } - Text(UserText.aboutUnsupportedDeviceInfo2Part4) } return HStack(alignment: .top) { @@ -169,6 +154,39 @@ extension Preferences { .cornerRadius(8) .frame(width: width, height: height) } - } + @available(macOS 12, *) + private var aboutUnsupportedDeviceInfo2Attributed: AttributedString { + let baseString = UserText.aboutUnsupportedDeviceInfo2(version: versionString) + var instructions = AttributedString(baseString) + if let range = instructions.range(of: "macOS \(versionString)") { + instructions[range].link = Self.softwareUpdateURL + } + return instructions + } + + @ViewBuilder + private func aboutUnsupportedDeviceInfo2DeprecatedView() -> some View { + HStack(alignment: .center, spacing: 0) { + Text(verbatim: UserText.aboutUnsupportedDeviceInfo2Part1 + " ") + Button(action: { + NSWorkspace.shared.open(Self.softwareUpdateURL) + }) { + Text(verbatim: UserText.aboutUnsupportedDeviceInfo2Part2(version: versionString) + " ") + .foregroundColor(Color.blue) + .underline() + } + .buttonStyle(PlainButtonStyle()) + .onHover { hovering in + if hovering { + NSCursor.pointingHand.set() + } else { + NSCursor.arrow.set() + } + } + Text(verbatim: UserText.aboutUnsupportedDeviceInfo2Part3) + } + Text(verbatim: UserText.aboutUnsupportedDeviceInfo2Part4) + } + } } diff --git a/DuckDuckGo/RecentlyClosed/View/RecentlyClosedMenu.swift b/DuckDuckGo/RecentlyClosed/View/RecentlyClosedMenu.swift index 19d72e71d3..79dfdb9fa0 100644 --- a/DuckDuckGo/RecentlyClosed/View/RecentlyClosedMenu.swift +++ b/DuckDuckGo/RecentlyClosed/View/RecentlyClosedMenu.swift @@ -100,18 +100,7 @@ private extension NSMenuItem { return nil } - if recentlyClosedWindow.tabs.count > 1 { - let moreCount = recentlyClosedWindow.tabs.count - 1 - let titleSuffix: String - if moreCount == 1 { - titleSuffix = UserText.recentlyClosedMenuItemSuffixOne - } else { - titleSuffix = String(format: UserText.recentlyClosedMenuItemSuffixMultiple, moreCount) - } - - item.title = item.title.truncated(length: MainMenu.Constants.maxTitleLength - titleSuffix.count) - item.title += titleSuffix - } + item.title = String(format: UserText.recentlyClosedWindowMenuItem, recentlyClosedWindow.tabs.count) item.representedObject = recentlyClosedWindow return item } diff --git a/DuckDuckGo/Waitlist/Views/WaitlistSteps/WaitlistTermsAndConditionsView.swift b/DuckDuckGo/Waitlist/Views/WaitlistSteps/WaitlistTermsAndConditionsView.swift index a57f240f43..78d79bbf5b 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistSteps/WaitlistTermsAndConditionsView.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistSteps/WaitlistTermsAndConditionsView.swift @@ -91,61 +91,61 @@ private extension Text { struct NetworkProtectionTermsAndConditionsContentView: View { var body: some View { VStack(alignment: .leading, spacing: 0) { - Text(UserText.networkProtectionPrivacyPolicyTitle) + Text(verbatim: UserText.networkProtectionPrivacyPolicyTitle) .font(.system(size: 15, weight: .bold)) .multilineTextAlignment(.leading) Group { - Text(UserText.networkProtectionPrivacyPolicySection1Title).titleStyle() + Text(verbatim: UserText.networkProtectionPrivacyPolicySection1Title).titleStyle() if #available(macOS 12.0, *) { - Text(LocalizedStringKey(UserText.networkProtectionPrivacyPolicySection1ListMarkdown)).bodyStyle() + Text(verbatim: UserText.networkProtectionPrivacyPolicySection1ListMarkdown).bodyStyle() } else { - Text(UserText.networkProtectionPrivacyPolicySection1ListNonMarkdown).bodyStyle() + Text(verbatim: UserText.networkProtectionPrivacyPolicySection1ListNonMarkdown).bodyStyle() } - Text(UserText.networkProtectionPrivacyPolicySection2Title).titleStyle() - Text(UserText.networkProtectionPrivacyPolicySection2List).bodyStyle() - Text(UserText.networkProtectionPrivacyPolicySection3Title).titleStyle() - Text(UserText.networkProtectionPrivacyPolicySection3List).bodyStyle() - Text(UserText.networkProtectionPrivacyPolicySection4Title).titleStyle() - Text(UserText.networkProtectionPrivacyPolicySection4List).bodyStyle() - Text(UserText.networkProtectionPrivacyPolicySection5Title).titleStyle() - Text(UserText.networkProtectionPrivacyPolicySection5List).bodyStyle() + Text(verbatim: UserText.networkProtectionPrivacyPolicySection2Title).titleStyle() + Text(verbatim: UserText.networkProtectionPrivacyPolicySection2List).bodyStyle() + Text(verbatim: UserText.networkProtectionPrivacyPolicySection3Title).titleStyle() + Text(verbatim: UserText.networkProtectionPrivacyPolicySection3List).bodyStyle() + Text(verbatim: UserText.networkProtectionPrivacyPolicySection4Title).titleStyle() + Text(verbatim: UserText.networkProtectionPrivacyPolicySection4List).bodyStyle() + Text(verbatim: UserText.networkProtectionPrivacyPolicySection5Title).titleStyle() + Text(verbatim: UserText.networkProtectionPrivacyPolicySection5List).bodyStyle() } - Text(UserText.networkProtectionTermsOfServiceTitle) + Text(verbatim: UserText.networkProtectionTermsOfServiceTitle) .font(.system(size: 15, weight: .bold)) .multilineTextAlignment(.leading) .padding(.top, 28) .padding(.bottom, 14) Group { - Text(UserText.networkProtectionTermsOfServiceSection1Title).titleStyle(topPadding: 0) - Text(UserText.networkProtectionTermsOfServiceSection1List).bodyStyle() - Text(UserText.networkProtectionTermsOfServiceSection2Title).titleStyle() + Text(verbatim: UserText.networkProtectionTermsOfServiceSection1Title).titleStyle(topPadding: 0) + Text(verbatim: UserText.networkProtectionTermsOfServiceSection1List).bodyStyle() + Text(verbatim: UserText.networkProtectionTermsOfServiceSection2Title).titleStyle() if #available(macOS 12.0, *) { - Text(LocalizedStringKey(UserText.networkProtectionTermsOfServiceSection2ListMarkdown)).bodyStyle() + Text(verbatim: UserText.networkProtectionTermsOfServiceSection2ListMarkdown).bodyStyle() } else { Text(UserText.networkProtectionTermsOfServiceSection2ListNonMarkdown).bodyStyle() } - Text(UserText.networkProtectionTermsOfServiceSection3Title).titleStyle() - Text(UserText.networkProtectionTermsOfServiceSection3List).bodyStyle() - Text(UserText.networkProtectionTermsOfServiceSection4Title).titleStyle() - Text(UserText.networkProtectionTermsOfServiceSection4List).bodyStyle() - Text(UserText.networkProtectionTermsOfServiceSection5Title).titleStyle() - Text(UserText.networkProtectionTermsOfServiceSection5List).bodyStyle() + Text(verbatim: UserText.networkProtectionTermsOfServiceSection3Title).titleStyle() + Text(verbatim: UserText.networkProtectionTermsOfServiceSection3List).bodyStyle() + Text(verbatim: UserText.networkProtectionTermsOfServiceSection4Title).titleStyle() + Text(verbatim: UserText.networkProtectionTermsOfServiceSection4List).bodyStyle() + Text(verbatim: UserText.networkProtectionTermsOfServiceSection5Title).titleStyle() + Text(verbatim: UserText.networkProtectionTermsOfServiceSection5List).bodyStyle() } Group { - Text(UserText.networkProtectionTermsOfServiceSection6Title).titleStyle() - Text(UserText.networkProtectionTermsOfServiceSection6List).bodyStyle() - Text(UserText.networkProtectionTermsOfServiceSection7Title).titleStyle() - Text(UserText.networkProtectionTermsOfServiceSection7List).bodyStyle() - Text(UserText.networkProtectionTermsOfServiceSection8Title).titleStyle() - Text(UserText.networkProtectionTermsOfServiceSection8List).bodyStyle() + Text(verbatim: UserText.networkProtectionTermsOfServiceSection6Title).titleStyle() + Text(verbatim: UserText.networkProtectionTermsOfServiceSection6List).bodyStyle() + Text(verbatim: UserText.networkProtectionTermsOfServiceSection7Title).titleStyle() + Text(verbatim: UserText.networkProtectionTermsOfServiceSection7List).bodyStyle() + Text(verbatim: UserText.networkProtectionTermsOfServiceSection8Title).titleStyle() + Text(verbatim: UserText.networkProtectionTermsOfServiceSection8List).bodyStyle() } } .padding(.all, 20) @@ -169,19 +169,19 @@ struct DataBrokerProtectionTermsAndConditionsContentView: View { var body: some View { VStack(alignment: .leading, spacing: 5) { - Text(UserText.dataBrokerProtectionPrivacyPolicyTitle) + Text(verbatim: UserText.dataBrokerProtectionPrivacyPolicyTitle) .font(.system(size: 15, weight: .bold)) .multilineTextAlignment(.leading) - Text("\nWe don’t save your personal information for this service to function.") + Text(verbatim: "\nWe don’t save your personal information for this service to function.") .fontWeight(.bold) .padding(.bottom, sectionBottomPadding) Group { - Text("• This Privacy Policy is for our waitlist beta service.") + Text(verbatim: "• This Privacy Policy is for our waitlist beta service.") HStack(spacing: 0) { - Text("• Our main ") - Text("Privacy Policy ") + Text(verbatim: "• Our main ") + Text(verbatim: "Privacy Policy ") .foregroundColor(Color.blue) .underline(color: .blue) .onTapGesture { @@ -189,94 +189,94 @@ struct DataBrokerProtectionTermsAndConditionsContentView: View { WindowsManager.openNewWindow(with: url, source: .ui, isBurner: false) } } - Text("also applies here.") + Text(verbatim: "also applies here.") } - Text("• This beta product may collect more diagnostic data than our typical products. Examples of such data include: alerts of low memory, application restarts, and user engagement with product features.") + Text(verbatim: "• This beta product may collect more diagnostic data than our typical products. Examples of such data include: alerts of low memory, application restarts, and user engagement with product features.") } .padding(.leading, groupLeadingPadding) - Text("\nYour personal information is stored locally on your device.") + Text(verbatim: "\nYour personal information is stored locally on your device.") .fontWeight(.bold) .padding(.bottom, sectionBottomPadding) Group { - Text("• The information you provide when you sign-up to use this service, for example your name, age, address, and phone number is stored on your device.") - Text("• We then scan data brokers from your device to check if any sites contain your personal information.") - Text("• We may find additional information on data broker sites through this scanning process, like alternative names or phone numbers, or the names of your relatives. This information is also stored locally on your device.") + Text(verbatim: "• The information you provide when you sign-up to use this service, for example your name, age, address, and phone number is stored on your device.") + Text(verbatim: "• We then scan data brokers from your device to check if any sites contain your personal information.") + Text(verbatim: "• We may find additional information on data broker sites through this scanning process, like alternative names or phone numbers, or the names of your relatives. This information is also stored locally on your device.") } .padding(.leading, groupLeadingPadding) - Text("\nWe submit removal requests to data broker sites on your behalf.") + Text(verbatim: "\nWe submit removal requests to data broker sites on your behalf.") .fontWeight(.bold) .padding(.bottom, sectionBottomPadding) Group { - Text("• We submit removal requests to the data broker sites directly from your device, unlike other services where the removal process is initiated on remote servers.") - Text("• The only personal information we may receive is a confirmation email from data broker sites which is deleted within 72 hours.") - Text("• We regularly re-scan data broker sites to check on the removal status of your information. If it has reappeared, we resubmit the removal request.") + Text(verbatim: "• We submit removal requests to the data broker sites directly from your device, unlike other services where the removal process is initiated on remote servers.") + Text(verbatim: "• The only personal information we may receive is a confirmation email from data broker sites which is deleted within 72 hours.") + Text(verbatim: "• We regularly re-scan data broker sites to check on the removal status of your information. If it has reappeared, we resubmit the removal request.") } .padding(.leading, groupLeadingPadding) - Text("\nTerms of Service") + Text(verbatim: "\nTerms of Service") .fontWeight(.bold) - Text("You must be eligible to use this service.") + Text(verbatim: "You must be eligible to use this service.") .fontWeight(.bold) .padding(.bottom, sectionBottomPadding) Group { - Text("• To use this service, you must be 18 or older.") + Text(verbatim: "• To use this service, you must be 18 or older.") } .padding(.leading, groupLeadingPadding) - Text("\nThe service is for limited and personal use only.") + Text(verbatim: "\nThe service is for limited and personal use only.") .fontWeight(.bold) .padding(.bottom, sectionBottomPadding) Group { - Text("• The service is available for your personal use only. You represent and warrant that you will only initiate removal of your own personal information.") - Text("• This service is available on one device only.") + Text(verbatim: "• The service is available for your personal use only. You represent and warrant that you will only initiate removal of your own personal information.") + Text(verbatim: "• This service is available on one device only.") } .padding(.leading, groupLeadingPadding) - Text("\nYou give DuckDuckGo authority to act on your Here's an updated version with the remaining content:") + Text(verbatim: "\nYou give DuckDuckGo authority to act on your Here's an updated version with the remaining content:") .fontWeight(.bold) .padding(.bottom, sectionBottomPadding) Group { - Text("• You hereby authorize DuckDuckGo to act on your behalf to request removal of your personal information from data broker sites.") - Text("• Because data broker sites often have multi-step processes required to have information removed, and because they regularly update their databases with new personal information, this authorization includes ongoing action on your behalf solely to perform the service.") + Text(verbatim: "• You hereby authorize DuckDuckGo to act on your behalf to request removal of your personal information from data broker sites.") + Text(verbatim: "• Because data broker sites often have multi-step processes required to have information removed, and because they regularly update their databases with new personal information, this authorization includes ongoing action on your behalf solely to perform the service.") } .padding(.leading, groupLeadingPadding) - Text("\nThe service cannot remove all of your information from the Internet.") + Text(verbatim: "\nThe service cannot remove all of your information from the Internet.") .fontWeight(.bold) .padding(.bottom, sectionBottomPadding) Group { - Text("• This service requests removal from a limited number of data broker sites only. You understand that we cannot guarantee that the third-party sites will honor the requests, or that your personal information will not reappear in the future.") - Text("• You understand that we will only be able to request the removal of information based upon the information you provide to us.") + Text(verbatim: "• This service requests removal from a limited number of data broker sites only. You understand that we cannot guarantee that the third-party sites will honor the requests, or that your personal information will not reappear in the future.") + Text(verbatim: "• You understand that we will only be able to request the removal of information based upon the information you provide to us.") } .padding(.leading, groupLeadingPadding) - Text("\nWe provide this beta service as-is, and without warranty.") + Text(verbatim: "\nWe provide this beta service as-is, and without warranty.") .fontWeight(.bold) .padding(.bottom, sectionBottomPadding) Group { - Text("• This service is provided as-is and without warranties or guarantees of any kind.") - Text("• To the extent possible under applicable law, DuckDuckGo will not be liable for any damage or loss arising from your use of the service. In any event, the total aggregate liability of DuckDuckGo shall not exceed $25 or the equivalent in your local currency.") - Text("• We may in the future transfer responsibility for the service to a subsidiary of DuckDuckGo. If that happens, you agree that references to “DuckDuckGo” will refer to our subsidiary, which will then become responsible for providing the service and for any liabilities relating to it.") + Text(verbatim: "• This service is provided as-is and without warranties or guarantees of any kind.") + Text(verbatim: "• To the extent possible under applicable law, DuckDuckGo will not be liable for any damage or loss arising from your use of the service. In any event, the total aggregate liability of DuckDuckGo shall not exceed $25 or the equivalent in your local currency.") + Text(verbatim: "• We may in the future transfer responsibility for the service to a subsidiary of DuckDuckGo. If that happens, you agree that references to “DuckDuckGo” will refer to our subsidiary, which will then become responsible for providing the service and for any liabilities relating to it.") } .padding(.leading, groupLeadingPadding) - Text("\nWe may terminate access at any time.") + Text(verbatim: "\nWe may terminate access at any time.") .fontWeight(.bold) .padding(.bottom, sectionBottomPadding) Group { - Text("• This service is in beta, and your access to it is temporary.") - Text("• We reserve the right to terminate access at any time in our sole discretion, including for violation of these terms or our DuckDuckGo Terms of Service, which are incorporated by reference.") + Text(verbatim: "• This service is in beta, and your access to it is temporary.") + Text(verbatim: "• We reserve the right to terminate access at any time in our sole discretion, including for violation of these terms or our DuckDuckGo Terms of Service, which are incorporated by reference.") } .padding(.leading, groupLeadingPadding) From 9512a2c38353ac011ab37479f5f9adc2db99a74b Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Fri, 16 Feb 2024 11:18:12 -0800 Subject: [PATCH 14/44] Release branch login item embed fix (#2216) Task/Issue URL: https://app.asana.com/0/1199230911884351/1206624161225335/f Tech Design URL: CC: Description: This PR fixes a couple release blockers. --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../NetworkProtectionAppEvents.swift | 2 +- .../NetworkProtectionFeatureVisibility.swift | 32 +++++-------------- UnitTests/Menus/MoreOptionsMenuTests.swift | 4 +++ 4 files changed, 14 insertions(+), 26 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 0ebcdc2ee8..50b55b4c09 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -9304,7 +9304,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "# Embeds login items for the App Store build.\n\n# Skip login item embedding for release builds until they're ready to go live.\nif [ \"${CONFIGURATION}\" = \"Release\" ]; then\n echo \"Skipping login item embedding for release build.\"\n exit 0\n \n VPN_AGENT_NAME=\"${AGENT_RELEASE_PRODUCT_NAME}\"\n PIR_AGENT_NAME=\"${DBP_BACKGROUND_AGENT_RELEASE_PRODUCT_NAME}\"\nelse\n VPN_AGENT_NAME=\"${AGENT_PRODUCT_NAME}\"\n PIR_AGENT_NAME=\"${DBP_BACKGROUND_AGENT_PRODUCT_NAME}\"\nfi\n\nVPN_AGENT_ORIGIN=$(readlink -f \"${CONFIGURATION_BUILD_DIR}/${VPN_AGENT_NAME}.app\")\nPIR_AGENT_ORIGIN=$(readlink -f \"${CONFIGURATION_BUILD_DIR}/${PIR_AGENT_NAME}.app\")\nAGENT_DESTINATION=\"${CONFIGURATION_BUILD_DIR}/${CONTENTS_FOLDER_PATH}/Library/LoginItems\"\n \n# Make sure that Library/LoginItems exists before copying\nmkdir -p \"$AGENT_DESTINATION\"\n \necho \"Copying VPN agent from $VPN_AGENT_ORIGIN to $AGENT_DESTINATION\"\nrsync -r --links \"$VPN_AGENT_ORIGIN\" \"$AGENT_DESTINATION\"\n \necho \"Copying Personal Information Removal agent from $PIR_AGENT_ORIGIN to $AGENT_DESTINATION\"\nrsync -r --links \"$PIR_AGENT_ORIGIN\" \"$AGENT_DESTINATION\"\n"; + shellScript = "# Embeds login items for the App Store build.\n\n# Skip login item embedding for release builds until they're ready to go live.\nif [ \"${CONFIGURATION}\" = \"Release\" ]; then\n VPN_AGENT_NAME=\"${AGENT_RELEASE_PRODUCT_NAME}\"\n PIR_AGENT_NAME=\"${DBP_BACKGROUND_AGENT_RELEASE_PRODUCT_NAME}\"\nelse\n VPN_AGENT_NAME=\"${AGENT_PRODUCT_NAME}\"\n PIR_AGENT_NAME=\"${DBP_BACKGROUND_AGENT_PRODUCT_NAME}\"\nfi\n\nVPN_AGENT_ORIGIN=$(readlink -f \"${CONFIGURATION_BUILD_DIR}/${VPN_AGENT_NAME}.app\")\nPIR_AGENT_ORIGIN=$(readlink -f \"${CONFIGURATION_BUILD_DIR}/${PIR_AGENT_NAME}.app\")\nAGENT_DESTINATION=\"${CONFIGURATION_BUILD_DIR}/${CONTENTS_FOLDER_PATH}/Library/LoginItems\"\n \n# Make sure that Library/LoginItems exists before copying\nmkdir -p \"$AGENT_DESTINATION\"\n \necho \"Copying VPN agent from $VPN_AGENT_ORIGIN to $AGENT_DESTINATION\"\nrsync -r --links \"$VPN_AGENT_ORIGIN\" \"$AGENT_DESTINATION\"\n \necho \"Copying Personal Information Removal agent from $PIR_AGENT_ORIGIN to $AGENT_DESTINATION\"\nrsync -r --links \"$PIR_AGENT_ORIGIN\" \"$AGENT_DESTINATION\"\n"; }; 7B31FD922AD126C40086AA24 /* Embed System Network Extension */ = { isa = PBXShellScriptBuildPhase; diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift index 981e4155b8..10877dc019 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift @@ -61,7 +61,7 @@ final class NetworkProtectionAppEvents { await removeLegacyLoginItemAndVPNConfiguration() migrateNetworkProtectionAuthTokenToSharedKeychainIfNecessary() - guard featureVisibility.isNetworkProtectionVisible() else { + if featureVisibility.shouldUninstallAutomatically() { featureVisibility.disableForAllUsers() return } diff --git a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift b/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift index 729e2e7114..35dd79662b 100644 --- a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift +++ b/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift @@ -27,6 +27,7 @@ import NetworkProtectionUI protocol NetworkProtectionFeatureVisibility { func isNetworkProtectionVisible() -> Bool + func shouldUninstallAutomatically() -> Bool func disableForAllUsers() func disableForWaitlistUsers() } @@ -65,30 +66,17 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { /// /// Once the waitlist beta has ended, we can trigger a remote change that removes the user's auth token and turn off the waitlist flag, hiding Network Protection from the user. func isNetworkProtectionVisible() -> Bool { - #if APPSTORE - return isEasterEggUser || (isUserLocaleAllowed && waitlistIsOngoing) - #else return isEasterEggUser || waitlistIsOngoing - #endif } - var isUserLocaleAllowed: Bool { - var regionCode: String? - if #available(macOS 13, *) { - regionCode = Locale.current.region?.identifier - } else { - regionCode = Locale.current.regionCode - } - - if isInternalUser { - regionCode = "US" - } + /// Returns whether Network Protection should be uninstalled automatically. + /// This is only true when the user is not an Easter Egg user, the waitlist test has ended, and the user is onboarded. + func shouldUninstallAutomatically() -> Bool { + let waitlistAccessEnded = isWaitlistUser && !waitlistIsOngoing + let isNotEasterEggUser = !isEasterEggUser + let isOnboarded = UserDefaults.netP.networkProtectionOnboardingStatus != .default - #if DEBUG // Always assume US for debug builds - regionCode = "US" - #endif - - return (regionCode ?? "US") == "US" + return isNotEasterEggUser && waitlistAccessEnded && isOnboarded } /// Whether the user is fully onboarded @@ -152,10 +140,6 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { } } - private var isInternalUser: Bool { - NSApp.delegateTyped.internalUserDecider.isInternalUser - } - func disableForAllUsers() { Task { await featureDisabler.disable(keepAuthToken: false, uninstallSystemExtension: false) diff --git a/UnitTests/Menus/MoreOptionsMenuTests.swift b/UnitTests/Menus/MoreOptionsMenuTests.swift index bee9716173..f64d87be9b 100644 --- a/UnitTests/Menus/MoreOptionsMenuTests.swift +++ b/UnitTests/Menus/MoreOptionsMenuTests.swift @@ -167,6 +167,10 @@ final class NetworkProtectionVisibilityMock: NetworkProtectionFeatureVisibility self.visible = visible } + func shouldUninstallAutomatically() -> Bool { + return !visible + } + func isNetworkProtectionVisible() -> Bool { return visible } From 68d3633e47b119ad7dd4a80963fdee4d0bf0b587 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Fri, 16 Feb 2024 19:29:02 +0000 Subject: [PATCH 15/44] Update embedded files --- .../AppPrivacyConfigurationDataProvider.swift | 4 +-- DuckDuckGo/ContentBlocker/macos-config.json | 33 ++++++++++++++----- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift b/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift index c7da20370a..e7f25afe8f 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 = "\"4df1da8b0cb81f8545df267fabe244c5\"" - public static let embeddedDataSHA = "22717483451eb903be6194c06300fb77f6c18d7eb060a68ea1d44f19ad07a5a6" + public static let embeddedDataETag = "\"40337866138aa6fdecbc748143547b73\"" + public static let embeddedDataSHA = "76ee226a289fafeccff2cf34de53fb143b280644403b9a87a826cf9b7fcc59cf" } var embeddedDataEtag: String { diff --git a/DuckDuckGo/ContentBlocker/macos-config.json b/DuckDuckGo/ContentBlocker/macos-config.json index 57a7a46e91..422cb7d83c 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": 1708001825742, + "version": 1708083739957, "features": { "adClickAttribution": { "readme": "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/#3rd-party-tracker-loading-protection", @@ -257,9 +257,6 @@ { "domain": "youtube.com" }, - { - "domain": "tuc.org.uk" - }, { "domain": "newsmax.com" }, @@ -279,11 +276,11 @@ "settings": { "disabledCMPs": [ "generic-cosmetic", - "EZoic" + "termsfeed3" ] }, "state": "enabled", - "hash": "df49b4912fc62299b3cc20b5538e3b89" + "hash": "ce906469aa6ac0fb5ecf3f39710ef05b" }, "autofill": { "exceptions": [ @@ -1076,6 +1073,10 @@ { "domain": "53.com", "reason": "https://github.com/duckduckgo/privacy-configuration/issues/1824" + }, + { + "domain": "www.evernote.com", + "reason": "https://github.com/duckduckgo/privacy-configuration/issues/1827" } ], "webViewDefault": [ @@ -1099,7 +1100,7 @@ }, "exceptions": [], "state": "enabled", - "hash": "d135871f701e3aa7e1052427ee721f31" + "hash": "653b8d161df5462850be095097e85143" }, "dbp": { "state": "enabled", @@ -5238,7 +5239,19 @@ "cloudflare.com": { "rules": [ { - "rule": "cdnjs.cloudflare.com/cdn-cgi/scripts/.*/cloudflare-static/rocket-loader.min.js", + "rule": "cloudflare.com/cdn-cgi/scripts/7089c43e/cloudflare-static/rocket-loader.min.js", + "domains": [ + "" + ] + }, + { + "rule": "cloudflare.com/cdn-cgi/scripts/7d0fa10a/cloudflare-static/rocket-loader.min.js", + "domains": [ + "" + ] + }, + { + "rule": "cloudflare.com/cdn-cgi/scripts/1680307200/cloudflare-static/rocket-loader.min.js", "domains": [ "" ] @@ -5968,6 +5981,7 @@ "doterra.com", "easyjet.com", "edx.org", + "saplinglearning.com", "worlddutyfree.com" ] }, @@ -6142,6 +6156,7 @@ "abril.com.br", "algomalegalclinic.com", "cosmicbook.news", + "eatroyo.com", "thesimsresource.com", "tradersync.com" ] @@ -7804,7 +7819,7 @@ "domain": "sundancecatalog.com" } ], - "hash": "faa9240f18945b1bb354bcb8b6f7483a" + "hash": "63057921f3dafa389fd7c132ea62393d" }, "trackingCookies1p": { "settings": { From ba18d0e096926a3dae6c0654928632388b58a448 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Fri, 16 Feb 2024 20:12:12 +0000 Subject: [PATCH 16/44] Bump version to 1.75.0 (120) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 9a2efd023d..459e731b44 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 119 +CURRENT_PROJECT_VERSION = 120 From ffff49ab19349fc424c26c63b42a26312b0de493 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Sun, 18 Feb 2024 14:17:58 +0600 Subject: [PATCH 17/44] Custom html error page implementation (#2136) Task/Issue URL: https://app.asana.com/0/72649045549333/1203487090719123/f Tech Design URL: https://app.asana.com/0/481882893211075/1203487090719150/f BSK PR: https://github.com/duckduckgo/BrowserServicesKit/pull/644 Content-Scope-Scripts PR: https://github.com/duckduckgo/content-scope-scripts/pull/906 --- Configuration/Tests/IntegrationTests.xcconfig | 2 + DuckDuckGo.xcodeproj/project.pbxproj | 100 +- .../Alert-Medium-Multicolor-16.pdf | Bin 0 -> 2374 bytes .../Contents.json | 12 + .../DefaultFavicon.imageset/Contents.json | 21 - .../DefaultFavicon.imageset/default-fav.png | Bin 180 -> 0 bytes .../Contents.json | 12 + .../Globe-Multicolor-16.pdf | Bin 0 -> 4115 bytes DuckDuckGo/Bridging.h | 1 + .../Extensions/NSObject+performSelector.h | 27 + .../Extensions/NSObject+performSelector.m | 52 + .../Common/Extensions/StringExtension.swift | 39 +- .../Common/Extensions/URLExtension.swift | 2 + .../WKBackForwardListItemExtension.swift | 35 + .../Extensions/WKWebViewExtension.swift | 17 + DuckDuckGo/Common/Localizables/UserText.swift | 8 +- DuckDuckGo/Common/View/AppKit/ColorView.swift | 1 + DuckDuckGo/DataExport/BookmarksExporter.swift | 8 +- .../ErrorPage/ErrorPageHTMLTemplate.swift | 45 + DuckDuckGo/Localizable.xcstrings | 52 +- .../MainWindow/MainViewController.swift | 2 +- .../AddressBarButtonsViewController.swift | 49 +- .../View/AddressBarViewController.swift | 10 +- .../View/NavigationButtonMenuDelegate.swift | 73 +- .../ViewModel/BackForwardListItem.swift | 40 +- ...ift => BackForwardListItemViewModel.swift} | 50 +- DuckDuckGo/Sharing/SharingMenu.swift | 1 + DuckDuckGo/Tab/Model/Tab+Navigation.swift | 7 +- DuckDuckGo/Tab/Model/Tab.swift | 185 +++- ...NonexistentDomainNavigationResponder.swift | 6 +- .../Tab/TabExtensions/TabExtensions.swift | 3 +- .../Tab/TabLazyLoader/LazyLoadable.swift | 8 +- .../Tab/View/Base.lproj/BrowserTab.storyboard | 121 --- .../Tab/View/BrowserTabViewController.swift | 102 +- DuckDuckGo/Tab/ViewModel/TabViewModel.swift | 68 +- .../TabPreview/TabPreviewViewController.swift | 2 +- .../YoutubePlayer/DuckURLSchemeHandler.swift | 23 +- .../Common/IntegrationTestsBridging.h | 21 + IntegrationTests/Tab/ErrorPageTests.swift | 930 ++++++++++++++++++ .../SearchNonexistentDomainTests.swift | 84 +- .../Extensions/StringExtensionTests.swift | 44 + UnitTests/Common/NSErrorAdditionalInfo.swift | 13 +- UnitTests/Common/TestsBridging.h | 1 + UnitTests/Common/WKURLSchemeTask+Private.h | 30 + .../Common/WKWebViewMockingExtension.swift | 8 +- .../DataImport/DataImportViewModelTests.swift | 3 + UnitTests/Tab/Model/TabTests.swift | 8 +- .../Tab/ViewModel/TabViewModelTests.swift | 17 +- ...bViewPrivateMethodsAvailabilityTests.swift | 4 + .../TabBar/ViewModel/TabLazyLoaderTests.swift | 4 +- 50 files changed, 1846 insertions(+), 505 deletions(-) create mode 100644 DuckDuckGo/Assets.xcassets/Images/Alert-Circle-Color-16.imageset/Alert-Medium-Multicolor-16.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/Alert-Circle-Color-16.imageset/Contents.json delete mode 100644 DuckDuckGo/Assets.xcassets/Images/DefaultFavicon.imageset/Contents.json delete mode 100644 DuckDuckGo/Assets.xcassets/Images/DefaultFavicon.imageset/default-fav.png create mode 100644 DuckDuckGo/Assets.xcassets/Images/Globe-Multicolor-16.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/Globe-Multicolor-16.imageset/Globe-Multicolor-16.pdf create mode 100644 DuckDuckGo/Common/Extensions/NSObject+performSelector.h create mode 100644 DuckDuckGo/Common/Extensions/NSObject+performSelector.m create mode 100644 DuckDuckGo/Common/Extensions/WKBackForwardListItemExtension.swift create mode 100644 DuckDuckGo/ErrorPage/ErrorPageHTMLTemplate.swift rename DuckDuckGo/NavigationBar/ViewModel/{WKBackForwardListItemViewModel.swift => BackForwardListItemViewModel.swift} (65%) delete mode 100644 DuckDuckGo/Tab/View/Base.lproj/BrowserTab.storyboard create mode 100644 IntegrationTests/Common/IntegrationTestsBridging.h create mode 100644 IntegrationTests/Tab/ErrorPageTests.swift rename IntegrationTests/{TabExtensions => Tab}/SearchNonexistentDomainTests.swift (77%) create mode 100644 UnitTests/Common/Extensions/StringExtensionTests.swift create mode 100644 UnitTests/Common/WKURLSchemeTask+Private.h diff --git a/Configuration/Tests/IntegrationTests.xcconfig b/Configuration/Tests/IntegrationTests.xcconfig index 6100fbe474..cee1523e37 100644 --- a/Configuration/Tests/IntegrationTests.xcconfig +++ b/Configuration/Tests/IntegrationTests.xcconfig @@ -22,4 +22,6 @@ FEATURE_FLAGS = FEEDBACK NETWORK_PROTECTION DBP INFOPLIST_FILE = IntegrationTests/Info.plist PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.Integration-Tests +SWIFT_OBJC_BRIDGING_HEADER = $(SRCROOT)/IntegrationTests/Common/IntegrationTestsBridging.h + TEST_HOST=$(BUILT_PRODUCTS_DIR)/DuckDuckGo.app/Contents/MacOS/DuckDuckGo diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 3c1c8e3cfe..bbbc6133bd 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -265,7 +265,7 @@ 3706FAE1293F65D500E42796 /* HomePageFavoritesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85589E8627BBB8F20038AD11 /* HomePageFavoritesModel.swift */; }; 3706FAE2293F65D500E42796 /* SequenceExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB88B4925B7B690006F6B06 /* SequenceExtensions.swift */; }; 3706FAE3293F65D500E42796 /* ChromiumDataImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B59023B26B35F3600489384 /* ChromiumDataImporter.swift */; }; - 3706FAE5293F65D500E42796 /* WKBackForwardListItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA0CC3B25337FAB0079BC96 /* WKBackForwardListItemViewModel.swift */; }; + 3706FAE5293F65D500E42796 /* BackForwardListItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA0CC3B25337FAB0079BC96 /* BackForwardListItemViewModel.swift */; }; 3706FAE6293F65D500E42796 /* BWNotRespondingAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D43EB3329297D760065E5D6 /* BWNotRespondingAlert.swift */; }; 3706FAE7293F65D500E42796 /* DebugUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB88B4425B7B55C006F6B06 /* DebugUserScript.swift */; }; 3706FAE8293F65D500E42796 /* RecentlyClosedTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC6881828626BF800D54247 /* RecentlyClosedTab.swift */; }; @@ -699,7 +699,6 @@ 3706FCEF293F65D500E42796 /* macos-config.json in Resources */ = {isa = PBXBuildFile; fileRef = 026ADE1326C3010C002518EE /* macos-config.json */; }; 3706FCF0293F65D500E42796 /* httpsMobileV2BloomSpec.json in Resources */ = {isa = PBXBuildFile; fileRef = 4B677427255DBEB800025BD8 /* httpsMobileV2BloomSpec.json */; }; 3706FCF1293F65D500E42796 /* TabBarFooter.xib in Resources */ = {isa = PBXBuildFile; fileRef = AA2CB12C2587BB5600AA6FBE /* TabBarFooter.xib */; }; - 3706FCF2293F65D500E42796 /* BrowserTab.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA80EC69256C4691007083E7 /* BrowserTab.storyboard */; }; 3706FCF3293F65D500E42796 /* FirePopoverCollectionViewItem.xib in Resources */ = {isa = PBXBuildFile; fileRef = AAE246F22709EF3B00BEEAEE /* FirePopoverCollectionViewItem.xib */; }; 3706FCF4293F65D500E42796 /* ProximaNova-Bold-webfont.woff2 in Resources */ = {isa = PBXBuildFile; fileRef = EAA29AE7278D2E43007070CF /* ProximaNova-Bold-webfont.woff2 */; }; 3706FCF5293F65D500E42796 /* dark-shield-dot.json in Resources */ = {isa = PBXBuildFile; fileRef = AA34396E2754D4E900B241FA /* dark-shield-dot.json */; }; @@ -1402,7 +1401,7 @@ 4B9579BB2AC7AE700062CA31 /* SequenceExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB88B4925B7B690006F6B06 /* SequenceExtensions.swift */; }; 4B9579BC2AC7AE700062CA31 /* WKBackForwardListExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B602E7CE2A93A5FF00F12201 /* WKBackForwardListExtension.swift */; }; 4B9579BD2AC7AE700062CA31 /* ChromiumDataImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B59023B26B35F3600489384 /* ChromiumDataImporter.swift */; }; - 4B9579BE2AC7AE700062CA31 /* WKBackForwardListItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA0CC3B25337FAB0079BC96 /* WKBackForwardListItemViewModel.swift */; }; + 4B9579BE2AC7AE700062CA31 /* BackForwardListItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA0CC3B25337FAB0079BC96 /* BackForwardListItemViewModel.swift */; }; 4B9579BF2AC7AE700062CA31 /* BWNotRespondingAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D43EB3329297D760065E5D6 /* BWNotRespondingAlert.swift */; }; 4B9579C02AC7AE700062CA31 /* DebugUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB88B4425B7B55C006F6B06 /* DebugUserScript.swift */; }; 4B9579C12AC7AE700062CA31 /* RecentlyClosedTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC6881828626BF800D54247 /* RecentlyClosedTab.swift */; }; @@ -1984,7 +1983,6 @@ 4B957C282AC7AE700062CA31 /* macos-config.json in Resources */ = {isa = PBXBuildFile; fileRef = 026ADE1326C3010C002518EE /* macos-config.json */; }; 4B957C292AC7AE700062CA31 /* httpsMobileV2BloomSpec.json in Resources */ = {isa = PBXBuildFile; fileRef = 4B677427255DBEB800025BD8 /* httpsMobileV2BloomSpec.json */; }; 4B957C2A2AC7AE700062CA31 /* TabBarFooter.xib in Resources */ = {isa = PBXBuildFile; fileRef = AA2CB12C2587BB5600AA6FBE /* TabBarFooter.xib */; }; - 4B957C2B2AC7AE700062CA31 /* BrowserTab.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA80EC69256C4691007083E7 /* BrowserTab.storyboard */; }; 4B957C2C2AC7AE700062CA31 /* FirePopoverCollectionViewItem.xib in Resources */ = {isa = PBXBuildFile; fileRef = AAE246F22709EF3B00BEEAEE /* FirePopoverCollectionViewItem.xib */; }; 4B957C2D2AC7AE700062CA31 /* ProximaNova-Bold-webfont.woff2 in Resources */ = {isa = PBXBuildFile; fileRef = EAA29AE7278D2E43007070CF /* ProximaNova-Bold-webfont.woff2 */; }; 4B957C2E2AC7AE700062CA31 /* dark-shield-dot.json in Resources */ = {isa = PBXBuildFile; fileRef = AA34396E2754D4E900B241FA /* dark-shield-dot.json */; }; @@ -2486,7 +2484,6 @@ AA7EB6EB27E880AE00036718 /* dark-shield-mouse-over.json in Resources */ = {isa = PBXBuildFile; fileRef = AA7EB6EA27E880AE00036718 /* dark-shield-mouse-over.json */; }; AA7EB6ED27E880B600036718 /* dark-shield-dot-mouse-over.json in Resources */ = {isa = PBXBuildFile; fileRef = AA7EB6EC27E880B600036718 /* dark-shield-dot-mouse-over.json */; }; AA80EC54256BE3BC007083E7 /* UserText.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA80EC53256BE3BC007083E7 /* UserText.swift */; }; - AA80EC67256C4691007083E7 /* BrowserTab.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA80EC69256C4691007083E7 /* BrowserTab.storyboard */; }; AA80EC73256C46A2007083E7 /* Suggestion.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA80EC75256C46A2007083E7 /* Suggestion.storyboard */; }; AA80EC79256C46AA007083E7 /* TabBar.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA80EC7B256C46AA007083E7 /* TabBar.storyboard */; }; AA840A9827319D1600E63CDD /* FirePopoverWrapperViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA840A9727319D1600E63CDD /* FirePopoverWrapperViewController.swift */; }; @@ -2505,7 +2502,7 @@ AA9FF95D24A1FA1C0039E328 /* TabCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA9FF95C24A1FA1C0039E328 /* TabCollection.swift */; }; AA9FF95F24A1FB690039E328 /* TabCollectionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA9FF95E24A1FB680039E328 /* TabCollectionViewModel.swift */; }; AAA0CC33252F181A0079BC96 /* NavigationButtonMenuDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA0CC32252F181A0079BC96 /* NavigationButtonMenuDelegate.swift */; }; - AAA0CC3C25337FAB0079BC96 /* WKBackForwardListItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA0CC3B25337FAB0079BC96 /* WKBackForwardListItemViewModel.swift */; }; + AAA0CC3C25337FAB0079BC96 /* BackForwardListItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA0CC3B25337FAB0079BC96 /* BackForwardListItemViewModel.swift */; }; AAA0CC472533833C0079BC96 /* MoreOptionsMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA0CC462533833C0079BC96 /* MoreOptionsMenu.swift */; }; AAA0CC572539EBC90079BC96 /* FaviconUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA0CC562539EBC90079BC96 /* FaviconUserScript.swift */; }; AAA0CC6A253CC43C0079BC96 /* WKUserContentControllerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA0CC69253CC43C0079BC96 /* WKUserContentControllerExtension.swift */; }; @@ -2788,6 +2785,17 @@ B68172AE269EB43F006D1092 /* GeolocationServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68172AD269EB43F006D1092 /* GeolocationServiceTests.swift */; }; B6830961274CDE99004B46BB /* FireproofDomainsContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6830960274CDE99004B46BB /* FireproofDomainsContainer.swift */; }; B6830963274CDEC7004B46BB /* FireproofDomainsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6830962274CDEC7004B46BB /* FireproofDomainsStore.swift */; }; + B68412142B694BA10092F66A /* NSObject+performSelector.m in Sources */ = {isa = PBXBuildFile; fileRef = B68412132B694BA10092F66A /* NSObject+performSelector.m */; }; + B68412152B694BA10092F66A /* NSObject+performSelector.m in Sources */ = {isa = PBXBuildFile; fileRef = B68412132B694BA10092F66A /* NSObject+performSelector.m */; }; + B68412162B694BA10092F66A /* NSObject+performSelector.m in Sources */ = {isa = PBXBuildFile; fileRef = B68412132B694BA10092F66A /* NSObject+performSelector.m */; }; + B684121C2B6A1D880092F66A /* ErrorPageHTMLTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B684121B2B6A1D880092F66A /* ErrorPageHTMLTemplate.swift */; }; + B684121D2B6A1D880092F66A /* ErrorPageHTMLTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B684121B2B6A1D880092F66A /* ErrorPageHTMLTemplate.swift */; }; + B684121E2B6A1D880092F66A /* ErrorPageHTMLTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B684121B2B6A1D880092F66A /* ErrorPageHTMLTemplate.swift */; }; + B68412202B6A30680092F66A /* StringExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B684121F2B6A30680092F66A /* StringExtensionTests.swift */; }; + B68412212B6A30680092F66A /* StringExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B684121F2B6A30680092F66A /* StringExtensionTests.swift */; }; + B68412272B6A68C10092F66A /* WKBackForwardListItemExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68412242B6A67920092F66A /* WKBackForwardListItemExtension.swift */; }; + B68412282B6A68C20092F66A /* WKBackForwardListItemExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68412242B6A67920092F66A /* WKBackForwardListItemExtension.swift */; }; + B68412292B6A68C90092F66A /* WKBackForwardListItemExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68412242B6A67920092F66A /* WKBackForwardListItemExtension.swift */; }; B68458B025C7E76A00DC17B6 /* WindowManager+StateRestoration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68458AF25C7E76A00DC17B6 /* WindowManager+StateRestoration.swift */; }; B68458B825C7E8B200DC17B6 /* Tab+NSSecureCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68458B725C7E8B200DC17B6 /* Tab+NSSecureCoding.swift */; }; B68458C025C7E9E000DC17B6 /* TabCollectionViewModel+NSSecureCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68458BF25C7E9E000DC17B6 /* TabCollectionViewModel+NSSecureCoding.swift */; }; @@ -2818,6 +2826,8 @@ B690152C2ACBF4DA00AD0BAB /* MenuPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = B690152B2ACBF4DA00AD0BAB /* MenuPreview.swift */; }; B690152D2ACBF4DA00AD0BAB /* MenuPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = B690152B2ACBF4DA00AD0BAB /* MenuPreview.swift */; }; B690152F2ACBF4DA00AD0BAB /* MenuPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = B690152B2ACBF4DA00AD0BAB /* MenuPreview.swift */; }; + B693766E2B6B5F27005BD9D4 /* ErrorPageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B693766D2B6B5F26005BD9D4 /* ErrorPageTests.swift */; }; + B693766F2B6B5F27005BD9D4 /* ErrorPageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B693766D2B6B5F26005BD9D4 /* ErrorPageTests.swift */; }; B693954B26F04BEB0015B914 /* MouseOverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B693953D26F04BE70015B914 /* MouseOverView.swift */; }; B693954C26F04BEB0015B914 /* FocusRingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B693953E26F04BE70015B914 /* FocusRingView.swift */; }; B693954E26F04BEB0015B914 /* LoadingProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B693954026F04BE80015B914 /* LoadingProgressView.swift */; }; @@ -4016,7 +4026,6 @@ AA7EB6EA27E880AE00036718 /* dark-shield-mouse-over.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "dark-shield-mouse-over.json"; sourceTree = ""; }; AA7EB6EC27E880B600036718 /* dark-shield-dot-mouse-over.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "dark-shield-dot-mouse-over.json"; sourceTree = ""; }; AA80EC53256BE3BC007083E7 /* UserText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserText.swift; sourceTree = ""; }; - AA80EC68256C4691007083E7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/BrowserTab.storyboard; sourceTree = ""; }; AA80EC74256C46A2007083E7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Suggestion.storyboard; sourceTree = ""; }; AA80EC7A256C46AA007083E7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/TabBar.storyboard; sourceTree = ""; }; AA840A9727319D1600E63CDD /* FirePopoverWrapperViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirePopoverWrapperViewController.swift; sourceTree = ""; }; @@ -4035,7 +4044,7 @@ AA9FF95C24A1FA1C0039E328 /* TabCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabCollection.swift; sourceTree = ""; }; AA9FF95E24A1FB680039E328 /* TabCollectionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabCollectionViewModel.swift; sourceTree = ""; }; AAA0CC32252F181A0079BC96 /* NavigationButtonMenuDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationButtonMenuDelegate.swift; sourceTree = ""; }; - AAA0CC3B25337FAB0079BC96 /* WKBackForwardListItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKBackForwardListItemViewModel.swift; sourceTree = ""; }; + AAA0CC3B25337FAB0079BC96 /* BackForwardListItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackForwardListItemViewModel.swift; sourceTree = ""; }; AAA0CC462533833C0079BC96 /* MoreOptionsMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoreOptionsMenu.swift; sourceTree = ""; }; AAA0CC562539EBC90079BC96 /* FaviconUserScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconUserScript.swift; sourceTree = ""; }; AAA0CC69253CC43C0079BC96 /* WKUserContentControllerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKUserContentControllerExtension.swift; sourceTree = ""; }; @@ -4225,6 +4234,11 @@ B68172AD269EB43F006D1092 /* GeolocationServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeolocationServiceTests.swift; sourceTree = ""; }; B6830960274CDE99004B46BB /* FireproofDomainsContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FireproofDomainsContainer.swift; sourceTree = ""; }; B6830962274CDEC7004B46BB /* FireproofDomainsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FireproofDomainsStore.swift; sourceTree = ""; }; + B68412122B694BA10092F66A /* NSObject+performSelector.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSObject+performSelector.h"; sourceTree = ""; }; + B68412132B694BA10092F66A /* NSObject+performSelector.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSObject+performSelector.m"; sourceTree = ""; }; + B684121B2B6A1D880092F66A /* ErrorPageHTMLTemplate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorPageHTMLTemplate.swift; sourceTree = ""; }; + B684121F2B6A30680092F66A /* StringExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtensionTests.swift; sourceTree = ""; }; + B68412242B6A67920092F66A /* WKBackForwardListItemExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKBackForwardListItemExtension.swift; sourceTree = ""; }; B68458AF25C7E76A00DC17B6 /* WindowManager+StateRestoration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WindowManager+StateRestoration.swift"; sourceTree = ""; }; B68458B725C7E8B200DC17B6 /* Tab+NSSecureCoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Tab+NSSecureCoding.swift"; sourceTree = ""; }; B68458BF25C7E9E000DC17B6 /* TabCollectionViewModel+NSSecureCoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TabCollectionViewModel+NSSecureCoding.swift"; sourceTree = ""; }; @@ -4243,6 +4257,7 @@ B68C92C0274E3EF4002AC6B0 /* PopUpWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopUpWindow.swift; sourceTree = ""; }; B68C92C32750EF76002AC6B0 /* PixelDataRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PixelDataRecord.swift; sourceTree = ""; }; B690152B2ACBF4DA00AD0BAB /* MenuPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuPreview.swift; sourceTree = ""; }; + B693766D2B6B5F26005BD9D4 /* ErrorPageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorPageTests.swift; sourceTree = ""; }; B693953D26F04BE70015B914 /* MouseOverView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MouseOverView.swift; sourceTree = ""; }; B693953E26F04BE70015B914 /* FocusRingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FocusRingView.swift; sourceTree = ""; }; B693954026F04BE80015B914 /* LoadingProgressView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadingProgressView.swift; sourceTree = ""; }; @@ -4289,6 +4304,8 @@ B6A5A27D25B9403E00AA7ADA /* FileStoreMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileStoreMock.swift; sourceTree = ""; }; B6A5A29F25B96E8300AA7ADA /* AppStateChangePublisherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateChangePublisherTests.swift; sourceTree = ""; }; B6A5A2A725BAA35500AA7ADA /* WindowManagerStateRestorationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowManagerStateRestorationTests.swift; sourceTree = ""; }; + B6A60E4F2B73C3B800FD4968 /* WKURLSchemeTask+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "WKURLSchemeTask+Private.h"; sourceTree = ""; }; + B6A60E502B73C46B00FD4968 /* IntegrationTestsBridging.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IntegrationTestsBridging.h; sourceTree = ""; }; B6A924D82664C72D001A28CA /* WebKitDownloadTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebKitDownloadTask.swift; sourceTree = ""; }; B6A9E45226142B070067D1B9 /* Pixel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Pixel.swift; sourceTree = ""; }; B6A9E46A2614618A0067D1B9 /* OperatingSystemVersionExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperatingSystemVersionExtension.swift; sourceTree = ""; }; @@ -5243,7 +5260,7 @@ B603973229BEF84900902A34 /* HTTPSUpgrade */, B62A233A29C322A000D22475 /* NavigationProtection */, B603973629BF0E9400902A34 /* PrivacyDashboard */, - B644B43C29D56811003FA9AB /* TabExtensions */, + B644B43C29D56811003FA9AB /* Tab */, 4B1AD91625FC46FB00261379 /* CoreDataEncryptionTests.swift */, 4BA1A6EA258C288C00F6F690 /* EncryptionKeyStoreTests.swift */, 4B1AD8A125FC27E200261379 /* Info.plist */, @@ -6481,6 +6498,7 @@ B6DA06E02913AEDB00225DE2 /* TestNavigationDelegate.swift */, B60C6F8029B1B4AD007BFAA8 /* TestRunHelper.swift */, B60C6F7D29B1B41D007BFAA8 /* TestRunHelperInitializer.m */, + B6A60E4F2B73C3B800FD4968 /* WKURLSchemeTask+Private.h */, ); path = Common; sourceTree = ""; @@ -6670,21 +6688,21 @@ isa = PBXGroup; children = ( B658BAB52B0F845D00D1F2C7 /* Localizable.xcstrings */, - 3192EC862A4DCF0E001E97A5 /* DBP */, - EEAEA3F4294D05CF00D04DF3 /* JSAlert */, + AA4D700525545EDE00C3411E /* Application */, B31055BB27A1BA0E001AC618 /* Autoconsent */, 7B1E819A27C8874900FF0E60 /* Autofill */, - AA4D700525545EDE00C3411E /* Application */, AAC5E4C025D6A6A9007F5990 /* Bookmarks */, 4BFD356E283ADE8B00CE9234 /* BookmarksBar */, AA86491324D831B9001BABEE /* Common */, 85D33F1025C82E93002B91A6 /* Configuration */, 4B6160D125B14E5E007DE5B2 /* ContentBlocker */, AAC30A24268DF93500D2D9CD /* CrashReports */, - 4B723DEA26B0002B00E14D75 /* DataImport */, 4B723DF826B0002B00E14D75 /* DataExport */, + 4B723DEA26B0002B00E14D75 /* DataImport */, + 3192EC862A4DCF0E001E97A5 /* DBP */, 4B379C1C27BDB7EA008A968E /* DeviceAuthentication */, 4B65143C26392483005B46EB /* Email */, + B68412192B6A16030092F66A /* ErrorPage */, AA5FA695275F823900DCE9C9 /* Favicons */, 1D36E651298A84F600AA485D /* FeatureFlagging */, AA3863C227A1E1C000749AB5 /* Feedback */, @@ -6694,13 +6712,13 @@ 4B02197B25E05FAC00ED7DEA /* Fireproofing */, B65536902684409300085A79 /* Geolocation */, AAE75275263B036300B973F8 /* History */, + AAE71DB225F66A0900D74437 /* HomePage */, + EEAEA3F4294D05CF00D04DF3 /* JSAlert */, 9D03F5A22AA74829001A50E8 /* LoginItems */, AA585DB02490E6FA00E9A3E2 /* MainWindow */, - AAE71DB225F66A0900D74437 /* HomePage */, AA97BF4425135CB60014931A /* Menus */, 85378D9A274E618C007C5CBF /* MessageViews */, AA86491524D83384001BABEE /* NavigationBar */, - 4B41EDAC2B168A66001EEDF4 /* VPNFeedbackForm */, 4B4D60542A0B29FA00BCD287 /* NetworkProtection */, 85B7184727677A7D00B4277F /* Onboarding */, 1D074B252909A371006E4AC3 /* PasswordManager */, @@ -6721,6 +6739,7 @@ AAE8B0FD258A416F00E81239 /* TabPreview */, B6040859274B8C5200680351 /* UnprotectedDomains */, AACF6FD426BC35C200CF09F9 /* UserAgent */, + 4B41EDAC2B168A66001EEDF4 /* VPNFeedbackForm */, 4B9DB0062A983B23000927DB /* Waitlist */, AA6EF9AE25066F99004754E6 /* Windows */, 31F28C4B28C8EE9000119F70 /* YoutubePlayer */, @@ -7109,7 +7128,6 @@ AA86491C24D83868001BABEE /* View */ = { isa = PBXGroup; children = ( - AA80EC69256C4691007083E7 /* BrowserTab.storyboard */, B6C0BB6929AF1C7000AE8E3C /* BrowserTabView.swift */, AA585D83248FD31100E9A3E2 /* BrowserTabViewController.swift */, AA6FFB4524DC3B5A0028F4D0 /* WebView.swift */, @@ -7255,7 +7273,7 @@ AAA0CC3A25337F990079BC96 /* ViewModel */ = { isa = PBXGroup; children = ( - AAA0CC3B25337FAB0079BC96 /* WKBackForwardListItemViewModel.swift */, + AAA0CC3B25337FAB0079BC96 /* BackForwardListItemViewModel.swift */, B689ECD426C247DB006FB0C5 /* BackForwardListItem.swift */, AA75A0AD26F3500C0086B667 /* PrivacyIconViewModel.swift */, ); @@ -7524,6 +7542,8 @@ AA6EF9B2250785D5004754E6 /* NSMenuExtension.swift */, AA72D5FD25FFF94E00C77619 /* NSMenuItemExtension.swift */, 4B980E202817604000282EE1 /* NSNotificationName+Debug.swift */, + B68412122B694BA10092F66A /* NSObject+performSelector.h */, + B68412132B694BA10092F66A /* NSObject+performSelector.m */, 4B0511DF262CAA8600F6079C /* NSOpenPanelExtensions.swift */, 4B0135CD2729F1AA00D54834 /* NSPasteboardExtension.swift */, 4B85A47F28821CC500FC4C39 /* NSPasteboardItemExtension.swift */, @@ -7556,6 +7576,7 @@ AA88D14A252A557100980B4E /* URLRequestExtension.swift */, B6DB3AEE278D5C370024C5C4 /* URLSessionExtension.swift */, B602E7CE2A93A5FF00F12201 /* WKBackForwardListExtension.swift */, + B68412242B6A67920092F66A /* WKBackForwardListItemExtension.swift */, B6DA06E7291401D700225DE2 /* WKMenuItemIdentifier.swift */, B645D8F529FA95440024461F /* WKProcessPoolExtension.swift */, AAA0CC69253CC43C0079BC96 /* WKUserContentControllerExtension.swift */, @@ -7683,6 +7704,7 @@ 85F69B3B25EDE81F00978E59 /* URLExtensionTests.swift */, 4B8AD0B027A86D9200AE44D6 /* WKWebsiteDataStoreExtensionTests.swift */, B6AA64722994B43300D99CD6 /* FutureExtensionTests.swift */, + B684121F2B6A30680092F66A /* StringExtensionTests.swift */, ); path = Extensions; sourceTree = ""; @@ -7727,6 +7749,7 @@ B603972A29BEDF0F00902A34 /* Common */ = { isa = PBXGroup; children = ( + B6A60E502B73C46B00FD4968 /* IntegrationTestsBridging.h */, B6EC37FB29B83E99001ACE79 /* TestsURLExtension.swift */, ); path = Common; @@ -7799,12 +7822,13 @@ path = History; sourceTree = ""; }; - B644B43C29D56811003FA9AB /* TabExtensions */ = { + B644B43C29D56811003FA9AB /* Tab */ = { isa = PBXGroup; children = ( B644B43929D565DB003FA9AB /* SearchNonexistentDomainTests.swift */, + B693766D2B6B5F26005BD9D4 /* ErrorPageTests.swift */, ); - path = TabExtensions; + path = Tab; sourceTree = ""; }; B647EFB32922539400BA628D /* TabExtensions */ = { @@ -7900,6 +7924,14 @@ path = Database; sourceTree = ""; }; + B68412192B6A16030092F66A /* ErrorPage */ = { + isa = PBXGroup; + children = ( + B684121B2B6A1D880092F66A /* ErrorPageHTMLTemplate.swift */, + ); + path = ErrorPage; + sourceTree = ""; + }; B68458AE25C7E75100DC17B6 /* StateRestoration */ = { isa = PBXGroup; children = ( @@ -8875,7 +8907,6 @@ 3706FCEF293F65D500E42796 /* macos-config.json in Resources */, 3706FCF0293F65D500E42796 /* httpsMobileV2BloomSpec.json in Resources */, 3706FCF1293F65D500E42796 /* TabBarFooter.xib in Resources */, - 3706FCF2293F65D500E42796 /* BrowserTab.storyboard in Resources */, 3706FCF3293F65D500E42796 /* FirePopoverCollectionViewItem.xib in Resources */, 3706FCF4293F65D500E42796 /* ProximaNova-Bold-webfont.woff2 in Resources */, 3706FCF5293F65D500E42796 /* dark-shield-dot.json in Resources */, @@ -9003,7 +9034,6 @@ 4B957C282AC7AE700062CA31 /* macos-config.json in Resources */, 4B957C292AC7AE700062CA31 /* httpsMobileV2BloomSpec.json in Resources */, 4B957C2A2AC7AE700062CA31 /* TabBarFooter.xib in Resources */, - 4B957C2B2AC7AE700062CA31 /* BrowserTab.storyboard in Resources */, 4B957C2C2AC7AE700062CA31 /* FirePopoverCollectionViewItem.xib in Resources */, 4B957C2D2AC7AE700062CA31 /* ProximaNova-Bold-webfont.woff2 in Resources */, 4B957C2E2AC7AE700062CA31 /* dark-shield-dot.json in Resources */, @@ -9100,7 +9130,6 @@ 026ADE1426C3010C002518EE /* macos-config.json in Resources */, 4B677432255DBEB800025BD8 /* httpsMobileV2BloomSpec.json in Resources */, AA2CB12D2587BB5600AA6FBE /* TabBarFooter.xib in Resources */, - AA80EC67256C4691007083E7 /* BrowserTab.storyboard in Resources */, AAE246F42709EF3B00BEEAEE /* FirePopoverCollectionViewItem.xib in Resources */, EAA29AE9278D2E43007070CF /* ProximaNova-Bold-webfont.woff2 in Resources */, AA3439702754D4E900B241FA /* dark-shield-dot.json in Resources */, @@ -9568,7 +9597,7 @@ 3706FAE1293F65D500E42796 /* HomePageFavoritesModel.swift in Sources */, 3706FAE2293F65D500E42796 /* SequenceExtensions.swift in Sources */, 3706FAE3293F65D500E42796 /* ChromiumDataImporter.swift in Sources */, - 3706FAE5293F65D500E42796 /* WKBackForwardListItemViewModel.swift in Sources */, + 3706FAE5293F65D500E42796 /* BackForwardListItemViewModel.swift in Sources */, 3706FAE6293F65D500E42796 /* BWNotRespondingAlert.swift in Sources */, 3706FAE7293F65D500E42796 /* DebugUserScript.swift in Sources */, 3706FAE8293F65D500E42796 /* RecentlyClosedTab.swift in Sources */, @@ -9884,6 +9913,8 @@ 3706FBD0293F65D500E42796 /* WebKitVersionProvider.swift in Sources */, 3706FBD1293F65D500E42796 /* NSCoderExtensions.swift in Sources */, B6D6A5DD2982A4CE001F5F11 /* Tab+Navigation.swift in Sources */, + B68412282B6A68C20092F66A /* WKBackForwardListItemExtension.swift in Sources */, + B684121D2B6A1D880092F66A /* ErrorPageHTMLTemplate.swift in Sources */, 3706FBD2293F65D500E42796 /* RunningApplicationCheck.swift in Sources */, 3706FBD3293F65D500E42796 /* StatePersistenceService.swift in Sources */, 3706FBD4293F65D500E42796 /* WindowManager+StateRestoration.swift in Sources */, @@ -10037,6 +10068,7 @@ 3706FC3C293F65D500E42796 /* HistoryCoordinator.swift in Sources */, 3706FC3E293F65D500E42796 /* VariantManager.swift in Sources */, 3706FC3F293F65D500E42796 /* ApplicationDockMenu.swift in Sources */, + B68412152B694BA10092F66A /* NSObject+performSelector.m in Sources */, 4B9DB03C2A983B24000927DB /* InvitedToWaitlistView.swift in Sources */, 3706FC40293F65D500E42796 /* SaveIdentityViewController.swift in Sources */, 3706FC41293F65D500E42796 /* FileStore.swift in Sources */, @@ -10242,6 +10274,7 @@ 3706FE13293F661700E42796 /* ConfigurationStorageTests.swift in Sources */, 3706FE14293F661700E42796 /* DownloadListStoreMock.swift in Sources */, 3706FE15293F661700E42796 /* PrivacyIconViewModelTests.swift in Sources */, + B68412212B6A30680092F66A /* StringExtensionTests.swift in Sources */, 1D8C2FEE2B70F5D0005E4BBD /* MockViewSnapshotRenderer.swift in Sources */, 56D145F229E6F06D00E3488A /* MockBookmarkManager.swift in Sources */, 3706FE16293F661700E42796 /* CSVImporterTests.swift in Sources */, @@ -10434,6 +10467,7 @@ B603972D29BEDF2100902A34 /* ExpectedNavigationExtension.swift in Sources */, 3706FEA6293F662100E42796 /* EncryptionKeyStoreTests.swift in Sources */, B6F5656A299A414300A04298 /* WKWebViewMockingExtension.swift in Sources */, + B693766F2B6B5F27005BD9D4 /* ErrorPageTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10474,6 +10508,7 @@ B603972C29BEDF2100902A34 /* ExpectedNavigationExtension.swift in Sources */, 4B1AD8D525FC38DD00261379 /* EncryptionKeyStoreTests.swift in Sources */, B6F56568299A414300A04298 /* WKWebViewMockingExtension.swift in Sources */, + B693766E2B6B5F27005BD9D4 /* ErrorPageTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10735,7 +10770,7 @@ 4B9579BB2AC7AE700062CA31 /* SequenceExtensions.swift in Sources */, 4B9579BC2AC7AE700062CA31 /* WKBackForwardListExtension.swift in Sources */, 4B9579BD2AC7AE700062CA31 /* ChromiumDataImporter.swift in Sources */, - 4B9579BE2AC7AE700062CA31 /* WKBackForwardListItemViewModel.swift in Sources */, + 4B9579BE2AC7AE700062CA31 /* BackForwardListItemViewModel.swift in Sources */, 4B9579BF2AC7AE700062CA31 /* BWNotRespondingAlert.swift in Sources */, 4B9579C02AC7AE700062CA31 /* DebugUserScript.swift in Sources */, 1DC669722B6CF0D700AA0645 /* TabSnapshotStore.swift in Sources */, @@ -10959,6 +10994,7 @@ 4B957A842AC7AE700062CA31 /* PreferencesDownloadsView.swift in Sources */, 4B957A852AC7AE700062CA31 /* QRSharingService.swift in Sources */, 4B957A862AC7AE700062CA31 /* ProcessExtension.swift in Sources */, + B68412162B694BA10092F66A /* NSObject+performSelector.m in Sources */, 4B957A872AC7AE700062CA31 /* PermissionAuthorizationQuery.swift in Sources */, 4B957A882AC7AE700062CA31 /* BadgeAnimationView.swift in Sources */, 4B957A892AC7AE700062CA31 /* BrowserTabSelectionDelegate.swift in Sources */, @@ -11212,6 +11248,7 @@ 4B957B6C2AC7AE700062CA31 /* MouseOverAnimationButton.swift in Sources */, 4B957B6D2AC7AE700062CA31 /* TabBarScrollView.swift in Sources */, B6B5F5822B024105008DB58A /* DataImportSummaryView.swift in Sources */, + B684121E2B6A1D880092F66A /* ErrorPageHTMLTemplate.swift in Sources */, 4B957B6E2AC7AE700062CA31 /* BookmarkListTreeControllerDataSource.swift in Sources */, 4B957B6F2AC7AE700062CA31 /* AddressBarViewController.swift in Sources */, 4B957B702AC7AE700062CA31 /* Permissions.swift in Sources */, @@ -11226,6 +11263,7 @@ 4B957B782AC7AE700062CA31 /* ContentOverlayViewController.swift in Sources */, 4B957B792AC7AE700062CA31 /* ContentBlockingTabExtension.swift in Sources */, 4B957B7A2AC7AE700062CA31 /* OnboardingViewController.swift in Sources */, + B68412292B6A68C90092F66A /* WKBackForwardListItemExtension.swift in Sources */, 4B957B7B2AC7AE700062CA31 /* DeviceAuthenticator.swift in Sources */, 4B957B7C2AC7AE700062CA31 /* NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift in Sources */, 4B957B7D2AC7AE700062CA31 /* TabBarCollectionView.swift in Sources */, @@ -11436,6 +11474,7 @@ 4BF0E5122AD25A2600FFEC9E /* DuckDuckGoUserAgent.swift in Sources */, AA6EF9AD25066F42004754E6 /* WindowsManager.swift in Sources */, 1D43EB3A292B63B00065E5D6 /* BWRequest.swift in Sources */, + B684121C2B6A1D880092F66A /* ErrorPageHTMLTemplate.swift in Sources */, B68458CD25C7EB9000DC17B6 /* WKWebViewConfigurationExtensions.swift in Sources */, 85AC7ADD27BEB6EE00FFB69B /* HomePageDefaultBrowserModel.swift in Sources */, B6619EFB2B111CC500CD9186 /* InstructionsFormatParser.swift in Sources */, @@ -11506,7 +11545,7 @@ B602E7CF2A93A5FF00F12201 /* WKBackForwardListExtension.swift in Sources */, 4B59024026B35F3600489384 /* ChromiumDataImporter.swift in Sources */, B62B48562ADE730D000DECE5 /* FileImportView.swift in Sources */, - AAA0CC3C25337FAB0079BC96 /* WKBackForwardListItemViewModel.swift in Sources */, + AAA0CC3C25337FAB0079BC96 /* BackForwardListItemViewModel.swift in Sources */, 1D43EB3429297D760065E5D6 /* BWNotRespondingAlert.swift in Sources */, 4BB88B4525B7B55C006F6B06 /* DebugUserScript.swift in Sources */, AAC6881928626BF800D54247 /* RecentlyClosedTab.swift in Sources */, @@ -11585,6 +11624,7 @@ 1D36E658298AA3BA00AA485D /* InternalUserDeciderStore.swift in Sources */, B634DBE5293C944700C3C99E /* NewWindowPolicy.swift in Sources */, 31CF3432288B0B1B0087244B /* NavigationBarBadgeAnimator.swift in Sources */, + B68412272B6A68C10092F66A /* WKBackForwardListItemExtension.swift in Sources */, 858A798526A8BB5D00A75A42 /* NSTextViewExtension.swift in Sources */, B634DBE7293C98C500C3C99E /* FutureExtension.swift in Sources */, B634DBE3293C900000C3C99E /* UserDialogRequest.swift in Sources */, @@ -11665,6 +11705,7 @@ 4BA1A6BD258B082300F6F690 /* EncryptionKeyStore.swift in Sources */, B6C00ED7292FB4B4009C73A6 /* TabExtensionsBuilder.swift in Sources */, 4BE65474271FCD40008D1D63 /* PasswordManagementIdentityItemView.swift in Sources */, + B68412142B694BA10092F66A /* NSObject+performSelector.m in Sources */, B6F41031264D2B23003DA42C /* ProgressExtension.swift in Sources */, 4B723E0F26B0006500E14D75 /* CSVParser.swift in Sources */, B6DA44082616B30600DD1EC2 /* PixelDataModel.xcdatamodeld in Sources */, @@ -12184,6 +12225,7 @@ 4B3F641E27A8D3BD00E0C118 /* BrowserProfileTests.swift in Sources */, B6106BA026A7BE0B0013B453 /* PermissionManagerTests.swift in Sources */, 4B59CC8C290083240058F2F6 /* ConnectBitwardenViewModelTests.swift in Sources */, + B68412202B6A30680092F66A /* StringExtensionTests.swift in Sources */, B662D3D92755D7AD0035D4D6 /* PixelStoreTests.swift in Sources */, B6106BB526A809E60013B453 /* GeolocationProviderTests.swift in Sources */, B6A5A2A025B96E8300AA7ADA /* AppStateChangePublisherTests.swift in Sources */, @@ -12494,14 +12536,6 @@ /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ - AA80EC69256C4691007083E7 /* BrowserTab.storyboard */ = { - isa = PBXVariantGroup; - children = ( - AA80EC68256C4691007083E7 /* Base */, - ); - name = BrowserTab.storyboard; - sourceTree = ""; - }; AA80EC75256C46A2007083E7 /* Suggestion.storyboard */ = { isa = PBXVariantGroup; children = ( diff --git a/DuckDuckGo/Assets.xcassets/Images/Alert-Circle-Color-16.imageset/Alert-Medium-Multicolor-16.pdf b/DuckDuckGo/Assets.xcassets/Images/Alert-Circle-Color-16.imageset/Alert-Medium-Multicolor-16.pdf new file mode 100644 index 0000000000000000000000000000000000000000..9d34524e44837add44d2b47fabc12e13d5ba3454 GIT binary patch literal 2374 zcmbVO+iuh_5PkPo_@z>dw2m)voJdtAx}^vKqHKAqcnI0FU9_9PCPl#4GvnCfw2QmNc=o34?#6p=09@g&VcfU% zj%63^w@uToUY_&i_54>eu&+!B=|3)`u^?-~5uafQ`>rXi>T&w~ffb#9k6R2YUFACvGxG4tG z@TV;~Kp}X{BZ5$JyhslAMKwC(Gr^v^v` zp2+Y$?rb^rxRtsx@OlYZsMzCWd=t_o+p%0IlS>KZq3492D!8;|VXF#j9G(->@GUky zWF$ex;dVlRcc_|tCkYB??vlWnNRp#fq9kFgFx86Iu`ferN{d{T8ZNgHwp0%8uPWjq z5fTK65F?>8>o=<;17y-p?ubD_5rGhVy+e3}k#V?00=~nZ`DYYIUYKWb_~D$JhN5!D z&`it;hFO_C@49{*_{~R{o$zHJetc>CaiqNq&}tUwQ}HrIHSu4{*-OzG(5%?(IA%b;Ur_ufdD4s3)Z6bEAOAX zY`4KW?w`Mw(X{AUO36QE8Ce7z7!U;WlAgccO*T)RrM-IXV?hpw@9V<)Z9dB^<31B1 V?3=N}{|?X@44$rjF6*2UngE(4L1+K~ diff --git a/DuckDuckGo/Assets.xcassets/Images/Globe-Multicolor-16.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Globe-Multicolor-16.imageset/Contents.json new file mode 100644 index 0000000000..771bdd6276 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Globe-Multicolor-16.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Globe-Multicolor-16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Globe-Multicolor-16.imageset/Globe-Multicolor-16.pdf b/DuckDuckGo/Assets.xcassets/Images/Globe-Multicolor-16.imageset/Globe-Multicolor-16.pdf new file mode 100644 index 0000000000000000000000000000000000000000..35a5fd2eea1a33da37d59f23c97ac76503ee6df3 GIT binary patch literal 4115 zcmeHKOOG5i5Wf3Y_!22WipTGV6h$Jt1R+2y6K)ZQp|`Ucv@-)U6BP36`O57s_s&L0 zaD_RnykEP@RbRdACpXuxpBt5gFjC9IFTV;YU%ZenUy5;mOaBVH#8=;q`@8dd=>V>2 z*Xev5H#@Pq9{<=5uoGH^s8jZkj7f)_ zu_!6}WaMesi=I%?dXK~{1QN|eWI_JIcS>M(mQViA(aQvKq&`C~f8#|>U1O*h zfKrRrU~88E$1YWFd262INByLQ(pcnCwy7XxLpI48Y2y5m1v-T0&?NF=WB)#XEsL+L z2Ewen9cSe%K`pHr#R7~_TWRJo#tT-(2rHoc<$rpY+EEQUuXS29Kn?&3DaTNXlMJyA zxG-DEDtLjEu0P4!8G#&YWpFmAHjanpn63k&t>Yp#tYop`e$27YpzBP$kyOi1Y41O*MX(`<>xW9(B|Ic)+~9$f(q z9@;S4oW+EosT5pJq1#*Y`of3%$+MacJsrujmLXmh9H9do)5e(?F;gs#(0Sn(ZW`!L zl!@If6^n&Y-75)|LOSXNgg1+QSavfR3h3#aFJOXXg zjApcsnh6UE>T@x$!(*n~K#DU$YcU+bx(8>Lyv9U^Wl=@UZ%j_35DI*iNr6{q5zm<< zK8DMFKue0oMqJ)_S5eNn975Mr6bK2hU~wKI7&LAt3$p+rkO&A&Znk%b&nEIO-LBz9 zKbfR?YrQ$L%0@48*dudms9Ox^6mLD8UxW%^3`kA`utgM*_r_uLqYkF912QEcx_~%4 zkgz4kjQbWR(cH!;hg+s~hz*`v>xCE9IY`-Ja97JyW6rp=QQ!1nKg?i}_XMKI991UQ z2rvo=CgWU6C_eUiS#v;})4e!>k=MxkUc{sTMV7-DU0@gWqn_rt zby{SBTjcrQPoJLYysxJ^YvWvBo=OzzqETp|sG5fr!h?%s?q1ZkcFDkE>e2hHJQk%K2buseRWIdAR{cMnBB9>;e=%Os|GKz}7(5-4H7 z?Pu@~pr70`CT n`)nd*wLToqk{qrCuik7wjj+Bx-#5o|b;sr6#FHmq{qXuP + +NS_ASSUME_NONNULL_BEGIN + +@interface NSObject (performSelector) +- (id)performSelector:(SEL)selector withArguments:(NSArray *)arguments; +@end + +NS_ASSUME_NONNULL_END diff --git a/DuckDuckGo/Common/Extensions/NSObject+performSelector.m b/DuckDuckGo/Common/Extensions/NSObject+performSelector.m new file mode 100644 index 0000000000..d5b7a7bb08 --- /dev/null +++ b/DuckDuckGo/Common/Extensions/NSObject+performSelector.m @@ -0,0 +1,52 @@ +// +// NSObject+performSelector.m +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "NSObject+performSelector.h" + +@implementation NSObject (performSelector) + +- (id)performSelector:(SEL)selector withArguments:(NSArray *)arguments { + NSMethodSignature *methodSignature = [self methodSignatureForSelector:selector]; + + if (!methodSignature) { + [[[NSException alloc] initWithName:@"InvalidSelectorOrTarget" reason:[NSString stringWithFormat:@"Could not get method signature for selector %@ on %@", NSStringFromSelector(selector), self] userInfo:nil] raise]; + } + + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; + [invocation setSelector:selector]; + [invocation setTarget:self]; + + for (NSInteger i = 0; i < arguments.count; i++) { + id argument = arguments[i]; + [invocation setArgument:&argument atIndex:i + 2]; // Indices 0 and 1 are reserved for target and selector + } + + [invocation invoke]; + + if (methodSignature.methodReturnLength > 0) { + id returnValue; + [invocation getReturnValue:&returnValue]; + + return returnValue; + } + + return nil; +} + + +@end diff --git a/DuckDuckGo/Common/Extensions/StringExtension.swift b/DuckDuckGo/Common/Extensions/StringExtension.swift index 53cd3148a2..b6d6affbb0 100644 --- a/DuckDuckGo/Common/Extensions/StringExtension.swift +++ b/DuckDuckGo/Common/Extensions/StringExtension.swift @@ -16,8 +16,9 @@ // limitations under the License. // -import Foundation import BrowserServicesKit +import Common +import Foundation import UniformTypeIdentifiers extension String { @@ -32,6 +33,42 @@ extension String { self.replacingOccurrences(of: "\\", with: "\\\\") .replacingOccurrences(of: "\"", with: "\\\"") .replacingOccurrences(of: "'", with: "\\'") + .replacingOccurrences(of: "\n", with: "\\n") + } + + private static let unicodeHtmlCharactersMapping: [Character: String] = [ + "&": "&", + "\"": """, + "'": "'", + "<": "<", + ">": ">", + "/": "/", + "!": "!", + "$": "$", + "%": "%", + "=": "=", + "#": "#", + "@": "@", + "[": "[", + "\\": "\", + "]": "]", + "^": "^", + "`": "a", + "{": "{", + "}": "}", + ] + func escapedUnicodeHtmlString() -> String { + var result = "" + + for character in self { + if let mapped = Self.unicodeHtmlCharactersMapping[character] { + result.append(mapped) + } else { + result.append(character) + } + } + + return result } init(_ staticString: StaticString) { diff --git a/DuckDuckGo/Common/Extensions/URLExtension.swift b/DuckDuckGo/Common/Extensions/URLExtension.swift index 7ab5a0deff..51144f4e3d 100644 --- a/DuckDuckGo/Common/Extensions/URLExtension.swift +++ b/DuckDuckGo/Common/Extensions/URLExtension.swift @@ -130,6 +130,8 @@ extension URL { static let welcome = URL(string: "duck://welcome")! static let settings = URL(string: "duck://settings")! static let bookmarks = URL(string: "duck://bookmarks")! + // base url for Error Page Alternate HTML loaded into Web View + static let error = URL(string: "duck://error")! static let dataBrokerProtection = URL(string: "duck://dbp")! diff --git a/DuckDuckGo/Common/Extensions/WKBackForwardListItemExtension.swift b/DuckDuckGo/Common/Extensions/WKBackForwardListItemExtension.swift new file mode 100644 index 0000000000..909d3bcc9c --- /dev/null +++ b/DuckDuckGo/Common/Extensions/WKBackForwardListItemExtension.swift @@ -0,0 +1,35 @@ +// +// WKBackForwardListItemExtension.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import WebKit + +extension WKBackForwardListItem { + + // sometimes WKBackForwardListItem returns wrong or outdated title + private static let tabTitleKey = UnsafeRawPointer(bitPattern: "tabTitleKey".hashValue)! + var tabTitle: String? { + get { + objc_getAssociatedObject(self, Self.tabTitleKey) as? String + } + set { + objc_setAssociatedObject(self, Self.tabTitleKey, newValue, .OBJC_ASSOCIATION_RETAIN) + } + } + +} diff --git a/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift b/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift index 01df6a149a..a2eb4706e4 100644 --- a/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift +++ b/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift @@ -16,6 +16,7 @@ // limitations under the License. // +import Common import Navigation import WebKit @@ -231,6 +232,21 @@ extension WKWebView { self.evaluateJavaScript("window.open(\(urlEnc), '_blank', 'noopener, noreferrer')") } + func loadAlternateHTML(_ html: String, baseURL: URL, forUnreachableURL failingURL: URL) { + guard responds(to: Selector.loadAlternateHTMLString) else { + if #available(macOS 12.0, *) { + os_log(.error, log: .navigation, "WKWebView._loadAlternateHTMLString not available") + loadSimulatedRequest(URLRequest(url: failingURL), responseHTML: html) + } + return + } + self.perform(Selector.loadAlternateHTMLString, withArguments: [html, baseURL, failingURL]) + } + + func setDocumentHtml(_ html: String) { + self.evaluateJavaScript("document.open(); document.write('\(html.escapedJavaScriptString())'); document.close()", in: nil, in: .defaultClient) + } + @MainActor var mimeType: String? { get async { @@ -285,6 +301,7 @@ extension WKWebView { enum Selector { static let fullScreenPlaceholderView = NSSelectorFromString("_fullScreenPlaceholderView") static let printOperationWithPrintInfoForFrame = NSSelectorFromString("_printOperationWithPrintInfo:forFrame:") + static let loadAlternateHTMLString = NSSelectorFromString("_loadAlternateHTMLString:baseURL:forUnreachableURL:") } } diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 985fd7fcaa..e08d3ae946 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -17,6 +17,7 @@ // import Foundation +import Navigation struct UserText { @@ -212,12 +213,15 @@ struct UserText { static let tabPreferencesTitle = NSLocalizedString("tab.preferences.title", value: "Settings", comment: "Tab preferences title") static let tabBookmarksTitle = NSLocalizedString("tab.bookmarks.title", value: "Bookmarks", comment: "Tab bookmarks title") static let tabOnboardingTitle = NSLocalizedString("tab.onboarding.title", value: "Welcome", comment: "Tab onboarding title") - static let tabErrorTitle = NSLocalizedString("tab.error.title", value: "Oops!", comment: "Tab error title") + static let tabErrorTitle = NSLocalizedString("tab.error.title", value: "Failed to open page", comment: "Tab error title") + static let errorPageHeader = NSLocalizedString("page.error.header", value: "DuckDuckGo can’t load this page.", comment: "Error page heading text") + static let webProcessCrashPageHeader = NSLocalizedString("page.crash.header", value: "This webpage has crashed.", comment: "Error page heading text shown when a Web Page process had crashed") + static let webProcessCrashPageMessage = NSLocalizedString("page.crash.message", value: "Try reloading the page or come back later.", comment: "Error page message text shown when a Web Page process had crashed") + static let openSystemPreferences = NSLocalizedString("open.preferences", value: "Open System Preferences", comment: "Open System Preferences (to re-enable permission for the App) (up to and including macOS 12") static let openSystemSettings = NSLocalizedString("open.settings", value: "Open System Settings…", comment: "") static let checkForUpdate = NSLocalizedString("check.for.update", value: "Check for Update", comment: "Button users can use to check for a new update") - static let unknownErrorMessage = NSLocalizedString("error.unknown", value: "An unknown error has occurred", comment: "Error page subtitle") static let unknownErrorTryAgainMessage = NSLocalizedString("error.unknown.try.again", value: "An unknown error has occurred", comment: "Generic error message on a dialog for when the cause is not known.") static let moveTabToNewWindow = NSLocalizedString("options.menu.move.tab.to.new.window", diff --git a/DuckDuckGo/Common/View/AppKit/ColorView.swift b/DuckDuckGo/Common/View/AppKit/ColorView.swift index 07e5a383f5..dd48e068e1 100644 --- a/DuckDuckGo/Common/View/AppKit/ColorView.swift +++ b/DuckDuckGo/Common/View/AppKit/ColorView.swift @@ -29,6 +29,7 @@ internal class ColorView: NSView { init(frame: NSRect, backgroundColor: NSColor? = nil, cornerRadius: CGFloat = 0, borderColor: NSColor? = nil, borderWidth: CGFloat = 0, interceptClickEvents: Bool = false) { super.init(frame: frame) + self.translatesAutoresizingMaskIntoConstraints = false self.backgroundColor = backgroundColor self.cornerRadius = cornerRadius self.borderColor = borderColor diff --git a/DuckDuckGo/DataExport/BookmarksExporter.swift b/DuckDuckGo/DataExport/BookmarksExporter.swift index c089132d9c..eb910ed472 100644 --- a/DuckDuckGo/DataExport/BookmarksExporter.swift +++ b/DuckDuckGo/DataExport/BookmarksExporter.swift @@ -34,7 +34,7 @@ struct BookmarksExporter { for entity in entities { if let bookmark = entity as? Bookmark { content.append(Template.bookmark(level: level, - title: bookmark.title.escapedForHTML, + title: bookmark.title.escapedUnicodeHtmlString(), url: bookmark.url, isFavorite: bookmark.isFavorite)) } @@ -100,12 +100,6 @@ extension BookmarksExporter { fileprivate extension String { - var escapedForHTML: String { - self.replacingOccurrences(of: "&", with: "&") - .replacingOccurrences(of: "<", with: "<") - .replacingOccurrences(of: ">", with: ">") - } - static func indent(by level: Int) -> String { return String(repeating: "\t", count: level) } diff --git a/DuckDuckGo/ErrorPage/ErrorPageHTMLTemplate.swift b/DuckDuckGo/ErrorPage/ErrorPageHTMLTemplate.swift new file mode 100644 index 0000000000..af9d04103e --- /dev/null +++ b/DuckDuckGo/ErrorPage/ErrorPageHTMLTemplate.swift @@ -0,0 +1,45 @@ +// +// ErrorPageHTMLTemplate.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import ContentScopeScripts +import WebKit + +struct ErrorPageHTMLTemplate { + + static var htmlTemplatePath: String { + guard let file = ContentScopeScripts.Bundle.path(forResource: "index", ofType: "html", inDirectory: "pages/errorpage") else { + assertionFailure("HTML template not found") + return "" + } + return file + } + + let error: WKError + let header: String + + func makeHTMLFromTemplate() -> String { + guard let html = try? String(contentsOfFile: Self.htmlTemplatePath) else { + assertionFailure("Should be able to load template") + return "" + } + return html.replacingOccurrences(of: "$ERROR_DESCRIPTION$", with: error.localizedDescription.escapedUnicodeHtmlString(), options: .literal) + .replacingOccurrences(of: "$HEADER$", with: header.escapedUnicodeHtmlString(), options: .literal) + } + +} diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index d98a50d6fd..6171229925 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -2889,18 +2889,6 @@ }, "Error message & code" : { - }, - "error.unknown" : { - "comment" : "Error page subtitle", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "An unknown error has occurred" - } - } - } }, "error.unknown.try.again" : { "comment" : "Generic error message on a dialog for when the cause is not known.", @@ -5914,6 +5902,42 @@ } } }, + "page.crash.header" : { + "comment" : "Error page heading text shown when a Web Page process had crashed", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "This webpage has crashed." + } + } + } + }, + "page.crash.message" : { + "comment" : "Error page message text shown when a Web Page process had crashed", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Try reloading the page or come back later." + } + } + } + }, + "page.error.header" : { + "comment" : "Error page heading text", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "DuckDuckGo can’t load this page." + } + } + } + }, "passsword.management" : { "comment" : "Used as title for password management user interface", "extractionState" : "extracted_with_value", @@ -8645,7 +8669,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "Oops!" + "value" : "Failed to open page" } } } @@ -9161,4 +9185,4 @@ } }, "version" : "1.0" -} \ No newline at end of file +} diff --git a/DuckDuckGo/MainWindow/MainViewController.swift b/DuckDuckGo/MainWindow/MainViewController.swift index 9b7c307eea..adae427d70 100644 --- a/DuckDuckGo/MainWindow/MainViewController.swift +++ b/DuckDuckGo/MainWindow/MainViewController.swift @@ -65,7 +65,7 @@ final class MainViewController: NSViewController { tabBarViewController = TabBarViewController.create(tabCollectionViewModel: tabCollectionViewModel) navigationBarViewController = NavigationBarViewController.create(tabCollectionViewModel: tabCollectionViewModel, isBurner: isBurner) - browserTabViewController = BrowserTabViewController.create(tabCollectionViewModel: tabCollectionViewModel) + browserTabViewController = BrowserTabViewController(tabCollectionViewModel: tabCollectionViewModel, bookmarkManager: bookmarkManager) findInPageViewController = FindInPageViewController.create() fireViewController = FireViewController.create(tabCollectionViewModel: tabCollectionViewModel) bookmarksBarViewController = BookmarksBarViewController.create(tabCollectionViewModel: tabCollectionViewModel, bookmarkManager: bookmarkManager) diff --git a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift index 702fd0aa8b..b5846ee9ce 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift @@ -282,12 +282,18 @@ final class AddressBarButtonsViewController: NSViewController { guard view.window?.isPopUpWindow == false else { return } bookmarkButton.setAccessibilityIdentifier("Bookmarks Button") let hasEmptyAddressBar = textFieldValue?.isEmpty ?? true - var isUrlBookmarked = false - if let url = tabCollectionViewModel.selectedTabViewModel?.tab.content.url, - bookmarkManager.isUrlBookmarked(url: url) { - isUrlBookmarked = true + var showBookmarkButton: Bool { + guard let selectedTabViewModel = tabCollectionViewModel.selectedTabViewModel, + selectedTabViewModel.canBeBookmarked else { return false } + + var isUrlBookmarked = false + if let url = selectedTabViewModel.tab.content.url, + bookmarkManager.isUrlBookmarked(url: url) { + isUrlBookmarked = true + } + + return clearButton.isHidden && !hasEmptyAddressBar && (isMouseOverNavigationBar || bookmarkPopover?.isShown == true || isUrlBookmarked) } - let showBookmarkButton = clearButton.isHidden && !hasEmptyAddressBar && (isMouseOverNavigationBar || bookmarkPopover?.isShown == true || isUrlBookmarked) bookmarkButton.isHidden = !showBookmarkButton } @@ -612,15 +618,18 @@ final class AddressBarButtonsViewController: NSViewController { guard let selectedTabViewModel = tabCollectionViewModel.selectedTabViewModel else { updateBookmarkButtonImage() - updateBookmarkButtonVisibility() + updateButtons() return } - urlCancellable = selectedTabViewModel.tab.$content.receive(on: DispatchQueue.main).sink { [weak self] _ in - self?.stopAnimations() - self?.updateBookmarkButtonImage() - self?.updateBookmarkButtonVisibility() - } + urlCancellable = selectedTabViewModel.tab.$content + .combineLatest(selectedTabViewModel.tab.$error) + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.stopAnimations() + self?.updateBookmarkButtonImage() + self?.updateButtons() + } } private func subscribeToPermissions() { @@ -690,8 +699,8 @@ final class AddressBarButtonsViewController: NSViewController { private func updatePermissionButtons() { permissionButtons.isHidden = isTextFieldEditorFirstResponder - || isAnyTrackerAnimationPlaying - || (tabCollectionViewModel.selectedTabViewModel?.errorViewState.isVisible ?? true) + || isAnyTrackerAnimationPlaying + || (tabCollectionViewModel.selectedTabViewModel?.isShowingErrorPage ?? true) defer { showOrHidePermissionPopoverIfNeeded() } @@ -750,6 +759,8 @@ final class AddressBarButtonsViewController: NSViewController { // Image button switch controllerMode { + case .browsing where selectedTabViewModel.isShowingErrorPage: + imageButton.image = Self.webImage case .browsing: imageButton.image = selectedTabViewModel.favicon case .editing(isUrl: true): @@ -774,12 +785,12 @@ final class AddressBarButtonsViewController: NSViewController { let isLocalUrl = selectedTabViewModel.tab.content.url?.isLocalURL ?? false // Privacy entry point button - privacyEntryPointButton.isHidden = isEditingMode || - isTextFieldEditorFirstResponder || - !isHypertextUrl || - selectedTabViewModel.errorViewState.isVisible || - isTextFieldValueText || - isLocalUrl + privacyEntryPointButton.isHidden = isEditingMode + || isTextFieldEditorFirstResponder + || !isHypertextUrl + || selectedTabViewModel.isShowingErrorPage + || isTextFieldValueText + || isLocalUrl imageButtonWrapper.isHidden = view.window?.isPopUpWindow == true || !privacyEntryPointButton.isHidden || isAnyTrackerAnimationPlaying diff --git a/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift index 24d7ebe5fc..e60a26b90f 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift @@ -257,12 +257,16 @@ final class AddressBarViewController: NSViewController { } .store(in: &tabViewModelCancellables) - selectedTabViewModel.$isLoading - .sink { [weak self] isLoading in + selectedTabViewModel.$isLoading.combineLatest(selectedTabViewModel.tab.$error) + .debounce(for: 0.1, scheduler: RunLoop.main) + .sink { [weak self] isLoading, error in guard let progressIndicator = self?.progressIndicator else { return } if isLoading, - selectedTabViewModel.tab.content.url?.isDuckDuckGoSearch == false { + let url = selectedTabViewModel.tab.content.url, + [.http, .https].contains(url.navigationalScheme), + url.isDuckDuckGoSearch == false, + error == nil { progressIndicator.show(progress: selectedTabViewModel.progress, startTime: selectedTabViewModel.loadingStartTime) diff --git a/DuckDuckGo/NavigationBar/View/NavigationButtonMenuDelegate.swift b/DuckDuckGo/NavigationBar/View/NavigationButtonMenuDelegate.swift index 55b81c2954..ebf24cfbfb 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationButtonMenuDelegate.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationButtonMenuDelegate.swift @@ -44,22 +44,21 @@ extension NavigationButtonMenuDelegate: NSMenuDelegate { let listItems = listItems // Don't show menu if there is just the current item - if listItems.items.count == 0 || (listItems.items.count == 1 && listItems.currentIndex == 0) { return 0 } + if listItems.count == 1 { return 0 } - return listItems.items.count + return listItems.count } func menu(_ menu: NSMenu, update item: NSMenuItem, at index: Int, shouldCancel: Bool) -> Bool { - let (listItems, currentIndex) = self.listItems guard let listItem = listItems[safe: index] else { os_log("%s: Index out of bounds", type: .error, className) return true } - let listItemViewModel = WKBackForwardListItemViewModel(backForwardListItem: listItem, - faviconManagement: FaviconManager.shared, - historyCoordinating: HistoryCoordinator.shared, - isCurrentItem: index == currentIndex) + let listItemViewModel = BackForwardListItemViewModel(backForwardListItem: listItem, + faviconManagement: FaviconManager.shared, + historyCoordinating: HistoryCoordinator.shared, + isCurrentItem: index == 0) item.title = listItemViewModel.title item.image = listItemViewModel.image @@ -74,67 +73,25 @@ extension NavigationButtonMenuDelegate: NSMenuDelegate { @MainActor @objc func menuItemAction(_ sender: NSMenuItem) { let index = sender.tag - let (listItems, currentIndex) = self.listItems guard let listItem = listItems[safe: index] else { os_log("%s: Index out of bounds", type: .error, className) return } - - guard currentIndex != index else { - // current item selected: do nothing - return - } - guard let selectedTabViewModel = tabCollectionViewModel.selectedTabViewModel else { - os_log("%s: Selected tab view model is nil", type: .error, className) - return - } - - switch listItem { - case .backForwardListItem(let wkListItem): - selectedTabViewModel.tab.go(to: wkListItem) - case .goBackToCloseItem(parentTab:): - tabCollectionViewModel.selectedTabViewModel?.tab.goBack() - case .error: - break - } + tabCollectionViewModel.selectedTabViewModel?.tab.go(to: listItem) } - private var listItems: (items: [BackForwardListItem], currentIndex: Int?) { + private var listItems: [BackForwardListItem] { guard let selectedTabViewModel = tabCollectionViewModel.selectedTabViewModel else { - os_log("%s: Selected tab view model is nil", type: .error, className) - return ([], nil) - } - - let backForwardList = selectedTabViewModel.tab.webView.backForwardList - let wkList = buttonType == .back ? backForwardList.backList.reversed() : backForwardList.forwardList - var list = wkList.map { BackForwardListItem.backForwardListItem($0) } - var currentIndex: Int? - - // Add closing with back button to the list - if list.count == 0, - let parentTab = selectedTabViewModel.tab.parentTab, - buttonType == .back { - list.insert(.goBackToCloseItem(parentTab: parentTab), at: 0) + assertionFailure("Selected tab view model is nil") + return [] } + guard let currentItem = selectedTabViewModel.tab.currentHistoryItem else { return [] } - // Add current item to the list - if let currentItem = selectedTabViewModel.tab.webView.backForwardList.currentItem { - list.insert(.backForwardListItem(currentItem), at: 0) - currentIndex = 0 - } - - // Add error to the list - if selectedTabViewModel.tab.error != nil { - if buttonType == .back { - list.insert(.error, at: 0) - currentIndex = 0 - } else { - list = [] - currentIndex = nil - } - } + let list = [currentItem] + (buttonType == .back + ? selectedTabViewModel.tab.backHistoryItems.reversed() + : selectedTabViewModel.tab.forwardHistoryItems) - return (list, currentIndex) + return list } } diff --git a/DuckDuckGo/NavigationBar/ViewModel/BackForwardListItem.swift b/DuckDuckGo/NavigationBar/ViewModel/BackForwardListItem.swift index af0c0289de..51fbfe4e81 100644 --- a/DuckDuckGo/NavigationBar/ViewModel/BackForwardListItem.swift +++ b/DuckDuckGo/NavigationBar/ViewModel/BackForwardListItem.swift @@ -16,22 +16,40 @@ // limitations under the License. // +import Navigation import WebKit -enum BackForwardListItem: Equatable { - case backForwardListItem(WKBackForwardListItem) - case goBackToCloseItem(parentTab: Tab) - case error +struct BackForwardListItem: Hashable { + + enum Kind: Hashable { + case url(URL) + case goBackToClose(URL?) + } + let kind: Kind + let title: String? + let identity: HistoryItemIdentity? var url: URL? { - switch self { - case .backForwardListItem(let item): - return item.url - case .goBackToCloseItem(parentTab: let tab): - return tab.content.url - case .error: - return nil + switch kind { + case .url(let url): return url + case .goBackToClose(let url): return url } } + init(kind: Kind, title: String?, identity: HistoryItemIdentity?) { + self.kind = kind + self.title = title + self.identity = identity + } + + init(_ wkItem: WKBackForwardListItem) { + self.init(kind: .url(wkItem.url), title: wkItem.tabTitle ?? wkItem.title, identity: wkItem.identity) + } + +} + +extension [BackForwardListItem] { + init(_ items: [WKBackForwardListItem]) { + self = items.map(BackForwardListItem.init) + } } diff --git a/DuckDuckGo/NavigationBar/ViewModel/WKBackForwardListItemViewModel.swift b/DuckDuckGo/NavigationBar/ViewModel/BackForwardListItemViewModel.swift similarity index 65% rename from DuckDuckGo/NavigationBar/ViewModel/WKBackForwardListItemViewModel.swift rename to DuckDuckGo/NavigationBar/ViewModel/BackForwardListItemViewModel.swift index 3eae6df4ae..2f3358c167 100644 --- a/DuckDuckGo/NavigationBar/ViewModel/WKBackForwardListItemViewModel.swift +++ b/DuckDuckGo/NavigationBar/ViewModel/BackForwardListItemViewModel.swift @@ -1,5 +1,5 @@ // -// WKBackForwardListItemViewModel.swift +// BackForwardListItemViewModel.swift // // Copyright © 2020 DuckDuckGo. All rights reserved. // @@ -17,9 +17,8 @@ // import Cocoa -import WebKit -final class WKBackForwardListItemViewModel { +final class BackForwardListItemViewModel { private let backForwardListItem: BackForwardListItem private let faviconManagement: FaviconManagement @@ -37,42 +36,33 @@ final class WKBackForwardListItemViewModel { } var title: String { - switch backForwardListItem { - case .backForwardListItem(let item): - if item.url == .newtab { + switch backForwardListItem.kind { + case .url(let url): + if url == .newtab { return UserText.tabHomeTitle } - var title = item.title + var title = backForwardListItem.title if title == nil || (title?.isEmpty ?? false) { - title = historyCoordinating.title(for: item.url) + title = historyCoordinating.title(for: url) } - return title ?? - item.url.host ?? - item.url.absoluteString + return (title ?? url.host ?? url.absoluteString).truncated(length: MainMenu.Constants.maxTitleLength) - case .goBackToCloseItem(parentTab: let tab): - if let title = tab.title, - !title.isEmpty { - return String(format: UserText.closeAndReturnToParentFormat, title) + case .goBackToClose(let url): + if let title = backForwardListItem.title ?? url?.absoluteString, !title.isEmpty { + return String(format: UserText.closeAndReturnToParentFormat, title.truncated(length: MainMenu.Constants.maxTitleLength)) } else { return UserText.closeAndReturnToParent } - case .error: - return UserText.tabErrorTitle } } @MainActor(unsafe) var image: NSImage? { - if case .error = backForwardListItem { - return nil - } - if backForwardListItem.url == .newtab { - return NSImage(named: "HomeFavicon") + return .homeFavicon } if backForwardListItem.url?.isDuckPlayer == true { @@ -85,23 +75,11 @@ final class WKBackForwardListItemViewModel { return image } - return NSImage(named: "DefaultFavicon") + return .globeMulticolor16 } var state: NSControl.StateValue { - if case .goBackToCloseItem = backForwardListItem { - return .off - } - - return isCurrentItem ? .on : .off - } - - var isGoBackToCloseItem: Bool { - if case .goBackToCloseItem = backForwardListItem { - return true - } - - return false + isCurrentItem ? .on : .off } } diff --git a/DuckDuckGo/Sharing/SharingMenu.swift b/DuckDuckGo/Sharing/SharingMenu.swift index c3a6c1ec12..50c5808e4a 100644 --- a/DuckDuckGo/Sharing/SharingMenu.swift +++ b/DuckDuckGo/Sharing/SharingMenu.swift @@ -48,6 +48,7 @@ final class SharingMenu: NSMenu { private func sharingData() -> SharingData? { guard let tabViewModel = WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.tabCollectionViewModel.selectedTabViewModel, tabViewModel.canReload, + !tabViewModel.isShowingErrorPage, let url = tabViewModel.tab.content.url else { return nil } let sharingData = DuckPlayer.shared.sharingData(for: tabViewModel.title, url: url) ?? (tabViewModel.title, url) diff --git a/DuckDuckGo/Tab/Model/Tab+Navigation.swift b/DuckDuckGo/Tab/Model/Tab+Navigation.swift index daaae97f59..3df38461d6 100644 --- a/DuckDuckGo/Tab/Model/Tab+Navigation.swift +++ b/DuckDuckGo/Tab/Model/Tab+Navigation.swift @@ -42,6 +42,10 @@ extension Tab: NavigationResponder { navigationDelegate.setResponders( .weak(nullable: self.navigationHotkeyHandler), + // redirect to SERP for non-valid domains entered by user + // should be before `self` to avoid Tab presenting an error screen + .weak(nullable: self.searchForNonexistentDomains), + .weak(self), // Duck Player overlay navigations handling @@ -65,9 +69,6 @@ extension Tab: NavigationResponder { // add extra headers to SERP requests .struct(SerpHeadersNavigationResponder()), - // redirect to SERP for non-valid domains entered by user - .weak(nullable: self.searchForNonexistentDomains), - // ensure Content Blocking Rules are applied before navigation .weak(nullable: self.contentBlockingAndSurrogates), // update click-to-load state diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift index f2c35a5a18..57cad4d4db 100644 --- a/DuckDuckGo/Tab/Model/Tab.swift +++ b/DuckDuckGo/Tab/Model/Tab.swift @@ -440,6 +440,7 @@ protocol NewWindowPolicyDecisionMaker { isTabPinned: { tabGetter().map { tab in pinnedTabsManager.isTabPinned(tab) } ?? false }, isTabBurner: burnerMode.isBurner, contentPublisher: _content.projectedValue.eraseToAnyPublisher(), + setContent: { tabGetter()?.setContent($0) }, titlePublisher: _title.projectedValue.eraseToAnyPublisher(), userScriptsPublisher: userScriptsPublisher, inheritedAttribution: parentTab?.adClickAttribution?.currentAttributionState, @@ -598,7 +599,6 @@ protocol NewWindowPolicyDecisionMaker { let webViewDidReceiveRedirectPublisher = PassthroughSubject() let webViewDidCommitNavigationPublisher = PassthroughSubject() let webViewDidFinishNavigationPublisher = PassthroughSubject() - let webViewDidFailNavigationPublisher = PassthroughSubject() // MARK: - Properties @@ -616,7 +616,6 @@ protocol NewWindowPolicyDecisionMaker { webView.stopAllMedia(shouldStopLoading: false) } handleFavicon(oldValue: oldValue) - invalidateInteractionStateData() if navigationDelegate.currentNavigation == nil { updateCanGoBackForward(withCurrentNavigation: nil) } @@ -674,6 +673,12 @@ protocol NewWindowPolicyDecisionMaker { @Published var title: String? private func updateTitle() { + if let error { + if error.code != .webContentProcessTerminated { + self.title = nil + } + return + } var title = webView.title?.trimmingWhitespace() if title?.isEmpty ?? true { title = webView.url?.host?.droppingWwwPrefix() @@ -682,14 +687,16 @@ protocol NewWindowPolicyDecisionMaker { if title != self.title { self.title = title } + + if let wkBackForwardListItem = webView.backForwardList.currentItem, + content.urlForWebView == wkBackForwardListItem.url { + wkBackForwardListItem.tabTitle = title + } } @PublishedAfter var error: WKError? { didSet { - if error == nil || error?.isFrameLoadInterrupted == true || error?.isNavigationCancelled == true { - return - } - webView.stopAllMediaPlayback() + updateTitle() } } let permissions: PermissionModel @@ -762,6 +769,23 @@ protocol NewWindowPolicyDecisionMaker { @Published private(set) var canGoBack: Bool = false @Published private(set) var canReload: Bool = false + @MainActor + var backHistoryItems: [BackForwardListItem] { + [BackForwardListItem](webView.backForwardList.backList) + + (canBeClosedWithBack ? [BackForwardListItem(kind: .goBackToClose(parentTab?.url), title: parentTab?.title, identity: nil)] : []) + } + @MainActor + var currentHistoryItem: BackForwardListItem? { + webView.backForwardList.currentItem.map(BackForwardListItem.init) + ?? (content.url ?? navigationDelegate.currentNavigation?.url).map { url in + BackForwardListItem(kind: .url(url), title: webView.title ?? title, identity: nil) + } + } + @MainActor + var forwardHistoryItems: [BackForwardListItem] { + [BackForwardListItem](webView.backForwardList.forwardList) + } + private func updateCanGoBackForward() { updateCanGoBackForward(withCurrentNavigation: navigationDelegate.currentNavigation) } @@ -780,8 +804,8 @@ protocol NewWindowPolicyDecisionMaker { return } - let canGoBack = webView.canGoBack || self.error != nil - let canGoForward = webView.canGoForward && self.error == nil + let canGoBack = webView.canGoBack + let canGoForward = webView.canGoForward let canReload = self.content.userEditableUrl != nil if canGoBack != self.canGoBack { @@ -805,10 +829,6 @@ protocol NewWindowPolicyDecisionMaker { return nil } - guard error == nil else { - return webView.navigator()?.reload(withExpectedNavigationType: .reload) - } - userInteractionDialog = nil return webView.navigator()?.goBack(withExpectedNavigationType: .backForward(distance: -1)) } @@ -817,14 +837,62 @@ protocol NewWindowPolicyDecisionMaker { @discardableResult func goForward() -> ExpectedNavigation? { guard canGoForward else { return nil } + + userInteractionDialog = nil return webView.navigator()?.goForward(withExpectedNavigationType: .backForward(distance: 1)) } - func go(to item: WKBackForwardListItem) { - webView.go(to: item) + @MainActor + @discardableResult + func go(to item: BackForwardListItem) -> ExpectedNavigation? { + userInteractionDialog = nil + + switch item.kind { + case .goBackToClose: + delegate?.closeTab(self) + return nil + + case .url: break + } + + var backForwardNavigation: (distance: Int, item: WKBackForwardListItem)? { + guard let identity = item.identity else { return nil } + + let backForwardList = webView.backForwardList + if let backItem = backForwardList.backItem, backItem.identity == identity { + return (-1, backItem) + } else if let forwardItem = backForwardList.forwardItem, forwardItem.identity == identity { + return (1, forwardItem) + } else if backForwardList.currentItem?.identity == identity { + return nil + } + + let forwardList = backForwardList.forwardList + if let forwardIndex = forwardList.firstIndex(where: { $0.identity == identity }) { + return (forwardIndex + 1, forwardList[forwardIndex]) // going forward, adding 1 to zero based index + } + + let backList = backForwardList.backList + if let backIndex = backList.lastIndex(where: { $0.identity == identity }) { + return (-(backList.count - backIndex), backList[backIndex]) // item is in _reversed_ backList + } + + return nil + + } + + guard let backForwardNavigation else { + os_log(.error, "item `\(item.title ?? "") – \(item.url?.absoluteString ?? "")` is not in the backForwardList") + return nil + } + + return webView.navigator()?.go(to: backForwardNavigation.item, + withExpectedNavigationType: .backForward(distance: backForwardNavigation.distance)) } func openHomePage() { + userInteractionDialog = nil + if startupPreferences.launchToCustomHomePage, let customURL = URL(string: startupPreferences.formattedCustomHomePageURL) { webView.load(URLRequest(url: customURL)) @@ -834,24 +902,35 @@ protocol NewWindowPolicyDecisionMaker { } func startOnboarding() { + userInteractionDialog = nil + webView.load(URLRequest(url: .welcome)) } - func reload() { + @MainActor(unsafe) + @discardableResult + func reload() -> ExpectedNavigation? { userInteractionDialog = nil // In the case of an error only reload web URLs to prevent uxss attacks via redirecting to javascript:// - if let error = error, let failingUrl = error.failingUrl, failingUrl.isHttp || failingUrl.isHttps { - webView.load(URLRequest(url: failingUrl, cachePolicy: .reloadIgnoringLocalCacheData)) - return + if let error = error, + let failingUrl = error.failingUrl ?? content.urlForWebView, + failingUrl.isHttp || failingUrl.isHttps, + // navigate in-place to preserve back-forward history + // launch navigation using javascript: URL navigation to prevent WebView from + // interpreting the action as user-initiated link navigation causing a new tab opening when Cmd is pressed + let redirectUrl = URL(string: "javascript:location.replace('\(failingUrl.absoluteString.escapedJavaScriptString())')") { + + webView.load(URLRequest(url: redirectUrl)) + return nil } if webView.url == nil, content.isUrl { self.content = content.forceReload() // load from cache or interactionStateData when called by lazy loader - reloadIfNeeded(shouldLoadInBackground: true) + return reloadIfNeeded(shouldLoadInBackground: true) } else { - webView.reload() + return webView.navigator(distributedNavigationDelegate: navigationDelegate).reload(withExpectedNavigationType: .reload) } } @@ -872,7 +951,11 @@ protocol NewWindowPolicyDecisionMaker { let forceReload = (url.absoluteString == content.userEnteredValue) ? shouldLoadInBackground : (source == .reload) if forceReload || shouldReload(url, shouldLoadInBackground: shouldLoadInBackground) { + if webView.url == url, webView.backForwardList.currentItem?.url == url, !webView.isLoading { + return reload() + } if restoreInteractionStateDataIfNeeded() { return nil /* session restored */ } + invalidateInteractionStateData() if url.isFileURL { return webView.navigator(distributedNavigationDelegate: navigationDelegate) @@ -883,7 +966,6 @@ protocol NewWindowPolicyDecisionMaker { if #available(macOS 12.0, *), content.isUserEnteredUrl { request.attribution = .user } - invalidateInteractionStateData() return webView.navigator(distributedNavigationDelegate: navigationDelegate) .load(request, withExpectedNavigationType: source.navigationType) @@ -897,7 +979,7 @@ protocol NewWindowPolicyDecisionMaker { guard url.isValid, webView.superview != nil || shouldLoadInBackground, // don‘t reload when already loaded - webView.url != url else { return false } + webView.url != url || error != nil else { return false } // if content not loaded inspect error switch error { @@ -981,9 +1063,13 @@ protocol NewWindowPolicyDecisionMaker { } }.store(in: &webViewCancellables) - webView.observe(\.url) { [weak self] _, _ in - self?.handleUrlDidChange() - }.store(in: &webViewCancellables) + webView.publisher(for: \.url) + .dropFirst() + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.handleUrlDidChange() + }.store(in: &webViewCancellables) + webView.observe(\.title) { [weak self] _, _ in self?.updateTitle() }.store(in: &webViewCancellables) @@ -1039,7 +1125,7 @@ protocol NewWindowPolicyDecisionMaker { @MainActor(unsafe) private func handleFavicon(oldValue: TabContent? = nil) { - guard content.isUrl, let url = content.urlForWebView else { + guard content.isUrl, let url = content.urlForWebView, error == nil else { favicon = nil return } @@ -1092,6 +1178,7 @@ extension Tab: FaviconUserScriptDelegate { func faviconUserScript(_ faviconUserScript: FaviconUserScript, didFindFaviconLinks faviconLinks: [FaviconUserScript.FaviconLink], for documentUrl: URL) { + guard documentUrl != .error else { return } faviconManagement.handleFaviconLinks(faviconLinks, documentUrl: documentUrl) { favicon in guard documentUrl == self.content.url, let favicon = favicon else { return @@ -1192,7 +1279,10 @@ extension Tab/*: NavigationResponder*/ { // to be moved to Tab+Navigation.swift userInteractionDialog = nil // Unnecessary assignment triggers publishing - if error != nil { error = nil } + if error != nil, + navigation.navigationAction.navigationType != .alternateHtmlLoad { // error page navigation + error = nil + } invalidateInteractionStateData() } @@ -1212,16 +1302,47 @@ extension Tab/*: NavigationResponder*/ { // to be moved to Tab+Navigation.swift @MainActor func navigation(_ navigation: Navigation, didFailWith error: WKError) { - if navigation.isCurrent { + invalidateInteractionStateData() + + let url = error.failingUrl ?? navigation.url + if navigation.isCurrent, + !error.isFrameLoadInterrupted, !error.isNavigationCancelled, + // don‘t show an error page if the error was already handled + // (by SearchNonexistentDomainNavigationResponder) or another navigation was triggered by `setContent` + self.content.urlForWebView == url { + self.error = error + // when already displaying the error page and reload navigation fails again: don‘t navigate, just update page HTML + let shouldPerformAlternateNavigation = navigation.url != webView.url || navigation.navigationAction.targetFrame?.url != .error + loadErrorHTML(error, header: UserText.errorPageHeader, forUnreachableURL: url, alternate: shouldPerformAlternateNavigation) } - - invalidateInteractionStateData() - webViewDidFailNavigationPublisher.send() } + @MainActor func webContentProcessDidTerminate(with reason: WKProcessTerminationReason?) { - Pixel.fire(.debug(event: .webKitDidTerminate, error: NSError(domain: "WKProcessTerminated", code: reason?.rawValue ?? -1))) + let error = WKError(.webContentProcessTerminated, userInfo: [ + WKProcessTerminationReason.userInfoKey: reason?.rawValue ?? -1, + NSLocalizedDescriptionKey: UserText.webProcessCrashPageMessage, + ]) + + if case.url(let url, _, _) = content { + self.error = error + + loadErrorHTML(error, header: UserText.webProcessCrashPageHeader, forUnreachableURL: url, alternate: true) + } + + Pixel.fire(.debug(event: .webKitDidTerminate, error: error)) + } + + @MainActor + private func loadErrorHTML(_ error: WKError, header: String, forUnreachableURL url: URL, alternate: Bool) { + let html = ErrorPageHTMLTemplate(error: error, header: header).makeHTMLFromTemplate() + if alternate { + webView.loadAlternateHTML(html, baseURL: .error, forUnreachableURL: url) + } else { + // this should be updated using an error page update script call when (if) we have a dynamic error page content implemented + webView.setDocumentHtml(html) + } } } diff --git a/DuckDuckGo/Tab/Navigation/SearchNonexistentDomainNavigationResponder.swift b/DuckDuckGo/Tab/Navigation/SearchNonexistentDomainNavigationResponder.swift index b157283f5d..713f737e12 100644 --- a/DuckDuckGo/Tab/Navigation/SearchNonexistentDomainNavigationResponder.swift +++ b/DuckDuckGo/Tab/Navigation/SearchNonexistentDomainNavigationResponder.swift @@ -24,11 +24,13 @@ import Navigation final class SearchNonexistentDomainNavigationResponder { private let tld: TLD + private let setContent: (Tab.TabContent) -> Void private var lastUserEnteredValue: String? private var cancellable: AnyCancellable? - init(tld: TLD, contentPublisher: some Publisher) { + init(tld: TLD, contentPublisher: some Publisher, setContent: @escaping (Tab.TabContent) -> Void) { self.tld = tld + self.setContent = setContent cancellable = contentPublisher.sink { [weak self] tabContent in if case .url(_, credential: .none, source: .userEntered(let userEnteredValue)) = tabContent { @@ -65,7 +67,7 @@ extension SearchNonexistentDomainNavigationResponder: NavigationResponder { // redirect to SERP for non-valid domains entered by user // https://app.asana.com/0/1177771139624306/1204041033469842/f - navigation.navigationAction.targetFrame?.webView?.load(URLRequest(url: url)) + setContent(.url(url, source: .userEntered(lastUserEnteredValue))) } } diff --git a/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift b/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift index 75d2050c92..dd6f107694 100644 --- a/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift +++ b/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift @@ -77,6 +77,7 @@ typealias TabExtensionsBuilderArguments = ( isTabPinned: () -> Bool, isTabBurner: Bool, contentPublisher: AnyPublisher, + setContent: (Tab.TabContent) -> Void, titlePublisher: AnyPublisher, userScriptsPublisher: AnyPublisher, inheritedAttribution: AdClickAttributionLogic.State?, @@ -161,7 +162,7 @@ extension TabExtensionsBuilder { isBurner: args.isTabBurner) } add { - SearchNonexistentDomainNavigationResponder(tld: dependencies.privacyFeatures.contentBlocking.tld, contentPublisher: args.contentPublisher) + SearchNonexistentDomainNavigationResponder(tld: dependencies.privacyFeatures.contentBlocking.tld, contentPublisher: args.contentPublisher, setContent: args.setContent) } add { HistoryTabExtension(isBurner: args.isTabBurner, diff --git a/DuckDuckGo/Tab/TabLazyLoader/LazyLoadable.swift b/DuckDuckGo/Tab/TabLazyLoader/LazyLoadable.swift index d690530c46..20415c11e6 100644 --- a/DuckDuckGo/Tab/TabLazyLoader/LazyLoadable.swift +++ b/DuckDuckGo/Tab/TabLazyLoader/LazyLoadable.swift @@ -16,8 +16,9 @@ // limitations under the License. // -import Foundation import Combine +import Foundation +import Navigation protocol LazyLoadable: AnyObject, Identifiable { @@ -28,7 +29,8 @@ protocol LazyLoadable: AnyObject, Identifiable { var isLazyLoadingInProgress: Bool { get set } var loadingFinishedPublisher: AnyPublisher { get } - func reload() + @discardableResult + func reload() -> ExpectedNavigation? func isNewer(than other: Self) -> Bool } @@ -38,7 +40,7 @@ extension Tab: LazyLoadable { var url: URL? { content.url } var loadingFinishedPublisher: AnyPublisher { - Publishers.Merge(webViewDidFinishNavigationPublisher, webViewDidFailNavigationPublisher) + webViewDidFinishNavigationPublisher .prefix(1) .map { self } .eraseToAnyPublisher() diff --git a/DuckDuckGo/Tab/View/Base.lproj/BrowserTab.storyboard b/DuckDuckGo/Tab/View/Base.lproj/BrowserTab.storyboard deleted file mode 100644 index b21cbc5cda..0000000000 --- a/DuckDuckGo/Tab/View/Base.lproj/BrowserTab.storyboard +++ /dev/null @@ -1,121 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/DuckDuckGo/Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Tab/View/BrowserTabViewController.swift index 3854e0068e..69a680f172 100644 --- a/DuckDuckGo/Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Tab/View/BrowserTabViewController.swift @@ -24,11 +24,11 @@ import SwiftUI import BrowserServicesKit final class BrowserTabViewController: NSViewController { - @IBOutlet var errorView: NSView! - @IBOutlet var homePageView: NSView! - @IBOutlet var errorMessageLabel: NSTextField! - @IBOutlet var hoverLabel: NSTextField! - @IBOutlet var hoverLabelContainer: NSView! + + private lazy var homePageView = NSView() + private lazy var hoverLabel = NSTextField(string: URL.duckDuckGo.absoluteString) + private lazy var hoverLabelContainer = ColorView(frame: .zero, backgroundColor: .browserTabBackground, borderWidth: 0) + private weak var webView: WebView? private weak var webViewContainer: NSView? private weak var webViewSnapshot: NSView? @@ -36,11 +36,11 @@ final class BrowserTabViewController: NSViewController { var tabViewModel: TabViewModel? private let tabCollectionViewModel: TabCollectionViewModel + private let bookmarkManager: BookmarkManager private var tabContentCancellable: AnyCancellable? private var userDialogsCancellable: AnyCancellable? private var activeUserDialogCancellable: Cancellable? - private var errorViewStateCancellable: AnyCancellable? private var hoverLinkCancellable: AnyCancellable? private var pinnedTabsDelegatesCancellable: AnyCancellable? private var keyWindowSelectedTabCancellable: AnyCancellable? @@ -53,32 +53,62 @@ final class BrowserTabViewController: NSViewController { private var transientTabContentViewController: NSViewController? - static func create(tabCollectionViewModel: TabCollectionViewModel) -> BrowserTabViewController { - NSStoryboard(name: "BrowserTab", bundle: nil).instantiateInitialController { coder in - self.init(coder: coder, tabCollectionViewModel: tabCollectionViewModel) - }! - } - required init?(coder: NSCoder) { fatalError("BrowserTabViewController: Bad initializer") } - init?(coder: NSCoder, tabCollectionViewModel: TabCollectionViewModel) { + init(tabCollectionViewModel: TabCollectionViewModel, bookmarkManager: BookmarkManager = LocalBookmarkManager.shared) { self.tabCollectionViewModel = tabCollectionViewModel + self.bookmarkManager = bookmarkManager - super.init(coder: coder) + super.init(nibName: nil, bundle: nil) } - override func viewDidLoad() { - super.viewDidLoad() + override func loadView() { + view = BrowserTabView(frame: .zero, backgroundColor: .browserTabBackground) - let homePageViewController = HomePageViewController(tabCollectionViewModel: tabCollectionViewModel, bookmarkManager: LocalBookmarkManager.shared) + homePageView.translatesAutoresizingMaskIntoConstraints = false + view.addAndLayout(homePageView) + + hoverLabelContainer.cornerRadius = 4 + view.addSubview(hoverLabelContainer) + + hoverLabel.focusRingType = .none + hoverLabel.translatesAutoresizingMaskIntoConstraints = false + hoverLabel.font = .systemFont(ofSize: 13) + hoverLabel.drawsBackground = false + hoverLabel.isEditable = false + hoverLabel.isBordered = false + hoverLabel.lineBreakMode = .byClipping + hoverLabel.textColor = .labelColor + hoverLabelContainer.addSubview(hoverLabel) + + setupLayout() + + let homePageViewController = HomePageViewController(tabCollectionViewModel: tabCollectionViewModel, bookmarkManager: bookmarkManager) self.addAndLayoutChild(homePageViewController, into: homePageView) + } + + private func setupLayout() { + hoverLabelContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: -2).isActive = true + view.bottomAnchor.constraint(equalTo: hoverLabelContainer.bottomAnchor, constant: -4).isActive = true + + hoverLabel.setContentHuggingPriority(.defaultHigh, for: .vertical) + hoverLabel.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal) + hoverLabel.setContentCompressionResistancePriority(.defaultHigh, for: .vertical) + hoverLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + hoverLabelContainer.bottomAnchor.constraint(equalTo: hoverLabel.bottomAnchor, constant: 10).isActive = true + hoverLabel.leadingAnchor.constraint(equalTo: hoverLabelContainer.leadingAnchor, constant: 12).isActive = true + hoverLabelContainer.trailingAnchor.constraint(equalTo: hoverLabel.trailingAnchor, constant: 8).isActive = true + hoverLabel.topAnchor.constraint(equalTo: hoverLabelContainer.topAnchor, constant: 6).isActive = true + } + + override func viewDidLoad() { + super.viewDidLoad() hoverLabelContainer.alphaValue = 0 subscribeToTabs() subscribeToSelectedTabViewModel() - subscribeToErrorViewState() view.registerForDraggedTypes([.URL, .fileURL]) } @@ -217,7 +247,6 @@ final class BrowserTabViewController: NSViewController { generateNativePreviewIfNeeded() self.tabViewModel = selectedTabViewModel self.showTabContent(of: selectedTabViewModel) - self.subscribeToErrorViewState() self.subscribeToTabContent(of: selectedTabViewModel) self.subscribeToHoveredLink(of: selectedTabViewModel) self.subscribeToUserDialogs(of: selectedTabViewModel) @@ -294,10 +323,8 @@ final class BrowserTabViewController: NSViewController { private func addWebViewToViewHierarchy(_ webView: WebView, tab: Tab) { let container = WebViewContainerView(tab: tab, webView: webView, frame: view.bounds) self.webViewContainer = container - view.addSubview(container) - // Make sure link preview (tooltip shown in the bottom-left) is on top - view.addSubview(hoverLabelContainer) + view.addSubview(container, positioned: .below, relativeTo: hoverLabelContainer) } private func changeWebView(tabViewModel: TabViewModel?) { @@ -355,11 +382,10 @@ final class BrowserTabViewController: NSViewController { // For URL tabs, we only want to show tab content (webView) when webView starts // navigation or when another navigation-related event happens. // We take the first such event and move forward. - return Publishers.Merge5( + return Publishers.Merge4( tabViewModel.tab.webViewDidStartNavigationPublisher, tabViewModel.tab.webViewDidReceiveRedirectPublisher, tabViewModel.tab.webViewDidCommitNavigationPublisher, - tabViewModel.tab.webViewDidFailNavigationPublisher, tabViewModel.tab.webViewDidReceiveUserInteractiveChallengePublisher ) .prefix(1) @@ -387,19 +413,15 @@ final class BrowserTabViewController: NSViewController { } } - private func subscribeToErrorViewState() { - errorViewStateCancellable = tabViewModel?.$errorViewState.receive(on: DispatchQueue.main).sink { [weak self] _ in - self?.displayErrorView( - self?.tabViewModel?.errorViewState.isVisible ?? false, - message: self?.tabViewModel?.errorViewState.message ?? UserText.unknownErrorMessage - ) - } - } - func subscribeToHoveredLink(of tabViewModel: TabViewModel?) { hoverLinkCancellable = tabViewModel?.tab.hoveredLinkPublisher.sink { [weak self] in self?.scheduleHoverLabelUpdatesForUrl($0) } +#if DEBUG + if case .xcPreviews = NSApp.runType { + self.scheduleHoverLabelUpdatesForUrl(.duckDuckGo) + } +#endif } func makeWebViewFirstResponder() { @@ -427,13 +449,6 @@ final class BrowserTabViewController: NSViewController { } } - private func displayErrorView(_ shown: Bool, message: String) { - errorMessageLabel.stringValue = message - errorView.isHidden = !shown - webView?.isHidden = shown - homePageView.isHidden = shown - } - func openNewTab(with content: Tab.TabContent) { guard tabCollectionViewModel.selectDisplayableTabIfPresent(content) == false else { return @@ -543,7 +558,7 @@ final class BrowserTabViewController: NSViewController { } func generateNativePreviewIfNeeded() { - guard let tabViewModel = tabViewModel, !tabViewModel.tab.content.isUrl, !tabViewModel.errorViewState.isVisible else { + guard let tabViewModel = tabViewModel, !tabViewModel.tab.content.isUrl, !tabViewModel.isShowingErrorPage else { return } @@ -1105,3 +1120,8 @@ fileprivate extension NSView { } } + +@available(macOS 14.0, *) +#Preview { + BrowserTabViewController(tabCollectionViewModel: TabCollectionViewModel(tabCollection: TabCollection(tabs: [.init(content: .url(.duckDuckGo, source: .ui))]))) +} diff --git a/DuckDuckGo/Tab/ViewModel/TabViewModel.swift b/DuckDuckGo/Tab/ViewModel/TabViewModel.swift index 972adad69a..91803f30ac 100644 --- a/DuckDuckGo/Tab/ViewModel/TabViewModel.swift +++ b/DuckDuckGo/Tab/ViewModel/TabViewModel.swift @@ -48,15 +48,8 @@ final class TabViewModel { } @Published var progress: Double = 0.0 - struct ErrorViewState { - var isVisible: Bool = false - var message: String? - } - @Published var errorViewState = ErrorViewState() { - didSet { - updateTitle() - updateFavicon() - } + var isShowingErrorPage: Bool { + tab.error != nil } @Published var autofillDataToSave: AutofillData? @@ -76,11 +69,11 @@ final class TabViewModel { @Published private(set) var permissionAuthorizationQuery: PermissionAuthorizationQuery? var canPrint: Bool { - self.canReload && tab.webView.canPrint + !isShowingErrorPage && canReload && tab.webView.canPrint } var canSaveContent: Bool { - self.canReload && !tab.webView.isInFullScreenMode + !isShowingErrorPage && canReload && !tab.webView.isInFullScreenMode } init(tab: Tab, appearancePreferences: AppearancePreferences = .shared) { @@ -169,18 +162,10 @@ final class TabViewModel { } private func subscribeToTabError() { - tab.$error - .map { error -> ErrorViewState in - - if let error = error, !error.isFrameLoadInterrupted, !error.isNavigationCancelled { - // don‘t show error for interrupted load like downloads and for cancelled loads - return .init(isVisible: true, message: error.localizedDescription) - } else { - return .init(isVisible: false, message: nil) - } - } - .assign(to: \.errorViewState, onWeaklyHeld: self) - .store(in: &cancellables) + tab.$error.sink { [weak self] _ in + self?.updateTitle() + self?.updateFavicon() + }.store(in: &cancellables) } private func subscribeToPermissions() { @@ -209,7 +194,7 @@ final class TabViewModel { } private func updateCanBeBookmarked() { - canBeBookmarked = tab.content.url ?? .blankPage != .blankPage + canBeBookmarked = !isShowingErrorPage && (tab.content.url ?? .blankPage) != .blankPage } private var tabURL: URL? { @@ -221,14 +206,6 @@ final class TabViewModel { } func updateAddressBarStrings() { - guard !errorViewState.isVisible else { - let failingUrl = tab.error?.failingUrl - let failingUrlHost = failingUrl?.host?.droppingWwwPrefix() ?? "" - addressBarString = failingUrl?.absoluteString ?? "" - passiveAddressBarString = appearancePreferences.showFullURL ? addressBarString : failingUrlHost - return - } - guard tab.content.isUrl, let url = tabURL else { addressBarString = "" passiveAddressBarString = "" @@ -273,13 +250,12 @@ final class TabViewModel { } } - private func updateTitle() { - guard !errorViewState.isVisible else { - title = UserText.tabErrorTitle - return - } - + private func updateTitle() { // swiftlint:disable:this cyclomatic_complexity + let title: String switch tab.content { + // keep an old tab title for web page terminated page, display "Failed to open page" for loading errors + case _ where isShowingErrorPage && (tab.error?.code != .webContentProcessTerminated || tab.title == nil): + title = UserText.tabErrorTitle case .dataBrokerProtection: title = UserText.tabDataBrokerProtectionTitle case .settings: @@ -295,20 +271,22 @@ final class TabViewModel { case .onboarding: title = UserText.tabOnboardingTitle case .url, .none, .subscription: - if let title = tab.title?.trimmingWhitespace(), - !title.isEmpty { - self.title = title + if let tabTitle = tab.title?.trimmingWhitespace(), !tabTitle.isEmpty { + title = tabTitle } else if let host = tab.url?.host?.droppingWwwPrefix() { - self.title = host + title = host } else { - self.title = addressBarString + title = addressBarString } } + if self.title != title { + self.title = title + } } private func updateFavicon() { - guard !errorViewState.isVisible else { - favicon = nil + guard !isShowingErrorPage else { + favicon = .alertCircleColor16 return } diff --git a/DuckDuckGo/TabPreview/TabPreviewViewController.swift b/DuckDuckGo/TabPreview/TabPreviewViewController.swift index 264fee1153..67c846ba7e 100644 --- a/DuckDuckGo/TabPreview/TabPreviewViewController.swift +++ b/DuckDuckGo/TabPreview/TabPreviewViewController.swift @@ -57,7 +57,7 @@ extension TabPreviewViewController { urlTextField.stringValue = "" } - if !isSelected, !tabViewModel.errorViewState.isVisible, let snapshot = tabViewModel.tab.tabSnapshot { + if !isSelected, !tabViewModel.isShowingErrorPage, let snapshot = tabViewModel.tab.tabSnapshot { snapshotImageView.image = snapshot snapshotImageViewHeightConstraint.constant = getHeight(for: tabViewModel.tab.tabSnapshot) } else { diff --git a/DuckDuckGo/YoutubePlayer/DuckURLSchemeHandler.swift b/DuckDuckGo/YoutubePlayer/DuckURLSchemeHandler.swift index fe35c9f578..85565b322d 100644 --- a/DuckDuckGo/YoutubePlayer/DuckURLSchemeHandler.swift +++ b/DuckDuckGo/YoutubePlayer/DuckURLSchemeHandler.swift @@ -21,6 +21,27 @@ import WebKit final class DuckURLSchemeHandler: NSObject, WKURLSchemeHandler { + static let emptyHtml = """ + + + + + + + """ + func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) { guard let requestURL = webView.url ?? urlSchemeTask.request.url else { assertionFailure("No URL for Private Player scheme handler") @@ -29,7 +50,7 @@ final class DuckURLSchemeHandler: NSObject, WKURLSchemeHandler { guard requestURL.isDuckPlayer else { // return empty page for native UI pages navigations (like the Home page or Settings) if the request is not for the Duck Player - let data = "".utf8data + let data = Self.emptyHtml.utf8data let response = URLResponse(url: requestURL, mimeType: "text/html", diff --git a/IntegrationTests/Common/IntegrationTestsBridging.h b/IntegrationTests/Common/IntegrationTestsBridging.h new file mode 100644 index 0000000000..b2f140218b --- /dev/null +++ b/IntegrationTests/Common/IntegrationTestsBridging.h @@ -0,0 +1,21 @@ +// +// IntegrationTestsBridging.h +// +// Copyright © 2021 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 "Bridging.h" + +#import "WKURLSchemeTask+Private.h" diff --git a/IntegrationTests/Tab/ErrorPageTests.swift b/IntegrationTests/Tab/ErrorPageTests.swift new file mode 100644 index 0000000000..3b8e4134ad --- /dev/null +++ b/IntegrationTests/Tab/ErrorPageTests.swift @@ -0,0 +1,930 @@ +// +// ErrorPageTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import Common +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +@available(macOS 12.0, *) +@MainActor +class ErrorPageTests: XCTestCase { + + var window: NSWindow! + + var mainViewController: MainViewController { + (window.contentViewController as! MainViewController) + } + + var tabViewModel: TabViewModel { + mainViewController.browserTabViewController.tabViewModel! + } + + var webViewConfiguration: WKWebViewConfiguration! + var schemeHandler: TestSchemeHandler! + + static let pageTitle = "test page" + static let testHtml = "\(pageTitle)test" + static let alternativeTitle = "alternative page" + static let alternativeHtml = "\(alternativeTitle)alternative body" + + static let sessionStateData = Data.sessionRestorationMagic + """ + + + + + IsAppInitiated + + SessionHistory + + SessionHistoryVersion + 1 + SessionHistoryEntries + + + SessionHistoryEntryOriginalURL + \(URL.newtab.absoluteString) + SessionHistoryEntryTitle + + SessionHistoryEntryShouldOpenExternalURLsPolicyKey + 1 + SessionHistoryEntryData + AAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAEGPVpVAQBgAAAAAAAAAAAP////8AAAAAD2PVpVAQBgD/////AAAAAAAAAAAAAIA/AAAAAP////8= + SessionHistoryEntryURL + \(URL.newtab.absoluteString) + + + SessionHistoryEntryOriginalURL + \(URL.test.absoluteString) + SessionHistoryEntryTitle + test page + SessionHistoryEntryShouldOpenExternalURLsPolicyKey + 1 + SessionHistoryEntryData + AAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAwvZLp1AQBgAAAAAAAAAAAP////8AAAAAwfZLp1AQBgD/////AAAAAAAAAAAAAIA/AAAAAP////8= + SessionHistoryEntryURL + \(URL.test.absoluteString) + + + SessionHistoryEntryOriginalURL + \(URL.alternative.absoluteString) + SessionHistoryEntryTitle + alternative page + SessionHistoryEntryShouldOpenExternalURLsPolicyKey + 1 + SessionHistoryEntryData + AAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAeWCYp1AQBgAAAAAAAAAAAP////8AAAAAeGCYp1AQBgD/////AAAAAAAAAAAAAAAAAAAAAP////8= + SessionHistoryEntryURL + \(URL.alternative.absoluteString) + + + SessionHistoryCurrentIndex + 1 + + RenderTreeSize + 4 + + + """.utf8data + + @MainActor + override func setUp() async throws { + schemeHandler = TestSchemeHandler() + WKWebView.customHandlerSchemes = [.http, .https] + + webViewConfiguration = WKWebViewConfiguration() + // ! uncomment this to view navigation logs + // OSLog.loggingCategories.insert(OSLog.AppCategories.navigation.rawValue) + + // tests return debugDescription instead of localizedDescription + NSError.disableSwizzledDescription = true + + // mock WebView https protocol handling + webViewConfiguration.setURLSchemeHandler(schemeHandler, forURLScheme: URL.NavigationalScheme.https.rawValue) + } + + @MainActor + override func tearDown() async throws { + window?.close() + window = nil + + webViewConfiguration = nil + schemeHandler = nil + WKWebView.customHandlerSchemes = [] + + NSError.disableSwizzledDescription = false + } + + func testWhenPageFailsToLoad_errorPageShown() async throws { + // open Tab with newtab page + let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration) + let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) + window = WindowsManager.openNewWindow(with: viewModel)! + let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + try await eNewtabPageLoaded.value + + // navigate to test url, fail with error + schemeHandler.middleware = [{ _ in + .failure(NSError.hostNotFound) + }] + tab.setContent(.url(.test, source: .userEntered(URL.test.absoluteString))) + + let eNavigationFailed = tab.$error.compactMap { $0 }.timeout(5).first().promise() + let eNavigationFinished = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + + let error = try await eNavigationFailed.value + _=try await eNavigationFinished.value + + XCTAssertEqual(error.errorCode, NSError.hostNotFound.code) + XCTAssertEqual(error.localizedDescription, NSError.hostNotFound.localizedDescription) + let headerText: String? = try await tab.webView.evaluateJavaScript("document.getElementsByClassName('error-header')[0].innerText") + let errorDescr: String? = try await tab.webView.evaluateJavaScript("document.getElementsByClassName('error-description')[0].innerText") + + XCTAssertNil(tab.title) + XCTAssertEqual(tabViewModel.title, UserText.tabErrorTitle) + XCTAssertEqual(headerText?.trimmingWhitespace(), UserText.errorPageHeader) + XCTAssertEqual(errorDescr?.trimmingWhitespace(), NSError.hostNotFound.localizedDescription) + XCTAssertTrue(tab.canGoBack) + XCTAssertFalse(tab.canGoForward) + XCTAssertTrue(tab.canReload) + XCTAssertFalse(viewModel.tabViewModel(at: 0)!.canSaveContent) + XCTAssertEqual(tab.backHistoryItems.count, 1) + XCTAssertEqual(tab.backHistoryItems.first?.url, .newtab) + XCTAssertNil(tab.currentHistoryItem?.title) + XCTAssertEqual(tab.currentHistoryItem?.url, .test) + XCTAssertEqual(tab.content.userEditableUrl, .test) + } + + func testWhenTabWithNoConnectionErrorActivated_reloadTriggered() async throws { + // open 2 Tabs with newtab page + let tab1 = Tab(content: .newtab, webViewConfiguration: webViewConfiguration) + let tab2 = Tab(content: .newtab, webViewConfiguration: webViewConfiguration) + let tabsViewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab1, tab2])) + window = WindowsManager.openNewWindow(with: tabsViewModel)! + + // wait until Home page loads + let eNewtabPageLoaded = tab1.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + try await eNewtabPageLoaded.value + + // navigate to a failing url + schemeHandler.middleware = [{ _ in + .failure(NSError.noConnection) + }] + tab1.setContent(.url(.test, source: .userEntered(URL.test.absoluteString))) + // wait for error page to open + let eNavigationFailed = tab1.$error.compactMap { $0 }.timeout(5).first().promise() + + _=try await eNavigationFailed.value + + // switch to tab 2 + tabsViewModel.select(at: .unpinned(1)) + + // next load should be ok + let eServerQueried = expectation(description: "server request sent") + schemeHandler.middleware = [{ _ in + eServerQueried.fulfill() + return .ok(.html(Self.testHtml)) + }] + // coming back to the failing tab 1 should trigger its reload + let eNavigationSucceeded = tab1.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + + tabsViewModel.select(at: .unpinned(0)) + + _=try await eNavigationSucceeded.value + await fulfillment(of: [eServerQueried], timeout: 1) + XCTAssertEqual(tab1.content.url, .test) + XCTAssertNil(tab1.error) + } + + func testWhenTabWithConnectionLostErrorActivatedAndReloadFailsAgain_errorPageIsShownOnce() async throws { + // open 2 Tabs with newtab page + // navigate to a failing url right away + schemeHandler.middleware = [{ _ in + .failure(NSError.connectionLost) + }] + let tab1 = Tab(content: .url(.test, source: .link), webViewConfiguration: webViewConfiguration) + let tab2 = Tab(content: .newtab, webViewConfiguration: webViewConfiguration) + let tabsViewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab1, tab2])) + window = WindowsManager.openNewWindow(with: tabsViewModel)! + + // wait for error page to open + let eNavigationFailed = tab1.$error.compactMap { $0 }.timeout(5).first().promise() + let eNavigationFinished = tab1.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + + _=try await eNavigationFailed.value + _=try await eNavigationFinished.value + + // switch to tab 2 + tabsViewModel.select(at: .unpinned(1)) + + // coming back to the failing tab 1 should trigger its reload but it will fail again + let eServerQueried = expectation(description: "server request sent") + schemeHandler.middleware = [{ _ in + eServerQueried.fulfill() + return .failure(NSError.noConnection) + }] + let eNavigationFailed2 = tab1.$error.compactMap { $0 }.filter { + $0.errorCode == NSError.noConnection.code + }.timeout(5).first().promise() + + tabsViewModel.select(at: .unpinned(0)) + + await fulfillment(of: [eServerQueried], timeout: 1) + let error = try await eNavigationFailed2.value + + let c = tab1.$isLoading.dropFirst().sink { isLoading in + XCTFail("Failing tab shouldn‘t reload again (isLoading: \(isLoading))") + } + + XCTAssertEqual(error.errorCode, NSError.noConnection.code) + XCTAssertEqual(error.localizedDescription, NSError.noConnection.localizedDescription) + let headerText: String? = try await tab1.webView.evaluateJavaScript("document.getElementsByClassName('error-header')[0].innerText") + let errorDescr: String? = try await tab1.webView.evaluateJavaScript("document.getElementsByClassName('error-description')[0].innerText") + XCTAssertNil(tab1.title) + XCTAssertEqual(tabsViewModel.tabViewModel(at: 0)?.title, UserText.tabErrorTitle) + XCTAssertEqual(headerText?.trimmingWhitespace(), UserText.errorPageHeader) + XCTAssertEqual(errorDescr?.trimmingWhitespace(), NSError.noConnection.localizedDescription) + + try await Task.sleep(interval: 0.4) // sleep a little to confirm no more navigations are performed + withExtendedLifetime(c) {} + } + + func testWhenTabWithOtherErrorActivated_reloadNotTriggered() async throws { + // open 2 Tabs with newtab page + // navigate to a failing url right away + schemeHandler.middleware = [{ _ in + .failure(NSError.hostNotFound) + }] + let tab1 = Tab(content: .url(.test, source: .link), webViewConfiguration: webViewConfiguration) + let tab2 = Tab(content: .newtab, webViewConfiguration: webViewConfiguration) + let tabsViewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab1, tab2])) + window = WindowsManager.openNewWindow(with: tabsViewModel)! + + // wait for error page to open + let eNavigationFailed = tab1.$error.compactMap { $0 }.timeout(5).first().promise() + let errorNavigationFinished = tab1.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + _=try await eNavigationFailed.value + _=try await errorNavigationFinished.value + + // switch to tab 2 + tabsViewModel.select(at: .unpinned(1)) + + // coming back to the failing tab 1 should not trigger reload + let c = tab1.$isLoading.filter { $0 == true }.sink { isLoading in + XCTFail("Failing tab shouldn‘t reload again (isLoading: \(isLoading))") + } + tabsViewModel.select(at: .unpinned(0)) + + try await Task.sleep(interval: 0.4) // sleep a little to confirm no more navigations are performed + withExtendedLifetime(c) {} + } + + func testWhenGoingBackToFailingPage_reloadIsTriggered() async throws { + // open Tab with newtab page + // navigate to a failing url right away + schemeHandler.middleware = [{ _ in + .failure(NSError.hostNotFound) + }] + let tab = Tab(content: .url(.test, source: .link), webViewConfiguration: webViewConfiguration) + window = WindowsManager.openNewWindow(with: tab)! + + // wait for navigation to fail + let eNavigationFailed = tab.$error.compactMap { $0 }.timeout(5).first().promise() + let errorNavigationFinished = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + _=try await eNavigationFailed.value + _=try await errorNavigationFinished.value + + let ePageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + // navigate to test url: success + schemeHandler.middleware = [{ _ in + .ok(.html(Self.alternativeHtml)) + }] + tab.setContent(.url(.alternative, source: .userEntered(URL.test.absoluteString))) + + try await ePageLoaded.value + + // navigate back to failing page: success + let eBackPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + let eRequestSent = expectation(description: "request sent") + schemeHandler.middleware = [{ _ in + eRequestSent.fulfill() + return .ok(.html(Self.testHtml)) + }] + tab.goBack() + try await eBackPageLoaded.value + await fulfillment(of: [eRequestSent]) + + let titleText: String? = try await tab.webView.evaluateJavaScript("document.getElementsByTagName('title')[0].innerText") + XCTAssertEqual(titleText, Self.pageTitle) + XCTAssertEqual(tab.title, titleText) + XCTAssertEqual(tab.currentHistoryItem?.url, .test) + XCTAssertEqual(tab.currentHistoryItem?.title, titleText) + + XCTAssertEqual(tab.backHistoryItems.count, 0) + XCTAssertFalse(tab.canGoBack) + + XCTAssertEqual(tab.forwardHistoryItems.count, 1) + XCTAssertEqual(tab.forwardHistoryItems.first?.url, .alternative) + XCTAssertEqual(tab.forwardHistoryItems.first?.title, Self.alternativeTitle) + XCTAssertTrue(tab.canGoForward) + + XCTAssertTrue(tab.canReload) + } + + func testWhenGoingBackToFailingPageAndItFailsAgain_errorPageIsUpdated() async throws { + // open Tab with newtab page + // navigate to a failing url right away + schemeHandler.middleware = [{ _ in + .failure(NSError.hostNotFound) + }] + let tab = Tab(content: .url(.test, source: .link), webViewConfiguration: webViewConfiguration) + window = WindowsManager.openNewWindow(with: tab)! + + // wait for navigation to fail + let eNavigationFailed = tab.$error.compactMap { $0 }.timeout(5).first().promise() + let errorNavigationFinished = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + _=try await eNavigationFailed.value + _=try await errorNavigationFinished.value + + let ePageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + // navigate to test url: success + schemeHandler.middleware = [{ _ in + .ok(.html(Self.alternativeHtml)) + }] + tab.setContent(.url(.alternative, source: .userEntered(URL.test.absoluteString))) + + try await ePageLoaded.value + + // navigate back to failing page: failure + let eBackPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + let eNavigationFailed2 = tab.$error.compactMap { $0 }.filter { + $0.errorCode == NSError.noConnection.code + }.timeout(5).first().promise() + + schemeHandler.middleware = [{ _ in + .failure(NSError.noConnection) + }] + tab.goBack() + _=try await eNavigationFailed2.value + _=try await eBackPageLoaded.value + + let headerText: String? = try await tab.webView.evaluateJavaScript("document.getElementsByClassName('error-header')[0].innerText") + let errorDescr: String? = try await tab.webView.evaluateJavaScript("document.getElementsByClassName('error-description')[0].innerText") + XCTAssertNil(tab.title) + XCTAssertEqual(tabViewModel.title, UserText.tabErrorTitle) + XCTAssertEqual(headerText?.trimmingWhitespace(), UserText.errorPageHeader) + XCTAssertEqual(errorDescr?.trimmingWhitespace(), NSError.noConnection.localizedDescription) + + XCTAssertEqual(tab.currentHistoryItem?.url, .test) + XCTAssertNil(tab.currentHistoryItem?.title) + + XCTAssertEqual(tab.backHistoryItems.count, 0) + XCTAssertFalse(tab.canGoBack) + + XCTAssertEqual(tab.forwardHistoryItems.count, 1) + XCTAssertEqual(tab.forwardHistoryItems.first?.url, .alternative) + XCTAssertEqual(tab.forwardHistoryItems.first?.title, Self.alternativeTitle) + XCTAssertTrue(tab.canGoForward) + + XCTAssertTrue(tab.canReload) + } + + func testWhenPageLoadedAndFailsOnRefreshAndOnConsequentRefresh_errorPageIsUpdatedKeepingForwardHistory() async throws { + // open Tab with newtab page + let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration) + let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) + window = WindowsManager.openNewWindow(with: viewModel)! + let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + try await eNewtabPageLoaded.value + + // navigate to test url, success + schemeHandler.middleware = [{ _ in + .ok(.html(Self.testHtml)) + }] + let eNavigationFinished2 = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + tab.setContent(.url(.test, source: .userEntered(URL.test.absoluteString))) + _=try await eNavigationFinished2.value + + // navigate to another url, success + schemeHandler.middleware = [{ _ in + .ok(.html(Self.alternativeHtml)) + }] + let eNavigationFinished3 = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + tab.setContent(.url(.alternative, source: .userEntered(URL.alternative.absoluteString))) + _=try await eNavigationFinished3.value + + // navigate back + let eBackNavigationFinished = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + tab.goBack() + _=try await eBackNavigationFinished.value + + // refresh: fail + schemeHandler.middleware = [{ _ in + .failure(NSError.noConnection) + }] + let eNavigationFailed = tab.$error.compactMap { $0 }.timeout(5).first().promise() + let eNavigationFinished4 = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + + tab.reload() + _=try await eNavigationFailed.value + _=try await eNavigationFinished4.value + + // refresh again: fail + let eServerQueried = expectation(description: "server request sent") + schemeHandler.middleware = [{ _ in + DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { + eServerQueried.fulfill() + } + return .failure(NSError.connectionLost) + }] + let eNavigationFailed2 = tab.$error.compactMap { $0 }.timeout(5).first().promise() + + tab.reload() + _=try await eNavigationFailed2.value + await fulfillment(of: [eServerQueried]) + + let headerText: String? = try await tab.webView.evaluateJavaScript("document.getElementsByClassName('error-header')[0].innerText") + let errorDescr: String? = try await tab.webView.evaluateJavaScript("document.getElementsByClassName('error-description')[0].innerText") + XCTAssertNil(tab.title) + XCTAssertEqual(tabViewModel.title, UserText.tabErrorTitle) + XCTAssertEqual(headerText?.trimmingWhitespace(), UserText.errorPageHeader) + XCTAssertEqual(errorDescr?.trimmingWhitespace(), NSError.connectionLost.localizedDescription) + + XCTAssertEqual(tab.currentHistoryItem?.url, .test) + XCTAssertEqual(tab.currentHistoryItem?.title, URL.test.host) + + XCTAssertEqual(tab.backHistoryItems.count, 1) + XCTAssertEqual(tab.backHistoryItems.first?.url, .newtab, "url") + XCTAssertTrue(tab.canGoBack) + + XCTAssertEqual(tab.forwardHistoryItems.count, 1) + XCTAssertEqual(tab.forwardHistoryItems.first?.url, .alternative, "url") + XCTAssertEqual(tab.forwardHistoryItems.first?.title, Self.alternativeTitle, "title") + XCTAssertTrue(tab.canGoForward) + + XCTAssertTrue(tab.canReload) + } + + func testWhenPageLoadedAndFailsOnRefreshAndSucceedsOnConsequentRefresh_forwardHistoryIsPreserved() async throws { + // open Tab with newtab page + let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration) + let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) + window = WindowsManager.openNewWindow(with: viewModel)! + let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + try await eNewtabPageLoaded.value + + // navigate to test url, success + schemeHandler.middleware = [{ _ in + .ok(.html(Self.testHtml)) + }] + let eNavigationFinished2 = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + tab.setContent(.url(.test, source: .userEntered(URL.test.absoluteString))) + _=try await eNavigationFinished2.value + + // navigate to another url, success + schemeHandler.middleware = [{ _ in + .ok(.html(Self.alternativeHtml)) + }] + let eNavigationFinished3 = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + tab.setContent(.url(.alternative, source: .userEntered(URL.alternative.absoluteString))) + _=try await eNavigationFinished3.value + + // navigate back + let eBackNavigationFinished = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + tab.goBack() + _=try await eBackNavigationFinished.value + + // refresh: fail + schemeHandler.middleware = [{ _ in + .failure(NSError.noConnection) + }] + let eNavigationFailed = tab.$error.compactMap { $0 }.timeout(5).first().promise() + let eNavigationFinished4 = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + + tab.reload() + _=try await eNavigationFailed.value + _=try await eNavigationFinished4.value + + // refresh again: success + schemeHandler.middleware = [{ _ in + .ok(.html(Self.testHtml)) + }] + let eNavigationFinished5 = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + + tab.reload() + _=try await eNavigationFinished5.value + + let titleText: String? = try await tab.webView.evaluateJavaScript("document.getElementsByTagName('title')[0].innerText") + XCTAssertEqual(tab.title, Self.pageTitle) + XCTAssertEqual(titleText?.trimmingWhitespace(), tab.title) + + XCTAssertEqual(tab.currentHistoryItem?.url, .test) + XCTAssertEqual(tab.currentHistoryItem?.title, Self.pageTitle) + + XCTAssertEqual(tab.backHistoryItems.count, 1) + XCTAssertEqual(tab.backHistoryItems.first?.url, .newtab) + XCTAssertTrue(tab.canGoBack) + + XCTAssertEqual(tab.forwardHistoryItems.count, 1) + XCTAssertEqual(tab.forwardHistoryItems.first?.url, .alternative) + XCTAssertEqual(tab.forwardHistoryItems.first?.title, Self.alternativeTitle) + XCTAssertTrue(tab.canGoForward) + + XCTAssertTrue(tab.canReload) + } + + func testWhenReloadingBySubmittingSameURL_errorPageRemainsSame() async throws { + // open Tab with newtab page + let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration) + let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) + window = WindowsManager.openNewWindow(with: viewModel)! + let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + try await eNewtabPageLoaded.value + + // navigate to test url, success + schemeHandler.middleware = [{ _ in + .ok(.html(Self.testHtml)) + }] + let eNavigationFinished2 = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + tab.setContent(.url(.test, source: .userEntered(URL.test.absoluteString))) + _=try await eNavigationFinished2.value + + // navigate to another url, success + schemeHandler.middleware = [{ _ in + .ok(.html(Self.alternativeHtml)) + }] + let eNavigationFinished3 = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + tab.setContent(.url(.alternative, source: .userEntered(URL.alternative.absoluteString))) + _=try await eNavigationFinished3.value + + // navigate back + let eBackNavigationFinished = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + tab.goBack() + _=try await eBackNavigationFinished.value + + // refresh: fail + schemeHandler.middleware = [{ _ in + .failure(NSError.noConnection) + }] + let eNavigationFailed = tab.$error.compactMap { $0 }.timeout(5).first().promise() + let eNavigationFinished4 = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + + tab.setContent(.url(.test, source: .userEntered(URL.test.absoluteString))) + _=try await eNavigationFailed.value + _=try await eNavigationFinished4.value + + // refresh again: fail + let eServerQueried = expectation(description: "server request sent") + schemeHandler.middleware = [{ _ in + eServerQueried.fulfill() + return .failure(NSError.connectionLost) + }] + let eNavigationFailed2 = tab.$error.compactMap { $0 }.timeout(5).first().promise() + + tab.setContent(.url(.test, source: .userEntered(URL.test.absoluteString))) + _=try await eNavigationFailed2.value + await fulfillment(of: [eServerQueried]) + + let headerText: String? = try await tab.webView.evaluateJavaScript("document.getElementsByClassName('error-header')[0].innerText") + let errorDescr: String? = try await tab.webView.evaluateJavaScript("document.getElementsByClassName('error-description')[0].innerText") + XCTAssertNil(tab.title) + XCTAssertEqual(tabViewModel.title, UserText.tabErrorTitle) + XCTAssertEqual(headerText?.trimmingWhitespace(), UserText.errorPageHeader) + XCTAssertEqual(errorDescr?.trimmingWhitespace(), NSError.connectionLost.localizedDescription) + + XCTAssertEqual(tab.currentHistoryItem?.url, .test) + XCTAssertEqual(tab.currentHistoryItem?.title, URL.test.host) + + XCTAssertEqual(tab.backHistoryItems.count, 1) + XCTAssertEqual(tab.backHistoryItems.first?.url, .newtab, "url") + XCTAssertNil(tab.backHistoryItems.first?.title, "title") + XCTAssertTrue(tab.canGoBack) + + XCTAssertEqual(tab.forwardHistoryItems.count, 1) + XCTAssertEqual(tab.forwardHistoryItems.first?.url, .alternative, "url") + XCTAssertEqual(tab.forwardHistoryItems.first?.title, Self.alternativeTitle, "title") + XCTAssertTrue(tab.canGoForward) + + XCTAssertTrue(tab.canReload) + } + + func testWhenGoingToAnotherUrlFails_newBackForwardHistoryItemIsAdded() async throws { + // open Tab with newtab page + let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration) + let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) + window = WindowsManager.openNewWindow(with: viewModel)! + let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + try await eNewtabPageLoaded.value + + // navigate to test url, success + schemeHandler.middleware = [{ _ in + .ok(.html(Self.testHtml)) + }] + let eNavigationFinished2 = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + tab.setContent(.url(.test, source: .userEntered(URL.test.absoluteString))) + _=try await eNavigationFinished2.value + + // navigate to another url, success + schemeHandler.middleware = [{ _ in + .ok(.html(Self.alternativeHtml)) + }] + let eNavigationFinished3 = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + tab.setContent(.url(.alternative, source: .userEntered(URL.alternative.absoluteString))) + _=try await eNavigationFinished3.value + + // navigate back + let eBackNavigationFinished = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + tab.goBack() + _=try await eBackNavigationFinished.value + + // refresh: fail + schemeHandler.middleware = [{ _ in + .failure(NSError.noConnection) + }] + let eNavigationFailed = tab.$error.compactMap { $0 }.timeout(5).first().promise() + let eNavigationFinished4 = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + + tab.setContent(.url(.test, source: .userEntered(URL.test.absoluteString))) + _=try await eNavigationFailed.value + _=try await eNavigationFinished4.value + + // go to another url: fail + let eServerQueried = expectation(description: "server request sent") + schemeHandler.middleware = [{ _ in + eServerQueried.fulfill() + return .failure(NSError.connectionLost) + }] + let eNavigationFailed2 = tab.$error.compactMap { $0 }.timeout(5).first().promise() + + tab.setContent(.url(.alternative, source: .userEntered(URL.alternative.absoluteString))) + _=try await eNavigationFailed2.value + await fulfillment(of: [eServerQueried]) + + let headerText: String? = try await tab.webView.evaluateJavaScript("document.getElementsByClassName('error-header')[0].innerText") + let errorDescr: String? = try await tab.webView.evaluateJavaScript("document.getElementsByClassName('error-description')[0].innerText") + XCTAssertNil(tab.title) + XCTAssertEqual(tabViewModel.title, UserText.tabErrorTitle) + XCTAssertEqual(headerText?.trimmingWhitespace(), UserText.errorPageHeader) + XCTAssertEqual(errorDescr?.trimmingWhitespace(), NSError.connectionLost.localizedDescription) + + XCTAssertEqual(tab.currentHistoryItem?.url, .alternative) + XCTAssertNil(tab.currentHistoryItem?.title) + + XCTAssertEqual(tab.backHistoryItems.count, 2) + XCTAssertEqual(tab.backHistoryItems[safe: 0]?.url, .newtab) + XCTAssertNil(tab.backHistoryItems[safe: 0]?.title) + XCTAssertEqual(tab.backHistoryItems[safe: 1]?.url, .test) + XCTAssertEqual(tab.backHistoryItems[safe: 1]?.title, Self.pageTitle) + XCTAssertTrue(tab.canGoBack) + + XCTAssertEqual(tab.forwardHistoryItems.count, 0) + XCTAssertFalse(tab.canGoForward) + + XCTAssertTrue(tab.canReload) + } + + func testWhenGoingToAnotherUrlSucceeds_newBackForwardHistoryItemIsAdded() async throws { + // open Tab with newtab page + let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration) + let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) + window = WindowsManager.openNewWindow(with: viewModel)! + let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + try await eNewtabPageLoaded.value + + // navigate to test url, success + schemeHandler.middleware = [{ _ in + .ok(.html(Self.testHtml)) + }] + let eNavigationFinished2 = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + tab.setContent(.url(.test, source: .userEntered(URL.test.absoluteString))) + _=try await eNavigationFinished2.value + + // navigate to another url, success + schemeHandler.middleware = [{ _ in + .ok(.html(Self.alternativeHtml)) + }] + let eNavigationFinished3 = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + tab.setContent(.url(.alternative, source: .userEntered(URL.alternative.absoluteString))) + _=try await eNavigationFinished3.value + + // navigate back + let eBackNavigationFinished = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + tab.goBack() + _=try await eBackNavigationFinished.value + + // refresh: fail + schemeHandler.middleware = [{ _ in + .failure(NSError.noConnection) + }] + let eNavigationFailed = tab.$error.compactMap { $0 }.timeout(5).first().promise() + let eNavigationFinished4 = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + + tab.setContent(.url(.test, source: .userEntered(URL.test.absoluteString))) + _=try await eNavigationFailed.value + _=try await eNavigationFinished4.value + + // go to another url: success + schemeHandler.middleware = [{ _ in + .ok(.html(Self.alternativeHtml)) + }] + let eNavigationFinished5 = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + + tab.setContent(.url(.alternative, source: .userEntered(URL.alternative.absoluteString))) + _=try await eNavigationFinished5.value + + let titleText: String? = try await tab.webView.evaluateJavaScript("document.getElementsByTagName('title')[0].innerText") + XCTAssertEqual(tab.title, Self.alternativeTitle) + XCTAssertEqual(titleText?.trimmingWhitespace(), tab.title) + + XCTAssertEqual(tab.currentHistoryItem?.url, .alternative) + XCTAssertEqual(tab.currentHistoryItem?.title, Self.alternativeTitle) + + XCTAssertEqual(tab.backHistoryItems.count, 2) + XCTAssertEqual(tab.backHistoryItems[safe: 0]?.url, .newtab) + XCTAssertNil(tab.backHistoryItems[safe: 0]?.title) + XCTAssertEqual(tab.backHistoryItems[safe: 1]?.url, .test) + XCTAssertEqual(tab.backHistoryItems[safe: 1]?.title, Self.pageTitle) + XCTAssertTrue(tab.canGoBack) + + XCTAssertEqual(tab.forwardHistoryItems.count, 0) + XCTAssertFalse(tab.canGoForward) + + XCTAssertTrue(tab.canReload) + } + + func testWhenLoadingFailsAfterSessionRestoration_navigationHistoryIsPreserved() async throws { + schemeHandler.middleware = [{ _ in + .failure(NSError.noConnection) + }] + + let tab = Tab(content: .url(.test, source: .pendingStateRestoration), webViewConfiguration: webViewConfiguration, interactionStateData: Self.sessionStateData) + let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) + window = WindowsManager.openNewWindow(with: viewModel)! + let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + try await eNewtabPageLoaded.value + + XCTAssertTrue(tab.canReload) + + schemeHandler.middleware = [{ _ in + .ok(.html(Self.testHtml)) + }] + // open new tab + viewModel.appendNewTab() + + // select the failing tab triggering its reload + let eReloadFinished = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + viewModel.select(at: .unpinned(0)) + _=try await eReloadFinished.value + + XCTAssertEqual(tab.currentHistoryItem?.url, .test) + XCTAssertEqual(tab.currentHistoryItem?.title, Self.pageTitle) + + XCTAssertEqual(tab.backHistoryItems.count, 1) + XCTAssertEqual(tab.backHistoryItems.first?.url, .newtab) + XCTAssertEqual(tab.backHistoryItems.first?.title ?? "", "") + XCTAssertTrue(tab.canGoBack) + + XCTAssertEqual(tab.forwardHistoryItems.count, 1) + XCTAssertEqual(tab.forwardHistoryItems.first?.url, .alternative) + XCTAssertEqual(tab.forwardHistoryItems.first?.title, Self.alternativeTitle) + XCTAssertTrue(tab.canGoForward) + + XCTAssertTrue(tab.canReload) + } + + func testPinnedTabDoesNotNavigateAway() async throws { + schemeHandler.middleware = [{ _ in + .ok(.html(Self.testHtml)) + }] + + let tab = Tab(content: .url(.alternative, source: .ui), webViewConfiguration: webViewConfiguration) + let manager = PinnedTabsManager() + manager.pin(tab) + + let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: []), pinnedTabsManager: manager) + window = WindowsManager.openNewWindow(with: viewModel)! + viewModel.select(at: .pinned(0)) + + // wait for tab to load + let eNavigationFinished = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + _=try await eNavigationFinished.value + + // refresh: fail + schemeHandler.middleware = [{ _ in + .failure(NSError.noConnection) + }] + let eNavigationFailed = tab.$error.compactMap { $0 }.timeout(5).first().promise() + let eNavigationFinished2 = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + + tab.reload() + _=try await eNavigationFailed.value + _=try await eNavigationFinished2.value + + XCTAssertNotNil(tab.error) + + schemeHandler.middleware = [{ _ in + .ok(.html(Self.testHtml)) + }] + let eNavigationFinished5 = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + + tab.reload() + _=try await eNavigationFinished5.value + + XCTAssertNil(tab.error) + XCTAssertEqual(viewModel.tabs.count, 1) + } + + func testWhenPageFailsToLoadAfterRedirect_errorPageShown() async throws { + // open Tab with newtab page + let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration) + let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) + window = WindowsManager.openNewWindow(with: viewModel)! + let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + try await eNewtabPageLoaded.value + + // navigate to alt url, redirect to test url, fail with error + schemeHandler.middleware = [{ request in + .init { task in + let response = URLResponse(url: request.url!, mimeType: nil, expectedContentLength: 0, textEncodingName: nil) + let newRequest = URLRequest(url: .test) + task._didPerformRedirection(response, newRequest: newRequest) + + task.didFailWithError(NSError.hostNotFound) + } + }] + tab.setContent(.url(.test, source: .userEntered(URL.alternative.absoluteString))) + + let eNavigationFailed = tab.$error.compactMap { $0 }.timeout(5).first().promise() + let eNavigationFinished = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + + let error = try await eNavigationFailed.value + _=try await eNavigationFinished.value + + XCTAssertEqual(error.errorCode, NSError.hostNotFound.code) + XCTAssertEqual(error.localizedDescription, NSError.hostNotFound.localizedDescription) + let headerText: String? = try await tab.webView.evaluateJavaScript("document.getElementsByClassName('error-header')[0].innerText") + let errorDescr: String? = try await tab.webView.evaluateJavaScript("document.getElementsByClassName('error-description')[0].innerText") + + XCTAssertNil(tab.title) + XCTAssertEqual(tabViewModel.title, UserText.tabErrorTitle) + XCTAssertEqual(headerText?.trimmingWhitespace(), UserText.errorPageHeader) + XCTAssertEqual(errorDescr?.trimmingWhitespace(), NSError.hostNotFound.localizedDescription) + XCTAssertTrue(tab.canGoBack) + XCTAssertFalse(tab.canGoForward) + XCTAssertTrue(tab.canReload) + XCTAssertFalse(viewModel.tabViewModel(at: 0)!.canSaveContent) + XCTAssertEqual(tab.backHistoryItems.count, 1) + XCTAssertEqual(tab.backHistoryItems.first?.url, .newtab) + XCTAssertNil(tab.currentHistoryItem?.title) + XCTAssertEqual(tab.currentHistoryItem?.url, .test) + XCTAssertEqual(tab.content.userEditableUrl, .test) + } + +} + +private extension URL { + static let test = URL(string: "https://test.com/")! + static let alternative = URL(string: "https://alternative.com/")! +} + +private extension NSError { + + static let hostNotFound: NSError = { + let errorCode = -1003 + let errorDescription = "hostname not found" + let wkError = NSError(domain: NSURLErrorDomain, code: errorCode, userInfo: [NSLocalizedDescriptionKey: errorDescription]) + return wkError + }() + + static let noConnection: NSError = { + let errorDescription = "no internet connection" + return URLError(.notConnectedToInternet, userInfo: [NSLocalizedDescriptionKey: errorDescription]) as NSError + }() + + static let connectionLost: NSError = { + let errorDescription = "connection lost" + return URLError(.networkConnectionLost, userInfo: [NSLocalizedDescriptionKey: errorDescription]) as NSError + }() + +} + +extension Data { + + static let sessionRestorationMagic = Data([0x00, 0x00, 0x00, 0x02]) + +} diff --git a/IntegrationTests/TabExtensions/SearchNonexistentDomainTests.swift b/IntegrationTests/Tab/SearchNonexistentDomainTests.swift similarity index 77% rename from IntegrationTests/TabExtensions/SearchNonexistentDomainTests.swift rename to IntegrationTests/Tab/SearchNonexistentDomainTests.swift index 778ee77770..346231ede7 100644 --- a/IntegrationTests/TabExtensions/SearchNonexistentDomainTests.swift +++ b/IntegrationTests/Tab/SearchNonexistentDomainTests.swift @@ -92,28 +92,27 @@ final class SearchNonexistentDomainTests: XCTestCase { let tab = Tab(content: .none, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) window = WindowsManager.openNewWindow(with: tab)! - let eRedirected = Future { promise in - self.schemeHandler.middleware = [{ request in - if request.url!.isDuckDuckGoSearch { - promise(.success(request.url!)) - return .ok(.html("")) - } else { - return .failure(NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotFindHost)) - } - }] - }.timeout(3).first().promise() - let url = urls.invalidTLD let enteredString = url.absoluteString.dropping(prefix: url.navigationalScheme!.separated()) + let eRedirected = expectation(description: "Redirected to SERP") + self.schemeHandler.middleware = [{ request in + if request.url!.isDuckDuckGoSearch { + XCTAssertEqual(request.url, URL.makeSearchUrl(from: enteredString)) + eRedirected.fulfill() + return .ok(.html("")) + } else { + return .failure(NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotFindHost)) + } + }] + addressBar.makeMeFirstResponder() addressBar.stringValue = enteredString NSApp.swizzled_currentEvent = NSEvent.keyEvent(with: .keyDown, location: .zero, modifierFlags: [], timestamp: Date().timeIntervalSinceReferenceDate, windowNumber: 0, context: nil, characters: "\n", charactersIgnoringModifiers: "", isARepeat: false, keyCode: UInt16(kVK_Return))! _=addressBar.control(addressBar, textView: addressBar.currentEditor() as! NSTextView, doCommandBy: #selector(NSResponder.insertNewline)) - let redirectUrl = try await eRedirected.value - XCTAssertEqual(redirectUrl, URL.makeSearchUrl(from: enteredString)) + await fulfillment(of: [eRedirected], timeout: 3) } @MainActor @@ -121,14 +120,20 @@ final class SearchNonexistentDomainTests: XCTestCase { let tab = Tab(content: .none, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) window = WindowsManager.openNewWindow(with: tab)! - self.schemeHandler.middleware = [{ _ in - .failure(NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotFindHost)) + self.schemeHandler.middleware = [{ request in + XCTAssertFalse(request.url!.isDuckDuckGoSearch) + return .failure(NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotFindHost)) }] let eNavigationFailed = tab.$error .compactMap { $0 } .timeout(3) .first() .promise() + // error page navigation + let eNavigationDidFinish = tab.webViewDidFinishNavigationPublisher + .timeout(3) + .first() + .promise() let url = urls.validTLD let enteredString = url.absoluteString.dropping(prefix: url.navigationalScheme!.separated()) @@ -140,6 +145,7 @@ final class SearchNonexistentDomainTests: XCTestCase { _=addressBar.control(addressBar, textView: addressBar.currentEditor() as! NSTextView, doCommandBy: #selector(NSResponder.insertNewline)) let error = try await eNavigationFailed.value + _=try await eNavigationDidFinish.value XCTAssertEqual(error.errorCode, NSURLErrorCannotFindHost) } @@ -148,14 +154,20 @@ final class SearchNonexistentDomainTests: XCTestCase { let tab = Tab(content: .none, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) window = WindowsManager.openNewWindow(with: tab)! - self.schemeHandler.middleware = [{ _ in - .failure(NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotFindHost)) + self.schemeHandler.middleware = [{ request in + XCTAssertFalse(request.url!.isDuckDuckGoSearch) + return .failure(NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotFindHost)) }] let eNavigationFailed = tab.$error .compactMap { $0 } .timeout(3) .first() .promise() + // error page navigation + let eNavigationDidFinish = tab.webViewDidFinishNavigationPublisher + .timeout(3) + .first() + .promise() let url = urls.invalidTLD let enteredString = url.absoluteString @@ -167,6 +179,7 @@ final class SearchNonexistentDomainTests: XCTestCase { _=addressBar.control(addressBar, textView: addressBar.currentEditor() as! NSTextView, doCommandBy: #selector(NSResponder.insertNewline)) let error = try await eNavigationFailed.value + _=try await eNavigationDidFinish.value XCTAssertEqual(error.errorCode, NSURLErrorCannotFindHost) } @@ -175,11 +188,17 @@ final class SearchNonexistentDomainTests: XCTestCase { let tab = Tab(content: .none, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) window = WindowsManager.openNewWindow(with: tab)! - self.schemeHandler.middleware = [{ _ in - .failure(NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotFindHost)) + self.schemeHandler.middleware = [{ request in + XCTAssertFalse(request.url!.isDuckDuckGoSearch) + return .failure(NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotFindHost)) }] let eNavigationFailed = tab.$error .compactMap { $0 } + .timeout(3) + .first() + .promise() + // error page navigation + let eNavigationDidFinish = tab.webViewDidFinishNavigationPublisher .timeout(10) .first() .promise() @@ -189,6 +208,7 @@ final class SearchNonexistentDomainTests: XCTestCase { tab.setUrl(url, source: .link) let error = try await eNavigationFailed.value + _=try await eNavigationDidFinish.value XCTAssertEqual(error.errorCode, NSURLErrorCannotFindHost) } @@ -197,20 +217,21 @@ final class SearchNonexistentDomainTests: XCTestCase { let tab = Tab(content: .none, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) window = WindowsManager.openNewWindow(with: tab)! - let eRedirected = Future { promise in - self.schemeHandler.middleware = [{ request in - if request.url!.isDuckDuckGoSearch { - promise(.success(request.url!)) - return .ok(.html("")) - } else { - return .failure(NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotFindHost)) - } - }] - }.timeout(3).first().promise() - let url = urls.invalidTLD let enteredString = url.absoluteString.dropping(prefix: url.navigationalScheme!.separated()) + let eRedirected = expectation(description: "Redirected to SERP") + self.schemeHandler.middleware = [{ request in + if request.url!.isDuckDuckGoSearch { + XCTAssertEqual(request.url, URL.makeSearchUrl(from: enteredString)) + eRedirected.fulfill() + + return .ok(.html("")) + } else { + return .failure(NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotFindHost)) + } + }] + addressBar.makeMeFirstResponder() addressBar.stringValue = enteredString @@ -223,8 +244,7 @@ final class SearchNonexistentDomainTests: XCTestCase { addressBar.suggestionViewControllerDidConfirmSelection(addressBar.suggestionViewController) - let redirectUrl = try await eRedirected.value - XCTAssertEqual(redirectUrl, URL.makeSearchUrl(from: enteredString)) + await fulfillment(of: [eRedirected], timeout: 3) } } diff --git a/UnitTests/Common/Extensions/StringExtensionTests.swift b/UnitTests/Common/Extensions/StringExtensionTests.swift new file mode 100644 index 0000000000..76a5b0fd58 --- /dev/null +++ b/UnitTests/Common/Extensions/StringExtensionTests.swift @@ -0,0 +1,44 @@ +// +// StringExtensionTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +class StringExtensionTests: XCTestCase { + + func testHtmlEscapedString() { + NSError.disableSwizzledDescription = true + defer { NSError.disableSwizzledDescription = false } + + XCTAssertEqual("\"DuckDuckGo\"®".escapedUnicodeHtmlString(), ""DuckDuckGo"®") + XCTAssertEqual("i don‘t want to 'sleep'™".escapedUnicodeHtmlString(), "i don‘t want to 'sleep'™") + XCTAssertEqual("{ $embraced [&text]}".escapedUnicodeHtmlString(), "{ $embraced [&text]}") + XCTAssertEqual("X ^ 2 + y / 2 = 4 < 6%".escapedUnicodeHtmlString(), "X ^ 2 + y / 2 = 4 < 6%") + XCTAssertEqual("".escapedUnicodeHtmlString(), "<some&tag>") + XCTAssertEqual("© “text” with «emojis» 🩷🦆".escapedUnicodeHtmlString(), "© “text” with «emojis» 🩷🦆") + XCTAssertEqual("`my.mail@duck.com`".escapedUnicodeHtmlString(), "amy.mail@duck.coma") + XCTAssertEqual("floop!burp".escapedUnicodeHtmlString(), + "<hey beep=\"#test\" boop='#' fool=1 >floop!<b>burp</b></hey>") + + XCTAssertEqual(URLError(URLError.Code.cannotConnectToHost, userInfo: [NSLocalizedDescriptionKey: "Could not connect to the server."]).localizedDescription.escapedUnicodeHtmlString(), "Could not connect to the server.") + XCTAssertEqual(URLError(URLError.Code.cannotConnectToHost).localizedDescription.escapedUnicodeHtmlString(), "The operation couldn’t be completed. (NSURLErrorDomain error -1004.)") + XCTAssertEqual(URLError(URLError.Code.cannotFindHost).localizedDescription.escapedUnicodeHtmlString(), "The operation couldn’t be completed. (NSURLErrorDomain error -1003.)") + } + +} diff --git a/UnitTests/Common/NSErrorAdditionalInfo.swift b/UnitTests/Common/NSErrorAdditionalInfo.swift index 7b8e6e0a31..6f861ae629 100644 --- a/UnitTests/Common/NSErrorAdditionalInfo.swift +++ b/UnitTests/Common/NSErrorAdditionalInfo.swift @@ -27,8 +27,17 @@ extension NSError { method_exchangeImplementations(originalLocalizedDescription, swizzledLocalizedDescription) }() - @objc func swizzledLocalizedDescription() -> String { - self.debugDescription + // use `NSError.disableSwizzledDescription = true` to return an original localizedDescription, don‘t forget to set it back in tearDown + @objc dynamic func swizzledLocalizedDescription() -> String { + if Self.disableSwizzledDescription { + self.swizzledLocalizedDescription() // return original + } else { + self.debugDescription + " – NSErrorAdditionalInfo.swift" + } } + private static let disableSwizzledDescriptionKey = UnsafeRawPointer(bitPattern: "disableSwizzledDescriptionKey".hashValue)! + + static var disableSwizzledDescription: Bool = false + } diff --git a/UnitTests/Common/TestsBridging.h b/UnitTests/Common/TestsBridging.h index 2d5e4175c3..b2184c97d8 100644 --- a/UnitTests/Common/TestsBridging.h +++ b/UnitTests/Common/TestsBridging.h @@ -19,3 +19,4 @@ #import "Bridging.h" #import "DownloadsWebViewMock.h" +#import "WKURLSchemeTask+Private.h" diff --git a/UnitTests/Common/WKURLSchemeTask+Private.h b/UnitTests/Common/WKURLSchemeTask+Private.h new file mode 100644 index 0000000000..2264f27d76 --- /dev/null +++ b/UnitTests/Common/WKURLSchemeTask+Private.h @@ -0,0 +1,30 @@ +// +// WKURLSchemeTask+Private.h +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol WKURLSchemeTaskPrivate + +- (void)_willPerformRedirection:(NSURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest *))completionHandler; +- (void)_didPerformRedirection:(NSURLResponse *)response newRequest:(NSURLRequest *)request; + +@end + +NS_ASSUME_NONNULL_END diff --git a/UnitTests/Common/WKWebViewMockingExtension.swift b/UnitTests/Common/WKWebViewMockingExtension.swift index aa5e512c39..2824714d35 100644 --- a/UnitTests/Common/WKWebViewMockingExtension.swift +++ b/UnitTests/Common/WKWebViewMockingExtension.swift @@ -53,7 +53,7 @@ class TestSchemeHandler: NSObject, WKURLSchemeHandler { func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) { for middleware in middleware { if let handler = middleware(urlSchemeTask.request) { - handler(urlSchemeTask) + handler(urlSchemeTask as! WKURLSchemeTaskPrivate) return } } @@ -103,12 +103,12 @@ struct WKURLSchemeTaskHandler { } } - let handler: (WKURLSchemeTask) -> Void - init(handler: @escaping (WKURLSchemeTask) -> Void) { + let handler: (WKURLSchemeTaskPrivate) -> Void + init(handler: @escaping (WKURLSchemeTaskPrivate) -> Void) { self.handler = handler } - func callAsFunction(_ task: WKURLSchemeTask) { + func callAsFunction(_ task: WKURLSchemeTaskPrivate) { handler(task) } diff --git a/UnitTests/DataImport/DataImportViewModelTests.swift b/UnitTests/DataImport/DataImportViewModelTests.swift index 147784dcb3..6d698185e4 100644 --- a/UnitTests/DataImport/DataImportViewModelTests.swift +++ b/UnitTests/DataImport/DataImportViewModelTests.swift @@ -36,6 +36,7 @@ import XCTest model = nil importTask = nil openPanelCallback = nil + NSError.disableSwizzledDescription = false } // MARK: - Tests @@ -1514,6 +1515,8 @@ import XCTest // MARK: - Feedback func testFeedbackSending() { + NSError.disableSwizzledDescription = true + let summary: [DataImportViewModel.DataTypeImportResult] = [ .init(.bookmarks, .success(.empty)), .init(.bookmarks, .failure(Failure(.passwords, .dataCorrupted))), diff --git a/UnitTests/Tab/Model/TabTests.swift b/UnitTests/Tab/Model/TabTests.swift index f366cc4ba7..faf05897e4 100644 --- a/UnitTests/Tab/Model/TabTests.swift +++ b/UnitTests/Tab/Model/TabTests.swift @@ -269,8 +269,8 @@ final class TabTests: XCTestCase { XCTAssertTrue(tab.canGoBack) XCTAssertFalse(tab.canGoForward) XCTAssertEqual(tab.webView.url, urls.url3) - XCTAssertEqual(tab.webView.backForwardList.backList.map(\.url), [urls.url]) - XCTAssertEqual(tab.webView.backForwardList.forwardList, []) + XCTAssertEqual(tab.backHistoryItems.map(\.url), [urls.url]) + XCTAssertEqual(tab.forwardHistoryItems, []) withExtendedLifetime((c1, c2)) {} } @@ -344,8 +344,8 @@ final class TabTests: XCTestCase { XCTAssertTrue(tab.canGoBack) XCTAssertFalse(tab.canGoForward) XCTAssertEqual(tab.webView.url, urls.url3) - XCTAssertEqual(tab.webView.backForwardList.backList.map(\.url), [urls.url, urls.url3]) - XCTAssertEqual(tab.webView.backForwardList.forwardList, []) + XCTAssertEqual(tab.backHistoryItems.map(\.url), [urls.url, urls.url3]) + XCTAssertEqual(tab.forwardHistoryItems, []) withExtendedLifetime((c1, c2)) {} } diff --git a/UnitTests/Tab/ViewModel/TabViewModelTests.swift b/UnitTests/Tab/ViewModel/TabViewModelTests.swift index 5f0029ad42..9d14bf585a 100644 --- a/UnitTests/Tab/ViewModel/TabViewModelTests.swift +++ b/UnitTests/Tab/ViewModel/TabViewModelTests.swift @@ -134,18 +134,23 @@ final class TabViewModelTests: XCTestCase { XCTAssertEqual(tabViewModel.title, "New Tab") } - func testWhenTabTitleIsNotNilThenTitleReflectsTabTitle() { + func testWhenTabTitleIsNotNilThenTitleReflectsTabTitle() async throws { let tabViewModel = TabViewModel.forTabWithURL(.duckDuckGo) let testTitle = "Test title" - tabViewModel.tab.title = testTitle let titleExpectation = expectation(description: "Title") - - tabViewModel.$title.debounce(for: 0.1, scheduler: RunLoop.main).sink { title in + tabViewModel.$title.dropFirst().sink { + if case .failure(let error) = $0 { + XCTFail("\(error)") + } + } receiveValue: { title in XCTAssertEqual(title, testTitle) titleExpectation.fulfill() } .store(in: &cancellables) - waitForExpectations(timeout: 1, handler: nil) + + tabViewModel.tab.title = testTitle + + await fulfillment(of: [titleExpectation], timeout: 0.5) } func testWhenTabTitleIsNilThenTitleIsAddressBarString() { @@ -153,7 +158,7 @@ final class TabViewModelTests: XCTestCase { let titleExpectation = expectation(description: "Title") - tabViewModel.$title.debounce(for: 0.1, scheduler: RunLoop.main).sink { title in + tabViewModel.$title.debounce(for: 0.01, scheduler: RunLoop.main).sink { title in XCTAssertEqual(title, URL.duckDuckGo.host!) titleExpectation.fulfill() } .store(in: &cancellables) diff --git a/UnitTests/Tab/WKWebViewPrivateMethodsAvailabilityTests.swift b/UnitTests/Tab/WKWebViewPrivateMethodsAvailabilityTests.swift index b5c840aa5d..806c139493 100644 --- a/UnitTests/Tab/WKWebViewPrivateMethodsAvailabilityTests.swift +++ b/UnitTests/Tab/WKWebViewPrivateMethodsAvailabilityTests.swift @@ -31,6 +31,10 @@ final class WKWebViewPrivateMethodsAvailabilityTests: XCTestCase { XCTAssertTrue(WKWebView.instancesRespond(to: WKWebView.Selector.fullScreenPlaceholderView)) } + func testWebViewRespondsTo_loadAlternateHTMLString() { + XCTAssertTrue(WKWebView.instancesRespond(to: WKWebView.Selector.loadAlternateHTMLString)) + } + func testWKBackForwardListRespondsTo_removeAllItems() { XCTAssertTrue(WKBackForwardList.instancesRespond(to: WKBackForwardList.removeAllItemsSelector)) } diff --git a/UnitTests/TabBar/ViewModel/TabLazyLoaderTests.swift b/UnitTests/TabBar/ViewModel/TabLazyLoaderTests.swift index d4ea051d82..b14feb90fd 100644 --- a/UnitTests/TabBar/ViewModel/TabLazyLoaderTests.swift +++ b/UnitTests/TabBar/ViewModel/TabLazyLoaderTests.swift @@ -18,6 +18,7 @@ import XCTest import Combine +import Navigation @testable import DuckDuckGo_Privacy_Browser private final class TabMock: LazyLoadable { @@ -32,7 +33,8 @@ private final class TabMock: LazyLoadable { lazy var loadingFinishedPublisher: AnyPublisher = loadingFinishedSubject.eraseToAnyPublisher() func isNewer(than other: TabMock) -> Bool { isNewerClosure(other) } - func reload() { reloadClosure(self) } + @discardableResult + func reload() -> ExpectedNavigation? { reloadClosure(self); return nil } var isNewerClosure: (TabMock) -> Bool = { _ in true } var reloadClosure: (TabMock) -> Void = { _ in } From ded95f1088dbcd1841c4fafd1652a34028f65dbf Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Sun, 18 Feb 2024 11:37:41 -0800 Subject: [PATCH 18/44] Improve VPN rekeying reliability (#2207) Task/Issue URL: https://app.asana.com/0/414235014887631/1206607513978260/f Tech Design URL: CC: Description: Client PR for duckduckgo/BrowserServicesKit#664 --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/LoginItems/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/PixelKit/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- LocalPackages/SwiftUIExtensions/Package.swift | 2 +- LocalPackages/SyncUI/Package.swift | 2 +- LocalPackages/SystemExtensionManager/Package.swift | 2 +- LocalPackages/XPCHelper/Package.swift | 2 +- 11 files changed, 12 insertions(+), 12 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index bbbc6133bd..a3bcf7326b 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -13387,7 +13387,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 107.0.1; + version = 108.0.0; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 606e967b2e..bd015532f4 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" : "93a9a4153429de38c8b9f186617ff34db84f9250", - "version" : "107.0.1" + "revision" : "39a0ed6853b823a33d85277ca3ee4385f81e4595", + "version" : "108.0.0" } }, { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index d791a1d36c..506ca5bd8d 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: "107.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "108.0.0"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper") diff --git a/LocalPackages/LoginItems/Package.swift b/LocalPackages/LoginItems/Package.swift index ba1b067330..058c01c576 100644 --- a/LocalPackages/LoginItems/Package.swift +++ b/LocalPackages/LoginItems/Package.swift @@ -13,7 +13,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "107.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "108.0.0"), ], targets: [ .target( diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index b461a64cc3..452909f0bc 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: "107.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "108.0.0"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems") diff --git a/LocalPackages/PixelKit/Package.swift b/LocalPackages/PixelKit/Package.swift index fa0471614f..b124f58a4f 100644 --- a/LocalPackages/PixelKit/Package.swift +++ b/LocalPackages/PixelKit/Package.swift @@ -20,7 +20,7 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "107.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "108.0.0"), ], targets: [ .target( diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 0070342a36..1c8a3eafee 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "107.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "108.0.0"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/LocalPackages/SwiftUIExtensions/Package.swift b/LocalPackages/SwiftUIExtensions/Package.swift index b39d10d9e1..dba70a367e 100644 --- a/LocalPackages/SwiftUIExtensions/Package.swift +++ b/LocalPackages/SwiftUIExtensions/Package.swift @@ -11,7 +11,7 @@ let package = Package( .library(name: "PreferencesViews", targets: ["PreferencesViews"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "107.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "108.0.0"), ], targets: [ .target( diff --git a/LocalPackages/SyncUI/Package.swift b/LocalPackages/SyncUI/Package.swift index 56579428b8..188d1b2b1c 100644 --- a/LocalPackages/SyncUI/Package.swift +++ b/LocalPackages/SyncUI/Package.swift @@ -14,7 +14,7 @@ let package = Package( ], dependencies: [ .package(path: "../SwiftUIExtensions"), - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "107.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "108.0.0"), ], targets: [ .target( diff --git a/LocalPackages/SystemExtensionManager/Package.swift b/LocalPackages/SystemExtensionManager/Package.swift index 9eb46d11a9..00416fd8fb 100644 --- a/LocalPackages/SystemExtensionManager/Package.swift +++ b/LocalPackages/SystemExtensionManager/Package.swift @@ -16,7 +16,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "107.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "108.0.0"), ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. diff --git a/LocalPackages/XPCHelper/Package.swift b/LocalPackages/XPCHelper/Package.swift index dbfc4d9353..d4eec00a7e 100644 --- a/LocalPackages/XPCHelper/Package.swift +++ b/LocalPackages/XPCHelper/Package.swift @@ -30,7 +30,7 @@ let package = Package( .library(name: "XPCHelper", targets: ["XPCHelper"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "107.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "108.0.0"), ], targets: [ .target( From 0f43e359242c59694926ec6f907b41f034bde6ad Mon Sep 17 00:00:00 2001 From: amddg44 Date: Mon, 19 Feb 2024 11:05:12 +0100 Subject: [PATCH 19/44] Add debug menu option to enable Autofill debug script (#2196) Task/Issue URL: https://app.asana.com/0/1203822806345703/1206298121584969/f Tech Design URL: CC: Description: Add control of Autofill JS debug script via UI debug menu --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 ++-- .../Common/Utilities/UserDefaultsWrapper.swift | 1 + .../ContentBlocker/ScriptSourceProviding.swift | 3 ++- DuckDuckGo/Menus/MainMenu.swift | 15 +++++++++++++++ .../Preferences/Model/AutofillPreferences.swift | 17 +++++++++++++++++ DuckDuckGo/Tab/Model/UserContentUpdating.swift | 1 + .../DataBrokerProtection/Package.swift | 2 +- LocalPackages/LoginItems/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/PixelKit/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- LocalPackages/SwiftUIExtensions/Package.swift | 2 +- LocalPackages/SyncUI/Package.swift | 2 +- .../SystemExtensionManager/Package.swift | 2 +- LocalPackages/XPCHelper/Package.swift | 2 +- .../AutofillPreferencesModelTests.swift | 1 + 17 files changed, 49 insertions(+), 13 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index a3bcf7326b..c084da3fc1 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -13387,7 +13387,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 108.0.0; + version = 108.1.0; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index bd015532f4..a0bbaf4e45 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" : "39a0ed6853b823a33d85277ca3ee4385f81e4595", - "version" : "108.0.0" + "revision" : "ab03bde3e1817b267debe9858a08b3f0caf72dc3", + "version" : "108.1.0" } }, { diff --git a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift index 286d5842fa..722a4fca75 100644 --- a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift +++ b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift @@ -69,6 +69,7 @@ public struct UserDefaultsWrapper { case askToSaveAddresses = "preferences.ask-to-save.addresses" case askToSavePaymentMethods = "preferences.ask-to-save.payment-methods" case autolockLocksFormFilling = "preferences.lock-autofill-form-fill" + case autofillDebugScriptEnabled = "preferences.enable-autofill-debug-script" case saveAsPreferredFileType = "saveAs.selected.filetype" diff --git a/DuckDuckGo/ContentBlocker/ScriptSourceProviding.swift b/DuckDuckGo/ContentBlocker/ScriptSourceProviding.swift index eeb484e27c..4013498fff 100644 --- a/DuckDuckGo/ContentBlocker/ScriptSourceProviding.swift +++ b/DuckDuckGo/ContentBlocker/ScriptSourceProviding.swift @@ -85,7 +85,8 @@ struct ScriptSourceProvider: ScriptSourceProviding { return DefaultAutofillSourceProvider.Builder(privacyConfigurationManager: privacyConfigurationManager, properties: ContentScopeProperties(gpcEnabled: privacySettings.gpcEnabled, sessionKey: self.sessionKey ?? "", - featureToggles: ContentScopeFeatureToggles.supportedFeaturesOnMacOS(privacyConfig))) + featureToggles: ContentScopeFeatureToggles.supportedFeaturesOnMacOS(privacyConfig)), + isDebug: AutofillPreferences().debugScriptEnabled) .withJSLoading() .build() } diff --git a/DuckDuckGo/Menus/MainMenu.swift b/DuckDuckGo/Menus/MainMenu.swift index 584481ac9f..7cf629775f 100644 --- a/DuckDuckGo/Menus/MainMenu.swift +++ b/DuckDuckGo/Menus/MainMenu.swift @@ -96,6 +96,7 @@ import SubscriptionUI private var loggingMenu: NSMenu? let customConfigurationUrlMenuItem = NSMenuItem(title: "Last Update Time", action: nil) let configurationDateAndTimeMenuItem = NSMenuItem(title: "Configuration URL", action: nil) + let autofillDebugScriptMenuItem = NSMenuItem(title: "Autofill Debug Script", action: #selector(MainMenu.toggleAutofillScriptDebugSettingsAction)) // MARK: - Help @@ -385,6 +386,7 @@ import SubscriptionUI updateLoggingMenuItems() updateInternalUserItem() updateRemoteConfigurationInfo() + updateAutofillDebugScriptMenuItem() } // MARK: - Bookmarks @@ -614,6 +616,9 @@ import SubscriptionUI menu.addItem(menuItem) } + menu.addItem(autofillDebugScriptMenuItem + .targetting(self)) + menu.addItem(.separator()) let debugLoggingMenuItem = NSMenuItem(title: OSLog.isRunningInDebugEnvironment ? "Disable DEBUG level logging…" : "Enable DEBUG level logging…", action: #selector(debugLoggingMenuItemAction), target: self) menu.addItem(debugLoggingMenuItem) @@ -642,6 +647,10 @@ import SubscriptionUI } } + private func updateAutofillDebugScriptMenuItem() { + autofillDebugScriptMenuItem.state = AutofillPreferences().debugScriptEnabled ? .on : .off + } + private func updateRemoteConfigurationInfo() { var dateString: String if let date = ConfigurationManager.shared.lastConfigurationInstallDate { @@ -672,6 +681,12 @@ import SubscriptionUI OSLog.loggingCategories = [] } + @objc private func toggleAutofillScriptDebugSettingsAction(_ sender: NSMenuItem) { + AutofillPreferences().debugScriptEnabled = !AutofillPreferences().debugScriptEnabled + NotificationCenter.default.post(name: .autofillScriptDebugSettingsDidChange, object: nil) + updateAutofillDebugScriptMenuItem() + } + @objc private func debugLoggingMenuItemAction(_ sender: NSMenuItem) { #if APPSTORE if !OSLog.isRunningInDebugEnvironment { diff --git a/DuckDuckGo/Preferences/Model/AutofillPreferences.swift b/DuckDuckGo/Preferences/Model/AutofillPreferences.swift index fcd0244637..58a55e86f1 100644 --- a/DuckDuckGo/Preferences/Model/AutofillPreferences.swift +++ b/DuckDuckGo/Preferences/Model/AutofillPreferences.swift @@ -26,6 +26,7 @@ protocol AutofillPreferencesPersistor { var askToSavePaymentMethods: Bool { get set } var autolockLocksFormFilling: Bool { get set } var passwordManager: PasswordManager { get set } + var debugScriptEnabled: Bool { get set } } enum PasswordManager: String, CaseIterable { @@ -67,6 +68,7 @@ enum AutofillAutoLockThreshold: String, CaseIterable { extension NSNotification.Name { static let autofillAutoLockSettingsDidChange = NSNotification.Name("autofillAutoLockSettingsDidChange") static let autofillUserSettingsDidChange = NSNotification.Name("autofillUserSettingsDidChange") + static let autofillScriptDebugSettingsDidChange = NSNotification.Name("autofillScriptDebugSettingsDidChange") } final class AutofillPreferences: AutofillPreferencesPersistor { @@ -132,6 +134,21 @@ final class AutofillPreferences: AutofillPreferencesPersistor { @UserDefaultsWrapper(key: .selectedPasswordManager, defaultValue: PasswordManager.duckduckgo.rawValue) private var selectedPasswordManager: String + @UserDefaultsWrapper(key: .autofillDebugScriptEnabled, defaultValue: false) + private var debugScriptEnabledWrapped: Bool + + var debugScriptEnabled: Bool { + get { + return debugScriptEnabledWrapped + } + + set { + if debugScriptEnabledWrapped != newValue { + debugScriptEnabledWrapped = newValue + } + } + } + private var statisticsStore: StatisticsStore { return injectedDependencyStore ?? defaultDependencyStore } diff --git a/DuckDuckGo/Tab/Model/UserContentUpdating.swift b/DuckDuckGo/Tab/Model/UserContentUpdating.swift index 1a7eb1d79f..ba9f8ba7b6 100644 --- a/DuckDuckGo/Tab/Model/UserContentUpdating.swift +++ b/DuckDuckGo/Tab/Model/UserContentUpdating.swift @@ -81,6 +81,7 @@ final class UserContentUpdating { .combineLatest(privacySecurityPreferences.$gpcEnabled) .map { $0.0 } // drop gpcEnabled value: $0.1 .combineLatest(onNotificationWithInitial(.autofillUserSettingsDidChange), combine) + .combineLatest(onNotificationWithInitial(.autofillScriptDebugSettingsDidChange), combine) // DefaultScriptSourceProvider instance should be created once per rules/config change and fed into UserScripts initialization .map(makeValue) .assign(to: \.bufferedValue, onWeaklyHeld: self) // buffer latest update value diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 506ca5bd8d..659f213c44 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: "108.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "108.1.0"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper") diff --git a/LocalPackages/LoginItems/Package.swift b/LocalPackages/LoginItems/Package.swift index 058c01c576..16df56baf1 100644 --- a/LocalPackages/LoginItems/Package.swift +++ b/LocalPackages/LoginItems/Package.swift @@ -13,7 +13,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "108.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "108.1.0"), ], targets: [ .target( diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 452909f0bc..27d2cf202e 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: "108.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "108.1.0"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems") diff --git a/LocalPackages/PixelKit/Package.swift b/LocalPackages/PixelKit/Package.swift index b124f58a4f..0fd74c1219 100644 --- a/LocalPackages/PixelKit/Package.swift +++ b/LocalPackages/PixelKit/Package.swift @@ -20,7 +20,7 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "108.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "108.1.0"), ], targets: [ .target( diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 1c8a3eafee..ea396a95a9 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "108.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "108.1.0"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/LocalPackages/SwiftUIExtensions/Package.swift b/LocalPackages/SwiftUIExtensions/Package.swift index dba70a367e..806e35d37e 100644 --- a/LocalPackages/SwiftUIExtensions/Package.swift +++ b/LocalPackages/SwiftUIExtensions/Package.swift @@ -11,7 +11,7 @@ let package = Package( .library(name: "PreferencesViews", targets: ["PreferencesViews"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "108.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "108.1.0"), ], targets: [ .target( diff --git a/LocalPackages/SyncUI/Package.swift b/LocalPackages/SyncUI/Package.swift index 188d1b2b1c..d37f24c657 100644 --- a/LocalPackages/SyncUI/Package.swift +++ b/LocalPackages/SyncUI/Package.swift @@ -14,7 +14,7 @@ let package = Package( ], dependencies: [ .package(path: "../SwiftUIExtensions"), - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "108.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "108.1.0"), ], targets: [ .target( diff --git a/LocalPackages/SystemExtensionManager/Package.swift b/LocalPackages/SystemExtensionManager/Package.swift index 00416fd8fb..319effb179 100644 --- a/LocalPackages/SystemExtensionManager/Package.swift +++ b/LocalPackages/SystemExtensionManager/Package.swift @@ -16,7 +16,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "108.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "108.1.0"), ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. diff --git a/LocalPackages/XPCHelper/Package.swift b/LocalPackages/XPCHelper/Package.swift index d4eec00a7e..337b3578cb 100644 --- a/LocalPackages/XPCHelper/Package.swift +++ b/LocalPackages/XPCHelper/Package.swift @@ -30,7 +30,7 @@ let package = Package( .library(name: "XPCHelper", targets: ["XPCHelper"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "108.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "108.1.0"), ], targets: [ .target( diff --git a/UnitTests/Preferences/AutofillPreferencesModelTests.swift b/UnitTests/Preferences/AutofillPreferencesModelTests.swift index 184758d8c9..6c77002c64 100644 --- a/UnitTests/Preferences/AutofillPreferencesModelTests.swift +++ b/UnitTests/Preferences/AutofillPreferencesModelTests.swift @@ -28,6 +28,7 @@ final class AutofillPreferencesPersistorMock: AutofillPreferencesPersistor { var askToSavePaymentMethods: Bool = true var passwordManager: PasswordManager = .duckduckgo var autolockLocksFormFilling: Bool = false + var debugScriptEnabled: Bool = false } final class UserAuthenticatorMock: UserAuthenticating { From 56bea2d87105307b178a0bd064940686cd7aaa81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20=C5=9Apiewak?= Date: Mon, 19 Feb 2024 11:30:20 +0100 Subject: [PATCH 20/44] Send MM message on errors for selected macOS workflows (#2187) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1199230911884351/1206553137165936/f **Description**: Sends MM message to actor in case of release, notarized release and create variants workflows failure. **Steps to test this PR**: 1. Make mentioned workflows fail 2. Receive MM message — ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --------- Co-authored-by: Dominik Kapusta --- .github/workflows/build_appstore.yml | 4 ++-- .github/workflows/build_notarized.yml | 14 ++++++++++++-- .github/workflows/create_variants.yml | 13 ++++++++++++- scripts/assets/appstore-release-mm-template.json | 15 ++++++++++++++- scripts/assets/release-mm-template.json | 15 ++++++++++++++- scripts/assets/variants-release-mm-template.json | 15 ++++++++++++++- 6 files changed, 68 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build_appstore.yml b/.github/workflows/build_appstore.yml index 7b0ccd1868..89ea1d2d61 100644 --- a/.github/workflows/build_appstore.yml +++ b/.github/workflows/build_appstore.yml @@ -149,6 +149,7 @@ jobs: template-name: debug-symbols-uploaded - name: Send Mattermost message + if: success() || failure() # Don't execute when cancelled env: WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} DESTINATION: ${{ env.destination }} @@ -158,8 +159,7 @@ jobs: if [[ -z "${MM_USER_HANDLE}" ]]; then echo "Mattermost user handle not known for ${{ github.actor }}, skipping sending message" else - curl -s -H 'Content-type: application/json' \ - -d "$(envsubst < ./scripts/assets/appstore-release-mm-template.json)" \ + -d "$(envsubst < ./scripts/assets/appstore-release-mm-template.json | jq ".${{ job.status }}")" \ ${{ secrets.MM_WEBHOOK_URL }} fi diff --git a/.github/workflows/build_notarized.yml b/.github/workflows/build_notarized.yml index 533f3fc3f3..9c7fca24c7 100644 --- a/.github/workflows/build_notarized.yml +++ b/.github/workflows/build_notarized.yml @@ -331,12 +331,17 @@ jobs: name: Send Mattermost message needs: [export-notarized-app, create-dmg] - if: ${{ always() && (needs.export-notarized-app.result == 'success') && (needs.create-dmg.result == 'success' || needs.create-dmg.result == 'skipped') }} + if: always() runs-on: ubuntu-latest + env: + success: ${{ (needs.export-notarized-app.result == 'success') && (needs.create-dmg.result == 'success' || needs.create-dmg.result == 'skipped') }} + failure: ${{ (needs.export-notarized-app.result == 'failure') || (needs.create-dmg.result == 'failure') }} + steps: - name: Send Mattermost message + if: ${{ env.success || env.failure }} # Don't execute when cancelled env: ASANA_TASK_URL: ${{ github.event.inputs.asana-task-url || inputs.asana-task-url }} GH_TOKEN: ${{ github.token }} @@ -356,7 +361,12 @@ jobs: export ASANA_LINK=" | [:asana: Asana task](${ASANA_TASK_URL})" fi + if [[ "${{ env.success }}" == "true" ]]; then + status="success" + else + status="failure" + fi curl -s -H 'Content-type: application/json' \ - -d "$(envsubst < message-template.json)" \ + -d "$(envsubst < message-template.json | jq ".${status}")" \ ${{ secrets.MM_WEBHOOK_URL }} fi diff --git a/.github/workflows/create_variants.yml b/.github/workflows/create_variants.yml index ac303b8937..2ec2ebbc33 100644 --- a/.github/workflows/create_variants.yml +++ b/.github/workflows/create_variants.yml @@ -150,8 +150,13 @@ jobs: runs-on: ubuntu-latest + env: + success: ${{ needs.create-atb-variants.result == 'success' }} + failure: ${{ needs.create-atb-variants.result == 'failure' }} + steps: - name: Send Mattermost message + if: ${{ env.success || env.failure }} # Don't execute when cancelled env: GH_TOKEN: ${{ github.token }} WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} @@ -164,7 +169,13 @@ jobs: if [[ -z "${MM_USER_HANDLE}" ]]; then echo "Mattermost user handle not known for ${{ github.actor }}, skipping sending message" else + + if [[ "${{ env.success }}" == "true" ]]; then + status="success" + else + status="failure" + fi curl -s -H 'Content-type: application/json' \ - -d "$(envsubst < message-template.json)" \ + -d "$(envsubst < message-template.json | jq ".${status}")" \ ${{ secrets.MM_WEBHOOK_URL }} fi diff --git a/scripts/assets/appstore-release-mm-template.json b/scripts/assets/appstore-release-mm-template.json index 6ea1754832..fe6ccb2b22 100644 --- a/scripts/assets/appstore-release-mm-template.json +++ b/scripts/assets/appstore-release-mm-template.json @@ -1 +1,14 @@ -{"channel":"${MM_USER_HANDLE}","username":"GitHub Actions","text":"**macOS app has been successfully uploaded to ${DESTINATION}** :goose_honk_tada: | [:github: Workflow run summary](${WORKFLOW_URL})","icon_url":"https://duckduckgo.com/assets/logo_header.v108.svg"} +{ + "success": { + "channel": "${MM_USER_HANDLE}", + "username": "GitHub Actions", + "text": "**macOS app has been successfully uploaded to ${DESTINATION}** :goose_honk_tada: | [:github: Workflow run summary](${WORKFLOW_URL})", + "icon_url": "https://duckduckgo.com/assets/logo_header.v108.svg" + }, + "failure": { + "channel": "${MM_USER_HANDLE}", + "username": "GitHub Actions", + "text": ":rotating_light: **macOS app ${DESTINATION} workflow failed** | [:github: Workflow run summary](${WORKFLOW_URL})", + "icon_url": "https://duckduckgo.com/assets/logo_header.v108.svg" + } +} diff --git a/scripts/assets/release-mm-template.json b/scripts/assets/release-mm-template.json index 21278d51a1..c8f2e88d81 100644 --- a/scripts/assets/release-mm-template.json +++ b/scripts/assets/release-mm-template.json @@ -1 +1,14 @@ -{"channel":"${MM_USER_HANDLE}","username":"GitHub Actions","text":"**Notarized macOS app `${RELEASE_TYPE}` build is ready** :goose_honk_tada: | [:github: Workflow run summary](${WORKFLOW_URL})${ASANA_LINK}","icon_url":"https://duckduckgo.com/assets/logo_header.v108.svg"} +{ + "success": { + "channel": "${MM_USER_HANDLE}", + "username": "GitHub Actions", + "text": "**Notarized macOS app `${RELEASE_TYPE}` build is ready** :goose_honk_tada: | [:github: Workflow run summary](${WORKFLOW_URL})${ASANA_LINK}", + "icon_url": "https://duckduckgo.com/assets/logo_header.v108.svg" + }, + "failure": { + "channel": "${MM_USER_HANDLE}", + "username": "GitHub Actions", + "text": ":rotating_light: **Notarized macOS app `${RELEASE_TYPE}` build failed** | [:github: Workflow run summary](${WORKFLOW_URL})${ASANA_LINK}", + "icon_url": "https://duckduckgo.com/assets/logo_header.v108.svg" + } +} diff --git a/scripts/assets/variants-release-mm-template.json b/scripts/assets/variants-release-mm-template.json index e95f2681a6..bef743a5f9 100644 --- a/scripts/assets/variants-release-mm-template.json +++ b/scripts/assets/variants-release-mm-template.json @@ -1 +1,14 @@ -{"channel":"${MM_USER_HANDLE}","username":"GitHub Actions","text":"**macOS app variants have been published successfully** :goose_honk_tada: | [:github: Workflow run summary](${WORKFLOW_URL})","icon_url":"https://duckduckgo.com/assets/logo_header.v108.svg"} +{ + "success": { + "channel": "${MM_USER_HANDLE}", + "username": "GitHub Actions", + "text": "**macOS app variants have been published successfully** :goose_honk_tada: | [:github: Workflow run summary](${WORKFLOW_URL})", + "icon_url": "https://duckduckgo.com/assets/logo_header.v108.svg" + }, + "failure": { + "channel": "${MM_USER_HANDLE}", + "username": "GitHub Actions", + "text": ":rotating_light: **macOS app variants workflow failed** | [:github: Workflow run summary](${WORKFLOW_URL})", + "icon_url": "https://duckduckgo.com/assets/logo_header.v108.svg" + } +} From 33c344f0cddc702a0bf0830e588f68ebfdd7d2e6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 11:52:38 +0100 Subject: [PATCH 21/44] Bump Submodules/privacy-reference-tests from `6b7ad1e` to `438faf5` (#2217) Bumps Submodules/privacy-reference-tests from 6b7ad1e to 438faf5. --- Submodules/privacy-reference-tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Submodules/privacy-reference-tests b/Submodules/privacy-reference-tests index 6b7ad1e7f1..438faf5160 160000 --- a/Submodules/privacy-reference-tests +++ b/Submodules/privacy-reference-tests @@ -1 +1 @@ -Subproject commit 6b7ad1e7f15270f9dfeb58a272199f4d57c3eb22 +Subproject commit 438faf5160f7db0fd2f2952945a809a33a9cdbac From 2778f1f0f1a6b69e0b053bcd7c18541a21448fd3 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Mon, 19 Feb 2024 11:14:16 +0000 Subject: [PATCH 22/44] Add debug menu option to reset DBP (#2212) Task/Issue URL: https://app.asana.com/0/1204167627774280/1206612078398080/f **Description**: Add debug menu option to reset DBP --- DuckDuckGo/Common/Extensions/NSAlertExtension.swift | 10 ++++++++++ DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/DuckDuckGo/Common/Extensions/NSAlertExtension.swift b/DuckDuckGo/Common/Extensions/NSAlertExtension.swift index 938977500c..616df15e44 100644 --- a/DuckDuckGo/Common/Extensions/NSAlertExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSAlertExtension.swift @@ -148,6 +148,16 @@ extension NSAlert { return alert } + static func removeAllDBPStateAndDataAlert() -> NSAlert { + let alert = NSAlert() + alert.messageText = "Uninstall Personal Information Removal Login Item?" + alert.informativeText = "This will remove the Personal Information Removal Login Item, delete all your data and reset the waitlist state." + alert.alertStyle = .warning + alert.addButton(withTitle: "Uninstall") + alert.addButton(withTitle: UserText.cancel) + return alert + } + static func noAccessToDownloads() -> NSAlert { let alert = NSAlert() alert.messageText = UserText.noAccessToDownloadsFolderHeader diff --git a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift index 650dcf5f25..ebe3179a76 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift @@ -138,6 +138,8 @@ final class DataBrokerProtectionDebugMenu: NSMenu { .targetting(self) NSMenuItem(title: "Run Personal Information Removal Debug Mode", action: #selector(DataBrokerProtectionDebugMenu.runCustomJSON)) .targetting(self) + NSMenuItem(title: "Reset All State and Delete All Data", action: #selector(DataBrokerProtectionDebugMenu.deleteAllDataAndStopAgent)) + .targetting(self) } } @@ -228,6 +230,14 @@ final class DataBrokerProtectionDebugMenu: NSMenu { LoginItemsManager().enableLoginItems([LoginItem.dbpBackgroundAgent], log: .dbp) } + @objc private func deleteAllDataAndStopAgent() { + Task { @MainActor in + guard case .alertFirstButtonReturn = await NSAlert.removeAllDBPStateAndDataAlert().runModal() else { return } + resetWaitlistState() + DataBrokerProtectionFeatureDisabler().disableAndDelete() + } + } + @objc private func showDatabaseBrowser() { let viewController = DataBrokerDatabaseBrowserViewController() let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 500, height: 400), From 5f31e19644b8a168088d7669fe249200442524c6 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Mon, 19 Feb 2024 22:19:20 +0100 Subject: [PATCH 23/44] Update of the subscription entitlements API (#2224) Task/Issue URL: https://app.asana.com/0/0/1206633753419161/f **Description**: Update of the subscription entitlements API **Steps to test this PR**: Ensure app is building and entitlements in preferences are properly respected. --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 +-- .../View/PreferencesRootView.swift | 25 ++++++++++++++----- .../DataBrokerProtection/Package.swift | 2 +- LocalPackages/LoginItems/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/PixelKit/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- .../DebugMenu/SubscriptionDebugMenu.swift | 12 +++++---- .../PreferencesSubscriptionModel.swift | 18 +++++++++---- .../PreferencesSubscriptionView.swift | 6 ++--- LocalPackages/SwiftUIExtensions/Package.swift | 2 +- LocalPackages/SyncUI/Package.swift | 2 +- .../SystemExtensionManager/Package.swift | 2 +- LocalPackages/XPCHelper/Package.swift | 2 +- 15 files changed, 54 insertions(+), 31 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index c084da3fc1..1ddafe2ae6 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -13387,7 +13387,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 108.1.0; + version = 109.0.0; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a0bbaf4e45..4796599b2d 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" : "ab03bde3e1817b267debe9858a08b3f0caf72dc3", - "version" : "108.1.0" + "revision" : "5ecf4fe56f334be6eaecb65f6d55632a6d53921c", + "version" : "109.0.0" } }, { diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index 2c0deacf52..2e298e833d 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -40,6 +40,18 @@ enum Preferences { @ObservedObject var model: PreferencesSidebarModel +#if SUBSCRIPTION + var subscriptionModel: PreferencesSubscriptionModel? +#endif + + init(model: PreferencesSidebarModel) { + self.model = model + +#if SUBSCRIPTION + self.subscriptionModel = makeSubscriptionViewModel() +#endif + } + var body: some View { HStack(spacing: 0) { Sidebar().environmentObject(model).frame(width: Const.sidebarWidth) @@ -67,7 +79,7 @@ enum Preferences { #if SUBSCRIPTION case .subscription: - makeSubscriptionView() + SubscriptionUI.PreferencesSubscriptionView(model: subscriptionModel!) #endif case .autofill: AutofillView(model: AutofillPreferencesModel()) @@ -98,18 +110,19 @@ enum Preferences { } #if SUBSCRIPTION - private func makeSubscriptionView() -> some View { + private func makeSubscriptionViewModel() -> PreferencesSubscriptionModel { let openURL: (URL) -> Void = { url in - WindowControllersManager.shared.showTab(with: .subscription(url)) + DispatchQueue.main.async { + WindowControllersManager.shared.showTab(with: .subscription(url)) + } } let sheetActionHandler = SubscriptionAccessActionHandlers(restorePurchases: { SubscriptionPagesUseSubscriptionFeature.startAppStoreRestoreFlow() }, openURLHandler: openURL, goToSyncPreferences: { self.model.selectPane(.sync) }) - let model = PreferencesSubscriptionModel(openURLHandler: openURL, - sheetActionHandler: sheetActionHandler) - return SubscriptionUI.PreferencesSubscriptionView(model: model) + return PreferencesSubscriptionModel(openURLHandler: openURL, + sheetActionHandler: sheetActionHandler) } #endif } diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 659f213c44..fb62529ee7 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: "108.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper") diff --git a/LocalPackages/LoginItems/Package.swift b/LocalPackages/LoginItems/Package.swift index 16df56baf1..bbe7e894b8 100644 --- a/LocalPackages/LoginItems/Package.swift +++ b/LocalPackages/LoginItems/Package.swift @@ -13,7 +13,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "108.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), ], targets: [ .target( diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 27d2cf202e..a1edbe7388 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: "108.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems") diff --git a/LocalPackages/PixelKit/Package.swift b/LocalPackages/PixelKit/Package.swift index 0fd74c1219..61de75cd32 100644 --- a/LocalPackages/PixelKit/Package.swift +++ b/LocalPackages/PixelKit/Package.swift @@ -20,7 +20,7 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "108.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), ], targets: [ .target( diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index ea396a95a9..197a2c4eee 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "108.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift index 7c4dbf2ed8..fcba4739e3 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift @@ -103,11 +103,13 @@ public final class SubscriptionDebugMenu: NSMenuItem { Task { var results: [String] = [] - for entitlementName in ["fake", "dummy1", "dummy2", "dummy3"] { - let result = await AccountManager().hasEntitlement(for: entitlementName) - let resultSummary = "Entitlement check for \(entitlementName): \(result)" - results.append(resultSummary) - print(resultSummary) + let entitlements: [AccountManager.Entitlement] = [.networkProtection, .dataBrokerProtection, .identityTheftRestoration] + for entitlement in entitlements { + if case let .success(result) = await AccountManager().hasEntitlement(for: entitlement) { + let resultSummary = "Entitlement check for \(entitlement.rawValue): \(result)" + results.append(resultSummary) + print(resultSummary) + } } showAlert(title: "Check Entitlements", message: results.joined(separator: "\n")) diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift index 2f670acfe3..15b6c3ba93 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift @@ -22,7 +22,7 @@ import Subscription public final class PreferencesSubscriptionModel: ObservableObject { @Published var isUserAuthenticated: Bool = false - @Published var hasEntitlements: Bool = false + @Published var cachedEntitlements: [AccountManager.Entitlement] = [] @Published var subscriptionDetails: String? private var subscriptionPlatform: SubscriptionService.GetSubscriptionDetailsResponse.Platform? @@ -33,6 +33,8 @@ public final class PreferencesSubscriptionModel: ObservableObject { private let openURLHandler: (URL) -> Void private let sheetActionHandler: SubscriptionAccessActionHandlers + private var fetchSubscriptionDetailsTask: Task<(), Never>? + private var signInObserver: Any? private var signOutObserver: Any? @@ -42,7 +44,6 @@ public final class PreferencesSubscriptionModel: ObservableObject { self.sheetActionHandler = sheetActionHandler self.isUserAuthenticated = accountManager.isUserAuthenticated - self.hasEntitlements = self.isUserAuthenticated if let cachedDate = SubscriptionService.cachedSubscriptionDetailsResponse?.expiresOrRenewsAt { updateDescription(for: cachedDate) @@ -170,12 +171,17 @@ public final class PreferencesSubscriptionModel: ObservableObject { @MainActor func fetchAndUpdateSubscriptionDetails() { - Task { + guard fetchSubscriptionDetailsTask == nil else { return } + + fetchSubscriptionDetailsTask = Task { guard let token = accountManager.accessToken else { return } if let cachedDate = SubscriptionService.cachedSubscriptionDetailsResponse?.expiresOrRenewsAt { updateDescription(for: cachedDate) - self.hasEntitlements = cachedDate.timeIntervalSinceNow > 0 + + if cachedDate.timeIntervalSinceNow > 0 { + self.cachedEntitlements = [] + } } if case .success(let response) = await SubscriptionService.getSubscriptionDetails(token: token) { @@ -189,7 +195,9 @@ public final class PreferencesSubscriptionModel: ObservableObject { subscriptionPlatform = response.platform } - self.hasEntitlements = await AccountManager().hasEntitlement(for: "dummy1") + if case let .success(entitlements) = await AccountManager().fetchEntitlements() { + self.cachedEntitlements = entitlements + } } } diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionView.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionView.swift index d20be23069..5ef3942106 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionView.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionView.swift @@ -139,7 +139,7 @@ public struct PreferencesSubscriptionView: View { description: UserText.vpnServiceDescription, buttonName: model.isUserAuthenticated ? "Manage" : nil, buttonAction: { model.openVPN() }, - enabled: !model.isUserAuthenticated || model.hasEntitlements) + enabled: !model.isUserAuthenticated || model.cachedEntitlements.contains(.networkProtection)) Divider() .foregroundColor(Color.secondary) @@ -149,7 +149,7 @@ public struct PreferencesSubscriptionView: View { description: UserText.personalInformationRemovalServiceDescription, buttonName: model.isUserAuthenticated ? "View" : nil, buttonAction: { model.openPersonalInformationRemoval() }, - enabled: !model.isUserAuthenticated || model.hasEntitlements) + enabled: !model.isUserAuthenticated || model.cachedEntitlements.contains(.dataBrokerProtection)) Divider() .foregroundColor(Color.secondary) @@ -159,7 +159,7 @@ public struct PreferencesSubscriptionView: View { description: UserText.identityTheftRestorationServiceDescription, buttonName: model.isUserAuthenticated ? "View" : nil, buttonAction: { model.openIdentityTheftRestoration() }, - enabled: !model.isUserAuthenticated || model.hasEntitlements) + enabled: !model.isUserAuthenticated || model.cachedEntitlements.contains(.identityTheftRestoration)) } .padding(10) .roundedBorder() diff --git a/LocalPackages/SwiftUIExtensions/Package.swift b/LocalPackages/SwiftUIExtensions/Package.swift index 806e35d37e..6f8c716bc3 100644 --- a/LocalPackages/SwiftUIExtensions/Package.swift +++ b/LocalPackages/SwiftUIExtensions/Package.swift @@ -11,7 +11,7 @@ let package = Package( .library(name: "PreferencesViews", targets: ["PreferencesViews"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "108.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), ], targets: [ .target( diff --git a/LocalPackages/SyncUI/Package.swift b/LocalPackages/SyncUI/Package.swift index d37f24c657..678077f716 100644 --- a/LocalPackages/SyncUI/Package.swift +++ b/LocalPackages/SyncUI/Package.swift @@ -14,7 +14,7 @@ let package = Package( ], dependencies: [ .package(path: "../SwiftUIExtensions"), - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "108.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), ], targets: [ .target( diff --git a/LocalPackages/SystemExtensionManager/Package.swift b/LocalPackages/SystemExtensionManager/Package.swift index 319effb179..188a15c3ad 100644 --- a/LocalPackages/SystemExtensionManager/Package.swift +++ b/LocalPackages/SystemExtensionManager/Package.swift @@ -16,7 +16,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "108.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. diff --git a/LocalPackages/XPCHelper/Package.swift b/LocalPackages/XPCHelper/Package.swift index 337b3578cb..fb49a79f25 100644 --- a/LocalPackages/XPCHelper/Package.swift +++ b/LocalPackages/XPCHelper/Package.swift @@ -30,7 +30,7 @@ let package = Package( .library(name: "XPCHelper", targets: ["XPCHelper"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "108.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), ], targets: [ .target( From edb4ab257bc80ed432d5e7e0d6d7ac38ed6f8308 Mon Sep 17 00:00:00 2001 From: Tom Strba <57389842+tomasstrba@users.noreply.github.com> Date: Tue, 20 Feb 2024 12:44:23 +0100 Subject: [PATCH 24/44] Hiding tab preview in case the window is minimized or going full screen (#2226) Task/Issue URL: https://app.asana.com/0/1177771139624306/1206635109534553/f **Description**: This PR fixes a bug with tab preview positioning when a window is maximized. --- DuckDuckGo/MainWindow/MainViewController.swift | 8 ++++++++ DuckDuckGo/MainWindow/MainWindowController.swift | 5 +++++ 2 files changed, 13 insertions(+) diff --git a/DuckDuckGo/MainWindow/MainViewController.swift b/DuckDuckGo/MainWindow/MainViewController.swift index adae427d70..843a190735 100644 --- a/DuckDuckGo/MainWindow/MainViewController.swift +++ b/DuckDuckGo/MainWindow/MainViewController.swift @@ -209,6 +209,14 @@ final class MainViewController: NSViewController { tabBarViewController.hideTabPreview() } + func windowWillMiniaturize() { + tabBarViewController.hideTabPreview() + } + + func windowWillEnterFullScreen() { + tabBarViewController.hideTabPreview() + } + func toggleBookmarksBarVisibility() { updateBookmarksBarViewVisibility(visible: !(mainView.bookmarksBarHeightConstraint.constant > 0)) } diff --git a/DuckDuckGo/MainWindow/MainWindowController.swift b/DuckDuckGo/MainWindow/MainWindowController.swift index d37dfab18a..06b35517fe 100644 --- a/DuckDuckGo/MainWindow/MainWindowController.swift +++ b/DuckDuckGo/MainWindow/MainWindowController.swift @@ -203,6 +203,11 @@ extension MainWindowController: NSWindowDelegate { func windowWillEnterFullScreen(_ notification: Notification) { mainViewController.tabBarViewController.draggingSpace.isHidden = true + mainViewController.windowWillEnterFullScreen() + } + + func windowWillMiniaturize(_ notification: Notification) { + mainViewController.windowWillMiniaturize() } func windowDidEnterFullScreen(_ notification: Notification) { From 17057fc85520291e6d5ef4f322a99f12fd0208b7 Mon Sep 17 00:00:00 2001 From: Sabrina Tardio <44158575+SabrinaTardio@users.noreply.github.com> Date: Tue, 20 Feb 2024 14:13:43 +0100 Subject: [PATCH 25/44] Fix sync UI localisation issues (#2225) Task/Issue URL: https://app.asana.com/0/0/1206628858529173/f **Description**: Fix some UI issues arising when copy is longer, adds some strings comments --- DuckDuckGo/Common/Localizables/UserText.swift | 64 +++++++++---------- .../DataImport/View/FileImportView.swift | 4 +- .../DataImport/View/ReportFeedbackView.swift | 2 +- DuckDuckGo/Localizable.xcstrings | 51 ++++++++++++--- .../Extensions/UserText+PasswordManager.swift | 2 +- .../PasswordManagementIdentityItemView.swift | 10 +-- .../SwiftUIExtensions/ButtonStyles.swift | 3 +- .../Dialogs/SyncWithAnotherDeviceView.swift | 10 ++- .../Views/ManagementView/SyncSetupView.swift | 5 +- .../SyncUI/Views/internal/SyncUIViews.swift | 4 +- 10 files changed, 96 insertions(+), 59 deletions(-) diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index e08d3ae946..c3f8ff0f68 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -77,7 +77,7 @@ struct UserText { // MARK: - Main Menu -> -File static let mainMenuFile = NSLocalizedString("main-menu.file", value:"File", comment: "Main Menu File") static let mainMenuFileNewTab = NSLocalizedString("main-menu.file.new-tab", value:"New Tab", comment: "Main Menu File item") - static let mainMenuFileOpenLocation = NSLocalizedString("main-menu.file.open-location", value:"Open Location…", comment: "Main Menu File item") + static let mainMenuFileOpenLocation = NSLocalizedString("main-menu.file.open-location", value:"Open Location…", comment: "Main Menu File item- Menu option that allows the user to connect to an address (type an address) on click the address bar of the browser is selected and the user can type.") static let mainMenuFileCloseWindow = NSLocalizedString("main-menu.file.close-window", value:"Close Window", comment: "Main Menu File item") static let mainMenuFileCloseAllWindows = NSLocalizedString("main-menu.file.close-all-windows", value:"Close All Windows", comment: "Main Menu File item") static let mainMenuFileSaveAs = NSLocalizedString("main-menu.file.save-as", value:"Save As…", comment: "Main Menu File item") @@ -93,7 +93,7 @@ struct UserText { static let mainMenuEditCut = NSLocalizedString("main-menu.edit.cut", value:"Cut", comment: "Main Menu Edit item") static let mainMenuEditCopy = NSLocalizedString("main-menu.edit.copy", value:"Copy", comment: "Main Menu Edit item") static let mainMenuEditPaste = NSLocalizedString("main-menu.edit.paste", value:"Paste", comment: "Main Menu Edit item") - static let mainMenuEditPasteAndMatchStyle = NSLocalizedString("main-menu.edit.paste-and-match-style", value:"Paste and Match Style", comment: "Main Menu Edit item") + static let mainMenuEditPasteAndMatchStyle = NSLocalizedString("main-menu.edit.paste-and-match-style", value:"Paste and Match Style", comment: "Main Menu Edit item - Action that allows the user to paste copy into a target document and the target document's style will be retained (instead of the source style)") static let mainMenuEditDelete = NSLocalizedString("main-menu.edit.delete", value:"Delete", comment: "Main Menu Edit item") static let mainMenuEditSelectAll = NSLocalizedString("main-menu.edit.select-all", value:"Select All", comment: "Main Menu Edit item") @@ -219,7 +219,7 @@ struct UserText { static let webProcessCrashPageMessage = NSLocalizedString("page.crash.message", value: "Try reloading the page or come back later.", comment: "Error page message text shown when a Web Page process had crashed") static let openSystemPreferences = NSLocalizedString("open.preferences", value: "Open System Preferences", comment: "Open System Preferences (to re-enable permission for the App) (up to and including macOS 12") - static let openSystemSettings = NSLocalizedString("open.settings", value: "Open System Settings…", comment: "") + static let openSystemSettings = NSLocalizedString("open.settings", value: "Open System Settings…", comment: "This string represents a prompt or button label prompting the user to open system settings") static let checkForUpdate = NSLocalizedString("check.for.update", value: "Check for Update", comment: "Button users can use to check for a new update") static let unknownErrorTryAgainMessage = NSLocalizedString("error.unknown.try.again", value: "An unknown error has occurred", comment: "Generic error message on a dialog for when the cause is not known.") @@ -347,7 +347,7 @@ struct UserText { static let autofillPasswordManagerBitwarden = NSLocalizedString("autofill.password-manager.bitwarden", value: "Bitwarden", comment: "Autofill password manager row title") static let autofillPasswordManagerBitwardenDisclaimer = NSLocalizedString("autofill.password-manager.bitwarden.disclaimer", value: "Setup requires installing the Bitwarden app.", comment: "Autofill password manager Bitwarden disclaimer") static let restartBitwarden = NSLocalizedString("restart.bitwarden", value: "Restart Bitwarden", comment: "Button to restart Bitwarden application") - static let restartBitwardenInfo = NSLocalizedString("restart.bitwarden.info", value: "Bitwarden is not responding. Please restart it to initiate the communication again", comment: "") + static let restartBitwardenInfo = NSLocalizedString("restart.bitwarden.info", value: "Bitwarden is not responding. Please restart it to initiate the communication again", comment: "This string represents a message informing the user that Bitwarden is not responding and prompts them to restart the application to initiate communication again.") static let autofillViewContentButton = NSLocalizedString("autofill.view-autofill-content", value: "View Autofill Content…", comment: "View Autofill Content Button name in the autofill settings") static let autofillAskToSave = NSLocalizedString("autofill.ask-to-save", value: "Save and Autofill", comment: "Autofill settings section title") @@ -531,7 +531,7 @@ struct UserText { static let downloads = NSLocalizedString("preferences.downloads", value: "Downloads", comment: "Show downloads browser preferences") static let isDefaultBrowser = NSLocalizedString("preferences.default-browser.active", value: "DuckDuckGo is your default browser", comment: "Indicate that the browser is the default") static let isNotDefaultBrowser = NSLocalizedString("preferences.default-browser.inactive", value: "DuckDuckGo is not your default browser.", comment: "Indicate that the browser is not the default") - static let makeDefaultBrowser = NSLocalizedString("preferences.default-browser.button.make-default", value: "Make DuckDuckGo Default…", comment: "") + static let makeDefaultBrowser = NSLocalizedString("preferences.default-browser.button.make-default", value: "Make DuckDuckGo Default…", comment: "represents a prompt message asking the user to make DuckDuckGo their default browser.") static let onStartup = NSLocalizedString("preferences.on-startup", value: "On Startup", comment: "Name of the preferences section related to app startup") static let reopenAllWindowsFromLastSession = NSLocalizedString("preferences.reopen-windows", value: "Reopen all windows from last session", comment: "Option to control session restoration") static let showHomePage = NSLocalizedString("preferences.show-home", value: "Open a new window", comment: "Option to control session startup") @@ -570,7 +570,7 @@ struct UserText { static let aboutDuckDuckGo = NSLocalizedString("preferences.about.about-duckduckgo", value: "About DuckDuckGo", comment: "About screen") static let privacySimplified = NSLocalizedString("preferences.about.privacy-simplified", value: "Privacy, simplified.", comment: "About screen") - static let aboutUnsupportedDeviceInfo1 = NSLocalizedString("preferences.about.unsupported-device-info1", value: "DuckDuckGo is no longer providing browser updates for your version of macOS.", comment: "") + static let aboutUnsupportedDeviceInfo1 = NSLocalizedString("preferences.about.unsupported-device-info1", value: "DuckDuckGo is no longer providing browser updates for your version of macOS.", comment: "This string represents a message informing the user that DuckDuckGo is no longer providing browser updates for their version of macOS") static func aboutUnsupportedDeviceInfo2(version: String) -> String { let localized = NSLocalizedString("preferences.about.unsupported-device-info2", value: "Please update to macOS %@ or later to use the most recent version of DuckDuckGo. You can also keep using your current version of the browser, but it will not receive further updates.", comment: "Copy in section that tells the user to update their macOS version since their current version is unsupported") return String(format: localized, version) @@ -581,7 +581,7 @@ struct UserText { } static let aboutUnsupportedDeviceInfo2Part3 = "or later to use the most recent version" static let aboutUnsupportedDeviceInfo2Part4 = "of DuckDuckGo. You can also keep using your current version of the browser, but it will not receive further updates." - static let unsupportedDeviceInfoAlertHeader = NSLocalizedString("unsupported.device.info.alert.header", value: "Your version of macOS is no longer supported.", comment: "") + static let unsupportedDeviceInfoAlertHeader = NSLocalizedString("unsupported.device.info.alert.header", value: "Your version of macOS is no longer supported.", comment: "his string represents the header for an alert informing the user that their version of macOS is no longer supported") static func moreAt(url: String) -> String { @@ -717,10 +717,10 @@ struct UserText { static let moreOrLessCollapse = NSLocalizedString("more.or.less.collapse", value: "Show Less", comment: "For collapsing views to show less.") static let moreOrLessExpand = NSLocalizedString("more.or.less.expand", value: "Show More", comment: "For expanding views to show more.") - static let defaultBrowserPromptMessage = NSLocalizedString("default.browser.prompt.message", value: "Make DuckDuckGo your default browser", comment: "") - static let defaultBrowserPromptButton = NSLocalizedString("default.browser.prompt.button", value: "Set Default…", comment: "") + static let defaultBrowserPromptMessage = NSLocalizedString("default.browser.prompt.message", value: "Make DuckDuckGo your default browser", comment: "represents a prompt message asking the user to make DuckDuckGo their default browser.") + static let defaultBrowserPromptButton = NSLocalizedString("default.browser.prompt.button", value: "Set Default…", comment: "represents a prompt message asking the user to make DuckDuckGo their default browser.") - static let homePageProtectionSummaryInfo = NSLocalizedString("home.page.protection.summary.info", value: "No recent activity", comment: "") + static let homePageProtectionSummaryInfo = NSLocalizedString("home.page.protection.summary.info", value: "No recent activity", comment: "This string represents a message in the protection summary on the home page, indicating that there is no recent activity") static func homePageProtectionSummaryMessage(numberOfTrackersBlocked: Int) -> String { let localized = NSLocalizedString("home.page.protection.summary.message", value: "%@ tracking attempts blocked", @@ -729,10 +729,10 @@ struct UserText { } static let homePageProtectionDurationInfo = NSLocalizedString("home.page.protection.duration", value: "PAST 7 DAYS", comment: "Past 7 days in uppercase.") - static let homePageEmptyStateItemTitle = NSLocalizedString("home.page.empty.state.item.title", value: "Recently visited sites appear here", comment: "") - static let homePageEmptyStateItemMessage = NSLocalizedString("home.page.empty.state.item.message", value: "Keep browsing to see how many trackers were blocked", comment: "") - static let homePageNoTrackersFound = NSLocalizedString("home.page.no.trackers.found", value: "No trackers found", comment: "") - static let homePageNoTrackersBlocked = NSLocalizedString("home.page.no.trackers.blocked", value: "No trackers blocked", comment: "") + static let homePageEmptyStateItemTitle = NSLocalizedString("home.page.empty.state.item.title", value: "Recently visited sites appear here", comment: "This string represents the title for an empty state item on the home page, indicating that recently visited sites will appear here") + static let homePageEmptyStateItemMessage = NSLocalizedString("home.page.empty.state.item.message", value: "Keep browsing to see how many trackers were blocked", comment: "This string represents the message for an empty state item on the home page, encouraging the user to keep browsing to see how many trackers were blocked") + static let homePageNoTrackersFound = NSLocalizedString("home.page.no.trackers.found", value: "No trackers found", comment: "This string represents a message on the home page indicating that no trackers were found") + static let homePageNoTrackersBlocked = NSLocalizedString("home.page.no.trackers.blocked", value: "No trackers blocked", comment: "This string represents a message on the home page indicating that no trackers were blocked") static let homePageBurnFireproofSiteAlert = NSLocalizedString("home.page.burn.fireproof.site.alert", value: "History will be cleared for this site, but related data will remain, because this site is Fireproof", comment: "Message for an alert displayed when trying to burn a fireproof website") static let homePageClearHistory = NSLocalizedString("home.page.clear.history", value: "Clear History", comment: "Button caption for the burn fireproof website alert") @@ -753,8 +753,8 @@ struct UserText { static let recentlyClosedWindowMenuItem = NSLocalizedString("n.more.tabs", value: "Window with multiple tabs (%d)", comment: "String in Recently Closed menu item for recently closed browser window and number of tabs contained in the closed window") - static let reopenLastClosedTab = NSLocalizedString("reopen.last.closed.tab", value: "Reopen Last Closed Tab", comment: "") - static let reopenLastClosedWindow = NSLocalizedString("reopen.last.closed.window", value: "Reopen Last Closed Window", comment: "") + static let reopenLastClosedTab = NSLocalizedString("reopen.last.closed.tab", value: "Reopen Last Closed Tab", comment: "This string represents an action to reopen the last closed tab in the browser") + static let reopenLastClosedWindow = NSLocalizedString("reopen.last.closed.window", value: "Reopen Last Closed Window", comment: "This string represents an action to reopen the last closed window in the browser") static let cookiePopupManagedNotification = NSLocalizedString("notification.badge.cookiesmanaged", value: "Cookies Managed", comment: "Notification that appears when browser automatically handle cookies") static let cookiePopupHiddenNotification = NSLocalizedString("notification.badge.popuphidden", value: "Pop-up Hidden", comment: "Notification that appears when browser cosmetically hides a cookie popup") @@ -796,24 +796,24 @@ struct UserText { static let passwordManager = NSLocalizedString("password.manager", value: "Password Manager", comment: "Section header") static let bitwardenPreferencesUnableToConnect = NSLocalizedString("bitwarden.preferences.unable-to-connect", value: "Unable to find or connect to Bitwarden", comment: "Dialog telling the user Bitwarden (a password manager) is not available") - static let bitwardenPreferencesCompleteSetup = NSLocalizedString("bitwarden.preferences.complete-setup", value: "Complete Setup…", comment: "") - static let bitwardenPreferencesOpenBitwarden = NSLocalizedString("bitwarden.preferences.open-bitwarden", value: "Open Bitwarden", comment: "") + static let bitwardenPreferencesCompleteSetup = NSLocalizedString("bitwarden.preferences.complete-setup", value: "Complete Setup…", comment: "action option that prompts the user to complete the setup process in Bitwarden preferences") + static let bitwardenPreferencesOpenBitwarden = NSLocalizedString("bitwarden.preferences.open-bitwarden", value: "Open Bitwarden", comment: "Button to open Bitwarden app") static let bitwardenPreferencesUnlock = NSLocalizedString("bitwarden.preferences.unlock", value: "Unlock Bitwarden", comment: "Asks the user to unlock the password manager Bitwarden") static let bitwardenPreferencesRun = NSLocalizedString("bitwarden.preferences.run", value: "Bitwarden app not running", comment: "Warns user that the password manager Bitwarden app is not running") - static let bitwardenError = NSLocalizedString("bitwarden.error", value: "Unable to find or connect to Bitwarden", comment: "") + static let bitwardenError = NSLocalizedString("bitwarden.error", value: "Unable to find or connect to Bitwarden", comment: "This message appears when the application is unable to find or connect to Bitwarden, indicating a connection issue.") static let bitwardenNotInstalled = NSLocalizedString("bitwarden.not.installed", value: "Bitwarden app is not installed", comment: "") - static let bitwardenOldVersion = NSLocalizedString("bitwarden.old.version", value: "Please update Bitwarden to the latest version", comment: "") - static let bitwardenIntegrationNotApproved = NSLocalizedString("bitwarden.integration.not.approved", value: "Integration with DuckDuckGo is not approved in Bitwarden app", comment: "") - static let bitwardenMissingHandshake = NSLocalizedString("bitwarden.missing.handshake", value: "Missing handshake", comment: "") - static let bitwardenWaitingForHandshake = NSLocalizedString("bitwarden.waiting.for.handshake", value: "Waiting for the handshake approval in Bitwarden app", comment: "") + static let bitwardenOldVersion = NSLocalizedString("bitwarden.old.version", value: "Please update Bitwarden to the latest version", comment: "Message that warns user they need to update their password manager Bitwarden app vesion") + static let bitwardenIntegrationNotApproved = NSLocalizedString("bitwarden.integration.not.approved", value: "Integration with DuckDuckGo is not approved in Bitwarden app", comment: "While the user tries to connect the DuckDuckGo Browser to password manager Bitwarden This message indicates that the integration with DuckDuckGo has not been approved in the Bitwarden app.") + static let bitwardenMissingHandshake = NSLocalizedString("bitwarden.missing.handshake", value: "Missing handshake", comment: "While the user tries to connect the DuckDuckGo Browser to password manager Bitwarden This message indicates a missing handshake (a way for two devices or systems to say hello to each other and agree to communicate or exchange information).") + static let bitwardenWaitingForHandshake = NSLocalizedString("bitwarden.waiting.for.handshake", value: "Waiting for the handshake approval in Bitwarden app", comment: "While the user tries to connect the DuckDuckGo Browser to password manager Bitwarden This message indicates the system is waiting for the handshake (a way for two devices or systems to say hello to each other and agree to communicate or exchange information).") static let bitwardenCantAccessContainer = NSLocalizedString("bitwarden.cant.access.container", value: "DuckDuckGo needs permission to access Bitwarden. You can grant DuckDuckGo Full Disk Access in System Settings, or switch back to the built-in password manager.", comment: "Requests user Full Disk access in order to access password manager Birwarden") - static let bitwardenHanshakeNotApproved = NSLocalizedString("bitwarden.handshake.not.approved", value: "Handshake not approved in Bitwarden app", comment: "") - static let bitwardenConnecting = NSLocalizedString("bitwarden.connecting", value: "Connecting to Bitwarden", comment: "") - static let bitwardenWaitingForStatusResponse = NSLocalizedString("bitwarden.waiting.for.status.response", value: "Waiting for the status response from Bitwarden", comment: "") + static let bitwardenHanshakeNotApproved = NSLocalizedString("bitwarden.handshake.not.approved", value: "Handshake not approved in Bitwarden app", comment: "It appears in a dialog when the users are connecting to Bitwardern and shows the status of the action. This message indicates that the handshake process was not approved in the Bitwarden app.") + static let bitwardenConnecting = NSLocalizedString("bitwarden.connecting", value: "Connecting to Bitwarden", comment: "It appears in a dialog when the users are connecting to Bitwardern and shows the status of the action, in this case we are in the progress of connecting the browser to the Bitwarden password maanger.") + static let bitwardenWaitingForStatusResponse = NSLocalizedString("bitwarden.waiting.for.status.response", value: "Waiting for the status response from Bitwarden", comment: "It appears in a dialog when the users are connecting to Bitwardern and shows the status of the action, in this case that the application is currently waiting for a response from the Bitwarden service.") static let connectToBitwarden = NSLocalizedString("bitwarden.connect.title", value: "Connect to Bitwarden", comment: "Title for the Bitwarden onboarding flow") - static let connectToBitwardenDescription = NSLocalizedString("bitwarden.connect.description", value: "We’ll walk you through connecting to Bitwarden, so you can use it in DuckDuckGo.", comment: "") + static let connectToBitwardenDescription = NSLocalizedString("bitwarden.connect.description", value: "We’ll walk you through connecting to Bitwarden, so you can use it in DuckDuckGo.", comment: "Description for when the user wants to connect the browser to the password manager Bitwarned.") static let connectToBitwardenPrivacy = NSLocalizedString("bitwarden.connect.privacy", value: "Privacy", comment: "") static let installBitwarden = NSLocalizedString("bitwarden.install", value: "Install Bitwarden", comment: "Button to install Bitwarden app") @@ -833,7 +833,7 @@ struct UserText { static let bitwardenIntegrationComplete = NSLocalizedString("bitwarden.integration.complete", value: "Bitwarden integration complete!", comment: "Setup of the integration with Bitwarden app") static let bitwardenIntegrationCompleteInfo = NSLocalizedString("bitwarden.integration.complete.info", value: "You are now using Bitwarden as your password manager.", comment: "Setup of the integration with Bitwarden app") - static let bitwardenCommunicationInfo = NSLocalizedString("bitwarden.connect.communication-info", value: "All communication between Bitwarden and DuckDuckGo is encrypted and the data never leaves your device.", comment: "") + static let bitwardenCommunicationInfo = NSLocalizedString("bitwarden.connect.communication-info", value: "All communication between Bitwarden and DuckDuckGo is encrypted and the data never leaves your device.", comment: "Warns users that all communication between the DuckDuckGo browser and the password manager Bitwarden is encrypted and doesn't leave the user device") static let bitwardenHistoryInfo = NSLocalizedString("bitwarden.connect.history-info", value: "Bitwarden will have access to your browsing history.", comment: "Warn users that the password Manager Bitwarden will have access to their browsing history") static let showAutofillShortcut = NSLocalizedString("pinning.show-autofill-shortcut", value: "Show Autofill Shortcut", comment: "Menu item for showing the autofill shortcut") @@ -987,10 +987,10 @@ struct UserText { static let newTabRecentActivitySectionTitle = NSLocalizedString("newTab.recent.activity.section.title", value: "Recent Activity", comment: "Title of the RecentActivity section in the home page") static let burnerWindowHeader = NSLocalizedString("burner.window.header", value: "Fire Window", comment: "Header shown on the hompage of the Fire Window") static let burnerTabHomeTitle = NSLocalizedString("burner.tab.home.title", value: "New Fire Tab", comment: "Tab title for Fire Tab") - static let burnerHomepageDescription1 = NSLocalizedString("burner.homepage.description.1", value: "Browse without saving local history", comment: "") - static let burnerHomepageDescription2 = NSLocalizedString("burner.homepage.description.2", value: "Sign in to a site with a different account", comment: "") - static let burnerHomepageDescription3 = NSLocalizedString("burner.homepage.description.3", value: "Troubleshoot websites", comment: "") - static let burnerHomepageDescription4 = NSLocalizedString("burner.homepage.description.4", value: "Fire windows are isolated from other browser data, and their data is burned when you close them. They have the same tracking protection as other windows.", comment: "") + static let burnerHomepageDescription1 = NSLocalizedString("burner.homepage.description.1", value: "Browse without saving local history", comment: "Descriptions of features Fire page. Provides information about browsing functionalities such as browsing without saving local history, signing in to a site with a different account, and troubleshooting websites.") + static let burnerHomepageDescription2 = NSLocalizedString("burner.homepage.description.2", value: "Sign in to a site with a different account", comment: "Descriptions of features Fire page. Provides information about browsing functionalities such as browsing without saving local history, signing in to a site with a different account, and troubleshooting websites.") + static let burnerHomepageDescription3 = NSLocalizedString("burner.homepage.description.3", value: "Troubleshoot websites", comment: "Descriptions of features Fire page. Provides information about browsing functionalities such as browsing without saving local history, signing in to a site with a different account, and troubleshooting websites.") + static let burnerHomepageDescription4 = NSLocalizedString("burner.homepage.description.4", value: "Fire windows are isolated from other browser data, and their data is burned when you close them. They have the same tracking protection as other windows.", comment: "This describes the functionality of one of out browser feature Fire Window, highlighting their isolation from other browser data and the automatic deletion of their data upon closure. Additionally, it emphasizes that fire windows offer the same level of tracking protection as other browsing windows.") // Email Protection Management static let disableEmailProtectionTitle = NSLocalizedString("disable.email.protection.title", value: "Disable Email Protection Autofill?", comment: "Title for alert shown when user disables email protection") diff --git a/DuckDuckGo/DataImport/View/FileImportView.swift b/DuckDuckGo/DataImport/View/FileImportView.swift index faab1cf62b..0663fb5350 100644 --- a/DuckDuckGo/DataImport/View/FileImportView.swift +++ b/DuckDuckGo/DataImport/View/FileImportView.swift @@ -476,9 +476,9 @@ struct FileImportView: View { { switch dataType { case .bookmarks: - Text("Import Bookmarks") + Text("Import Bookmarks", comment: "Title of dialog with instruction for the user to import bookmarks from another browser") case .passwords: - Text("Import Passwords") + Text("Import Passwords", comment: "Title of dialog with instruction for the user to import passwords from another browser") } }().bold() diff --git a/DuckDuckGo/DataImport/View/ReportFeedbackView.swift b/DuckDuckGo/DataImport/View/ReportFeedbackView.swift index 577b779cc6..dc35dc0745 100644 --- a/DuckDuckGo/DataImport/View/ReportFeedbackView.swift +++ b/DuckDuckGo/DataImport/View/ReportFeedbackView.swift @@ -52,7 +52,7 @@ struct ReportFeedbackView: View { Text("The version of the browser you are trying to import from", comment: "Data import failure Report dialog description of a report field providing version of a browser user is trying to import data from") } InfoItemView(model.error.localizedDescription) { - Text("Error message & code", comment: "") + Text("Error message & code", comment: "Title of the section of a dialog (form where the user can report feedback) where the error message and the error code are shown") } } .padding(.bottom, 24) diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 6171229925..faef1b6820 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -230,7 +230,7 @@ } }, "Address" : { - + "comment" : "Title of the section of the Identities manager where the user can add/modify an address (street city etc,)" }, "Address:" : { "comment" : "Add Bookmark dialog bookmark url field heading" @@ -986,7 +986,7 @@ } }, "Birthday" : { - + "comment" : "Title of the section of the Identities manager where the user can add/modify a date of birth" }, "bitwarden.app.found" : { "comment" : "Setup of the integration with Bitwarden app", @@ -1013,6 +1013,7 @@ } }, "bitwarden.connect.communication-info" : { + "comment" : "Warns users that all communication between the DuckDuckGo browser and the password manager Bitwarden is encrypted and doesn't leave the user device", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1024,6 +1025,7 @@ } }, "bitwarden.connect.description" : { + "comment" : "Description for when the user wants to connect the browser to the password manager Bitwarned.", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1070,6 +1072,7 @@ } }, "bitwarden.connecting" : { + "comment" : "It appears in a dialog when the users are connecting to Bitwardern and shows the status of the action, in this case we are in the progress of connecting the browser to the Bitwarden password maanger.", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1081,6 +1084,7 @@ } }, "bitwarden.error" : { + "comment" : "This message appears when the application is unable to find or connect to Bitwarden, indicating a connection issue.", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1092,6 +1096,7 @@ } }, "bitwarden.handshake.not.approved" : { + "comment" : "It appears in a dialog when the users are connecting to Bitwardern and shows the status of the action. This message indicates that the handshake process was not approved in the Bitwarden app.", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1151,6 +1156,7 @@ } }, "bitwarden.integration.not.approved" : { + "comment" : "While the user tries to connect the DuckDuckGo Browser to password manager Bitwarden This message indicates that the integration with DuckDuckGo has not been approved in the Bitwarden app.", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1174,6 +1180,7 @@ } }, "bitwarden.missing.handshake" : { + "comment" : "While the user tries to connect the DuckDuckGo Browser to password manager Bitwarden This message indicates a missing handshake (a way for two devices or systems to say hello to each other and agree to communicate or exchange information).", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1196,6 +1203,7 @@ } }, "bitwarden.old.version" : { + "comment" : "Message that warns user they need to update their password manager Bitwarden app vesion", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1207,6 +1215,7 @@ } }, "bitwarden.preferences.complete-setup" : { + "comment" : "action option that prompts the user to complete the setup process in Bitwarden preferences", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1218,6 +1227,7 @@ } }, "bitwarden.preferences.open-bitwarden" : { + "comment" : "Button to open Bitwarden app", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1265,6 +1275,7 @@ } }, "bitwarden.waiting.for.handshake" : { + "comment" : "While the user tries to connect the DuckDuckGo Browser to password manager Bitwarden This message indicates the system is waiting for the handshake (a way for two devices or systems to say hello to each other and agree to communicate or exchange information).", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1288,6 +1299,7 @@ } }, "bitwarden.waiting.for.status.response" : { + "comment" : "It appears in a dialog when the users are connecting to Bitwardern and shows the status of the action, in this case that the application is currently waiting for a response from the Bitwarden service.", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1773,6 +1785,7 @@ "comment" : "Main Menu Window item" }, "burner.homepage.description.1" : { + "comment" : "Descriptions of features Fire page. Provides information about browsing functionalities such as browsing without saving local history, signing in to a site with a different account, and troubleshooting websites.", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1784,6 +1797,7 @@ } }, "burner.homepage.description.2" : { + "comment" : "Descriptions of features Fire page. Provides information about browsing functionalities such as browsing without saving local history, signing in to a site with a different account, and troubleshooting websites.", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1795,6 +1809,7 @@ } }, "burner.homepage.description.3" : { + "comment" : "Descriptions of features Fire page. Provides information about browsing functionalities such as browsing without saving local history, signing in to a site with a different account, and troubleshooting websites.", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1806,6 +1821,7 @@ } }, "burner.homepage.description.4" : { + "comment" : "This describes the functionality of one of out browser feature Fire Window, highlighting their isolation from other browser data and the automatic deletion of their data upon closure. Additionally, it emphasizes that fire windows offer the same level of tracking protection as other browsing windows.", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1979,7 +1995,7 @@ } }, "Contact Info" : { - + "comment" : "Title of the section of the Identities manager where the user can add/modify contact info (phone, email address)" }, "copy" : { "comment" : "Copy button", @@ -2045,7 +2061,7 @@ } }, "Country" : { - + "comment" : "Title of the section of the Identities manager where the user can add/modify a country (US,UK, Italy etc...)" }, "crash-report.description" : { "comment" : "Description of the dialog where the user can send a crash report", @@ -2195,6 +2211,7 @@ } }, "default.browser.prompt.button" : { + "comment" : "represents a prompt message asking the user to make DuckDuckGo their default browser.", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -2206,6 +2223,7 @@ } }, "default.browser.prompt.message" : { + "comment" : "represents a prompt message asking the user to make DuckDuckGo their default browser.", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -2888,7 +2906,7 @@ "comment" : "Main Menu View item" }, "Error message & code" : { - + "comment" : "Title of the section of a dialog (form where the user can report feedback) where the error message and the error code are shown" }, "error.unknown.try.again" : { "comment" : "Generic error message on a dialog for when the cause is not known.", @@ -3749,6 +3767,7 @@ } }, "home.page.empty.state.item.message" : { + "comment" : "This string represents the message for an empty state item on the home page, encouraging the user to keep browsing to see how many trackers were blocked", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -3760,6 +3779,7 @@ } }, "home.page.empty.state.item.title" : { + "comment" : "This string represents the title for an empty state item on the home page, indicating that recently visited sites will appear here", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -3771,6 +3791,7 @@ } }, "home.page.no.trackers.blocked" : { + "comment" : "This string represents a message on the home page indicating that no trackers were blocked", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -3782,6 +3803,7 @@ } }, "home.page.no.trackers.found" : { + "comment" : "This string represents a message on the home page indicating that no trackers were found", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -3805,6 +3827,7 @@ } }, "home.page.protection.summary.info" : { + "comment" : "This string represents a message in the protection summary on the home page, indicating that there is no recent activity", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -3834,10 +3857,10 @@ "comment" : "Menu item" }, "Import Bookmarks" : { - + "comment" : "Title of dialog with instruction for the user to import bookmarks from another browser" }, "Import Passwords" : { - "comment" : "my comment" + "comment" : "Title of dialog with instruction for the user to import passwords from another browser" }, "Import Results:" : { "comment" : "Data Import result summary headline" @@ -4695,7 +4718,7 @@ } }, "main-menu.edit.paste-and-match-style" : { - "comment" : "Main Menu Edit item", + "comment" : "Main Menu Edit item - Action that allows the user to paste copy into a target document and the target document's style will be retained (instead of the source style)", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -4911,7 +4934,7 @@ } }, "main-menu.file.open-location" : { - "comment" : "Main Menu File item", + "comment" : "Main Menu File item- Menu option that allows the user to connect to an address (type an address) on click the address bar of the browser is selected and the user can type.", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -5856,6 +5879,7 @@ } }, "open.settings" : { + "comment" : "This string represents a prompt or button label prompting the user to open system settings", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -7118,6 +7142,7 @@ } }, "pm.lock-screen.duration" : { + "comment" : "Message about the duration for which autofill information remains unlocked on the lock screen.", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -7873,6 +7898,7 @@ } }, "preferences.about.unsupported-device-info1" : { + "comment" : "This string represents a message informing the user that DuckDuckGo is no longer providing browser updates for their version of macOS", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -8052,6 +8078,7 @@ } }, "preferences.default-browser.button.make-default" : { + "comment" : "represents a prompt message asking the user to make DuckDuckGo their default browser.", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -8270,6 +8297,7 @@ "comment" : "Main Menu History item" }, "reopen.last.closed.tab" : { + "comment" : "This string represents an action to reopen the last closed tab in the browser", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -8281,6 +8309,7 @@ } }, "reopen.last.closed.window" : { + "comment" : "This string represents an action to reopen the last closed window in the browser", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -8316,6 +8345,7 @@ } }, "restart.bitwarden.info" : { + "comment" : "This string represents a message informing the user that Bitwarden is not responding and prompts them to restart the application to initiate communication again.", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -9083,6 +9113,7 @@ } }, "unsupported.device.info.alert.header" : { + "comment" : "his string represents the header for an alert informing the user that their version of macOS is no longer supported", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -9185,4 +9216,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/DuckDuckGo/SecureVault/Extensions/UserText+PasswordManager.swift b/DuckDuckGo/SecureVault/Extensions/UserText+PasswordManager.swift index 39f7872e27..c61f85672a 100644 --- a/DuckDuckGo/SecureVault/Extensions/UserText+PasswordManager.swift +++ b/DuckDuckGo/SecureVault/Extensions/UserText+PasswordManager.swift @@ -107,7 +107,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 %@.", - comment: "") + comment: "Message about the duration for which autofill information remains unlocked on the lock screen.") return String(format: localized, duration) } diff --git a/DuckDuckGo/SecureVault/View/PasswordManagementIdentityItemView.swift b/DuckDuckGo/SecureVault/View/PasswordManagementIdentityItemView.swift index c82e1b5126..639c11a82f 100644 --- a/DuckDuckGo/SecureVault/View/PasswordManagementIdentityItemView.swift +++ b/DuckDuckGo/SecureVault/View/PasswordManagementIdentityItemView.swift @@ -106,7 +106,7 @@ private struct IdentificationView: View { EditableIdentityField(textFieldValue: $model.lastName, title: UserText.pmLastName) if model.isInEditMode { - Text("Birthday") + Text("Birthday", comment: "Title of the section of the Identities manager where the user can add/modify a date of birth") .bold() .padding(.bottom, 5) @@ -219,7 +219,7 @@ private struct AddressView: View { !model.addressPostalCode.isEmpty || !model.addressCountryCode.isEmpty || model.isInEditMode { - Text("Address") + Text("Address", comment: "Title of the section of the Identities manager where the user can add/modify an address (street city etc,)") .bold() .foregroundColor(.gray) .padding(.bottom, 20) @@ -232,7 +232,7 @@ private struct AddressView: View { EditableIdentityField(textFieldValue: $model.addressPostalCode, title: UserText.pmAddressPostalCode) if model.isInEditMode { - Text("Country") + Text("Country", comment: "Title of the section of the Identities manager where the user can add/modify a country (US,UK, Italy etc...)") .bold() .padding(.bottom, 5) @@ -252,7 +252,7 @@ private struct AddressView: View { .padding(.bottom, 5) } else if !model.addressCountryCode.isEmpty { - Text("Country") + Text("Country", comment: "Title of the section of the Identities manager where the user can add/modify a country (US,UK, Italy etc...)") .bold() .padding(.bottom, 5) @@ -273,7 +273,7 @@ private struct ContactInfoView: View { VStack(alignment: .leading, spacing: 0) { if !model.homePhone.isEmpty || !model.mobilePhone.isEmpty || !model.emailAddress.isEmpty || model.isInEditMode { - Text("Contact Info") + Text("Contact Info", comment: "Title of the section of the Identities manager where the user can add/modify contact info (phone, email address)") .bold() .foregroundColor(.gray) .padding(.bottom, 20) diff --git a/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/ButtonStyles.swift b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/ButtonStyles.swift index f0dd3f4171..2c2aad1b82 100644 --- a/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/ButtonStyles.swift +++ b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/ButtonStyles.swift @@ -55,8 +55,9 @@ public struct DefaultActionButtonStyle: ButtonStyle { let labelColor = enabled ? Color.white : Color.primary.opacity(0.3) configuration.label - .lineLimit(1) .font(.system(size: 13)) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) .frame(minWidth: 44) // OK buttons will match the width of "Cancel" at least in English .padding(.top, 2.5) .padding(.bottom, 3) diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/Dialogs/SyncWithAnotherDeviceView.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/Dialogs/SyncWithAnotherDeviceView.swift index 7c24341d57..61ce3d1bb8 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Views/Dialogs/SyncWithAnotherDeviceView.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/Dialogs/SyncWithAnotherDeviceView.swift @@ -56,7 +56,8 @@ struct SyncWithAnotherDeviceView: View { Spacer(minLength: 0) } .padding(.top, 16) - .frame(width: 380, height: 332) + .frame(height: 332) + .frame(minWidth: 380) .roundedBorder() } @@ -83,7 +84,8 @@ struct SyncWithAnotherDeviceView: View { pickerOptionView(imageName: "QR-Icon", title: UserText.syncWithAnotherDeviceShowCodeButton, tag: 0) pickerOptionView(imageName: "Keyboard-16D", title: UserText.syncWithAnotherDeviceEnterCodeButton, tag: 1) } - .frame(width: 348, height: 32) + .frame(height: 32) + .frame(minWidth: 348) .roundedBorder() } @@ -96,7 +98,9 @@ struct SyncWithAnotherDeviceView: View { Image(imageName) Text(title) } - .frame(width: 172, height: 28) + .frame(height: 28) + .frame(minWidth: 172) + .padding(.horizontal, 8) .background( ZStack { RoundedRectangle(cornerRadius: 8) diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/ManagementView/SyncSetupView.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/ManagementView/SyncSetupView.swift index e1d77dcb9f..6743f311ce 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Views/ManagementView/SyncSetupView.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/ManagementView/SyncSetupView.swift @@ -28,7 +28,6 @@ struct SyncSetupView: View where ViewModel: ManagementViewModel { syncUnavailableView() syncWithAnotherDeviceView() SyncUIViews.TextDetailSecondary(text: UserText.beginSyncFooter) - .frame(height: 28) .padding(.bottom, 24) .padding(.horizontal, 110) .font(.system(size: 11)) @@ -109,10 +108,10 @@ private struct SyncWithAnotherDeviceButtonStyle: ButtonStyle { configuration.label .lineLimit(1) .font(.body.bold()) - .frame(width: 220, height: 32) + .frame(height: 32) + .padding(.horizontal, 24) .background(enabled ? enabledBackgroundColor : disabledBackgroundColor) .foregroundColor(labelColor) .cornerRadius(8) - } } diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/internal/SyncUIViews.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/internal/SyncUIViews.swift index 10e33ed47f..b36dd20085 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Views/internal/SyncUIViews.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/internal/SyncUIViews.swift @@ -27,6 +27,8 @@ enum SyncUIViews { Text(text) .bold() .font(.system(size: 17)) + .fixedSize(horizontal: false, vertical: true) + .multilineTextAlignment(.center) } } @@ -47,7 +49,7 @@ enum SyncUIViews { var body: some View { Text(text) - .fixedSize(horizontal: false, vertical: /*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/) + .fixedSize(horizontal: false, vertical: true) .multilineTextAlignment(.center) } } From 48bbfe1525c82d22500930bf537049960a19a31a Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Tue, 20 Feb 2024 19:58:32 +0600 Subject: [PATCH 26/44] drop TabPreview.storyboard (#2222) Task/Issue URL: https://app.asana.com/0/0/1206632173901383/f --- DuckDuckGo.xcodeproj/project.pbxproj | 8 - .../TabBar/View/TabBarViewController.swift | 6 +- DuckDuckGo/TabPreview/TabPreview.storyboard | 127 ------------- .../TabPreview/TabPreviewViewController.swift | 178 ++++++++++++++++-- .../TabPreviewWindowController.swift | 27 ++- 5 files changed, 189 insertions(+), 157 deletions(-) delete mode 100644 DuckDuckGo/TabPreview/TabPreview.storyboard diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 1ddafe2ae6..0f1e8a2d3e 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -666,7 +666,6 @@ 3706FCC0293F65D500E42796 /* FindInPage.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 85A0117325AF2EDF00FA6A0C /* FindInPage.storyboard */; }; 3706FCC3293F65D500E42796 /* userscript.js in Resources */ = {isa = PBXBuildFile; fileRef = B31055BE27A1BA1D001AC618 /* userscript.js */; }; 3706FCC4293F65D500E42796 /* fb-tds.json in Resources */ = {isa = PBXBuildFile; fileRef = EA4617EF273A28A700F110A2 /* fb-tds.json */; }; - 3706FCC5293F65D500E42796 /* TabPreview.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AAE8B101258A41C000E81239 /* TabPreview.storyboard */; }; 3706FCC6293F65D500E42796 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = AA68C3D62490F821001B8783 /* README.md */; }; 3706FCC8293F65D500E42796 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AA585D85248FD31400E9A3E2 /* Assets.xcassets */; }; 3706FCC9293F65D500E42796 /* NavigationBar.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 85589E8C27BBBB870038AD11 /* NavigationBar.storyboard */; }; @@ -1950,7 +1949,6 @@ 4B957BFD2AC7AE700062CA31 /* JSAlert.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = EEC111E3294D06020086524F /* JSAlert.storyboard */; }; 4B957C002AC7AE700062CA31 /* userscript.js in Resources */ = {isa = PBXBuildFile; fileRef = B31055BE27A1BA1D001AC618 /* userscript.js */; }; 4B957C012AC7AE700062CA31 /* fb-tds.json in Resources */ = {isa = PBXBuildFile; fileRef = EA4617EF273A28A700F110A2 /* fb-tds.json */; }; - 4B957C022AC7AE700062CA31 /* TabPreview.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AAE8B101258A41C000E81239 /* TabPreview.storyboard */; }; 4B957C032AC7AE700062CA31 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = AA68C3D62490F821001B8783 /* README.md */; }; 4B957C042AC7AE700062CA31 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AA585D85248FD31400E9A3E2 /* Assets.xcassets */; }; 4B957C052AC7AE700062CA31 /* NavigationBar.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 85589E8C27BBBB870038AD11 /* NavigationBar.storyboard */; }; @@ -2554,7 +2552,6 @@ AAE7527C263B056C00B973F8 /* HistoryStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE7527B263B056C00B973F8 /* HistoryStore.swift */; }; AAE7527E263B05C600B973F8 /* HistoryEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE7527D263B05C600B973F8 /* HistoryEntry.swift */; }; AAE75280263B0A4D00B973F8 /* HistoryCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE7527F263B0A4D00B973F8 /* HistoryCoordinator.swift */; }; - AAE8B102258A41C000E81239 /* TabPreview.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AAE8B101258A41C000E81239 /* TabPreview.storyboard */; }; AAE8B110258A456C00E81239 /* TabPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE8B10F258A456C00E81239 /* TabPreviewViewController.swift */; }; AAE99B8927088A19008B6BD9 /* FirePopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE99B8827088A19008B6BD9 /* FirePopover.swift */; }; AAEC74B22642C57200C2EFBC /* HistoryCoordinatingMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAEC74B12642C57200C2EFBC /* HistoryCoordinatingMock.swift */; }; @@ -4098,7 +4095,6 @@ AAE7527B263B056C00B973F8 /* HistoryStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryStore.swift; sourceTree = ""; }; AAE7527D263B05C600B973F8 /* HistoryEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryEntry.swift; sourceTree = ""; }; AAE7527F263B0A4D00B973F8 /* HistoryCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryCoordinator.swift; sourceTree = ""; }; - AAE8B101258A41C000E81239 /* TabPreview.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = TabPreview.storyboard; sourceTree = ""; }; AAE8B10F258A456C00E81239 /* TabPreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabPreviewViewController.swift; sourceTree = ""; }; AAE99B8827088A19008B6BD9 /* FirePopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirePopover.swift; sourceTree = ""; }; AAEC74B12642C57200C2EFBC /* HistoryCoordinatingMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryCoordinatingMock.swift; sourceTree = ""; }; @@ -7656,7 +7652,6 @@ AAE8B0FD258A416F00E81239 /* TabPreview */ = { isa = PBXGroup; children = ( - AAE8B101258A41C000E81239 /* TabPreview.storyboard */, AAC82C5F258B6CB5009B6B42 /* TabPreviewWindowController.swift */, AAE8B10F258A456C00E81239 /* TabPreviewViewController.swift */, 1DB67F272B6FE21D003DF243 /* Model */, @@ -8873,7 +8868,6 @@ EEC8EB3F2982CA440065AA39 /* JSAlert.storyboard in Resources */, 3706FCC3293F65D500E42796 /* userscript.js in Resources */, 3706FCC4293F65D500E42796 /* fb-tds.json in Resources */, - 3706FCC5293F65D500E42796 /* TabPreview.storyboard in Resources */, 3706FCC6293F65D500E42796 /* README.md in Resources */, 3706FCC8293F65D500E42796 /* Assets.xcassets in Resources */, 3706FCC9293F65D500E42796 /* NavigationBar.storyboard in Resources */, @@ -9000,7 +8994,6 @@ 4B957BFD2AC7AE700062CA31 /* JSAlert.storyboard in Resources */, 4B957C002AC7AE700062CA31 /* userscript.js in Resources */, 4B957C012AC7AE700062CA31 /* fb-tds.json in Resources */, - 4B957C022AC7AE700062CA31 /* TabPreview.storyboard in Resources */, 4B957C032AC7AE700062CA31 /* README.md in Resources */, 4B957C042AC7AE700062CA31 /* Assets.xcassets in Resources */, 4B957C052AC7AE700062CA31 /* NavigationBar.storyboard in Resources */, @@ -9096,7 +9089,6 @@ EEC111E4294D06020086524F /* JSAlert.storyboard in Resources */, B31055C627A1BA1D001AC618 /* userscript.js in Resources */, EA4617F0273A28A700F110A2 /* fb-tds.json in Resources */, - AAE8B102258A41C000E81239 /* TabPreview.storyboard in Resources */, AA68C3D72490F821001B8783 /* README.md in Resources */, AA585D86248FD31400E9A3E2 /* Assets.xcassets in Resources */, 85589E8D27BBBB870038AD11 /* NavigationBar.storyboard in Resources */, diff --git a/DuckDuckGo/TabBar/View/TabBarViewController.swift b/DuckDuckGo/TabBar/View/TabBarViewController.swift index 6142ee5d3b..919cd75eff 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewController.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewController.swift @@ -558,11 +558,7 @@ final class TabBarViewController: NSViewController { // MARK: - Tab Preview - private var tabPreviewWindowController: TabPreviewWindowController = { - let storyboard = NSStoryboard(name: "TabPreview", bundle: nil) - // swiftlint:disable:next force_cast - return storyboard.instantiateController(withIdentifier: "TabPreviewWindowController") as! TabPreviewWindowController - }() + private lazy var tabPreviewWindowController = TabPreviewWindowController() private func showTabPreview(for tabBarViewItem: TabBarViewItem) { guard let indexPath = collectionView.indexPath(for: tabBarViewItem), diff --git a/DuckDuckGo/TabPreview/TabPreview.storyboard b/DuckDuckGo/TabPreview/TabPreview.storyboard deleted file mode 100644 index f8ffbfe7b0..0000000000 --- a/DuckDuckGo/TabPreview/TabPreview.storyboard +++ /dev/null @@ -1,127 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/DuckDuckGo/TabPreview/TabPreviewViewController.swift b/DuckDuckGo/TabPreview/TabPreviewViewController.swift index 67c846ba7e..e805efac9c 100644 --- a/DuckDuckGo/TabPreview/TabPreviewViewController.swift +++ b/DuckDuckGo/TabPreview/TabPreviewViewController.swift @@ -18,34 +18,119 @@ import Cocoa -final class TabPreviewViewController: NSViewController { - - @IBOutlet weak var titleTextField: NSTextField! - @IBOutlet weak var urlTextField: NSTextField! - @IBOutlet weak var snapshotImageView: NSImageView! - @IBOutlet weak var snapshotImageViewHeightConstraint: NSLayoutConstraint! +protocol Previewable { + var shouldShowPreview: Bool { get } + var title: String { get } + var tabContent: Tab.TabContent { get } + var snapshot: NSImage? { get } } -extension TabPreviewViewController { +final class TabPreviewViewController: NSViewController { enum TextFieldMaskGradientSize: CGFloat { case width = 6 case trailingSpace = 12 } - override func viewDidLoad() { - super.viewDidLoad() + private lazy var viewColorView = ColorView(frame: .zero, backgroundColor: .controlBackgroundColor) + private lazy var titleTextField = NSTextField() + private lazy var urlTextField = NSTextField() + private lazy var box = NSBox() + private lazy var snapshotImageView = NSImageView() + + private var snapshotImageViewHeightConstraint: NSLayoutConstraint! + + init() { + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("\(Self.self): Bad initializer") + } + + override func loadView() { + view = NSView() + + view.addSubview(viewColorView) + view.addSubview(titleTextField) + view.addSubview(urlTextField) + view.addSubview(box) + view.addSubview(snapshotImageView) + + snapshotImageView.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal) + snapshotImageView.setContentHuggingPriority(.init(rawValue: 251), for: .vertical) + snapshotImageView.translatesAutoresizingMaskIntoConstraints = false + snapshotImageView.imageScaling = .scaleProportionallyDown + box.boxType = .separator + box.setContentHuggingPriority(.defaultHigh, for: .vertical) + box.translatesAutoresizingMaskIntoConstraints = false + + urlTextField.isEditable = false + urlTextField.isBordered = false + urlTextField.setContentHuggingPriority(.defaultHigh, for: .vertical) + urlTextField.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal) + urlTextField.translatesAutoresizingMaskIntoConstraints = false + urlTextField.backgroundColor = .textBackgroundColor + urlTextField.font = .systemFont(ofSize: 13) + urlTextField.lineBreakMode = .byTruncatingTail + urlTextField.textColor = .tabPreviewSecondaryTint + + titleTextField.isEditable = false + titleTextField.isBordered = false + titleTextField.setContentHuggingPriority(.defaultHigh, for: .vertical) + titleTextField.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal) + titleTextField.translatesAutoresizingMaskIntoConstraints = false + titleTextField.backgroundColor = .textBackgroundColor + titleTextField.font = .systemFont(ofSize: 13, weight: .medium) + titleTextField.textColor = .tabPreviewTint titleTextField.maximumNumberOfLines = 3 titleTextField.cell?.truncatesLastVisibleLine = true + + viewColorView.translatesAutoresizingMaskIntoConstraints = false + + setupLayout() + } + + private func setupLayout() { + + viewColorView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true + titleTextField.topAnchor.constraint(equalTo: viewColorView.topAnchor, constant: 10).isActive = true + box.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true + view.trailingAnchor.constraint(equalTo: snapshotImageView.trailingAnchor).isActive = true + view.trailingAnchor.constraint(equalTo: viewColorView.trailingAnchor).isActive = true + urlTextField.topAnchor.constraint(equalTo: titleTextField.bottomAnchor, constant: 4).isActive = true + viewColorView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true + view.bottomAnchor.constraint(equalTo: snapshotImageView.bottomAnchor).isActive = true + titleTextField.topAnchor.constraint(equalTo: view.topAnchor, constant: 10).isActive = true + snapshotImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true + urlTextField.bottomAnchor.constraint(equalTo: viewColorView.bottomAnchor, constant: -12).isActive = true + titleTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16).isActive = true + view.trailingAnchor.constraint(equalTo: box.trailingAnchor).isActive = true + urlTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16).isActive = true + view.trailingAnchor.constraint(equalTo: urlTextField.trailingAnchor, constant: 8).isActive = true + view.trailingAnchor.constraint(equalTo: titleTextField.trailingAnchor, constant: 8).isActive = true + box.bottomAnchor.constraint(equalTo: viewColorView.bottomAnchor).isActive = true + snapshotImageView.topAnchor.constraint(equalTo: viewColorView.bottomAnchor).isActive = true + + box.heightAnchor.constraint(equalToConstant: 1).isActive = true + + titleTextField.widthAnchor.constraint(equalToConstant: 256).isActive = true + + viewColorView.heightAnchor.constraint(greaterThanOrEqualToConstant: 57).isActive = true + + snapshotImageViewHeightConstraint = snapshotImageView.heightAnchor.constraint(equalToConstant: 0) + snapshotImageViewHeightConstraint.isActive = true } - func display(tabViewModel: TabViewModel, isSelected: Bool) { + func display(tabViewModel: Previewable, isSelected: Bool) { + _=view // load view if needed + titleTextField.stringValue = tabViewModel.title titleTextField.lineBreakMode = isSelected ? .byWordWrapping : .byTruncatingTail - switch tabViewModel.tab.content { + switch tabViewModel.tabContent { case .url(let url, credential: _, source: _): urlTextField.stringValue = url.toString(decodePunycode: true, dropScheme: true, @@ -57,9 +142,9 @@ extension TabPreviewViewController { urlTextField.stringValue = "" } - if !isSelected, !tabViewModel.isShowingErrorPage, let snapshot = tabViewModel.tab.tabSnapshot { + if !isSelected, tabViewModel.shouldShowPreview, let snapshot = tabViewModel.snapshot { snapshotImageView.image = snapshot - snapshotImageViewHeightConstraint.constant = getHeight(for: tabViewModel.tab.tabSnapshot) + snapshotImageViewHeightConstraint.constant = getHeight(for: snapshot) } else { snapshotImageView.image = nil snapshotImageViewHeightConstraint.constant = 0 @@ -76,3 +161,70 @@ extension TabPreviewViewController { } } + +extension TabViewModel: Previewable { + + var shouldShowPreview: Bool { + !isShowingErrorPage + } + + var snapshot: NSImage? { + tab.tabSnapshot + } + + var tabContent: Tab.TabContent { + tab.content + } + +} + +#if DEBUG +extension TabPreviewViewController { + func displayMockPreview(of size: NSSize, withTitle title: String, content: Tab.TabContent, previewable: Bool, isSelected: Bool) { + + struct PreviewableMock: Previewable { + let size: NSSize + let title: String + var tabContent: Tab.TabContent + let shouldShowPreview: Bool + + var snapshot: NSImage? { + let image = NSImage(size: size) + image.lockFocus() + NSColor(deviceRed: 0.95, green: 0.98, blue: 0.99, alpha: 1).setFill() + NSRect(origin: .zero, size: image.size).fill() + image.unlockFocus() + return image + } + } + + self.display(tabViewModel: PreviewableMock(size: size, title: title, tabContent: content, shouldShowPreview: previewable), isSelected: isSelected) + } +} + +import Combine +private let previewSize = NSSize(width: 280, height: 220) + +@available(macOS 14.0, *) +#Preview(traits: .fixedLayout(width: previewSize.width, height: previewSize.height)) { { + + let vc = TabPreviewViewController() + vc.displayMockPreview(of: NSSize(width: 1280, height: 560), + withTitle: "Some reasonably long tab preview title that won‘t fit in one line", + content: .url(.makeSearchUrl(from: "SERP query string to search for some ducks")!, source: .ui), + previewable: true, + isSelected: true) + + var c: AnyCancellable! + c = vc.publisher(for: \.view.window).sink { window in + window?.titlebarAppearsTransparent = true + window?.titleVisibility = .hidden + window?.styleMask = [] + window?.setFrame(NSRect(origin: .zero, size: vc.view.bounds.size), display: true) + withExtendedLifetime(c) {} + } + + return vc + +}() } +#endif diff --git a/DuckDuckGo/TabPreview/TabPreviewWindowController.swift b/DuckDuckGo/TabPreview/TabPreviewWindowController.swift index 6e4e5f4456..f9fca79fcd 100644 --- a/DuckDuckGo/TabPreview/TabPreviewWindowController.swift +++ b/DuckDuckGo/TabPreview/TabPreviewWindowController.swift @@ -34,20 +34,39 @@ final class TabPreviewWindowController: NSWindowController { // swiftlint:disable force_cast var tabPreviewViewController: TabPreviewViewController { - contentViewController as! TabPreviewViewController + return self.window!.contentViewController as! TabPreviewViewController } // swiftlint:enable force_cast - override func windowDidLoad() { - super.windowDidLoad() + init() { + super.init(window: Self.loadWindow()) - window?.animationBehavior = .utilityWindow NotificationCenter.default.addObserver(self, selector: #selector(suggestionWindowOpenNotification(_:)), name: .suggestionWindowOpen, object: nil) } + required init?(coder: NSCoder) { + fatalError("\(Self.self): Bad initializer") + } + + private static func loadWindow() -> NSWindow { + let tabPreviewViewController = TabPreviewViewController() + + let window = NSWindow(contentRect: CGRect(x: 294, y: 313, width: 280, height: 58), styleMask: [.titled, .fullSizeContentView], backing: .buffered, defer: true) + window.contentViewController = tabPreviewViewController + + window.allowsToolTipsWhenApplicationIsInactive = false + window.autorecalculatesKeyViewLoop = false + window.isReleasedWhenClosed = false + window.titleVisibility = .hidden + window.titlebarAppearsTransparent = true + window.animationBehavior = .utilityWindow + + return window + } + deinit { NotificationCenter.default.removeObserver(self) } From 5b1fdd4cd83be0530b69ac101e823e1c386d61e9 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Tue, 20 Feb 2024 15:12:18 +0000 Subject: [PATCH 27/44] Update embedded files --- .../ContentBlocker/AppPrivacyConfigurationDataProvider.swift | 4 ++-- DuckDuckGo/ContentBlocker/macos-config.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift b/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift index e7f25afe8f..e35137c46b 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 = "\"40337866138aa6fdecbc748143547b73\"" - public static let embeddedDataSHA = "76ee226a289fafeccff2cf34de53fb143b280644403b9a87a826cf9b7fcc59cf" + public static let embeddedDataETag = "\"3f7639dcb62ac27e380627dc7391ebf3\"" + public static let embeddedDataSHA = "7c4c52a8d470962fd3d90a1ad9dbbbd26387d92034b001028c1da46f054d482b" } var embeddedDataEtag: String { diff --git a/DuckDuckGo/ContentBlocker/macos-config.json b/DuckDuckGo/ContentBlocker/macos-config.json index 422cb7d83c..db536115a3 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": 1708083739957, + "version": 1708360600128, "features": { "adClickAttribution": { "readme": "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/#3rd-party-tracker-loading-protection", From 7ebfb8639f1ed5cbb78318f35f32d69cbd88ed8a Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Tue, 20 Feb 2024 15:12:18 +0000 Subject: [PATCH 28/44] Set marketing version to 1.76.0 --- Configuration/Version.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/Version.xcconfig b/Configuration/Version.xcconfig index 39ca579f82..bb31eb4eda 100644 --- a/Configuration/Version.xcconfig +++ b/Configuration/Version.xcconfig @@ -1 +1 @@ -MARKETING_VERSION = 1.75.0 +MARKETING_VERSION = 1.76.0 From db05f989252259ff6a873fbfff9416b57ef6c3be Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Tue, 20 Feb 2024 15:21:53 +0000 Subject: [PATCH 29/44] Bump version to 1.76.0 (121) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 459e731b44..ba1a9b2a82 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 120 +CURRENT_PROJECT_VERSION = 121 From 3e583cc4f0a718f44a90f8805de43572a8485e12 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Tue, 20 Feb 2024 16:33:40 +0100 Subject: [PATCH 30/44] Fix a typo in build_notarized.yml --- .github/workflows/build_notarized.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_notarized.yml b/.github/workflows/build_notarized.yml index 9c7fca24c7..8144e8517c 100644 --- a/.github/workflows/build_notarized.yml +++ b/.github/workflows/build_notarized.yml @@ -296,7 +296,7 @@ jobs: run: | aws s3 cp \ ${{ github.workspace }}/${{ steps.create-dmg.outputs.dmg }} \ - s3://${{ env.RELEASE_BUCKET_NAME }}/${{ env.RELEASE_BUCKET_PREFIX }}/ \\ + s3://${{ env.RELEASE_BUCKET_NAME }}/${{ env.RELEASE_BUCKET_PREFIX }}/ \ --acl public-read - name: Report success From ec61d40bee277432f50a00125a4c71748c749acf Mon Sep 17 00:00:00 2001 From: Tom Strba <57389842+tomasstrba@users.noreply.github.com> Date: Wed, 14 Feb 2024 11:08:34 +0100 Subject: [PATCH 31/44] Positioning of the preview fixed for external display (#2200) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1177771139624306/1206591283698064/f **Description**: This PR fixes an issue with previews being at the wrong position on external display when the main MacBook display is set as main Screenshot 2024-02-13 at 2 14 47 PM --- DuckDuckGo/TabPreview/TabPreviewWindowController.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/DuckDuckGo/TabPreview/TabPreviewWindowController.swift b/DuckDuckGo/TabPreview/TabPreviewWindowController.swift index 681a6da639..6e4e5f4456 100644 --- a/DuckDuckGo/TabPreview/TabPreviewWindowController.swift +++ b/DuckDuckGo/TabPreview/TabPreviewWindowController.swift @@ -129,7 +129,8 @@ final class TabPreviewWindowController: NSWindowController { // Make sure preview is presented within screen if let screenVisibleFrame = window.screen?.visibleFrame { - topLeftPoint.x = min(topLeftPoint.x, screenVisibleFrame.width - window.frame.width) + topLeftPoint.x = min(topLeftPoint.x, screenVisibleFrame.origin.x + screenVisibleFrame.width - window.frame.width) + topLeftPoint.x = max(topLeftPoint.x, screenVisibleFrame.origin.x) let windowHeight = window.frame.size.height if topLeftPoint.y <= windowHeight + screenVisibleFrame.origin.y { From 394378e8f3503edefd97384107f50e9c70550667 Mon Sep 17 00:00:00 2001 From: Tomas Strba Date: Tue, 20 Feb 2024 18:05:17 +0100 Subject: [PATCH 32/44] 1.75.1 (121) --- Configuration/BuildNumber.xcconfig | 2 +- Configuration/Version.xcconfig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 459e731b44..ba1a9b2a82 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 120 +CURRENT_PROJECT_VERSION = 121 diff --git a/Configuration/Version.xcconfig b/Configuration/Version.xcconfig index 39ca579f82..18e593a3b9 100644 --- a/Configuration/Version.xcconfig +++ b/Configuration/Version.xcconfig @@ -1 +1 @@ -MARKETING_VERSION = 1.75.0 +MARKETING_VERSION = 1.75.1 From f76e8cc1ef9b836c8775a9509b73d3f34f9350e3 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Wed, 21 Feb 2024 14:45:10 +0600 Subject: [PATCH 33/44] fix onboarding on first launch (#2229) Task/Issue URL: https://app.asana.com/0/1177771139624306/1206646111353020/f Description: Fixes onboarding opening on first launch --- DuckDuckGo/Tab/Model/Tab.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift index 57cad4d4db..aba8f89859 100644 --- a/DuckDuckGo/Tab/Model/Tab.swift +++ b/DuckDuckGo/Tab/Model/Tab.swift @@ -904,7 +904,7 @@ protocol NewWindowPolicyDecisionMaker { func startOnboarding() { userInteractionDialog = nil - webView.load(URLRequest(url: .welcome)) + setContent(.onboarding) } @MainActor(unsafe) From 3295013191ca51a78537915684a313ebd605683b Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Wed, 21 Feb 2024 08:58:53 +0000 Subject: [PATCH 34/44] Bump version to 1.76.0 (122) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index ba1a9b2a82..060339a249 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 121 +CURRENT_PROJECT_VERSION = 122 From 2ff496cc62982263656a1ee9742b5b8db5366f75 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 21 Feb 2024 11:18:40 +0100 Subject: [PATCH 35/44] Fix S3 access secrets --- .github/workflows/publish_dmg_release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish_dmg_release.yml b/.github/workflows/publish_dmg_release.yml index 86c787cb1d..8ccbe6905b 100644 --- a/.github/workflows/publish_dmg_release.yml +++ b/.github/workflows/publish_dmg_release.yml @@ -109,8 +109,8 @@ jobs: - name: Upload to S3 id: upload env: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID_RELEASE_S3 }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY_RELEASE_S3 }} AWS_DEFAULT_REGION: ${{ vars.AWS_DEFAULT_REGION }} run: | # Back up existing appcast2.xml From 60c5f366ba3978e7b975a9a4a422781c0673a79a Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 21 Feb 2024 11:46:56 +0100 Subject: [PATCH 36/44] Update upload_to_s3.sh to use curl for checking files existence on CDN --- scripts/upload_to_s3/upload_to_s3.sh | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/scripts/upload_to_s3/upload_to_s3.sh b/scripts/upload_to_s3/upload_to_s3.sh index ac7979f4c2..ddce4555c6 100755 --- a/scripts/upload_to_s3/upload_to_s3.sh +++ b/scripts/upload_to_s3/upload_to_s3.sh @@ -2,6 +2,7 @@ # Constants S3_PATH="s3://ddg-staticcdn/macos-desktop-browser/" +CDN_PATH="https://staticcdn.duckduckgo.com/macos-desktop-browser/" # Defaults if [[ -n "$CI" ]]; then @@ -149,13 +150,13 @@ for FILENAME in $FILES_TO_UPLOAD; do fi # Check if the file exists on S3 - AWS_CMD="$AWS s3 ls ${S3_PATH}${FILENAME}" - echo "Checking S3 for ${S3_PATH}${FILENAME}..." - if ! $AWS s3 ls "${S3_PATH}${FILENAME}" > /dev/null 2>&1; then - echo "$FILENAME not found on S3. Marking for upload." - MISSING_FILES+=("$FILENAME") + printf '%s' "Checking CDN for ${CDN_PATH}${FILENAME} ... " + if curl -fLSsI "${CDN_PATH}${FILENAME}" >/dev/null 2>&1; then + echo "✅" else - echo "$FILENAME exists on S3. Skipping." + echo "❌" + echo "🚢 Marking $FILENAME for upload." + MISSING_FILES+=("$FILENAME") fi done From 0ae321b117f224ae71a9e10cabe64ce88f4828a6 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Wed, 21 Feb 2024 18:28:57 +0100 Subject: [PATCH 37/44] Fix refreshing of subscription preference pane when switching between panes (#2230) Task/Issue URL: https://app.asana.com/0/1199230911884351/1206654425458681/f Tech Design URL: CC: **Description**: Fix refreshing of subscription preference pane when switching between panes. Also the task var was not nilled so it was only fired once per viewModel instance. Additionally the check for the expiry date of subscription was wrong and it preemptively cleared cached entitlements resulting in blinking and refresh for every settings pane switch. --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --- .../PreferencesSubscriptionModel.swift | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift index 15b6c3ba93..22ab992e79 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift @@ -173,14 +173,18 @@ public final class PreferencesSubscriptionModel: ObservableObject { func fetchAndUpdateSubscriptionDetails() { guard fetchSubscriptionDetailsTask == nil else { return } - fetchSubscriptionDetailsTask = Task { - guard let token = accountManager.accessToken else { return } + fetchSubscriptionDetailsTask = Task { [weak self] in + defer { + self?.fetchSubscriptionDetailsTask = nil + } + + guard let token = self?.accountManager.accessToken else { return } if let cachedDate = SubscriptionService.cachedSubscriptionDetailsResponse?.expiresOrRenewsAt { - updateDescription(for: cachedDate) + self?.updateDescription(for: cachedDate) - if cachedDate.timeIntervalSinceNow > 0 { - self.cachedEntitlements = [] + if cachedDate.timeIntervalSinceNow < 0 { + self?.cachedEntitlements = [] } } @@ -190,13 +194,13 @@ public final class PreferencesSubscriptionModel: ObservableObject { return } - updateDescription(for: response.expiresOrRenewsAt) + self?.updateDescription(for: response.expiresOrRenewsAt) - subscriptionPlatform = response.platform + self?.subscriptionPlatform = response.platform } if case let .success(entitlements) = await AccountManager().fetchEntitlements() { - self.cachedEntitlements = entitlements + self?.cachedEntitlements = entitlements } } } From 5e2a8c0c225cc11787b8422c015b05cf292ee464 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Wed, 21 Feb 2024 18:39:18 +0000 Subject: [PATCH 38/44] DBP API changes for WebUI - Release (#2233) Cherry picked from https://github.com/duckduckgo/macos-browser/pull/2146 Task/Issue URL: https://app.asana.com/0/1204167627774280/1206480273655878/f **Description**: Add url property to data broker model, and changes result for WebUI call to support new UI designs --- .../DataBrokerProtectionDataManager.swift | 22 +++ .../DataBrokerDatabaseBrowserView.swift | 12 +- .../Model/DBPUICommunicationModel.swift | 17 ++ .../Model/DataBroker.swift | 51 +++++ .../Operations/DataBrokerOperation.swift | 2 +- .../DataBrokerProtectionBrokerUpdater.swift | 2 +- .../JSON/advancedbackgroundchecks.com.json | 11 +- .../Resources/JSON/backgroundcheck.run.json | 7 +- .../Resources/JSON/centeda.com.json | 13 +- .../Resources/JSON/clubset.com.json | 9 +- .../Resources/JSON/clustrmaps.com.json | 13 +- .../Resources/JSON/councilon.com.json | 9 +- .../Resources/JSON/curadvisor.com.json | 11 +- .../JSON/cyberbackgroundchecks.com.json | 9 +- .../Resources/JSON/dataveria.com.json | 13 +- .../JSON/fastbackgroundcheck.com.json | 11 +- .../JSON/freepeopledirectory.com.json | 9 +- .../Resources/JSON/inforver.com.json | 13 +- .../Resources/JSON/kwold.com.json | 9 +- .../Resources/JSON/neighbor.report.json | 25 +-- .../Resources/JSON/newenglandfacts.com.json | 9 +- .../Resources/JSON/officialusa.com.json | 11 +- .../JSON/people-background-check.com.json | 9 +- .../Resources/JSON/peoplefinders.com.json | 35 ++-- .../Resources/JSON/peoplesearchnow.com.json | 9 +- .../Resources/JSON/pub360.com.json | 13 +- .../Resources/JSON/publicreports.com.json | 9 +- .../Resources/JSON/quickpeopletrace.com.json | 12 +- .../Resources/JSON/searchpeoplefree.com.json | 9 +- .../JSON/smartbackgroundchecks.com.json | 9 +- .../Resources/JSON/spokeo.com.json | 55 ++++-- .../Resources/JSON/truepeoplesearch.com.json | 9 +- .../Resources/JSON/usa-people-search.com.json | 13 +- .../Resources/JSON/usatrace.com.json | 9 +- .../Resources/JSON/usphonebook.com.json | 13 +- .../Resources/JSON/verecor.com.json | 29 +-- .../Resources/JSON/vericora.com.json | 13 +- .../Resources/JSON/veriforia.com.json | 13 +- .../Resources/JSON/veripages.com.json | 13 +- .../Resources/JSON/virtory.com.json | 9 +- .../Resources/JSON/wellnut.com.json | 9 +- .../DataBrokerProtectionProcessor.swift | 1 - .../Services/EmailService.swift | 6 +- ...DataBrokerProtectionDatabaseProvider.swift | 18 +- .../Storage/Mappers.swift | 3 +- .../Storage/SchedulerSchema.swift | 4 + .../UI/DBPUICommunicationLayer.swift | 10 +- .../DataBrokerProtection/UI/UIMapper.swift | 80 ++++++-- .../BrokerJSONCodableTests.swift | 187 +++++++++++++++++- ...kerProfileQueryOperationManagerTests.swift | 13 +- .../DataBrokerProtectionUpdaterTests.swift | 6 +- .../EmailServiceTests.swift | 6 +- .../MapperToUITests.swift | 54 ++--- .../MismatchCalculatorUseCaseTests.swift | 2 + .../DataBrokerProtectionTests/Mocks.swift | 8 +- .../OperationPreferredDateUpdaterTests.swift | 1 + 56 files changed, 691 insertions(+), 286 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift index 857f174360..52eabbae92 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift @@ -290,4 +290,26 @@ extension InMemoryDataCache: DBPUICommunicationDelegate { return mapper.maintenanceScanState(brokerProfileQueryData) } + + func getDataBrokers() async -> [DBPUIDataBroker] { + brokerProfileQueryData + // 1. We get all brokers (in this list brokers are repeated) + .map { $0.dataBroker } + // 2. We map the brokers to the UI model + .flatMap { dataBroker -> [DBPUIDataBroker] in + var result: [DBPUIDataBroker] = [] + result.append(DBPUIDataBroker(name: dataBroker.name, url: dataBroker.url)) + + for mirrorSite in dataBroker.mirrorSites { + result.append(DBPUIDataBroker(name: mirrorSite.name, url: mirrorSite.url)) + } + return result + } + // 3. We delete duplicates + .reduce(into: [DBPUIDataBroker]()) { (result, dataBroker) in + if !result.contains(where: { $0.url == dataBroker.url }) { + result.append(dataBroker) + } + } + } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBaseBrowser/DataBrokerDatabaseBrowserView.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBaseBrowser/DataBrokerDatabaseBrowserView.swift index 18b5ffa6b6..fd09e77417 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBaseBrowser/DataBrokerDatabaseBrowserView.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBaseBrowser/DataBrokerDatabaseBrowserView.swift @@ -49,6 +49,7 @@ struct DatabaseView: View { @State private var isPopoverVisible = false @State private var selectedData: String = "" let data: [DataBrokerDatabaseBrowserData.Row] + let rowHeight: CGFloat = 40.0 var body: some View { if data.count > 0 { @@ -62,6 +63,11 @@ struct DatabaseView: View { } } + private func spacerHeight(_ geometry: GeometryProxy) -> CGFloat { + let result = geometry.size.height - CGFloat(data.count) * rowHeight + return max(0, result) + } + private func dataView() -> some View { GeometryReader { geometry in ScrollView([.horizontal, .vertical]) { @@ -86,7 +92,8 @@ struct DatabaseView: View { ForEach(row.data.keys.sorted(), id: \.self) { key in VStack { Text("\(row.data[key]?.description ?? "")") - .frame(maxWidth: 200, maxHeight: 50) + .frame(maxWidth: 200) + .frame(height: rowHeight) .frame(minWidth: 60) .onTapGesture { selectedData = row.data[key]?.description ?? "" @@ -100,7 +107,8 @@ struct DatabaseView: View { } } } - Spacer(minLength: geometry.size.height) + Spacer() + .frame(height: spacerHeight(geometry)) } .frame(minWidth: geometry.size.width, minHeight: 0, alignment: .topLeading) } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift index 353e3912df..5f242b34a2 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift @@ -103,12 +103,24 @@ struct DBPUIAddressAtIndex: Codable { /// Message Object representing a data broker struct DBPUIDataBroker: Codable, Hashable { let name: String + let url: String + let date: Double? + + init(name: String, url: String, date: Double? = nil) { + self.name = name + self.url = url + self.date = date + } func hash(into hasher: inout Hasher) { hasher.combine(name) } } +struct DBPUIDataBrokerList: DBPUISendableMessage { + let dataBrokers: [DBPUIDataBroker] +} + /// Message Object representing a requested change to the user profile's brith year struct DBPUIBirthYear: Codable { let year: Int @@ -123,6 +135,7 @@ struct DBPUIDataBrokerProfileMatch: Codable { let addresses: [DBPUIUserProfileAddress] let alternativeNames: [String] let relatives: [String] + let date: Double? // Used in some methods to set the removedDate or found date } /// Protocol to represent a message that can be passed from the host to the UI @@ -139,6 +152,10 @@ struct DBPUIScanAndOptOutMaintenanceState: DBPUISendableMessage { struct DBPUIOptOutMatch: DBPUISendableMessage { let dataBroker: DBPUIDataBroker let matches: Int + let name: String + let alternativeNames: [String] + let addresses: [DBPUIUserProfileAddress] + let date: Double } /// Data representing the initial scan progress diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DataBroker.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DataBroker.swift index fb96638206..e1a17f590b 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DataBroker.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DataBroker.swift @@ -32,13 +32,46 @@ extension Int { struct MirrorSite: Codable, Sendable { let name: String + let url: String let addedAt: Date let removedAt: Date? + + enum CodingKeys: CodingKey { + case name + case url + case addedAt + case removedAt + } + + init(name: String, url: String, addedAt: Date, removedAt: Date? = nil) { + self.name = name + self.url = url + self.addedAt = addedAt + self.removedAt = removedAt + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + name = try container.decode(String.self, forKey: .name) + + // The older versions of the JSON file did not have a URL property. + // When decoding those cases, we fallback to its name, since the name was the URL. + do { + url = try container.decode(String.self, forKey: .url) + } catch { + url = name + } + + addedAt = try container.decode(Date.self, forKey: .addedAt) + removedAt = try? container.decode(Date.self, forKey: .removedAt) + + } } struct DataBroker: Codable, Sendable { let id: Int64? let name: String + let url: String let steps: [Step] let version: String let schedulingConfig: DataBrokerScheduleConfig @@ -51,6 +84,7 @@ struct DataBroker: Codable, Sendable { enum CodingKeys: CodingKey { case name + case url case steps case version case schedulingConfig @@ -60,6 +94,7 @@ struct DataBroker: Codable, Sendable { init(id: Int64? = nil, name: String, + url: String, steps: [Step], version: String, schedulingConfig: DataBrokerScheduleConfig, @@ -68,6 +103,13 @@ struct DataBroker: Codable, Sendable { ) { self.id = id self.name = name + + if url.isEmpty { + self.url = name + } else { + self.url = url + } + self.steps = steps self.version = version self.schedulingConfig = schedulingConfig @@ -78,6 +120,15 @@ struct DataBroker: Codable, Sendable { init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) name = try container.decode(String.self, forKey: .name) + + // The older versions of the JSON file did not have a URL property. + // When decoding those cases, we fallback to its name, since the name was the URL. + do { + url = try container.decode(String.self, forKey: .url) + } catch { + url = name + } + version = try container.decode(String.self, forKey: .version) steps = try container.decode([Step].self, forKey: .steps) schedulingConfig = try container.decode(DataBrokerScheduleConfig.self, forKey: .schedulingConfig) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift index 5777596838..37cd385e4e 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift @@ -99,7 +99,7 @@ extension DataBrokerOperation { if action.needsEmail { do { stageCalculator?.setStage(.emailGenerate) - extractedProfile?.email = try await emailService.getEmail(dataBrokerName: query.dataBroker.name) + extractedProfile?.email = try await emailService.getEmail(dataBrokerURL: query.dataBroker.url) stageCalculator?.fireOptOutEmailGenerate() } catch { await onError(error: DataBrokerProtectionError.emailError(error as? EmailError)) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift index 28c2128a00..f3203334e2 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift @@ -152,7 +152,7 @@ public struct DataBrokerProtectionBrokerUpdater { // 2. If does exist, we check the number version, if the version number is new, we update it // 3. If it does not exist, we add it, and we create the scan operations related to it private func update(_ broker: DataBroker) throws { - guard let savedBroker = try vault.fetchBroker(with: broker.name) else { + guard let savedBroker = try vault.fetchBroker(with: broker.url) else { // The broker does not exist in the current storage. We need to add it. try add(broker) return diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/advancedbackgroundchecks.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/advancedbackgroundchecks.com.json index 60823c78ac..ee35af6d17 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/advancedbackgroundchecks.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/advancedbackgroundchecks.com.json @@ -1,8 +1,9 @@ { - "name": "advancedbackgroundchecks.com", - "version": "0.1.0", + "name": "AdvancedBackgroundChecks", + "url": "advancedbackgroundchecks.com", + "version": "0.1.4", "parent": "peoplefinders.com", - "addedDatetime": 1678078800000, + "addedDatetime": 1678082400000, "steps": [ { "stepType": "scan", @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "ef8031e6-5e61-4183-b57e-7df156c7129a", + "id": "c73ba931-9e01-4d37-9e15-2fd7a14eefa3", "url": "https://www.advancedbackgroundchecks.com/names/${firstName}-${lastName}_${city}-${state}_age_${age}" }, { "actionType": "extract", - "id": "f3ed744c-6cfc-4a99-b46e-6095587eadfc", + "id": "94003082-0a9d-4418-ac88-68595c7f4953", "selector": ".card-block", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/backgroundcheck.run.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/backgroundcheck.run.json index 5b3800cfcf..552923ca1f 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/backgroundcheck.run.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/backgroundcheck.run.json @@ -1,6 +1,7 @@ { "name": "backgroundcheck.run", - "version": "0.1.1", + "url": "backgroundcheck.run", + "version": "0.1.4", "parent": "verecor.com", "addedDatetime": 1677736800000, "steps": [ @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "aa12b430-8e5d-4c64-bb77-2961f19a1bc8", + "id": "5f90e39f-cb94-4b8d-94ed-48ba0060dc08", "url": "https://backgroundcheck.run/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}" }, { "actionType": "extract", - "id": "75fd2e16-d84a-4bbe-9cf1-79c6d1cc4dec", + "id": "3225fa15-4e00-4e6a-bfc7-a85dfb504c86", "selector": ".b-pfl-list", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/centeda.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/centeda.com.json index 130c996369..bb15f0093f 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/centeda.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/centeda.com.json @@ -1,8 +1,9 @@ { - "name": "centeda.com", - "version": "0.1.1", + "name": "Centeda", + "url": "centeda.com", + "version": "0.1.4", "parent": "verecor.com", - "addedDatetime": 1677733200000, + "addedDatetime": 1677736800000, "steps": [ { "stepType": "scan", @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "25990359-3d58-45de-bdfd-d524b1946e57", + "id": "2f6639c0-201f-4d5e-8467-ae0ba457b409", "url": "https://centeda.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "7108af78-dbbf-47ec-8bb9-e44be505993e", + "id": "e2e236b0-515b-43b3-9154-0432ed9b7566", "selector": ".search-item", "profile": { "name": { @@ -63,4 +64,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/clubset.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/clubset.com.json index f871133c15..8b6801fc48 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/clubset.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/clubset.com.json @@ -1,6 +1,7 @@ { - "name": "clubset.com", - "version": "0.1.1", + "name": "Clubset", + "url": "clubset.com", + "version": "0.1.4", "parent": "verecor.com", "addedDatetime": 1702965600000, "steps": [ @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "917f5d40-2011-4fe5-9ef6-136d6bfaea35", + "id": "5c559c67-c13c-4055-a318-6ba35d62a2cf", "url": "https://clubset.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state|upcase}&city=${city|capitalize}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "06e37215-ef34-4971-bf86-e5a03dfe46e8", + "id": "866bdfc5-069e-4734-9ce0-a19976fa796b", "selector": ".card", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/clustrmaps.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/clustrmaps.com.json index 0aca895c02..4c2bd20999 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/clustrmaps.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/clustrmaps.com.json @@ -1,8 +1,9 @@ { - "name": "clustrmaps.com", - "version": "0.1.1", + "name": "ClustrMaps", + "url": "clustrmaps.com", + "version": "0.1.4", "parent": "neighbor.report", - "addedDatetime": 1692590400000, + "addedDatetime": 1692594000000, "steps": [ { "stepType": "scan", @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "a39655de-5c23-477d-9887-1d34966a1069", + "id": "e6929e37-4764-450a-be2a-73479f11842a", "url": "https://clustrmaps.com/persons/${firstName}-${lastName}/${state|stateFull|capitalize}/${city|hyphenated}" }, { "actionType": "extract", - "id": "4e3a628e-3634-4a2b-b632-4fbb8ce0b52b", + "id": "06f39df7-89c2-40da-b288-cdf3ed0e4bfd", "selector": ".//div[@itemprop='Person']", "profile": { "name": { @@ -55,4 +56,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/councilon.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/councilon.com.json index d68f6b9f4c..3df8d7f195 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/councilon.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/councilon.com.json @@ -1,6 +1,7 @@ { - "name": "councilon.com", - "version": "0.1.1", + "name": "Councilon", + "url": "councilon.com", + "version": "0.1.4", "parent": "verecor.com", "addedDatetime": 1702965600000, "steps": [ @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "295418e5-e1da-43b4-af50-75576ca4f843", + "id": "a5052dda-d4e7-4d3f-97bc-ef9f0aa9ae5f", "url": "https://councilon.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "eead1b72-7d6b-4cdc-988d-5ea66eb398f1", + "id": "55a50a37-9b1b-40fa-8533-af1273a26258", "selector": ".card", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/curadvisor.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/curadvisor.com.json index 33f27d0c79..3b59ed585e 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/curadvisor.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/curadvisor.com.json @@ -1,8 +1,9 @@ { - "name": "curadvisor.com", - "version": "0.1.1", + "name": "CurAdvisor", + "url": "curadvisor.com", + "version": "0.1.4", "parent": "verecor.com", - "addedDatetime": 1677736800000, + "addedDatetime": 1703052000000, "steps": [ { "stepType": "scan", @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "bdb69f52-8ece-4d65-9b78-543fef0e90ae", + "id": "ab5503c7-bd11-4320-b38e-c637b239182e", "url": "https://curadvisor.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "3b9bc992-ecc0-4dc5-b716-fcea021cbcdb", + "id": "d273c1cf-2635-40d7-b26f-6f34467282cf", "selector": ".card", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/cyberbackgroundchecks.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/cyberbackgroundchecks.com.json index e27b744790..f930834db9 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/cyberbackgroundchecks.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/cyberbackgroundchecks.com.json @@ -1,6 +1,7 @@ { - "name": "cyberbackgroundchecks.com", - "version": "0.1.1", + "name": "Cyber Background Checks", + "url": "cyberbackgroundchecks.com", + "version": "0.1.4", "parent": "peoplefinders.com", "addedDatetime": 1705644000000, "steps": [ @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "4b7037f3-9c6a-42b5-929e-621256e0a044", + "id": "d8c84470-d8b3-4c46-a645-01cc6b139b3b", "url": "https://www.cyberbackgroundchecks.com/people/${firstName}-${lastName}/${state}/${city}" }, { "actionType": "extract", - "id": "f36a73d7-9efb-452e-8c60-6d9df2964bcf", + "id": "b4c12cf2-0fd6-4209-8816-3bf2cce23cde", "selector": ".card", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/dataveria.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/dataveria.com.json index 3dfe8e5431..9949e04675 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/dataveria.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/dataveria.com.json @@ -1,8 +1,9 @@ { - "name": "dataveria.com", - "version": "0.1.1", + "name": "Dataveria", + "url": "dataveria.com", + "version": "0.1.4", "parent": "verecor.com", - "addedDatetime": 1677733200000, + "addedDatetime": 1677736800000, "steps": [ { "stepType": "scan", @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "fc449310-7b7b-45d4-bcf9-0c5d51c246f8", + "id": "a8f3a259-2d39-4ae3-ac13-65aa63a53331", "url": "https://dataveria.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "0481dc49-43e8-4af0-b697-680fb57ec24b", + "id": "e810cc23-2d2a-4e6e-b06f-dfc8a2e1e85d", "selector": ".search-item", "profile": { "name": { @@ -64,4 +65,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/fastbackgroundcheck.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/fastbackgroundcheck.com.json index 4462e0c86d..9c1129a333 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/fastbackgroundcheck.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/fastbackgroundcheck.com.json @@ -1,8 +1,9 @@ { - "name": "fastbackgroundcheck.com", - "version": "0.1.1", + "name": "FastBackgroundCheck.com", + "url": "fastbackgroundcheck.com", + "version": "0.1.4", "parent": "peoplefinders.com", - "addedDatetime": 1678082400000, + "addedDatetime": 1706248800000, "steps": [ { "stepType": "scan", @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "2a3a5979-9de0-44b2-ae03-f25422f0c2aa", + "id": "997adf8d-023c-409e-9206-57871cd25f0a", "url": "https://www.fastbackgroundcheck.com/people/${firstName}-${lastName}/${city}-${state}" }, { "actionType": "extract", - "id": "4818ff1c-d419-44c2-8168-501b456c6c6a", + "id": "2f531e34-2ac0-4743-a760-065187d6c951", "selector": ".person-container", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/freepeopledirectory.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/freepeopledirectory.com.json index 2c215abdf6..c448989448 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/freepeopledirectory.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/freepeopledirectory.com.json @@ -1,6 +1,7 @@ { - "name": "freepeopledirectory.com", - "version": "0.1.1", + "name": "FreePeopleDirectory", + "url": "freepeopledirectory.com", + "version": "0.1.4", "parent": "spokeo.com", "addedDatetime": 1674540000000, "steps": [ @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "815a1cd3-2577-4f43-a163-0cf4d22e66a4", + "id": "4c607417-36bc-47d4-8562-9c2244db354d", "url": "https://www.freepeopledirectory.com/name/${firstName}-${lastName}/${state|upcase}/${city}" }, { "actionType": "extract", - "id": "10738ba0-bc6b-42ba-a37c-487ff3927dd5", + "id": "a1637310-ca7a-40b0-b2f5-db22b43b5d54", "selector": ".whole-card", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/inforver.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/inforver.com.json index 961bb83ae3..2c035a980c 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/inforver.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/inforver.com.json @@ -1,8 +1,9 @@ { - "name": "inforver.com", - "version": "0.1.1", + "name": "Inforver", + "url": "inforver.com", + "version": "0.1.4", "parent": "verecor.com", - "addedDatetime": 1677733200000, + "addedDatetime": 1677736800000, "steps": [ { "stepType": "scan", @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "a56ab792-fc1b-4e60-b0b9-0bd4f580476f", + "id": "85fac850-36ad-4d9c-ad7c-c1250c7b5585", "url": "https://inforver.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "591ba784-106c-421b-b188-a376f1f9cb01", + "id": "e5e9c1b0-4af4-4fb6-bd2d-7d026ffd95e7", "selector": ".search-item", "profile": { "name": { @@ -64,4 +65,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/kwold.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/kwold.com.json index 31b5dc20a8..5f7e750909 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/kwold.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/kwold.com.json @@ -1,6 +1,7 @@ { - "name": "kwold.com", - "version": "0.1.1", + "name": "Kwold", + "url": "kwold.com", + "version": "0.1.4", "parent": "verecor.com", "addedDatetime": 1702965600000, "steps": [ @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "47152fc1-79d5-4bcc-b930-6b5cdc66e972", + "id": "936eee30-d31e-48fb-8cc4-9391869934b9", "url": "https://kwold.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "50507ab4-2e75-4f1d-af23-9725b9955bc3", + "id": "870ee174-275a-4ea8-b2d7-a222418e5de9", "selector": ".card", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/neighbor.report.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/neighbor.report.json index d640212852..92a0d2af57 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/neighbor.report.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/neighbor.report.json @@ -1,7 +1,8 @@ { - "name": "neighbor.report", + "name": "Neighbor Report", + "url": "neighbor.report", "version": "0.1.4", - "addedDatetime": 1703559600000, + "addedDatetime": 1703570400000, "steps": [ { "stepType": "scan", @@ -9,12 +10,12 @@ "actions": [ { "actionType": "navigate", - "id": "a554d7d2-f348-487a-97de-8d4f0d1d35c0", + "id": "bbaf8a18-fef8-42a6-9682-747b8ff485b2", "url": "https://neighbor.report/${firstName}-${lastName}/${state|stateFull|hyphenated}/${city|hyphenated}" }, { "actionType": "extract", - "id": "17f80250-1e3c-4e55-8e50-68fe98a6ce23", + "id": "0dac4a6d-1291-47c3-97b8-56200f751ac8", "selector": ".lstd", "profile": { "name": { @@ -50,12 +51,12 @@ "actions": [ { "actionType": "navigate", - "id": "59cc488d-e317-4fb4-8aaa-a20cb71f7480", + "id": "b1f7f4ab-51b0-4885-ba73-97be0822d0ba", "url": "https://neighbor.report/remove" }, { "actionType": "fillForm", - "id": "3a4c1775-941a-4f48-873d-c780f5ea25a0", + "id": "743afa6c-7dea-4115-934b-bea369307acd", "selector": ".form-horizontal", "elements": [ { @@ -74,17 +75,17 @@ }, { "actionType": "getCaptchaInfo", - "id": "f470e245-d5ee-4908-bd6c-16e604a1a29b", + "id": "24ce0da0-7cc3-47e7-bf8e-6f5fe98b7a91", "selector": ".recaptcha-div" }, { "actionType": "solveCaptcha", - "id": "7f0a8fc6-32a3-4f4a-b61d-267f9666de91", + "id": "b720de9a-f519-466f-980d-d9c52d8870a2", "selector": ".recaptcha-div" }, { "actionType": "click", - "id": "7fbc5a97-bc57-41bc-a556-0fdfd8a0845d", + "id": "46690938-f112-4091-bd07-b5641e38151f", "elements": [ { "type": "button", @@ -94,7 +95,7 @@ }, { "actionType": "click", - "id": "d1513f65-a746-4597-9ed2-4cd5e40dead3", + "id": "07cfed17-9d75-471a-b6a0-0522add35ffa", "elements": [ { "type": "button", @@ -108,7 +109,7 @@ }, { "actionType": "expectation", - "id": "8acd9c96-443d-4593-a3a7-9efc9fd5070a", + "id": "ebd61347-60e1-4c19-bc41-dd1ce36d3138", "expectations": [ { "type": "text", @@ -125,4 +126,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/newenglandfacts.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/newenglandfacts.com.json index ddb83134f9..54f8d23ac8 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/newenglandfacts.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/newenglandfacts.com.json @@ -1,6 +1,7 @@ { - "name": "newenglandfacts.com", - "version": "0.1.1", + "name": "New England Facts", + "url": "newenglandfacts.com", + "version": "0.1.4", "parent": "verecor.com", "addedDatetime": 1703052000000, "steps": [ @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "8bd7953c-ee22-49be-8937-a1798046a0c1", + "id": "05725a5a-ec3f-49c8-875b-ab9787b9385f", "url": "https://newenglandfacts.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "4012d312-2f7f-4cc1-bf7a-b7655f550c1a", + "id": "7f41b78a-bb65-4bb2-a6ca-1a6ab55890ce", "selector": ".b-pfl-list", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/officialusa.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/officialusa.com.json index a7b4efd714..9cb63483be 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/officialusa.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/officialusa.com.json @@ -1,8 +1,9 @@ { - "name": "officialusa.com", - "version": "0.1.0", + "name": "OfficialUSA", + "url": "officialusa.com", + "version": "0.1.4", "parent": "neighbor.report", - "addedDatetime": 1692590400000, + "addedDatetime": 1692594000000, "steps": [ { "stepType": "scan", @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "dad25b4c-743b-4bca-a395-05f1e76ef5c9", + "id": "b430e29e-89f0-4994-96b2-08d0cbdc388c", "url": "https://officialusa.com/names/${firstName}-${lastName}/" }, { "actionType": "extract", - "id": "b867d570-6124-40d9-9076-7ee0fa5b4d68", + "id": "d989f3b7-9b8a-44a6-a51e-70762255f3fc", "selector": ".person", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/people-background-check.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/people-background-check.com.json index 7b1d26eb38..b8550c93e4 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/people-background-check.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/people-background-check.com.json @@ -1,6 +1,7 @@ { - "name": "people-background-check.com", - "version": "0.1.1", + "name": "People Background Check", + "url": "people-background-check.com", + "version": "0.1.4", "parent": "verecor.com", "addedDatetime": 1702965600000, "steps": [ @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "18e35c3b-b837-40e9-b353-20230d36bc4d", + "id": "6fee90c5-5f7e-4fd0-badf-069e2b94a65d", "url": "https://people-background-check.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}" }, { "actionType": "extract", - "id": "7a23b927-acfc-4d29-b4b6-3f204687619c", + "id": "ee03ba42-e9a5-4489-a7d6-d50bf21238aa", "selector": ".b-pfl-list", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplefinders.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplefinders.com.json index 7e690167c8..34bc5b8770 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplefinders.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplefinders.com.json @@ -1,7 +1,8 @@ { - "name": "peoplefinders.com", - "version": "0.1.0", - "addedDatetime": 1677128400000, + "name": "PeopleFinders", + "url": "peoplefinders.com", + "version": "0.1.4", + "addedDatetime": 1677132000000, "steps": [ { "stepType": "scan", @@ -9,12 +10,12 @@ "actions": [ { "actionType": "navigate", - "id": "aafba5bd-a157-4e35-b653-0797a732d94c", + "id": "71c7cb2f-14fe-43b8-9623-452b8bd10d4e", "url": "https://www.peoplefinders.com/people/${firstName}-${lastName}/${state}/${city}?landing=all&age=${age}" }, { "actionType": "extract", - "id": "b8f10f20-3363-4781-a03b-c4958b6269c7", + "id": "5c5af912-091f-4f48-922f-ba554951ddd9", "selector": ".record", "profile": { "name": { @@ -48,12 +49,12 @@ "actions": [ { "actionType": "navigate", - "id": "f5fbd4f5-23f7-45ed-a9ce-3e9b0a5a7a0a", + "id": "4b065fde-35c7-43d7-aed6-3abcdac94f08", "url": "https://www.peoplefinders.com/opt-out" }, { "actionType": "click", - "id": "7b33cd1b-3948-4454-8434-e703cc235123", + "id": "b5c0929e-e362-4570-815b-0433ef97fddf", "elements": [ { "type": "button", @@ -63,7 +64,7 @@ }, { "actionType": "fillForm", - "id": "32056b7a-dc80-4d5d-b9cc-dccd32cb56be", + "id": "2fb91804-e5ea-414e-9354-fba98f3c00e1", "selector": ".opt-out-form", "elements": [ { @@ -78,17 +79,17 @@ }, { "actionType": "getCaptchaInfo", - "id": "5d5068aa-5c16-4fdc-8f3b-e412ad4eabed", + "id": "4b9706ef-dd9b-47d6-b337-12f66a5f9138", "selector": ".g-recaptcha" }, { "actionType": "solveCaptcha", - "id": "3443e060-8aee-4bd0-ab2c-ea03f8b8f93c", + "id": "770019d3-fa88-400a-8480-7cc31d6b3382", "selector": ".g-recaptcha" }, { "actionType": "click", - "id": "cb9ef5b0-0155-42f1-a766-145b3c14586b", + "id": "a7285f44-6c99-44b1-8199-eb6c383fe12b", "elements": [ { "type": "button", @@ -98,22 +99,22 @@ }, { "actionType": "emailConfirmation", - "id": "5cc7cfa5-e8ab-4dc1-b58b-973af3d3f364", + "id": "05cc08ea-fb80-40fb-8cce-3ca674eea03b", "pollingTime": 30 }, { "actionType": "getCaptchaInfo", - "id": "3a44c15d-1dd0-4e92-beba-bf3d8544c6e9", + "id": "8cb4256a-b162-407f-8434-5536c7560c98", "selector": ".g-recaptcha" }, { "actionType": "solveCaptcha", - "id": "d2566371-8b02-4414-9a24-1f9d2761eb1d", + "id": "38c64eec-6bd9-4751-a7cf-8cbe9901b0f6", "selector": ".g-recaptcha" }, { "actionType": "click", - "id": "259d8895-ac58-46b0-a209-7f209171e13c", + "id": "d1d25423-912b-4828-825b-eb83809ada08", "elements": [ { "type": "button", @@ -123,7 +124,7 @@ }, { "actionType": "expectation", - "id": "ed02f55b-67b3-4efc-a3cc-ce6b6c7ceeed", + "id": "fdb755da-8970-426f-b09e-12165c2169dd", "expectations": [ { "type": "url", @@ -139,4 +140,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplesearchnow.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplesearchnow.com.json index 6e477c6e13..6189f3d311 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplesearchnow.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplesearchnow.com.json @@ -1,6 +1,7 @@ { - "name": "peoplesearchnow.com", - "version": "0.1.1", + "name": "People Search Now", + "url": "peoplesearchnow.com", + "version": "0.1.4", "parent": "peoplefinders.com", "addedDatetime": 1705989600000, "steps": [ @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "b6994b26-9904-407b-9bcf-0fd6f809771d", + "id": "db9e093d-68c2-45e1-a529-29a2dc67dfab", "url": "https://peoplesearchnow.com/person/${firstName}-${lastName}_${city}_${state}/" }, { "actionType": "extract", - "id": "4e7f0e9a-1d24-47c0-886f-a08d88074878", + "id": "78912133-761b-4971-9780-4e16c8dd43b2", "selector": ".result-search-block", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/pub360.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/pub360.com.json index 503392f378..815fdcb8fc 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/pub360.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/pub360.com.json @@ -1,8 +1,9 @@ { - "name": "pub360.com", - "version": "0.1.1", + "name": "Pub360", + "url": "pub360.com", + "version": "0.1.4", "parent": "verecor.com", - "addedDatetime": 1677733200000, + "addedDatetime": 1677736800000, "steps": [ { "stepType": "scan", @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "72fc91c4-e8bc-4656-8260-cd3bb15e2001", + "id": "8e2a1251-2685-476a-b4c1-53d138331abe", "url": "https://pub360.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "2cb4778d-e3d6-4432-8421-84438c280e19", + "id": "9ce62e6f-b103-45f6-9f92-56785eb22320", "selector": ".search-item", "profile": { "name": { @@ -64,4 +65,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/publicreports.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/publicreports.com.json index 991d4d2b2c..5ea3d241e4 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/publicreports.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/publicreports.com.json @@ -1,6 +1,7 @@ { - "name": "publicreports.com", - "version": "0.1.1", + "name": "PublicReports", + "url": "publicreports.com", + "version": "0.1.4", "parent": "verecor.com", "addedDatetime": 1703052000000, "steps": [ @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "ae0104a2-a75c-4d97-bada-dda4f21dd446", + "id": "b995b1bf-6610-4085-9d07-d38857807535", "url": "https://publicreports.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "388c55e3-fa12-4376-9a02-01190b8a30fd", + "id": "7fb121fb-e2a0-4fa2-9b97-51130104971c", "selector": ".card", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/quickpeopletrace.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/quickpeopletrace.com.json index 7409fd240b..e8b18f9ec8 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/quickpeopletrace.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/quickpeopletrace.com.json @@ -1,6 +1,7 @@ { - "name": "quickpeopletrace.com", - "version": "0.1.1", + "name": "Quick People Trace", + "url": "quickpeopletrace.com", + "version": "0.1.4", "parent": "peoplefinders.com", "addedDatetime": 1674540000000, "steps": [ @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "45443ab2-7563-4c7d-8bf2-b1b550f4b825", + "id": "2db8c120-a8c3-4aa0-a9ce-b075ca85fc68", "url": "https://www.quickpeopletrace.com/search/?addresssearch=1&tabid=1&teaser-firstname=${firstName}&teaser-middlename=&teaser-lastname=${lastName}&teaser-city=${city}&teaser-state=${state|upcase}&teaser-submitted=Search" }, { "actionType": "extract", - "id": "08607047-96e8-4fbb-9af9-bf7b8e163b20", + "id": "bd48b737-89c4-408a-a28c-2dfa828aebd8", "selector": "//table/tbody/tr[position() > 1]", "profile": { "name": { @@ -24,9 +25,6 @@ "age": { "selector": ".//td[3]" }, - "addressCityState": { - "selector": ".//td[4]/strong" - }, "addressCityStateList": { "selector": ".//td[4]" }, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/searchpeoplefree.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/searchpeoplefree.com.json index 535ec0f63d..4a68c912e3 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/searchpeoplefree.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/searchpeoplefree.com.json @@ -1,6 +1,7 @@ { - "name": "searchpeoplefree.com", - "version": "0.1.1", + "name": "Search People FREE", + "url": "searchpeoplefree.com", + "version": "0.1.4", "parent": "peoplefinders.com", "addedDatetime": 1703052000000, "steps": [ @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "2b537ff2-8967-465c-ad5c-c4c2d31f60e1", + "id": "f5bad072-6f55-4357-b23b-1df4c9584e67", "url": "https://searchpeoplefree.com/find/${firstName}-${lastName}/${state}/${city}" }, { "actionType": "extract", - "id": "70728718-fe02-43f6-b86f-6d6c6bbbf009", + "id": "749fb8fe-9994-41e2-a0ea-ae6334c5aee0", "selector": "//li[@class='toc l-i mb-5']", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/smartbackgroundchecks.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/smartbackgroundchecks.com.json index 31424db72d..23f588c796 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/smartbackgroundchecks.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/smartbackgroundchecks.com.json @@ -1,6 +1,7 @@ { - "name": "smartbackgroundchecks.com", - "version": "0.1.1", + "name": "SmartBackgroundChecks", + "url": "smartbackgroundchecks.com", + "version": "0.1.4", "parent": "peoplefinders.com", "addedDatetime": 1678082400000, "steps": [ @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "97b307c8-e3e4-4090-a6ab-c5eeb599d248", + "id": "1c6bdc6e-12dd-47db-b5b0-13055c1f3d5d", "url": "https://www.smartbackgroundchecks.com/people/${firstName}-${lastName}/${city}/${state}" }, { "actionType": "extract", - "id": "ca20a933-b703-427e-8cbf-e2f25cd763a6", + "id": "ac554b4f-e4a0-44c5-81a6-c04e46e4ce3b", "selector": ".card-block", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/spokeo.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/spokeo.com.json index 2618099829..3c0f0008f8 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/spokeo.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/spokeo.com.json @@ -1,12 +1,33 @@ { - "name": "spokeo.com", - "version": "0.1.3", + "name": "Spokeo", + "url": "spokeo.com", + "version": "0.1.4", "addedDatetime": 1692594000000, "mirrorSites": [ - { "name": "callersmart.com", "addedAt": 1705599286529, "removedAt": null }, - { "name": "selfie.network", "addedAt": 1705599286529, "removedAt": null }, - { "name": "selfie.systems", "addedAt": 1705599286529, "removedAt": null }, - { "name": "peoplewin.com", "addedAt": 1705599286529, "removedAt": null } + { + "name": "CallerSmart", + "url": "callersmart.com", + "addedAt": 1705599286529, + "removedAt": null + }, + { + "name": "Selfie Network", + "url": "selfie.network", + "addedAt": 1705599286529, + "removedAt": null + }, + { + "name": "Selfie Systems", + "url": "selfie.systems", + "addedAt": 1705599286529, + "removedAt": null + }, + { + "name": "PeopleWin", + "url": "peoplewin.com", + "addedAt": 1705599286529, + "removedAt": null + } ], "steps": [ { @@ -15,12 +36,12 @@ "actions": [ { "actionType": "navigate", - "id": "d3174bd8-3253-45e3-88f0-1366882a2df7", + "id": "9b617d27-b330-46fc-bdb0-6239c0873897", "url": "https://www.spokeo.com/${firstName}-${lastName}/${state|stateFull}/${city}" }, { "actionType": "extract", - "id": "e47f5f27-dfbf-4f2c-8d7a-43f581abdaa2", + "id": "4f7124c2-bd8c-4649-84f2-04f0962225b5", "selector": ".single-column-list-item", "profile": { "name": { @@ -52,12 +73,12 @@ "actions": [ { "actionType": "navigate", - "id": "dba8f444-a433-4ad2-9819-3c555bfedd9c", + "id": "df75e4fb-f14b-4b65-afe2-82e03b71c6a9", "url": "https://www.spokeo.com/optout" }, { "actionType": "fillForm", - "id": "b1145fca-3e35-4ee9-86f2-e35c393846d3", + "id": "42cbfc2b-d96b-4bd6-8d16-0542a672d869", "selector": ".optout_container", "elements": [ { @@ -72,17 +93,17 @@ }, { "actionType": "getCaptchaInfo", - "id": "8f9608b4-bbf7-4540-8b22-5c381225cd02", + "id": "e1581b9e-7460-4bbd-a010-634c2db12ca1", "selector": "#g-recaptcha" }, { "actionType": "solveCaptcha", - "id": "a5a884b8-12f6-4029-aa80-244f1a163f67", + "id": "01ca39d9-e842-41cf-b0f9-a7d517bc0dd6", "selector": "#g-recaptcha" }, { "actionType": "click", - "id": "a7d4fdd4-30b8-46f7-8700-67c633da1f91", + "id": "7556edd5-570b-4c4a-acc7-f1066138d513", "elements": [ { "type": "button", @@ -92,7 +113,7 @@ }, { "actionType": "expectation", - "id": "d4a804a3-de62-4f66-a56e-e9d7e65fb8bb", + "id": "f7b5125e-0dda-4a14-8943-8c20c09125bc", "expectations": [ { "type": "text", @@ -103,12 +124,12 @@ }, { "actionType": "emailConfirmation", - "id": "5138062c-99d3-4523-b222-8123b13bc524", + "id": "dbd875b6-bdc7-48ca-962b-885941e6284a", "pollingTime": 30 }, { "actionType": "expectation", - "id": "cc14f3ea-35f8-4d31-a280-dc97526de12a", + "id": "b2f1c371-d779-4b3b-8516-0d13169cf873", "expectations": [ { "type": "text", @@ -125,4 +146,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/truepeoplesearch.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/truepeoplesearch.com.json index 84d468f943..a226c959a0 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/truepeoplesearch.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/truepeoplesearch.com.json @@ -1,6 +1,7 @@ { - "name": "truepeoplesearch.com", - "version": "0.1.1", + "name": "TruePeopleSearch", + "url": "truepeoplesearch.com", + "version": "0.1.4", "parent": "peoplefinders.com", "addedDatetime": 1703138400000, "steps": [ @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "4b7d8751-d3cd-4e4b-b2a2-66219eb6a8e8", + "id": "12eb70c1-53d5-4881-9dce-74ed4fada583", "url": "https://www.truepeoplesearch.com/results?name=${firstName}%20${lastName}&citystatezip=${city|capitalize},${state|upcase}" }, { "actionType": "extract", - "id": "cdb5940a-8505-4b28-9699-d98235e1fff1", + "id": "881e0e21-c375-4083-a9be-86f82063849b", "selector": ".card-summary", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usa-people-search.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usa-people-search.com.json index 71c8e711ed..b8fba84277 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usa-people-search.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usa-people-search.com.json @@ -1,8 +1,9 @@ { - "name": "usa-people-search.com", - "version": "0.1.1", + "name": "USA People Search", + "url": "usa-people-search.com", + "version": "0.1.4", "parent": "peoplefinders.com", - "addedDatetime": 1678078800000, + "addedDatetime": 1678082400000, "steps": [ { "stepType": "scan", @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "2c4b31a3-661b-4f30-a4d3-b5a4a13c95db", + "id": "67e80e69-f542-4714-8705-c43af630ac72", "url": "https://usa-people-search.com/name/${firstName|downcase}-${lastName|downcase}/${city|downcase}-${state|stateFull|downcase}?age=${age}" }, { "actionType": "extract", - "id": "20a4d510-56b6-46a8-92ce-be16ed3ce049", + "id": "c0a82b15-7564-4e12-8c4e-084174242623", "selector": ".card-block", "profile": { "name": { @@ -62,4 +63,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usatrace.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usatrace.com.json index 412a14d7d9..4645c85dd0 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usatrace.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usatrace.com.json @@ -1,6 +1,7 @@ { - "name": "usatrace.com", - "version": "0.1.1", + "name": "USA Trace", + "url": "usatrace.com", + "version": "0.1.4", "parent": "peoplefinders.com", "addedDatetime": 1674540000000, "steps": [ @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "5071e480-88ae-49b5-91b6-1daf26c55acf", + "id": "17217b04-28ae-4262-aa33-ee3695bb6bd6", "url": "https://www.usatrace.com/people-search/${firstName}-${lastName}/${city}-${state|upcase}" }, { "actionType": "extract", - "id": "3237fc09-247c-4942-9920-9bbb937f6ac2", + "id": "426d8e8a-2f32-46f3-9d1d-e7f6e2fddadb", "selector": "//table/tbody/tr[position() > 1]", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usphonebook.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usphonebook.com.json index 0770b2f474..5aff073d4a 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usphonebook.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usphonebook.com.json @@ -1,8 +1,9 @@ { - "name": "usphonebook.com", - "version": "0.1.1", + "name": "USPhoneBook", + "url": "usphonebook.com", + "version": "0.1.4", "parent": "peoplefinders.com", - "addedDatetime": 1678078800000, + "addedDatetime": 1678082400000, "steps": [ { "stepType": "scan", @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "f214150b-4f02-46e1-b7ea-81f6bb1bf097", + "id": "6ee93554-95da-4a36-a7f7-c059d8f53ca3", "url": "https://www.usphonebook.com/${firstName}-${lastName}/${state|stateFull}/${city}" }, { "actionType": "extract", - "id": "af98bb63-b885-4f47-bb47-5f9ec5b491a4", + "id": "fffae12f-4ca1-4a8f-81b9-00adf0487129", "selector": ".ls_contacts-people-finder-wrapper", "profile": { "name": { @@ -56,4 +57,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/verecor.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/verecor.com.json index f493fbd347..5aff5bd46e 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/verecor.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/verecor.com.json @@ -1,7 +1,8 @@ { - "name": "verecor.com", - "version": "0.1.2", - "addedDatetime": 1677128400000, + "name": "Verecor", + "url": "verecor.com", + "version": "0.1.4", + "addedDatetime": 1677132000000, "steps": [ { "stepType": "scan", @@ -9,7 +10,7 @@ "actions": [ { "actionType": "navigate", - "id": "6f53d146-af6a-4bce-970d-f1dcbc496037", + "id": "37fc63a6-e434-4ba0-9e9e-d80898e4dfa4", "url": "https://verecor.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -23,7 +24,7 @@ }, { "actionType": "extract", - "id": "e8c09200-030c-492a-8e54-22bc6bdb6829", + "id": "a955924c-7959-48c8-9511-3f843baed729", "selector": ".search-item", "profile": { "name": { @@ -58,12 +59,12 @@ "actions": [ { "actionType": "navigate", - "id": "f4bbe480-a6ff-40a5-aa25-8ff9ac40c9bf", + "id": "85cd9682-94d8-46ac-9999-e03dfa9f8d4e", "url": "https://verecor.com/ng/control/privacy" }, { "actionType": "fillForm", - "id": "1e6302e1-daf9-49d6-951f-506ea5e266a0", + "id": "ed45c76b-e537-4072-9f46-9515c6e215be", "selector": ".ahm", "elements": [ { @@ -82,17 +83,17 @@ }, { "actionType": "getCaptchaInfo", - "id": "6a3dc470-3bf7-4b8b-bb44-f77ef1a2c540", + "id": "0e1474f0-24fe-4f6a-8d2e-2dfd91cf574b", "selector": ".g-recaptcha" }, { "actionType": "solveCaptcha", - "id": "83157244-c5bf-44a9-979c-679e1404d67d", + "id": "52a858f5-7dc5-40aa-aaa7-7090e06ea55e", "selector": ".g-recaptcha" }, { "actionType": "click", - "id": "15c50d7f-0e72-4509-be2b-40cde34b48e6", + "id": "759e0dd2-3a93-42a8-9a83-5e3408f5566b", "elements": [ { "type": "button", @@ -102,7 +103,7 @@ }, { "actionType": "expectation", - "id": "2ed336a2-a7a9-4cbd-933c-cd463df4f553", + "id": "089924be-5ea3-48a9-a325-8976d262f39b", "expectations": [ { "type": "text", @@ -113,12 +114,12 @@ }, { "actionType": "emailConfirmation", - "id": "88c09081-e848-4e75-a7b9-3ee28e95a459", + "id": "8094718e-412a-418f-b74d-cd4fc5e42c56", "pollingTime": 30 }, { "actionType": "expectation", - "id": "dd03cf9f-8227-4881-86bf-09ce158bf151", + "id": "af8fb89b-88d2-4901-b90c-eaac3c7566db", "expectations": [ { "type": "text", @@ -135,4 +136,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/vericora.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/vericora.com.json index 814e908fae..c7a3bad5c2 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/vericora.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/vericora.com.json @@ -1,8 +1,9 @@ { - "name": "vericora.com", - "version": "0.1.1", + "name": "Vericora", + "url": "vericora.com", + "version": "0.1.4", "parent": "verecor.com", - "addedDatetime": 1677733200000, + "addedDatetime": 1677736800000, "steps": [ { "stepType": "scan", @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "9488e141-d109-4cbf-bc65-1b9036728ff4", + "id": "69175f1a-0024-4efd-ab3e-67bcf915a770", "url": "https://vericora.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "baaecb74-8d63-496c-a3e0-a8acbdee2c99", + "id": "bd941009-4462-4d59-ba44-46250f580531", "selector": ".search-item", "profile": { "name": { @@ -64,4 +65,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/veriforia.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/veriforia.com.json index 121bc68d3e..5f4f307f92 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/veriforia.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/veriforia.com.json @@ -1,8 +1,9 @@ { - "name": "veriforia.com", - "version": "0.1.1", + "name": "Veriforia", + "url": "veriforia.com", + "version": "0.1.4", "parent": "verecor.com", - "addedDatetime": 1677733200000, + "addedDatetime": 1677736800000, "steps": [ { "stepType": "scan", @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "ffb30e97-b03f-4157-a511-09ad8ffb8b54", + "id": "17442975-944c-4b01-8518-7f1dff171ad2", "url": "https://veriforia.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "1d8d9c20-9897-4386-8bc1-bd591abe7c81", + "id": "32e963e1-4959-4e5e-981b-550f1bf36f9a", "selector": ".search-item", "profile": { "name": { @@ -64,4 +65,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/veripages.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/veripages.com.json index 43d95caf8a..61becad701 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/veripages.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/veripages.com.json @@ -1,8 +1,9 @@ { - "name": "veripages.com", - "version": "0.1.2", + "name": "Veripages", + "url": "veripages.com", + "version": "0.1.4", "parent": "verecor.com", - "addedDatetime": 1691982000000, + "addedDatetime": 1691989200000, "steps": [ { "stepType": "scan", @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "5bf98772-1804-4939-a06b-dbf9cd31f198", + "id": "2346b569-1c46-4ef9-8ea0-fa18bea967fa", "url": "https://veripages.com/inner/profile/search?fname=${firstName}&lname=${lastName}&fage=${age|ageRange}&state=${state}&city=${city}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "f3d53642-9a58-4275-b97f-5547e3ef8e55", + "id": "c4281ca8-d4d0-4091-b6c2-3094801e99c0", "selector": ".search-item", "profile": { "name": { @@ -66,4 +67,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/virtory.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/virtory.com.json index d12865681f..3d94019338 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/virtory.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/virtory.com.json @@ -1,6 +1,7 @@ { - "name": "virtory.com", - "version": "0.1.1", + "name": "Virtory", + "url": "virtory.com", + "version": "0.1.4", "parent": "verecor.com", "addedDatetime": 1703052000000, "steps": [ @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "99465bd6-ce87-4fc6-96a2-eea8137e4a30", + "id": "0568e4f5-73c2-4b1a-9eb6-ac3571b1a01e", "url": "https://virtory.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "8d66ac98-f788-4fbd-acec-56034682b4b1", + "id": "df2216f3-0890-4d13-b2aa-233084167720", "selector": ".card", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/wellnut.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/wellnut.com.json index b4fd3669ec..12c43b7fa4 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/wellnut.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/wellnut.com.json @@ -1,6 +1,7 @@ { - "name": "wellnut.com", - "version": "0.1.1", + "name": "Wellnut", + "url": "wellnut.com", + "version": "0.1.4", "parent": "verecor.com", "addedDatetime": 1703052000000, "steps": [ @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "b9db3c1e-ece6-45d1-94ec-1143da9607aa", + "id": "a38752f3-ae69-45c3-ba3f-3a73e549e644", "url": "https://wellnut.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "e4b7c983-c96e-4ce8-8703-3cb319454db7", + "id": "b7747e92-5fe5-46f7-b083-5df6fbdc2b84", "selector": ".card", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift index 0e6b930d71..c61178c0f9 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift @@ -106,7 +106,6 @@ final class DataBrokerProtectionProcessor { completion: @escaping () -> Void) { // Before running new operations we check if there is any updates to the broker files. - // This runs only once per 24 hours. if let vault = try? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: nil) { let brokerUpdater = DataBrokerProtectionBrokerUpdater(vault: vault) brokerUpdater.checkForUpdatesInBrokerJSONFiles() diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/EmailService.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/EmailService.swift index 75b80b195a..038c72c2d8 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/EmailService.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/EmailService.swift @@ -30,7 +30,7 @@ public enum EmailError: Error, Equatable, Codable { } protocol EmailServiceProtocol { - func getEmail(dataBrokerName: String?) async throws -> String + func getEmail(dataBrokerURL: String?) async throws -> String func getConfirmationLink(from email: String, numberOfRetries: Int, pollingIntervalInSeconds: Int, @@ -51,10 +51,10 @@ struct EmailService: EmailServiceProtocol { self.redeemUseCase = redeemUseCase } - func getEmail(dataBrokerName: String? = nil) async throws -> String { + func getEmail(dataBrokerURL: String? = nil) async throws -> String { var urlString = Constants.baseUrl + "/generate" - if let dataBrokerValue = dataBrokerName { + if let dataBrokerValue = dataBrokerURL { urlString += "?dataBroker=\(dataBrokerValue)" } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionDatabaseProvider.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionDatabaseProvider.swift index ab59f166cc..a70ad31818 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionDatabaseProvider.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionDatabaseProvider.swift @@ -34,7 +34,7 @@ protocol DataBrokerProtectionDatabaseProvider: SecureStorageDatabaseProvider { func save(_ broker: BrokerDB) throws -> Int64 func update(_ broker: BrokerDB) throws func fetchBroker(with id: Int64) throws -> BrokerDB? - func fetchBroker(with name: String) throws -> BrokerDB? + func fetchBroker(with url: String) throws -> BrokerDB? func fetchAllBrokers() throws -> [BrokerDB] func save(_ profileQuery: ProfileQueryDB) throws -> Int64 @@ -85,6 +85,8 @@ final class DefaultDataBrokerProtectionDatabaseProvider: GRDBSecureStorageDataba public init(file: URL = DefaultDataBrokerProtectionDatabaseProvider.defaultDatabaseURL(), key: Data) throws { try super.init(file: file, key: key, writerType: .pool) { migrator in migrator.registerMigration("v1", migrate: Self.migrateV1(database:)) + migrator.registerMigration("v2", migrate: Self.migrateV2(database:)) + } } @@ -259,6 +261,16 @@ final class DefaultDataBrokerProtectionDatabaseProvider: GRDBSecureStorageDataba $0.column(OptOutAttemptDB.Columns.startDate.name, .date).notNull() } } + + static func migrateV2(database: Database) throws { + try database.alter(table: BrokerDB.databaseTableName) { + $0.add(column: BrokerDB.Columns.url.name, .text) + } + try database.execute(sql: """ + UPDATE \(BrokerDB.databaseTableName) SET \(BrokerDB.Columns.url.name) = \(BrokerDB.Columns.name.name) + """) + } + // swiftlint:enable function_body_length func updateProfile(profile: DataBrokerProtectionProfile, mapperToDB: MapperToDB) throws -> Int64 { @@ -359,10 +371,10 @@ final class DefaultDataBrokerProtectionDatabaseProvider: GRDBSecureStorageDataba } } - func fetchBroker(with name: String) throws -> BrokerDB? { + func fetchBroker(with url: String) throws -> BrokerDB? { try db.read { db in return try BrokerDB - .filter(Column(BrokerDB.Columns.name.name) == name) + .filter(Column(BrokerDB.Columns.url.name) == url) .fetchOne(db) } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/Mappers.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/Mappers.swift index 267b013ed8..56b08dd5f2 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/Mappers.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/Mappers.swift @@ -58,7 +58,7 @@ struct MapperToDB { func mapToDB(_ broker: DataBroker, id: Int64? = nil) throws -> BrokerDB { let encodedBroker = try jsonEncoder.encode(broker) - return .init(id: id, name: broker.name, json: encodedBroker, version: broker.version) + return .init(id: id, name: broker.name, json: encodedBroker, version: broker.version, url: broker.url) } func mapToDB(_ profileQuery: ProfileQuery, relatedTo profileId: Int64) throws -> ProfileQueryDB { @@ -171,6 +171,7 @@ struct MapperToModel { return DataBroker( id: brokerDB.id, name: decodedBroker.name, + url: decodedBroker.url, steps: decodedBroker.steps, version: decodedBroker.version, schedulingConfig: decodedBroker.schedulingConfig, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/SchedulerSchema.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/SchedulerSchema.swift index 348d99180d..a772e4d543 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/SchedulerSchema.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/SchedulerSchema.swift @@ -94,6 +94,7 @@ struct BrokerDB: Codable { let name: String let json: Data let version: String + let url: String } extension BrokerDB: PersistableRecord, FetchableRecord { @@ -104,6 +105,7 @@ extension BrokerDB: PersistableRecord, FetchableRecord { case name case json case version + case url } init(row: Row) throws { @@ -111,6 +113,7 @@ extension BrokerDB: PersistableRecord, FetchableRecord { name = row[Columns.name] json = row[Columns.json] version = row[Columns.version] + url = row[Columns.url] } func encode(to container: inout PersistenceContainer) throws { @@ -118,6 +121,7 @@ extension BrokerDB: PersistableRecord, FetchableRecord { container[Columns.name] = name container[Columns.json] = json container[Columns.version] = version + container[Columns.url] = url } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DBPUICommunicationLayer.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DBPUICommunicationLayer.swift index 63b1b4a47e..9f77b8a675 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DBPUICommunicationLayer.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DBPUICommunicationLayer.swift @@ -36,6 +36,7 @@ protocol DBPUICommunicationDelegate: AnyObject { func startScanAndOptOut() -> Bool func getInitialScanState() async -> DBPUIInitialScanState func getMaintananceScanState() async -> DBPUIScanAndOptOutMaintenanceState + func getDataBrokers() async -> [DBPUIDataBroker] } enum DBPUIReceivedMethodName: String { @@ -53,6 +54,7 @@ enum DBPUIReceivedMethodName: String { case startScanAndOptOut case initialScanStatus case maintenanceScanStatus + case getDataBrokers } enum DBPUISendableMethodName: String { @@ -69,7 +71,7 @@ struct DBPUICommunicationLayer: Subfeature { weak var delegate: DBPUICommunicationDelegate? private enum Constants { - static let version = 1 + static let version = 2 } internal init(webURLSettings: DataBrokerProtectionWebUIURLSettingsRepresentable) { @@ -101,6 +103,7 @@ struct DBPUICommunicationLayer: Subfeature { case .startScanAndOptOut: return startScanAndOptOut case .initialScanStatus: return initialScanStatus case .maintenanceScanStatus: return maintenanceScanStatus + case .getDataBrokers: return getDataBrokers } } @@ -264,6 +267,11 @@ struct DBPUICommunicationLayer: Subfeature { return maintenanceScanStatus } + func getDataBrokers(params: Any, origin: WKScriptMessage) async throws -> Encodable? { + let dataBrokers = await delegate?.getDataBrokers() ?? [DBPUIDataBroker]() + return DBPUIDataBrokerList(dataBrokers: dataBrokers) + } + func sendMessageToUI(method: DBPUISendableMethodName, params: DBPUISendableMessage, into webView: WKWebView) { broker?.push(method: method.rawValue, params: params, for: self, into: webView) } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift index 836bd52a30..454f1c75a9 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift @@ -26,22 +26,24 @@ struct MapperToUI { name: extractedProfile.fullName ?? "No name", addresses: extractedProfile.addresses?.map(mapToUI) ?? [], alternativeNames: extractedProfile.alternativeNames ?? [String](), - relatives: extractedProfile.relatives ?? [String]() + relatives: extractedProfile.relatives ?? [String](), + date: extractedProfile.removedDate?.timeIntervalSince1970 ) } - func mapToUI(_ dataBrokerName: String, extractedProfile: ExtractedProfile) -> DBPUIDataBrokerProfileMatch { + func mapToUI(_ dataBrokerName: String, databrokerURL: String, extractedProfile: ExtractedProfile) -> DBPUIDataBrokerProfileMatch { DBPUIDataBrokerProfileMatch( - dataBroker: DBPUIDataBroker(name: dataBrokerName), + dataBroker: DBPUIDataBroker(name: dataBrokerName, url: databrokerURL), name: extractedProfile.fullName ?? "No name", addresses: extractedProfile.addresses?.map(mapToUI) ?? [], alternativeNames: extractedProfile.alternativeNames ?? [String](), - relatives: extractedProfile.relatives ?? [String]() + relatives: extractedProfile.relatives ?? [String](), + date: extractedProfile.removedDate?.timeIntervalSince1970 ) } func mapToUI(_ dataBroker: DataBroker) -> DBPUIDataBroker { - DBPUIDataBroker(name: dataBroker.name) + DBPUIDataBroker(name: dataBroker.name, url: dataBroker.url) } func mapToUI(_ address: AddressCityState) -> DBPUIUserProfileAddress { @@ -75,7 +77,7 @@ struct MapperToUI { if !$0.dataBroker.mirrorSites.isEmpty { let mirrorSitesMatches = $0.dataBroker.mirrorSites.compactMap { mirrorSite in if mirrorSite.shouldWeIncludeMirrorSite() { - return mapToUI(mirrorSite.name, extractedProfile: extractedProfile) + return mapToUI(mirrorSite.name, databrokerURL: mirrorSite.url, extractedProfile: extractedProfile) } return nil @@ -110,7 +112,7 @@ struct MapperToUI { if let closestMatchesFoundEvent = scanOperation.closestMatchesFoundEvent() { for mirrorSite in dataBroker.mirrorSites where mirrorSite.shouldWeIncludeMirrorSite(for: closestMatchesFoundEvent.date) { - let mirrorSiteMatch = mapToUI(mirrorSite.name, extractedProfile: extractedProfile) + let mirrorSiteMatch = mapToUI(mirrorSite.name, databrokerURL: mirrorSite.url, extractedProfile: extractedProfile) if let extractedProfileRemovedDate = extractedProfile.removedDate, mirrorSite.shouldWeIncludeMirrorSite(for: extractedProfileRemovedDate) { @@ -124,11 +126,21 @@ struct MapperToUI { } let completedOptOutsDictionary = Dictionary(grouping: removedProfiles, by: { $0.dataBroker }) - let completedOptOuts = completedOptOutsDictionary.map { (key: DBPUIDataBroker, value: [DBPUIDataBrokerProfileMatch]) in - DBPUIOptOutMatch(dataBroker: key, matches: value.count) - } - let lastScans = getLastScanInformation(brokerProfileQueryData: brokerProfileQueryData) - let nextScans = getNextScansInformation(brokerProfileQueryData: brokerProfileQueryData) + let completedOptOuts: [DBPUIOptOutMatch] = completedOptOutsDictionary.compactMap { (key: DBPUIDataBroker, value: [DBPUIDataBrokerProfileMatch]) in + value.compactMap { match in + guard let removedDate = match.date else { return nil } + return DBPUIOptOutMatch(dataBroker: key, + matches: value.count, + name: match.name, + alternativeNames: match.alternativeNames, + addresses: match.addresses, + date: removedDate) + } + }.flatMap { $0 } + + let nearestScanByBrokerURL = nearestRunDates(for: brokerProfileQueryData) + let lastScans = getLastScanInformation(brokerProfileQueryData: brokerProfileQueryData, nearestScanOperationByBroker: nearestScanByBrokerURL) + let nextScans = getNextScansInformation(brokerProfileQueryData: brokerProfileQueryData, nearestScanOperationByBroker: nearestScanByBrokerURL) return DBPUIScanAndOptOutMaintenanceState( inProgressOptOuts: inProgressOptOuts, @@ -140,7 +152,8 @@ struct MapperToUI { private func getLastScanInformation(brokerProfileQueryData: [BrokerProfileQueryData], currentDate: Date = Date(), - format: String = "dd/MM/yyyy") -> DBUIScanDate { + format: String = "dd/MM/yyyy", + nearestScanOperationByBroker: [String: Date]) -> DBUIScanDate { let scansGroupedByLastRunDate = Dictionary(grouping: brokerProfileQueryData, by: { $0.scanOperationData.lastRunDate?.toFormat(format) }) let closestScansBeforeToday = scansGroupedByLastRunDate .filter { $0.key != nil && $0.key!.toDate(using: format) < currentDate } @@ -148,12 +161,13 @@ struct MapperToUI { .flatMap { [$0.key?.toDate(using: format): $0.value] } .last - return scanDate(element: closestScansBeforeToday) + return scanDate(element: closestScansBeforeToday, nearestScanOperationByBroker: nearestScanOperationByBroker) } private func getNextScansInformation(brokerProfileQueryData: [BrokerProfileQueryData], currentDate: Date = Date(), - format: String = "dd/MM/yyyy") -> DBUIScanDate { + format: String = "dd/MM/yyyy", + nearestScanOperationByBroker: [String: Date]) -> DBUIScanDate { let scansGroupedByPreferredRunDate = Dictionary(grouping: brokerProfileQueryData, by: { $0.scanOperationData.preferredRunDate?.toFormat(format) }) let closestScansAfterToday = scansGroupedByPreferredRunDate .filter { $0.key != nil && $0.key!.toDate(using: format) > currentDate } @@ -161,22 +175,50 @@ struct MapperToUI { .flatMap { [$0.key?.toDate(using: format): $0.value] } .first - return scanDate(element: closestScansAfterToday) + return scanDate(element: closestScansAfterToday, nearestScanOperationByBroker: nearestScanOperationByBroker) + } + + // A dictionary containing the closest scan by broker + private func nearestRunDates(for brokerData: [BrokerProfileQueryData]) -> [String: Date] { + let today = Date() + let nearestDates = brokerData.reduce(into: [String: Date]()) { result, data in + let url = data.dataBroker.url + if let operationDate = data.scanOperationData.preferredRunDate { + if operationDate > today { + if let existingDate = result[url] { + if operationDate < existingDate { + result[url] = operationDate + } + } else { + result[url] = operationDate + } + } + } + } + return nearestDates } - private func scanDate(element: Dictionary.Element?) -> DBUIScanDate { + private func scanDate(element: Dictionary.Element?, + nearestScanOperationByBroker: [String: Date]) -> DBUIScanDate { if let element = element, let date = element.key { return DBUIScanDate( date: date.timeIntervalSince1970, dataBrokers: element.value.flatMap { - var brokers = [DBPUIDataBroker(name: $0.dataBroker.name)] + let brokerOperationDate = nearestScanOperationByBroker[$0.dataBroker.url] + var brokers = [DBPUIDataBroker(name: $0.dataBroker.name, url: $0.dataBroker.url, date: brokerOperationDate?.timeIntervalSince1970 ?? nil)] for mirrorSite in $0.dataBroker.mirrorSites where mirrorSite.shouldWeIncludeMirrorSite(for: date) { - brokers.append(DBPUIDataBroker(name: mirrorSite.name)) + brokers.append(DBPUIDataBroker(name: mirrorSite.name, url: mirrorSite.url, date: brokerOperationDate?.timeIntervalSince1970 ?? nil)) } return brokers } + .reduce(into: []) { result, dataBroker in // Remove dupes + guard !result.contains(where: { $0.url == dataBroker.url }) else { + return + } + result.append(dataBroker) + } ) } else { return DBUIScanDate(date: 0, dataBrokers: [DBPUIDataBroker]()) diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/BrokerJSONCodableTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/BrokerJSONCodableTests.swift index 2a6108f826..a604a0a4d7 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/BrokerJSONCodableTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/BrokerJSONCodableTests.swift @@ -20,11 +20,174 @@ import XCTest @testable import DataBrokerProtection final class BrokerJSONCodableTests: XCTestCase { - let verecorJSONString = """ + let verecorWithURLJSONString = """ + { + "name": "Verecor", + "url": "verecor.com", + "version": "0.1.0", + "addedDatetime": 1677128400000, + "mirrorSites": [ + { + "name": "Potato", + "url": "potato.com", + "addedAt": 1705599286529, + "removedAt": null + }, + { + "name": "Tomato", + "url": "tomato.com", + "addedAt": 1705599286529, + "removedAt": null + } + ], + "steps": [ + { + "stepType": "scan", + "scanType": "templatedUrl", + "actions": [ + { + "actionType": "navigate", + "id": "84aa05bc-1ca0-4f16-ae74-dfb352ce0eee", + "url": "https://verecor.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${ageRange}", + "ageRange": [ + "18-30", + "31-40", + "41-50", + "51-60", + "61-70", + "71-80", + "81+" + ] + }, + { + "actionType": "extract", + "id": "92252eb5-ccaf-4b00-a3fe-019110ce0534", + "selector": ".search-item", + "profile": { + "name": { + "selector": "h4" + }, + "alternativeNamesList": { + "selector": ".//div[@class='col-sm-24 col-md-16 name']//li", + "findElements": true + }, + "age": { + "selector": ".age" + }, + "addressCityStateList": { + "selector": ".//div[@class='col-sm-24 col-md-8 location']//li", + "findElements": true + }, + "profileUrl": { + "selector": "a" + } + } + } + ] + }, + { + "stepType": "optOut", + "optOutType": "formOptOut", + "actions": [ + { + "actionType": "navigate", + "id": "49f9aa73-4f97-47c0-b8bf-1729e9c169c0", + "url": "https://verecor.com/ng/control/privacy" + }, + { + "actionType": "fillForm", + "id": "55b1d0bb-d303-4b6f-bf9e-3fd96746f27e", + "selector": ".ahm", + "elements": [ + { + "type": "fullName", + "selector": "#user_name" + }, + { + "type": "email", + "selector": "#user_email" + }, + { + "type": "profileUrl", + "selector": "#url" + } + ] + }, + { + "actionType": "getCaptchaInfo", + "id": "9efb1153-8f52-41e4-a8fb-3077a97a586d", + "selector": ".g-recaptcha" + }, + { + "actionType": "solveCaptcha", + "id": "ed49e4c3-0cfa-4f1e-b3d1-06ad7b8b9ba4", + "selector": ".g-recaptcha" + }, + { + "actionType": "click", + "id": "6b986aa4-3d1b-44d5-8b2b-5463ee8916c9", + "elements": [ + { + "type": "button", + "selector": ".btn-sbmt" + } + ] + }, + { + "actionType": "expectation", + "id": "d4c64d9b-1004-487e-ab06-ae74869bc9a7", + "expectations": [ + { + "type": "text", + "selector": "body", + "expect": "Your removal request has been received" + } + ] + }, + { + "actionType": "emailConfirmation", + "id": "3b4c611a-61ab-4792-810e-d5b3633ea203", + "pollingTime": 30 + }, + { + "actionType": "expectation", + "id": "afe805a0-d422-473c-b47f-995a8672d476", + "expectations": [ + { + "type": "text", + "selector": "body", + "expect": "Your information control request has been confirmed." + } + ] + } + ] + } + ], + "schedulingConfig": { + "retryError": 48, + "confirmOptOutScan": 72, + "maintenanceScan": 240 + } + } + + """ + let verecorNoURLJSONString = """ { "name": "verecor.com", "version": "0.1.0", "addedDatetime": 1677128400000, + "mirrorSites": [ + { + "name": "tomato.com", + "addedAt": 1705599286529, + "removedAt": null + }, + { + "name": "potato.com", + "addedAt": 1705599286529, + "removedAt": null + } + ], "steps": [ { "stepType": "scan", @@ -157,9 +320,27 @@ final class BrokerJSONCodableTests: XCTestCase { """ - func testVerecorJSON_isCorrectlyParsed() { + func testVerecorJSONNoURL_isCorrectlyParsed() { do { - _ = try JSONDecoder().decode(DataBroker.self, from: verecorJSONString.data(using: .utf8)!) + let broker = try JSONDecoder().decode(DataBroker.self, from: verecorNoURLJSONString.data(using: .utf8)!) + XCTAssertEqual(broker.url, broker.name) + for mirror in broker.mirrorSites { + XCTAssertEqual(mirror.url, mirror.name) + } + } catch { + XCTFail("JSON string should be parsed correctly.") + } + } + + func testVerecorJSONWithURL_isCorrectlyParsed() { + do { + let broker = try JSONDecoder().decode(DataBroker.self, from: verecorWithURLJSONString.data(using: .utf8)!) + XCTAssertEqual(broker.url, "verecor.com") + XCTAssertEqual(broker.name, "Verecor") + + for mirror in broker.mirrorSites { + XCTAssertNotEqual(mirror.url, mirror.name) + } } catch { XCTFail("JSON string should be parsed correctly.") } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift index 3a65f7ae9a..5fa7f4b069 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift @@ -44,7 +44,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let extractedProfileId: Int64 = 1 let currentPreferredRunDate = Date() - let mockDataBroker = DataBroker(name: "databroker", steps: [Step](), version: "1.0", schedulingConfig: config) + let mockDataBroker = DataBroker(name: "databroker", url: "databroker.com", steps: [Step](), version: "1.0", schedulingConfig: config) let mockProfileQuery = ProfileQuery(id: profileQueryId, firstName: "a", lastName: "b", city: "c", state: "d", birthYear: 1222) let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] @@ -92,7 +92,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let extractedProfileId: Int64 = 1 let currentPreferredRunDate = Date() - let mockDataBroker = DataBroker(name: "databroker", steps: [Step](), version: "1.0", schedulingConfig: config) + let mockDataBroker = DataBroker(name: "databroker", url: "databroker.com", steps: [Step](), version: "1.0", schedulingConfig: config) let mockProfileQuery = ProfileQuery(id: profileQueryId, firstName: "a", lastName: "b", city: "c", state: "d", birthYear: 1222) let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] @@ -143,7 +143,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let extractedProfileId: Int64 = 1 let currentPreferredRunDate = Date() - let mockDataBroker = DataBroker(name: "databroker", steps: [Step](), version: "1.0", schedulingConfig: config) + let mockDataBroker = DataBroker(name: "databroker", url: "databroker.com", steps: [Step](), version: "1.0", schedulingConfig: config) let mockProfileQuery = ProfileQuery(id: profileQueryId, firstName: "a", lastName: "b", city: "c", state: "d", birthYear: 1222) let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] @@ -745,7 +745,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let extractedProfileId: Int64 = 1 let currentPreferredRunDate = Date() - let mockDataBroker = DataBroker(name: "databroker", steps: [Step](), version: "1.0", schedulingConfig: config) + let mockDataBroker = DataBroker(name: "databroker", url: "databroker.com", steps: [Step](), version: "1.0", schedulingConfig: config) let mockProfileQuery = ProfileQuery(id: profileQueryId, firstName: "a", lastName: "b", city: "c", state: "d", birthYear: 1222) let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] @@ -770,7 +770,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let currentPreferredRunDate = Date() let expectedPreferredRunDate = Date().addingTimeInterval(config.confirmOptOutScan.hoursToSeconds) - let mockDataBroker = DataBroker(name: "databroker", steps: [Step](), version: "1.0", schedulingConfig: config) + let mockDataBroker = DataBroker(name: "databroker", url: "databroker.com", steps: [Step](), version: "1.0", schedulingConfig: config) let mockProfileQuery = ProfileQuery(id: profileQueryId, firstName: "a", lastName: "b", city: "c", state: "d", birthYear: 1222) let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] @@ -846,6 +846,7 @@ extension DataBroker { DataBroker( id: 1, name: "Test broker", + url: "testbroker.com", steps: [ Step(type: .scan, actions: [Action]()), Step(type: .optOut, actions: [Action]()) @@ -863,6 +864,7 @@ extension DataBroker { DataBroker( id: 1, name: "Test broker", + url: "testbroker.com", steps: [ Step(type: .scan, actions: [Action]()), Step(type: .optOut, actions: [Action](), optOutType: .parentSiteOptOut) @@ -879,6 +881,7 @@ extension DataBroker { static var mockWithoutId: DataBroker { DataBroker( name: "Test broker", + url: "testbroker.com", steps: [Step](), version: "1.0", schedulingConfig: DataBrokerScheduleConfig( diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionUpdaterTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionUpdaterTests.swift index dcbc31a911..2036ac9a1d 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionUpdaterTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionUpdaterTests.swift @@ -111,7 +111,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { if let vault = self.vault { let sut = DataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) repository.lastCheckedVersion = nil - resources.brokersList = [.init(id: 1, name: "Broker", steps: [Step](), version: "1.0.1", schedulingConfig: .mock)] + resources.brokersList = [.init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.1", schedulingConfig: .mock)] vault.shouldReturnOldVersionBroker = true sut.checkForUpdatesInBrokerJSONFiles() @@ -129,7 +129,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { if let vault = self.vault { let sut = DataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) repository.lastCheckedVersion = nil - resources.brokersList = [.init(id: 1, name: "Broker", steps: [Step](), version: "1.0.1", schedulingConfig: .mock)] + resources.brokersList = [.init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.1", schedulingConfig: .mock)] vault.shouldReturnNewVersionBroker = true sut.checkForUpdatesInBrokerJSONFiles() @@ -146,7 +146,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { if let vault = self.vault { let sut = DataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) repository.lastCheckedVersion = nil - resources.brokersList = [.init(id: 1, name: "Broker", steps: [Step](), version: "1.0.0", schedulingConfig: .mock)] + resources.brokersList = [.init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.0", schedulingConfig: .mock)] vault.profileQueries = [.mock] sut.checkForUpdatesInBrokerJSONFiles() diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/EmailServiceTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/EmailServiceTests.swift index cc23589ec8..c82680d1e9 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/EmailServiceTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/EmailServiceTests.swift @@ -41,7 +41,7 @@ final class EmailServiceTests: XCTestCase { let sut = EmailService(urlSession: mockURLSession, redeemUseCase: MockRedeemUseCase()) do { - _ = try await sut.getEmail(dataBrokerName: "fakeBroker") + _ = try await sut.getEmail(dataBrokerURL: "fakeBroker") XCTFail("Expected an error to be thrown") } catch { if let error = error as? EmailError, @@ -62,7 +62,7 @@ final class EmailServiceTests: XCTestCase { let sut = EmailService(urlSession: mockURLSession, redeemUseCase: MockRedeemUseCase()) do { - _ = try await sut.getEmail(dataBrokerName: "fakeBroker") + _ = try await sut.getEmail(dataBrokerURL: "fakeBroker") XCTFail("Expected an error to be thrown") } catch { if let error = error as? EmailError, case .cantFindEmail = error { @@ -81,7 +81,7 @@ final class EmailServiceTests: XCTestCase { let sut = EmailService(urlSession: mockURLSession, redeemUseCase: MockRedeemUseCase()) do { - let email = try await sut.getEmail(dataBrokerName: "fakeBroker") + let email = try await sut.getEmail(dataBrokerURL: "fakeBroker") XCTAssertEqual("test@ddg.com", email) } catch { XCTFail("Unexpected. It should not throw") diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MapperToUITests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MapperToUITests.swift index 4d1ff6b8f8..c59ee80486 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MapperToUITests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MapperToUITests.swift @@ -140,9 +140,9 @@ final class MapperToUITests: XCTestCase { func testLastScans_areMappedCorrectly() { let brokerProfileQueryData: [BrokerProfileQueryData] = [ - .mock(dataBrokerName: "Broker #1", lastRunDate: Date().yesterday), - .mock(dataBrokerName: "Broker #2", lastRunDate: Date().yesterday), - .mock(dataBrokerName: "Broker #3") + .mock(dataBrokerName: "Broker #1", url: "broker1.com", lastRunDate: Date().yesterday), + .mock(dataBrokerName: "Broker #2", url: "broker2.com", lastRunDate: Date().yesterday), + .mock(dataBrokerName: "Broker #3", url: "broker3.com") ] let result = sut.maintenanceScanState(brokerProfileQueryData) @@ -153,9 +153,9 @@ final class MapperToUITests: XCTestCase { func testNextScans_areMappedCorrectly() { let brokerProfileQueryData: [BrokerProfileQueryData] = [ - .mock(dataBrokerName: "Broker #1", preferredRunDate: Date().tomorrow), - .mock(dataBrokerName: "Broker #2", preferredRunDate: Date().tomorrow), - .mock(dataBrokerName: "Broker #3") + .mock(dataBrokerName: "Broker #1", url: "broker1.com", preferredRunDate: Date().tomorrow), + .mock(dataBrokerName: "Broker #2", url: "broker2.com", preferredRunDate: Date().tomorrow), + .mock(dataBrokerName: "Broker #3", url: "broker3.com") ] let result = sut.maintenanceScanState(brokerProfileQueryData) @@ -165,7 +165,7 @@ final class MapperToUITests: XCTestCase { } func testWhenMirrorSiteIsNotInRemovedPeriod_thenItShouldBeAddedToTotalScans() { - let brokerProfileQueryWithMirrorSite: BrokerProfileQueryData = .mock(dataBrokerName: "Broker #1", mirrorSites: [.init(name: "mirror", addedAt: Date(), removedAt: nil)]) + let brokerProfileQueryWithMirrorSite: BrokerProfileQueryData = .mock(dataBrokerName: "Broker #1", mirrorSites: [.init(name: "mirror", url: "mirror1.com", addedAt: Date(), removedAt: nil)]) let brokerProfileQueryData: [BrokerProfileQueryData] = [ brokerProfileQueryWithMirrorSite, brokerProfileQueryWithMirrorSite, @@ -178,7 +178,7 @@ final class MapperToUITests: XCTestCase { } func testWhenMirrorSiteIsInRemovedPeriod_thenItShouldNotBeAddedToTotalScans() { - let brokerWithMirrorSiteThatWasRemoved = BrokerProfileQueryData.mock(dataBrokerName: "Broker #1", mirrorSites: [.init(name: "mirror", addedAt: Date(), removedAt: Date().yesterday)]) + let brokerWithMirrorSiteThatWasRemoved = BrokerProfileQueryData.mock(dataBrokerName: "Broker #1", mirrorSites: [.init(name: "mirror", url: "mirror1.com", addedAt: Date(), removedAt: Date().yesterday)]) let brokerProfileQueryData: [BrokerProfileQueryData] = [.mock(dataBrokerName: "Broker #1"), brokerWithMirrorSiteThatWasRemoved, .mock(dataBrokerName: "Broker #2")] let result = sut.initialScanState(brokerProfileQueryData) @@ -190,7 +190,7 @@ final class MapperToUITests: XCTestCase { let brokerWithMirrorSiteNotRemovedAndWithScan = BrokerProfileQueryData.mock( dataBrokerName: "Broker #1", lastRunDate: Date(), - mirrorSites: [.init(name: "mirror", addedAt: Date(), removedAt: nil)] + mirrorSites: [.init(name: "mirror", url: "mirror.com", addedAt: Date(), removedAt: nil)] ) let brokerProfileQueryData: [BrokerProfileQueryData] = [ brokerWithMirrorSiteNotRemovedAndWithScan, @@ -207,7 +207,7 @@ final class MapperToUITests: XCTestCase { let brokerWithMirrorSiteRemovedAndWithScan = BrokerProfileQueryData.mock( dataBrokerName: "Broker #2", lastRunDate: Date(), - mirrorSites: [.init(name: "mirror", addedAt: Date(), removedAt: Date().yesterday)] + mirrorSites: [.init(name: "mirror", url: "mirror1.com", addedAt: Date(), removedAt: Date().yesterday)] ) let brokerProfileQueryData: [BrokerProfileQueryData] = [ .mock(dataBrokerName: "Broker #1"), @@ -223,7 +223,7 @@ final class MapperToUITests: XCTestCase { func testWhenMirrorSiteIsNotInRemovedPeriod_thenMatchIsAdded() { let brokerWithMirrorSiteNotRemovedAndWithMatch = BrokerProfileQueryData.mock( extractedProfile: .mockWithoutRemovedDate, - mirrorSites: [.init(name: "mirror", addedAt: Date(), removedAt: nil)] + mirrorSites: [.init(name: "mirror", url: "mirror1.com", addedAt: Date(), removedAt: nil)] ) let brokerProfileQueryData: [BrokerProfileQueryData] = [.mock(), .mock(), brokerWithMirrorSiteNotRemovedAndWithMatch] @@ -235,7 +235,7 @@ final class MapperToUITests: XCTestCase { func testWhenMirrorSiteIsInRemovedPeriod_thenMatchIsNotAdded() { let brokerWithMirrorSiteRemovedAndWithMatch = BrokerProfileQueryData.mock( extractedProfile: .mockWithoutRemovedDate, - mirrorSites: [.init(name: "mirror", addedAt: Date(), removedAt: Date().yesterday)] + mirrorSites: [.init(name: "mirror", url: "mirror1.com", addedAt: Date(), removedAt: Date().yesterday)] ) let brokerProfileQueryData: [BrokerProfileQueryData] = [.mock(), .mock(), brokerWithMirrorSiteRemovedAndWithMatch] @@ -246,8 +246,8 @@ final class MapperToUITests: XCTestCase { func testMirrorSites_areCorrectlyMappedToInProgressOptOuts() { let scanHistoryEventsWithMatchesFound: [HistoryEvent] = [.init(brokerId: 1, profileQueryId: 1, type: .matchesFound(count: 1), date: Date())] - let mirrorSiteNotRemoved = MirrorSite(name: "mirror #1", addedAt: Date.distantPast, removedAt: nil) - let mirrorSiteRemoved = MirrorSite(name: "mirror #2", addedAt: Date.distantPast, removedAt: Date().yesterday) // Should not be added + let mirrorSiteNotRemoved = MirrorSite(name: "mirror #1", url: "mirror1.com", addedAt: Date.distantPast, removedAt: nil) + let mirrorSiteRemoved = MirrorSite(name: "mirror #2", url: "mirror2.com", addedAt: Date.distantPast, removedAt: Date().yesterday) // Should not be added let brokerProfileQueryData: [BrokerProfileQueryData] = [ .mock(extractedProfile: .mockWithoutRemovedDate, scanHistoryEvents: scanHistoryEventsWithMatchesFound, @@ -261,10 +261,10 @@ final class MapperToUITests: XCTestCase { func testWhenMirrorSiteRemovedIsInRangeToPastRemovedProfile_thenIsAddedToCompletedOptOuts() { let scanHistoryEventsWithMatchesFound: [HistoryEvent] = [.init(brokerId: 1, profileQueryId: 1, type: .matchesFound(count: 1), date: Date().yesterday!)] - let mirrorSiteRemoved = MirrorSite(name: "mirror #1", addedAt: Date.distantPast, removedAt: Date()) // Should be added + let mirrorSiteRemoved = MirrorSite(name: "mirror #1", url: "mirror1.com", addedAt: Date.distantPast, removedAt: Date()) // Should be added // The next two mirror sites should not be added. New mirror sites should not count for old opt-outs - let newMirrorSiteOne = MirrorSite(name: "mirror #2", addedAt: Date(), removedAt: nil) - let newMirrorSiteTwo = MirrorSite(name: "mirror #3", addedAt: Date(), removedAt: nil) + let newMirrorSiteOne = MirrorSite(name: "mirror #2", url: "mirror2.com", addedAt: Date(), removedAt: nil) + let newMirrorSiteTwo = MirrorSite(name: "mirror #3", url: "mirror3.com", addedAt: Date(), removedAt: nil) let brokerProfileQuery = BrokerProfileQueryData.mock(extractedProfile: .mockWithRemoveDate(Date().yesterday!), scanHistoryEvents: scanHistoryEventsWithMatchesFound, mirrorSites: [mirrorSiteRemoved, newMirrorSiteOne, newMirrorSiteTwo]) @@ -276,12 +276,12 @@ final class MapperToUITests: XCTestCase { } func testLastScansWithMirrorSites_areMappedCorrectly() { - let includedMirrorSite = MirrorSite(name: "mirror #1", addedAt: Date.distantPast, removedAt: nil) - let notIncludedMirrorSite = MirrorSite(name: "mirror #2", addedAt: Date(), removedAt: nil) + let includedMirrorSite = MirrorSite(name: "mirror #1", url: "mirror1.com", addedAt: Date.distantPast, removedAt: nil) + let notIncludedMirrorSite = MirrorSite(name: "mirror #2", url: "mirror2.com", addedAt: Date(), removedAt: nil) let brokerProfileQueryData: [BrokerProfileQueryData] = [ - .mock(dataBrokerName: "Broker #1", lastRunDate: Date().yesterday, mirrorSites: [includedMirrorSite, notIncludedMirrorSite]), - .mock(dataBrokerName: "Broker #2", lastRunDate: Date().yesterday), - .mock(dataBrokerName: "Broker #3") + .mock(dataBrokerName: "Broker #1", url: "broker1.com", lastRunDate: Date().yesterday, mirrorSites: [includedMirrorSite, notIncludedMirrorSite]), + .mock(dataBrokerName: "Broker #2", url: "broker2.com", lastRunDate: Date().yesterday), + .mock(dataBrokerName: "Broker #3", url: "broker3.com") ] let result = sut.maintenanceScanState(brokerProfileQueryData) @@ -291,12 +291,12 @@ final class MapperToUITests: XCTestCase { } func testNextScansWithMirrorSites_areMappedCorrectly() { - let includedMirrorSite = MirrorSite(name: "mirror #1", addedAt: Date.distantPast, removedAt: nil) - let notIncludedMirrorSite = MirrorSite(name: "mirror #2", addedAt: Date.distantPast, removedAt: Date()) + let includedMirrorSite = MirrorSite(name: "mirror #1", url: "mirror1.com", addedAt: Date.distantPast, removedAt: nil) + let notIncludedMirrorSite = MirrorSite(name: "mirror #2", url: "mirror2.com", addedAt: Date.distantPast, removedAt: Date()) let brokerProfileQueryData: [BrokerProfileQueryData] = [ - .mock(dataBrokerName: "Broker #1", preferredRunDate: Date().tomorrow, mirrorSites: [includedMirrorSite, notIncludedMirrorSite]), - .mock(dataBrokerName: "Broker #2", preferredRunDate: Date().tomorrow), - .mock(dataBrokerName: "Broker #3") + .mock(dataBrokerName: "Broker #1", url: "broker1.com", preferredRunDate: Date().tomorrow, mirrorSites: [includedMirrorSite, notIncludedMirrorSite]), + .mock(dataBrokerName: "Broker #2", url: "broker2.com", preferredRunDate: Date().tomorrow), + .mock(dataBrokerName: "Broker #3", url: "broker3.com") ] let result = sut.maintenanceScanState(brokerProfileQueryData) diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift index 8585792744..12b7120077 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift @@ -152,6 +152,7 @@ extension BrokerProfileQueryData { BrokerProfileQueryData( dataBroker: DataBroker( name: "parent", + url: "parent.com", steps: [Step](), version: "1.0.0", schedulingConfig: DataBrokerScheduleConfig.mock @@ -165,6 +166,7 @@ extension BrokerProfileQueryData { BrokerProfileQueryData( dataBroker: DataBroker( name: "child", + url: "child.com", steps: [Step](), version: "1.0.0", schedulingConfig: DataBrokerScheduleConfig.mock, diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift index d053404255..95ea0d1493 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift @@ -27,6 +27,7 @@ import GRDB extension BrokerProfileQueryData { static func mock(with steps: [Step] = [Step](), dataBrokerName: String = "test", + url: String = "test.com", lastRunDate: Date? = nil, preferredRunDate: Date? = nil, extractedProfile: ExtractedProfile? = nil, @@ -36,6 +37,7 @@ extension BrokerProfileQueryData { BrokerProfileQueryData( dataBroker: DataBroker( name: dataBrokerName, + url: url, steps: steps, version: "1.0.0", schedulingConfig: DataBrokerScheduleConfig.mock, @@ -232,7 +234,7 @@ final class EmailServiceMock: EmailServiceProtocol { var shouldThrow: Bool = false - func getEmail(dataBrokerName: String?) async throws -> String { + func getEmail(dataBrokerURL: String?) async throws -> String { if shouldThrow { throw DataBrokerProtectionError.emailError(nil) } @@ -491,9 +493,9 @@ final class DataBrokerProtectionSecureVaultMock: DataBrokerProtectionSecureVault func fetchBroker(with name: String) throws -> DataBroker? { if shouldReturnOldVersionBroker { - return .init(id: 1, name: "Broker", steps: [Step](), version: "1.0.0", schedulingConfig: .mock) + return .init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.0", schedulingConfig: .mock) } else if shouldReturnNewVersionBroker { - return .init(id: 1, name: "Broker", steps: [Step](), version: "1.0.1", schedulingConfig: .mock) + return .init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.1", schedulingConfig: .mock) } return nil diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateUpdaterTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateUpdaterTests.swift index b9472a9ab9..369f80ee32 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateUpdaterTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateUpdaterTests.swift @@ -35,6 +35,7 @@ final class OperationPreferredDateUpdaterTests: XCTestCase { let childBroker = DataBroker( id: 1, name: "Child broker", + url: "childbroker.com", steps: [Step](), version: "1.0", schedulingConfig: DataBrokerScheduleConfig( From 3002b7dfe73703eda29eb3997254afa097ff1de3 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Wed, 21 Feb 2024 18:51:13 +0000 Subject: [PATCH 39/44] Bump version to 1.76.0 (123) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 060339a249..550b71a3ee 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 122 +CURRENT_PROJECT_VERSION = 123 From 883db580de28433681a5bf97f5ddfab4600ebebe Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Wed, 21 Feb 2024 16:00:31 -0300 Subject: [PATCH 40/44] DBP: Add initial loading indicator when loading web UI (#2227) --- .../DataBrokerProtectionViewController.swift | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DataBrokerProtectionViewController.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DataBrokerProtectionViewController.swift index accf119bb7..ce258c3565 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DataBrokerProtectionViewController.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DataBrokerProtectionViewController.swift @@ -26,6 +26,7 @@ final public class DataBrokerProtectionViewController: NSViewController { private let dataManager: DataBrokerProtectionDataManaging private let scheduler: DataBrokerProtectionScheduler private var webView: WKWebView? + private var loader: NSProgressIndicator! private let webUISettings: DataBrokerProtectionWebUIURLSettingsRepresentable private let webUIViewModel: DBPUIViewModel @@ -63,9 +64,10 @@ final public class DataBrokerProtectionViewController: NSViewController { public override func viewDidLoad() { super.viewDidLoad() + addLoadingIndicator() reloadObserver = NotificationCenter.default.addObserver(forName: DataBrokerProtectionNotifications.shouldReloadUI, - object: nil, - queue: .main) { [weak self] _ in + object: nil, + queue: .main) { [weak self] _ in self?.webView?.reload() } } @@ -75,16 +77,39 @@ final public class DataBrokerProtectionViewController: NSViewController { webView = WKWebView(frame: CGRect(x: 0, y: 0, width: 1024, height: 768), configuration: configuration) webView?.uiDelegate = self + webView?.navigationDelegate = self view = webView! if let url = URL(string: webUISettings.selectedURL) { webView?.load(url) } else { + removeLoadingIndicator() assertionFailure("Selected URL is not valid \(webUISettings.selectedURL)") } } + private func addLoadingIndicator() { + loader = NSProgressIndicator() + loader.wantsLayer = true + loader.style = .spinning + loader.controlSize = .regular + loader.sizeToFit() + loader.translatesAutoresizingMaskIntoConstraints = false + loader.controlSize = .large + view.addSubview(loader) + + NSLayoutConstraint.activate([ + loader.centerXAnchor.constraint(equalTo: view.centerXAnchor), + loader.centerYAnchor.constraint(equalTo: view.centerYAnchor), + ]) + } + + private func removeLoadingIndicator() { + loader.stopAnimation(nil) + loader.removeFromSuperview() + } + deinit { if let reloadObserver { NotificationCenter.default.removeObserver(reloadObserver) @@ -98,3 +123,14 @@ extension DataBrokerProtectionViewController: WKUIDelegate { return nil } } + +extension DataBrokerProtectionViewController: WKNavigationDelegate { + + public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { + loader.startAnimation(nil) + } + + public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + removeLoadingIndicator() + } +} From d5c1756e2264d08cf308f79abe8fd22d126fccd1 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Wed, 21 Feb 2024 21:54:34 +0100 Subject: [PATCH 41/44] macOS: Transparent proxy for excluding VPN traffic. (#2128) Task/Issue URL: https://app.asana.com/0/0/1206462407536023/f Tech Design URLs: - [Tech Design: How to exclude Data Broker traffic?](https://app.asana.com/0/481882893211075/1206363506060150/f) - [Tech Design: Mechanism to allow PIR to start excluding its traffic from the VPN tunnel](https://app.asana.com/0/481882893211075/1206446978081253/f) - [Tech Design: How will the proxy recover from failure?](https://app.asana.com/0/481882893211075/1206446978546262) iOS PR: https://github.com/duckduckgo/iOS/pull/2429 BSK PR: https://github.com/duckduckgo/BrowserServicesKit/pull/652 ## Description Adds a transparent proxy that allows excluding app and domain traffic from the VPN. ## Known issues / limitations ### Issue 1: Exclusion delay on existing flows When switching off an exclusion, connection flows seem to switch immediately to routing through the tunnel interface again. However when turning the exclusion back ON, connection flows seem to take a bit before routing back through the proxy. This should not be a big problem as eventually connections start being excluded correctly again. It's unclear at this point if this is a macOS bug, or a bug on our proxy - but I don't think this should be a blocker by any means. --- Configuration/App/DuckDuckGoAppStore.xcconfig | 5 - Configuration/AppStore.xcconfig | 26 +- Configuration/DeveloperID.xcconfig | 14 + .../NetworkProtectionAppExtension.xcconfig | 40 +- .../VPNProxyExtension.xcconfig | 52 +++ DuckDuckGo.xcodeproj/project.pbxproj | 287 +++++++++++-- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../DBP/DataBrokerProtectionDebugMenu.swift | 10 + .../DBP/DataBrokerProtectionManager.swift | 10 +- .../DBP/LoginItem+DataBrokerProtection.swift | 1 + DuckDuckGo/DuckDuckGo.entitlements | 1 + DuckDuckGo/DuckDuckGoAppStore.entitlements | 5 + DuckDuckGo/DuckDuckGoAppStoreCI.entitlements | 4 - DuckDuckGo/DuckDuckGoDebug.entitlements | 1 + DuckDuckGo/DuckDuckGo_NetP_Debug.entitlements | 30 -- .../DuckDuckGo_NetP_Release.entitlements | 38 -- DuckDuckGo/InfoPlist.xcstrings | 2 +- .../Bundle+VPN.swift | 65 +++ .../NetworkProtectionBundle.swift | 78 ---- .../NetworkProtectionDebugMenu.swift | 46 ++- .../NetworkProtectionTunnelController.swift | 19 +- .../MacPacketTunnelProvider.swift | 20 +- .../MacTransparentProxyProvider.swift | 94 +++++ ...NetworkProtectionAppExtension.entitlements | 1 + .../BrowserWindowManager.swift | 64 +++ .../IPCServiceManager.swift | 13 +- DuckDuckGoVPN/DuckDuckGoVPN.entitlements | 1 + DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift | 93 ++++- DuckDuckGoVPN/DuckDuckGoVPNDebug.entitlements | 1 + DuckDuckGoVPN/Info-AppStore.plist | 6 +- DuckDuckGoVPN/Info.plist | 6 +- DuckDuckGoVPN/VPNProxyLauncher.swift | 149 +++++++ .../DataBrokerProtection/Package.swift | 2 +- .../IPC/DataBrokerProtectionIPCClient.swift | 13 +- .../IPC/DataBrokerProtectionIPCServer.swift | 16 + .../Pixels/DataBrokerProtectionPixels.swift | 1 - LocalPackages/LoginItems/Package.swift | 2 +- .../NetworkProtectionMac/Package.resolved | 104 +++++ .../NetworkProtectionMac/Package.swift | 16 +- .../FlowManagers/TCPFlowManager.swift | 242 +++++++++++ .../FlowManagers/UDPFlowManager.swift | 329 +++++++++++++++ .../TransparentProxyAppMessageHandler.swift | 82 ++++ .../IPC/TransparentProxyRequest.swift | 67 +++ .../TransparentProxyControllerPixel.swift | 89 ++++ .../TransparentProxyProviderPixel.swift | 93 +++++ .../RoutingRules/VPNAppRoutingRules.swift | 16 +- .../RoutingRules/VPNRoutingRule.swift | 20 +- .../Settings/TransparentProxySettings.swift | 134 ++++++ .../UserDefaults+excludedApps.swift | 79 ++++ .../UserDefaults+excludedDomains.swift | 51 +++ .../TransparentProxyController.swift | 293 +++++++++++++ .../TransparentProxyProvider.swift | 389 ++++++++++++++++++ ...ransparentProxyProviderConfiguration.swift | 40 ++ .../NetworkProtectionStatusView.swift | 4 + .../NetworkProtectionStatusViewModel.swift | 34 +- ...TransparentProxyControllerPixelTests.swift | 120 ++++++ .../TransparentProxyProviderPixelTests.swift | 66 +++ LocalPackages/PixelKit/Package.swift | 2 +- .../PixelKit/PixelKit+Parameters.swift | 6 +- .../PixelKit/Sources/PixelKit/PixelKit.swift | 24 +- .../Sources/PixelKit/PixelKitEvent.swift | 2 +- .../Sources/PixelKit/PixelKitEventV2.swift | 70 ++++ .../PixelFireExpectations.swift | 36 ++ .../XCTestCase+PixelKit.swift | 148 +++++++ LocalPackages/SubscriptionUI/Package.swift | 2 +- LocalPackages/SwiftUIExtensions/Package.swift | 2 +- LocalPackages/SyncUI/Package.swift | 2 +- .../SystemExtensionManager/Package.swift | 2 +- LocalPackages/XPCHelper/Package.swift | 2 +- NetworkProtectionSystemExtension/Info.plist | 2 + ...otectionSystemExtension_Debug.entitlements | 1 + ...ectionSystemExtension_Release.entitlements | 1 + VPNProxyExtension/Info.plist | 17 + .../VPNProxyExtension.entitlements | 25 ++ fastlane/Matchfile | 2 + scripts/assets/AppStoreExportOptions.plist | 4 + 76 files changed, 3497 insertions(+), 341 deletions(-) create mode 100644 Configuration/Extensions/NetworkProtection/VPNProxyExtension.xcconfig delete mode 100644 DuckDuckGo/DuckDuckGo_NetP_Debug.entitlements delete mode 100644 DuckDuckGo/DuckDuckGo_NetP_Release.entitlements create mode 100644 DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/Bundle+VPN.swift delete mode 100644 DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/NetworkProtectionBundle.swift create mode 100644 DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacTransparentProxyProvider.swift create mode 100644 DuckDuckGoDBPBackgroundAgent/BrowserWindowManager.swift create mode 100644 DuckDuckGoVPN/VPNProxyLauncher.swift create mode 100644 LocalPackages/NetworkProtectionMac/Package.resolved create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/FlowManagers/TCPFlowManager.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/FlowManagers/UDPFlowManager.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/IPC/TransparentProxyAppMessageHandler.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/IPC/TransparentProxyRequest.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyControllerPixel.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyProviderPixel.swift rename DuckDuckGoVPN/Bundle+Configuration.swift => LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/RoutingRules/VPNAppRoutingRules.swift (56%) rename DuckDuckGo/NetworkProtection/NetworkExtensionTargets/SystemExtensionAndNotificationTargets/NetworkProtectionExtensionMachService.swift => LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/RoutingRules/VPNRoutingRule.swift (59%) create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/TransparentProxySettings.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/UserDefaultsExtensions/UserDefaults+excludedApps.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/UserDefaultsExtensions/UserDefaults+excludedDomains.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyController.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyProvider.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyProviderConfiguration.swift create mode 100644 LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionProxyTests/TransparentProxyControllerPixelTests.swift create mode 100644 LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionProxyTests/TransparentProxyProviderPixelTests.swift create mode 100644 LocalPackages/PixelKit/Sources/PixelKit/PixelKitEventV2.swift create mode 100644 LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/PixelFireExpectations.swift create mode 100644 LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift create mode 100644 VPNProxyExtension/Info.plist create mode 100644 VPNProxyExtension/VPNProxyExtension.entitlements diff --git a/Configuration/App/DuckDuckGoAppStore.xcconfig b/Configuration/App/DuckDuckGoAppStore.xcconfig index 3ee212ad5e..904caca8c5 100644 --- a/Configuration/App/DuckDuckGoAppStore.xcconfig +++ b/Configuration/App/DuckDuckGoAppStore.xcconfig @@ -17,11 +17,6 @@ #include "../AppStore.xcconfig" #include "ManualAppStoreRelease.xcconfig" -AGENT_BUNDLE_ID[sdk=*] = com.duckduckgo.mobile.ios.vpn.agent -AGENT_BUNDLE_ID[config=Debug][sdk=*] = com.duckduckgo.mobile.ios.vpn.agent.debug -AGENT_BUNDLE_ID[config=CI][sdk=*] = com.duckduckgo.mobile.ios.vpn.agent.debug -AGENT_BUNDLE_ID[config=Review][sdk=*] = com.duckduckgo.mobile.ios.vpn.agent.review - PRODUCT_BUNDLE_IDENTIFIER = $(MAIN_BUNDLE_IDENTIFIER) CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAppStore.entitlements diff --git a/Configuration/AppStore.xcconfig b/Configuration/AppStore.xcconfig index c2ae87c9b5..0ad3f9f6b5 100644 --- a/Configuration/AppStore.xcconfig +++ b/Configuration/AppStore.xcconfig @@ -50,21 +50,23 @@ AGENT_BUNDLE_ID[config=Review][sdk=*] = $(AGENT_BUNDLE_ID_BASE).review AGENT_PRODUCT_NAME = DuckDuckGo VPN App Store AGENT_RELEASE_PRODUCT_NAME = DuckDuckGo VPN -SYSEX_BUNDLE_ID[sdk=*] = com.duckduckgo.mobile.ios.vpn.agent -SYSEX_BUNDLE_ID[config=Debug][sdk=*] = com.duckduckgo.mobile.ios.vpn.agent.debug.network-protection-extension -SYSEX_BUNDLE_ID[config=CI][sdk=*] = com.duckduckgo.mobile.ios.vpn.agent.debug.network-protection-extension -SYSEX_BUNDLE_ID[config=Review][sdk=*] = com.duckduckgo.mobile.ios.vpn.agent.review.network-protection-extension -SYSEX_BUNDLE_ID[config=Release][sdk=*] = com.duckduckgo.mobile.ios.vpn.agent.network-protection-extension +// Extensions -// Distributed Notifications Prefix +PROXY_EXTENSION_BUNDLE_ID[sdk=*] = $(AGENT_BUNDLE_ID).proxy +PROXY_EXTENSION_BUNDLE_ID[config=Debug][sdk=*] = $(AGENT_BUNDLE_ID).proxy +PROXY_EXTENSION_BUNDLE_ID[config=CI][sdk=*] = $(AGENT_BUNDLE_ID).proxy +PROXY_EXTENSION_BUNDLE_ID[config=Review][sdk=*] = $(AGENT_BUNDLE_ID).proxy +PROXY_EXTENSION_BUNDLE_ID[config=Release][sdk=*] = $(AGENT_BUNDLE_ID).proxy + +TUNNEL_EXTENSION_BUNDLE_ID[sdk=*] = $(AGENT_BUNDLE_ID).network-protection-extension +TUNNEL_EXTENSION_BUNDLE_ID[config=Debug][sdk=*] = $(AGENT_BUNDLE_ID).network-protection-extension +TUNNEL_EXTENSION_BUNDLE_ID[config=CI][sdk=*] = $(AGENT_BUNDLE_ID).network-protection-extension +TUNNEL_EXTENSION_BUNDLE_ID[config=Review][sdk=*] = $(AGENT_BUNDLE_ID).network-protection-extension +TUNNEL_EXTENSION_BUNDLE_ID[config=Release][sdk=*] = $(AGENT_BUNDLE_ID).network-protection-extension -SYSEX_BUNDLE_ID_BASE[sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension -SYSEX_BUNDLE_ID_BASE[config=Debug][sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension -SYSEX_BUNDLE_ID_BASE[config=CI][sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension -SYSEX_BUNDLE_ID_BASE[config=Review][sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension -SYSEX_BUNDLE_ID_BASE[config=Release][sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension +// Distributed Notifications Prefix -DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE = $(SYSEX_BUNDLE_ID_BASE) +DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE = $(AGENT_BUNDLE_ID_BASE).network-extension DISTRIBUTED_NOTIFICATIONS_PREFIX[config=CI][sdk=*] = $(DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE).ci DISTRIBUTED_NOTIFICATIONS_PREFIX[config=Review][sdk=*] = $(DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE).review diff --git a/Configuration/DeveloperID.xcconfig b/Configuration/DeveloperID.xcconfig index 0bfb9bb8cb..b66acc76d2 100644 --- a/Configuration/DeveloperID.xcconfig +++ b/Configuration/DeveloperID.xcconfig @@ -65,6 +65,20 @@ AGENT_BUNDLE_ID[config=CI][sdk=*] = $(AGENT_BUNDLE_ID_BASE).debug AGENT_BUNDLE_ID[config=Review][sdk=*] = $(AGENT_BUNDLE_ID_BASE).review AGENT_PRODUCT_NAME = DuckDuckGo VPN +// Extensions + +PROXY_EXTENSION_BUNDLE_ID[sdk=*] = $(SYSEX_BUNDLE_ID) +PROXY_EXTENSION_BUNDLE_ID[config=Debug][sdk=*] = $(SYSEX_BUNDLE_ID) +PROXY_EXTENSION_BUNDLE_ID[config=CI][sdk=*] = $(SYSEX_BUNDLE_ID) +PROXY_EXTENSION_BUNDLE_ID[config=Review][sdk=*] = $(SYSEX_BUNDLE_ID) +PROXY_EXTENSION_BUNDLE_ID[config=Release][sdk=*] = $(SYSEX_BUNDLE_ID) + +TUNNEL_EXTENSION_BUNDLE_ID[sdk=*] = $(SYSEX_BUNDLE_ID) +TUNNEL_EXTENSION_BUNDLE_ID[config=Debug][sdk=*] = $(SYSEX_BUNDLE_ID) +TUNNEL_EXTENSION_BUNDLE_ID[config=CI][sdk=*] = $(SYSEX_BUNDLE_ID) +TUNNEL_EXTENSION_BUNDLE_ID[config=Review][sdk=*] = $(SYSEX_BUNDLE_ID) +TUNNEL_EXTENSION_BUNDLE_ID[config=Release][sdk=*] = $(SYSEX_BUNDLE_ID) + // DBP DBP_BACKGROUND_AGENT_PRODUCT_NAME = DuckDuckGo Personal Information Removal diff --git a/Configuration/Extensions/NetworkProtection/NetworkProtectionAppExtension.xcconfig b/Configuration/Extensions/NetworkProtection/NetworkProtectionAppExtension.xcconfig index 60ad407569..2fb095fc56 100644 --- a/Configuration/Extensions/NetworkProtection/NetworkProtectionAppExtension.xcconfig +++ b/Configuration/Extensions/NetworkProtection/NetworkProtectionAppExtension.xcconfig @@ -14,10 +14,7 @@ // #include "../ExtensionBase.xcconfig" - -// Since we're using nonstandard bundle IDs we'll just define them here, but we should consider -// standardizing the bundle IDs so we can just define BUNDLE_IDENTIFIER_PREFIX -BUNDLE_IDENTIFIER_PREFIX = com.duckduckgo.mobile.ios.vpn.agent +#include "../../AppStore.xcconfig" CODE_SIGN_ENTITLEMENTS[config=CI][sdk=macosx*] = CODE_SIGN_ENTITLEMENTS[config=Debug][sdk=macosx*] = DuckDuckGo/NetworkProtectionAppExtension.entitlements @@ -38,17 +35,11 @@ FEATURE_FLAGS[config=CI][arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION FEATURE_FLAGS[config=Debug][arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION FEATURE_FLAGS[config=Review][arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION -NETP_BASE_APP_GROUP = $(DEVELOPMENT_TEAM).com.duckduckgo.macos.browser.network-protection -NETP_APP_GROUP[config=CI][sdk=macos*] = $(NETP_BASE_APP_GROUP).debug -NETP_APP_GROUP[config=Review][sdk=macos*] = $(NETP_BASE_APP_GROUP).review -NETP_APP_GROUP[config=Debug][sdk=macos*] = $(NETP_BASE_APP_GROUP).debug -NETP_APP_GROUP[config=Release][sdk=macos*] = $(NETP_BASE_APP_GROUP) - PRODUCT_BUNDLE_IDENTIFIER[sdk=*] = -PRODUCT_BUNDLE_IDENTIFIER[config=CI][sdk=*] = $(BUNDLE_IDENTIFIER_PREFIX).debug.network-protection-extension -PRODUCT_BUNDLE_IDENTIFIER[config=Debug][sdk=*] = $(BUNDLE_IDENTIFIER_PREFIX).debug.network-protection-extension -PRODUCT_BUNDLE_IDENTIFIER[config=Release][sdk=*] = $(BUNDLE_IDENTIFIER_PREFIX).network-protection-extension -PRODUCT_BUNDLE_IDENTIFIER[config=Review][sdk=*] = $(BUNDLE_IDENTIFIER_PREFIX).review.network-protection-extension +PRODUCT_BUNDLE_IDENTIFIER[config=CI][sdk=*] = $(TUNNEL_EXTENSION_BUNDLE_ID) +PRODUCT_BUNDLE_IDENTIFIER[config=Debug][sdk=*] = $(TUNNEL_EXTENSION_BUNDLE_ID) +PRODUCT_BUNDLE_IDENTIFIER[config=Release][sdk=*] = $(TUNNEL_EXTENSION_BUNDLE_ID) +PRODUCT_BUNDLE_IDENTIFIER[config=Review][sdk=*] = $(TUNNEL_EXTENSION_BUNDLE_ID) PROVISIONING_PROFILE_SPECIFIER[config=CI][sdk=macosx*] = PROVISIONING_PROFILE_SPECIFIER[config=Release][sdk=macosx*] = match AppStore com.duckduckgo.mobile.ios.vpn.agent.network-protection-extension macos @@ -59,24 +50,3 @@ SKIP_INSTALL = YES SWIFT_EMIT_LOC_STRINGS = YES LD_RUNPATH_SEARCH_PATHS = @executable_path/../Frameworks @executable_path/../../../../Frameworks - -// Distributed Notifications: - -AGENT_BUNDLE_ID_BASE[sdk=*] = com.duckduckgo.mobile.ios.vpn.agent -AGENT_BUNDLE_ID[sdk=*] = $(AGENT_BUNDLE_ID_BASE) -AGENT_BUNDLE_ID[config=Debug][sdk=*] = $(AGENT_BUNDLE_ID_BASE).debug -AGENT_BUNDLE_ID[config=CI][sdk=*] = $(AGENT_BUNDLE_ID_BASE).debug -AGENT_BUNDLE_ID[config=Review][sdk=*] = $(AGENT_BUNDLE_ID_BASE).review - -SYSEX_BUNDLE_ID_BASE[sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension -SYSEX_BUNDLE_ID_BASE[config=Debug][sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension -SYSEX_BUNDLE_ID_BASE[config=CI][sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension -SYSEX_BUNDLE_ID_BASE[config=Review][sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension -SYSEX_BUNDLE_ID_BASE[config=Release][sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension - -DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE = $(SYSEX_BUNDLE_ID_BASE) - -DISTRIBUTED_NOTIFICATIONS_PREFIX[config=CI][sdk=*] = $(DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE).ci -DISTRIBUTED_NOTIFICATIONS_PREFIX[config=Review][sdk=*] = $(DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE).review -DISTRIBUTED_NOTIFICATIONS_PREFIX[config=Debug][sdk=*] = $(DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE).debug -DISTRIBUTED_NOTIFICATIONS_PREFIX[config=Release][sdk=*] = $(DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE) diff --git a/Configuration/Extensions/NetworkProtection/VPNProxyExtension.xcconfig b/Configuration/Extensions/NetworkProtection/VPNProxyExtension.xcconfig new file mode 100644 index 0000000000..5f70d87091 --- /dev/null +++ b/Configuration/Extensions/NetworkProtection/VPNProxyExtension.xcconfig @@ -0,0 +1,52 @@ +// 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. +// + +#include "../ExtensionBase.xcconfig" +#include "../../AppStore.xcconfig" + +CODE_SIGN_ENTITLEMENTS[config=CI][sdk=macosx*] = +CODE_SIGN_ENTITLEMENTS[config=Debug][sdk=macosx*] = VPNProxyExtension/VPNProxyExtension.entitlements +CODE_SIGN_ENTITLEMENTS[config=Release][sdk=macosx*] = VPNProxyExtension/VPNProxyExtension.entitlements +CODE_SIGN_ENTITLEMENTS[config=Review][sdk=macosx*] = VPNProxyExtension/VPNProxyExtension.entitlements +CODE_SIGN_STYLE[config=Debug][sdk=*] = Automatic + +CODE_SIGN_IDENTITY[sdk=macosx*] = 3rd Party Mac Developer Application +CODE_SIGN_IDENTITY[config=Debug][sdk=macosx*] = Apple Development +CODE_SIGN_IDENTITY[config=CI][sdk=macosx*] = + +GENERATE_INFOPLIST_FILE = YES +INFOPLIST_FILE = VPNProxyExtension/Info.plist +INFOPLIST_KEY_NSHumanReadableCopyright = Copyright © 2023 DuckDuckGo. All rights reserved. + +FEATURE_FLAGS[arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION +FEATURE_FLAGS[config=CI][arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION +FEATURE_FLAGS[config=Debug][arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION +FEATURE_FLAGS[config=Review][arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION + +PRODUCT_BUNDLE_IDENTIFIER[sdk=*] = +PRODUCT_BUNDLE_IDENTIFIER[config=CI][sdk=*] = $(PROXY_EXTENSION_BUNDLE_ID) +PRODUCT_BUNDLE_IDENTIFIER[config=Debug][sdk=*] = $(PROXY_EXTENSION_BUNDLE_ID) +PRODUCT_BUNDLE_IDENTIFIER[config=Release][sdk=*] = $(PROXY_EXTENSION_BUNDLE_ID) +PRODUCT_BUNDLE_IDENTIFIER[config=Review][sdk=*] = $(PROXY_EXTENSION_BUNDLE_ID) + +PROVISIONING_PROFILE_SPECIFIER[config=CI][sdk=macosx*] = +PROVISIONING_PROFILE_SPECIFIER[config=Release][sdk=macosx*] = match AppStore $(AGENT_BUNDLE_ID).proxy macos +PROVISIONING_PROFILE_SPECIFIER[config=Review][sdk=macosx*] = match AppStore $(AGENT_BUNDLE_ID).proxy macos + +SDKROOT = macosx +SKIP_INSTALL = YES +SWIFT_EMIT_LOC_STRINGS = YES + +LD_RUNPATH_SEARCH_PATHS = @executable_path/../Frameworks @executable_path/../../../../Frameworks diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 0f1e8a2d3e..0a586a3376 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1099,12 +1099,11 @@ 4B25375B2A11BE7300610219 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B4D603E2A0B290200BCD287 /* NetworkExtension.framework */; }; 4B2537722A11BF8B00610219 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B25376F2A11BF8B00610219 /* main.swift */; }; 4B2537772A11BFE100610219 /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 4B2537762A11BFE100610219 /* PixelKit */; }; - 4B2537782A11C00F00610219 /* NetworkProtectionExtensionMachService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60782A0B29FA00BCD287 /* NetworkProtectionExtensionMachService.swift */; }; 4B25377A2A11C01700610219 /* UserText+NetworkProtectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D607C2A0B29FA00BCD287 /* UserText+NetworkProtectionExtensions.swift */; }; 4B29759728281F0900187C4E /* FirefoxEncryptionKeyReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B29759628281F0900187C4E /* FirefoxEncryptionKeyReader.swift */; }; 4B2975992828285900187C4E /* FirefoxKeyReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B2975982828285900187C4E /* FirefoxKeyReaderTests.swift */; }; 4B2AAAF529E70DEA0026AFC0 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 4B2AAAF429E70DEA0026AFC0 /* Lottie */; }; - 4B2D06292A11C0C900DE1F49 /* NetworkProtectionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */; }; + 4B2D06292A11C0C900DE1F49 /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; 4B2D062A2A11C0C900DE1F49 /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; 4B2D062C2A11C0E100DE1F49 /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = 4B2D062B2A11C0E100DE1F49 /* Networking */; }; 4B2D062D2A11C12300DE1F49 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85799C1725DEBB3F0007EC87 /* Logging.swift */; }; @@ -1168,17 +1167,16 @@ 4B44FEF52B1FEF5A000619D8 /* FocusableTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B44FEF22B1FEF5A000619D8 /* FocusableTextEditor.swift */; }; 4B4BEC3D2A11B56B001D9AC5 /* DuckDuckGoNotificationsAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4BEC382A11B509001D9AC5 /* DuckDuckGoNotificationsAppDelegate.swift */; }; 4B4BEC3E2A11B56E001D9AC5 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4BEC322A11B509001D9AC5 /* Logging.swift */; }; - 4B4BEC402A11B5B5001D9AC5 /* NetworkProtectionExtensionMachService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60782A0B29FA00BCD287 /* NetworkProtectionExtensionMachService.swift */; }; 4B4BEC412A11B5BD001D9AC5 /* NetworkProtectionUNNotificationsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60762A0B29FA00BCD287 /* NetworkProtectionUNNotificationsPresenter.swift */; }; 4B4BEC422A11B5C7001D9AC5 /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; - 4B4BEC432A11B5C7001D9AC5 /* NetworkProtectionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */; }; + 4B4BEC432A11B5C7001D9AC5 /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; 4B4BEC452A11B5EE001D9AC5 /* UserText+NetworkProtectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D607C2A0B29FA00BCD287 /* UserText+NetworkProtectionExtensions.swift */; }; 4B4BEC482A11B61F001D9AC5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4B4BEC342A11B509001D9AC5 /* Assets.xcassets */; }; 4B4D603F2A0B290200BCD287 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B4D603E2A0B290200BCD287 /* NetworkExtension.framework */; }; 4B4D60892A0B2A1C00BCD287 /* NetworkProtectionUNNotificationsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60762A0B29FA00BCD287 /* NetworkProtectionUNNotificationsPresenter.swift */; }; 4B4D60982A0B2A5C00BCD287 /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 4B4D60972A0B2A5C00BCD287 /* PixelKit */; }; 4B4D609F2A0B2C7300BCD287 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85799C1725DEBB3F0007EC87 /* Logging.swift */; }; - 4B4D60A02A0B2D5B00BCD287 /* NetworkProtectionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */; }; + 4B4D60A02A0B2D5B00BCD287 /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; 4B4D60A12A0B2D6100BCD287 /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; 4B4D60A52A0B2EC000BCD287 /* UserText+NetworkProtectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D607C2A0B29FA00BCD287 /* UserText+NetworkProtectionExtensions.swift */; }; 4B4D60AC2A0C804B00BCD287 /* OptionalExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B637273C26CCF0C200C8CB02 /* OptionalExtension.swift */; }; @@ -1202,7 +1200,7 @@ 4B4D60D42A0C84F700BCD287 /* UserText+NetworkProtection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60D22A0C84F700BCD287 /* UserText+NetworkProtection.swift */; }; 4B4D60DD2A0C875E00BCD287 /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; 4B4D60DF2A0C875F00BCD287 /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; - 4B4D60E02A0C875F00BCD287 /* NetworkProtectionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */; }; + 4B4D60E02A0C875F00BCD287 /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; 4B4D60E22A0C883A00BCD287 /* AppMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60E12A0C883A00BCD287 /* AppMain.swift */; }; 4B4D60E32A0C883A00BCD287 /* AppMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60E12A0C883A00BCD287 /* AppMain.swift */; }; 4B4F72EC266B2ED300814C60 /* CollectionExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4F72EB266B2ED300814C60 /* CollectionExtension.swift */; }; @@ -1528,7 +1526,7 @@ 4B957A422AC7AE700062CA31 /* SafariFaviconsReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0AACAD28BC6FD0001038AC /* SafariFaviconsReader.swift */; }; 4B957A432AC7AE700062CA31 /* NSScreenExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B3E0DC2657E9CF0040E0A2 /* NSScreenExtension.swift */; }; 4B957A442AC7AE700062CA31 /* NSBezierPathExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B65E6B9F26D9F10600095F96 /* NSBezierPathExtension.swift */; }; - 4B957A452AC7AE700062CA31 /* NetworkProtectionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */; }; + 4B957A452AC7AE700062CA31 /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; 4B957A462AC7AE700062CA31 /* WebsiteDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA6820E325502F19005ED0D5 /* WebsiteDataStore.swift */; }; 4B957A472AC7AE700062CA31 /* NetworkProtectionFeatureVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD8679A2A9E9E000063B9F7 /* NetworkProtectionFeatureVisibility.swift */; }; 4B957A482AC7AE700062CA31 /* PermissionContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64C852926942AC90048FEBE /* PermissionContextMenu.swift */; }; @@ -2049,7 +2047,7 @@ 4BA7C4DA2B3F639800AFE511 /* NetworkProtectionTunnelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8F52402A18326600BE7131 /* NetworkProtectionTunnelController.swift */; }; 4BA7C4DB2B3F63AE00AFE511 /* NetworkExtensionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD1688D2AD4A4C400D24876 /* NetworkExtensionController.swift */; }; 4BA7C4DD2B3F64E500AFE511 /* LoginItems in Frameworks */ = {isa = PBXBuildFile; productRef = 4BA7C4DC2B3F64E500AFE511 /* LoginItems */; }; - 4BA7C4E12B3F6F8600AFE511 /* NetworkProtectionAppExtension.appex in CopyFiles */ = {isa = PBXBuildFile; fileRef = 4B4D603D2A0B290200BCD287 /* NetworkProtectionAppExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 4BA7C4E12B3F6F8600AFE511 /* NetworkProtectionAppExtension.appex in Embed Network Extensions */ = {isa = PBXBuildFile; fileRef = 4B4D603D2A0B290200BCD287 /* NetworkProtectionAppExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 4BB6CE5F26B77ED000EC5860 /* Cryptography.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB6CE5E26B77ED000EC5860 /* Cryptography.swift */; }; 4BB88B4525B7B55C006F6B06 /* DebugUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB88B4425B7B55C006F6B06 /* DebugUserScript.swift */; }; 4BB88B4A25B7B690006F6B06 /* SequenceExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB88B4925B7B690006F6B06 /* SequenceExtensions.swift */; }; @@ -2133,7 +2131,7 @@ 4BF97AD62B43C45800EB4240 /* NetworkProtectionNavBarPopoverManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B3618C12ADE75C8000D6154 /* NetworkProtectionNavBarPopoverManager.swift */; }; 4BF97AD72B43C53D00EB4240 /* NetworkProtectionIPCTunnelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC4D2AD11F6F0042E5CE /* NetworkProtectionIPCTunnelController.swift */; }; 4BF97AD82B43C5B300EB4240 /* NetworkProtectionAppEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B2DDCF72A93A8BB0039D884 /* NetworkProtectionAppEvents.swift */; }; - 4BF97AD92B43C5C000EB4240 /* NetworkProtectionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */; }; + 4BF97AD92B43C5C000EB4240 /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; 4BF97ADA2B43C5DC00EB4240 /* VPNFeedbackCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0526632B1D55D80054955A /* VPNFeedbackCategory.swift */; }; 4BF97ADB2B43C5E000EB4240 /* VPNMetadataCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B05265D2B1AE5C70054955A /* VPNMetadataCollector.swift */; }; 4BF97ADC2B43C5E200EB4240 /* VPNFeedbackSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0526602B1D55320054955A /* VPNFeedbackSender.swift */; }; @@ -2174,6 +2172,14 @@ 56D145F229E6F06D00E3488A /* MockBookmarkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D145F029E6F06D00E3488A /* MockBookmarkManager.swift */; }; 56D6A3D629DB2BAB0055215A /* ContinueSetUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D6A3D529DB2BAB0055215A /* ContinueSetUpView.swift */; }; 56D6A3D729DB2BAB0055215A /* ContinueSetUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D6A3D529DB2BAB0055215A /* ContinueSetUpView.swift */; }; + 7B0099792B65013800FE7C31 /* BrowserWindowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0099782B65013800FE7C31 /* BrowserWindowManager.swift */; }; + 7B00997D2B6508B700FE7C31 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7B00997C2B6508B700FE7C31 /* NetworkProtectionProxy */; }; + 7B00997F2B6508C200FE7C31 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7B00997E2B6508C200FE7C31 /* NetworkProtectionProxy */; }; + 7B0099822B65C6B300FE7C31 /* MacTransparentProxyProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0099802B65C6B300FE7C31 /* MacTransparentProxyProvider.swift */; }; + 7B0694982B6E980F00FA4DBA /* VPNProxyLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0694972B6E980F00FA4DBA /* VPNProxyLauncher.swift */; }; + 7B1459542B7D437200047F2C /* BrowserWindowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0099782B65013800FE7C31 /* BrowserWindowManager.swift */; }; + 7B1459552B7D438F00047F2C /* VPNProxyLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0694972B6E980F00FA4DBA /* VPNProxyLauncher.swift */; }; + 7B1459572B7D43E500047F2C /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7B1459562B7D43E500047F2C /* NetworkProtectionProxy */; }; 7B1E819E27C8874900FF0E60 /* ContentOverlayPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1E819B27C8874900FF0E60 /* ContentOverlayPopover.swift */; }; 7B1E819F27C8874900FF0E60 /* ContentOverlay.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7B1E819C27C8874900FF0E60 /* ContentOverlay.storyboard */; }; 7B1E81A027C8874900FF0E60 /* ContentOverlayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1E819D27C8874900FF0E60 /* ContentOverlayViewController.swift */; }; @@ -2190,15 +2196,25 @@ 7B4CE8E726F02135009134B1 /* TabBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4CE8E626F02134009134B1 /* TabBarTests.swift */; }; 7B5DD69A2AE51FFA001DE99C /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7B5DD6992AE51FFA001DE99C /* PixelKit */; }; 7B5F9A752AE2BE4E002AEBC0 /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7B5F9A742AE2BE4E002AEBC0 /* PixelKit */; }; + 7B7DFB202B7E736B009EA1A3 /* MacPacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */; }; + 7B7DFB222B7E7473009EA1A3 /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = 7B7DFB212B7E7473009EA1A3 /* Networking */; }; 7B8C083C2AE1268E00F4C67F /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7B8C083B2AE1268E00F4C67F /* PixelKit */; }; 7B8DB31A2B504D7500EC16DA /* VPNAppEventsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8DB3192B504D7500EC16DA /* VPNAppEventsHandler.swift */; }; 7B934C412A866DD400FC8F9C /* UserDefaults+NetworkProtectionShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B934C402A866DD400FC8F9C /* UserDefaults+NetworkProtectionShared.swift */; }; + 7B94E1652B7ED95100E32B96 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7B94E1642B7ED95100E32B96 /* NetworkProtectionProxy */; }; + 7B97CD592B7E0B57004FEF43 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7B97CD582B7E0B57004FEF43 /* NetworkProtectionProxy */; }; + 7B97CD5B2B7E0B85004FEF43 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 7B97CD5A2B7E0B85004FEF43 /* Common */; }; + 7B97CD5C2B7E0BBB004FEF43 /* UserDefaultsWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85C6A29525CC1FFD00EEB5F1 /* UserDefaultsWrapper.swift */; }; + 7B97CD5D2B7E0BCE004FEF43 /* BundleExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6106B9D26A565DA0013B453 /* BundleExtension.swift */; }; + 7B97CD5E2B7E0BEA004FEF43 /* OptionalExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B637273C26CCF0C200C8CB02 /* OptionalExtension.swift */; }; + 7B97CD5F2B7E0BF7004FEF43 /* NSApplicationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5C8F622591021700748EB7 /* NSApplicationExtension.swift */; }; + 7B97CD602B7E0C2E004FEF43 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85799C1725DEBB3F0007EC87 /* Logging.swift */; }; + 7B97CD622B7E0C4B004FEF43 /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7B97CD612B7E0C4B004FEF43 /* PixelKit */; }; + 7BA076BB2B65D61400D7FB72 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7BA076BA2B65D61400D7FB72 /* NetworkProtectionProxy */; }; 7BA4727D26F01BC400EAA165 /* CoreDataTestUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292C42667104B00AD2C21 /* CoreDataTestUtilities.swift */; }; 7BA59C9B2AE18B49009A97B1 /* SystemExtensionManager in Frameworks */ = {isa = PBXBuildFile; productRef = 7BA59C9A2AE18B49009A97B1 /* SystemExtensionManager */; }; 7BA7CC392AD11E2D0042E5CE /* DuckDuckGoVPNAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC0E2AD11DC80042E5CE /* DuckDuckGoVPNAppDelegate.swift */; }; 7BA7CC3A2AD11E2D0042E5CE /* DuckDuckGoVPNAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC0E2AD11DC80042E5CE /* DuckDuckGoVPNAppDelegate.swift */; }; - 7BA7CC3B2AD11E330042E5CE /* Bundle+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC0F2AD11DC80042E5CE /* Bundle+Configuration.swift */; }; - 7BA7CC3C2AD11E330042E5CE /* Bundle+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC0F2AD11DC80042E5CE /* Bundle+Configuration.swift */; }; 7BA7CC3D2AD11E380042E5CE /* TunnelControllerIPCService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC112AD11DC80042E5CE /* TunnelControllerIPCService.swift */; }; 7BA7CC3E2AD11E380042E5CE /* TunnelControllerIPCService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC112AD11DC80042E5CE /* TunnelControllerIPCService.swift */; }; 7BA7CC3F2AD11E3D0042E5CE /* AppLauncher+DefaultInitializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC132AD11DC80042E5CE /* AppLauncher+DefaultInitializer.swift */; }; @@ -2214,8 +2230,8 @@ 7BA7CC4C2AD11EC70042E5CE /* NetworkProtectionControllerErrorStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D606A2A0B29FA00BCD287 /* NetworkProtectionControllerErrorStore.swift */; }; 7BA7CC4E2AD11F6F0042E5CE /* NetworkProtectionIPCTunnelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC4D2AD11F6F0042E5CE /* NetworkProtectionIPCTunnelController.swift */; }; 7BA7CC502AD11F6F0042E5CE /* NetworkProtectionIPCTunnelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC4D2AD11F6F0042E5CE /* NetworkProtectionIPCTunnelController.swift */; }; - 7BA7CC532AD11FCE0042E5CE /* NetworkProtectionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */; }; - 7BA7CC542AD11FCE0042E5CE /* NetworkProtectionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */; }; + 7BA7CC532AD11FCE0042E5CE /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; + 7BA7CC542AD11FCE0042E5CE /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; 7BA7CC552AD11FFB0042E5CE /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; 7BA7CC562AD11FFB0042E5CE /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; 7BA7CC582AD1203A0042E5CE /* UserText+NetworkProtection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60D22A0C84F700BCD287 /* UserText+NetworkProtection.swift */; }; @@ -2233,9 +2249,13 @@ 7BBD44282AD730A400D0A064 /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7BBD44272AD730A400D0A064 /* PixelKit */; }; 7BBD45B12A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBD45B02A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift */; }; 7BBD45B22A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBD45B02A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift */; }; + 7BBE2B7B2B61663C00697445 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7BBE2B7A2B61663C00697445 /* NetworkProtectionProxy */; }; 7BD01C192AD8319C0088B32E /* IPCServiceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD01C182AD8319C0088B32E /* IPCServiceManager.swift */; }; 7BD1688E2AD4A4C400D24876 /* NetworkExtensionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD1688D2AD4A4C400D24876 /* NetworkExtensionController.swift */; }; 7BD3AF5D2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD3AF5C2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift */; }; + 7BDA36E62B7E037100AD5388 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B4D603E2A0B290200BCD287 /* NetworkExtension.framework */; }; + 7BDA36F52B7E055800AD5388 /* MacTransparentProxyProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0099802B65C6B300FE7C31 /* MacTransparentProxyProvider.swift */; }; + 7BDA36F92B7E084A00AD5388 /* VPNProxyExtension.appex in Embed Network Extensions */ = {isa = PBXBuildFile; fileRef = 7BDA36E52B7E037100AD5388 /* VPNProxyExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 7BE146072A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BE146062A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift */; }; 7BE146082A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BE146062A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift */; }; 7BEC182F2AD5D8DC00D30536 /* SystemExtensionManager in Frameworks */ = {isa = PBXBuildFile; productRef = 7BEC182E2AD5D8DC00D30536 /* SystemExtensionManager */; }; @@ -3104,7 +3124,6 @@ EEC8EB402982CD550065AA39 /* JSAlertViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF53E172950CED5002D78F4 /* JSAlertViewModelTests.swift */; }; EECE10E529DD77E60044D027 /* FeatureFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = EECE10E429DD77E60044D027 /* FeatureFlag.swift */; }; EECE10E629DD77E60044D027 /* FeatureFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = EECE10E429DD77E60044D027 /* FeatureFlag.swift */; }; - EEF12E6E2A2111880023E6BF /* MacPacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */; }; EEF12E6F2A2111880023E6BF /* MacPacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */; }; EEF53E182950CED5002D78F4 /* JSAlertViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF53E172950CED5002D78F4 /* JSAlertViewModelTests.swift */; }; F41D174125CB131900472416 /* NSColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41D174025CB131900472416 /* NSColorExtension.swift */; }; @@ -3185,6 +3204,13 @@ remoteGlobalIDString = AA585D7D248FD31100E9A3E2; remoteInfo = "DuckDuckGo Privacy Browser"; }; + 7BDA36F72B7E082100AD5388 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = AA585D76248FD31100E9A3E2 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 7BDA36E42B7E037100AD5388; + remoteInfo = VPNProxyExtension; + }; 7BEC18302AD5DA3300D30536 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = AA585D76248FD31100E9A3E2 /* Project object */; @@ -3242,14 +3268,16 @@ name = "Embed Login Items"; runOnlyForDeploymentPostprocessing = 0; }; - 4BA7C4E02B3F6F7500AFE511 /* CopyFiles */ = { + 4BA7C4E02B3F6F7500AFE511 /* Embed Network Extensions */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 13; files = ( - 4BA7C4E12B3F6F8600AFE511 /* NetworkProtectionAppExtension.appex in CopyFiles */, + 7BDA36F92B7E084A00AD5388 /* VPNProxyExtension.appex in Embed Network Extensions */, + 4BA7C4E12B3F6F8600AFE511 /* NetworkProtectionAppExtension.appex in Embed Network Extensions */, ); + name = "Embed Network Extensions"; runOnlyForDeploymentPostprocessing = 0; }; B6EC37E629B5DA2A001ACE79 /* CopyFiles */ = { @@ -3546,7 +3574,7 @@ 4B4D604F2A0B293C00BCD287 /* NetworkProtectionSystemExtension.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = NetworkProtectionSystemExtension.xcconfig; sourceTree = ""; }; 4B4D60502A0B293C00BCD287 /* NetworkProtectionAppExtension.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = NetworkProtectionAppExtension.xcconfig; sourceTree = ""; }; 4B4D60512A0B293C00BCD287 /* ExtensionBase.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ExtensionBase.xcconfig; sourceTree = ""; }; - 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionBundle.swift; sourceTree = ""; }; + 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+VPN.swift"; sourceTree = ""; }; 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionOptionKeyExtension.swift; sourceTree = ""; }; 4B4D60652A0B29FA00BCD287 /* NetworkProtectionNavBarButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionNavBarButtonModel.swift; sourceTree = ""; }; 4B4D60692A0B29FA00BCD287 /* NetworkProtection+ConvenienceInitializers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NetworkProtection+ConvenienceInitializers.swift"; sourceTree = ""; }; @@ -3556,10 +3584,7 @@ 4B4D60702A0B29FA00BCD287 /* NetworkProtectionInviteCodeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionInviteCodeViewModel.swift; sourceTree = ""; }; 4B4D60722A0B29FA00BCD287 /* EventMapping+NetworkProtectionError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EventMapping+NetworkProtectionError.swift"; sourceTree = ""; }; 4B4D60762A0B29FA00BCD287 /* NetworkProtectionUNNotificationsPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionUNNotificationsPresenter.swift; sourceTree = ""; }; - 4B4D60782A0B29FA00BCD287 /* NetworkProtectionExtensionMachService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionExtensionMachService.swift; sourceTree = ""; }; 4B4D607C2A0B29FA00BCD287 /* UserText+NetworkProtectionExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserText+NetworkProtectionExtensions.swift"; sourceTree = ""; }; - 4B4D609C2A0B2C2300BCD287 /* DuckDuckGo_NetP_Release.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DuckDuckGo_NetP_Release.entitlements; sourceTree = ""; }; - 4B4D609E2A0B2C2300BCD287 /* DuckDuckGo_NetP_Debug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DuckDuckGo_NetP_Debug.entitlements; sourceTree = ""; }; 4B4D60D22A0C84F700BCD287 /* UserText+NetworkProtection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UserText+NetworkProtection.swift"; sourceTree = ""; }; 4B4D60E12A0C883A00BCD287 /* AppMain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppMain.swift; sourceTree = ""; }; 4B4F72EB266B2ED300814C60 /* CollectionExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionExtension.swift; sourceTree = ""; }; @@ -3766,7 +3791,10 @@ 56D145ED29E6DAD900E3488A /* DataImportProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataImportProviderTests.swift; sourceTree = ""; }; 56D145F029E6F06D00E3488A /* MockBookmarkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBookmarkManager.swift; sourceTree = ""; }; 56D6A3D529DB2BAB0055215A /* ContinueSetUpView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContinueSetUpView.swift; sourceTree = ""; }; + 7B0099782B65013800FE7C31 /* BrowserWindowManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserWindowManager.swift; sourceTree = ""; }; + 7B0099802B65C6B300FE7C31 /* MacTransparentProxyProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacTransparentProxyProvider.swift; sourceTree = ""; }; 7B05829D2A812AC000AC3F7C /* NetworkProtectionOnboardingMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionOnboardingMenu.swift; sourceTree = ""; }; + 7B0694972B6E980F00FA4DBA /* VPNProxyLauncher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNProxyLauncher.swift; sourceTree = ""; }; 7B1E819B27C8874900FF0E60 /* ContentOverlayPopover.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentOverlayPopover.swift; sourceTree = ""; }; 7B1E819C27C8874900FF0E60 /* ContentOverlay.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = ContentOverlay.storyboard; sourceTree = ""; }; 7B1E819D27C8874900FF0E60 /* ContentOverlayViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentOverlayViewController.swift; sourceTree = ""; }; @@ -3790,7 +3818,6 @@ 7BA7CC0B2AD11D1E0042E5CE /* DuckDuckGoVPNAppStore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DuckDuckGoVPNAppStore.xcconfig; sourceTree = ""; }; 7BA7CC0C2AD11D1E0042E5CE /* DuckDuckGoVPN.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DuckDuckGoVPN.xcconfig; sourceTree = ""; }; 7BA7CC0E2AD11DC80042E5CE /* DuckDuckGoVPNAppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DuckDuckGoVPNAppDelegate.swift; sourceTree = ""; }; - 7BA7CC0F2AD11DC80042E5CE /* Bundle+Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Bundle+Configuration.swift"; sourceTree = ""; }; 7BA7CC102AD11DC80042E5CE /* Info-AppStore.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Info-AppStore.plist"; sourceTree = ""; }; 7BA7CC112AD11DC80042E5CE /* TunnelControllerIPCService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelControllerIPCService.swift; sourceTree = ""; }; 7BA7CC122AD11DC80042E5CE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -3809,6 +3836,10 @@ 7BD1688D2AD4A4C400D24876 /* NetworkExtensionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkExtensionController.swift; sourceTree = ""; }; 7BD3AF5C2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KeychainType+ClientDefault.swift"; sourceTree = ""; }; 7BD8679A2A9E9E000063B9F7 /* NetworkProtectionFeatureVisibility.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionFeatureVisibility.swift; sourceTree = ""; }; + 7BDA36E52B7E037100AD5388 /* VPNProxyExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = VPNProxyExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 7BDA36EA2B7E037200AD5388 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 7BDA36EB2B7E037200AD5388 /* VPNProxyExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = VPNProxyExtension.entitlements; sourceTree = ""; }; + 7BDA36F62B7E06A300AD5388 /* VPNProxyExtension.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = VPNProxyExtension.xcconfig; sourceTree = ""; }; 7BE146062A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionDebugMenu.swift; sourceTree = ""; }; 7BEC182D2AD5D89C00D30536 /* SystemExtensionManager */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = SystemExtensionManager; sourceTree = ""; }; 7BEC20402B0F505F00243D3E /* AddBookmarkPopoverView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddBookmarkPopoverView.swift; sourceTree = ""; }; @@ -4443,6 +4474,7 @@ 373FB4B32B4D6C4B004C88D6 /* PreferencesViews in Frameworks */, 7B5F9A752AE2BE4E002AEBC0 /* PixelKit in Frameworks */, 4BF97AD32B43C43F00EB4240 /* NetworkProtectionUI in Frameworks */, + 7B1459572B7D43E500047F2C /* NetworkProtectionProxy in Frameworks */, B6F7128229F6820A00594A45 /* QuickLookUI.framework in Frameworks */, 984FD3BF299ACF35007334DD /* Bookmarks in Frameworks */, 37A5E2F0298AA1B20047046B /* Persistence in Frameworks */, @@ -4510,6 +4542,7 @@ 37269F012B332FC8005E8E46 /* Common in Frameworks */, EE7295E92A545BC4008C0991 /* NetworkProtection in Frameworks */, 4B2537772A11BFE100610219 /* PixelKit in Frameworks */, + 7BBE2B7B2B61663C00697445 /* NetworkProtectionProxy in Frameworks */, 4B2D062C2A11C0E100DE1F49 /* Networking in Frameworks */, 4B25375B2A11BE7300610219 /* NetworkExtension.framework in Frameworks */, ); @@ -4520,6 +4553,7 @@ buildActionMask = 2147483647; files = ( 4B41EDAB2B1544B2001EEDF4 /* LoginItems in Frameworks */, + 7B00997D2B6508B700FE7C31 /* NetworkProtectionProxy in Frameworks */, 7BEEA5122AD1235B00A9E72B /* NetworkProtectionIPC in Frameworks */, 7BA7CC5F2AD1210C0042E5CE /* Networking in Frameworks */, 7BEEA5162AD1236E00A9E72B /* NetworkProtectionUI in Frameworks */, @@ -4536,6 +4570,7 @@ 7BFCB7502ADE7E2300DA3EA7 /* PixelKit in Frameworks */, 7BA7CC612AD1211C0042E5CE /* Networking in Frameworks */, 7BEEA5142AD1236300A9E72B /* NetworkProtectionIPC in Frameworks */, + 7B00997F2B6508C200FE7C31 /* NetworkProtectionProxy in Frameworks */, EE7295EF2A545C12008C0991 /* NetworkProtection in Frameworks */, 4B2D067F2A1334D700DE1F49 /* NetworkProtectionUI in Frameworks */, 4BA7C4DD2B3F64E500AFE511 /* LoginItems in Frameworks */, @@ -4571,6 +4606,7 @@ 3143C8792B0D1F3D00382627 /* DataBrokerProtection in Frameworks */, 372217842B33380E00B8E9C2 /* TestUtils in Frameworks */, 4B957BD62AC7AE700062CA31 /* LoginItems in Frameworks */, + 7B94E1652B7ED95100E32B96 /* NetworkProtectionProxy in Frameworks */, 4B957BD72AC7AE700062CA31 /* NetworkProtection in Frameworks */, 4B957BD82AC7AE700062CA31 /* BrowserServicesKit in Frameworks */, 4B957BDA2AC7AE700062CA31 /* Bookmarks in Frameworks */, @@ -4613,6 +4649,18 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7BDA36E22B7E037100AD5388 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7BDA36E62B7E037100AD5388 /* NetworkExtension.framework in Frameworks */, + 7B97CD592B7E0B57004FEF43 /* NetworkProtectionProxy in Frameworks */, + 7B97CD622B7E0C4B004FEF43 /* PixelKit in Frameworks */, + 7B7DFB222B7E7473009EA1A3 /* Networking in Frameworks */, + 7B97CD5B2B7E0B85004FEF43 /* Common in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 9D9AE8C62AAA39A70026E7DC /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -4661,6 +4709,7 @@ 37DF000529F9C056002B7D3E /* SyncDataProviders in Frameworks */, 37BA812D29B3CD690053F1A3 /* SyncUI in Frameworks */, 372217802B3337FE00B8E9C2 /* TestUtils in Frameworks */, + 7BA076BB2B65D61400D7FB72 /* NetworkProtectionProxy in Frameworks */, 4B4D60B12A0C83B900BCD287 /* NetworkProtectionUI in Frameworks */, 98A50964294B691800D10880 /* Persistence in Frameworks */, ); @@ -5240,8 +5289,9 @@ 4B18E32C2A1ECF1F005D0AAA /* NetworkProtection */ = { isa = PBXGroup; children = ( - 4B4D604F2A0B293C00BCD287 /* NetworkProtectionSystemExtension.xcconfig */, 4B4D60502A0B293C00BCD287 /* NetworkProtectionAppExtension.xcconfig */, + 4B4D604F2A0B293C00BCD287 /* NetworkProtectionSystemExtension.xcconfig */, + 7BDA36F62B7E06A300AD5388 /* VPNProxyExtension.xcconfig */, ); path = NetworkProtection; sourceTree = ""; @@ -5379,7 +5429,7 @@ 4B4D605D2A0B29FA00BCD287 /* AppAndExtensionAndNotificationTargets */ = { isa = PBXGroup; children = ( - 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */, + 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */, 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */, B602E8152A1E2570006D261F /* URL+NetworkProtection.swift */, ); @@ -5463,7 +5513,6 @@ isa = PBXGroup; children = ( B602E81F2A1E2603006D261F /* Bundle+NetworkProtectionExtensions.swift */, - 4B4D60782A0B29FA00BCD287 /* NetworkProtectionExtensionMachService.swift */, ); path = SystemExtensionAndNotificationTargets; sourceTree = ""; @@ -5481,6 +5530,7 @@ children = ( 4B41ED9F2B15437A001EEDF4 /* NetworkProtectionNotificationsPresenterFactory.swift */, EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */, + 7B0099802B65C6B300FE7C31 /* MacTransparentProxyProvider.swift */, ); path = NetworkExtensionTargets; sourceTree = ""; @@ -6126,11 +6176,11 @@ isa = PBXGroup; children = ( 7BA7CC132AD11DC80042E5CE /* AppLauncher+DefaultInitializer.swift */, - 7BA7CC0F2AD11DC80042E5CE /* Bundle+Configuration.swift */, 7BA7CC0E2AD11DC80042E5CE /* DuckDuckGoVPNAppDelegate.swift */, 7BD1688D2AD4A4C400D24876 /* NetworkExtensionController.swift */, 7BA7CC152AD11DC80042E5CE /* NetworkProtectionBouncer.swift */, 7B8DB3192B504D7500EC16DA /* VPNAppEventsHandler.swift */, + 7B0694972B6E980F00FA4DBA /* VPNProxyLauncher.swift */, 7BA7CC112AD11DC80042E5CE /* TunnelControllerIPCService.swift */, 7BA7CC172AD11DC80042E5CE /* UserText.swift */, 7BA7CC122AD11DC80042E5CE /* Assets.xcassets */, @@ -6160,6 +6210,15 @@ path = LetsMove1.25; sourceTree = ""; }; + 7BDA36E72B7E037200AD5388 /* VPNProxyExtension */ = { + isa = PBXGroup; + children = ( + 7BDA36EA2B7E037200AD5388 /* Info.plist */, + 7BDA36EB2B7E037200AD5388 /* VPNProxyExtension.entitlements */, + ); + path = VPNProxyExtension; + sourceTree = ""; + }; 853014D425E6709500FB8205 /* Support */ = { isa = PBXGroup; children = ( @@ -6523,6 +6582,7 @@ 9D9AE9152AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift */, 9D9AE9282AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift */, 7BD01C182AD8319C0088B32E /* IPCServiceManager.swift */, + 7B0099782B65013800FE7C31 /* BrowserWindowManager.swift */, 9D9AE92B2AAB84FF0026E7DC /* DBPMocks.swift */, 9D9AE9172AAA3B450026E7DC /* UserText.swift */, 9D9AE9162AAA3B450026E7DC /* Assets.xcassets */, @@ -6646,6 +6706,7 @@ B6EC37E929B5DA2A001ACE79 /* tests-server */, 7B96D0D02ADFDA7F007E02C8 /* DuckDuckGoDBPTests */, 4B5F14F72A148B230060320F /* NetworkProtectionAppExtension */, + 7BDA36E72B7E037200AD5388 /* VPNProxyExtension */, 4B25375C2A11BE7500610219 /* NetworkProtectionSystemExtension */, 9D9AE9132AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgent */, 7BA7CC0D2AD11DC80042E5CE /* DuckDuckGoVPN */, @@ -6676,6 +6737,7 @@ 4B957C412AC7AE700062CA31 /* DuckDuckGo Privacy Pro.app */, 565E46DD2B2725DC0013AC2A /* SyncE2EUITests.xctest */, 376113D42B29CD5B00E794BB /* SyncE2EUITests App Store.xctest */, + 7BDA36E52B7E037100AD5388 /* VPNProxyExtension.appex */, ); name = Products; sourceTree = ""; @@ -6746,8 +6808,6 @@ 4B5F15032A1570F10060320F /* DuckDuckGoDebug.entitlements */, 37D9BBA329376EE8000B99F9 /* DuckDuckGoAppStore.entitlements */, 377E54382937B7C400780A0A /* DuckDuckGoAppStoreCI.entitlements */, - 4B4D609E2A0B2C2300BCD287 /* DuckDuckGo_NetP_Debug.entitlements */, - 4B4D609C2A0B2C2300BCD287 /* DuckDuckGo_NetP_Release.entitlements */, 4B2D06642A132F3A00DE1F49 /* NetworkProtectionAppExtension.entitlements */, 4B5F14C42A145D6A0060320F /* NetworkProtectionVPNController.entitlements */, 56CEE9092B7A66C500CF10AA /* Info.plist */, @@ -8280,6 +8340,7 @@ 4BF97AD42B43C43F00EB4240 /* NetworkProtection */, 373FB4B22B4D6C4B004C88D6 /* PreferencesViews */, 312978892B64131200B67619 /* DataBrokerProtection */, + 7B1459562B7D43E500047F2C /* NetworkProtectionProxy */, ); productName = DuckDuckGo; productReference = 3706FD05293F65D500E42796 /* DuckDuckGo App Store.app */; @@ -8395,6 +8456,7 @@ 4B2D062B2A11C0E100DE1F49 /* Networking */, EE7295E82A545BC4008C0991 /* NetworkProtection */, 37269F002B332FC8005E8E46 /* Common */, + 7BBE2B7A2B61663C00697445 /* NetworkProtectionProxy */, ); productName = NetworkProtectionSystemExtension; productReference = 4B25375A2A11BE7300610219 /* com.duckduckgo.macos.vpn.network-extension.debug.systemextension */; @@ -8425,6 +8487,7 @@ 7BEC182E2AD5D8DC00D30536 /* SystemExtensionManager */, 7BFCB74D2ADE7E1A00DA3EA7 /* PixelKit */, 4B41EDAA2B1544B2001EEDF4 /* LoginItems */, + 7B00997C2B6508B700FE7C31 /* NetworkProtectionProxy */, ); productName = DuckDuckGoAgent; productReference = 4B2D06392A11CFBB00DE1F49 /* DuckDuckGo VPN.app */; @@ -8438,11 +8501,12 @@ 4B2D06662A13318400DE1F49 /* Frameworks */, 4B2D06672A13318400DE1F49 /* Resources */, 4B2D067D2A13341200DE1F49 /* ShellScript */, - 4BA7C4E02B3F6F7500AFE511 /* CopyFiles */, + 4BA7C4E02B3F6F7500AFE511 /* Embed Network Extensions */, ); buildRules = ( ); dependencies = ( + 7BDA36F82B7E082100AD5388 /* PBXTargetDependency */, 4BA7C4DF2B3F6F4900AFE511 /* PBXTargetDependency */, B6080BA52B20AF8800B418EF /* PBXTargetDependency */, ); @@ -8453,6 +8517,7 @@ 7BA7CC602AD1211C0042E5CE /* Networking */, 7BEEA5132AD1236300A9E72B /* NetworkProtectionIPC */, 7BFCB74F2ADE7E2300DA3EA7 /* PixelKit */, + 7B00997E2B6508C200FE7C31 /* NetworkProtectionProxy */, 4BA7C4DC2B3F64E500AFE511 /* LoginItems */, ); productName = DuckDuckGoAgentAppStore; @@ -8552,6 +8617,7 @@ 372217832B33380E00B8E9C2 /* TestUtils */, 373FB4B42B4D6C57004C88D6 /* PreferencesViews */, 1E21F8E22B73E48600FB272E /* Subscription */, + 7B94E1642B7ED95100E32B96 /* NetworkProtectionProxy */, ); productName = DuckDuckGo; productReference = 4B957C412AC7AE700062CA31 /* DuckDuckGo Privacy Pro.app */; @@ -8596,6 +8662,29 @@ productReference = 7B4CE8DA26F02108009134B1 /* UI Tests.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; }; + 7BDA36E42B7E037100AD5388 /* VPNProxyExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7BDA36F42B7E037200AD5388 /* Build configuration list for PBXNativeTarget "VPNProxyExtension" */; + buildPhases = ( + 7BDA36E12B7E037100AD5388 /* Sources */, + 7BDA36E22B7E037100AD5388 /* Frameworks */, + 7BDA36E32B7E037100AD5388 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = VPNProxyExtension; + packageProductDependencies = ( + 7B97CD582B7E0B57004FEF43 /* NetworkProtectionProxy */, + 7B97CD5A2B7E0B85004FEF43 /* Common */, + 7B97CD612B7E0C4B004FEF43 /* PixelKit */, + 7B7DFB212B7E7473009EA1A3 /* Networking */, + ); + productName = VPNProxyExtension; + productReference = 7BDA36E52B7E037100AD5388 /* VPNProxyExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; 9D9AE8B22AAA39A70026E7DC /* DuckDuckGoDBPBackgroundAgent */ = { isa = PBXNativeTarget; buildConfigurationList = 9D9AE8CC2AAA39A70026E7DC /* Build configuration list for PBXNativeTarget "DuckDuckGoDBPBackgroundAgent" */; @@ -8690,6 +8779,7 @@ 37269EFA2B332F9E005E8E46 /* Common */, 3722177F2B3337FE00B8E9C2 /* TestUtils */, 373FB4B02B4D6C42004C88D6 /* PreferencesViews */, + 7BA076BA2B65D61400D7FB72 /* NetworkProtectionProxy */, ); productName = DuckDuckGo; productReference = AA585D7E248FD31100E9A3E2 /* DuckDuckGo.app */; @@ -8746,7 +8836,7 @@ AA585D76248FD31100E9A3E2 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1500; + LastSwiftUpdateCheck = 1520; LastUpgradeCheck = 1400; ORGANIZATIONNAME = DuckDuckGo; TargetAttributes = { @@ -8788,6 +8878,9 @@ CreatedOnToolsVersion = 12.5.1; TestTargetID = AA585D7D248FD31100E9A3E2; }; + 7BDA36E42B7E037100AD5388 = { + CreatedOnToolsVersion = 15.2; + }; AA585D7D248FD31100E9A3E2 = { CreatedOnToolsVersion = 11.5; }; @@ -8833,6 +8926,7 @@ 3706FE9B293F662100E42796 /* Integration Tests App Store */, B6EC37E729B5DA2A001ACE79 /* tests-server */, 4B4D603C2A0B290200BCD287 /* NetworkProtectionAppExtension */, + 7BDA36E42B7E037100AD5388 /* VPNProxyExtension */, 4B2537592A11BE7300610219 /* NetworkProtectionSystemExtension */, 4B4BEC1F2A11B4E2001D9AC5 /* DuckDuckGoNotifications */, 4B2D06382A11CFBA00DE1F49 /* DuckDuckGoVPN */, @@ -9050,6 +9144,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7BDA36E32B7E037100AD5388 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 9D9AE8C92AAA39A70026E7DC /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -9861,7 +9962,7 @@ B66260E829ACD0C900E9E3EE /* DuckPlayerTabExtension.swift in Sources */, 3706FBAA293F65D500E42796 /* HoverUserScript.swift in Sources */, 3706FBAC293F65D500E42796 /* MainMenuActions.swift in Sources */, - 4BF97AD92B43C5C000EB4240 /* NetworkProtectionBundle.swift in Sources */, + 4BF97AD92B43C5C000EB4240 /* Bundle+VPN.swift in Sources */, 3706FBAE293F65D500E42796 /* DataImport.swift in Sources */, 3706FBAF293F65D500E42796 /* FireproofDomains.xcdatamodeld in Sources */, B626A7552991413000053070 /* SerpHeadersNavigationResponder.swift in Sources */, @@ -10520,11 +10621,11 @@ 4B2D06332A11C1E300DE1F49 /* OptionalExtension.swift in Sources */, 4BF0E50B2AD2552200FFEC9E /* NetworkProtectionPixelEvent.swift in Sources */, 4B41EDA12B15437A001EEDF4 /* NetworkProtectionNotificationsPresenterFactory.swift in Sources */, - 4B2537782A11C00F00610219 /* NetworkProtectionExtensionMachService.swift in Sources */, + 7B0099822B65C6B300FE7C31 /* MacTransparentProxyProvider.swift in Sources */, B65DA5F32A77D3C700CBEE8D /* UserDefaultsWrapper.swift in Sources */, 4B2537722A11BF8B00610219 /* main.swift in Sources */, EEF12E6F2A2111880023E6BF /* MacPacketTunnelProvider.swift in Sources */, - 4B2D06292A11C0C900DE1F49 /* NetworkProtectionBundle.swift in Sources */, + 4B2D06292A11C0C900DE1F49 /* Bundle+VPN.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10543,14 +10644,14 @@ 7BA7CC4C2AD11EC70042E5CE /* NetworkProtectionControllerErrorStore.swift in Sources */, B6F92BAC2A6937B3002ABA6B /* OptionalExtension.swift in Sources */, 7B8DB31A2B504D7500EC16DA /* VPNAppEventsHandler.swift in Sources */, - 7BA7CC532AD11FCE0042E5CE /* NetworkProtectionBundle.swift in Sources */, - 7BA7CC3C2AD11E330042E5CE /* Bundle+Configuration.swift in Sources */, + 7BA7CC532AD11FCE0042E5CE /* Bundle+VPN.swift in Sources */, 7BFE95562A9DF29B0081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift in Sources */, 7BA7CC5D2AD120C30042E5CE /* EventMapping+NetworkProtectionError.swift in Sources */, 7BA7CC4A2AD11EA00042E5CE /* NetworkProtectionTunnelController.swift in Sources */, 7BD1688E2AD4A4C400D24876 /* NetworkExtensionController.swift in Sources */, 7BA7CC3E2AD11E380042E5CE /* TunnelControllerIPCService.swift in Sources */, 7BA7CC402AD11E3D0042E5CE /* AppLauncher+DefaultInitializer.swift in Sources */, + 7B0694982B6E980F00FA4DBA /* VPNProxyLauncher.swift in Sources */, EEC589DB2A4F1CE700BCD60C /* AppLauncher.swift in Sources */, B65DA5EF2A77CC3A00CBEE8D /* Bundle+NetworkProtectionExtensions.swift in Sources */, 4BF0E5072AD2551A00FFEC9E /* NetworkProtectionPixelEvent.swift in Sources */, @@ -10569,10 +10670,10 @@ B6F92BA32A691583002ABA6B /* UserDefaultsWrapper.swift in Sources */, 4BA7C4DB2B3F63AE00AFE511 /* NetworkExtensionController.swift in Sources */, 4B2D067C2A13340900DE1F49 /* Logging.swift in Sources */, + 7B1459552B7D438F00047F2C /* VPNProxyLauncher.swift in Sources */, B6F92BAD2A6937B5002ABA6B /* OptionalExtension.swift in Sources */, 4BA7C4D92B3F61FB00AFE511 /* BundleExtension.swift in Sources */, 7BA7CC5A2AD120640042E5CE /* NetworkProtection+ConvenienceInitializers.swift in Sources */, - 7BA7CC3B2AD11E330042E5CE /* Bundle+Configuration.swift in Sources */, EEC589DC2A4F1CE800BCD60C /* AppLauncher.swift in Sources */, 7BA7CC3F2AD11E3D0042E5CE /* AppLauncher+DefaultInitializer.swift in Sources */, 4B0EF7292B5780EB009D6481 /* VPNAppEventsHandler.swift in Sources */, @@ -10590,7 +10691,7 @@ 7BA7CC3D2AD11E380042E5CE /* TunnelControllerIPCService.swift in Sources */, 4BA7C4DA2B3F639800AFE511 /* NetworkProtectionTunnelController.swift in Sources */, 7BA7CC432AD11E480042E5CE /* UserText.swift in Sources */, - 7BA7CC542AD11FCE0042E5CE /* NetworkProtectionBundle.swift in Sources */, + 7BA7CC542AD11FCE0042E5CE /* Bundle+VPN.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10601,12 +10702,11 @@ 4B4BEC3D2A11B56B001D9AC5 /* DuckDuckGoNotificationsAppDelegate.swift in Sources */, 4B4BEC3E2A11B56E001D9AC5 /* Logging.swift in Sources */, 4B4BEC412A11B5BD001D9AC5 /* NetworkProtectionUNNotificationsPresenter.swift in Sources */, - 4B4BEC432A11B5C7001D9AC5 /* NetworkProtectionBundle.swift in Sources */, + 4B4BEC432A11B5C7001D9AC5 /* Bundle+VPN.swift in Sources */, 4B4BEC452A11B5EE001D9AC5 /* UserText+NetworkProtectionExtensions.swift in Sources */, 4B4BEC422A11B5C7001D9AC5 /* NetworkProtectionOptionKeyExtension.swift in Sources */, B602E8222A1E2603006D261F /* Bundle+NetworkProtectionExtensions.swift in Sources */, B602E81A2A1E2570006D261F /* URL+NetworkProtection.swift in Sources */, - 4B4BEC402A11B5B5001D9AC5 /* NetworkProtectionExtensionMachService.swift in Sources */, EEAD7A7C2A1D3E20002A24E7 /* AppLauncher.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -10617,14 +10717,14 @@ files = ( 4B41EDA02B15437A001EEDF4 /* NetworkProtectionNotificationsPresenterFactory.swift in Sources */, 4B4D609F2A0B2C7300BCD287 /* Logging.swift in Sources */, + 7B7DFB202B7E736B009EA1A3 /* MacPacketTunnelProvider.swift in Sources */, 4B4D60A12A0B2D6100BCD287 /* NetworkProtectionOptionKeyExtension.swift in Sources */, B602E8182A1E2570006D261F /* URL+NetworkProtection.swift in Sources */, B65DA5F52A77D3FA00CBEE8D /* BundleExtension.swift in Sources */, 4B4D60892A0B2A1C00BCD287 /* NetworkProtectionUNNotificationsPresenter.swift in Sources */, - 4B4D60A02A0B2D5B00BCD287 /* NetworkProtectionBundle.swift in Sources */, + 4B4D60A02A0B2D5B00BCD287 /* Bundle+VPN.swift in Sources */, 4B4D60AD2A0C807300BCD287 /* NSApplicationExtension.swift in Sources */, 4B4D60A52A0B2EC000BCD287 /* UserText+NetworkProtectionExtensions.swift in Sources */, - EEF12E6E2A2111880023E6BF /* MacPacketTunnelProvider.swift in Sources */, 4BF0E50C2AD2552300FFEC9E /* NetworkProtectionPixelEvent.swift in Sources */, 4B4D60AC2A0C804B00BCD287 /* OptionalExtension.swift in Sources */, B65DA5F22A77D3C600CBEE8D /* UserDefaultsWrapper.swift in Sources */, @@ -10909,7 +11009,7 @@ 4B957A422AC7AE700062CA31 /* SafariFaviconsReader.swift in Sources */, 4B957A432AC7AE700062CA31 /* NSScreenExtension.swift in Sources */, 4B957A442AC7AE700062CA31 /* NSBezierPathExtension.swift in Sources */, - 4B957A452AC7AE700062CA31 /* NetworkProtectionBundle.swift in Sources */, + 4B957A452AC7AE700062CA31 /* Bundle+VPN.swift in Sources */, B68D21CA2ACBC971002DA3C2 /* MockPrivacyConfiguration.swift in Sources */, 4B957A462AC7AE700062CA31 /* WebsiteDataStore.swift in Sources */, 4B957A472AC7AE700062CA31 /* NetworkProtectionFeatureVisibility.swift in Sources */, @@ -11378,11 +11478,25 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7BDA36E12B7E037100AD5388 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7B97CD5C2B7E0BBB004FEF43 /* UserDefaultsWrapper.swift in Sources */, + 7B97CD602B7E0C2E004FEF43 /* Logging.swift in Sources */, + 7B97CD5E2B7E0BEA004FEF43 /* OptionalExtension.swift in Sources */, + 7B97CD5F2B7E0BF7004FEF43 /* NSApplicationExtension.swift in Sources */, + 7BDA36F52B7E055800AD5388 /* MacTransparentProxyProvider.swift in Sources */, + 7B97CD5D2B7E0BCE004FEF43 /* BundleExtension.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 9D9AE8B62AAA39A70026E7DC /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 9D9AE92C2AAB84FF0026E7DC /* DBPMocks.swift in Sources */, + 7B0099792B65013800FE7C31 /* BrowserWindowManager.swift in Sources */, 9D9AE9292AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift in Sources */, 9D9AE91D2AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift in Sources */, 9D9AE9212AAA3B450026E7DC /* UserText.swift in Sources */, @@ -11394,6 +11508,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7B1459542B7D437200047F2C /* BrowserWindowManager.swift in Sources */, 9D9AE92D2AAB84FF0026E7DC /* DBPMocks.swift in Sources */, 9D9AE92A2AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift in Sources */, 9D9AE91E2AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift in Sources */, @@ -11677,7 +11792,7 @@ 4B0AACAE28BC6FD0001038AC /* SafariFaviconsReader.swift in Sources */, B6B3E0E12657EA7A0040E0A2 /* NSScreenExtension.swift in Sources */, B65E6BA026D9F10600095F96 /* NSBezierPathExtension.swift in Sources */, - 4B4D60E02A0C875F00BCD287 /* NetworkProtectionBundle.swift in Sources */, + 4B4D60E02A0C875F00BCD287 /* Bundle+VPN.swift in Sources */, AA6820E425502F19005ED0D5 /* WebsiteDataStore.swift in Sources */, B6F9BDDC2B45B7EE00677B33 /* WebsiteInfo.swift in Sources */, 4B67854A2AA8DE75008A5004 /* NetworkProtectionFeatureVisibility.swift in Sources */, @@ -12449,6 +12564,11 @@ target = AA585D7D248FD31100E9A3E2 /* DuckDuckGo Privacy Browser */; targetProxy = 7B4CE8DF26F02108009134B1 /* PBXContainerItemProxy */; }; + 7BDA36F82B7E082100AD5388 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 7BDA36E42B7E037100AD5388 /* VPNProxyExtension */; + targetProxy = 7BDA36F72B7E082100AD5388 /* PBXContainerItemProxy */; + }; 7BEC18312AD5DA3300D30536 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 4B2537592A11BE7300610219 /* NetworkProtectionSystemExtension */; @@ -12918,6 +13038,34 @@ }; name = Release; }; + 7BDA36F02B7E037200AD5388 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7BDA36F62B7E06A300AD5388 /* VPNProxyExtension.xcconfig */; + buildSettings = { + }; + name = Debug; + }; + 7BDA36F12B7E037200AD5388 /* CI */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7BDA36F62B7E06A300AD5388 /* VPNProxyExtension.xcconfig */; + buildSettings = { + }; + name = CI; + }; + 7BDA36F22B7E037200AD5388 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7BDA36F62B7E06A300AD5388 /* VPNProxyExtension.xcconfig */; + buildSettings = { + }; + name = Release; + }; + 7BDA36F32B7E037200AD5388 /* Review */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7BDA36F62B7E06A300AD5388 /* VPNProxyExtension.xcconfig */; + buildSettings = { + }; + name = Review; + }; 9D9AE8CD2AAA39A70026E7DC /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7B6EC5E52AE2D8AF004FE6DF /* DuckDuckGoDBPAgent.xcconfig */; @@ -13225,6 +13373,17 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 7BDA36F42B7E037200AD5388 /* Build configuration list for PBXNativeTarget "VPNProxyExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7BDA36F02B7E037200AD5388 /* Debug */, + 7BDA36F12B7E037200AD5388 /* CI */, + 7BDA36F22B7E037200AD5388 /* Release */, + 7BDA36F32B7E037200AD5388 /* Review */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 9D9AE8CC2AAA39A70026E7DC /* Build configuration list for PBXNativeTarget "DuckDuckGoDBPBackgroundAgent" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -13379,7 +13538,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 109.0.0; + version = 109.0.1; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { @@ -13768,6 +13927,18 @@ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = NetworkProtection; }; + 7B00997C2B6508B700FE7C31 /* NetworkProtectionProxy */ = { + isa = XCSwiftPackageProductDependency; + productName = NetworkProtectionProxy; + }; + 7B00997E2B6508C200FE7C31 /* NetworkProtectionProxy */ = { + isa = XCSwiftPackageProductDependency; + productName = NetworkProtectionProxy; + }; + 7B1459562B7D43E500047F2C /* NetworkProtectionProxy */ = { + isa = XCSwiftPackageProductDependency; + productName = NetworkProtectionProxy; + }; 7B31FD8B2AD125620086AA24 /* NetworkProtectionIPC */ = { isa = XCSwiftPackageProductDependency; productName = NetworkProtectionIPC; @@ -13784,10 +13955,36 @@ isa = XCSwiftPackageProductDependency; productName = PixelKit; }; + 7B7DFB212B7E7473009EA1A3 /* Networking */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Networking; + }; 7B8C083B2AE1268E00F4C67F /* PixelKit */ = { isa = XCSwiftPackageProductDependency; productName = PixelKit; }; + 7B94E1642B7ED95100E32B96 /* NetworkProtectionProxy */ = { + isa = XCSwiftPackageProductDependency; + productName = NetworkProtectionProxy; + }; + 7B97CD582B7E0B57004FEF43 /* NetworkProtectionProxy */ = { + isa = XCSwiftPackageProductDependency; + productName = NetworkProtectionProxy; + }; + 7B97CD5A2B7E0B85004FEF43 /* Common */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Common; + }; + 7B97CD612B7E0C4B004FEF43 /* PixelKit */ = { + isa = XCSwiftPackageProductDependency; + productName = PixelKit; + }; + 7BA076BA2B65D61400D7FB72 /* NetworkProtectionProxy */ = { + isa = XCSwiftPackageProductDependency; + productName = NetworkProtectionProxy; + }; 7BA59C9A2AE18B49009A97B1 /* SystemExtensionManager */ = { isa = XCSwiftPackageProductDependency; productName = SystemExtensionManager; @@ -13806,6 +14003,10 @@ isa = XCSwiftPackageProductDependency; productName = PixelKit; }; + 7BBE2B7A2B61663C00697445 /* NetworkProtectionProxy */ = { + isa = XCSwiftPackageProductDependency; + productName = NetworkProtectionProxy; + }; 7BEC182E2AD5D8DC00D30536 /* SystemExtensionManager */ = { isa = XCSwiftPackageProductDependency; productName = SystemExtensionManager; diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4796599b2d..e0a1c42442 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" : "5ecf4fe56f334be6eaecb65f6d55632a6d53921c", - "version" : "109.0.0" + "revision" : "da6a822844922401d80e26963b8b11dcd6ef221a", + "version" : "109.0.1" } }, { diff --git a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift index ebe3179a76..2f5d747ade 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift @@ -23,6 +23,7 @@ import Foundation import AppKit import Common import LoginItems +import NetworkProtectionProxy @MainActor final class DataBrokerProtectionDebugMenu: NSMenu { @@ -82,6 +83,11 @@ final class DataBrokerProtectionDebugMenu: NSMenu { NSMenuItem(title: "Restart", action: #selector(DataBrokerProtectionDebugMenu.backgroundAgentRestart)) .targetting(self) + + NSMenuItem.separator() + + NSMenuItem(title: "Show agent IP address", action: #selector(DataBrokerProtectionDebugMenu.showAgentIPAddress)) + .targetting(self) } NSMenuItem(title: "Operations") { @@ -253,6 +259,10 @@ final class DataBrokerProtectionDebugMenu: NSMenu { window.delegate = self } + @objc private func showAgentIPAddress() { + DataBrokerProtectionManager.shared.showAgentIPAddress() + } + @objc private func showForceOptOutWindow() { let viewController = DataBrokerForceOptOutViewController() let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 500, height: 400), diff --git a/DuckDuckGo/DBP/DataBrokerProtectionManager.swift b/DuckDuckGo/DBP/DataBrokerProtectionManager.swift index f2c9bebd4d..75f8c3e855 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionManager.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionManager.swift @@ -41,8 +41,10 @@ public final class DataBrokerProtectionManager { return dataManager }() + private lazy var ipcClient = DataBrokerProtectionIPCClient(machServiceName: Bundle.main.dbpBackgroundAgentBundleId, pixelHandler: pixelHandler) + lazy var scheduler: DataBrokerProtectionLoginItemScheduler = { - let ipcClient = DataBrokerProtectionIPCClient(machServiceName: Bundle.main.dbpBackgroundAgentBundleId, pixelHandler: pixelHandler) + let ipcScheduler = DataBrokerProtectionIPCScheduler(ipcClient: ipcClient) return DataBrokerProtectionLoginItemScheduler(ipcScheduler: ipcScheduler, pixelHandler: pixelHandler) @@ -57,6 +59,12 @@ public final class DataBrokerProtectionManager { public func shouldAskForInviteCode() -> Bool { redeemUseCase.shouldAskForInviteCode() } + + // MARK: - Debugging Features + + public func showAgentIPAddress() { + ipcClient.openBrowser(domain: "https://www.whatismyip.com") + } } extension DataBrokerProtectionManager: DataBrokerProtectionDataManagerDelegate { diff --git a/DuckDuckGo/DBP/LoginItem+DataBrokerProtection.swift b/DuckDuckGo/DBP/LoginItem+DataBrokerProtection.swift index e1e00e38c7..cdbfa623a9 100644 --- a/DuckDuckGo/DBP/LoginItem+DataBrokerProtection.swift +++ b/DuckDuckGo/DBP/LoginItem+DataBrokerProtection.swift @@ -16,6 +16,7 @@ // limitations under the License. // +import Foundation import LoginItems #if DBP diff --git a/DuckDuckGo/DuckDuckGo.entitlements b/DuckDuckGo/DuckDuckGo.entitlements index 7b79b8b2fe..757dc88e2c 100644 --- a/DuckDuckGo/DuckDuckGo.entitlements +++ b/DuckDuckGo/DuckDuckGo.entitlements @@ -5,6 +5,7 @@ com.apple.developer.networking.networkextension packet-tunnel-provider-systemextension + app-proxy-provider-systemextension com.apple.developer.system-extension.install diff --git a/DuckDuckGo/DuckDuckGoAppStore.entitlements b/DuckDuckGo/DuckDuckGoAppStore.entitlements index e419bc0920..97443cb452 100644 --- a/DuckDuckGo/DuckDuckGoAppStore.entitlements +++ b/DuckDuckGo/DuckDuckGoAppStore.entitlements @@ -19,6 +19,11 @@ com.apple.security.files.user-selected.read-write + com.apple.developer.networking.networkextension + + packet-tunnel-provider + app-proxy-provider + com.apple.security.network.client com.apple.security.personal-information.location diff --git a/DuckDuckGo/DuckDuckGoAppStoreCI.entitlements b/DuckDuckGo/DuckDuckGoAppStoreCI.entitlements index a2c7bd6bd5..13ea43d233 100644 --- a/DuckDuckGo/DuckDuckGoAppStoreCI.entitlements +++ b/DuckDuckGo/DuckDuckGoAppStoreCI.entitlements @@ -2,10 +2,6 @@ - com.apple.developer.networking.networkextension - - packet-tunnel-provider - com.apple.security.app-sandbox com.apple.security.application-groups diff --git a/DuckDuckGo/DuckDuckGoDebug.entitlements b/DuckDuckGo/DuckDuckGoDebug.entitlements index dcffb16791..dad1686cba 100644 --- a/DuckDuckGo/DuckDuckGoDebug.entitlements +++ b/DuckDuckGo/DuckDuckGoDebug.entitlements @@ -5,6 +5,7 @@ com.apple.developer.networking.networkextension packet-tunnel-provider + app-proxy-provider com.apple.developer.system-extension.install diff --git a/DuckDuckGo/DuckDuckGo_NetP_Debug.entitlements b/DuckDuckGo/DuckDuckGo_NetP_Debug.entitlements deleted file mode 100644 index 069c866e05..0000000000 --- a/DuckDuckGo/DuckDuckGo_NetP_Debug.entitlements +++ /dev/null @@ -1,30 +0,0 @@ - - - - - com.apple.developer.networking.networkextension - - packet-tunnel-provider - - com.apple.developer.system-extension.install - - com.apple.developer.usernotifications.time-sensitive - - com.apple.security.application-groups - - HKE973VLUW.com.duckduckgo.network-protection - $(NETP_APP_GROUP) - - com.apple.security.device.audio-input - - com.apple.security.device.camera - - com.apple.security.personal-information.location - - keychain-access-groups - - $(AppIdentifierPrefix)com.duckduckgo.macos.browser - $(AppIdentifierPrefix)com.duckduckgo.network-protection - - - diff --git a/DuckDuckGo/DuckDuckGo_NetP_Release.entitlements b/DuckDuckGo/DuckDuckGo_NetP_Release.entitlements deleted file mode 100644 index a2226d1f8d..0000000000 --- a/DuckDuckGo/DuckDuckGo_NetP_Release.entitlements +++ /dev/null @@ -1,38 +0,0 @@ - - - - - com.apple.developer.networking.networkextension - - packet-tunnel-provider-systemextension - - com.apple.developer.system-extension.install - - com.apple.developer.usernotifications.time-sensitive - - com.apple.security.application-groups - - $(TeamIdentifierPrefix)com.duckduckgo.macos.browser.network-protection - $(NETP_APP_GROUP) - - com.apple.security.device.audio-input - - com.apple.security.device.camera - - com.apple.security.personal-information.location - - keychain-access-groups - - $(AppIdentifierPrefix)com.duckduckgo.macos.browser - $(AppIdentifierPrefix)com.duckduckgo.network-protection - - com.apple.security.personal-information.location - - com.apple.developer.networking.networkextension - - packet-tunnel-provider-systemextension - - com.apple.developer.system-extension.install - - - diff --git a/DuckDuckGo/InfoPlist.xcstrings b/DuckDuckGo/InfoPlist.xcstrings index 4d7c2d94c0..70d7389fb7 100644 --- a/DuckDuckGo/InfoPlist.xcstrings +++ b/DuckDuckGo/InfoPlist.xcstrings @@ -8,7 +8,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "DuckDuckGo" + "value" : "DuckDuckGo Privacy Pro" } } } diff --git a/DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/Bundle+VPN.swift b/DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/Bundle+VPN.swift new file mode 100644 index 0000000000..169f0ceb50 --- /dev/null +++ b/DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/Bundle+VPN.swift @@ -0,0 +1,65 @@ +// +// Bundle+VPN.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 +import NetworkProtection + +extension Bundle { + + private enum VPNInfoKey: String { + case tunnelExtensionBundleID = "TUNNEL_EXTENSION_BUNDLE_ID" + case proxyExtensionBundleID = "PROXY_EXTENSION_BUNDLE_ID" + } + + static var tunnelExtensionBundleID: String { + string(for: .tunnelExtensionBundleID) + } + + static var proxyExtensionBundleID: String { + string(for: .proxyExtensionBundleID) + } + + private static func string(for key: VPNInfoKey) -> String { + guard let bundleID = Bundle.main.object(forInfoDictionaryKey: key.rawValue) as? String else { + fatalError("Info.plist is missing \(key)") + } + + return bundleID + } + +#if !NETWORK_EXTENSION + // for the Main or Launcher Agent app + static func mainAppBundle() -> Bundle { + return Bundle.main + } +#elseif NETP_SYSTEM_EXTENSION + // for the System Extension (Developer ID) + static func mainAppBundle() -> Bundle { + return Bundle(url: .mainAppBundleURL)! + } + // AppEx (App Store) can‘t access Main App Bundle +#endif + + static let keychainType: KeychainType = { +#if NETP_SYSTEM_EXTENSION + .system +#else + .dataProtection(.named(Bundle.main.appGroup(bundle: .netP))) +#endif + }() +} diff --git a/DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/NetworkProtectionBundle.swift b/DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/NetworkProtectionBundle.swift deleted file mode 100644 index e14b7f1e84..0000000000 --- a/DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/NetworkProtectionBundle.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// NetworkProtectionBundle.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 -import NetworkProtection - -enum NetworkProtectionBundle { - -#if !NETWORK_EXTENSION - // for the Main or Launcher Agent app - static func mainAppBundle() -> Bundle { - return Bundle.main - } -#elseif NETP_SYSTEM_EXTENSION - // for the System Extension (Developer ID) - static func mainAppBundle() -> Bundle { - return Bundle(url: .mainAppBundleURL)! - } - // AppEx (App Store) can‘t access Main App Bundle -#endif - - static func extensionBundle() -> Bundle { -#if NETWORK_EXTENSION // When this code is compiled for any network-extension - return Bundle.main -#elseif NETP_SYSTEM_EXTENSION // When this code is compiled for the app when configured to use the sysex - let extensionsDirectoryURL = URL(fileURLWithPath: "Contents/Library/SystemExtensions", relativeTo: Bundle.main.bundleURL) - return extensionBundle(at: extensionsDirectoryURL) -#else // When this code is compiled for the app when configured to use the appex - let extensionsDirectoryURL = URL(fileURLWithPath: "Contents/Plugins", relativeTo: Bundle.main.bundleURL) - return extensionBundle(at: extensionsDirectoryURL) -#endif - } - - static func extensionBundle(at url: URL) -> Bundle { - let extensionURLs: [URL] - do { - extensionURLs = try FileManager.default.contentsOfDirectory(at: url, - includingPropertiesForKeys: nil, - options: .skipsHiddenFiles) - } catch let error { - fatalError("🔵 Failed to get the contents of \(url.absoluteString): \(error.localizedDescription)") - } - - // This should be updated to work well with other extensions - guard let extensionURL = extensionURLs.first else { - fatalError("🔵 Failed to find any system extensions") - } - - guard let extensionBundle = Bundle(url: extensionURL) else { - fatalError("🔵 Failed to create a bundle with URL \(extensionURL.absoluteString)") - } - - return extensionBundle - } - - static let keychainType: KeychainType = { -#if NETP_SYSTEM_EXTENSION - .system -#else - .dataProtection(.named(Bundle.main.appGroup(bundle: .netP))) -#endif - }() -} diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift index e1696a7aba..f54236f387 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift @@ -22,6 +22,7 @@ import AppKit import Common import Foundation import NetworkProtection +import NetworkProtectionProxy import SwiftUI /// Controller for the Network Protection debug menu. @@ -29,6 +30,10 @@ import SwiftUI @MainActor final class NetworkProtectionDebugMenu: NSMenu { + private let transparentProxySettings = TransparentProxySettings(defaults: .netP) + + // MARK: - Menus + private let environmentMenu = NSMenu() private let preferredServerMenu: NSMenu @@ -39,7 +44,9 @@ final class NetworkProtectionDebugMenu: NSMenu { private let resetToDefaults = NSMenuItem(title: "Reset Settings to defaults", action: #selector(NetworkProtectionDebugMenu.resetSettings)) - private let exclusionsMenu = NSMenu() + private let excludedRoutesMenu = NSMenu() + private let excludeDDGBrowserTrafficFromVPN = NSMenuItem(title: "DDG Browser", action: #selector(toggleExcludeDDGBrowser)) + private let excludeDBPTrafficFromVPN = NSMenuItem(title: "DBP Background Agent", action: #selector(toggleExcludeDBPBackgroundAgent)) private let shouldEnforceRoutesMenuItem = NSMenuItem(title: "Kill Switch (enforceRoutes)", action: #selector(NetworkProtectionDebugMenu.toggleEnforceRoutesAction)) private let shouldIncludeAllNetworksMenuItem = NSMenuItem(title: "includeAllNetworks", action: #selector(NetworkProtectionDebugMenu.toggleIncludeAllNetworks)) @@ -89,7 +96,6 @@ final class NetworkProtectionDebugMenu: NSMenu { .targetting(self) shouldEnforceRoutesMenuItem .targetting(self) - NSMenuItem(title: "Excluded Routes").submenu(exclusionsMenu) NSMenuItem.separator() NSMenuItem(title: "Send Test Notification", action: #selector(NetworkProtectionDebugMenu.sendTestNotification)) @@ -104,6 +110,14 @@ final class NetworkProtectionDebugMenu: NSMenu { NSMenuItem(title: "Environment") .submenu(environmentMenu) + NSMenuItem(title: "Exclusions") { + NSMenuItem(title: "Excluded Apps") { + excludeDDGBrowserTrafficFromVPN.targetting(self) + excludeDBPTrafficFromVPN.targetting(self) + } + NSMenuItem(title: "Excluded Routes").submenu(excludedRoutesMenu) + } + NSMenuItem(title: "Preferred Server").submenu(preferredServerMenu) NSMenuItem(title: "Registration Key") { @@ -172,8 +186,8 @@ final class NetworkProtectionDebugMenu: NSMenu { populateNetworkProtectionServerListMenuItems() populateNetworkProtectionRegistrationKeyValidityMenuItems() - exclusionsMenu.delegate = self - exclusionsMenu.autoenablesItems = false + excludedRoutesMenu.delegate = self + excludedRoutesMenu.autoenablesItems = false populateExclusionsMenuItems() } @@ -391,7 +405,7 @@ final class NetworkProtectionDebugMenu: NSMenu { } private func populateExclusionsMenuItems() { - exclusionsMenu.removeAllItems() + excludedRoutesMenu.removeAllItems() for item in settings.excludedRoutes { let menuItem: NSMenuItem @@ -406,7 +420,7 @@ final class NetworkProtectionDebugMenu: NSMenu { target: self, representedObject: range.stringRepresentation) } - exclusionsMenu.addItem(menuItem) + excludedRoutesMenu.addItem(menuItem) } // Only allow testers to enter a custom code if they're on the waitlist, to simulate the correct path through the flow @@ -419,6 +433,7 @@ final class NetworkProtectionDebugMenu: NSMenu { override func update() { updateEnvironmentMenu() + updateExclusionsMenu() updatePreferredServerMenu() updateRekeyValidityMenu() updateNetworkProtectionMenuItemsState() @@ -588,6 +603,7 @@ final class NetworkProtectionDebugMenu: NSMenu { } // MARK: Environment + @objc func setSelectedEnvironment(_ menuItem: NSMenuItem) { let title = menuItem.title let selectedEnvironment: VPNSettings.SelectedEnvironment @@ -608,6 +624,24 @@ final class NetworkProtectionDebugMenu: NSMenu { settings.selectedServer = .automatic } } + + // MARK: - Exclusions + + private let dbpBackgroundAppIdentifier = Bundle.main.dbpBackgroundAgentBundleId + private let ddgBrowserAppIdentifier = Bundle.main.bundleIdentifier! + + private func updateExclusionsMenu() { + excludeDBPTrafficFromVPN.state = transparentProxySettings.isExcluding(dbpBackgroundAppIdentifier) ? .on : .off + excludeDDGBrowserTrafficFromVPN.state = transparentProxySettings.isExcluding(ddgBrowserAppIdentifier) ? .on : .off + } + + @objc private func toggleExcludeDBPBackgroundAgent() { + transparentProxySettings.toggleExclusion(for: dbpBackgroundAppIdentifier) + } + + @objc private func toggleExcludeDDGBrowser() { + transparentProxySettings.toggleExclusion(for: ddgBrowserAppIdentifier) + } } extension NetworkProtectionDebugMenu: NSMenuDelegate { diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift index f67223a545..0bd4975196 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift @@ -24,6 +24,7 @@ import SwiftUI import Common import NetworkExtension import NetworkProtection +import NetworkProtectionProxy import NetworkProtectionUI import Networking import PixelKit @@ -38,6 +39,8 @@ typealias NetworkProtectionConfigChangeHandler = () -> Void final class NetworkProtectionTunnelController: TunnelController, TunnelSessionProvider { + // MARK: - Settings + let settings: VPNSettings // MARK: - Combine Cancellables @@ -60,6 +63,8 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr /// private let controllerErrorStore = NetworkProtectionControllerErrorStore() + private let notificationCenter: NotificationCenter + // MARK: - VPN Tunnel & Configuration /// Auth token store @@ -95,6 +100,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr /// Loads the configuration matching our ``extensionID``. /// + @MainActor public var manager: NETunnelProviderManager? { get async { if let internalManager { @@ -139,13 +145,14 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr init(networkExtensionBundleID: String, networkExtensionController: NetworkExtensionController, settings: VPNSettings, - notificationCenter: NotificationCenter = .default, tokenStore: NetworkProtectionTokenStore = NetworkProtectionKeychainTokenStore(), + notificationCenter: NotificationCenter = .default, logger: NetworkProtectionLogger = DefaultNetworkProtectionLogger()) { self.logger = logger self.networkExtensionBundleID = networkExtensionBundleID self.networkExtensionController = networkExtensionController + self.notificationCenter = notificationCenter self.settings = settings self.tokenStore = tokenStore @@ -254,7 +261,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr tunnelManager.protocolConfiguration = { let protocolConfiguration = tunnelManager.protocolConfiguration as? NETunnelProviderProtocol ?? NETunnelProviderProtocol() protocolConfiguration.serverAddress = "127.0.0.1" // Dummy address... the NetP service will take care of grabbing a real server - protocolConfiguration.providerBundleIdentifier = NetworkProtectionBundle.extensionBundle().bundleIdentifier + protocolConfiguration.providerBundleIdentifier = Bundle.tunnelExtensionBundleID protocolConfiguration.providerConfiguration = [ NetworkProtectionOptionKey.defaultPixelHeaders: APIRequest.Headers().httpHeaders, NetworkProtectionOptionKey.includedRoutes: includedRoutes().map(\.stringRepresentation) as NSArray @@ -304,6 +311,14 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr } } + // MARK: - Connection + + public var status: NEVPNStatus { + get async { + await connection?.status ?? .disconnected + } + } + // MARK: - Connection Status Querying /// Queries Network Protection to know if its VPN is connected. diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift index 5770b78a2f..3a3a392736 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift @@ -224,7 +224,7 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { let tunnelHealthStore = NetworkProtectionTunnelHealthStore(notificationCenter: notificationCenter) let controllerErrorStore = NetworkProtectionTunnelErrorStore(notificationCenter: notificationCenter) let debugEvents = Self.networkProtectionDebugEvents(controllerErrorStore: controllerErrorStore) - let tokenStore = NetworkProtectionKeychainTokenStore(keychainType: NetworkProtectionBundle.keychainType, + let tokenStore = NetworkProtectionKeychainTokenStore(keychainType: Bundle.keychainType, serviceName: Self.tokenServiceName, errorEvents: debugEvents) let notificationsPresenter = NetworkProtectionNotificationsPresenterFactory().make(settings: settings) @@ -232,7 +232,7 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { super.init(notificationsPresenter: notificationsPresenter, tunnelHealthStore: tunnelHealthStore, controllerErrorStore: controllerErrorStore, - keychainType: NetworkProtectionBundle.keychainType, + keychainType: Bundle.keychainType, tokenStore: tokenStore, debugEvents: debugEvents, providerEvents: Self.packetTunnelProviderEvents, @@ -323,13 +323,6 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { case missingPixelHeaders } - override func prepareToConnect(using provider: NETunnelProviderProtocol?) { - super.prepareToConnect(using: provider) - - guard PixelKit.shared == nil, let options = provider?.providerConfiguration else { return } - try? loadDefaultPixelHeaders(from: options) - } - public override func loadVendorOptions(from provider: NETunnelProviderProtocol?) throws { try super.loadVendorOptions(from: provider) @@ -350,6 +343,15 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { setupPixels(defaultHeaders: defaultPixelHeaders) } + // MARK: - Overrideable Connection Events + + override func prepareToConnect(using provider: NETunnelProviderProtocol?) { + super.prepareToConnect(using: provider) + + guard PixelKit.shared == nil, let options = provider?.providerConfiguration else { return } + try? loadDefaultPixelHeaders(from: options) + } + // MARK: - Start/Stop Tunnel override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacTransparentProxyProvider.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacTransparentProxyProvider.swift new file mode 100644 index 0000000000..d300309ec6 --- /dev/null +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacTransparentProxyProvider.swift @@ -0,0 +1,94 @@ +// +// MacTransparentProxyProvider.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import Common +import Foundation +import Networking +import NetworkExtension +import NetworkProtectionProxy +import os.log // swiftlint:disable:this enforce_os_log_wrapper +import PixelKit + +final class MacTransparentProxyProvider: TransparentProxyProvider { + + static var vpnProxyLogger = Logger(subsystem: OSLog.subsystem, category: "VPN Proxy") + + private var cancellables = Set() + + @objc init() { + let loadSettingsFromStartupOptions: Bool = { +#if NETP_SYSTEM_EXTENSION + true +#else + false +#endif + }() + + let settings: TransparentProxySettings = { +#if NETP_SYSTEM_EXTENSION + /// Because our System Extension is running in the system context and doesn't have access + /// to shared user defaults, we just make it use the `.standard` defaults. + TransparentProxySettings(defaults: .standard) +#else + /// Because our App Extension is running in the user context and has access + /// to shared user defaults, we take advantage of this and use the `.netP` defaults. + TransparentProxySettings(defaults: .netP) +#endif + }() + + let configuration = TransparentProxyProvider.Configuration( + loadSettingsFromProviderConfiguration: loadSettingsFromStartupOptions) + + super.init(settings: settings, + configuration: configuration, + logger: Self.vpnProxyLogger) + + eventHandler = eventHandler(_:) + +#if !NETP_SYSTEM_EXTENSION + let dryRun: Bool +#if DEBUG + dryRun = true +#else + dryRun = false +#endif + + PixelKit.setUp(dryRun: dryRun, + appVersion: AppVersion.shared.versionNumber, + source: "vpnProxyExtension", + defaultHeaders: [:], + log: .networkProtectionPixel, + defaults: .netP) { (pixelName: String, headers: [String: String], parameters: [String: String], _, _, onComplete: @escaping PixelKit.CompletionBlock) in + + let url = URL.pixelUrl(forPixelNamed: pixelName) + let apiHeaders = APIRequest.Headers(additionalHeaders: headers) + let configuration = APIRequest.Configuration(url: url, method: .get, queryParameters: parameters, headers: apiHeaders) + let request = APIRequest(configuration: configuration) + + request.fetch { _, error in + onComplete(error == nil, error) + } + } +#endif + } + + private func eventHandler(_ event: TransparentProxyProvider.Event) { + PixelKit.fire(event) + } +} diff --git a/DuckDuckGo/NetworkProtectionAppExtension.entitlements b/DuckDuckGo/NetworkProtectionAppExtension.entitlements index 13dd983ca1..d37610bb07 100644 --- a/DuckDuckGo/NetworkProtectionAppExtension.entitlements +++ b/DuckDuckGo/NetworkProtectionAppExtension.entitlements @@ -5,6 +5,7 @@ com.apple.developer.networking.networkextension packet-tunnel-provider + app-proxy-provider com.apple.security.app-sandbox diff --git a/DuckDuckGoDBPBackgroundAgent/BrowserWindowManager.swift b/DuckDuckGoDBPBackgroundAgent/BrowserWindowManager.swift new file mode 100644 index 0000000000..85891c6604 --- /dev/null +++ b/DuckDuckGoDBPBackgroundAgent/BrowserWindowManager.swift @@ -0,0 +1,64 @@ +// +// BrowserWindowManager.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import AppKit +import Foundation +import WebKit + +/// A class that offers functionality to quickly show an interactive browser window. +/// +/// This class is meant to aid with debugging and should not be included in release builds. +/// . +final class BrowserWindowManager: NSObject { + private var interactiveBrowserWindow: NSWindow? + + @MainActor + func show(domain: String) { + if let interactiveBrowserWindow, interactiveBrowserWindow.isVisible { + return + } + + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 800, height: 600), + styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], + backing: .buffered, defer: false) + window.center() + window.title = "Web Browser" + window.delegate = self + interactiveBrowserWindow = window + + // Create the WKWebView. + let webView = WKWebView(frame: window.contentView!.bounds) + webView.autoresizingMask = [.width, .height] + window.contentView!.addSubview(webView) + + // Load a URL. + let url = URL(string: domain)! + let request = URLRequest(url: url) + webView.load(request) + + // Show the window. + window.makeKeyAndOrderFront(nil) + } +} + +extension BrowserWindowManager: NSWindowDelegate { + func windowWillClose(_ notification: Notification) { + interactiveBrowserWindow = nil + } +} diff --git a/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift b/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift index 1d7c0403fb..c452200f7b 100644 --- a/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift +++ b/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift @@ -17,10 +17,10 @@ // import Combine -import Foundation +import Common import DataBrokerProtection +import Foundation import PixelKit -import Common /// Manages the IPC service for the Agent app /// @@ -28,6 +28,7 @@ import Common /// demand interaction with. /// final class IPCServiceManager { + private var browserWindowManager: BrowserWindowManager private let ipcServer: DataBrokerProtectionIPCServer private let scheduler: DataBrokerProtectionScheduler private let pixelHandler: EventMapping @@ -41,6 +42,8 @@ final class IPCServiceManager { self.scheduler = scheduler self.pixelHandler = pixelHandler + browserWindowManager = BrowserWindowManager() + ipcServer.serverDelegate = self ipcServer.activate() } @@ -102,4 +105,10 @@ extension IPCServiceManager: IPCServerInterface { pixelHandler.fire(.ipcServerRunAllOperations) scheduler.runAllOperations(showWebView: showWebView) } + + func openBrowser(domain: String) { + Task { @MainActor in + browserWindowManager.show(domain: domain) + } + } } diff --git a/DuckDuckGoVPN/DuckDuckGoVPN.entitlements b/DuckDuckGoVPN/DuckDuckGoVPN.entitlements index 653311b9ec..2797c3f947 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPN.entitlements +++ b/DuckDuckGoVPN/DuckDuckGoVPN.entitlements @@ -5,6 +5,7 @@ com.apple.developer.networking.networkextension packet-tunnel-provider-systemextension + app-proxy-provider-systemextension com.apple.developer.system-extension.install diff --git a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift index add4af8a6d..f14bbac165 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift +++ b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift @@ -23,7 +23,7 @@ import LoginItems import Networking import NetworkExtension import NetworkProtection -import NetworkProtectionIPC +import NetworkProtectionProxy import NetworkProtectionUI import ServiceManagement import PixelKit @@ -60,18 +60,82 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { private var cancellables = Set() - var networkExtensionBundleID: String { - Bundle.main.networkExtensionBundleID + var proxyExtensionBundleID: String { + Bundle.proxyExtensionBundleID } -#if NETWORK_PROTECTION - private lazy var networkExtensionController = NetworkExtensionController(extensionBundleID: networkExtensionBundleID) + var tunnelExtensionBundleID: String { + Bundle.tunnelExtensionBundleID + } + + private lazy var networkExtensionController = NetworkExtensionController(extensionBundleID: tunnelExtensionBundleID) + + private var storeProxySettingsInProviderConfiguration: Bool { +#if NETP_SYSTEM_EXTENSION + true +#else + false #endif + } private lazy var tunnelSettings = VPNSettings(defaults: .netP) + private lazy var proxySettings = TransparentProxySettings(defaults: .netP) + + @MainActor + private lazy var vpnProxyLauncher = VPNProxyLauncher( + tunnelController: tunnelController, + proxyController: proxyController) + + @MainActor + private lazy var proxyController: TransparentProxyController = { + let controller = TransparentProxyController( + extensionID: proxyExtensionBundleID, + storeSettingsInProviderConfiguration: storeProxySettingsInProviderConfiguration, + settings: proxySettings) { [weak self] manager in + guard let self else { return } + + manager.localizedDescription = "DuckDuckGo VPN Proxy" + + if !manager.isEnabled { + manager.isEnabled = true + } + + manager.protocolConfiguration = { + let protocolConfiguration = manager.protocolConfiguration as? NETunnelProviderProtocol ?? NETunnelProviderProtocol() + protocolConfiguration.serverAddress = "127.0.0.1" // Dummy address... the NetP service will take care of grabbing a real server + protocolConfiguration.providerBundleIdentifier = self.proxyExtensionBundleID + + // always-on + protocolConfiguration.disconnectOnSleep = false + + // kill switch + // protocolConfiguration.enforceRoutes = false + + // this setting breaks Connection Tester + // protocolConfiguration.includeAllNetworks = settings.includeAllNetworks + + // This is intentionally not used but left here for documentation purposes. + // The reason for this is that we want to have full control of the routes that + // are excluded, so instead of using this setting we're just configuring the + // excluded routes through our VPNSettings class, which our extension reads directly. + // protocolConfiguration.excludeLocalNetworks = settings.excludeLocalNetworks + return protocolConfiguration + }() + } + + controller.eventHandler = handleControllerEvent(_:) + + return controller + }() + + private func handleControllerEvent(_ event: TransparentProxyController.Event) { + PixelKit.fire(event) + } + + @MainActor private lazy var tunnelController = NetworkProtectionTunnelController( - networkExtensionBundleID: networkExtensionBundleID, + networkExtensionBundleID: tunnelExtensionBundleID, networkExtensionController: networkExtensionController, settings: tunnelSettings) @@ -79,6 +143,7 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { /// /// This is used by our main app to control the tunnel through the VPN login item. /// + @MainActor private lazy var tunnelControllerIPCService: TunnelControllerIPCService = { let ipcServer = TunnelControllerIPCService( tunnelController: tunnelController, @@ -88,17 +153,19 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { return ipcServer }() + @MainActor + private lazy var statusObserver = ConnectionStatusObserverThroughSession( + tunnelSessionProvider: tunnelController, + platformNotificationCenter: NSWorkspace.shared.notificationCenter, + platformDidWakeNotification: NSWorkspace.didWakeNotification) + + @MainActor private lazy var statusReporter: NetworkProtectionStatusReporter = { let errorObserver = ConnectionErrorObserverThroughSession( tunnelSessionProvider: tunnelController, platformNotificationCenter: NSWorkspace.shared.notificationCenter, platformDidWakeNotification: NSWorkspace.didWakeNotification) - let statusObserver = ConnectionStatusObserverThroughSession( - tunnelSessionProvider: tunnelController, - platformNotificationCenter: NSWorkspace.shared.notificationCenter, - platformDidWakeNotification: NSWorkspace.didWakeNotification) - let serverInfoObserver = ConnectionServerInfoObserverThroughSession( tunnelSessionProvider: tunnelController, platformNotificationCenter: NSWorkspace.shared.notificationCenter, @@ -113,6 +180,7 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { ) }() + @MainActor private lazy var vpnAppEventsHandler = { VPNAppEventsHandler(tunnelController: tunnelController) }() @@ -175,8 +243,9 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { bouncer.requireAuthTokenOrKillApp() - // Initialize the IPC server + // Initialize lazy properties _ = tunnelControllerIPCService + _ = vpnProxyLauncher let dryRun: Bool diff --git a/DuckDuckGoVPN/DuckDuckGoVPNDebug.entitlements b/DuckDuckGoVPN/DuckDuckGoVPNDebug.entitlements index a6ed34f64f..f531d0bc0c 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNDebug.entitlements +++ b/DuckDuckGoVPN/DuckDuckGoVPNDebug.entitlements @@ -5,6 +5,7 @@ com.apple.developer.networking.networkextension packet-tunnel-provider + app-proxy-provider com.apple.developer.system-extension.install diff --git a/DuckDuckGoVPN/Info-AppStore.plist b/DuckDuckGoVPN/Info-AppStore.plist index 7627fdd9c9..a1b2b02a02 100644 --- a/DuckDuckGoVPN/Info-AppStore.plist +++ b/DuckDuckGoVPN/Info-AppStore.plist @@ -6,8 +6,10 @@ $(DISTRIBUTED_NOTIFICATIONS_PREFIX) NETP_APP_GROUP $(NETP_APP_GROUP) - SYSEX_BUNDLE_ID - $(SYSEX_BUNDLE_ID) + PROXY_EXTENSION_BUNDLE_ID + $(PROXY_EXTENSION_BUNDLE_ID) + TUNNEL_EXTENSION_BUNDLE_ID + $(TUNNEL_EXTENSION_BUNDLE_ID) LSApplicationCategoryType public.app-category.productivity CFBundleShortVersionString diff --git a/DuckDuckGoVPN/Info.plist b/DuckDuckGoVPN/Info.plist index 7627fdd9c9..a1b2b02a02 100644 --- a/DuckDuckGoVPN/Info.plist +++ b/DuckDuckGoVPN/Info.plist @@ -6,8 +6,10 @@ $(DISTRIBUTED_NOTIFICATIONS_PREFIX) NETP_APP_GROUP $(NETP_APP_GROUP) - SYSEX_BUNDLE_ID - $(SYSEX_BUNDLE_ID) + PROXY_EXTENSION_BUNDLE_ID + $(PROXY_EXTENSION_BUNDLE_ID) + TUNNEL_EXTENSION_BUNDLE_ID + $(TUNNEL_EXTENSION_BUNDLE_ID) LSApplicationCategoryType public.app-category.productivity CFBundleShortVersionString diff --git a/DuckDuckGoVPN/VPNProxyLauncher.swift b/DuckDuckGoVPN/VPNProxyLauncher.swift new file mode 100644 index 0000000000..c99d187cf2 --- /dev/null +++ b/DuckDuckGoVPN/VPNProxyLauncher.swift @@ -0,0 +1,149 @@ +// +// VPNProxyLauncher.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import Foundation +import NetworkProtectionProxy +import NetworkExtension + +/// Starts and stops the VPN proxy component. +/// +/// This class looks at the tunnel and the proxy components and their status and settings, and decides based on +/// a number of conditions whether to start the proxy, stop it, or just leave it be. +/// +@MainActor +final class VPNProxyLauncher { + private let tunnelController: NetworkProtectionTunnelController + private let proxyController: TransparentProxyController + private let notificationCenter: NotificationCenter + private var cancellables = Set() + + init(tunnelController: NetworkProtectionTunnelController, + proxyController: TransparentProxyController, + notificationCenter: NotificationCenter = .default) { + + self.notificationCenter = notificationCenter + self.proxyController = proxyController + self.tunnelController = tunnelController + + subscribeToStatusChanges() + subscribeToProxySettingChanges() + } + + // MARK: - Status Changes + + private func subscribeToStatusChanges() { + notificationCenter.publisher(for: .NEVPNStatusDidChange) + .receive(on: DispatchQueue.main) + .removeDuplicates() + .sink(receiveValue: statusChanged(notification:)) + .store(in: &cancellables) + } + + private func statusChanged(notification: Notification) { + Task { @MainActor in + let isProxyConnectionStatusChange = await proxyController.connection == notification.object as? NEVPNConnection + + try await startOrStopProxyIfNeeded(isProxyConnectionStatusChange: isProxyConnectionStatusChange) + } + } + + // MARK: - Proxy Settings Changes + + private func subscribeToProxySettingChanges() { + proxyController.settings.changePublisher + .sink(receiveValue: proxySettingChanged(_:)) + .store(in: &cancellables) + } + + private func proxySettingChanged(_ change: TransparentProxySettings.Change) { + Task { @MainActor in + try await startOrStopProxyIfNeeded() + } + } + + // MARK: - Auto starting & stopping the proxy component + + private var isControllingProxy = false + + private func startOrStopProxyIfNeeded(isProxyConnectionStatusChange: Bool = false) async throws { + if await shouldStartProxy { + guard !isControllingProxy else { + return + } + + isControllingProxy = true + + // When we're auto-starting the proxy because its own status changed to + // disconnected, we want to give it a pause because if it fails to connect again + // we risk the proxy entering a frenetic connect / disconnect loop + if isProxyConnectionStatusChange { + // If the proxy connection was stopped, let's wait a bit before trying to enable it again + try await Task.sleep(interval: .seconds(10)) + + // And we want to check again if the proxy still needs to start after waiting + guard await shouldStartProxy else { + return + } + } + + do { + try await proxyController.start() + isControllingProxy = false + } catch { + isControllingProxy = false + throw error + } + } else if await shouldStopProxy { + guard !isControllingProxy else { + return + } + + isControllingProxy = true + await proxyController.stop() + isControllingProxy = false + } + } + + private var shouldStartProxy: Bool { + get async { + let proxyIsDisconnected = await proxyController.status == .disconnected + let tunnelIsConnected = await tunnelController.status == .connected + + // Starting the proxy only when it's required for active features + // is a product decision. It may change once we decide the proxy + // is stable enough to be running at all times. + return proxyIsDisconnected + && tunnelIsConnected + && proxyController.isRequiredForActiveFeatures + } + } + + private var shouldStopProxy: Bool { + get async { + let proxyIsConnected = await proxyController.status == .connected + let tunnelIsDisconnected = await tunnelController.status == .disconnected + + // Stopping the proxy when it's not required for active features + // is a product decision. It may change once we decide the proxy + // is stable enough to be running at all times. + return proxyIsConnected + && (tunnelIsDisconnected || !proxyController.isRequiredForActiveFeatures) + } + } +} diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index fb62529ee7..64fa721012 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: "109.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper") diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift index 4a6f58ac28..2aa3953437 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift @@ -17,9 +17,9 @@ // import Combine +import Common import Foundation import XPCHelper -import Common /// This protocol describes the server-side IPC interface for controlling the tunnel /// @@ -150,6 +150,17 @@ extension DataBrokerProtectionIPCClient: IPCServerInterface { // If you add a completion block, please remember to call it here too! }) } + + public func openBrowser(domain: String) { + self.pixelHandler.fire(.ipcServerRunAllOperations) + xpc.execute(call: { server in + server.openBrowser(domain: domain) + }, xpcReplyErrorHandler: { error in + os_log("Error \(error.localizedDescription)") + // Intentional no-op as there's no completion block + // If you add a completion block, please remember to call it here too! + }) + } } // MARK: - Incoming communication from the server diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift index fde0274a5f..a2bc3d0e56 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift @@ -42,6 +42,12 @@ public protocol IPCServerInterface: AnyObject { func scanAllBrokers(showWebView: Bool, completion: @escaping ((Error?) -> Void)) func runQueuedOperations(showWebView: Bool, completion: @escaping ((Error?) -> Void)) func runAllOperations(showWebView: Bool) + + // MARK: - Debugging Features + + /// Opens a browser window with the specified domain + /// + func openBrowser(domain: String) } /// This protocol describes the server-side XPC interface. @@ -71,6 +77,12 @@ protocol XPCServerInterface { func scanAllBrokers(showWebView: Bool, completion: @escaping ((Error?) -> Void)) func runQueuedOperations(showWebView: Bool, completion: @escaping ((Error?) -> Void)) func runAllOperations(showWebView: Bool) + + // MARK: - Debugging Features + + /// Opens a browser window with the specified domain + /// + func openBrowser(domain: String) } public final class DataBrokerProtectionIPCServer { @@ -146,4 +158,8 @@ extension DataBrokerProtectionIPCServer: XPCServerInterface { func runAllOperations(showWebView: Bool) { serverDelegate?.runAllOperations(showWebView: showWebView) } + + func openBrowser(domain: String) { + serverDelegate?.openBrowser(domain: domain) + } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift index ad3dfda484..9f12bbab0c 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift @@ -279,7 +279,6 @@ public enum DataBrokerProtectionPixels { } extension DataBrokerProtectionPixels: PixelKitEvent { - public var name: String { switch self { case .parentChildMatches: return "m_mac_dbp_macos_parent-child-broker-matches" diff --git a/LocalPackages/LoginItems/Package.swift b/LocalPackages/LoginItems/Package.swift index bbe7e894b8..a631a199d9 100644 --- a/LocalPackages/LoginItems/Package.swift +++ b/LocalPackages/LoginItems/Package.swift @@ -13,7 +13,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), ], targets: [ .target( diff --git a/LocalPackages/NetworkProtectionMac/Package.resolved b/LocalPackages/NetworkProtectionMac/Package.resolved new file mode 100644 index 0000000000..08c5add0f4 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Package.resolved @@ -0,0 +1,104 @@ +{ + "pins" : [ + { + "identity" : "bloom_cpp", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/bloom_cpp.git", + "state" : { + "revision" : "8076199456290b61b4544bf2f4caf296759906a0", + "version" : "3.0.0" + } + }, + { + "identity" : "browserserviceskit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/BrowserServicesKit", + "state" : { + "revision" : "1f7932fe67a0d8b1ae97e62cb333639353d4772f", + "version" : "101.2.2" + } + }, + { + "identity" : "content-scope-scripts", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/content-scope-scripts", + "state" : { + "revision" : "0b68b0d404d8d4f32296cd84fa160b18b0aeaf44", + "version" : "4.59.1" + } + }, + { + "identity" : "duckduckgo-autofill", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/duckduckgo-autofill.git", + "state" : { + "revision" : "b972bc0ab6ee1d57a0a18a197dcc31e40ae6ac57", + "version" : "10.0.3" + } + }, + { + "identity" : "grdb.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/GRDB.swift.git", + "state" : { + "revision" : "9f049d7b97b1e68ffd86744b500660d34a9e79b8", + "version" : "2.3.0" + } + }, + { + "identity" : "privacy-dashboard", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/privacy-dashboard", + "state" : { + "revision" : "38336a574e13090764ba09a6b877d15ee514e371", + "version" : "3.1.1" + } + }, + { + "identity" : "punycodeswift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/gumob/PunycodeSwift.git", + "state" : { + "revision" : "4356ec54e073741449640d3d50a1fd24fd1e1b8b", + "version" : "2.1.0" + } + }, + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser", + "state" : { + "revision" : "c8ed701b513cf5177118a175d85fbbbcd707ab41", + "version" : "1.3.0" + } + }, + { + "identity" : "sync_crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/sync_crypto", + "state" : { + "revision" : "2ab6ab6f0f96b259c14c2de3fc948935fc16ac78", + "version" : "0.2.0" + } + }, + { + "identity" : "trackerradarkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/TrackerRadarKit", + "state" : { + "revision" : "a6b7ba151d9dc6684484f3785293875ec01cc1ff", + "version" : "1.2.2" + } + }, + { + "identity" : "wireguard-apple", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/wireguard-apple", + "state" : { + "revision" : "2d8172c11478ab11b0f5ad49bdb4f93f4b3d5e0d", + "version" : "1.1.1" + } + } + ], + "version" : 2 +} diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index a1edbe7388..e2dc908671 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -27,10 +27,11 @@ let package = Package( ], products: [ .library(name: "NetworkProtectionIPC", targets: ["NetworkProtectionIPC"]), + .library(name: "NetworkProtectionProxy", targets: ["NetworkProtectionProxy"]), .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems") @@ -50,6 +51,19 @@ let package = Package( plugins: [.plugin(name: "SwiftLintPlugin", package: "BrowserServicesKit")] ), + // MARK: - NetworkProtectionProxy + + .target( + name: "NetworkProtectionProxy", + dependencies: [ + .product(name: "NetworkProtection", package: "BrowserServicesKit") + ], + swiftSettings: [ + .define("DEBUG", .when(configuration: .debug)) + ], + plugins: [.plugin(name: "SwiftLintPlugin", package: "BrowserServicesKit")] + ), + // MARK: - NetworkProtectionUI .target( diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/FlowManagers/TCPFlowManager.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/FlowManagers/TCPFlowManager.swift new file mode 100644 index 0000000000..882eb19734 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/FlowManagers/TCPFlowManager.swift @@ -0,0 +1,242 @@ +// +// TCPFlowManager.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 +import NetworkExtension +import OSLog // swiftlint:disable:this enforce_os_log_wrapper + +/// A private global actor to handle UDP flows management +/// +@globalActor +struct TCPFlowActor { + actor ActorType { } + + static let shared: ActorType = ActorType() +} + +@TCPFlowActor +enum RemoteConnectionError: Error { + case complete + case cancelled + case couldNotEstablishConnection(_ error: Error) + case unhandledError(_ error: Error) +} + +final class TCPFlowManager { + private let flow: NEAppProxyTCPFlow + private var connectionTask: Task? + private var connection: NWConnection? + + init(flow: NEAppProxyTCPFlow) { + self.flow = flow + } + + deinit { + // Just making extra sure we don't have any unexpected retain cycle + connection?.stateUpdateHandler = nil + connection?.cancel() + } + + func start(interface: NWInterface) async throws { + guard let remoteEndpoint = flow.remoteEndpoint as? NWHostEndpoint else { + return + } + + try await connectAndStartRunLoop(remoteEndpoint: remoteEndpoint, interface: interface) + } + + private func connectAndStartRunLoop(remoteEndpoint: NWHostEndpoint, interface: NWInterface) async throws { + let remoteConnection = try await connect(to: remoteEndpoint, interface: interface) + try await flow.open(withLocalEndpoint: nil) + + do { + try await startDataCopyLoop(for: remoteConnection) + + remoteConnection.cancel() + flow.closeReadWithError(nil) + flow.closeWriteWithError(nil) + } catch { + remoteConnection.cancel() + flow.closeReadWithError(error) + flow.closeWriteWithError(error) + } + } + + func connect(to remoteEndpoint: NWHostEndpoint, interface: NWInterface) async throws -> NWConnection { + try await withCheckedThrowingContinuation { continuation in + connect(to: remoteEndpoint, interface: interface) { result in + switch result { + case .success(let connection): + continuation.resume(returning: connection) + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + + func connect(to remoteEndpoint: NWHostEndpoint, interface: NWInterface, completion: @escaping @TCPFlowActor (Result) -> Void) { + let host = Network.NWEndpoint.Host(remoteEndpoint.hostname) + let port = Network.NWEndpoint.Port(remoteEndpoint.port)! + + let parameters = NWParameters.tcp + parameters.preferNoProxies = true + parameters.requiredInterface = interface + parameters.prohibitedInterfaceTypes = [.other] + + let connection = NWConnection(host: host, port: port, using: parameters) + self.connection = connection + + connection.stateUpdateHandler = { (state: NWConnection.State) in + Task { @TCPFlowActor in + switch state { + case .ready: + connection.stateUpdateHandler = nil + completion(.success(connection)) + case .cancelled: + connection.stateUpdateHandler = nil + completion(.failure(RemoteConnectionError.cancelled)) + case .failed(let error), .waiting(let error): + connection.stateUpdateHandler = nil + completion(.failure(RemoteConnectionError.couldNotEstablishConnection(error))) + default: + break + } + } + } + + connection.start(queue: .global()) + } + + private func startDataCopyLoop(for remoteConnection: NWConnection) async throws { + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { [weak self] in + while true { + guard let self else { + throw RemoteConnectionError.cancelled + } + + try Task.checkCancellation() + try await self.copyOutboundTraffic(to: remoteConnection) + } + } + + group.addTask { [weak self] in + while true { + guard let self else { + throw RemoteConnectionError.cancelled + } + + try Task.checkCancellation() + try await self.copyInboundTraffic(from: remoteConnection) + } + } + + while !group.isEmpty { + do { + try await group.next() + + } catch { + group.cancelAll() + throw error + } + } + } + } + + @MainActor + func closeFlow(remoteConnection: NWConnection, error: Error?) { + remoteConnection.forceCancel() + flow.closeReadWithError(error) + flow.closeWriteWithError(error) + } + + static let maxReceiveSize: Int = Int(Measurement(value: 2, unit: UnitInformationStorage.megabytes).converted(to: .bytes).value) + + func copyInboundTraffic(from remoteConnection: NWConnection) async throws { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + Task { @TCPFlowActor in + remoteConnection.receive(minimumIncompleteLength: 1, + maximumLength: Self.maxReceiveSize) { [weak flow] (data, _, isComplete, error) in + guard let flow else { + continuation.resume(throwing: RemoteConnectionError.cancelled) + return + } + + switch (data, isComplete, error) { + case (.some(let data), _, _) where !data.isEmpty: + flow.write(data) { writeError in + if let writeError { + continuation.resume(throwing: writeError) + remoteConnection.cancel() + } else { + continuation.resume() + } + } + case (_, isComplete, _) where isComplete == true: + continuation.resume(throwing: RemoteConnectionError.complete) + remoteConnection.cancel() + case (_, _, .some(let error)): + continuation.resume(throwing: RemoteConnectionError.unhandledError(error)) + remoteConnection.cancel() + default: + continuation.resume(throwing: RemoteConnectionError.complete) + remoteConnection.cancel() + } + } + } + } + } + + func copyOutboundTraffic(to remoteConnection: NWConnection) async throws { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + Task { @TCPFlowActor in + flow.readData { data, error in + switch (data, error) { + case (.some(let data), _) where !data.isEmpty: + remoteConnection.send(content: data, completion: .contentProcessed({ error in + if let error { + continuation.resume(throwing: error) + remoteConnection.cancel() + return + } + + continuation.resume() + })) + case (_, .some(let error)): + continuation.resume(throwing: error) + remoteConnection.cancel() + default: + continuation.resume(throwing: RemoteConnectionError.complete) + remoteConnection.cancel() + } + } + } + } + } +} + +extension TCPFlowManager: Hashable { + static func == (lhs: TCPFlowManager, rhs: TCPFlowManager) -> Bool { + lhs === rhs + } + + func hash(into hasher: inout Hasher) { + hasher.combine(flow) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/FlowManagers/UDPFlowManager.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/FlowManagers/UDPFlowManager.swift new file mode 100644 index 0000000000..000f37d20e --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/FlowManagers/UDPFlowManager.swift @@ -0,0 +1,329 @@ +// +// UDPFlowManager.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 +import NetworkExtension +import OSLog // swiftlint:disable:this enforce_os_log_wrapper + +/// A private global actor to handle UDP flows management +/// +@globalActor +struct UDPFlowActor { + actor ActorType { } + + static let shared: ActorType = ActorType() +} + +/// Class to handle UDP connections +/// +/// This is necessary because as described in the reference comment for this implementation (see ``UDPFlowManager``'s documentation) +/// it's noted that a single UDP flow can have to manage multiple connections. +/// +@UDPFlowActor +final class UDPConnectionManager { + let endpoint: NWEndpoint + private let connection: NWConnection + private let onReceive: (_ endpoint: NWEndpoint, _ result: Result) async -> Void + + init(endpoint: NWHostEndpoint, interface: NWInterface?, onReceive: @UDPFlowActor @escaping (_ endpoint: NWEndpoint, _ result: Result) async -> Void) { + let host = Network.NWEndpoint.Host(endpoint.hostname) + let port = Network.NWEndpoint.Port(endpoint.port)! + + let parameters = NWParameters.udp + parameters.preferNoProxies = true + parameters.requiredInterface = interface + parameters.prohibitedInterfaceTypes = [.other] + + let connection = NWConnection(host: host, port: port, using: parameters) + + self.connection = connection + self.endpoint = endpoint + self.onReceive = onReceive + } + + deinit { + // Just making extra sure we don't retain anything we don't need to + connection.stateUpdateHandler = nil + connection.cancel() + } + + // MARK: - General Operation + + /// Starts the operation of this connection manager + /// + /// Can be called multiple times safely. + /// + private func start() async throws { + guard connection.state == .setup else { + return + } + + try await connect() + + Task { + while true { + do { + let datagram = try await receive() + await onReceive(endpoint, .success(datagram)) + } catch { + connection.cancel() + await onReceive(endpoint, .failure(error)) + break + } + } + } + } + + // MARK: - Connection Management + + private func connect() async throws { + try await withCheckedThrowingContinuation { continuation in + connect { result in + switch result { + case .success: + continuation.resume() + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + + private func connect(completion: @escaping (Result) -> Void) { + connection.stateUpdateHandler = { [connection] (state: NWConnection.State) in + switch state { + case .ready: + connection.stateUpdateHandler = nil + completion(.success(())) + case .cancelled: + connection.stateUpdateHandler = nil + completion(.failure(RemoteConnectionError.cancelled)) + case .failed(let error), .waiting(let error): + connection.stateUpdateHandler = nil + completion(.failure(RemoteConnectionError.couldNotEstablishConnection(error))) + default: + break + } + } + + connection.start(queue: .global()) + } + + // MARK: - Receiving from remote + + private func receive() async throws -> Data { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + connection.receiveMessage { [weak self] data, _, isComplete, error in + + guard self != nil else { + continuation.resume(throwing: RemoteConnectionError.cancelled) + return + } + + switch (data, isComplete, error) { + case (let data?, _, _): + continuation.resume(returning: data) + case (_, true, _): + continuation.resume(throwing: RemoteConnectionError.cancelled) + case (_, _, let error?): + continuation.resume(throwing: RemoteConnectionError.unhandledError(error)) + default: + continuation.resume(throwing: RemoteConnectionError.cancelled) + } + } + } + } + + // MARK: - Writing datagrams + + func write(datagram: Data) async throws { + try await start() + + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + connection.send(content: datagram, completion: .contentProcessed({ error in + if let error { + continuation.resume(throwing: error) + return + } + + continuation.resume() + })) + } + } +} + +extension UDPConnectionManager: Hashable, Equatable { + // MARK: - Equatable + + static func == (lhs: UDPConnectionManager, rhs: UDPConnectionManager) -> Bool { + lhs.endpoint == rhs.endpoint + } + + // MARK: - Hashable + + func hash(into hasher: inout Hasher) { + hasher.combine(endpoint) + } +} + +/// UDP flow manager class +/// +/// There is documentation explaining how to handle TCP flows here: +/// https://developer.apple.com/documentation/networkextension/app_proxy_provider/handling_flow_copying?changes=_8 +/// +/// Unfortunately there isn't good official documentation showcasing how to implement UDP flow management. +/// The best we could fine are two comments by an Apple engineer that shine some light on how that implementation should be like: +/// https://developer.apple.com/forums/thread/678464?answerId=671531022#671531022 +/// https://developer.apple.com/forums/thread/678464?answerId=671892022#671892022 +/// +/// This class is the result of implementing the description found in that comment. +/// +@UDPFlowActor +final class UDPFlowManager { + private let flow: NEAppProxyUDPFlow + private var interface: NWInterface? + + private var connectionManagers = [NWEndpoint: UDPConnectionManager]() + + init(flow: NEAppProxyUDPFlow) { + self.flow = flow + } + + func start(interface: NWInterface) async throws { + self.interface = interface + try await connectAndStartRunLoop() + } + + private func connectAndStartRunLoop() async throws { + do { + try await flow.open(withLocalEndpoint: nil) + try await startDataCopyLoop() + + flow.closeReadWithError(nil) + flow.closeWriteWithError(nil) + } catch { + flow.closeReadWithError(error) + flow.closeWriteWithError(error) + } + } + + private func startDataCopyLoop() async throws { + while true { + try await copyOutoundTraffic() + } + } + + func copyInboundTraffic(endpoint: NWEndpoint, result: Result) async { + switch result { + case .success(let data): + do { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + flow.writeDatagrams([data], sentBy: [endpoint]) { error in + if let error { + continuation.resume(throwing: error) + return + } + + continuation.resume() + } + } + } catch { + // Any failure means we close the connection + connectionManagers.removeValue(forKey: endpoint) + } + case .failure: + // Any failure means we close the connection + connectionManagers.removeValue(forKey: endpoint) + } + } + + func copyOutoundTraffic() async throws { + let (datagrams, endpoints) = try await read() + + // Ref: https://developer.apple.com/documentation/networkextension/neappproxyudpflow/1406576-readdatagrams + if datagrams.isEmpty || endpoints.isEmpty { + throw NEAppProxyFlowError(.aborted) + } + + for (datagram, endpoint) in zip(datagrams, endpoints) { + guard let endpoint = endpoint as? NWHostEndpoint else { + // Not sure what to do about this... + continue + } + + let manager = connectionManagers[endpoint] ?? { + let manager = UDPConnectionManager(endpoint: endpoint, interface: interface, onReceive: copyInboundTraffic) + connectionManagers[endpoint] = manager + return manager + }() + + do { + try await manager.write(datagram: datagram) + } catch { + // Any failure means we close the connection + connectionManagers.removeValue(forKey: endpoint) + } + } + } + + /// Reads datagrams from the flow. + /// + /// Apple's documentation is very bad here, but it seems each datagram is corresponded with an endpoint at the same position in the array + /// as mentioned here: https://developer.apple.com/forums/thread/75893 + /// + private func read() async throws -> (datagrams: [Data], endpoints: [NWEndpoint]) { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<([Data], [NWEndpoint]), Error>) in + flow.readDatagrams { datagrams, endpoints, error in + if let error { + continuation.resume(throwing: error) + return + } + + guard let datagrams, let endpoints else { + continuation.resume(throwing: NEAppProxyFlowError(.aborted)) + return + } + + continuation.resume(returning: (datagrams, endpoints)) + } + } + } + + private func send(datagram: Data, through remoteConnection: NWConnection) async throws { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + remoteConnection.send(content: datagram, completion: .contentProcessed({ error in + if let error { + continuation.resume(throwing: error) + return + } + + continuation.resume() + })) + } + } +} + +extension UDPFlowManager: Hashable { + static func == (lhs: UDPFlowManager, rhs: UDPFlowManager) -> Bool { + lhs === rhs + } + + func hash(into hasher: inout Hasher) { + hasher.combine(flow) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/IPC/TransparentProxyAppMessageHandler.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/IPC/TransparentProxyAppMessageHandler.swift new file mode 100644 index 0000000000..12339a673d --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/IPC/TransparentProxyAppMessageHandler.swift @@ -0,0 +1,82 @@ +// +// TransparentProxyAppMessageHandler.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import OSLog // swiftlint:disable:this enforce_os_log_wrapper + +/// Handles app messages +/// +final class TransparentProxyAppMessageHandler { + + private let settings: TransparentProxySettings + + init(settings: TransparentProxySettings) { + self.settings = settings + } + + func handle(_ data: Data) async -> Data? { + do { + let message = try JSONDecoder().decode(TransparentProxyMessage.self, from: data) + return await handle(message) + } catch { + return nil + } + } + + /// Handles a message. + /// + /// This method will wrap the message into a request with a completion handler, and will process it. + /// The reason why this method wraps the message in a request is to ensure that the response + /// type stays syncrhonized between app and provider. + /// + private func handle(_ message: TransparentProxyMessage) async -> Data? { + await withCheckedContinuation { continuation in + var request: TransparentProxyRequest + + switch message { + case .changeSetting(let change): + request = .changeSetting(change, responseHandler: { + continuation.resume(returning: nil) + }) + } + + handle(request) + } + } + + /// Handles a request and calls the response handler when done. + /// + private func handle(_ request: TransparentProxyRequest) { + switch request { + case .changeSetting(let change, let responseHandler): + handle(change) + responseHandler() + } + } + + /// Handles a settings change. + /// + private func handle(_ settingChange: TransparentProxySettings.Change) { + switch settingChange { + case .appRoutingRules(let routingRules): + settings.appRoutingRules = routingRules + case .excludedDomains(let excludedDomains): + settings.excludedDomains = excludedDomains + } + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/IPC/TransparentProxyRequest.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/IPC/TransparentProxyRequest.swift new file mode 100644 index 0000000000..0881dba3b1 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/IPC/TransparentProxyRequest.swift @@ -0,0 +1,67 @@ +// +// TransparentProxyRequest.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import NetworkExtension + +public enum TransparentProxyMessage: Codable { + case changeSetting(_ change: TransparentProxySettings.Change) +} + +/// A request for the TransparentProxyProvider. +/// +/// This enum associates a request with a response handler making XPC communication simpler. +/// Once the request completes, `responseHandler` will be called with the result. +/// +public enum TransparentProxyRequest { + case changeSetting(_ settingChange: TransparentProxySettings.Change, responseHandler: () -> Void) + + var message: TransparentProxyMessage { + switch self { + case .changeSetting(let change, _): + return .changeSetting(change) + } + } + + func handleResponse(data: Data?) { + switch self { + case .changeSetting(_, let handleResponse): + handleResponse() + } + } +} + +/// Respresents a transparent proxy session. +/// +/// Offers basic IPC communication support for the app that owns the proxy. This mechanism +/// is implemented through `NETunnelProviderSession` which means only the app that +/// owns the proxy can use this class. +/// +public class TransparentProxySession { + + private let session: NETunnelProviderSession + + init(_ session: NETunnelProviderSession) { + self.session = session + } + + func send(_ request: TransparentProxyRequest) throws { + let payload = try JSONEncoder().encode(request.message) + try session.sendProviderMessage(payload, responseHandler: request.handleResponse(data:)) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyControllerPixel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyControllerPixel.swift new file mode 100644 index 0000000000..b4c9b8cebb --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyControllerPixel.swift @@ -0,0 +1,89 @@ +// +// TransparentProxyControllerPixel.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import PixelKit + +extension TransparentProxyController.StartError: PixelKitEventErrorDetails { + public var underlyingError: Error? { + switch self { + case .failedToLoadConfiguration(let underlyingError), + .failedToSaveConfiguration(let underlyingError), + .failedToStartProvider(let underlyingError): + return underlyingError + default: + return nil + } + } +} + +extension TransparentProxyController { + + public enum Event: PixelKitEventV2 { + case startInitiated + case startSuccess + case startFailure(_ error: Error) + + // MARK: - PixelKit.Event + + public var name: String { + namePrefix + "_" + nameSuffix + } + + public var parameters: [String: String]? { + switch self { + case .startInitiated: + return nil + case .startSuccess: + return nil + case .startFailure: + return nil + } + } + + // MARK: - PixelKit Support + + private static let pixelNamePrefix = "vpn_proxy_controller" + + private var namePrefix: String { + Self.pixelNamePrefix + } + + private var nameSuffix: String { + switch self { + case .startInitiated: + return "start_initiated" + case .startFailure: + return "start_failure" + case .startSuccess: + return "start_success" + } + } + + public var error: Error? { + switch self { + case .startInitiated: + return nil + case .startFailure(let error): + return error + case .startSuccess: + return nil + } + } + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyProviderPixel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyProviderPixel.swift new file mode 100644 index 0000000000..aff7421bea --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyProviderPixel.swift @@ -0,0 +1,93 @@ +// +// TransparentProxyProviderPixel.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import PixelKit + +extension TransparentProxyProvider.StartError: ErrorWithPixelParameters { + public var errorParameters: [String: String] { + switch self { + case .failedToUpdateNetworkSettings(let underlyingError): + return [ + PixelKit.Parameters.underlyingErrorCode: "\((underlyingError as NSError).code)", + PixelKit.Parameters.underlyingErrorDesc: (underlyingError as NSError).domain, + ] + default: + return [:] + } + } +} + +extension TransparentProxyProvider { + + public enum Event: PixelKitEventV2 { + case failedToUpdateNetworkSettings(_ error: Error) + case startInitiated + case startSuccess + case startFailure(_ error: Error) + + private static let pixelNamePrefix = "vpn_proxy_provider" + + private var namePrefix: String { + Self.pixelNamePrefix + } + + private var namePostfix: String { + switch self { + case .failedToUpdateNetworkSettings: + return "failed_to_update_network_settings" + case .startFailure: + return "start_failure" + case .startInitiated: + return "start_initiated" + case .startSuccess: + return "start_success" + } + } + + public var name: String { + namePrefix + "_" + namePostfix + } + + public var parameters: [String: String]? { + switch self { + case.failedToUpdateNetworkSettings: + return nil + case .startFailure: + return nil + case .startInitiated: + return nil + case .startSuccess: + return nil + } + } + + public var error: Error? { + switch self { + case .failedToUpdateNetworkSettings(let error): + return error + case .startInitiated: + return nil + case .startFailure(let error): + return error + case .startSuccess: + return nil + } + } + } +} diff --git a/DuckDuckGoVPN/Bundle+Configuration.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/RoutingRules/VPNAppRoutingRules.swift similarity index 56% rename from DuckDuckGoVPN/Bundle+Configuration.swift rename to LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/RoutingRules/VPNAppRoutingRules.swift index 936c44a4a8..15d4a4e1a1 100644 --- a/DuckDuckGoVPN/Bundle+Configuration.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/RoutingRules/VPNAppRoutingRules.swift @@ -1,7 +1,7 @@ // -// Bundle+Configuration.swift +// VPNAppRoutingRules.swift // -// Copyright © 2023 DuckDuckGo. All rights reserved. +// Copyright © 2024 DuckDuckGo. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -18,14 +18,4 @@ import Foundation -extension Bundle { - private static let networkExtensionBundleIDKey = "SYSEX_BUNDLE_ID" - - var networkExtensionBundleID: String { - guard let bundleID = object(forInfoDictionaryKey: Self.networkExtensionBundleIDKey) as? String else { - fatalError("Info.plist is missing \(Self.networkExtensionBundleIDKey)") - } - - return bundleID - } -} +public typealias VPNAppRoutingRules = [String: VPNRoutingRule] diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/SystemExtensionAndNotificationTargets/NetworkProtectionExtensionMachService.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/RoutingRules/VPNRoutingRule.swift similarity index 59% rename from DuckDuckGo/NetworkProtection/NetworkExtensionTargets/SystemExtensionAndNotificationTargets/NetworkProtectionExtensionMachService.swift rename to LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/RoutingRules/VPNRoutingRule.swift index e90627cbc2..b96a0773cf 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/SystemExtensionAndNotificationTargets/NetworkProtectionExtensionMachService.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/RoutingRules/VPNRoutingRule.swift @@ -1,7 +1,7 @@ // -// NetworkProtectionExtensionMachService.swift +// VPNRoutingRule.swift // -// Copyright © 2023 DuckDuckGo. All rights reserved. +// Copyright © 2024 DuckDuckGo. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -18,14 +18,12 @@ import Foundation -/// Helper methods associated with mach services. +/// Routing rules /// -final class NetworkProtectionExtensionMachService { - - /// Retrieves the mach service name from a network extension bundle. - /// - static func serviceName() -> String { - NetworkProtectionBundle.extensionBundle().machServiceName - } - +/// Note that there's no need for an `ignore` case because that's achieved by not having a rule +/// in the first place. +/// +public enum VPNRoutingRule: Codable, Equatable { + case block + case exclude } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/TransparentProxySettings.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/TransparentProxySettings.swift new file mode 100644 index 0000000000..db010ec2b5 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/TransparentProxySettings.swift @@ -0,0 +1,134 @@ +// +// TransparentProxySettings.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import Foundation + +public final class TransparentProxySettings { + public enum Change: Codable { + case appRoutingRules(_ routingRules: VPNAppRoutingRules) + case excludedDomains(_ excludedDomains: [String]) + } + + let defaults: UserDefaults + + private(set) public lazy var changePublisher: AnyPublisher = { + Publishers.MergeMany( + defaults.vpnProxyAppRoutingRulesPublisher + .dropFirst() + .removeDuplicates() + .map { routingRules in + Change.appRoutingRules(routingRules) + }.eraseToAnyPublisher(), + defaults.vpnProxyExcludedDomainsPublisher + .dropFirst() + .removeDuplicates() + .map { excludedDomains in + Change.excludedDomains(excludedDomains) + }.eraseToAnyPublisher() + ).eraseToAnyPublisher() + }() + + public init(defaults: UserDefaults) { + self.defaults = defaults + } + + // MARK: - Settings + + public var appRoutingRules: VPNAppRoutingRules { + get { + defaults.vpnProxyAppRoutingRules + } + + set { + defaults.vpnProxyAppRoutingRules = newValue + } + } + + public var excludedDomains: [String] { + get { + defaults.vpnProxyExcludedDomains + } + + set { + defaults.vpnProxyExcludedDomains = newValue + } + } + + // MARK: - Reset to factory defaults + + public func resetAll() { + defaults.resetVPNProxyAppRoutingRules() + defaults.resetVPNProxyExcludedDomains() + } + + // MARK: - App routing rules logic + + public func isBlocking(_ appIdentifier: String) -> Bool { + appRoutingRules[appIdentifier] == .block + } + + public func isExcluding(_ appIdentifier: String) -> Bool { + appRoutingRules[appIdentifier] == .exclude + } + + public func toggleBlocking(for appIdentifier: String) { + if isBlocking(appIdentifier) { + appRoutingRules.removeValue(forKey: appIdentifier) + } else { + appRoutingRules[appIdentifier] = .block + } + } + + public func toggleExclusion(for appIdentifier: String) { + if isExcluding(appIdentifier) { + appRoutingRules.removeValue(forKey: appIdentifier) + } else { + appRoutingRules[appIdentifier] = .exclude + } + } + + // MARK: - Snapshot support + + public func snapshot() -> TransparentProxySettingsSnapshot { + .init(appRoutingRules: appRoutingRules, excludedDomains: excludedDomains) + } + + public func apply(_ snapshot: TransparentProxySettingsSnapshot) { + appRoutingRules = snapshot.appRoutingRules + excludedDomains = snapshot.excludedDomains + } +} + +extension TransparentProxySettings: CustomStringConvertible { + public var description: String { + """ + TransparentProxySettings {\n + appRoutingRules: \(appRoutingRules)\n + excludedDomains: \(excludedDomains)\n + } + """ + } +} + +public struct TransparentProxySettingsSnapshot: Codable { + public static let key = "com.duckduckgo.TransparentProxySettingsSnapshot" + + public let appRoutingRules: VPNAppRoutingRules + public let excludedDomains: [String] +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/UserDefaultsExtensions/UserDefaults+excludedApps.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/UserDefaultsExtensions/UserDefaults+excludedApps.swift new file mode 100644 index 0000000000..1090ed1626 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/UserDefaultsExtensions/UserDefaults+excludedApps.swift @@ -0,0 +1,79 @@ +// +// UserDefaults+excludedApps.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import Foundation + +extension UserDefaults { + private var vpnProxyAppRoutingRulesDataKey: String { + "vpnProxyAppRoutingRulesData" + } + + @objc + dynamic var vpnProxyAppRoutingRulesData: Data? { + get { + object(forKey: vpnProxyAppRoutingRulesDataKey) as? Data + } + + set { + guard let newValue, + newValue.count > 0 else { + + removeObject(forKey: vpnProxyAppRoutingRulesDataKey) + return + } + + set(newValue, forKey: vpnProxyAppRoutingRulesDataKey) + } + } + + var vpnProxyAppRoutingRules: VPNAppRoutingRules { + get { + guard let data = vpnProxyAppRoutingRulesData, + let routingRules = try? JSONDecoder().decode(VPNAppRoutingRules.self, from: data) else { + return [:] + } + + return routingRules + } + + set { + if newValue.isEmpty { + vpnProxyAppRoutingRulesData = nil + return + } + + guard let data = try? JSONEncoder().encode(newValue) else { + vpnProxyAppRoutingRulesData = nil + return + } + + vpnProxyAppRoutingRulesData = data + } + } + + var vpnProxyAppRoutingRulesPublisher: AnyPublisher { + publisher(for: \.vpnProxyAppRoutingRulesData).map { [weak self] _ in + self?.vpnProxyAppRoutingRules ?? [:] + }.eraseToAnyPublisher() + } + + func resetVPNProxyAppRoutingRules() { + removeObject(forKey: vpnProxyAppRoutingRulesDataKey) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/UserDefaultsExtensions/UserDefaults+excludedDomains.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/UserDefaultsExtensions/UserDefaults+excludedDomains.swift new file mode 100644 index 0000000000..7500178da7 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/UserDefaultsExtensions/UserDefaults+excludedDomains.swift @@ -0,0 +1,51 @@ +// +// UserDefaults+excludedDomains.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import Foundation + +extension UserDefaults { + private var vpnProxyExcludedDomainsKey: String { + "vpnProxyExcludedDomains" + } + + @objc + dynamic var vpnProxyExcludedDomains: [String] { + get { + object(forKey: vpnProxyExcludedDomainsKey) as? [String] ?? [] + } + + set { + guard newValue.count > 0 else { + + removeObject(forKey: vpnProxyExcludedDomainsKey) + return + } + + set(newValue, forKey: vpnProxyExcludedDomainsKey) + } + } + + var vpnProxyExcludedDomainsPublisher: AnyPublisher<[String], Never> { + publisher(for: \.vpnProxyExcludedDomains).eraseToAnyPublisher() + } + + func resetVPNProxyExcludedDomains() { + removeObject(forKey: vpnProxyExcludedDomainsKey) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyController.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyController.swift new file mode 100644 index 0000000000..fdc7fb3177 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyController.swift @@ -0,0 +1,293 @@ +// +// TransparentProxyController.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import Foundation +import NetworkExtension +import NetworkProtection +import OSLog // swiftlint:disable:this enforce_os_log_wrapper +import PixelKit +import SystemExtensions + +/// Controller for ``TransparentProxyProvider`` +/// +@MainActor +public final class TransparentProxyController { + + public enum StartError: Error { + case attemptToStartWithoutBackingActiveFeatures + case couldNotRetrieveProtocolConfiguration + case couldNotEncodeSettingsSnapshot + case failedToLoadConfiguration(_ error: Error) + case failedToSaveConfiguration(_ error: Error) + case failedToStartProvider(_ error: Error) + } + + public typealias EventCallback = (Event) -> Void + public typealias ManagerSetupCallback = (_ manager: NETransparentProxyManager) async -> Void + + /// Dry mode means this won't really do anything to start or stop the proxy. + /// + /// This is useful for testing. + /// + private let dryMode: Bool + + /// The bundleID of the extension that contains the ``TransparentProxyProvider``. + /// + private let extensionID: String + + /// The event handler + /// + public var eventHandler: EventCallback? + + /// Callback to set up a ``NETransparentProxyManager``. + /// + public let setup: ManagerSetupCallback + + private var internalManager: NETransparentProxyManager? + + /// Whether the proxy settings should be stored in the provider configuration. + /// + /// We recommend setting this to true if the provider is running in a System Extension and can't access + /// shared `TransparentProxySettings`. If the provider is in an App Extension you should instead + /// use a shared `TransparentProxySettings` and set this to false. + /// + private let storeSettingsInProviderConfiguration: Bool + public let settings: TransparentProxySettings + private let notificationCenter: NotificationCenter + private var cancellables = Set() + + // MARK: - Initializers + + /// Default initializer. + /// + /// - Parameters: + /// - extensionID: the bundleID for the extension that contains the ``TransparentProxyProvider``. + /// This class DOES NOT take any responsibility in installing the system extension. It only uses + /// the extensionID to identify the appropriate manager configuration to load / save. + /// - storeSettingsInProviderConfiguration: whether the provider configuration will be used for storing + /// the proxy settings. Should be `true` when using a System Extension and `false` when using + /// an App Extension. + /// - settings: the settings to use for this proxy. + /// - dryMode: whether this class is initialized in dry mode. + /// - setup: a callback that will be called whenever a ``NETransparentProxyManager`` needs + /// to be setup. + /// + public init(extensionID: String, + storeSettingsInProviderConfiguration: Bool, + settings: TransparentProxySettings, + notificationCenter: NotificationCenter = .default, + dryMode: Bool = false, + setup: @escaping ManagerSetupCallback) { + + self.dryMode = dryMode + self.extensionID = extensionID + self.notificationCenter = notificationCenter + self.settings = settings + self.setup = setup + self.storeSettingsInProviderConfiguration = storeSettingsInProviderConfiguration + + subscribeToProviderConfigurationChanges() + subscribeToSettingsChanges() + } + + // MARK: - Relay Settings Changes + + private func subscribeToProviderConfigurationChanges() { + notificationCenter.publisher(for: .NEVPNConfigurationChange) + .receive(on: DispatchQueue.main) + .sink { _ in + self.reloadProviderConfiguration() + } + .store(in: &cancellables) + } + + private func reloadProviderConfiguration() { + Task { @MainActor in + try? await self.manager?.loadFromPreferences() + } + } + + private func subscribeToSettingsChanges() { + settings.changePublisher + .receive(on: DispatchQueue.main) + .sink(receiveValue: relay(_:)) + .store(in: &cancellables) + } + + private func relay(_ change: TransparentProxySettings.Change) { + Task { @MainActor in + guard let session = await session else { + return + } + + switch session.status { + case .connected, .connecting, .reasserting: + break + default: + return + } + + try TransparentProxySession(session).send(.changeSetting(change, responseHandler: { + // no-op + })) + } + } + + // MARK: - Setting up NETransparentProxyManager + + /// Loads a saved manager + /// + /// This is a bit of a hack that will be run just once for the instance. The reason we want this to run only once is that + /// `NETransparentProxyManager.loadAllFromPreferences()` has a bug where it triggers status change + /// notifications. If the code trying to retrieve the manager is the result of a notification, we may soon find outselves + /// in an infinite loop. + /// + private var triedLoadingManager = false + + /// Loads the configuration matching our ``extensionID``. + /// + public var manager: NETransparentProxyManager? { + get async { + if let internalManager { + return internalManager + } + + if !triedLoadingManager { + triedLoadingManager = true + + let manager = try? await NETransparentProxyManager.loadAllFromPreferences().first { manager in + (manager.protocolConfiguration as? NETunnelProviderProtocol)?.providerBundleIdentifier == extensionID + } + self.internalManager = manager + } + + return internalManager + } + } + + /// Loads an existing configuration or creates a new one, if one doesn't exist. + /// + /// - Returns a properly configured `NETransparentProxyManager`. + /// + public func loadOrCreateConfiguration() async throws -> NETransparentProxyManager { + let manager = await manager ?? { + let manager = NETransparentProxyManager() + internalManager = manager + return manager + }() + + await setup(manager) + try setupAdditionalProviderConfiguration(manager) + + try await manager.saveToPreferences() + try await manager.loadFromPreferences() + + return manager + } + + private func setupAdditionalProviderConfiguration(_ manager: NETransparentProxyManager) throws { + guard storeSettingsInProviderConfiguration else { + return + } + + guard let providerProtocol = manager.protocolConfiguration as? NETunnelProviderProtocol else { + throw StartError.couldNotRetrieveProtocolConfiguration + } + + var providerConfiguration = providerProtocol.providerConfiguration ?? [String: Any]() + + guard let encodedSettings = try? JSONEncoder().encode(settings.snapshot()), + let encodedSettingsString = String(data: encodedSettings, encoding: .utf8) else { + + throw StartError.couldNotEncodeSettingsSnapshot + } + + providerConfiguration[TransparentProxySettingsSnapshot.key] = encodedSettingsString as NSString + providerProtocol.providerConfiguration = providerConfiguration + + } + + // MARK: - Connection & Session + + public var connection: NEVPNConnection? { + get async { + await manager?.connection + } + } + + public var session: NETunnelProviderSession? { + get async { + guard let manager = await manager, + let session = manager.connection as? NETunnelProviderSession else { + + // The active connection is not running, so there's no session, this is acceptable + return nil + } + + return session + } + } + + // MARK: - Connection + + public var status: NEVPNStatus { + get async { + await connection?.status ?? .disconnected + } + } + + // MARK: - Start & stop the proxy + + public var isRequiredForActiveFeatures: Bool { + settings.appRoutingRules.count > 0 || settings.excludedDomains.count > 0 + } + + public func start() async throws { + eventHandler?(.startInitiated) + + guard isRequiredForActiveFeatures else { + let error = StartError.attemptToStartWithoutBackingActiveFeatures + eventHandler?(.startFailure(error)) + throw error + } + + let manager: NETransparentProxyManager + + do { + manager = try await loadOrCreateConfiguration() + } catch { + eventHandler?(.startFailure(error)) + throw error + } + + do { + try manager.connection.startVPNTunnel(options: [:]) + } catch { + let error = StartError.failedToStartProvider(error) + eventHandler?(.startFailure(error)) + throw error + } + + eventHandler?(.startSuccess) + } + + public func stop() async { + await connection?.stopVPNTunnel() + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyProvider.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyProvider.swift new file mode 100644 index 0000000000..33b75fd73b --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyProvider.swift @@ -0,0 +1,389 @@ +// +// TransparentProxyProvider.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import NetworkExtension +import NetworkProtection +import os.log // swiftlint:disable:this enforce_os_log_wrapper +import SystemConfiguration + +open class TransparentProxyProvider: NETransparentProxyProvider { + + public enum StartError: Error { + case missingProviderConfiguration + case failedToUpdateNetworkSettings(underlyingError: Error) + } + + public typealias EventCallback = (Event) -> Void + public typealias LoadOptionsCallback = (_ options: [String: Any]?) throws -> Void + + static let dnsPort = 53 + + @TCPFlowActor + var tcpFlowManagers = Set() + + @UDPFlowActor + var udpFlowManagers = Set() + + private let monitor = nw_path_monitor_create() + var directInterface: nw_interface_t? + + private let bMonitor = NWPathMonitor() + var interface: NWInterface? + + public let configuration: Configuration + public let settings: TransparentProxySettings + + @MainActor + public var isRunning = false + + public var eventHandler: EventCallback? + private let logger: Logger + + private lazy var appMessageHandler = TransparentProxyAppMessageHandler(settings: settings) + + // MARK: - Init + + public init(settings: TransparentProxySettings, + configuration: Configuration, + logger: Logger) { + + self.configuration = configuration + self.logger = logger + self.settings = settings + + logger.debug("[+] \(String(describing: Self.self), privacy: .public)") + } + + deinit { + logger.debug("[-] \(String(describing: Self.self), privacy: .public)") + } + + private func loadProviderConfiguration() throws { + guard configuration.loadSettingsFromProviderConfiguration else { + return + } + + guard let providerConfiguration = (protocolConfiguration as? NETunnelProviderProtocol)?.providerConfiguration, + let encodedSettingsString = providerConfiguration[TransparentProxySettingsSnapshot.key] as? String, + let encodedSettings = encodedSettingsString.data(using: .utf8) else { + + throw StartError.missingProviderConfiguration + } + + let snapshot = try JSONDecoder().decode(TransparentProxySettingsSnapshot.self, from: encodedSettings) + settings.apply(snapshot) + } + + @MainActor + public func updateNetworkSettings() async throws { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + Task { @MainActor in + let networkSettings = makeNetworkSettings() + logger.log("Updating network settings: \(String(describing: networkSettings), privacy: .public)") + + setTunnelNetworkSettings(networkSettings) { [eventHandler, logger] error in + if let error { + logger.error("Failed to update network settings: \(String(describing: error), privacy: .public)") + eventHandler?(.failedToUpdateNetworkSettings(error)) + continuation.resume(throwing: error) + return + } + + logger.log("Successfully Updated network settings: \(String(describing: error), privacy: .public))") + continuation.resume() + } + } + } + } + + private func makeNetworkSettings() -> NETransparentProxyNetworkSettings { + let networkSettings = NETransparentProxyNetworkSettings(tunnelRemoteAddress: "127.0.0.1") + + networkSettings.includedNetworkRules = [ + NENetworkRule(remoteNetwork: NWHostEndpoint(hostname: "127.0.0.1", port: ""), remotePrefix: 0, localNetwork: nil, localPrefix: 0, protocol: .any, direction: .outbound) + ] + + return networkSettings + } + + override public func startProxy(options: [String: Any]?, + completionHandler: @escaping (Error?) -> Void) { + + eventHandler?(.startInitiated) + + logger.log( + """ + Starting proxy\n + > configuration: \(String(describing: self.configuration), privacy: .public)\n + > settings: \(String(describing: self.settings), privacy: .public)\n + > options: \(String(describing: options), privacy: .public) + """) + + do { + try loadProviderConfiguration() + } catch { + logger.error("Failed to load provider configuration, bailing out") + eventHandler?(.startFailure(error)) + completionHandler(error) + return + } + + Task { @MainActor in + do { + startMonitoringNetworkInterfaces() + + try await updateNetworkSettings() + logger.log("Proxy started successfully") + isRunning = true + eventHandler?(.startSuccess) + completionHandler(nil) + } catch { + let error = StartError.failedToUpdateNetworkSettings(underlyingError: error) + logger.error("Proxy failed to start \(String(reflecting: error), privacy: .public)") + eventHandler?(.startFailure(error)) + completionHandler(error) + } + } + } + + override public func stopProxy(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { + logger.log("Stopping proxy with reason: \(String(reflecting: reason), privacy: .public)") + + Task { @MainActor in + stopMonitoringNetworkInterfaces() + isRunning = false + completionHandler() + } + } + + override public func sleep(completionHandler: @escaping () -> Void) { + Task { @MainActor in + stopMonitoringNetworkInterfaces() + logger.log("The proxy is now sleeping") + completionHandler() + } + } + + override public func wake() { + Task { @MainActor in + logger.log("The proxy is now awake") + startMonitoringNetworkInterfaces() + } + } + + override public func handleNewFlow(_ flow: NEAppProxyFlow) -> Bool { + guard let flow = flow as? NEAppProxyTCPFlow else { + logger.info("Expected a TCP flow, but got something else. We're ignoring it.") + return false + } + + guard let remoteEndpoint = flow.remoteEndpoint as? NWHostEndpoint, + !isDnsServer(remoteEndpoint) else { + return false + } + + let printableRemote = flow.remoteHostname ?? (flow.remoteEndpoint as? NWHostEndpoint)?.hostname ?? "unknown" + + logger.debug( + """ + [TCP] New flow: \(String(describing: flow), privacy: .public) + - remote: \(printableRemote, privacy: .public) + - flowID: \(String(describing: flow.metaData.filterFlowIdentifier?.uuidString), privacy: .public) + - appID: \(String(describing: flow.metaData.sourceAppSigningIdentifier), privacy: .public) + """) + + guard let interface else { + logger.error("[TCP: \(String(describing: flow), privacy: .public)] Expected an interface to exclude traffic through") + return false + } + + switch path(for: flow) { + case .block(let reason): + switch reason { + case .appRule: + logger.debug("[TCP: \(String(describing: flow), privacy: .public)] Blocking traffic due to app rule") + case .domainRule: + logger.debug("[TCP: \(String(describing: flow), privacy: .public)] Blocking traffic due to domain rule") + } + case .excludeFromVPN(let reason): + switch reason { + case .appRule: + logger.debug("[TCP: \(String(describing: flow), privacy: .public)] Excluding traffic due to app rule") + case .domainRule: + logger.debug("[TCP: \(String(describing: flow), privacy: .public)] Excluding traffic due to domain rule") + } + case .routeThroughVPN: + return false + } + + flow.networkInterface = directInterface + + Task { @TCPFlowActor in + let flowManager = TCPFlowManager(flow: flow) + tcpFlowManagers.insert(flowManager) + + try? await flowManager.start(interface: interface) + tcpFlowManagers.remove(flowManager) + } + + return true + } + + override public func handleNewUDPFlow(_ flow: NEAppProxyUDPFlow, initialRemoteEndpoint remoteEndpoint: NWEndpoint) -> Bool { + + guard let remoteEndpoint = remoteEndpoint as? NWHostEndpoint, + !isDnsServer(remoteEndpoint) else { + return false + } + + let printableRemote = remoteEndpoint.hostname + + logger.log( + """ + [UDP] New flow: \(String(describing: flow), privacy: .public) + - remote: \(printableRemote, privacy: .public) + - flowID: \(String(describing: flow.metaData.filterFlowIdentifier?.uuidString), privacy: .public) + - appID: \(String(describing: flow.metaData.sourceAppSigningIdentifier), privacy: .public) + """) + + guard let interface else { + logger.error("[UDP: \(String(describing: flow), privacy: .public)] Expected an interface to exclude traffic through") + return false + } + + switch path(for: flow) { + case .block(let reason): + switch reason { + case .appRule: + logger.debug("[UDP: \(String(describing: flow), privacy: .public)] Blocking traffic due to app rule") + case .domainRule: + logger.debug("[UDP: \(String(describing: flow), privacy: .public)] Blocking traffic due to domain rule") + } + case .excludeFromVPN(let reason): + switch reason { + case .appRule: + logger.debug("[UDP: \(String(describing: flow), privacy: .public)] Excluding traffic due to app rule") + case .domainRule: + logger.debug("[UDP: \(String(describing: flow), privacy: .public)] Excluding traffic due to domain rule") + } + case .routeThroughVPN: + return false + } + + flow.networkInterface = directInterface + + Task { @UDPFlowActor in + let flowManager = UDPFlowManager(flow: flow) + udpFlowManagers.insert(flowManager) + + try? await flowManager.start(interface: interface) + udpFlowManagers.remove(flowManager) + } + + return true + } + + // MARK: - Path Monitors + + @MainActor + private func startMonitoringNetworkInterfaces() { + bMonitor.pathUpdateHandler = { [weak self, logger] path in + logger.log("Available interfaces updated: \(String(reflecting: path.availableInterfaces), privacy: .public)") + + self?.interface = path.availableInterfaces.first { interface in + interface.type != .other + } + } + bMonitor.start(queue: .main) + + nw_path_monitor_set_queue(monitor, .main) + nw_path_monitor_set_update_handler(monitor) { [weak self, logger] path in + guard let self else { return } + + let interfaces = SCNetworkInterfaceCopyAll() + logger.log("Available interfaces updated: \(String(reflecting: interfaces), privacy: .public)") + + nw_path_enumerate_interfaces(path) { interface in + guard nw_interface_get_type(interface) != nw_interface_type_other else { + return true + } + + self.directInterface = interface + return false + } + } + + nw_path_monitor_start(monitor) + } + + @MainActor + private func stopMonitoringNetworkInterfaces() { + bMonitor.cancel() + nw_path_monitor_cancel(monitor) + } + + // MARK: - Ignoring DNS flows + + private func isDnsServer(_ endpoint: NWHostEndpoint) -> Bool { + Int(endpoint.port) == Self.dnsPort + } + + // MARK: - VPN exclusions logic + + private enum FlowPath { + case block(dueTo: Reason) + case excludeFromVPN(dueTo: Reason) + case routeThroughVPN + + enum Reason { + case appRule + case domainRule + } + } + + private func path(for flow: NEAppProxyFlow) -> FlowPath { + let appIdentifier = flow.metaData.sourceAppSigningIdentifier + + switch settings.appRoutingRules[appIdentifier] { + case .none: + if let hostname = flow.remoteHostname, + isExcludedDomain(hostname) { + return .excludeFromVPN(dueTo: .domainRule) + } + + return .routeThroughVPN + case .block: + return .block(dueTo: .appRule) + case .exclude: + return .excludeFromVPN(dueTo: .domainRule) + } + } + + private func isExcludedDomain(_ hostname: String) -> Bool { + settings.excludedDomains.contains { excludedDomain in + hostname.hasSuffix(excludedDomain) + } + } + + // MARK: - Communication with App + + override public func handleAppMessage(_ messageData: Data) async -> Data? { + await appMessageHandler.handle(messageData) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyProviderConfiguration.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyProviderConfiguration.swift new file mode 100644 index 0000000000..3e841faeca --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyProviderConfiguration.swift @@ -0,0 +1,40 @@ +// +// TransparentProxyProviderConfiguration.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +extension TransparentProxyProvider { + /// Configuration to define behaviour for the provider based on the parent process' + /// business domain. + /// + /// This should not be passed in the startup options dictionary. + /// + public struct Configuration { + /// Whether the proxy settings should be loaded from the provider configuration in the startup options. + /// + /// We recommend setting this to true if the provider is running in a System Extension and can't access + /// shared `TransparentProxySettings`. If the provider is in an App Extension you should instead + /// use a shared `TransparentProxySettings` and set this to false. + /// + let loadSettingsFromProviderConfiguration: Bool + + public init(loadSettingsFromProviderConfiguration: Bool) { + self.loadSettingsFromProviderConfiguration = loadSettingsFromProviderConfiguration + } + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift index 66f9bb15ea..2575803866 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift @@ -54,9 +54,11 @@ public struct NetworkProtectionStatusView: View { PromptActionView(model: promptActionViewModel) .padding(.horizontal, 5) .padding(.top, 5) + .transition(.slide) } else { if let healthWarning = model.issueDescription { connectionHealthWarningView(message: healthWarning) + .transition(.slide) } } @@ -67,12 +69,14 @@ public struct NetworkProtectionStatusView: View { if model.showDebugInformation { DebugInformationView(model: DebugInformationViewModel()) + .transition(.slide) } bottomMenuView() } .padding(5) .frame(maxWidth: 350, alignment: .top) + .transition(.slide) } // MARK: - Composite Views diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift index 2a86e999d0..754ca81034 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift @@ -143,9 +143,9 @@ extension NetworkProtectionStatusView { onboardingStatusPublisher .receive(on: DispatchQueue.main) .sink { [weak self] status in - self?.onboardingStatus = status - } - .store(in: &cancellables) + self?.onboardingStatus = status + } + .store(in: &cancellables) } func refreshLoginItemStatus() { @@ -184,14 +184,14 @@ extension NetworkProtectionStatusView { .subscribe(on: Self.tunnelErrorDispatchQueue) .sink { [weak self] errorMessage in - guard let self else { - return - } + guard let self else { + return + } - Task { @MainActor in - self.lastTunnelErrorMessage = errorMessage - } - }.store(in: &cancellables) + Task { @MainActor in + self.lastTunnelErrorMessage = errorMessage + } + }.store(in: &cancellables) } private func subscribeToControllerErrorMessages() { @@ -199,14 +199,14 @@ extension NetworkProtectionStatusView { .subscribe(on: Self.controllerErrorDispatchQueue) .sink { [weak self] errorMessage in - guard let self else { - return - } + guard let self else { + return + } - Task { @MainActor in - self.lastControllerErrorMessage = errorMessage - } - }.store(in: &cancellables) + Task { @MainActor in + self.lastControllerErrorMessage = errorMessage + } + }.store(in: &cancellables) } private func subscribeToDebugInformationChanges() { diff --git a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionProxyTests/TransparentProxyControllerPixelTests.swift b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionProxyTests/TransparentProxyControllerPixelTests.swift new file mode 100644 index 0000000000..bd21fe50d1 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionProxyTests/TransparentProxyControllerPixelTests.swift @@ -0,0 +1,120 @@ +// +// TransparentProxyControllerPixelTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +@testable import NetworkProtectionProxy +import PixelKit +import PixelKitTestingUtilities +import XCTest + +extension TransparentProxyController.Event: Hashable { + public static func == (lhs: NetworkProtectionProxy.TransparentProxyController.Event, rhs: NetworkProtectionProxy.TransparentProxyController.Event) -> Bool { + + lhs.name == rhs.name && lhs.parameters == rhs.parameters + } + + public func hash(into hasher: inout Hasher) { + name.hash(into: &hasher) + parameters.hash(into: &hasher) + } +} + +extension TransparentProxyController.StartError: Hashable { + public static func == (lhs: NetworkProtectionProxy.TransparentProxyController.StartError, rhs: NetworkProtectionProxy.TransparentProxyController.StartError) -> Bool { + + let lhs = lhs as NSError + let rhs = rhs as NSError + + return lhs.code == rhs.code && lhs.domain == rhs.domain + } + + public func hash(into hasher: inout Hasher) { + (self as NSError).hash(into: &hasher) + (underlyingError as? NSError)?.hash(into: &hasher) + } +} + +final class TransparentProxyControllerPixelTests: XCTestCase { + + static let startFailureFullPixelName = "m_mac_vpn_proxy_controller_start_failure" + static let startInitiatedFullPixelName = "m_mac_vpn_proxy_controller_start_initiated" + static let startSuccessFullPixelName = "m_mac_vpn_proxy_controller_start_success" + + enum TestError: PixelKitEventErrorDetails { + case testError + + static let underlyingError = NSError(domain: "test", code: 1) + + var underlyingError: Error? { + Self.underlyingError + } + } + + // MARK: - Test Firing Pixels + + func testFiringPixelsWithoutParameters() { + let tests: [TransparentProxyController.Event: PixelFireExpectations] = [ + .startInitiated: PixelFireExpectations(pixelName: Self.startInitiatedFullPixelName), + .startSuccess: PixelFireExpectations(pixelName: Self.startSuccessFullPixelName) + ] + + for (event, expectations) in tests { + verifyThat(event, + meets: expectations, + file: #filePath, + line: #line) + } + } + + func testFiringStartFailures() { + // Just a convenience method to return the right expectation for each error + func expectaton(forError error: TransparentProxyController.StartError) -> PixelFireExpectations { + switch error { + case .attemptToStartWithoutBackingActiveFeatures, + .couldNotEncodeSettingsSnapshot, + .couldNotRetrieveProtocolConfiguration: + return PixelFireExpectations( + pixelName: Self.startFailureFullPixelName, + error: error) + case .failedToLoadConfiguration(let underlyingError), + .failedToSaveConfiguration(let underlyingError), + .failedToStartProvider(let underlyingError): + return PixelFireExpectations( + pixelName: Self.startFailureFullPixelName, + error: error, + underlyingError: underlyingError) + } + } + + let errors: [TransparentProxyController.StartError] = [ + .attemptToStartWithoutBackingActiveFeatures, + .couldNotEncodeSettingsSnapshot, + .couldNotRetrieveProtocolConfiguration, + .failedToLoadConfiguration(TestError.underlyingError), + .failedToSaveConfiguration(TestError.underlyingError), + .failedToStartProvider(TestError.underlyingError) + ] + + for error in errors { + verifyThat(TransparentProxyController.Event.startFailure(error), + meets: expectaton(forError: error), + file: #filePath, + line: #line) + } + } +} diff --git a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionProxyTests/TransparentProxyProviderPixelTests.swift b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionProxyTests/TransparentProxyProviderPixelTests.swift new file mode 100644 index 0000000000..faf31729a4 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionProxyTests/TransparentProxyProviderPixelTests.swift @@ -0,0 +1,66 @@ +// +// TransparentProxyProviderPixelTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +@testable import NetworkProtectionProxy +import PixelKit +import PixelKitTestingUtilities +import XCTest + +extension TransparentProxyProvider.Event: Hashable { + public static func == (lhs: NetworkProtectionProxy.TransparentProxyProvider.Event, rhs: NetworkProtectionProxy.TransparentProxyProvider.Event) -> Bool { + + lhs.name == rhs.name && lhs.parameters == rhs.parameters + } + + public func hash(into hasher: inout Hasher) { + name.hash(into: &hasher) + parameters.hash(into: &hasher) + } +} + +final class TransparentProxyProviderPixelTests: XCTestCase { + + static let startFailureFullPixelName = "m_mac_vpn_proxy_provider_start_failure" + static let startInitiatedFullPixelName = "m_mac_vpn_proxy_provider_start_initiated" + static let startSuccessFullPixelName = "m_mac_vpn_proxy_provider_start_success" + + enum TestError: Error { + case testError + } + + // MARK: - Test Firing Pixels + + func testFiringPixels() { + let tests: [TransparentProxyProvider.Event: PixelFireExpectations] = [ + .startInitiated: PixelFireExpectations(pixelName: Self.startInitiatedFullPixelName), + .startFailure(TestError.testError): + PixelFireExpectations( + pixelName: Self.startFailureFullPixelName, + error: TestError.testError), + .startSuccess: PixelFireExpectations(pixelName: Self.startSuccessFullPixelName) + ] + + for (event, expectations) in tests { + verifyThat(event, + meets: expectations, + file: #filePath, + line: #line) + } + } +} diff --git a/LocalPackages/PixelKit/Package.swift b/LocalPackages/PixelKit/Package.swift index 61de75cd32..1222931baa 100644 --- a/LocalPackages/PixelKit/Package.swift +++ b/LocalPackages/PixelKit/Package.swift @@ -20,7 +20,7 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), ], targets: [ .target( diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift index ffd9d88796..7a25f133ff 100644 --- a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift +++ b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift @@ -94,11 +94,13 @@ public extension Error { let nsError = self as NSError params[PixelKit.Parameters.errorCode] = "\(nsError.code)" - params[PixelKit.Parameters.errorDesc] = nsError.domain + params[PixelKit.Parameters.errorDomain] = nsError.domain + params[PixelKit.Parameters.errorDesc] = nsError.localizedDescription if let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] as? NSError { params[PixelKit.Parameters.underlyingErrorCode] = "\(underlyingError.code)" - params[PixelKit.Parameters.underlyingErrorDesc] = underlyingError.domain + params[PixelKit.Parameters.underlyingErrorDomain] = underlyingError.domain + params[PixelKit.Parameters.underlyingErrorDesc] = underlyingError.localizedDescription } if let sqlErrorCode = nsError.userInfo["SQLiteResultCode"] as? NSNumber { diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift index b13aea7b17..9c616d0060 100644 --- a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift +++ b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift @@ -275,11 +275,23 @@ public final class PixelKit { newParams = nil } + let newError: Error? + + if let event = event as? PixelKitEventV2 { + // For v2 events we only consider the error specified in the event + // and purposedly ignore the parameter in this call. + // This is to encourage moving the error over to the protocol error + // instead of still relying on the parameter of this call. + newError = event.error + } else { + newError = error + } + fire(pixelNamed: pixelName, frequency: frequency, withHeaders: headers, withAdditionalParameters: newParams, - withError: error, + withError: newError, allowedQueryReservedCharacters: allowedQueryReservedCharacters, includeAppVersionParameter: includeAppVersionParameter, onComplete: onComplete) @@ -365,8 +377,16 @@ extension Dictionary where Key == String, Value == String { self[PixelKit.Parameters.errorCode] = "\(nsError.code)" self[PixelKit.Parameters.errorDomain] = nsError.domain + self[PixelKit.Parameters.errorDesc] = nsError.localizedDescription + + if let error = error as? PixelKitEventErrorDetails, + let underlyingError = error.underlyingError { - if let underlyingError = nsError.userInfo["NSUnderlyingError"] as? NSError { + let underlyingNSError = underlyingError as NSError + self[PixelKit.Parameters.underlyingErrorCode] = "\(underlyingNSError.code)" + self[PixelKit.Parameters.underlyingErrorDomain] = underlyingNSError.domain + self[PixelKit.Parameters.underlyingErrorDesc] = underlyingNSError.localizedDescription + } else 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 { diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEvent.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEvent.swift index 83965ba999..bc87070df3 100644 --- a/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEvent.swift +++ b/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEvent.swift @@ -34,7 +34,7 @@ public final class DebugEvent: PixelKitEvent { } public let eventType: EventType - private let error: Error? + public let error: Error? public init(eventType: EventType, error: Error? = nil) { self.eventType = eventType diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEventV2.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEventV2.swift new file mode 100644 index 0000000000..7048519e32 --- /dev/null +++ b/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEventV2.swift @@ -0,0 +1,70 @@ +// +// PixelKitEventV2.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 + +public protocol PixelKitEventErrorDetails: Error { + var underlyingError: Error? { get } +} + +extension PixelKitEventErrorDetails { + var underlyingErrorParameters: [String: String] { + guard let nsError = underlyingError as? NSError else { + return [:] + } + + return [ + PixelKit.Parameters.underlyingErrorCode: "\(nsError.code)", + PixelKit.Parameters.underlyingErrorDomain: nsError.domain, + PixelKit.Parameters.underlyingErrorDesc: nsError.localizedDescription + ] + } +} + +/// New version of this protocol that allows us to maintain backwards-compatibility with PixelKitEvent +/// +/// This new implementation seeks to unify the handling of standard pixel parameters inside PixelKit. +/// The starting example of how this can be useful is error parameter handling - this protocol allows +/// the implementer to speciy an error without having to know about the parametrization of the error. +/// +/// The reason this wasn't done directly in `PixelKitEvent` is to reduce the risk of breaking existing +/// pixels, and to allow us to migrate towards this incrementally. +/// +public protocol PixelKitEventV2: PixelKitEvent { + var error: Error? { get } +} + +extension PixelKitEventV2 { + var pixelParameters: [String: String] { + guard let error else { + return [:] + } + + let nsError = error as NSError + var parameters = [ + PixelKit.Parameters.errorCode: "\(nsError.code)", + PixelKit.Parameters.errorDomain: nsError.domain, + ] + + if let error = error as? PixelKitEventErrorDetails { + parameters.merge(error.underlyingErrorParameters, uniquingKeysWith: { $1 }) + } + + return parameters + } +} diff --git a/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/PixelFireExpectations.swift b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/PixelFireExpectations.swift new file mode 100644 index 0000000000..067eee091e --- /dev/null +++ b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/PixelFireExpectations.swift @@ -0,0 +1,36 @@ +// +// PixelFireExpectations.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 + +/// Structure containing information about a pixel fire event. +/// +/// This is useful for test validation for libraries that rely on PixelKit, to make sure the pixels contain +/// all of the fields they are supposed to contain.. +/// +public struct PixelFireExpectations { + let pixelName: String + var error: Error? + var underlyingError: Error? + + public init(pixelName: String, error: Error? = nil, underlyingError: Error? = nil) { + self.pixelName = pixelName + self.error = error + self.underlyingError = underlyingError + } +} diff --git a/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift new file mode 100644 index 0000000000..5088ba1371 --- /dev/null +++ b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift @@ -0,0 +1,148 @@ +// +// XCTestCase+PixelKit.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 +@testable import PixelKit +import XCTest + +public extension XCTestCase { + + // MARK: - Parameters + + /// List of standard pixel parameters. + /// + /// This is useful to support filtering these parameters out if needed. + /// + private static var standardPixelParameters = [ + PixelKit.Parameters.appVersion, + PixelKit.Parameters.pixelSource, + PixelKit.Parameters.test + ] + + /// List of errror pixel parameters + /// + private static var errorPixelParameters = [ + PixelKit.Parameters.errorCode, + PixelKit.Parameters.errorDomain, + PixelKit.Parameters.errorDesc + ] + + /// List of underlying error pixel parameters + /// + private static var underlyingErrorPixelParameters = [ + PixelKit.Parameters.underlyingErrorCode, + PixelKit.Parameters.underlyingErrorDomain, + PixelKit.Parameters.underlyingErrorDesc + ] + + /// Filter out the standard parameters. + /// + private static func filterStandardPixelParameters(from parameters: [String: String]) -> [String: String] { + parameters.filter { element in + !standardPixelParameters.contains(element.key) + } + } + + static var pixelPlatformPrefix: String { +#if os(macOS) + return "m_mac_" +#else + // Intentionally left blank for now because PixelKit currently doesn't support + // other platforms, but if we decide to implement another platform this'll fail + // and indicate that we need a value here. +#endif + } + + func expectedParameters(for event: PixelKitEventV2) -> [String: String] { + var expectedParameters = [String: String]() + + if let error = event.error { + let nsError = error as NSError + expectedParameters[PixelKit.Parameters.errorCode] = "\(nsError.code)" + expectedParameters[PixelKit.Parameters.errorDomain] = nsError.domain + expectedParameters[PixelKit.Parameters.errorDesc] = nsError.localizedDescription + + if let underlyingError = (error as? PixelKitEventErrorDetails)?.underlyingError { + let underlyingNSError = underlyingError as NSError + expectedParameters[PixelKit.Parameters.underlyingErrorCode] = "\(underlyingNSError.code)" + expectedParameters[PixelKit.Parameters.underlyingErrorDomain] = underlyingNSError.domain + expectedParameters[PixelKit.Parameters.underlyingErrorDesc] = underlyingNSError.localizedDescription + } + } + + return expectedParameters + } + + // MARK: - Misc Convenience + + private var userDefaults: UserDefaults { + UserDefaults(suiteName: "testing_\(UUID().uuidString)")! + } + + // MARK: - Pixel Firing Expectations + + /// Provides some snapshot of a fired pixel so that external libraries can validate all the expected info is included. + /// + /// This method also checks that there is internal consistency in the expected fields. + /// + func verifyThat(_ event: PixelKitEventV2, meets expectations: PixelFireExpectations, file: StaticString, line: UInt) { + + let expectedPixelName = Self.pixelPlatformPrefix + event.name + let expectedParameters = expectedParameters(for: event) + let callbackExecutedExpectation = expectation(description: "The PixelKit callback has been executed") + + PixelKit.setUp(dryRun: false, + appVersion: "1.0.5", + source: "test-app", + defaultHeaders: [:], + log: .disabled, + defaults: userDefaults) { firedPixelName, _, firedParameters, _, _, completion in + callbackExecutedExpectation.fulfill() + + let firedParameters = Self.filterStandardPixelParameters(from: firedParameters) + + // Internal validations + + XCTAssertEqual(firedPixelName, expectedPixelName, file: file, line: line) + XCTAssertEqual(firedParameters, expectedParameters, file: file, line: line) + + // Expectations + + XCTAssertEqual(firedPixelName, expectations.pixelName) + + if let error = expectations.error { + let nsError = error as NSError + XCTAssertEqual(firedParameters[PixelKit.Parameters.errorCode], String(nsError.code), file: file, line: line) + XCTAssertEqual(firedParameters[PixelKit.Parameters.errorDomain], nsError.domain, file: file, line: line) + XCTAssertEqual(firedParameters[PixelKit.Parameters.errorDesc], nsError.localizedDescription, file: file, line: line) + } + + if let underlyingError = expectations.underlyingError { + let nsError = underlyingError as NSError + XCTAssertEqual(firedParameters[PixelKit.Parameters.underlyingErrorCode], String(nsError.code), file: file, line: line) + XCTAssertEqual(firedParameters[PixelKit.Parameters.underlyingErrorDomain], nsError.domain, file: file, line: line) + XCTAssertEqual(firedParameters[PixelKit.Parameters.underlyingErrorDesc], nsError.localizedDescription, file: file, line: line) + } + + completion(true, nil) + } + + PixelKit.fire(event) + waitForExpectations(timeout: 0.1) + } +} diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 197a2c4eee..e5192fc149 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/LocalPackages/SwiftUIExtensions/Package.swift b/LocalPackages/SwiftUIExtensions/Package.swift index 6f8c716bc3..10d667a750 100644 --- a/LocalPackages/SwiftUIExtensions/Package.swift +++ b/LocalPackages/SwiftUIExtensions/Package.swift @@ -11,7 +11,7 @@ let package = Package( .library(name: "PreferencesViews", targets: ["PreferencesViews"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), ], targets: [ .target( diff --git a/LocalPackages/SyncUI/Package.swift b/LocalPackages/SyncUI/Package.swift index 678077f716..6da8332d6a 100644 --- a/LocalPackages/SyncUI/Package.swift +++ b/LocalPackages/SyncUI/Package.swift @@ -14,7 +14,7 @@ let package = Package( ], dependencies: [ .package(path: "../SwiftUIExtensions"), - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), ], targets: [ .target( diff --git a/LocalPackages/SystemExtensionManager/Package.swift b/LocalPackages/SystemExtensionManager/Package.swift index 188a15c3ad..43bafef378 100644 --- a/LocalPackages/SystemExtensionManager/Package.swift +++ b/LocalPackages/SystemExtensionManager/Package.swift @@ -16,7 +16,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. diff --git a/LocalPackages/XPCHelper/Package.swift b/LocalPackages/XPCHelper/Package.swift index fb49a79f25..e62141ec7a 100644 --- a/LocalPackages/XPCHelper/Package.swift +++ b/LocalPackages/XPCHelper/Package.swift @@ -30,7 +30,7 @@ let package = Package( .library(name: "XPCHelper", targets: ["XPCHelper"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), ], targets: [ .target( diff --git a/NetworkProtectionSystemExtension/Info.plist b/NetworkProtectionSystemExtension/Info.plist index c35c8be1ca..43844d5702 100644 --- a/NetworkProtectionSystemExtension/Info.plist +++ b/NetworkProtectionSystemExtension/Info.plist @@ -14,6 +14,8 @@ com.apple.networkextension.packet-tunnel $(PRODUCT_MODULE_NAME).MacPacketTunnelProvider + com.apple.networkextension.app-proxy + $(PRODUCT_MODULE_NAME).MacTransparentProxyProvider MAIN_BUNDLE_IDENTIFIER $(MAIN_BUNDLE_IDENTIFIER) diff --git a/NetworkProtectionSystemExtension/NetworkProtectionSystemExtension_Debug.entitlements b/NetworkProtectionSystemExtension/NetworkProtectionSystemExtension_Debug.entitlements index a049fa6886..4252e67c8e 100644 --- a/NetworkProtectionSystemExtension/NetworkProtectionSystemExtension_Debug.entitlements +++ b/NetworkProtectionSystemExtension/NetworkProtectionSystemExtension_Debug.entitlements @@ -5,6 +5,7 @@ com.apple.developer.networking.networkextension packet-tunnel-provider + app-proxy-provider com.apple.security.app-sandbox diff --git a/NetworkProtectionSystemExtension/NetworkProtectionSystemExtension_Release.entitlements b/NetworkProtectionSystemExtension/NetworkProtectionSystemExtension_Release.entitlements index f7d87546d2..23068f001f 100644 --- a/NetworkProtectionSystemExtension/NetworkProtectionSystemExtension_Release.entitlements +++ b/NetworkProtectionSystemExtension/NetworkProtectionSystemExtension_Release.entitlements @@ -5,6 +5,7 @@ com.apple.developer.networking.networkextension packet-tunnel-provider-systemextension + app-proxy-provider-systemextension com.apple.security.app-sandbox diff --git a/VPNProxyExtension/Info.plist b/VPNProxyExtension/Info.plist new file mode 100644 index 0000000000..7f2489c298 --- /dev/null +++ b/VPNProxyExtension/Info.plist @@ -0,0 +1,17 @@ + + + + + DISTRIBUTED_NOTIFICATIONS_PREFIX + $(DISTRIBUTED_NOTIFICATIONS_PREFIX) + NETP_APP_GROUP + $(NETP_APP_GROUP) + NSExtension + + NSExtensionPointIdentifier + com.apple.networkextension.app-proxy + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).MacTransparentProxyProvider + + + diff --git a/VPNProxyExtension/VPNProxyExtension.entitlements b/VPNProxyExtension/VPNProxyExtension.entitlements new file mode 100644 index 0000000000..968c758f97 --- /dev/null +++ b/VPNProxyExtension/VPNProxyExtension.entitlements @@ -0,0 +1,25 @@ + + + + + com.apple.security.application-groups + + $(TeamIdentifierPrefix)com.duckduckgo.macos.browser.network-protection + $(NETP_APP_GROUP) + + com.apple.security.app-sandbox + + com.apple.security.network.server + + keychain-access-groups + + $(NETP_APP_GROUP) + + com.apple.developer.networking.networkextension + + app-proxy-provider + + com.apple.security.network.client + + + diff --git a/fastlane/Matchfile b/fastlane/Matchfile index 8dbddfccb8..2af9eed1fe 100644 --- a/fastlane/Matchfile +++ b/fastlane/Matchfile @@ -11,6 +11,8 @@ app_identifier [ "com.duckduckgo.mobile.ios.review", "com.duckduckgo.mobile.ios.vpn.agent.review", "com.duckduckgo.mobile.ios.vpn.agent.review.network-protection-extension", + "com.duckduckgo.mobile.ios.vpn.agent.proxy", + "com.duckduckgo.mobile.ios.vpn.agent.review.proxy", "com.duckduckgo.mobile.ios.DBP.backgroundAgent.review", "com.duckduckgo.mobile.ios.DBP.backgroundAgent" diff --git a/scripts/assets/AppStoreExportOptions.plist b/scripts/assets/AppStoreExportOptions.plist index b9395f914a..9ebf3d7885 100644 --- a/scripts/assets/AppStoreExportOptions.plist +++ b/scripts/assets/AppStoreExportOptions.plist @@ -14,12 +14,16 @@ match AppStore com.duckduckgo.mobile.ios.vpn.agent macos com.duckduckgo.mobile.ios.vpn.agent.network-protection-extension match AppStore com.duckduckgo.mobile.ios.vpn.agent.network-protection-extension macos + com.duckduckgo.mobile.ios.vpn.agent.proxy + match AppStore com.duckduckgo.mobile.ios.vpn.agent.proxy macos com.duckduckgo.mobile.ios.review match AppStore com.duckduckgo.mobile.ios.review macos com.duckduckgo.mobile.ios.vpn.agent.review match AppStore com.duckduckgo.mobile.ios.vpn.agent.review macos com.duckduckgo.mobile.ios.vpn.agent.review.network-protection-extension match AppStore com.duckduckgo.mobile.ios.vpn.agent.review.network-protection-extension macos + com.duckduckgo.mobile.ios.vpn.agent.review.proxy + match AppStore com.duckduckgo.mobile.ios.vpn.agent.review.proxy macos com.duckduckgo.mobile.ios.DBP.backgroundAgent match AppStore com.duckduckgo.mobile.ios.DBP.backgroundAgent macos com.duckduckgo.mobile.ios.DBP.backgroundAgent.review From 7c2e3e4057681ecacb972eb8d7eecda0f3762c12 Mon Sep 17 00:00:00 2001 From: Sabrina Tardio <44158575+SabrinaTardio@users.noreply.github.com> Date: Thu, 22 Feb 2024 11:01:25 +0100 Subject: [PATCH 42/44] add bundle to sync strings (#2232) Task/Issue URL: https://app.asana.com/0/0/1206643467042714/f **Description**: Adds bundle to sync strings **Steps to test this PR**: 1. Check the app builds as expected --- LocalPackages/SyncUI/Package.swift | 3 + .../Localizable.xcstrings | 8 +- .../Sources/SyncUI/internal/UserText.swift | 197 +++++++++--------- 3 files changed, 106 insertions(+), 102 deletions(-) rename LocalPackages/SyncUI/Sources/SyncUI/{internal => Resources}/Localizable.xcstrings (99%) diff --git a/LocalPackages/SyncUI/Package.swift b/LocalPackages/SyncUI/Package.swift index 6da8332d6a..b927525124 100644 --- a/LocalPackages/SyncUI/Package.swift +++ b/LocalPackages/SyncUI/Package.swift @@ -23,6 +23,9 @@ let package = Package( .product(name: "PreferencesViews", package: "SwiftUIExtensions"), .product(name: "SwiftUIExtensions", package: "SwiftUIExtensions") ], + resources: [ + .process("Resources") + ], swiftSettings: [ .define("DEBUG", .when(configuration: .debug)) ], diff --git a/LocalPackages/SyncUI/Sources/SyncUI/internal/Localizable.xcstrings b/LocalPackages/SyncUI/Sources/SyncUI/Resources/Localizable.xcstrings similarity index 99% rename from LocalPackages/SyncUI/Sources/SyncUI/internal/Localizable.xcstrings rename to LocalPackages/SyncUI/Sources/SyncUI/Resources/Localizable.xcstrings index abcf797258..340db4147a 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/internal/Localizable.xcstrings +++ b/LocalPackages/SyncUI/Sources/SyncUI/Resources/Localizable.xcstrings @@ -209,7 +209,7 @@ } }, "paste-from-clipboard" : { - "comment" : "Paste button", + "comment" : "Paste from Clipboard button", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -245,7 +245,7 @@ } }, "preferences.begin-sync.card-footer" : { - "comment" : "Footer / captoin on the Begin Syncing card in sync settings", + "comment" : "Footer / caption on the Begin Syncing card in sync settings", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -353,7 +353,7 @@ } }, "preferences.preparing-to-sync.dialog-title" : { - "comment" : "Peparing to sync dialog title during sync set up", + "comment" : "Preparing to sync dialog title during sync set up", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -389,7 +389,7 @@ } }, "preferences.recover-synced-data.dialog-subtitle" : { - "comment" : "Recover synced data during Sync revoery process dialog subtitle", + "comment" : "Recover synced data during Sync recovery process dialog subtitle", "extractionState" : "extracted_with_value", "localizations" : { "en" : { diff --git a/LocalPackages/SyncUI/Sources/SyncUI/internal/UserText.swift b/LocalPackages/SyncUI/Sources/SyncUI/internal/UserText.swift index ca0babbe0f..3d9356af3e 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/internal/UserText.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/internal/UserText.swift @@ -21,150 +21,151 @@ import Foundation enum UserText { // Generic Buttons - static let ok = NSLocalizedString("ok", value: "OK", comment: "OK button") - static let notNow = NSLocalizedString("notnow", value: "Not Now", comment: "Not Now button") - static let cancel = NSLocalizedString("cancel", value: "Cancel", comment: "Cancel button") - static let submit = NSLocalizedString("submit", value: "Submit", comment: "Submit button") - static let next = NSLocalizedString("next", value: "Next", comment: "Next button") - static let copy = NSLocalizedString("copy", value: "Copy", comment: "Copy button") - static let share = NSLocalizedString("share", value: "Share", comment: "Share button") - static let paste = NSLocalizedString("paste", value: "Paste", comment: "Paste button") - static let pasteFromClipboard = NSLocalizedString("paste-from-clipboard", value: "Paste from Clipboard", comment: "Paste button") - static let done = NSLocalizedString("done", value: "Done", comment: "Done button") + static let ok = NSLocalizedString("ok", bundle: Bundle.module, value: "OK", comment: "OK button") + static let notNow = NSLocalizedString("notnow", bundle: Bundle.module, value: "Not Now", comment: "Not Now button") + static let cancel = NSLocalizedString("cancel", bundle: Bundle.module, value: "Cancel", comment: "Cancel button") + static let submit = NSLocalizedString("submit", bundle: Bundle.module, value: "Submit", comment: "Submit button") + static let next = NSLocalizedString("next", bundle: Bundle.module, value: "Next", comment: "Next button") + static let copy = NSLocalizedString("copy", bundle: Bundle.module, value: "Copy", comment: "Copy button") + static let share = NSLocalizedString("share", bundle: Bundle.module, value: "Share", comment: "Share button") + static let paste = NSLocalizedString("paste", bundle: Bundle.module, value: "Paste", comment: "Paste button") + static let pasteFromClipboard = NSLocalizedString("paste-from-clipboard", bundle: Bundle.module, value: "Paste from Clipboard", comment: "Paste from Clipboard button") + static let done = NSLocalizedString("done", bundle: Bundle.module, value: "Done", comment: "Done button") // Sync Set Up View // Begin Sync card - static let beginSyncTitle = NSLocalizedString("preferences.begin-sync.card-title", value: "Begin Syncing", comment: "Begin Syncing card title in sync settings") - static let beginSyncDescription = NSLocalizedString("preferences.begin-sync.card-description", value: "Securely sync bookmarks and passwords between your devices.", comment: "Begin Syncing card description in sync settings") - static let beginSyncButton = NSLocalizedString("preferences.begin-sync.card-button", value: "Sync With Another Device", comment: "Button text on the Begin Syncing card in sync settings") - static let beginSyncFooter = NSLocalizedString("preferences.begin-sync.card-footer", value: "Your data is end-to-end encrypted, and DuckDuckGo does not have access to the encryption key.", comment: "Footer / captoin on the Begin Syncing card in sync settings") + static let beginSyncTitle = NSLocalizedString("preferences.begin-sync.card-title", bundle: Bundle.module, value: "Begin Syncing", comment: "Begin Syncing card title in sync settings") + static let beginSyncDescription = NSLocalizedString("preferences.begin-sync.card-description", bundle: Bundle.module, value: "Securely sync bookmarks and passwords between your devices.", comment: "Begin Syncing card description in sync settings") + static let beginSyncButton = NSLocalizedString("preferences.begin-sync.card-button", bundle: Bundle.module, value: "Sync With Another Device", comment: "Button text on the Begin Syncing card in sync settings") + static let beginSyncFooter = NSLocalizedString("preferences.begin-sync.card-footer", bundle: Bundle.module, value: "Your data is end-to-end encrypted, and DuckDuckGo does not have access to the encryption key.", comment: "Footer / caption on the Begin Syncing card in sync settings") + // Options - static let otherOptionsSectionTitle = NSLocalizedString("preferences.other-options.section-title", value: "Other Options", comment: "Sync settings. Other Options section title") - static let syncThisDeviceLink = NSLocalizedString("preferences.sync-this-device.link-title", value: "Sync and Back Up This Device", comment: "Sync settings. Title of a link to start setting up sync and backup the device") - static let recoverDataLink = NSLocalizedString("preferences.recover-data.link-title", value: "Recover Synced Data", comment: "Sync settings. Link to recover synced data.") + static let otherOptionsSectionTitle = NSLocalizedString("preferences.other-options.section-title", bundle: Bundle.module, value: "Other Options", comment: "Sync settings. Other Options section title") + static let syncThisDeviceLink = NSLocalizedString("preferences.sync-this-device.link-title", bundle: Bundle.module, value: "Sync and Back Up This Device", comment: "Sync settings. Title of a link to start setting up sync and backup the device") + static let recoverDataLink = NSLocalizedString("preferences.recover-data.link-title", bundle: Bundle.module, value: "Recover Synced Data", comment: "Sync settings. Link to recover synced data.") // Preparing to sync dialog - static let preparingToSyncDialogTitle = NSLocalizedString("preferences.preparing-to-sync.dialog-title", value: "Preparing To Sync", comment: "Peparing to sync dialog title during sync set up") - static let preparingToSyncDialogSubTitle = NSLocalizedString("preferences.preparing-to-sync.dialog-subtitle", value: "We're setting up the connection to synchronize your bookmarks and saved logins with the other device.", comment: "Preparing to sync dialog subtitle during sync set up") - static let preparingToSyncDialogAction = NSLocalizedString("preferences.preparing-to-sync.dialog-action", value: "Connecting…", comment: "Sync preparing to sync dialog action") + static let preparingToSyncDialogTitle = NSLocalizedString("preferences.preparing-to-sync.dialog-title", bundle: Bundle.module, value: "Preparing To Sync", comment: "Preparing to sync dialog title during sync set up") + static let preparingToSyncDialogSubTitle = NSLocalizedString("preferences.preparing-to-sync.dialog-subtitle", bundle: Bundle.module, value: "We're setting up the connection to synchronize your bookmarks and saved logins with the other device.", comment: "Preparing to sync dialog subtitle during sync set up") + static let preparingToSyncDialogAction = NSLocalizedString("preferences.preparing-to-sync.dialog-action", bundle: Bundle.module, value: "Connecting…", comment: "Sync preparing to sync dialog action") // Enter recovery code dialog - static let enterRecoveryCodeDialogTitle = NSLocalizedString("preferences.enter-recovery-code.dialog-title", value: "Enter Code", comment: "Sync enter recovery code dialog title") - static let enterRecoveryCodeDialogSubtitle = NSLocalizedString("preferences.enter-recovery-code.dialog-subtitle", value: "Enter the code on your Recovery PDF, or another synced device, to recover your synced data.", comment: "Sync enter recovery code dialog subtitle") - static let enterRecoveryCodeDialogAction1 = NSLocalizedString("preferences.enter-recovery-code.dialog-action1", value: "Paste Code Here", comment: "Sync enter recovery code dialog first possible action") - static let enterRecoveryCodeDialogAction2 = NSLocalizedString("preferences.enter-recovery-code.dialog-action2", value: "or scan QR code with a device that is still connected", comment: "Sync enter recovery code dialog second possible action") + static let enterRecoveryCodeDialogTitle = NSLocalizedString("preferences.enter-recovery-code.dialog-title", bundle: Bundle.module, value: "Enter Code", comment: "Sync enter recovery code dialog title") + static let enterRecoveryCodeDialogSubtitle = NSLocalizedString("preferences.enter-recovery-code.dialog-subtitle", bundle: Bundle.module, value: "Enter the code on your Recovery PDF, or another synced device, to recover your synced data.", comment: "Sync enter recovery code dialog subtitle") + static let enterRecoveryCodeDialogAction1 = NSLocalizedString("preferences.enter-recovery-code.dialog-action1", bundle: Bundle.module, value: "Paste Code Here", comment: "Sync enter recovery code dialog first possible action") + static let enterRecoveryCodeDialogAction2 = NSLocalizedString("preferences.enter-recovery-code.dialog-action2", bundle: Bundle.module, value: "or scan QR code with a device that is still connected", comment: "Sync enter recovery code dialog second possible action") // Recover synced data dialog - static let reciverSyncedDataDialogTitle = NSLocalizedString("preferences.recover-synced-data.dialog-title", value: "Recover Synced Data", comment: "Sync recover synced data dialog title") - static let reciverSyncedDataDialogSubitle = NSLocalizedString("preferences.recover-synced-data.dialog-subtitle", value: "To restore your synced data, you'll need the Recovery Code you saved when you first set up Sync. This code may have been saved as a PDF on the device you originally used to set up Sync.", comment: "Recover synced data during Sync revoery process dialog subtitle") - static let reciverSyncedDataDialogButton = NSLocalizedString("preferences.recover-synced-data.dialog-button", value: "Get Started", comment: "Sync recover synced data dialog button") + static let reciverSyncedDataDialogTitle = NSLocalizedString("preferences.recover-synced-data.dialog-title", bundle: Bundle.module, value: "Recover Synced Data", comment: "Sync recover synced data dialog title") + static let reciverSyncedDataDialogSubitle = NSLocalizedString("preferences.recover-synced-data.dialog-subtitle", bundle: Bundle.module, value: "To restore your synced data, you'll need the Recovery Code you saved when you first set up Sync. This code may have been saved as a PDF on the device you originally used to set up Sync.", comment: "Recover synced data during Sync recovery process dialog subtitle") + static let reciverSyncedDataDialogButton = NSLocalizedString("preferences.recover-synced-data.dialog-button", bundle: Bundle.module, value: "Get Started", comment: "Sync recover synced data dialog button") // Sync Title - static let sync = NSLocalizedString("preferences.sync", value: "Sync & Backup", comment: "Show sync preferences") - static let syncRollOutBannerDescription = NSLocalizedString("preferences.sync.rollout-banner.description", value: "Sync & Backup is rolling out gradually and may not be available yet within DuckDuckGo on your other devices.", comment: "Description of rollout banner") + static let sync = NSLocalizedString("preferences.sync", bundle: Bundle.module, value: "Sync & Backup", comment: "Show sync preferences") + static let syncRollOutBannerDescription = NSLocalizedString("preferences.sync.rollout-banner.description", bundle: Bundle.module, value: "Sync & Backup is rolling out gradually and may not be available yet within DuckDuckGo on your other devices.", comment: "Description of rollout banner") - static let turnOff = NSLocalizedString("preferences.sync.turn-off", value: "Turn Off", comment: "Turn off sync confirmation dialog button title") - static let turnOffSync = NSLocalizedString("preferences.sync.turn-off.ellipsis", value: "Turn Off Sync…", comment: "Disable sync button caption") + static let turnOff = NSLocalizedString("preferences.sync.turn-off", bundle: Bundle.module, value: "Turn Off", comment: "Turn off sync confirmation dialog button title") + static let turnOffSync = NSLocalizedString("preferences.sync.turn-off.ellipsis", bundle: Bundle.module, value: "Turn Off Sync…", comment: "Disable sync button caption") // Sync Enabled View // Turn off sync dialog - static let turnOffSyncConfirmTitle = NSLocalizedString("preferences.sync.turn-off.confirm.title", value: "Turn off sync?", comment: "Turn off sync confirmation dialog title") - static let turnOffSyncConfirmMessage = NSLocalizedString("preferences.sync.turn-off.confirm.message", value: "This device will no longer be able to access your synced data.", comment: "Turn off sync confirmation dialog message") + static let turnOffSyncConfirmTitle = NSLocalizedString("preferences.sync.turn-off.confirm.title", bundle: Bundle.module, value: "Turn off sync?", comment: "Turn off sync confirmation dialog title") + static let turnOffSyncConfirmMessage = NSLocalizedString("preferences.sync.turn-off.confirm.message", bundle: Bundle.module, value: "This device will no longer be able to access your synced data.", comment: "Turn off sync confirmation dialog message") // Delete server data - static let turnOffAndDeleteServerData = NSLocalizedString("preferences.sync.turn-off-and-delete-data", value: "Turn Off and Delete Server Data…", comment: "Disable and delete data sync button caption") + static let turnOffAndDeleteServerData = NSLocalizedString("preferences.sync.turn-off-and-delete-data", bundle: Bundle.module, value: "Turn Off and Delete Server Data…", comment: "Disable and delete data sync button caption") // sync connected - static let syncConnected = NSLocalizedString("preferences.sync.connected", value: "Sync Enabled", comment: "Sync state is enabled") + static let syncConnected = NSLocalizedString("preferences.sync.connected", bundle: Bundle.module, value: "Sync Enabled", comment: "Sync state is enabled") // synced devices - static let syncedDevices = NSLocalizedString("preferences.sync.synced-devices", value: "Synced Devices", comment: "Settings section title") - static let thisDevice = NSLocalizedString("preferences.sync.this-device", value: "This Device", comment: "Indicator of a current user's device on the list") - static let currentDeviceDetails = NSLocalizedString("preferences.sync.current-device-details", value: "Details...", comment: "Sync Settings device details button") - static let removeDeviceButton = NSLocalizedString("preferences.sync.remove-device", value: "Remove...", comment: "Button to remove a device") + static let syncedDevices = NSLocalizedString("preferences.sync.synced-devices", bundle: Bundle.module, value: "Synced Devices", comment: "Settings section title") + static let thisDevice = NSLocalizedString("preferences.sync.this-device", bundle: Bundle.module, value: "This Device", comment: "Indicator of a current user's device on the list") + static let currentDeviceDetails = NSLocalizedString("preferences.sync.current-device-details", bundle: Bundle.module, value: "Details...", comment: "Sync Settings device details button") + static let removeDeviceButton = NSLocalizedString("preferences.sync.remove-device", bundle: Bundle.module, value: "Remove...", comment: "Button to remove a device") // Remove device dialog - static let removeDeviceConfirmTitle = NSLocalizedString("preferences.sync.remove-device-title", value: "Remove device?", comment: "Title on remove a device confirmation") - static let removeDeviceConfirmButton = NSLocalizedString("preferences.sync.remove-device-button", value: "Remove Device", comment: "Button text on remove a device confirmation button") + static let removeDeviceConfirmTitle = NSLocalizedString("preferences.sync.remove-device-title", bundle: Bundle.module, value: "Remove device?", comment: "Title on remove a device confirmation") + static let removeDeviceConfirmButton = NSLocalizedString("preferences.sync.remove-device-button", bundle: Bundle.module, value: "Remove Device", comment: "Button text on remove a device confirmation button") static func removeDeviceConfirmMessage(_ deviceName: String) -> String { let localized = NSLocalizedString("preferences.sync.remove-device-message", - value: "\"%@\" will no longer be able to access your synced data.", + bundle: Bundle.module, value: "\"%@\" will no longer be able to access your synced data.", comment: "Message to confirm the device will no longer be able to access the synced data - devoce name item inserted") return String(format: localized, deviceName) } - static let recovery = NSLocalizedString("prefrences.sync.recovery", value: "Recovery", comment: "Sync settings section title") - static let recoveryInstructions = NSLocalizedString("prefrences.sync.recovery-instructions", value: "If you lose your device, you will need this recovery code to restore your synced data.", comment: "Instructions on how to restore synced data") + static let recovery = NSLocalizedString("prefrences.sync.recovery", bundle: Bundle.module, value: "Recovery", comment: "Sync settings section title") + static let recoveryInstructions = NSLocalizedString("prefrences.sync.recovery-instructions", bundle: Bundle.module, value: "If you lose your device, you will need this recovery code to restore your synced data.", comment: "Instructions on how to restore synced data") // Sync with another device dialog - static let syncWithAnotherDeviceTitle = NSLocalizedString("preferences.sync.sync-with-another-device.dialog-title", value: "Sync With Another Device", comment: "Sync with another device dialog title") + static let syncWithAnotherDeviceTitle = NSLocalizedString("preferences.sync.sync-with-another-device.dialog-title", bundle: Bundle.module, value: "Sync With Another Device", comment: "Sync with another device dialog title") static func syncWithAnotherDeviceSubtitle(syncMenuPath: String) -> String { - let localized = NSLocalizedString("preferences.sync.sync-with-another-device.dialog-subtitle1", value: "Go to %@ in the DuckDuckGo Browser on another device and select Sync With Another Device.", comment: "Sync with another device dialog subtitle - Instruction with sync menu path item inserted") + let localized = NSLocalizedString("preferences.sync.sync-with-another-device.dialog-subtitle1", bundle: Bundle.module, value: "Go to %@ in the DuckDuckGo Browser on another device and select Sync With Another Device.", comment: "Sync with another device dialog subtitle - Instruction with sync menu path item inserted") return String(format: localized, syncMenuPath) } - static let syncMenuPath = NSLocalizedString("sync.menu.path", value: "Settings › Sync & Backup", comment: "Sync Menu Path") - static let syncWithAnotherDeviceShowCodeButton = NSLocalizedString("preferences.sync.sync-with-another-device.show-code-button", value: "Show Code", comment: "Text on show code button on Sync with another device dialog") - static let syncWithAnotherDeviceEnterCodeButton = NSLocalizedString("preferences.sync.sync-with-another-device.enter-code-button", value: "Enter Code", comment: "Text on enter code button on Sync with another device dialog") - static let syncWithAnotherDeviceShowQRCodeExplanation = NSLocalizedString("preferences.sync.sync-with-another-device.show-qr-code-explanation", value: "Scan this QR code to connect.", comment: "Sync with another device dialog show qr code explanation") - static let syncWithAnotherDeviceEnterCodeExplanation = NSLocalizedString("preferences.sync.sync-with-another-device.enter-code-explanation", value: "Paste the code here to sync.", comment: "Sync with another device dialog enter code explanation") - static let syncWithAnotherDeviceShowCodeExplanation = NSLocalizedString("preferences.sync.sync-with-another-device.show-code-explanation", value: "Share this code to connect with a desktop machine.", comment: "Sync with another device dialog show code explanation") - static let syncWithAnotherDeviceViewQRCode = NSLocalizedString("preferences.sync.sync-with-another-device.view-qr-code-link", value: "View QR Code", comment: "Sync with another device dialog view qr code link") - static let syncWithAnotherDeviceViewTextCode = NSLocalizedString("preferences.sync.sync-with-another-device.view-text-code-link", value: "View Text Code", comment: "Sync with another device dialog view text code link") + static let syncMenuPath = NSLocalizedString("sync.menu.path", bundle: Bundle.module, value: "Settings › Sync & Backup", comment: "Sync Menu Path") + static let syncWithAnotherDeviceShowCodeButton = NSLocalizedString("preferences.sync.sync-with-another-device.show-code-button", bundle: Bundle.module, value: "Show Code", comment: "Text on show code button on Sync with another device dialog") + static let syncWithAnotherDeviceEnterCodeButton = NSLocalizedString("preferences.sync.sync-with-another-device.enter-code-button", bundle: Bundle.module, value: "Enter Code", comment: "Text on enter code button on Sync with another device dialog") + static let syncWithAnotherDeviceShowQRCodeExplanation = NSLocalizedString("preferences.sync.sync-with-another-device.show-qr-code-explanation", bundle: Bundle.module, value: "Scan this QR code to connect.", comment: "Sync with another device dialog show qr code explanation") + static let syncWithAnotherDeviceEnterCodeExplanation = NSLocalizedString("preferences.sync.sync-with-another-device.enter-code-explanation", bundle: Bundle.module, value: "Paste the code here to sync.", comment: "Sync with another device dialog enter code explanation") + static let syncWithAnotherDeviceShowCodeExplanation = NSLocalizedString("preferences.sync.sync-with-another-device.show-code-explanation", bundle: Bundle.module, value: "Share this code to connect with a desktop machine.", comment: "Sync with another device dialog show code explanation") + static let syncWithAnotherDeviceViewQRCode = NSLocalizedString("preferences.sync.sync-with-another-device.view-qr-code-link", bundle: Bundle.module, value: "View QR Code", comment: "Sync with another device dialog view qr code link") + static let syncWithAnotherDeviceViewTextCode = NSLocalizedString("preferences.sync.sync-with-another-device.view-text-code-link", bundle: Bundle.module, value: "View Text Code", comment: "Sync with another device dialog view text code link") // Save recovery PDF dialog - static let saveRecoveryPDF = NSLocalizedString("prefrences.sync.save-recovery-pdf", value: "Save Your Recovery Code", comment: "Caption for a button to save Sync recovery PDF") - static let recoveryPDFExplanation = NSLocalizedString("prefrences.sync.recovery-pdf-explanation", value: "If you lose access to your devices, you will need this code to recover your synced data. You can save this code to your device as a PDF.", comment: "Sync recovery PDF explanation") - static let recoveryPDFCopyCodeButton = NSLocalizedString("prefrences.sync.recovery-pdf-copy-code-button", value: "Copy Code", comment: "Sync recovery PDF copy code button") - static let recoveryPDFSavePDFButton = NSLocalizedString("prefrences.sync.recovery-pdf-save-pdf-button", value: "Save PDF", comment: "Sync recovery PDF save pdf button") - static let recoveryPDFWarning = NSLocalizedString("prefrences.sync.recovery-pdf-warning", value: "Anyone with access to this code can access your synced data, so please keep it in a safe place.", comment: "Sync recovery PDF warning") + static let saveRecoveryPDF = NSLocalizedString("prefrences.sync.save-recovery-pdf", bundle: Bundle.module, value: "Save Your Recovery Code", comment: "Caption for a button to save Sync recovery PDF") + static let recoveryPDFExplanation = NSLocalizedString("prefrences.sync.recovery-pdf-explanation", bundle: Bundle.module, value: "If you lose access to your devices, you will need this code to recover your synced data. You can save this code to your device as a PDF.", comment: "Sync recovery PDF explanation") + static let recoveryPDFCopyCodeButton = NSLocalizedString("prefrences.sync.recovery-pdf-copy-code-button", bundle: Bundle.module, value: "Copy Code", comment: "Sync recovery PDF copy code button") + static let recoveryPDFSavePDFButton = NSLocalizedString("prefrences.sync.recovery-pdf-save-pdf-button", bundle: Bundle.module, value: "Save PDF", comment: "Sync recovery PDF save pdf button") + static let recoveryPDFWarning = NSLocalizedString("prefrences.sync.recovery-pdf-warning", bundle: Bundle.module, value: "Anyone with access to this code can access your synced data, so please keep it in a safe place.", comment: "Sync recovery PDF warning") // Sync with server dialog - static let syncWithServerTitle = NSLocalizedString("preferences.sync.sync-with-server-title", value: "Sync and Back Up This Device", comment: "Sync with server dialog title") - static let syncWithServerSubtitle1 = NSLocalizedString("preferences.sync.sync-with-server-subtitle1", value: "This creates an encrypted backup of your bookmarks and passwords on DuckDuckGo’s secure server, which can be synced with your other devices.", comment: "Sync with server dialog first subtitle") - static let syncWithServerSubtitle2 = NSLocalizedString("preferences.sync.sync-with-server-subtitle2", value: "The encryption key is only stored on your device, DuckDuckGo cannot access it.", comment: "Sync with server dialog second subtitle") - static let syncWithServerButton = NSLocalizedString("preferences.sync.sync-with-server-button", value: "Turn On Sync & Backup", comment: "Sync with server dialog button") + static let syncWithServerTitle = NSLocalizedString("preferences.sync.sync-with-server-title", bundle: Bundle.module, value: "Sync and Back Up This Device", comment: "Sync with server dialog title") + static let syncWithServerSubtitle1 = NSLocalizedString("preferences.sync.sync-with-server-subtitle1", bundle: Bundle.module, value: "This creates an encrypted backup of your bookmarks and passwords on DuckDuckGo’s secure server, which can be synced with your other devices.", comment: "Sync with server dialog first subtitle") + static let syncWithServerSubtitle2 = NSLocalizedString("preferences.sync.sync-with-server-subtitle2", bundle: Bundle.module, value: "The encryption key is only stored on your device, DuckDuckGo cannot access it.", comment: "Sync with server dialog second subtitle") + static let syncWithServerButton = NSLocalizedString("preferences.sync.sync-with-server-button", bundle: Bundle.module, value: "Turn On Sync & Backup", comment: "Sync with server dialog button") // Device synced dialog - static let deviceSynced = NSLocalizedString("prefrences.sync.device-synced", value: "Your data is synced!", comment: "Sync setup confirmation dialog title") + static let deviceSynced = NSLocalizedString("prefrences.sync.device-synced", bundle: Bundle.module, value: "Your data is synced!", comment: "Sync setup confirmation dialog title") // Device details - static let deviceDetailsTitle = NSLocalizedString("prefrences.sync.device-details.title", value: "Device Details", comment: "The title of the device details dialog") - static let deviceDetailsLabel = NSLocalizedString("prefrences.sync.device-details.label", value: "Name", comment: "The text entry label to name the device") - static let deviceDetailsPrompt = NSLocalizedString("prefrences.sync.device-details.prompt", value: "Device name", comment: "The text entry prompt to name the device") + static let deviceDetailsTitle = NSLocalizedString("prefrences.sync.device-details.title", bundle: Bundle.module, value: "Device Details", comment: "The title of the device details dialog") + static let deviceDetailsLabel = NSLocalizedString("prefrences.sync.device-details.label", bundle: Bundle.module, value: "Name", comment: "The text entry label to name the device") + static let deviceDetailsPrompt = NSLocalizedString("prefrences.sync.device-details.prompt", bundle: Bundle.module, value: "Device name", comment: "The text entry prompt to name the device") // Delete Account Dialog - static let deleteAccountTitle = NSLocalizedString("prefrences.sync.delete-account.title", value: "Delete server data?", comment: "Title for delete account confirmation pop up") - static let deleteAccountMessage = NSLocalizedString("prefrences.sync.delete-account.message", value: "These devices will be disconnected and your synced data will be deleted from the server.", comment: "Message for delete account confirmation pop up") - static let deleteAccountButton = NSLocalizedString("prefrences.sync.delete-account.button", value: "Delete Data", comment: "Label for delete account button") + static let deleteAccountTitle = NSLocalizedString("prefrences.sync.delete-account.title", bundle: Bundle.module, value: "Delete server data?", comment: "Title for delete account confirmation pop up") + static let deleteAccountMessage = NSLocalizedString("prefrences.sync.delete-account.message", bundle: Bundle.module, value: "These devices will be disconnected and your synced data will be deleted from the server.", comment: "Message for delete account confirmation pop up") + static let deleteAccountButton = NSLocalizedString("prefrences.sync.delete-account.button", bundle: Bundle.module, value: "Delete Data", comment: "Label for delete account button") // Sync enabled options - static let optionsSectionTitle = NSLocalizedString("prefrences.sync.options-section-title", value: "Options", comment: "Title for options settings") - static let shareFavoritesOptionTitle = NSLocalizedString("prefrences.sync.share-favorite-option-title", value: "Unify Favorites Across Devices", comment: "Title for share favorite option") - static let shareFavoritesOptionCaption = NSLocalizedString("prefrences.sync.share-favorite-option-caption", value: "Use the same favorite bookmarks on all your devices. Leave off to keep mobile and desktop favorites separate.", comment: "Caption for share favorite option") - static let fetchFaviconsOptionTitle = NSLocalizedString("prefrences.sync.fetch-favicons-option-title", value: "Auto-Download Icons", comment: "Title for fetch favicons option") - static let fetchFaviconsOptionCaption = NSLocalizedString("prefrences.sync.fetch-favicons-option-caption", value: "Automatically download icons for synced bookmarks. Icon downloads are exposed to your network.", comment: "Caption for fetch favicons option") + static let optionsSectionTitle = NSLocalizedString("prefrences.sync.options-section-title", bundle: Bundle.module, value: "Options", comment: "Title for options settings") + static let shareFavoritesOptionTitle = NSLocalizedString("prefrences.sync.share-favorite-option-title", bundle: Bundle.module, value: "Unify Favorites Across Devices", comment: "Title for share favorite option") + static let shareFavoritesOptionCaption = NSLocalizedString("prefrences.sync.share-favorite-option-caption", bundle: Bundle.module, value: "Use the same favorite bookmarks on all your devices. Leave off to keep mobile and desktop favorites separate.", comment: "Caption for share favorite option") + static let fetchFaviconsOptionTitle = NSLocalizedString("prefrences.sync.fetch-favicons-option-title", bundle: Bundle.module, value: "Auto-Download Icons", comment: "Title for fetch favicons option") + static let fetchFaviconsOptionCaption = NSLocalizedString("prefrences.sync.fetch-favicons-option-caption", bundle: Bundle.module, value: "Automatically download icons for synced bookmarks. Icon downloads are exposed to your network.", comment: "Caption for fetch favicons option") // sync enabled errors - static let syncLimitExceededTitle = NSLocalizedString("prefrences.sync.limit-exceeded-title", value: "Sync Paused", comment: "Title for sync limits exceeded warning") - 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 go to manage bookmarks") - static let credentialsLimitExceededAction = NSLocalizedString("prefrences.sync.credentials-limit-exceeded-action", value: "Manage passwords…", comment: "Button title for sync credentials limits exceeded warning to go to manage passwords") - static let syncErrorAlertTitle = NSLocalizedString("alert.sync-error", value: "Sync & Backup Error", comment: "Title for sync error alert") - static let unableToSyncToServerDescription = NSLocalizedString("alert.unable-to-sync-to-server-description", value: "Unable to connect to the server.", comment: "Description for unable to sync to server error") - static let unableToSyncWithAnotherDeviceDescription = NSLocalizedString("alert.unable-to-sync-with-another-device-description", value: "Unable to Sync with another device.", comment: "Description for unable to sync with another device error") - static let unableToMergeTwoAccountsDescription = NSLocalizedString("alert.unable-to-merge-two-accounts-description", value: "To pair these devices, turn off Sync & Backup on one device then tap \"Sync With Another Device\" on the other device.", comment: "Description for unable to merge two accounts error") - static let unableToUpdateDeviceNameDescription = NSLocalizedString("alert.unable-to-update-device-name-description", value: "Unable to update the device name.", comment: "Description for unable to update device name error") - static let unableToTurnSyncOffDescription = NSLocalizedString("alert.unable-to-turn-sync-off-description", value: "Unable to turn Sync & Backup off.", comment: "Description for unable to turn sync off error") - static let unableToDeleteDataDescription = NSLocalizedString("alert.unable-to-delete-data-description", value: "Unable to delete data on the server.", comment: "Description for unable to delete data error") - static let unableToRemoveDeviceDescription = NSLocalizedString("alert.unable-to-remove-device-description", value: "Unable to remove this device from Sync & Backup.", comment: "Description for unable to remove device error") - static let invalidCodeDescription = NSLocalizedString("alert.invalid-code-description", value: "Sorry, this code is invalid. Please make sure it was entered correctly.", comment: "Description for invalid code error") - static let unableCreateRecoveryPdfDescription = NSLocalizedString("alert.unable-to-create-recovery-pdf-description", value: "Unable to create the recovery PDF.", comment: "Description for unable to create recovery pdf error") - - static let fetchFaviconsOnboardingTitle = NSLocalizedString("prefrences.sync.fetch-favicons-onboarding-title", value: "Download Missing Icons?", comment: "Title for fetch favicons onboarding dialog") - static let fetchFaviconsOnboardingMessage = NSLocalizedString("prefrences.sync.fetch-favicons-onboarding-message", value: "Do you want this device to automatically download icons for any new bookmarks synced from your other devices? This will expose the download to your network any time a bookmark is synced.", comment: "Text for fetch favicons onboarding dialog") - static let keepFaviconsUpdated = NSLocalizedString("prefrences.sync.keep-favicons-updated", value: "Keep Bookmarks Icons Updated", comment: "Title of the confirmation button for favicons fetching") + static let syncLimitExceededTitle = NSLocalizedString("prefrences.sync.limit-exceeded-title", bundle: Bundle.module, value: "Sync Paused", comment: "Title for sync limits exceeded warning") + static let bookmarksLimitExceededDescription = NSLocalizedString("prefrences.sync.bookmarks-limit-exceeded-description", bundle: Bundle.module, 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", bundle: Bundle.module, 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", bundle: Bundle.module, value: "Manage Bookmarks", comment: "Button title for sync bookmarks limits exceeded warning to go to manage bookmarks") + static let credentialsLimitExceededAction = NSLocalizedString("prefrences.sync.credentials-limit-exceeded-action", bundle: Bundle.module, value: "Manage passwords…", comment: "Button title for sync credentials limits exceeded warning to go to manage passwords") + static let syncErrorAlertTitle = NSLocalizedString("alert.sync-error", bundle: Bundle.module, value: "Sync & Backup Error", comment: "Title for sync error alert") + static let unableToSyncToServerDescription = NSLocalizedString("alert.unable-to-sync-to-server-description", bundle: Bundle.module, value: "Unable to connect to the server.", comment: "Description for unable to sync to server error") + static let unableToSyncWithAnotherDeviceDescription = NSLocalizedString("alert.unable-to-sync-with-another-device-description", bundle: Bundle.module, value: "Unable to Sync with another device.", comment: "Description for unable to sync with another device error") + static let unableToMergeTwoAccountsDescription = NSLocalizedString("alert.unable-to-merge-two-accounts-description", bundle: Bundle.module, value: "To pair these devices, turn off Sync & Backup on one device then tap \"Sync With Another Device\" on the other device.", comment: "Description for unable to merge two accounts error") + static let unableToUpdateDeviceNameDescription = NSLocalizedString("alert.unable-to-update-device-name-description", bundle: Bundle.module, value: "Unable to update the device name.", comment: "Description for unable to update device name error") + static let unableToTurnSyncOffDescription = NSLocalizedString("alert.unable-to-turn-sync-off-description", bundle: Bundle.module, value: "Unable to turn Sync & Backup off.", comment: "Description for unable to turn sync off error") + static let unableToDeleteDataDescription = NSLocalizedString("alert.unable-to-delete-data-description", bundle: Bundle.module, value: "Unable to delete data on the server.", comment: "Description for unable to delete data error") + static let unableToRemoveDeviceDescription = NSLocalizedString("alert.unable-to-remove-device-description", bundle: Bundle.module, value: "Unable to remove this device from Sync & Backup.", comment: "Description for unable to remove device error") + static let invalidCodeDescription = NSLocalizedString("alert.invalid-code-description", bundle: Bundle.module, value: "Sorry, this code is invalid. Please make sure it was entered correctly.", comment: "Description for invalid code error") + static let unableCreateRecoveryPdfDescription = NSLocalizedString("alert.unable-to-create-recovery-pdf-description", bundle: Bundle.module, value: "Unable to create the recovery PDF.", comment: "Description for unable to create recovery pdf error") + + static let fetchFaviconsOnboardingTitle = NSLocalizedString("prefrences.sync.fetch-favicons-onboarding-title", bundle: Bundle.module, value: "Download Missing Icons?", comment: "Title for fetch favicons onboarding dialog") + static let fetchFaviconsOnboardingMessage = NSLocalizedString("prefrences.sync.fetch-favicons-onboarding-message", bundle: Bundle.module, value: "Do you want this device to automatically download icons for any new bookmarks synced from your other devices? This will expose the download to your network any time a bookmark is synced.", comment: "Text for fetch favicons onboarding dialog") + static let keepFaviconsUpdated = NSLocalizedString("prefrences.sync.keep-favicons-updated", bundle: Bundle.module, value: "Keep Bookmarks Icons Updated", comment: "Title of the confirmation button for favicons fetching") // Sync Feature Flags - static let syncUnavailableTitle = NSLocalizedString("sync.warning.sync-unavailable", value: "Sync & Backup is Unavailable", comment: "Title of the warning message that sync and backup are unavailable") - static let syncPausedTitle = NSLocalizedString("sync.warning.sync-paused", value: "Sync & Backup is Paused", comment: "Title of the warning message that Sync & Backup is Paused") - static let syncUnavailableMessage = NSLocalizedString("sync.warning.sync-unavailable-message", value: "Sorry, but Sync & Backup is currently unavailable. Please try again later.", comment: "Data syncing unavailable warning message") - static let syncUnavailableMessageUpgradeRequired = NSLocalizedString("sync.warning.data-syncing-disabled-upgrade-required", value: "Sorry, but Sync & Backup is no longer available in this app version. Please update DuckDuckGo to the latest version to continue.", comment: "Data syncing unavailable warning message") + static let syncUnavailableTitle = NSLocalizedString("sync.warning.sync-unavailable", bundle: Bundle.module, value: "Sync & Backup is Unavailable", comment: "Title of the warning message that sync and backup are unavailable") + static let syncPausedTitle = NSLocalizedString("sync.warning.sync-paused", bundle: Bundle.module, value: "Sync & Backup is Paused", comment: "Title of the warning message that Sync & Backup is Paused") + static let syncUnavailableMessage = NSLocalizedString("sync.warning.sync-unavailable-message", bundle: Bundle.module, value: "Sorry, but Sync & Backup is currently unavailable. Please try again later.", comment: "Data syncing unavailable warning message") + static let syncUnavailableMessageUpgradeRequired = NSLocalizedString("sync.warning.data-syncing-disabled-upgrade-required", bundle: Bundle.module, value: "Sorry, but Sync & Backup is no longer available in this app version. Please update DuckDuckGo to the latest version to continue.", comment: "Data syncing unavailable warning message") } From 3dad84b6044b297e113ac6ce7b774a62928e96bb Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Thu, 22 Feb 2024 09:46:59 -0300 Subject: [PATCH 43/44] Mute/unmute tab (#2019) --- .../MutedTabIconColor.colorset/Contents.json | 38 ++++++++ .../Contents.json | 38 ++++++++ .../Audio-Mute.imageset/Audio-Mute-12.pdf | Bin 0 -> 4374 bytes .../Images/Audio-Mute.imageset/Contents.json | 12 +++ .../Images/Audio.imageset/Audio-12.pdf | Bin 0 -> 4307 bytes .../Images/Audio.imageset/Contents.json | 12 +++ .../Extensions/WKWebViewExtension.swift | 48 ++++++++++ DuckDuckGo/Common/Localizables/UserText.swift | 2 + DuckDuckGo/Localizable.xcstrings | 24 +++++ .../Model/PinnedTabsViewModel.swift | 27 ++++++ .../PinnedTabs/View/PinnedTabView.swift | 50 +++++++++-- DuckDuckGo/Tab/Model/Tab.swift | 9 ++ .../TabBar/View/Base.lproj/TabBar.storyboard | 21 +++-- .../TabBar/View/TabBarViewController.swift | 23 +++++ DuckDuckGo/TabBar/View/TabBarViewItem.swift | 83 ++++++++++++++++-- DuckDuckGo/TabBar/View/TabBarViewItem.xib | 41 ++++++--- .../PinnedTabs/PinnedTabsViewModelTests.swift | 6 +- .../TabBar/View/MockTabViewItemDelegate.swift | 13 +++ .../TabBar/View/TabBarViewItemTests.swift | 22 +++++ 19 files changed, 427 insertions(+), 42 deletions(-) create mode 100644 DuckDuckGo/Assets.xcassets/Colors/MutedTabIconColor.colorset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Colors/PinnedTabMuteStateCircleColor.colorset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/Audio-Mute.imageset/Audio-Mute-12.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/Audio-Mute.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Audio-12.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Contents.json diff --git a/DuckDuckGo/Assets.xcassets/Colors/MutedTabIconColor.colorset/Contents.json b/DuckDuckGo/Assets.xcassets/Colors/MutedTabIconColor.colorset/Contents.json new file mode 100644 index 0000000000..3fe9b59242 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Colors/MutedTabIconColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0x00", + "red" : "0x00" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFE" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Colors/PinnedTabMuteStateCircleColor.colorset/Contents.json b/DuckDuckGo/Assets.xcassets/Colors/PinnedTabMuteStateCircleColor.colorset/Contents.json new file mode 100644 index 0000000000..802fa68a4c --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Colors/PinnedTabMuteStateCircleColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFE" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0x00", + "red" : "0x00" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Audio-Mute.imageset/Audio-Mute-12.pdf b/DuckDuckGo/Assets.xcassets/Images/Audio-Mute.imageset/Audio-Mute-12.pdf new file mode 100644 index 0000000000000000000000000000000000000000..de1f4718ed0a91177028ae95b498623cae76a1cb GIT binary patch literal 4374 zcmeH~TaOb*5QX38SM&=KJZ!t~7YQM8SCk;gZXS?O9*lQ@CA&7UO$7P%`Kmph@m>-Z zo-;!1_)J%I^{G?U-FIF+d3I!V?mDZCTD|?)DRuw8n*H|mQ?F*PU%vg=FScMjvtRn< z`|Sti7e44NO<>Kq5?++dfu1-F`-L^@et=1O< zcHD1PpVy0iBU6kcA3a)qQE!ydrajY(b$e)f`yE$UZno=we$maI^naZ#`s3%1)Z>%G zUyDumXJ_~3qwAyb?fSQ%LpI3-wYccUVb@RnRsX=Zw1OtkEH);;i!8 z)*4e*=K?0i*{GaODdtoaP>#m)#`0?QIS!EZPFWpY4z8$$nC4JRB$2v=l1r9x1ZblN zvbmVjOuch5inRq_qcuBQ85f;G3?^pFvZ_wr+MESSCObY>ZEf|bs3Ss&t{RVR(Ir+B zl7KGPn1cfHHnSk0l!H$eQxi+@&9r*f4R2l55(yC;2?f!ZB(vsPDyCjL9zw3VhQ=A* zgeNPrvb0!Iwr$$rT`EaIvo+C_wu{Ib^PDPPux%;}%;&T(!3HCy#bz=~DxJ5XHg6%hkS!@Uux46{2qsuxjDQi2 zTFk-LY-Jp*2j|7xRkbX-58b8G(0#vY@A{#`+ zxEwuFYk^cI6;mk(*J3ltmufLZ^DrT^dI|zmwCpHi!UQ7i7V!WDYLf{Zgc}yoOaY6C z%t07p$TH5?6r(_a5`vu@X#eqaI~cSj-li#~eYeZ#zK|-iJz`N)p}8iK9i;t88cCSH zie;qAVr3kqj((ATWR20og<$>%tB(MGWmquN|DxDTnW-+ z9nPeL=<_LhLkBk6dpfMcm%&$}RZ83N)Uq^NiHhi@A7!L{hzv1^uVGZ6<+WJF*MLP$ zcMABYsXK&PRMMXHu%eNh5s@`kV|}dCD9Ib7me!54p|42c22HU@pQ^-PE&Ilpae8*i zkpdo(Q5QchN9xYA6VW!mxkrxEQn!RCIk}j$ca*5$1|A3!Fts+{R+a)g12vg+OlVS; z>opC4?x&8VcX;}28#(X-SlKAk0Wnm0(TQ=5${`m(HWmI&)L`mZ7Oi42)yx$5`46{i zbj5Z>Q~Wkzvbh&ebCeB(mR8VNJ5^LiOJ=7>(w_a&wl{XtbOl4TRpdQdqa)eKXo0AV zLqxP}=kk5RLL|DjYwfL(KieDiA}VBe6PL)iBzAGwrUYu1sb!;$*)R~###v9BJ%FV4 zAbqry`ta6Q+O5=@HFWd?BvX>a#vx#D$5tZ@5XB9=ObO}w<>_!3tQ zt>7xeetH@5o?FXRC_tG!qc5MX8|LeyIJ;jptR4<8`3jtNMrga9pPl#XPU!vV3FO}0 wtSh9U;y&c7~J1!o9%qPZR`Y-eRt>1)0fZw0h>RF!2kdN literal 0 HcmV?d00001 diff --git a/DuckDuckGo/Assets.xcassets/Images/Audio-Mute.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Audio-Mute.imageset/Contents.json new file mode 100644 index 0000000000..b609317961 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Audio-Mute.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Audio-Mute-12.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Audio-12.pdf b/DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Audio-12.pdf new file mode 100644 index 0000000000000000000000000000000000000000..635e45f8743e3360c3aa3f3fe264526a6d890242 GIT binary patch literal 4307 zcmeH~O>Y!O5QgvbEBb;2k(ll84+&WU8>}cn5IYAXl*6(fj1zm;*ysCcO`Dp=D6LlSes7d|@f`O6LH)abS8aO3bNsF6%gyb4-7OcN@4P=c8eE-!e7CKezF4hq z2JEcctUj(6-A1+;S3Y~T`lQ|}-RkX z{l6BQ=Fi6L%tzOc$ESPW1>>X1YH`zy>kc0_i)G_mTXardNnlYYW7G{`v1E`1jK%rj zRoq1YWPK{e?m?z?Aw*l$qM6pUF}diHx}GrIrfgiWYSH7|+Y)mK0biyq z+PJ8dYh5xaMSPhy+CXgk7;bF}E>GB)t8aBFj$rqYQesNtf5dXDo%O}we-A6R6fz&Z zmo|nZtHu~cWEX2FYQ(PZ@k(LmjVNGt_dhht=9lkQQEa^r24=r5qSRXJs2)5(q^RkpNtnkH1JPS+3%W{e_Qp{Hkl;%Y z<_$%!b=A&z=Shs3MQ1gFq^o0ykyQyf2dR&fKFA=&7wa9piBORUcBM5wX*8%?-sK$V zM0%P|klweb$Mi^XPBA@XUqTQ=s%aq*uo#!?I^(m2X9loC>r4{13`W~1P}VV8h6}MG zyHv7zOiGUAWSx(6GFwnJBC8!|bc!(vR3Nm@2_b_x*}@Sx@fP^;K}&T7GT25S3kUeX zH2X|%v1?)!*(6!mHsp{~Vx4y02BM?Kit)OdmLw`8U>Z00R84~+ew81*G9pTO0$;2&fHj3%TOoNDc^@IfjH-#cyOdlXW#MB?%|GHKEcZ z-rz|FprP9AAe(v-C?k>HFufuCnItu3^b{{W6bw(YDl>z#AeQAsLs$tK`&enGl4Mdj zDQ#v}4{@PO<}0E)?Ae8A-XFfjknUcaaJsGc(?#)4dLW(x{n6 z5yUZKP*&mVWKoaUnM{4)HMg0MNvO&SIYI_Vv{pN^Lq$1R>?n}KH~G?yStQ1O&L!F= zwuB&+WD?V}Gy)KOd1?y8H^E~Nm}xffj%i3Cbg=jh)ZWw8b%@ip^%LftD<0+24ph)t zK1eYHhnTA=13MEfD!?#0S%*|ikcog83(Zw`wX%vrWjGOO1IjX#AvM;irWKuG(9#FN z!i<+h4Ly}XWWFbdA#^~eM;6j}hV+FYI!eTdrH{VO2_3RWyFf@g(=K%?5QV84&@mZ? z6ttmrnp_k>p718s)DT`x`Kr=aHC(EZOx>2`GhU2g)CHa8L+q$9bO9kZ@xnVM7ox$I z=|Y3!M@Ff3a05i|O1UiMN^&&l4^caUeae-kdd6Z%@DU1zu}E#iTQZEfzzGHY)<96Z zM;j64l~fyob&M!_L3Dzpd<&tc)UPxH0`95EVe3wcX}aabaFXLmPOFWaM*CwcV>rly z3M1WbJI!+P`2RG^{HCAl?;D28xU_E&%$P3I^Yp|SW;~IPMtg_9`>@HP9xCdfyr=9^ zwxR6Kj#cL)yjS(%_EN7WZ;-lFdb&kC|Fk_l-_EyPegE9l|IS~|H-FTp{sJU_-np(! zx0<_8vr{E4H{6TvzB~A9#)atV_3elG*In-}JqJDn7;b4}CjaK{=%`N{XFGkwJ4_8X zJLX)^&^O(dW^i`7X=YG+vf|`-PnE=BUnL*Mt(l#BMS8kiuC^QXR_-w&o!I4alA+#`C@_$#667IAwl3-_EaBmv?PHt-JS)Ox30rsr;+tn->)L@RJSX zr)qW|P7ZWmLWvaRH~Mnn&@djZiUq>%8p`qD3y7282yNH%tLtvv2)#d^ zK<>lM>ZW_79$sBM>M5R`tyb_W^woOJ&aeLNgbT>sZ0GB3WyfnBn+Fe`zk2Zx7O#DG literal 0 HcmV?d00001 diff --git a/DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Contents.json new file mode 100644 index 0000000000..35d4dda319 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Audio-12.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift b/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift index a2eb4706e4..4c70d4bacc 100644 --- a/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift +++ b/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift @@ -29,6 +29,12 @@ extension WKWebView { return false } + enum AudioState { + case muted + case unmuted + case notSupported + } + enum CaptureState { case none case active @@ -129,6 +135,48 @@ extension WKWebView { } #endif + func muteOrUnmute() { +#if !APPSTORE + guard self.responds(to: #selector(WKWebView._setPageMuted(_:))) else { + assertionFailure("WKWebView does not respond to selector _stopMediaCapture") + return + } + let mutedState: _WKMediaMutedState = { + guard self.responds(to: #selector(WKWebView._mediaMutedState)) else { return [] } + return self._mediaMutedState() + }() + var newState = mutedState + + if newState == .audioMuted { + newState.remove(.audioMuted) + } else { + newState.insert(.audioMuted) + } + guard newState != mutedState else { return } + self._setPageMuted(newState) +#endif + } + + /// Returns the audio state of the WKWebView. + /// + /// - Returns: `muted` if the web view is muted + /// `unmuted` if the web view is unmuted + /// `notSupported` if the web view does not support fetching the current audio state + func audioState() -> AudioState { +#if APPSTORE + return .notSupported +#else + guard self.responds(to: #selector(WKWebView._mediaMutedState)) else { + assertionFailure("WKWebView does not respond to selector _mediaMutedState") + return .notSupported + } + + let mutedState = self._mediaMutedState() + + return mutedState.contains(.audioMuted) ? .muted : .unmuted +#endif + } + func stopMediaCapture() { guard #available(macOS 12.0, *) else { #if !APPSTORE diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index c3f8ff0f68..eee5b3f4e5 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -199,6 +199,8 @@ struct UserText { static let pinTab = NSLocalizedString("pin.tab", value: "Pin Tab", comment: "Menu item. Pin as a verb") static let unpinTab = NSLocalizedString("unpin.tab", value: "Unpin Tab", comment: "Menu item. Unpin as a verb") static let closeTab = NSLocalizedString("close.tab", value: "Close Tab", comment: "Menu item") + static let muteTab = NSLocalizedString("mute.tab", value: "Mute Tab", comment: "Menu item. Mute tab") + static let unmuteTab = NSLocalizedString("unmute.tab", value: "Unmute Tab", comment: "Menu item. Unmute tab") static let closeOtherTabs = NSLocalizedString("close.other.tabs", value: "Close Other Tabs", comment: "Menu item") static let closeTabsToTheRight = NSLocalizedString("close.tabs.to.the.right", value: "Close Tabs to the Right", comment: "Menu item") static let openInNewTab = NSLocalizedString("open.in.new.tab", value: "Open in New Tab", comment: "Menu item that opens the link in a new tab") diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index faef1b6820..ddf6e62331 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -5092,6 +5092,18 @@ } } }, + "mute.tab" : { + "comment" : "Menu item. Mute tab", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Mute Tab" + } + } + } + }, "n.more.tabs" : { "comment" : "String in Recently Closed menu item for recently closed browser window and number of tabs contained in the closed window", "extractionState" : "extracted_with_value", @@ -9100,6 +9112,18 @@ } } }, + "unmute.tab" : { + "comment" : "Menu item. Unmute tab", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Unmute Tab" + } + } + } + }, "unpin.tab" : { "comment" : "Menu item. Unpin as a verb", "extractionState" : "extracted_with_value", diff --git a/DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift b/DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift index 2c714304df..ee5c9f0c91 100644 --- a/DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift +++ b/DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift @@ -46,6 +46,7 @@ final class PinnedTabsViewModel: ObservableObject { didSet { if let selectedItem = selectedItem { selectedItemIndex = items.firstIndex(of: selectedItem) + updateTabAudioState(tab: selectedItem) } else { selectedItemIndex = nil } @@ -57,6 +58,7 @@ final class PinnedTabsViewModel: ObservableObject { didSet { if let hoveredItem = hoveredItem { hoveredItemIndex = items.firstIndex(of: hoveredItem) + updateTabAudioState(tab: hoveredItem) } else { hoveredItemIndex = nil } @@ -72,6 +74,7 @@ final class PinnedTabsViewModel: ObservableObject { @Published private(set) var selectedItemIndex: Int? @Published private(set) var hoveredItemIndex: Int? @Published private(set) var dragMovesWindow: Bool = true + @Published private(set) var audioStateView: AudioStateView = .notSupported @Published private(set) var itemsWithoutSeparator: Set = [] @@ -111,6 +114,18 @@ final class PinnedTabsViewModel: ObservableObject { } itemsWithoutSeparator = items } + + private func updateTabAudioState(tab: Tab) { + let audioState = tab.audioState + switch audioState { + case .muted: + audioStateView = .muted + case .unmuted: + audioStateView = .unmuted + case .notSupported: + audioStateView = .notSupported + } + } } // MARK: - Context Menu @@ -124,6 +139,13 @@ extension PinnedTabsViewModel { case fireproof(Tab) case removeFireproofing(Tab) case close(Int) + case muteOrUnmute(Tab) + } + + enum AudioStateView { + case muted + case unmuted + case notSupported } func isFireproof(_ tab: Tab) -> Bool { @@ -168,4 +190,9 @@ extension PinnedTabsViewModel { func removeFireproofing(_ tab: Tab) { contextMenuActionSubject.send(.removeFireproofing(tab)) } + + func muteOrUmute(_ tab: Tab) { + contextMenuActionSubject.send(.muteOrUnmute(tab)) + updateTabAudioState(tab: tab) + } } diff --git a/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift b/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift index 148b0d5d82..278fdfa7ca 100644 --- a/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift +++ b/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift @@ -21,8 +21,8 @@ import SwiftUIExtensions struct PinnedTabView: View { enum Const { - static let dimension: CGFloat = 32 - static let cornerRadius: CGFloat = 6 + static let dimension: CGFloat = 34 + static let cornerRadius: CGFloat = 10 } @ObservedObject var model: Tab @@ -96,7 +96,17 @@ struct PinnedTabView: View { fireproofAction Divider() - + switch collectionModel.audioStateView { + case .muted, .unmuted: + let audioStateText = collectionModel.audioStateView == .muted ? UserText.unmuteTab : UserText.muteTab + Button(audioStateText) { [weak collectionModel, weak model] in + guard let model = model else { return } + collectionModel?.muteOrUmute(model) + } + Divider() + case .notSupported: + EmptyView() + } Button(UserText.closeTab) { [weak collectionModel, weak model] in guard let model = model else { return } collectionModel?.close(model) @@ -163,6 +173,7 @@ struct PinnedTabInnerView: View { var foregroundColor: Color var drawSeparator: Bool = true + @Environment(\.colorScheme) var colorScheme @EnvironmentObject var model: Tab @Environment(\.controlActiveState) private var controlActiveState @@ -187,11 +198,32 @@ struct PinnedTabInnerView: View { .frame(width: PinnedTabView.Const.dimension) } + @ViewBuilder + var mutedTabIndicator: some View { + switch model.audioState { + case .muted: + ZStack { + Circle() + .stroke(Color.gray.opacity(0.5), lineWidth: 0.5) + .background(Circle().foregroundColor(Color("PinnedTabMuteStateCircleColor"))) + .frame(width: 16, height: 16) + Image("Audio-Mute") + .resizable() + .renderingMode(.template) + .frame(width: 12, height: 12) + }.offset(x: 8, y: -8) + default: EmptyView() + } + } + @ViewBuilder var favicon: some View { if let favicon = model.favicon { - Image(nsImage: favicon) - .resizable() + ZStack(alignment: .topTrailing) { + Image(nsImage: favicon) + .resizable() + mutedTabIndicator + } } else if let domain = model.content.url?.host, let eTLDplus1 = ContentBlocking.shared.tld.eTLDplus1(domain), let firstLetter = eTLDplus1.capitalized.first.flatMap(String.init) { ZStack { Rectangle() @@ -199,11 +231,15 @@ struct PinnedTabInnerView: View { Text(firstLetter) .font(.caption) .foregroundColor(.white) + mutedTabIndicator } .cornerRadius(4.0) } else { - Image(nsImage: #imageLiteral(resourceName: "Web")) - .resizable() + ZStack { + Image(nsImage: #imageLiteral(resourceName: "Web")) + .resizable() + mutedTabIndicator + } } } } diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift index aba8f89859..5c1c11d1db 100644 --- a/DuckDuckGo/Tab/Model/Tab.swift +++ b/DuckDuckGo/Tab/Model/Tab.swift @@ -488,6 +488,7 @@ protocol NewWindowPolicyDecisionMaker { } #endif + self.audioState = webView.audioState() addDeallocationChecks(for: webView) } @@ -934,6 +935,14 @@ protocol NewWindowPolicyDecisionMaker { } } + @Published private(set) var audioState: WKWebView.AudioState = .notSupported + + func muteUnmuteTab() { + webView.muteOrUnmute() + + audioState = webView.audioState() + } + @MainActor(unsafe) @discardableResult private func reloadIfNeeded(shouldLoadInBackground: Bool = false) -> ExpectedNavigation? { diff --git a/DuckDuckGo/TabBar/View/Base.lproj/TabBar.storyboard b/DuckDuckGo/TabBar/View/Base.lproj/TabBar.storyboard index 2bf37ba9b2..ff559dfdfe 100644 --- a/DuckDuckGo/TabBar/View/Base.lproj/TabBar.storyboard +++ b/DuckDuckGo/TabBar/View/Base.lproj/TabBar.storyboard @@ -1,7 +1,7 @@ - + - + @@ -62,7 +62,7 @@ - + - + @@ -116,7 +125,7 @@ - + @@ -124,7 +133,9 @@ + + @@ -134,11 +145,12 @@ - - + + + @@ -147,7 +159,7 @@ - + @@ -158,17 +170,20 @@ + + + diff --git a/UnitTests/PinnedTabs/PinnedTabsViewModelTests.swift b/UnitTests/PinnedTabs/PinnedTabsViewModelTests.swift index 6e34010422..c9f063d54f 100644 --- a/UnitTests/PinnedTabs/PinnedTabsViewModelTests.swift +++ b/UnitTests/PinnedTabs/PinnedTabsViewModelTests.swift @@ -149,17 +149,19 @@ class PinnedTabsViewModelTests: XCTestCase { model.fireproof(tabA) model.removeFireproofing(tabB) model.close(tabA) + model.muteOrUmute(tabB) cancellable.cancel() - XCTAssertEqual(events.count, 6) + XCTAssertEqual(events.count, 7) guard case .bookmark(tabA) = events[0], case .unpin(1) = events[1], case .duplicate(0) = events[2], case .fireproof(tabA) = events[3], case .removeFireproofing(tabB) = events[4], - case .close(0) = events[5] + case .close(0) = events[5], + case .muteOrUnmute(tabB) = events[6] else { XCTFail("Incorrect context menu action") return diff --git a/UnitTests/TabBar/View/MockTabViewItemDelegate.swift b/UnitTests/TabBar/View/MockTabViewItemDelegate.swift index 9d576f6550..ab577650a2 100644 --- a/UnitTests/TabBar/View/MockTabViewItemDelegate.swift +++ b/UnitTests/TabBar/View/MockTabViewItemDelegate.swift @@ -22,6 +22,7 @@ import Foundation class MockTabViewItemDelegate: TabBarViewItemDelegate { var hasItemsToTheRight = false + var audioState: WKWebView.AudioState = .notSupported func tabBarViewItem(_ tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem, isMouseOver: Bool) { @@ -75,8 +76,20 @@ class MockTabViewItemDelegate: TabBarViewItemDelegate { } + func tabBarViewItemAudioState(_ tabBarViewItem: TabBarViewItem) -> WKWebView.AudioState { + return audioState + } + + func tabBarViewItemMuteUnmuteSite(_ tabBarViewItem: TabBarViewItem) { + + } + func otherTabBarViewItemsState(for tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem) -> DuckDuckGo_Privacy_Browser.OtherTabBarViewItemsState { OtherTabBarViewItemsState(hasItemsToTheLeft: false, hasItemsToTheRight: hasItemsToTheRight) } + func clear() { + self.audioState = .notSupported + } + } diff --git a/UnitTests/TabBar/View/TabBarViewItemTests.swift b/UnitTests/TabBar/View/TabBarViewItemTests.swift index d855470109..c4e8f4418b 100644 --- a/UnitTests/TabBar/View/TabBarViewItemTests.swift +++ b/UnitTests/TabBar/View/TabBarViewItemTests.swift @@ -33,6 +33,10 @@ final class TabBarViewItemTests: XCTestCase { tabBarViewItem.delegate = delegate } + override func tearDown() { + delegate.clear() + } + func testThatAllExpectedItemsAreShown() { tabBarViewItem.menuNeedsUpdate(menu) @@ -48,6 +52,24 @@ final class TabBarViewItemTests: XCTestCase { XCTAssertEqual(menu.item(at: 9)?.title, UserText.moveTabToNewWindow) } + func testThatMuteIsShownWhenCurrentAudioStateIsUnmuted() { + delegate.audioState = .unmuted + tabBarViewItem.menuNeedsUpdate(menu) + + XCTAssertTrue(menu.item(at: 5)?.isSeparatorItem ?? false) + XCTAssertEqual(menu.item(at: 6)?.title, UserText.muteTab) + XCTAssertTrue(menu.item(at: 7)?.isSeparatorItem ?? false) + } + + func testThatUnmuteIsShownWhenCurrentAudioStateIsMuted() { + delegate.audioState = .muted + tabBarViewItem.menuNeedsUpdate(menu) + + XCTAssertTrue(menu.item(at: 5)?.isSeparatorItem ?? false) + XCTAssertEqual(menu.item(at: 6)?.title, UserText.unmuteTab) + XCTAssertTrue(menu.item(at: 7)?.isSeparatorItem ?? false) + } + func testWhenOneTabCloseThenOtherTabsItemIsDisabled() { tabBarViewItem.menuNeedsUpdate(menu) From 469dd5bc110d5955933660f657e1620d4d7ed4cb Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Thu, 22 Feb 2024 18:21:01 +0000 Subject: [PATCH 44/44] Add identifier model to ExtractedProfile (#2188) Task/Issue URL: https://app.asana.com/0/1204167627774280/1205703170200803/f **Description**: Add identifier model to ExtractedProfile --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 8 ++--- .../DataBrokerProtection/Package.swift | 2 +- .../Model/ExtractedProfile.swift | 31 +++++++++++++++++-- ...taBrokerProfileQueryOperationManager.swift | 6 ++-- .../JSON/advancedbackgroundchecks.com.json | 16 +++++----- .../Resources/JSON/centeda.com.json | 12 +++---- .../JSON/freepeopledirectory.com.json | 10 ++++-- .../Storage/Mappers.swift | 3 +- LocalPackages/LoginItems/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/PixelKit/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- LocalPackages/SwiftUIExtensions/Package.swift | 2 +- LocalPackages/SyncUI/Package.swift | 2 +- .../SystemExtensionManager/Package.swift | 2 +- LocalPackages/XPCHelper/Package.swift | 2 +- 17 files changed, 70 insertions(+), 36 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 0a586a3376..68632bf6e3 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -13538,7 +13538,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 109.0.1; + version = 109.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 e0a1c42442..dbe440a602 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" : "da6a822844922401d80e26963b8b11dcd6ef221a", - "version" : "109.0.1" + "revision" : "da5f8ae73e7ad7fc47931f82f5ac6c4fafa6ac94", + "version" : "109.0.2" } }, { @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "revision" : "063b560e59a50e03d9b00b88a7fcb2ed2b562395", - "version" : "4.61.0" + "revision" : "36ddba2cbac52a41b9a9275af06d32fa8a56d2d7", + "version" : "4.64.0" } }, { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 64fa721012..e8853bdc23 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: "109.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.2"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper") diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/ExtractedProfile.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/ExtractedProfile.swift index 29757bc891..1fd0366c8e 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/ExtractedProfile.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/ExtractedProfile.swift @@ -77,6 +77,7 @@ struct ExtractedProfile: Codable, Sendable { var email: String? var removedDate: Date? let fullName: String? + let identifier: String? enum CodingKeys: CodingKey { case id @@ -92,6 +93,7 @@ struct ExtractedProfile: Codable, Sendable { case email case removedDate case fullName + case identifier } init(id: Int64? = nil, @@ -105,7 +107,8 @@ struct ExtractedProfile: Codable, Sendable { reportId: String? = nil, age: String? = nil, email: String? = nil, - removedDate: Date? = nil) { + removedDate: Date? = nil, + identifier: String? = nil) { self.id = id self.name = name self.alternativeNames = alternativeNames @@ -119,6 +122,29 @@ struct ExtractedProfile: Codable, Sendable { self.email = email self.removedDate = removedDate self.fullName = name + self.identifier = identifier + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decodeIfPresent(Int64.self, forKey: .id) + name = try container.decodeIfPresent(String.self, forKey: .name) + alternativeNames = try container.decodeIfPresent([String].self, forKey: .alternativeNames) + addressFull = try container.decodeIfPresent(String.self, forKey: .addressFull) + addresses = try container.decodeIfPresent([AddressCityState].self, forKey: .addresses) + phoneNumbers = try container.decodeIfPresent([String].self, forKey: .phoneNumbers) + relatives = try container.decodeIfPresent([String].self, forKey: .relatives) + profileUrl = try container.decode(String.self, forKey: .profileUrl) + reportId = try container.decodeIfPresent(String.self, forKey: .reportId) + age = try container.decodeIfPresent(String.self, forKey: .age) + email = try container.decodeIfPresent(String.self, forKey: .email) + removedDate = try container.decodeIfPresent(Date.self, forKey: .removedDate) + fullName = try container.decodeIfPresent(String.self, forKey: .fullName) + if let identifier = try container.decodeIfPresent(String.self, forKey: .identifier) { + self.identifier = identifier + } else { + self.identifier = profileUrl + } } func merge(with profile: ProfileQuery) -> ExtractedProfile { @@ -134,7 +160,8 @@ struct ExtractedProfile: Codable, Sendable { reportId: self.reportId, age: self.age ?? String(profile.age), email: self.email, - removedDate: self.removedDate + removedDate: self.removedDate, + identifier: self.identifier ) } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift index 1b043a6d41..73f08604f6 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift @@ -135,10 +135,10 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { // We check if the profile exists in the database. let extractedProfilesForBroker = database.fetchExtractedProfiles(for: brokerId) - let doesProfileExistsInDatabase = extractedProfilesForBroker.contains { $0.profileUrl == extractedProfile.profileUrl } + let doesProfileExistsInDatabase = extractedProfilesForBroker.contains { $0.identifier == extractedProfile.identifier } // If the profile exists we do not create a new opt-out operation - if doesProfileExistsInDatabase, let alreadyInDatabaseProfile = extractedProfilesForBroker.first(where: { $0.profileUrl == extractedProfile.profileUrl }), let id = alreadyInDatabaseProfile.id { + if doesProfileExistsInDatabase, let alreadyInDatabaseProfile = extractedProfilesForBroker.first(where: { $0.identifier == extractedProfile.identifier }), let id = alreadyInDatabaseProfile.id { // If it was removed in the past but was found again when scanning, it means it appearead again, so we reset the remove date. if alreadyInDatabaseProfile.removedDate != nil { database.updateRemovedDate(nil, on: id) @@ -176,7 +176,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { // Check for removed profiles let removedProfiles = brokerProfileQueryData.extractedProfiles.filter { savedProfile in !extractedProfiles.contains { recentlyFoundProfile in - recentlyFoundProfile.profileUrl == savedProfile.profileUrl + recentlyFoundProfile.identifier == savedProfile.identifier } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/advancedbackgroundchecks.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/advancedbackgroundchecks.com.json index ee35af6d17..d7daac5036 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/advancedbackgroundchecks.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/advancedbackgroundchecks.com.json @@ -1,9 +1,9 @@ { "name": "AdvancedBackgroundChecks", "url": "advancedbackgroundchecks.com", - "version": "0.1.4", + "version": "0.1.5", "parent": "peoplefinders.com", - "addedDatetime": 1678082400000, + "addedDatetime": 1678060800000, "steps": [ { "stepType": "scan", @@ -11,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "c73ba931-9e01-4d37-9e15-2fd7a14eefa3", + "id": "7967f064-e3c5-442d-8380-99cf752fb8df", "url": "https://www.advancedbackgroundchecks.com/names/${firstName}-${lastName}_${city}-${state}_age_${age}" }, { "actionType": "extract", - "id": "94003082-0a9d-4418-ac88-68595c7f4953", + "id": "6f6bb616-a4cb-4231-9abb-522722208f95", "selector": ".card-block", "profile": { "name": { @@ -25,7 +25,8 @@ }, "alternativeNamesList": { "selector": "(.//p[@class='card-text max-lines-1'])[1]", - "afterText": "AKA:" + "afterText": "AKA:", + "separator": "," }, "age": { "selector": ".card-title", @@ -40,7 +41,8 @@ }, "relativesList": { "selector": "(.//p[@class='card-text max-lines-1'])[2]", - "afterText": "Related to:" + "afterText": "Related to:", + "separator": "," }, "profileUrl": { "selector": ".link-to-details" @@ -60,4 +62,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} \ No newline at end of file +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/centeda.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/centeda.com.json index bb15f0093f..7050bab137 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/centeda.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/centeda.com.json @@ -3,7 +3,7 @@ "url": "centeda.com", "version": "0.1.4", "parent": "verecor.com", - "addedDatetime": 1677736800000, + "addedDatetime": 1677715200000, "steps": [ { "stepType": "scan", @@ -11,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "2f6639c0-201f-4d5e-8467-ae0ba457b409", + "id": "af9c9f03-e778-4c29-85fc-e5cbbfec563c", "url": "https://centeda.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -25,8 +25,8 @@ }, { "actionType": "extract", - "id": "e2e236b0-515b-43b3-9154-0432ed9b7566", - "selector": ".search-item", + "id": "79fa2a1c-65b4-417a-a8ac-2ca6d729ffc1", + "selector": ".search-result > a", "profile": { "name": { "selector": ".title", @@ -47,7 +47,7 @@ "selector": ".//div[@class='col-sm-24 col-md-8 related-to']//li" }, "profileUrl": { - "selector": ".get-report-btn" + "selector": "a" } } } @@ -64,4 +64,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} \ No newline at end of file +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/freepeopledirectory.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/freepeopledirectory.com.json index c448989448..9d114d48b3 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/freepeopledirectory.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/freepeopledirectory.com.json @@ -11,17 +11,21 @@ "actions": [ { "actionType": "navigate", - "id": "4c607417-36bc-47d4-8562-9c2244db354d", + "id": "b8b912b0-201d-4cd1-8237-235c34fe0fea", "url": "https://www.freepeopledirectory.com/name/${firstName}-${lastName}/${state|upcase}/${city}" }, { "actionType": "extract", - "id": "a1637310-ca7a-40b0-b2f5-db22b43b5d54", + "id": "50e30922-ef1d-4820-abbd-f536378472d4", "selector": ".whole-card", "profile": { "name": { "selector": ".card-title" }, + "alternativeNamesList": { + "selector": ".//h3/span[contains(text(),'AKA:')]/following-sibling::span", + "afterText": "No other aliases." + }, "addressCityState": { "selector": ".city" }, @@ -50,4 +54,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} \ No newline at end of file +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/Mappers.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/Mappers.swift index 56b08dd5f2..81cf02f70d 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/Mappers.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/Mappers.swift @@ -233,7 +233,8 @@ struct MapperToModel { reportId: extractedProfile.reportId, age: extractedProfile.age, email: extractedProfile.email, - removedDate: extractedProfileDB.removedDate) + removedDate: extractedProfileDB.removedDate, + identifier: extractedProfile.identifier) } func mapToModel(_ scanEvent: ScanHistoryEventDB) throws -> HistoryEvent { diff --git a/LocalPackages/LoginItems/Package.swift b/LocalPackages/LoginItems/Package.swift index a631a199d9..b7b7bb72d9 100644 --- a/LocalPackages/LoginItems/Package.swift +++ b/LocalPackages/LoginItems/Package.swift @@ -13,7 +13,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.2"), ], targets: [ .target( diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index e2dc908671..f1e2140732 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.2"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems") diff --git a/LocalPackages/PixelKit/Package.swift b/LocalPackages/PixelKit/Package.swift index 1222931baa..0cc6651ae2 100644 --- a/LocalPackages/PixelKit/Package.swift +++ b/LocalPackages/PixelKit/Package.swift @@ -20,7 +20,7 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.2"), ], targets: [ .target( diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index e5192fc149..b7af9e0bbb 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.2"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/LocalPackages/SwiftUIExtensions/Package.swift b/LocalPackages/SwiftUIExtensions/Package.swift index 10d667a750..249d2a6cbb 100644 --- a/LocalPackages/SwiftUIExtensions/Package.swift +++ b/LocalPackages/SwiftUIExtensions/Package.swift @@ -11,7 +11,7 @@ let package = Package( .library(name: "PreferencesViews", targets: ["PreferencesViews"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.2"), ], targets: [ .target( diff --git a/LocalPackages/SyncUI/Package.swift b/LocalPackages/SyncUI/Package.swift index b927525124..8928521a9e 100644 --- a/LocalPackages/SyncUI/Package.swift +++ b/LocalPackages/SyncUI/Package.swift @@ -14,7 +14,7 @@ let package = Package( ], dependencies: [ .package(path: "../SwiftUIExtensions"), - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.2"), ], targets: [ .target( diff --git a/LocalPackages/SystemExtensionManager/Package.swift b/LocalPackages/SystemExtensionManager/Package.swift index 43bafef378..0ae14903d5 100644 --- a/LocalPackages/SystemExtensionManager/Package.swift +++ b/LocalPackages/SystemExtensionManager/Package.swift @@ -16,7 +16,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.2"), ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. diff --git a/LocalPackages/XPCHelper/Package.swift b/LocalPackages/XPCHelper/Package.swift index e62141ec7a..323f247b91 100644 --- a/LocalPackages/XPCHelper/Package.swift +++ b/LocalPackages/XPCHelper/Package.swift @@ -30,7 +30,7 @@ let package = Package( .library(name: "XPCHelper", targets: ["XPCHelper"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.2"), ], targets: [ .target(