diff --git a/.github/workflows/bump_internal_release.yml b/.github/workflows/bump_internal_release.yml index 4d0d9f440a..13280743b5 100644 --- a/.github/workflows/bump_internal_release.yml +++ b/.github/workflows/bump_internal_release.yml @@ -118,10 +118,10 @@ jobs: 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 - release_notes="$(" ]]; then - echo "::error::Release notes are empty. Please add release notes to the Asana task and restart the workflow." + | ./scripts/extract_release_notes.sh -r > raw_release_notes.txt + raw_release_notes="$("* ]]; then + echo "::error::Release notes are empty or contain a placeholder. Please add release notes to the Asana task and restart the workflow." exit 1 fi diff --git a/.github/workflows/code_freeze.yml b/.github/workflows/code_freeze.yml index 2da53a932a..755282af5e 100644 --- a/.github/workflows/code_freeze.yml +++ b/.github/workflows/code_freeze.yml @@ -172,6 +172,7 @@ jobs: uses: ./.github/workflows/tag_release.yml with: asana-task-url: ${{ needs.create_release_branch.outputs.asana_task_url }} + base-branch: ${{ github.ref_name }} branch: ${{ needs.create_release_branch.outputs.release_branch_name }} prerelease: true secrets: diff --git a/.github/workflows/create_variant.yml b/.github/workflows/create_variant.yml index 682467036d..c51a9c0697 100644 --- a/.github/workflows/create_variant.yml +++ b/.github/workflows/create_variant.yml @@ -86,14 +86,28 @@ jobs: .github scripts + - name: Download DMG artifact + id: download-dmg-artifact + continue-on-error: true + uses: actions/download-artifact@v4 + with: + name: duckduckgo-dmg + path: ${{ github.workspace }} + - name: Download release app + # Download the release app only if download-dmg-artifact fails + if: ${{ steps.download-dmg-artifact.outcome == 'failure' }} run: | curl -fLSs "${{ vars.RELEASE_DMG_URL }}" --output duckduckgo.dmg - hdiutil attach duckduckgo.dmg -mountpoint vanilla - mkdir -p dmg - cp -R vanilla/DuckDuckGo.app dmg/DuckDuckGo.app - hdiutil detach vanilla - rm -f duckduckgo.dmg + + - name: Extract App from DMG + id: extract-app-from-dmg + run: | + hdiutil attach duckduckgo.dmg -mountpoint vanilla + mkdir -p dmg + cp -R vanilla/DuckDuckGo.app dmg/DuckDuckGo.app + hdiutil detach vanilla + rm -f duckduckgo.dmg - name: Install create-dmg run: brew install create-dmg diff --git a/.github/workflows/create_variants.yml b/.github/workflows/create_variants.yml index ccddcecbb2..c48424a947 100644 --- a/.github/workflows/create_variants.yml +++ b/.github/workflows/create_variants.yml @@ -72,11 +72,29 @@ jobs: atb-asana-task-id: ${{ vars.DMG_VARIANTS_LIST_TASK_ID }} origin-asana-section-id: ${{ vars.DMG_VARIANTS_ORIGIN_SECTION_ID }} - + download-dmg-and-upload-artifact: + + name: Download Release App and upload artifact + + runs-on: macos-13 + timeout-minutes: 15 + + steps: + - name: Download release app + run: | + curl -fLSs "${{ vars.RELEASE_DMG_URL }}" --output duckduckgo.dmg + + - name: Upload DMG artifact + uses: actions/upload-artifact@v4 + with: + name: duckduckgo-dmg + path: ${{ github.workspace }}/duckduckgo.dmg + retention-days: 1 + create-variants: name: Create Variant - needs: set-up-variants + needs: [set-up-variants, download-dmg-and-upload-artifact] strategy: fail-fast: false diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 528b219281..fd2341e1b6 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -54,6 +54,36 @@ jobs: env: SHELLCHECK_OPTS: -x -P scripts -P scripts/helpers + bats: + + name: Test Shell Scripts + + runs-on: macos-13 + + steps: + - name: Check out the code + if: github.event_name == 'pull_request' || github.event_name == 'push' + uses: actions/checkout@v4 + + - name: Check out the code + if: github.event_name != 'pull_request' && github.event_name != 'push' + uses: actions/checkout@v4 + with: + ref: ${{ inputs.branch || github.ref_name }} + + - name: Install Bats + run: brew install bats-core + + - name: Run Bats tests + run: bats --formatter junit scripts/tests/* > bats-tests.xml + + - name: Publish unit tests report + uses: mikepenz/action-junit-report@v3 + if: always() # always run even if the previous step fails + with: + check_name: "Test Report: Shell Scripts" + report_paths: 'bats-tests.xml' + tests: name: Test @@ -343,7 +373,7 @@ jobs: create-asana-task: name: Create Asana Task - needs: [swiftlint, tests, release-build, verify-autoconsent-bundle, private-api] + needs: [swiftlint, bats, tests, release-build, verify-autoconsent-bundle, private-api] if: failure() && github.ref_name == 'main' && github.run_attempt == 1 @@ -360,7 +390,7 @@ jobs: close-asana-task: name: Close Asana Task - needs: [swiftlint, tests, release-build, verify-autoconsent-bundle, private-api] + needs: [swiftlint, bats, tests, release-build, verify-autoconsent-bundle, private-api] if: success() && github.ref_name == 'main' && github.run_attempt > 1 diff --git a/.github/workflows/publish_dmg_release.yml b/.github/workflows/publish_dmg_release.yml index 8daecea7be..2dd82d53ed 100644 --- a/.github/workflows/publish_dmg_release.yml +++ b/.github/workflows/publish_dmg_release.yml @@ -145,14 +145,14 @@ jobs: 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 - release_notes="$(" ]]; then - echo "::error::Release notes are empty. Please add release notes to the Asana task and restart the workflow." + | jq -r .data.notes > release_task_content.txt + raw_release_notes="$(./scripts/extract_release_notes.sh -r < release_task_content.txt)" + if [[ ${#raw_release_notes} == 0 || "$raw_release_notes" == *"<-- Add release notes here -->"* ]]; then + echo "::error::Release notes are empty or contain a placeholder. Please add release notes to the Asana task and restart the workflow." exit 1 fi - echo "RELEASE_NOTES_FILE=release_notes.txt" >> $GITHUB_ENV + ./scripts/extract_release_notes.sh < release_task_content.txt > release_notes.html + echo "RELEASE_NOTES_FILE=release_notes.html" >> $GITHUB_ENV - name: Set up Sparkle tools env: @@ -189,21 +189,21 @@ jobs: ./scripts/appcast_manager/appcastManager.swift \ --release-to-internal-channel \ --dmg ${DMG_PATH} \ - --release-notes release_notes.txt \ + --release-notes-html release_notes.html \ --key sparkle_private_key ;; "public") ./scripts/appcast_manager/appcastManager.swift \ --release-to-public-channel \ --version ${VERSION} \ - --release-notes release_notes.txt \ + --release-notes-html release_notes.html \ --key sparkle_private_key ;; "hotfix") ./scripts/appcast_manager/appcastManager.swift \ --release-hotfix-to-public-channel \ --dmg ${DMG_PATH} \ - --release-notes release_notes.txt \ + --release-notes-html release_notes.html \ --key sparkle_private_key ;; *) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 199ecb7d47..fe80d801b7 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 169 +CURRENT_PROJECT_VERSION = 176 diff --git a/Configuration/Version.xcconfig b/Configuration/Version.xcconfig index 1e0572489f..b517e1e1fb 100644 --- a/Configuration/Version.xcconfig +++ b/Configuration/Version.xcconfig @@ -1 +1 @@ -MARKETING_VERSION = 1.84.0 +MARKETING_VERSION = 1.85.0 diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index c0c1e1d6f0..228dd20d50 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -134,7 +134,6 @@ 1DFAB5232A8983E100A0F7F6 /* SetExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFAB51F2A89830D00A0F7F6 /* SetExtensionTests.swift */; }; 1E0C72062ABC63BD00802009 /* SubscriptionPagesUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E0C72052ABC63BD00802009 /* SubscriptionPagesUserScript.swift */; }; 1E0C72072ABC63BD00802009 /* SubscriptionPagesUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E0C72052ABC63BD00802009 /* SubscriptionPagesUserScript.swift */; }; - 1E46E1A02BD029BD0007273A /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = 1E46E19F2BD029BD0007273A /* Subscription */; }; 1E559BB12BBCA9F1002B4AF6 /* RedirectNavigationResponder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E559BB02BBCA9F1002B4AF6 /* RedirectNavigationResponder.swift */; }; 1E559BB22BBCA9F1002B4AF6 /* RedirectNavigationResponder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E559BB02BBCA9F1002B4AF6 /* RedirectNavigationResponder.swift */; }; 1E7E2E9029029A2A00C01B54 /* ContentBlockingRulesUpdateObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7E2E8F29029A2A00C01B54 /* ContentBlockingRulesUpdateObserver.swift */; }; @@ -166,6 +165,12 @@ 315AA07028CA5CC800200030 /* YoutubePlayerNavigationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 315AA06F28CA5CC800200030 /* YoutubePlayerNavigationHandler.swift */; }; 3168506D2AF3AD1D009A2828 /* WaitlistViewControllerPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3168506C2AF3AD1C009A2828 /* WaitlistViewControllerPresenter.swift */; }; 3168506E2AF3AD1D009A2828 /* WaitlistViewControllerPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3168506C2AF3AD1C009A2828 /* WaitlistViewControllerPresenter.swift */; }; + 316913232BD2B6250051B46D /* DataBrokerProtectionPixelsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316913222BD2B6250051B46D /* DataBrokerProtectionPixelsHandler.swift */; }; + 316913242BD2B6250051B46D /* DataBrokerProtectionPixelsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316913222BD2B6250051B46D /* DataBrokerProtectionPixelsHandler.swift */; }; + 316913262BD2B76F0051B46D /* DataBrokerPrerequisitesStatusVerifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316913252BD2B76F0051B46D /* DataBrokerPrerequisitesStatusVerifier.swift */; }; + 316913272BD2B76F0051B46D /* DataBrokerPrerequisitesStatusVerifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316913252BD2B76F0051B46D /* DataBrokerPrerequisitesStatusVerifier.swift */; }; + 316913292BD2C7570051B46D /* DataBrokerProtectionErrorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316913282BD2C7570051B46D /* DataBrokerProtectionErrorViewController.swift */; }; + 3169132A2BD2C7570051B46D /* DataBrokerProtectionErrorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316913282BD2C7570051B46D /* DataBrokerProtectionErrorViewController.swift */; }; 3171D6B82889849F0068632A /* CookieManagedNotificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3171D6B72889849F0068632A /* CookieManagedNotificationView.swift */; }; 3171D6BA288984D00068632A /* BadgeAnimationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3171D6B9288984D00068632A /* BadgeAnimationView.swift */; }; 3171D6DB2889B64D0068632A /* CookieManagedNotificationContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3171D6DA2889B64D0068632A /* CookieManagedNotificationContainerView.swift */; }; @@ -186,6 +191,8 @@ 31C9ADE62AF0564500CEF57D /* WaitlistFeatureSetupHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C9ADE42AF0564500CEF57D /* WaitlistFeatureSetupHandler.swift */; }; 31CF3432288B0B1B0087244B /* NavigationBarBadgeAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31CF3431288B0B1B0087244B /* NavigationBarBadgeAnimator.swift */; }; 31D5375C291D944100407A95 /* PasswordManagementBitwardenItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D5375B291D944100407A95 /* PasswordManagementBitwardenItemView.swift */; }; + 31DC2F222BD6DE6C001354EF /* DataBrokerPrerequisitesStatusVerifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31DC2F202BD6DE65001354EF /* DataBrokerPrerequisitesStatusVerifierTests.swift */; }; + 31DC2F232BD6E028001354EF /* DataBrokerPrerequisitesStatusVerifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31DC2F202BD6DE65001354EF /* DataBrokerPrerequisitesStatusVerifierTests.swift */; }; 31E163BA293A56F400963C10 /* BrokenSiteReportingReferenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31E163B9293A56F400963C10 /* BrokenSiteReportingReferenceTests.swift */; }; 31E163BD293A579E00963C10 /* PrivacyReferenceTestHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31E163BC293A579E00963C10 /* PrivacyReferenceTestHelper.swift */; }; 31E163C0293A581900963C10 /* privacy-reference-tests in Resources */ = {isa = PBXBuildFile; fileRef = 31E163BF293A581900963C10 /* privacy-reference-tests */; }; @@ -611,7 +618,7 @@ 3706FC71293F65D500E42796 /* NSColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41D174025CB131900472416 /* NSColorExtension.swift */; }; 3706FC73293F65D500E42796 /* AddressBarButtonsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC5E4F525D6BF2C007F5990 /* AddressBarButtonsViewController.swift */; }; 3706FC77293F65D500E42796 /* PageObserverUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 853014D525E671A000FB8205 /* PageObserverUserScript.swift */; }; - 3706FC78293F65D500E42796 /* SecureVaultErrorReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B642738127B65BAC0005DFD1 /* SecureVaultErrorReporter.swift */; }; + 3706FC78293F65D500E42796 /* SecureVaultReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B642738127B65BAC0005DFD1 /* SecureVaultReporter.swift */; }; 3706FC79293F65D500E42796 /* NSImageExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B139AFC26B60BD800894F82 /* NSImageExtensions.swift */; }; 3706FC7B293F65D500E42796 /* PasswordManagementViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85625995269C953C00EE44BC /* PasswordManagementViewController.swift */; }; 3706FC7C293F65D500E42796 /* ImportedBookmarks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB99CFA26FE191E001E4761 /* ImportedBookmarks.swift */; }; @@ -630,7 +637,6 @@ 3706FC8C293F65D500E42796 /* FirefoxDataImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B5FF67726B602B100D42879 /* FirefoxDataImporter.swift */; }; 3706FC8D293F65D500E42796 /* PreferencesGeneralView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AFCE8A27DB69BC00471A10 /* PreferencesGeneralView.swift */; }; 3706FC8E293F65D500E42796 /* PinnedTabsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BF3F1F286F0A7A00BD9014 /* PinnedTabsView.swift */; }; - 3706FC92293F65D500E42796 /* NSStoryboardExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE0DF0426781961006337B7 /* NSStoryboardExtension.swift */; }; 3706FC93293F65D500E42796 /* PreferencesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AFCE8027DA2CA600471A10 /* PreferencesViewController.swift */; }; 3706FC94293F65D500E42796 /* FireproofDomains.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B02198125E05FAC00ED7DEA /* FireproofDomains.swift */; }; 3706FC95293F65D500E42796 /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B677440255DBEEA00025BD8 /* Database.swift */; }; @@ -947,7 +953,6 @@ 373A1AB228451ED400586521 /* BookmarksHTMLImporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373A1AB128451ED400586521 /* BookmarksHTMLImporterTests.swift */; }; 373D9B4829EEAC1B00381FDD /* SyncMetadataDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373D9B4729EEAC1B00381FDD /* SyncMetadataDatabase.swift */; }; 373D9B4929EEAC1B00381FDD /* SyncMetadataDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373D9B4729EEAC1B00381FDD /* SyncMetadataDatabase.swift */; }; - 373FB4B12B4D6C42004C88D6 /* PreferencesViews in Frameworks */ = {isa = PBXBuildFile; productRef = 373FB4B02B4D6C42004C88D6 /* PreferencesViews */; }; 373FB4B32B4D6C4B004C88D6 /* PreferencesViews in Frameworks */ = {isa = PBXBuildFile; productRef = 373FB4B22B4D6C4B004C88D6 /* PreferencesViews */; }; 37445F992A1566420029F789 /* SyncDataProviders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37445F982A1566420029F789 /* SyncDataProviders.swift */; }; 37445F9A2A1566420029F789 /* SyncDataProviders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37445F982A1566420029F789 /* SyncDataProviders.swift */; }; @@ -1028,8 +1033,6 @@ 37CD54D027F2FDD100F1F7B9 /* DefaultBrowserPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CD54C827F2FDD100F1F7B9 /* DefaultBrowserPreferences.swift */; }; 37CEFCA92A6737A2001EF741 /* CredentialsCleanupErrorHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEFCA82A6737A2001EF741 /* CredentialsCleanupErrorHandling.swift */; }; 37CEFCAA2A6737A2001EF741 /* CredentialsCleanupErrorHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEFCA82A6737A2001EF741 /* CredentialsCleanupErrorHandling.swift */; }; - 37CF91592BB416A500BADCAE /* Crashes in Frameworks */ = {isa = PBXBuildFile; productRef = 37CF91582BB416A500BADCAE /* Crashes */; }; - 37CF915B2BB416AC00BADCAE /* Crashes in Frameworks */ = {isa = PBXBuildFile; productRef = 37CF915A2BB416AC00BADCAE /* Crashes */; }; 37D2377A287EB8CA00BCE03B /* TabIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D23779287EB8CA00BCE03B /* TabIndex.swift */; }; 37D2377C287EBDA300BCE03B /* TabIndexTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D2377B287EBDA300BCE03B /* TabIndexTests.swift */; }; 37D23780287EFEE200BCE03B /* PinnedTabsManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D2377F287EFEE200BCE03B /* PinnedTabsManagerTests.swift */; }; @@ -1080,14 +1083,12 @@ 4B1E6EF227AB5E5D00F51793 /* PasswordManagementItemList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B1E6EF027AB5E5D00F51793 /* PasswordManagementItemList.swift */; }; 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 */; }; 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 */; }; 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 */; }; 4B2D06322A11C1D300DE1F49 /* NSApplicationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5C8F622591021700748EB7 /* NSApplicationExtension.swift */; }; 4B2D06332A11C1E300DE1F49 /* OptionalExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B637273C26CCF0C200C8CB02 /* OptionalExtension.swift */; }; 4B2D065B2A11D1FF00DE1F49 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4BEC322A11B509001D9AC5 /* Logging.swift */; }; @@ -1143,7 +1144,6 @@ 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 /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; 4B4D60A12A0B2D6100BCD287 /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; @@ -1210,8 +1210,6 @@ 4B7534CC2A1FD7EA00158A99 /* NetworkProtectionInviteDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D606C2A0B29FA00BCD287 /* NetworkProtectionInviteDialog.swift */; }; 4B7A57CF279A4EF300B1C70E /* ChromiumPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B7A57CE279A4EF300B1C70E /* ChromiumPreferences.swift */; }; 4B7A60A1273E0BE400BBDFEB /* WKWebsiteDataStoreExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B7A60A0273E0BE400BBDFEB /* WKWebsiteDataStoreExtension.swift */; }; - 4B81AD352B29512B00706C96 /* PixelKitTestingUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = 4B81AD342B29512B00706C96 /* PixelKitTestingUtilities */; }; - 4B81AD372B29513100706C96 /* PixelKitTestingUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = 4B81AD362B29513100706C96 /* PixelKitTestingUtilities */; }; 4B85A48028821CC500FC4C39 /* NSPasteboardItemExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B85A47F28821CC500FC4C39 /* NSPasteboardItemExtension.swift */; }; 4B8A4DFF27C83B29005F40E8 /* SaveIdentityViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8A4DFE27C83B29005F40E8 /* SaveIdentityViewController.swift */; }; 4B8A4E0127C8447E005F40E8 /* SaveIdentityPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8A4E0027C8447E005F40E8 /* SaveIdentityPopover.swift */; }; @@ -1361,7 +1359,6 @@ 4BD57C042AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD57C032AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift */; }; 4BD57C052AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD57C032AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift */; }; 4BDFA4AE27BF19E500648192 /* ToggleableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BDFA4AD27BF19E500648192 /* ToggleableScrollView.swift */; }; - 4BE0DF06267819A1006337B7 /* NSStoryboardExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE0DF0426781961006337B7 /* NSStoryboardExtension.swift */; }; 4BE344EE2B2376DF003FC223 /* VPNFeedbackFormViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE344ED2B2376DF003FC223 /* VPNFeedbackFormViewModelTests.swift */; }; 4BE344EF2B23786F003FC223 /* VPNFeedbackFormViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE344ED2B2376DF003FC223 /* VPNFeedbackFormViewModelTests.swift */; }; 4BE4005327CF3DC3007D3161 /* SavePaymentMethodPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE4005227CF3DC3007D3161 /* SavePaymentMethodPopover.swift */; }; @@ -1483,8 +1480,10 @@ 7B430EA12A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B430EA02A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift */; }; 7B430EA22A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B430EA02A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift */; }; 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 */; }; + 7B4D8A212BDA857300852966 /* VPNOperationErrorRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4D8A202BDA857300852966 /* VPNOperationErrorRecorder.swift */; }; + 7B4D8A222BDA857300852966 /* VPNOperationErrorRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4D8A202BDA857300852966 /* VPNOperationErrorRecorder.swift */; }; + 7B4D8A232BDA857300852966 /* VPNOperationErrorRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4D8A202BDA857300852966 /* VPNOperationErrorRecorder.swift */; }; + 7B4D8A242BDA857300852966 /* VPNOperationErrorRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4D8A202BDA857300852966 /* VPNOperationErrorRecorder.swift */; }; 7B624F172BA25C1F00A6C544 /* NetworkProtectionUI in Frameworks */ = {isa = PBXBuildFile; productRef = 7B624F162BA25C1F00A6C544 /* NetworkProtectionUI */; }; 7B7DFB202B7E736B009EA1A3 /* MacPacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */; }; 7B7DFB222B7E7473009EA1A3 /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = 7B7DFB212B7E7473009EA1A3 /* Networking */; }; @@ -1498,8 +1497,6 @@ 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 */; }; @@ -1538,10 +1535,6 @@ 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 */; }; - 7BBE650D2BC67BA0008F4EE9 /* NetworkProtectionIPCTunnelControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBE650C2BC67BA0008F4EE9 /* NetworkProtectionIPCTunnelControllerTests.swift */; }; - 7BBE650E2BC67BA0008F4EE9 /* NetworkProtectionIPCTunnelControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBE650C2BC67BA0008F4EE9 /* NetworkProtectionIPCTunnelControllerTests.swift */; }; - 7BBE65102BC67EED008F4EE9 /* NetworkProtectionTestingSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBE650F2BC67EED008F4EE9 /* NetworkProtectionTestingSupport.swift */; }; - 7BBE65112BC67EED008F4EE9 /* NetworkProtectionTestingSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBE650F2BC67EED008F4EE9 /* NetworkProtectionTestingSupport.swift */; }; 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 */; }; @@ -1557,8 +1550,6 @@ 7BEEA5122AD1235B00A9E72B /* NetworkProtectionIPC in Frameworks */ = {isa = PBXBuildFile; productRef = 7BEEA5112AD1235B00A9E72B /* NetworkProtectionIPC */; }; 7BEEA5142AD1236300A9E72B /* NetworkProtectionIPC in Frameworks */ = {isa = PBXBuildFile; productRef = 7BEEA5132AD1236300A9E72B /* NetworkProtectionIPC */; }; 7BEEA5162AD1236E00A9E72B /* NetworkProtectionUI in Frameworks */ = {isa = PBXBuildFile; productRef = 7BEEA5152AD1236E00A9E72B /* NetworkProtectionUI */; }; - 7BFCB74E2ADE7E1A00DA3EA7 /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7BFCB74D2ADE7E1A00DA3EA7 /* PixelKit */; }; - 7BFCB7502ADE7E2300DA3EA7 /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7BFCB74F2ADE7E2300DA3EA7 /* PixelKit */; }; 7BFE95522A9DF1CE0081ABE9 /* NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFE95512A9DF1CE0081ABE9 /* NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift */; }; 7BFE95542A9DF2930081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFE95532A9DF2930081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift */; }; 7BFE95552A9DF2990081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFE95532A9DF2930081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift */; }; @@ -1683,8 +1674,6 @@ 98A50964294B691800D10880 /* Persistence in Frameworks */ = {isa = PBXBuildFile; productRef = 98A50963294B691800D10880 /* Persistence */; }; 98A95D88299A2DF900B9B81A /* BookmarkMigrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98A95D87299A2DF900B9B81A /* BookmarkMigrationTests.swift */; }; 98EB5D1027516A4800681FE6 /* AppPrivacyConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98EB5D0F27516A4800681FE6 /* AppPrivacyConfigurationTests.swift */; }; - 9D6983F92AC773C3002C02FC /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 9D6983F82AC773C3002C02FC /* PixelKit */; }; - 9D6983FB2AC773C8002C02FC /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 9D6983FA2AC773C8002C02FC /* PixelKit */; }; 9D9AE8692AA76CDC0026E7DC /* LoginItem+NetworkProtection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AE8682AA76CDC0026E7DC /* LoginItem+NetworkProtection.swift */; }; 9D9AE86B2AA76CF90026E7DC /* LoginItemsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AE86A2AA76CF90026E7DC /* LoginItemsManager.swift */; }; 9D9AE86C2AA76D1B0026E7DC /* LoginItemsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AE86A2AA76CF90026E7DC /* LoginItemsManager.swift */; }; @@ -1701,11 +1690,18 @@ 9D9AE92A2AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AE9282AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift */; }; 9D9AE92C2AAB84FF0026E7DC /* DBPMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AE92B2AAB84FF0026E7DC /* DBPMocks.swift */; }; 9D9AE92D2AAB84FF0026E7DC /* DBPMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AE92B2AAB84FF0026E7DC /* DBPMocks.swift */; }; - 9DB6E7242AA0DC5800A17F3C /* LoginItems in Frameworks */ = {isa = PBXBuildFile; productRef = 9DB6E7232AA0DC5800A17F3C /* LoginItems */; }; 9DC70B1A2AA1FA5B005A844B /* LoginItems in Frameworks */ = {isa = PBXBuildFile; productRef = 9DC70B192AA1FA5B005A844B /* LoginItems */; }; 9DEF97E12B06C4EE00764F03 /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = 9DEF97E02B06C4EE00764F03 /* Networking */; }; 9F0A2CF82B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0A2CF72B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift */; }; 9F0A2CF92B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0A2CF72B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift */; }; + 9F0FFFB42BCCAE37007C87DD /* BookmarkAllTabsDialogCoordinatorViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0FFFB32BCCAE37007C87DD /* BookmarkAllTabsDialogCoordinatorViewModelTests.swift */; }; + 9F0FFFB52BCCAE37007C87DD /* BookmarkAllTabsDialogCoordinatorViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0FFFB32BCCAE37007C87DD /* BookmarkAllTabsDialogCoordinatorViewModelTests.swift */; }; + 9F0FFFB82BCCAE9C007C87DD /* AddEditBookmarkDialogViewModelMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0FFFB72BCCAE9C007C87DD /* AddEditBookmarkDialogViewModelMock.swift */; }; + 9F0FFFB92BCCAE9C007C87DD /* AddEditBookmarkDialogViewModelMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0FFFB72BCCAE9C007C87DD /* AddEditBookmarkDialogViewModelMock.swift */; }; + 9F0FFFBB2BCCAEC2007C87DD /* AddEditBookmarkFolderDialogViewModelMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0FFFBA2BCCAEC2007C87DD /* AddEditBookmarkFolderDialogViewModelMock.swift */; }; + 9F0FFFBC2BCCAEC2007C87DD /* AddEditBookmarkFolderDialogViewModelMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0FFFBA2BCCAEC2007C87DD /* AddEditBookmarkFolderDialogViewModelMock.swift */; }; + 9F0FFFBE2BCCAF1F007C87DD /* BookmarkAllTabsDialogViewModelMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0FFFBD2BCCAF1F007C87DD /* BookmarkAllTabsDialogViewModelMock.swift */; }; + 9F0FFFBF2BCCAF1F007C87DD /* BookmarkAllTabsDialogViewModelMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0FFFBD2BCCAF1F007C87DD /* BookmarkAllTabsDialogViewModelMock.swift */; }; 9F180D0F2B69C553000D695F /* Tab+WKUIDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F180D0E2B69C553000D695F /* Tab+WKUIDelegateTests.swift */; }; 9F180D102B69C553000D695F /* Tab+WKUIDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F180D0E2B69C553000D695F /* Tab+WKUIDelegateTests.swift */; }; 9F180D122B69C665000D695F /* DownloadsTabExtensionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F180D112B69C665000D695F /* DownloadsTabExtensionMock.swift */; }; @@ -1738,10 +1734,20 @@ 9F872DA12B90644800138637 /* ContextualMenuTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872D9F2B90644800138637 /* ContextualMenuTests.swift */; }; 9F872DA32B90920F00138637 /* BookmarkFolderInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872DA22B90920F00138637 /* BookmarkFolderInfo.swift */; }; 9F872DA42B90920F00138637 /* BookmarkFolderInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872DA22B90920F00138637 /* BookmarkFolderInfo.swift */; }; + 9F8D57322BCCCB9A00AEA660 /* UserDefaultsBookmarkFoldersStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F8D57312BCCCB9A00AEA660 /* UserDefaultsBookmarkFoldersStoreTests.swift */; }; + 9F8D57332BCCCB9A00AEA660 /* UserDefaultsBookmarkFoldersStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F8D57312BCCCB9A00AEA660 /* UserDefaultsBookmarkFoldersStoreTests.swift */; }; 9F982F0D2B8224BF00231028 /* AddEditBookmarkFolderDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */; }; 9F982F0E2B8224BF00231028 /* AddEditBookmarkFolderDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */; }; 9F982F132B822B7B00231028 /* AddEditBookmarkFolderDialogViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F982F112B82268F00231028 /* AddEditBookmarkFolderDialogViewModelTests.swift */; }; 9F982F142B822C7400231028 /* AddEditBookmarkFolderDialogViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F982F112B82268F00231028 /* AddEditBookmarkFolderDialogViewModelTests.swift */; }; + 9F9C49F62BC786790099738D /* MoreOptionsMenu+BookmarksTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9C49F52BC786790099738D /* MoreOptionsMenu+BookmarksTests.swift */; }; + 9F9C49F72BC786790099738D /* MoreOptionsMenu+BookmarksTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9C49F52BC786790099738D /* MoreOptionsMenu+BookmarksTests.swift */; }; + 9F9C49F92BC7BC970099738D /* BookmarkAllTabsDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9C49F82BC7BC970099738D /* BookmarkAllTabsDialogView.swift */; }; + 9F9C49FA2BC7BC970099738D /* BookmarkAllTabsDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9C49F82BC7BC970099738D /* BookmarkAllTabsDialogView.swift */; }; + 9F9C49FD2BC7E9830099738D /* BookmarkAllTabsDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9C49FC2BC7E9820099738D /* BookmarkAllTabsDialogViewModel.swift */; }; + 9F9C49FE2BC7E9830099738D /* BookmarkAllTabsDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9C49FC2BC7E9820099738D /* BookmarkAllTabsDialogViewModel.swift */; }; + 9F9C4A012BC7F36D0099738D /* BookmarkAllTabsDialogCoordinatorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9C4A002BC7F36D0099738D /* BookmarkAllTabsDialogCoordinatorViewModel.swift */; }; + 9F9C4A022BC7F36D0099738D /* BookmarkAllTabsDialogCoordinatorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9C4A002BC7F36D0099738D /* BookmarkAllTabsDialogCoordinatorViewModel.swift */; }; 9FA173DA2B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173D92B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift */; }; 9FA173DB2B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173D92B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift */; }; 9FA173DF2B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173DE2B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift */; }; @@ -1752,8 +1758,18 @@ 9FA173E82B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173E62B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift */; }; 9FA173EB2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173EA2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift */; }; 9FA173EC2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173EA2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift */; }; + 9FA5A0A52BC8F34900153786 /* UserDefaultsBookmarkFoldersStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA5A0A42BC8F34900153786 /* UserDefaultsBookmarkFoldersStore.swift */; }; + 9FA5A0A62BC8F34900153786 /* UserDefaultsBookmarkFoldersStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA5A0A42BC8F34900153786 /* UserDefaultsBookmarkFoldersStore.swift */; }; + 9FA5A0A92BC900FC00153786 /* BookmarkAllTabsDialogViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA5A0A82BC900FC00153786 /* BookmarkAllTabsDialogViewModelTests.swift */; }; + 9FA5A0AA2BC900FC00153786 /* BookmarkAllTabsDialogViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA5A0A82BC900FC00153786 /* BookmarkAllTabsDialogViewModelTests.swift */; }; + 9FA5A0B02BC9039200153786 /* BookmarkFolderStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA5A0AC2BC9037A00153786 /* BookmarkFolderStoreMock.swift */; }; + 9FA5A0B12BC9039300153786 /* BookmarkFolderStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA5A0AC2BC9037A00153786 /* BookmarkFolderStoreMock.swift */; }; 9FA75A3E2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA75A3D2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift */; }; 9FA75A3F2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA75A3D2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift */; }; + 9FAD623A2BCFDB32007F3A65 /* WebsiteInfoHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FAD62392BCFDB32007F3A65 /* WebsiteInfoHelpers.swift */; }; + 9FAD623B2BCFDB32007F3A65 /* WebsiteInfoHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FAD62392BCFDB32007F3A65 /* WebsiteInfoHelpers.swift */; }; + 9FAD623D2BD09DE5007F3A65 /* WebsiteInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FAD623C2BD09DE5007F3A65 /* WebsiteInfoTests.swift */; }; + 9FAD623E2BD09DE5007F3A65 /* WebsiteInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FAD623C2BD09DE5007F3A65 /* WebsiteInfoTests.swift */; }; 9FBD84522BB3AACB00220859 /* AttributionOriginFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBD84512BB3AACB00220859 /* AttributionOriginFileProvider.swift */; }; 9FBD84532BB3AACB00220859 /* AttributionOriginFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBD84512BB3AACB00220859 /* AttributionOriginFileProvider.swift */; }; 9FBD84562BB3ACFD00220859 /* AttributionOriginFileProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBD84552BB3ACFD00220859 /* AttributionOriginFileProviderTests.swift */; }; @@ -2058,7 +2074,7 @@ B63ED0E026AFE32F00A9DAD1 /* GeolocationProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B63ED0DF26AFE32F00A9DAD1 /* GeolocationProviderMock.swift */; }; B63ED0E326B3E7FA00A9DAD1 /* CLLocationManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B63ED0E226B3E7FA00A9DAD1 /* CLLocationManagerMock.swift */; }; B63ED0E526BB8FB900A9DAD1 /* SharingMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = B63ED0E426BB8FB900A9DAD1 /* SharingMenu.swift */; }; - B642738227B65BAC0005DFD1 /* SecureVaultErrorReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B642738127B65BAC0005DFD1 /* SecureVaultErrorReporter.swift */; }; + B642738227B65BAC0005DFD1 /* SecureVaultReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B642738127B65BAC0005DFD1 /* SecureVaultReporter.swift */; }; B643BF1427ABF772000BACEC /* NSWorkspaceExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B643BF1327ABF772000BACEC /* NSWorkspaceExtension.swift */; }; B644B43D29D56829003FA9AB /* SearchNonexistentDomainTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B644B43929D565DB003FA9AB /* SearchNonexistentDomainTests.swift */; }; B644B43E29D5682B003FA9AB /* SearchNonexistentDomainTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B644B43929D565DB003FA9AB /* SearchNonexistentDomainTests.swift */; }; @@ -2526,6 +2542,9 @@ EEDE50122BA360C80017F3C4 /* NetworkProtection+VPNAgentConvenienceInitializers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEDE50102BA360C80017F3C4 /* NetworkProtection+VPNAgentConvenienceInitializers.swift */; }; EEF12E6F2A2111880023E6BF /* MacPacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */; }; EEF53E182950CED5002D78F4 /* JSAlertViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF53E172950CED5002D78F4 /* JSAlertViewModelTests.swift */; }; + F116A7C32BD1924B00F3FCF7 /* PixelKitTestingUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = F116A7C22BD1924B00F3FCF7 /* PixelKitTestingUtilities */; }; + F116A7C72BD1925500F3FCF7 /* PixelKitTestingUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = F116A7C62BD1925500F3FCF7 /* PixelKitTestingUtilities */; }; + F116A7C92BD1929000F3FCF7 /* PixelKitTestingUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = F116A7C82BD1929000F3FCF7 /* PixelKitTestingUtilities */; }; F188267C2BBEB3AA00D9AC4F /* GeneralPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F188267B2BBEB3AA00D9AC4F /* GeneralPixel.swift */; }; F188267D2BBEB3AA00D9AC4F /* GeneralPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F188267B2BBEB3AA00D9AC4F /* GeneralPixel.swift */; }; F18826802BBEB58100D9AC4F /* PrivacyProPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F188267F2BBEB58100D9AC4F /* PrivacyProPixel.swift */; }; @@ -2538,6 +2557,14 @@ F18826912BC0105800D9AC4F /* PixelDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6DA44012616B28300DD1EC2 /* PixelDataStore.swift */; }; F18826922BC0105900D9AC4F /* PixelDataRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68C92C32750EF76002AC6B0 /* PixelDataRecord.swift */; }; F18826932BC0105900D9AC4F /* PixelDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6DA44012616B28300DD1EC2 /* PixelDataStore.swift */; }; + F198C7122BD18A28000BF24D /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = F198C7112BD18A28000BF24D /* PixelKit */; }; + F198C7142BD18A30000BF24D /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = F198C7132BD18A30000BF24D /* PixelKit */; }; + F198C7162BD18A44000BF24D /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = F198C7152BD18A44000BF24D /* PixelKit */; }; + F198C7182BD18A4C000BF24D /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = F198C7172BD18A4C000BF24D /* PixelKit */; }; + F198C71A2BD18A5B000BF24D /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = F198C7192BD18A5B000BF24D /* PixelKit */; }; + F198C71C2BD18A61000BF24D /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = F198C71B2BD18A61000BF24D /* PixelKit */; }; + F198C71E2BD18D88000BF24D /* SwiftLintTool in Frameworks */ = {isa = PBXBuildFile; productRef = F198C71D2BD18D88000BF24D /* SwiftLintTool */; }; + F198C7202BD18D92000BF24D /* SwiftLintTool in Frameworks */ = {isa = PBXBuildFile; productRef = F198C71F2BD18D92000BF24D /* SwiftLintTool */; }; F1B33DF22BAD929D001128B3 /* SubscriptionAppStoreRestorer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1B33DF12BAD929D001128B3 /* SubscriptionAppStoreRestorer.swift */; }; F1B33DF32BAD929D001128B3 /* SubscriptionAppStoreRestorer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1B33DF12BAD929D001128B3 /* SubscriptionAppStoreRestorer.swift */; }; F1B33DF62BAD970E001128B3 /* SubscriptionErrorReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1B33DF52BAD970E001128B3 /* SubscriptionErrorReporter.swift */; }; @@ -2546,6 +2573,10 @@ F1D43AEF2B98D8DF00BAB743 /* MainMenuActions+VanillaBrowser.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D43AED2B98D8DF00BAB743 /* MainMenuActions+VanillaBrowser.swift */; }; F1D43AF32B98E47800BAB743 /* BareBonesBrowserKit in Frameworks */ = {isa = PBXBuildFile; productRef = F1D43AF22B98E47800BAB743 /* BareBonesBrowserKit */; }; F1D43AF52B98E48900BAB743 /* BareBonesBrowserKit in Frameworks */ = {isa = PBXBuildFile; productRef = F1D43AF42B98E48900BAB743 /* BareBonesBrowserKit */; }; + F1DF95E32BD1807C0045E591 /* Crashes in Frameworks */ = {isa = PBXBuildFile; productRef = 08D4923DC968236E22E373E2 /* Crashes */; }; + F1DF95E42BD1807C0045E591 /* Crashes in Frameworks */ = {isa = PBXBuildFile; productRef = 537FC71EA5115A983FAF3170 /* Crashes */; }; + F1DF95E52BD1807C0045E591 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = DC3F73D49B2D44464AFEFCD8 /* Subscription */; }; + F1DF95E72BD188B60045E591 /* LoginItems in Frameworks */ = {isa = PBXBuildFile; productRef = F1DF95E62BD188B60045E591 /* LoginItems */; }; F41D174125CB131900472416 /* NSColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41D174025CB131900472416 /* NSColorExtension.swift */; }; F44C130225C2DA0400426E3E /* NSAppearanceExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F44C130125C2DA0400426E3E /* NSAppearanceExtension.swift */; }; F4A6198C283CFFBB007F2080 /* ContentScopeFeatureFlagging.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4A6198B283CFFBB007F2080 /* ContentScopeFeatureFlagging.swift */; }; @@ -2805,6 +2836,9 @@ 315AA06F28CA5CC800200030 /* YoutubePlayerNavigationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YoutubePlayerNavigationHandler.swift; sourceTree = ""; }; 3168506C2AF3AD1C009A2828 /* WaitlistViewControllerPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WaitlistViewControllerPresenter.swift; sourceTree = ""; }; 316850712AF3AD58009A2828 /* DataBrokerProtectionDebugMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionDebugMenu.swift; sourceTree = ""; }; + 316913222BD2B6250051B46D /* DataBrokerProtectionPixelsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionPixelsHandler.swift; sourceTree = ""; }; + 316913252BD2B76F0051B46D /* DataBrokerPrerequisitesStatusVerifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerPrerequisitesStatusVerifier.swift; sourceTree = ""; }; + 316913282BD2C7570051B46D /* DataBrokerProtectionErrorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionErrorViewController.swift; sourceTree = ""; }; 3171D6B72889849F0068632A /* CookieManagedNotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieManagedNotificationView.swift; sourceTree = ""; }; 3171D6B9288984D00068632A /* BadgeAnimationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeAnimationView.swift; sourceTree = ""; }; 3171D6DA2889B64D0068632A /* CookieManagedNotificationContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieManagedNotificationContainerView.swift; sourceTree = ""; }; @@ -2824,6 +2858,7 @@ 31C9ADE42AF0564500CEF57D /* WaitlistFeatureSetupHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistFeatureSetupHandler.swift; sourceTree = ""; }; 31CF3431288B0B1B0087244B /* NavigationBarBadgeAnimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarBadgeAnimator.swift; sourceTree = ""; }; 31D5375B291D944100407A95 /* PasswordManagementBitwardenItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasswordManagementBitwardenItemView.swift; sourceTree = ""; }; + 31DC2F202BD6DE65001354EF /* DataBrokerPrerequisitesStatusVerifierTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerPrerequisitesStatusVerifierTests.swift; sourceTree = ""; }; 31E163B9293A56F400963C10 /* BrokenSiteReportingReferenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrokenSiteReportingReferenceTests.swift; sourceTree = ""; }; 31E163BC293A579E00963C10 /* PrivacyReferenceTestHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyReferenceTestHelper.swift; sourceTree = ""; }; 31E163BF293A581900963C10 /* privacy-reference-tests */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = "privacy-reference-tests"; path = "Submodules/privacy-reference-tests"; sourceTree = SOURCE_ROOT; }; @@ -2869,6 +2904,7 @@ 376C4DB828A1A48A00CC0F5B /* FirePopoverViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirePopoverViewModelTests.swift; sourceTree = ""; }; 376CC8B4296EB630006B63A7 /* AppStore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppStore.xcconfig; sourceTree = ""; }; 376CC8B5296EBA8F006B63A7 /* BuildNumber.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = BuildNumber.xcconfig; sourceTree = ""; }; + 376E708D2BD686260082B7EB /* UI Tests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "UI Tests.xctestplan"; sourceTree = ""; }; 37717E66296B5A20002FAEDF /* Global.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Global.xcconfig; sourceTree = ""; }; 3775912C29AAC72700E26367 /* SyncPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncPreferences.swift; sourceTree = ""; }; 3775913529AB9A1C00E26367 /* SyncManagementDialogViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncManagementDialogViewController.swift; sourceTree = ""; }; @@ -3180,8 +3216,6 @@ 4BD18F04283F151F00058124 /* BookmarksBar.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = BookmarksBar.storyboard; sourceTree = ""; }; 4BD57C032AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionRemoteMessagingTests.swift; sourceTree = ""; }; 4BDFA4AD27BF19E500648192 /* ToggleableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleableScrollView.swift; sourceTree = ""; }; - 4BE0DF0426781961006337B7 /* NSStoryboardExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSStoryboardExtension.swift; sourceTree = ""; }; - 4BE15DB12A0B0DD500898243 /* PixelKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = PixelKit; sourceTree = ""; }; 4BE344ED2B2376DF003FC223 /* VPNFeedbackFormViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNFeedbackFormViewModelTests.swift; sourceTree = ""; }; 4BE4005227CF3DC3007D3161 /* SavePaymentMethodPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavePaymentMethodPopover.swift; sourceTree = ""; }; 4BE4005427CF3F19007D3161 /* SavePaymentMethodViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavePaymentMethodViewController.swift; sourceTree = ""; }; @@ -3250,6 +3284,7 @@ 7B430EA02A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionSimulateFailureMenu.swift; sourceTree = ""; }; 7B4CE8DA26F02108009134B1 /* UI Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "UI Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 7B4CE8E626F02134009134B1 /* TabBarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarTests.swift; sourceTree = ""; }; + 7B4D8A202BDA857300852966 /* VPNOperationErrorRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNOperationErrorRecorder.swift; sourceTree = ""; }; 7B5291882A1697680022E406 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 7B5291892A169BC90022E406 /* DeveloperID.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DeveloperID.xcconfig; sourceTree = ""; }; 7B6D98662ADEB4B000CD35FE /* DataBrokerProtectionLoginItemScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionLoginItemScheduler.swift; sourceTree = ""; }; @@ -3278,8 +3313,6 @@ 7BB108582A43375D000AB95F /* PFMoveApplication.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFMoveApplication.m; sourceTree = ""; }; 7BBA7CE52BAB03C1007579A3 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift"; sourceTree = ""; }; 7BBD45B02A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionDebugUtilities.swift; sourceTree = ""; }; - 7BBE650C2BC67BA0008F4EE9 /* NetworkProtectionIPCTunnelControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionIPCTunnelControllerTests.swift; sourceTree = ""; }; - 7BBE650F2BC67EED008F4EE9 /* NetworkProtectionTestingSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionTestingSupport.swift; sourceTree = ""; }; 7BD01C182AD8319C0088B32E /* IPCServiceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPCServiceManager.swift; sourceTree = ""; }; 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 = ""; }; @@ -3410,6 +3443,10 @@ 9D9AE92B2AAB84FF0026E7DC /* DBPMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBPMocks.swift; sourceTree = ""; }; 9DB6E7222AA0DA7A00A17F3C /* LoginItems */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = LoginItems; sourceTree = ""; }; 9F0A2CF72B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseBookmarkEntityTests.swift; sourceTree = ""; }; + 9F0FFFB32BCCAE37007C87DD /* BookmarkAllTabsDialogCoordinatorViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkAllTabsDialogCoordinatorViewModelTests.swift; sourceTree = ""; }; + 9F0FFFB72BCCAE9C007C87DD /* AddEditBookmarkDialogViewModelMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogViewModelMock.swift; sourceTree = ""; }; + 9F0FFFBA2BCCAEC2007C87DD /* AddEditBookmarkFolderDialogViewModelMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkFolderDialogViewModelMock.swift; sourceTree = ""; }; + 9F0FFFBD2BCCAF1F007C87DD /* BookmarkAllTabsDialogViewModelMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkAllTabsDialogViewModelMock.swift; sourceTree = ""; }; 9F180D0E2B69C553000D695F /* Tab+WKUIDelegateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Tab+WKUIDelegateTests.swift"; sourceTree = ""; }; 9F180D112B69C665000D695F /* DownloadsTabExtensionMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsTabExtensionMock.swift; sourceTree = ""; }; 9F2606092B85C20400819292 /* AddEditBookmarkDialogViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogViewModelTests.swift; sourceTree = ""; }; @@ -3426,14 +3463,24 @@ 9F872D9C2B9058D000138637 /* Bookmarks+TabTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bookmarks+TabTests.swift"; sourceTree = ""; }; 9F872D9F2B90644800138637 /* ContextualMenuTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextualMenuTests.swift; sourceTree = ""; }; 9F872DA22B90920F00138637 /* BookmarkFolderInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkFolderInfo.swift; sourceTree = ""; }; + 9F8D57312BCCCB9A00AEA660 /* UserDefaultsBookmarkFoldersStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsBookmarkFoldersStoreTests.swift; sourceTree = ""; }; 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkFolderDialogViewModel.swift; sourceTree = ""; }; 9F982F112B82268F00231028 /* AddEditBookmarkFolderDialogViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkFolderDialogViewModelTests.swift; sourceTree = ""; }; + 9F9C49F52BC786790099738D /* MoreOptionsMenu+BookmarksTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MoreOptionsMenu+BookmarksTests.swift"; sourceTree = ""; }; + 9F9C49F82BC7BC970099738D /* BookmarkAllTabsDialogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkAllTabsDialogView.swift; sourceTree = ""; }; + 9F9C49FC2BC7E9820099738D /* BookmarkAllTabsDialogViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkAllTabsDialogViewModel.swift; sourceTree = ""; }; + 9F9C4A002BC7F36D0099738D /* BookmarkAllTabsDialogCoordinatorViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkAllTabsDialogCoordinatorViewModel.swift; sourceTree = ""; }; 9FA173D92B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkDialogContainerView.swift; sourceTree = ""; }; 9FA173DE2B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkDialogButtonsView.swift; sourceTree = ""; }; 9FA173E22B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkDialogFolderManagementView.swift; sourceTree = ""; }; 9FA173E62B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkDialogStackedContentView.swift; sourceTree = ""; }; 9FA173EA2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogView.swift; sourceTree = ""; }; + 9FA5A0A42BC8F34900153786 /* UserDefaultsBookmarkFoldersStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsBookmarkFoldersStore.swift; sourceTree = ""; }; + 9FA5A0A82BC900FC00153786 /* BookmarkAllTabsDialogViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkAllTabsDialogViewModelTests.swift; sourceTree = ""; }; + 9FA5A0AC2BC9037A00153786 /* BookmarkFolderStoreMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkFolderStoreMock.swift; sourceTree = ""; }; 9FA75A3D2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksBarMenuFactoryTests.swift; sourceTree = ""; }; + 9FAD62392BCFDB32007F3A65 /* WebsiteInfoHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteInfoHelpers.swift; sourceTree = ""; }; + 9FAD623C2BD09DE5007F3A65 /* WebsiteInfoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteInfoTests.swift; sourceTree = ""; }; 9FBD84512BB3AACB00220859 /* AttributionOriginFileProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributionOriginFileProvider.swift; sourceTree = ""; }; 9FBD84552BB3ACFD00220859 /* AttributionOriginFileProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributionOriginFileProviderTests.swift; sourceTree = ""; }; 9FBD845C2BB3B80300220859 /* Origin.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = Origin.txt; sourceTree = ""; }; @@ -3686,7 +3733,7 @@ B63ED0DF26AFE32F00A9DAD1 /* GeolocationProviderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeolocationProviderMock.swift; sourceTree = ""; }; B63ED0E226B3E7FA00A9DAD1 /* CLLocationManagerMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CLLocationManagerMock.swift; sourceTree = ""; }; B63ED0E426BB8FB900A9DAD1 /* SharingMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharingMenu.swift; sourceTree = ""; }; - B642738127B65BAC0005DFD1 /* SecureVaultErrorReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureVaultErrorReporter.swift; sourceTree = ""; }; + B642738127B65BAC0005DFD1 /* SecureVaultReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureVaultReporter.swift; sourceTree = ""; }; B643BF1327ABF772000BACEC /* NSWorkspaceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSWorkspaceExtension.swift; sourceTree = ""; }; B644B43929D565DB003FA9AB /* SearchNonexistentDomainTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchNonexistentDomainTests.swift; sourceTree = ""; }; B645D8F529FA95440024461F /* WKProcessPoolExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKProcessPoolExtension.swift; sourceTree = ""; }; @@ -3998,8 +4045,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + F1DF95E42BD1807C0045E591 /* Crashes in Frameworks */, 373FB4B32B4D6C4B004C88D6 /* PreferencesViews in Frameworks */, - 7B5F9A752AE2BE4E002AEBC0 /* PixelKit in Frameworks */, 4BF97AD32B43C43F00EB4240 /* NetworkProtectionUI in Frameworks */, 7B1459572B7D43E500047F2C /* NetworkProtectionProxy in Frameworks */, B6F7128229F6820A00594A45 /* QuickLookUI.framework in Frameworks */, @@ -4008,10 +4055,12 @@ 37A5E2F0298AA1B20047046B /* Persistence in Frameworks */, 9DC70B1A2AA1FA5B005A844B /* LoginItems in Frameworks */, 37269EFD2B332FAC005E8E46 /* Common in Frameworks */, + F198C7142BD18A30000BF24D /* PixelKit in Frameworks */, F1D43AF52B98E48900BAB743 /* BareBonesBrowserKit in Frameworks */, 378F44E629B4BDEE00899924 /* SwiftUIExtensions in Frameworks */, 3706FCA7293F65D500E42796 /* BrowserServicesKit in Frameworks */, 3129788A2B64131200B67619 /* DataBrokerProtection in Frameworks */, + F198C7202BD18D92000BF24D /* SwiftLintTool in Frameworks */, 4BCBE4582BA7E17800FC75A1 /* SubscriptionUI in Frameworks */, 3706FCA9293F65D500E42796 /* ContentBlocking in Frameworks */, 85D44B882BA08D30001B4AB5 /* Suggestions in Frameworks */, @@ -4021,7 +4070,6 @@ B6EC37FF29B8D915001ACE79 /* Configuration in Frameworks */, 372217822B33380700B8E9C2 /* TestUtils in Frameworks */, 3706FCAA293F65D500E42796 /* UserScript in Frameworks */, - 37CF915B2BB416AC00BADCAE /* Crashes in Frameworks */, 3706FCAB293F65D500E42796 /* TrackerRadarKit in Frameworks */, 85E2BBD02B8F534A00DBEC7A /* History in Frameworks */, 4BF97AD52B43C43F00EB4240 /* NetworkProtection in Frameworks */, @@ -4037,9 +4085,9 @@ buildActionMask = 2147483647; files = ( 3706FE88293F661700E42796 /* OHHTTPStubs in Frameworks */, + F116A7C72BD1925500F3FCF7 /* PixelKitTestingUtilities in Frameworks */, B65CD8CF2B316E0200A595BB /* SnapshotTesting in Frameworks */, 3706FE89293F661700E42796 /* OHHTTPStubsSwift in Frameworks */, - 4B81AD372B29513100706C96 /* PixelKitTestingUtilities in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4062,6 +4110,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + F116A7C92BD1929000F3FCF7 /* PixelKitTestingUtilities in Frameworks */, B65CD8CD2B316DFC00A595BB /* SnapshotTesting in Frameworks */, B6AE39F329374AEC00C37AA4 /* OHHTTPStubs in Frameworks */, B6AE39F529374AEC00C37AA4 /* OHHTTPStubsSwift in Frameworks */, @@ -4074,8 +4123,8 @@ files = ( 37269F012B332FC8005E8E46 /* Common in Frameworks */, EE7295E92A545BC4008C0991 /* NetworkProtection in Frameworks */, - 4B2537772A11BFE100610219 /* PixelKit in Frameworks */, 7B37C7A52BAA32A50062546A /* Subscription in Frameworks */, + F198C7182BD18A4C000BF24D /* PixelKit in Frameworks */, 7BBE2B7B2B61663C00697445 /* NetworkProtectionProxy in Frameworks */, 4B2D062C2A11C0E100DE1F49 /* Networking in Frameworks */, 4B25375B2A11BE7300610219 /* NetworkExtension.framework in Frameworks */, @@ -4086,13 +4135,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + F198C71A2BD18A5B000BF24D /* PixelKit in Frameworks */, 4B41EDAB2B1544B2001EEDF4 /* LoginItems in Frameworks */, 7B00997D2B6508B700FE7C31 /* NetworkProtectionProxy in Frameworks */, 7BEEA5122AD1235B00A9E72B /* NetworkProtectionIPC in Frameworks */, 7BA7CC5F2AD1210C0042E5CE /* Networking in Frameworks */, 7BEEA5162AD1236E00A9E72B /* NetworkProtectionUI in Frameworks */, BDADBDC92BD2BC2200421B9B /* Lottie in Frameworks */, - 7BFCB74E2ADE7E1A00DA3EA7 /* PixelKit in Frameworks */, EE7295ED2A545C0A008C0991 /* NetworkProtection in Frameworks */, EE2F9C5B2B90F2FF00D45FC9 /* Subscription in Frameworks */, 7BEC182F2AD5D8DC00D30536 /* SystemExtensionManager in Frameworks */, @@ -4103,9 +4152,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 7BFCB7502ADE7E2300DA3EA7 /* PixelKit in Frameworks */, 4BCBE45C2BA7E18500FC75A1 /* Subscription in Frameworks */, 7BA7CC612AD1211C0042E5CE /* Networking in Frameworks */, + F198C71C2BD18A61000BF24D /* PixelKit in Frameworks */, 7BEEA5142AD1236300A9E72B /* NetworkProtectionIPC in Frameworks */, BDADBDCB2BD2BC2800421B9B /* Lottie in Frameworks */, 7B00997F2B6508C200FE7C31 /* NetworkProtectionProxy in Frameworks */, @@ -4128,10 +4177,10 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + F1DF95E52BD1807C0045E591 /* Subscription in Frameworks */, 37269EFF2B332FBB005E8E46 /* Common in Frameworks */, EE7295E72A545BBB008C0991 /* NetworkProtection in Frameworks */, - 4B4D60982A0B2A5C00BCD287 /* PixelKit in Frameworks */, - 1E46E1A02BD029BD0007273A /* Subscription in Frameworks */, + F198C7162BD18A44000BF24D /* PixelKit in Frameworks */, 4B4D60AF2A0C837F00BCD287 /* Networking in Frameworks */, 7B25856E2BA2F2ED00D49F79 /* NetworkProtectionUI in Frameworks */, 4B4D603F2A0B290200BCD287 /* NetworkExtension.framework in Frameworks */, @@ -4160,7 +4209,6 @@ files = ( 7BDA36E62B7E037100AD5388 /* NetworkExtension.framework in Frameworks */, 7B97CD592B7E0B57004FEF43 /* NetworkProtectionProxy in Frameworks */, - 7B97CD622B7E0C4B004FEF43 /* PixelKit in Frameworks */, 7B7DFB222B7E7473009EA1A3 /* Networking in Frameworks */, 7B97CD5B2B7E0B85004FEF43 /* Common in Frameworks */, ); @@ -4171,7 +4219,6 @@ buildActionMask = 2147483647; files = ( 9DEF97E12B06C4EE00764F03 /* Networking in Frameworks */, - 9D6983F92AC773C3002C02FC /* PixelKit in Frameworks */, 9D9AE8F92AAA3AD00026E7DC /* DataBrokerProtection in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4180,7 +4227,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9D6983FB2AC773C8002C02FC /* PixelKit in Frameworks */, 315A023F2B6421AE00BFA577 /* Networking in Frameworks */, 9D9AE8FB2AAA3AD90026E7DC /* DataBrokerProtection in Frameworks */, ); @@ -4190,16 +4236,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 37CF91592BB416A500BADCAE /* Crashes in Frameworks */, - 373FB4B12B4D6C42004C88D6 /* PreferencesViews in Frameworks */, + F1DF95E32BD1807C0045E591 /* Crashes in Frameworks */, 85E2BBCE2B8F534000DBEC7A /* History in Frameworks */, 1EA7B8D32B7E078C000330A4 /* SubscriptionUI in Frameworks */, B6F7128129F681EB00594A45 /* QuickLookUI.framework in Frameworks */, - 9DB6E7242AA0DC5800A17F3C /* LoginItems in Frameworks */, EE7295E32A545B9A008C0991 /* NetworkProtection in Frameworks */, 9807F645278CA16F00E1547B /* BrowserServicesKit in Frameworks */, 987799ED299998B1005D8EB6 /* Bookmarks in Frameworks */, - 7B5DD69A2AE51FFA001DE99C /* PixelKit in Frameworks */, 1E950E3F2912A10D0051A99B /* ContentBlocking in Frameworks */, 31A3A4E32B0C115F0021063C /* DataBrokerProtection in Frameworks */, 378F44E429B4BDE900899924 /* SwiftUIExtensions in Frameworks */, @@ -4209,8 +4252,10 @@ 7B31FD8C2AD125620086AA24 /* NetworkProtectionIPC in Frameworks */, 37269EFB2B332F9E005E8E46 /* Common in Frameworks */, AA06B6B72672AF8100F541C5 /* Sparkle in Frameworks */, + F198C71E2BD18D88000BF24D /* SwiftLintTool in Frameworks */, 1EA7B8D52B7E078C000330A4 /* Subscription in Frameworks */, B6B77BE8297973D4001E68A1 /* Navigation in Frameworks */, + F198C7122BD18A28000BF24D /* PixelKit in Frameworks */, 3739326729AE4B42009346AE /* DDGSync in Frameworks */, 7BA59C9B2AE18B49009A97B1 /* SystemExtensionManager in Frameworks */, 371D00E129D8509400EC8598 /* OpenSSL in Frameworks */, @@ -4219,6 +4264,7 @@ 85D44B862BA08D29001B4AB5 /* Suggestions in Frameworks */, 37DF000529F9C056002B7D3E /* SyncDataProviders in Frameworks */, 37BA812D29B3CD690053F1A3 /* SyncUI in Frameworks */, + F1DF95E72BD188B60045E591 /* LoginItems in Frameworks */, 372217802B3337FE00B8E9C2 /* TestUtils in Frameworks */, 7BA076BB2B65D61400D7FB72 /* NetworkProtectionProxy in Frameworks */, 4B4D60B12A0C83B900BCD287 /* NetworkProtectionUI in Frameworks */, @@ -4231,9 +4277,9 @@ buildActionMask = 2147483647; files = ( B6DA44172616C13800DD1EC2 /* OHHTTPStubs in Frameworks */, + F116A7C32BD1924B00F3FCF7 /* PixelKitTestingUtilities in Frameworks */, B65CD8CB2B316DF100A595BB /* SnapshotTesting in Frameworks */, B6DA44192616C13800DD1EC2 /* OHHTTPStubsSwift in Frameworks */, - 4B81AD352B29512B00706C96 /* PixelKitTestingUtilities in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4438,6 +4484,14 @@ path = Subscription; sourceTree = ""; }; + 3169132B2BD2C7960051B46D /* ErrorView */ = { + isa = PBXGroup; + children = ( + 316913282BD2C7570051B46D /* DataBrokerProtectionErrorViewController.swift */, + ); + path = ErrorView; + sourceTree = ""; + }; 3171D6DC2889B6700068632A /* CookieManaged */ = { isa = PBXGroup; children = ( @@ -4460,6 +4514,8 @@ 3192EC862A4DCF0E001E97A5 /* DBP */ = { isa = PBXGroup; children = ( + 3169132B2BD2C7960051B46D /* ErrorView */, + 316913222BD2B6250051B46D /* DataBrokerProtectionPixelsHandler.swift */, 316850712AF3AD58009A2828 /* DataBrokerProtectionDebugMenu.swift */, 3192EC872A4DCF21001E97A5 /* DBPHomeViewController.swift */, 3139A1512AA4B3C000969C7D /* DataBrokerProtectionManager.swift */, @@ -4472,6 +4528,7 @@ BBDFDC592B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift */, BB5789712B2CA70F0009DFE2 /* DataBrokerProtectionSubscriptionEventHandler.swift */, 4B37EE652B4CFC9500A89A61 /* RemoteMessaging */, + 316913252BD2B76F0051B46D /* DataBrokerPrerequisitesStatusVerifier.swift */, ); path = DBP; sourceTree = ""; @@ -4480,6 +4537,7 @@ isa = PBXGroup; children = ( 31A2FD162BAB41C500D0E741 /* DataBrokerProtectionVisibilityTests.swift */, + 31DC2F202BD6DE65001354EF /* DataBrokerPrerequisitesStatusVerifierTests.swift */, ); path = DBP; sourceTree = ""; @@ -4599,6 +4657,7 @@ 566B195F29CDB7A9007E38F4 /* Mocks */, 378205FA283C277800D1D4AA /* MainMenuTests.swift */, 566B195C29CDB692007E38F4 /* MoreOptionsMenuTests.swift */, + 9F9C49F52BC786790099738D /* MoreOptionsMenu+BookmarksTests.swift */, ); path = Menus; sourceTree = ""; @@ -4664,7 +4723,6 @@ 3192A2702A4C4E330084EA89 /* DataBrokerProtection */, 9DB6E7222AA0DA7A00A17F3C /* LoginItems */, 7B25FE322AD12C990012AFAB /* NetworkProtectionMac */, - 4BE15DB12A0B0DD500898243 /* PixelKit */, 378F44E229B4B7B600899924 /* SwiftUIExtensions */, 37BA812B29B3CB8A0053F1A3 /* SyncUI */, 1E862A882A9FC01200F84D4B /* SubscriptionUI */, @@ -5588,13 +5646,11 @@ isa = PBXGroup; children = ( BDA7648F2BC4E56200D0400C /* Mocks */, - 7BBE65122BC67EF6008F4EE9 /* Support */, 4BCF15E62ABB98A20083F6DF /* Resources */, 4BCF15E42ABB98990083F6DF /* NetworkProtectionRemoteMessageTests.swift */, 4BD57C032AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift */, 7B09CBA72BA4BE7000CF245B /* NetworkProtectionPixelEventTests.swift */, BDA7648C2BC4E4EF00D0400C /* DefaultVPNLocationFormatterTests.swift */, - 7BBE650C2BC67BA0008F4EE9 /* NetworkProtectionIPCTunnelControllerTests.swift */, ); path = NetworkProtection; sourceTree = ""; @@ -5729,6 +5785,7 @@ 7B4CE8DB26F02108009134B1 /* UITests */ = { isa = PBXGroup; children = ( + 376E708D2BD686260082B7EB /* UI Tests.xctestplan */, EEBCE6802BA444FA00B9DF00 /* Common */, EEC7BE2D2BC6C09400F86835 /* AddressBarKeyboardShortcutsTests.swift */, EED735352BB46B6000F173D6 /* AutocompleteTests.swift */, @@ -5800,14 +5857,6 @@ path = LetsMove1.25; sourceTree = ""; }; - 7BBE65122BC67EF6008F4EE9 /* Support */ = { - isa = PBXGroup; - children = ( - 7BBE650F2BC67EED008F4EE9 /* NetworkProtectionTestingSupport.swift */, - ); - path = Support; - sourceTree = ""; - }; 7BDA36E72B7E037200AD5388 /* VPNProxyExtension */ = { isa = PBXGroup; children = ( @@ -5972,7 +6021,7 @@ 85CC1D7826A05E790062F04E /* Model */, 85CC1D7F26A05F6C0062F04E /* Services */, 85CC1D7926A05E820062F04E /* View */, - B642738127B65BAC0005DFD1 /* SecureVaultErrorReporter.swift */, + B642738127B65BAC0005DFD1 /* SecureVaultReporter.swift */, ); path = SecureVault; sourceTree = ""; @@ -6205,6 +6254,16 @@ path = DuckDuckGoDBPBackgroundAgent; sourceTree = ""; }; + 9F0FFFB62BCCAE80007C87DD /* Mocks */ = { + isa = PBXGroup; + children = ( + 9F0FFFB72BCCAE9C007C87DD /* AddEditBookmarkDialogViewModelMock.swift */, + 9F0FFFBA2BCCAEC2007C87DD /* AddEditBookmarkFolderDialogViewModelMock.swift */, + 9F0FFFBD2BCCAF1F007C87DD /* BookmarkAllTabsDialogViewModelMock.swift */, + ); + path = Mocks; + sourceTree = ""; + }; 9F872D9B2B9058B000138637 /* Extensions */ = { isa = PBXGroup; children = ( @@ -6216,9 +6275,12 @@ 9F982F102B82264400231028 /* ViewModels */ = { isa = PBXGroup; children = ( + 9F0FFFB62BCCAE80007C87DD /* Mocks */, 9F982F112B82268F00231028 /* AddEditBookmarkFolderDialogViewModelTests.swift */, 9F2606092B85C20400819292 /* AddEditBookmarkDialogViewModelTests.swift */, 9F26060D2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift */, + 9F0FFFB32BCCAE37007C87DD /* BookmarkAllTabsDialogCoordinatorViewModelTests.swift */, + 9FA5A0A82BC900FC00153786 /* BookmarkAllTabsDialogViewModelTests.swift */, ); path = ViewModels; sourceTree = ""; @@ -6236,6 +6298,7 @@ 9F56CFA82B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift */, 9FEE98642B846870002E44E8 /* AddEditBookmarkView.swift */, 9F56CFB02B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift */, + 9F9C49F82BC7BC970099738D /* BookmarkAllTabsDialogView.swift */, ); path = Dialog; sourceTree = ""; @@ -6248,6 +6311,14 @@ path = Factory; sourceTree = ""; }; + 9FAD62382BCFDB1D007F3A65 /* Helpers */ = { + isa = PBXGroup; + children = ( + 9FAD62392BCFDB32007F3A65 /* WebsiteInfoHelpers.swift */, + ); + path = Helpers; + sourceTree = ""; + }; AA0877B626D515EE00B05660 /* UserAgent */ = { isa = PBXGroup; children = ( @@ -6368,7 +6439,6 @@ 565E46DE2B2725DD0013AC2A /* SyncE2EUITests */, AA585D7F248FD31100E9A3E2 /* Products */, 85AE2FF024A33A2D002D507F /* Frameworks */, - EE0629702B90EE3500D868B4 /* Recovered References */, ); sourceTree = ""; }; @@ -6598,6 +6668,7 @@ AA652CAB25DD820D009059CC /* Bookmarks */ = { isa = PBXGroup; children = ( + 9FAD62382BCFDB1D007F3A65 /* Helpers */, 9F872D9B2B9058B000138637 /* Extensions */, 9FA75A3C2BA00DF500DA5FA6 /* Factory */, 9F982F102B82264400231028 /* ViewModels */, @@ -6624,6 +6695,7 @@ 98A95D87299A2DF900B9B81A /* BookmarkMigrationTests.swift */, 9F872D9F2B90644800138637 /* ContextualMenuTests.swift */, 9F0A2CF72B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift */, + 9FAD623C2BD09DE5007F3A65 /* WebsiteInfoTests.swift */, ); path = Model; sourceTree = ""; @@ -6633,6 +6705,8 @@ children = ( AA652CB025DD825B009059CC /* LocalBookmarkStoreTests.swift */, 986189E52A7CFB3E001B4519 /* LocalBookmarkStoreSavingTests.swift */, + 9FA5A0AC2BC9037A00153786 /* BookmarkFolderStoreMock.swift */, + 9F8D57312BCCCB9A00AEA660 /* UserDefaultsBookmarkFoldersStoreTests.swift */, ); path = Services; sourceTree = ""; @@ -7017,6 +7091,8 @@ 9F56CFAC2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift */, 9FEE98682B85B869002E44E8 /* BookmarksDialogViewModel.swift */, 9FEE986C2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift */, + 9F9C4A002BC7F36D0099738D /* BookmarkAllTabsDialogCoordinatorViewModel.swift */, + 9F9C49FC2BC7E9820099738D /* BookmarkAllTabsDialogViewModel.swift */, ); path = ViewModel; sourceTree = ""; @@ -7095,10 +7171,10 @@ 4B9292CD2667123700AD2C21 /* BookmarkManagementDetailViewController.swift */, 4B9292C72667123700AD2C21 /* BookmarkManagementSidebarViewController.swift */, 4B9292C82667123700AD2C21 /* BookmarkManagementSplitViewController.swift */, + 4B9292C92667123700AD2C21 /* BookmarkTableRowView.swift */, 4B92928726670D1600AD2C21 /* BookmarkOutlineCellView.swift */, 4B92928526670D1600AD2C21 /* BookmarksOutlineView.swift */, 4B92928926670D1700AD2C21 /* BookmarkTableCellView.swift */, - 4B9292C92667123700AD2C21 /* BookmarkTableRowView.swift */, 4B9292C62667123700AD2C21 /* BrowserTabSelectionDelegate.swift */, 4B92928626670D1600AD2C21 /* OutlineSeparatorViewCell.swift */, 4B0511B3262CAA5A00F6079C /* RoundedSelectionRowView.swift */, @@ -7139,6 +7215,7 @@ B6DA06E52913F39400225DE2 /* MenuItemSelectors.swift */, AAC5E4D625D6A710007F5990 /* BookmarkStore.swift */, 987799F829999973005D8EB6 /* LocalBookmarkStore.swift */, + 9FA5A0A42BC8F34900153786 /* UserDefaultsBookmarkFoldersStore.swift */, ); path = Services; sourceTree = ""; @@ -7280,7 +7357,6 @@ B6B3E0DC2657E9CF0040E0A2 /* NSScreenExtension.swift */, AAC5E4E325D6BA9C007F5990 /* NSSizeExtension.swift */, 4B39AAF527D9B2C700A73FD5 /* NSStackViewExtension.swift */, - 4BE0DF0426781961006337B7 /* NSStoryboardExtension.swift */, AA5C8F58258FE21F00748EB7 /* NSTextFieldExtension.swift */, 858A798426A8BB5D00A75A42 /* NSTextViewExtension.swift */, 4B0511E0262CAA8600F6079C /* NSViewControllerExtension.swift */, @@ -7993,13 +8069,6 @@ path = fonts; sourceTree = ""; }; - EE0629702B90EE3500D868B4 /* Recovered References */ = { - isa = PBXGroup; - children = ( - ); - name = "Recovered References"; - sourceTree = ""; - }; EEA3EEAF2B24EB5100E8333A /* VPNLocation */ = { isa = PBXGroup; children = ( @@ -8040,6 +8109,7 @@ 4BF0E5042AD2551A00FFEC9E /* NetworkProtectionPixelEvent.swift */, 7B934C402A866DD400FC8F9C /* UserDefaults+NetworkProtectionShared.swift */, 7BFE95532A9DF2930081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift */, + 7B4D8A202BDA857300852966 /* VPNOperationErrorRecorder.swift */, ); path = AppAndExtensionAndAgentTargets; sourceTree = ""; @@ -8072,16 +8142,15 @@ CBCCF59F2996681700C02DFE /* Assert Xcode version */, 378E2798296F6D1D00FCADA2 /* Validate PRODUCT_NAME */, 3706FA79293F65D500E42796 /* Check Embedded Config URLs */, - B6E6BA192BA2D8BE008AA7E1 /* Run swiftlint */, 3706FA7A293F65D500E42796 /* Sources */, 3706FCA6293F65D500E42796 /* Frameworks */, 3706FCB1293F65D500E42796 /* Resources */, 4BBA2D272B6AC09D00F6A470 /* Embed Login Items */, + 6A8856B31B2BC5078B61ED81 /* Run swiftlint */, ); buildRules = ( ); dependencies = ( - B637D1BD2BC6AE6200C7DCA7 /* PBXTargetDependency */, 4BBA2D2B2B6AD01E00F6A470 /* PBXTargetDependency */, 4BBA2D292B6ACD4D00F6A470 /* PBXTargetDependency */, 4B5F14FE2A1529230060320F /* PBXTargetDependency */, @@ -8102,7 +8171,6 @@ B6EC37FE29B8D915001ACE79 /* Configuration */, 37DF000629F9C061002B7D3E /* SyncDataProviders */, 9DC70B192AA1FA5B005A844B /* LoginItems */, - 7B5F9A742AE2BE4E002AEBC0 /* PixelKit */, 37269EFC2B332FAC005E8E46 /* Common */, 372217812B33380700B8E9C2 /* TestUtils */, 4BF97AD02B43C43F00EB4240 /* NetworkProtectionIPC */, @@ -8116,8 +8184,10 @@ 4BCBE4572BA7E17800FC75A1 /* SubscriptionUI */, 85D44B872BA08D30001B4AB5 /* Suggestions */, 4BCBE4592BA7E17800FC75A1 /* Subscription */, - 37CF915A2BB416AC00BADCAE /* Crashes */, 9FF521472BAA909C00B9819B /* Lottie */, + 537FC71EA5115A983FAF3170 /* Crashes */, + F198C7132BD18A30000BF24D /* PixelKit */, + F198C71F2BD18D92000BF24D /* SwiftLintTool */, ); productName = DuckDuckGo; productReference = 3706FD05293F65D500E42796 /* DuckDuckGo App Store.app */; @@ -8141,8 +8211,8 @@ packageProductDependencies = ( 3706FDD6293F661700E42796 /* OHHTTPStubs */, 3706FDD8293F661700E42796 /* OHHTTPStubsSwift */, - 4B81AD362B29513100706C96 /* PixelKitTestingUtilities */, B65CD8CE2B316E0200A595BB /* SnapshotTesting */, + F116A7C62BD1925500F3FCF7 /* PixelKitTestingUtilities */, ); productName = DuckDuckGoTests; productReference = 3706FE99293F661700E42796 /* Unit Tests App Store.xctest */; @@ -8184,8 +8254,6 @@ 376113D62B29CD6800E794BB /* PBXTargetDependency */, ); name = "SyncE2EUITests App Store"; - packageProductDependencies = ( - ); productName = DuckDuckGoSyncUITests; productReference = 376113D42B29CD5B00E794BB /* SyncE2EUITests App Store.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; @@ -8209,6 +8277,7 @@ B6AE39F229374AEC00C37AA4 /* OHHTTPStubs */, B6AE39F429374AEC00C37AA4 /* OHHTTPStubsSwift */, B65CD8CC2B316DFC00A595BB /* SnapshotTesting */, + F116A7C82BD1929000F3FCF7 /* PixelKitTestingUtilities */, ); productName = "Integration Tests"; productReference = 4B1AD89D25FC27E200261379 /* Integration Tests.xctest */; @@ -8229,12 +8298,12 @@ ); name = NetworkProtectionSystemExtension; packageProductDependencies = ( - 4B2537762A11BFE100610219 /* PixelKit */, 4B2D062B2A11C0E100DE1F49 /* Networking */, EE7295E82A545BC4008C0991 /* NetworkProtection */, 37269F002B332FC8005E8E46 /* Common */, 7BBE2B7A2B61663C00697445 /* NetworkProtectionProxy */, 7B37C7A42BAA32A50062546A /* Subscription */, + F198C7172BD18A4C000BF24D /* PixelKit */, ); productName = NetworkProtectionSystemExtension; productReference = 4B25375A2A11BE7300610219 /* com.duckduckgo.macos.vpn.network-extension.debug.systemextension */; @@ -8262,10 +8331,10 @@ 7BEEA5112AD1235B00A9E72B /* NetworkProtectionIPC */, 7BEEA5152AD1236E00A9E72B /* NetworkProtectionUI */, 7BEC182E2AD5D8DC00D30536 /* SystemExtensionManager */, - 7BFCB74D2ADE7E1A00DA3EA7 /* PixelKit */, 4B41EDAA2B1544B2001EEDF4 /* LoginItems */, 7B00997C2B6508B700FE7C31 /* NetworkProtectionProxy */, EE2F9C5A2B90F2FF00D45FC9 /* Subscription */, + F198C7192BD18A5B000BF24D /* PixelKit */, BDADBDC82BD2BC2200421B9B /* Lottie */, ); productName = DuckDuckGoAgent; @@ -8295,10 +8364,10 @@ EE7295EE2A545C12008C0991 /* NetworkProtection */, 7BA7CC602AD1211C0042E5CE /* Networking */, 7BEEA5132AD1236300A9E72B /* NetworkProtectionIPC */, - 7BFCB74F2ADE7E2300DA3EA7 /* PixelKit */, 7B00997E2B6508C200FE7C31 /* NetworkProtectionProxy */, 4BA7C4DC2B3F64E500AFE511 /* LoginItems */, 4BCBE45B2BA7E18500FC75A1 /* Subscription */, + F198C71B2BD18A61000BF24D /* PixelKit */, BDADBDCA2BD2BC2800421B9B /* Lottie */, ); productName = DuckDuckGoAgentAppStore; @@ -8342,12 +8411,12 @@ ); name = NetworkProtectionAppExtension; packageProductDependencies = ( - 4B4D60972A0B2A5C00BCD287 /* PixelKit */, 4B4D60AE2A0C837F00BCD287 /* Networking */, EE7295E62A545BBB008C0991 /* NetworkProtection */, 37269EFE2B332FBB005E8E46 /* Common */, 7B25856D2BA2F2ED00D49F79 /* NetworkProtectionUI */, - 1E46E19F2BD029BD0007273A /* Subscription */, + DC3F73D49B2D44464AFEFCD8 /* Subscription */, + F198C7152BD18A44000BF24D /* PixelKit */, ); productName = NetworkProtectionAppExtension; productReference = 4B4D603D2A0B290200BCD287 /* NetworkProtectionAppExtension.appex */; @@ -8366,8 +8435,6 @@ dependencies = ( ); name = SyncE2EUITests; - packageProductDependencies = ( - ); productName = DuckDuckGoSyncUITests; productReference = 565E46DD2B2725DC0013AC2A /* SyncE2EUITests.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; @@ -8410,7 +8477,6 @@ packageProductDependencies = ( 7B97CD582B7E0B57004FEF43 /* NetworkProtectionProxy */, 7B97CD5A2B7E0B85004FEF43 /* Common */, - 7B97CD612B7E0C4B004FEF43 /* PixelKit */, 7B7DFB212B7E7473009EA1A3 /* Networking */, ); productName = VPNProxyExtension; @@ -8433,7 +8499,6 @@ name = DuckDuckGoDBPBackgroundAgent; packageProductDependencies = ( 9D9AE8F82AAA3AD00026E7DC /* DataBrokerProtection */, - 9D6983F82AC773C3002C02FC /* PixelKit */, 9DEF97E02B06C4EE00764F03 /* Networking */, ); productName = DuckDuckGoAgent; @@ -8456,7 +8521,6 @@ name = DuckDuckGoDBPBackgroundAgentAppStore; packageProductDependencies = ( 9D9AE8FA2AAA3AD90026E7DC /* DataBrokerProtection */, - 9D6983FA2AC773C8002C02FC /* PixelKit */, 315A023E2B6421AE00BFA577 /* Networking */, ); productName = DuckDuckGoAgent; @@ -8469,17 +8533,16 @@ buildPhases = ( CBCCF59E299667B700C02DFE /* Assert Xcode version */, 3705272528992C8A000C06A2 /* Check Embedded Config URLs */, - B6409DC52BC7BD1F00D66F9E /* Run swiftlint */, AA585D7A248FD31100E9A3E2 /* Sources */, AA585D7B248FD31100E9A3E2 /* Frameworks */, AA585D7C248FD31100E9A3E2 /* Resources */, B6F2C8722A7A4C7D000498CF /* Make /Applications symlink, remove app on Clean build */, 4B2D065D2A11D2AE00DE1F49 /* Embed Login Items */, + 28003FDBDB96625F1630CFF2 /* Run swiftlint */, ); buildRules = ( ); dependencies = ( - B637D1BB2BC6AE5600C7DCA7 /* PBXTargetDependency */, 7B4627742B9AF2C8004ACE0B /* PBXTargetDependency */, 4B5F14FC2A15291D0060320F /* PBXTargetDependency */, 31C6E9AD2B0C07BA0086DC30 /* PBXTargetDependency */, @@ -8502,22 +8565,22 @@ 37DF000429F9C056002B7D3E /* SyncDataProviders */, 4B4D60B02A0C83B900BCD287 /* NetworkProtectionUI */, EE7295E22A545B9A008C0991 /* NetworkProtection */, - 9DB6E7232AA0DC5800A17F3C /* LoginItems */, 7B31FD8B2AD125620086AA24 /* NetworkProtectionIPC */, 7BA59C9A2AE18B49009A97B1 /* SystemExtensionManager */, - 7B5DD6992AE51FFA001DE99C /* PixelKit */, 31A3A4E22B0C115F0021063C /* DataBrokerProtection */, 37269EFA2B332F9E005E8E46 /* Common */, 3722177F2B3337FE00B8E9C2 /* TestUtils */, - 373FB4B02B4D6C42004C88D6 /* PreferencesViews */, 7BA076BA2B65D61400D7FB72 /* NetworkProtectionProxy */, 85E2BBCD2B8F534000DBEC7A /* History */, 1EA7B8D22B7E078C000330A4 /* SubscriptionUI */, 1EA7B8D42B7E078C000330A4 /* Subscription */, F1D43AF22B98E47800BAB743 /* BareBonesBrowserKit */, 85D44B852BA08D29001B4AB5 /* Suggestions */, - 37CF91582BB416A500BADCAE /* Crashes */, 9FF521452BAA908500B9819B /* Lottie */, + 08D4923DC968236E22E373E2 /* Crashes */, + F1DF95E62BD188B60045E591 /* LoginItems */, + F198C7112BD18A28000BF24D /* PixelKit */, + F198C71D2BD18D88000BF24D /* SwiftLintTool */, ); productName = DuckDuckGo; productReference = AA585D7E248FD31100E9A3E2 /* DuckDuckGo.app */; @@ -8541,8 +8604,8 @@ packageProductDependencies = ( B6DA44162616C13800DD1EC2 /* OHHTTPStubs */, B6DA44182616C13800DD1EC2 /* OHHTTPStubsSwift */, - 4B81AD342B29512B00706C96 /* PixelKitTestingUtilities */, B65CD8CA2B316DF100A595BB /* SnapshotTesting */, + F116A7C22BD1924B00F3FCF7 /* PixelKitTestingUtilities */, ); productName = DuckDuckGoTests; productReference = AA585D90248FD31400E9A3E2 /* Unit Tests.xctest */; @@ -8985,6 +9048,25 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 28003FDBDB96625F1630CFF2 /* Run swiftlint */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Run swiftlint"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [ \"$CONFIGURATION\" != \"Debug\" ] || [ \"$ENABLE_PREVIEWS\" = \"YES\" ]; then exit 0; fi\n${BUILT_PRODUCTS_DIR}/SwiftLintTool\n"; + }; 3121F62B2B64266A002F706A /* Copy Swift Package resources */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -9117,7 +9199,7 @@ 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 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 */ = { + 6A8856B31B2BC5078B61ED81 /* Run swiftlint */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; @@ -9127,16 +9209,16 @@ ); inputPaths = ( ); - name = "Embed System Network Extension"; + name = "Run swiftlint"; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if [[ -z \"${SYSEX_BUNDLE_ID}\" ]]; then\n echo \"Required build settings are not defined, please check xcconfig files\"\n exit 1\nfi\n\n\necho \"ditto ${BUILT_PRODUCTS_DIR}/${SYSEX_BUNDLE_ID}.systemextension $BUILT_PRODUCTS_DIR/${CONTENTS_FOLDER_PATH}/Library/SystemExtensions/${SYSEX_BUNDLE_ID}.systemextension\"\n\nditto \"${BUILT_PRODUCTS_DIR}/${SYSEX_BUNDLE_ID}.systemextension\" \"$BUILT_PRODUCTS_DIR/${CONTENTS_FOLDER_PATH}/Library/SystemExtensions/${SYSEX_BUNDLE_ID}.systemextension\" || exit 1\n"; + shellScript = "if [ \"$CONFIGURATION\" != \"Debug\" ] || [ \"$ENABLE_PREVIEWS\" = \"YES\" ]; then exit 0; fi\n${BUILT_PRODUCTS_DIR}/SwiftLintTool\n"; }; - 7B557F2A2B8CA2A400099746 /* Embed Debug-only Network Extensions */ = { + 7B31FD922AD126C40086AA24 /* Embed System Network Extension */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; @@ -9146,16 +9228,16 @@ ); inputPaths = ( ); - name = "Embed Debug-only Network Extensions"; + name = "Embed System Network Extension"; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "# Support for embedding debug-only extensions (https://stackoverflow.com/a/76948057/712306)\nfunction embedExtensions() {\n for extension in $1\n do\n rsync -r --copy-links \"${CONFIGURATION_BUILD_DIR}/${extension}.appex\" \"${CONFIGURATION_BUILD_DIR}/${PLUGINS_FOLDER_PATH}\"\n done\n}\n\ndebug_extensions=(\"VPNProxyExtension\")\n\nif [ \"${CONFIGURATION}\" != \"Release\" ]\nthen\n embedExtensions $debug_extensions\nfi\n"; + shellScript = "if [[ -z \"${SYSEX_BUNDLE_ID}\" ]]; then\n echo \"Required build settings are not defined, please check xcconfig files\"\n exit 1\nfi\n\n\necho \"ditto ${BUILT_PRODUCTS_DIR}/${SYSEX_BUNDLE_ID}.systemextension $BUILT_PRODUCTS_DIR/${CONTENTS_FOLDER_PATH}/Library/SystemExtensions/${SYSEX_BUNDLE_ID}.systemextension\"\n\nditto \"${BUILT_PRODUCTS_DIR}/${SYSEX_BUNDLE_ID}.systemextension\" \"$BUILT_PRODUCTS_DIR/${CONTENTS_FOLDER_PATH}/Library/SystemExtensions/${SYSEX_BUNDLE_ID}.systemextension\" || exit 1\n"; }; - 7BB34F502AD98394005691AE /* Copy Swift Package resources */ = { + 7B557F2A2B8CA2A400099746 /* Embed Debug-only Network Extensions */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; @@ -9165,16 +9247,16 @@ ); inputPaths = ( ); - name = "Copy Swift Package resources"; + name = "Embed Debug-only Network Extensions"; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "# We had issues where the Swift Package resources were not being added to the Agent Apps,\n# so we're manually coping them here.\n# It seems to be a known issue: https://forums.swift.org/t/swift-packages-resource-bundle-not-present-in-xcarchive-when-framework-using-said-package-is-archived/50084/2\ncp -RL \"${BUILT_PRODUCTS_DIR}\"/ContentScopeScripts_ContentScopeScripts.bundle \"${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/\"\ncp -RL \"${BUILT_PRODUCTS_DIR}\"/DataBrokerProtection_DataBrokerProtection.bundle \"${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/\"\n"; + shellScript = "# Support for embedding debug-only extensions (https://stackoverflow.com/a/76948057/712306)\nfunction embedExtensions() {\n for extension in $1\n do\n rsync -r --copy-links \"${CONFIGURATION_BUILD_DIR}/${extension}.appex\" \"${CONFIGURATION_BUILD_DIR}/${PLUGINS_FOLDER_PATH}\"\n done\n}\n\ndebug_extensions=(\"VPNProxyExtension\")\n\nif [ \"${CONFIGURATION}\" != \"Release\" ]\nthen\n embedExtensions $debug_extensions\nfi\n"; }; - B6409DC52BC7BD1F00D66F9E /* Run swiftlint */ = { + 7BB34F502AD98394005691AE /* Copy Swift Package resources */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; @@ -9184,14 +9266,14 @@ ); inputPaths = ( ); - name = "Run swiftlint"; + name = "Copy Swift Package resources"; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if [ \"$CONFIGURATION\" != \"Debug\" ] || [ \"$ENABLE_PREVIEWS\" = \"YES\" ]; then exit 0; fi\n${BUILT_PRODUCTS_DIR}/SwiftLintTool\n"; + shellScript = "# We had issues where the Swift Package resources were not being added to the Agent Apps,\n# so we're manually coping them here.\n# It seems to be a known issue: https://forums.swift.org/t/swift-packages-resource-bundle-not-present-in-xcarchive-when-framework-using-said-package-is-archived/50084/2\ncp -RL \"${BUILT_PRODUCTS_DIR}\"/ContentScopeScripts_ContentScopeScripts.bundle \"${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/\"\ncp -RL \"${BUILT_PRODUCTS_DIR}\"/DataBrokerProtection_DataBrokerProtection.bundle \"${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/\"\n"; }; B6AEB5532BA3029B00781A09 /* Cleanup entitlements */ = { isa = PBXShellScriptBuildPhase; @@ -9231,25 +9313,6 @@ shellPath = /bin/sh; shellScript = "FRAMEWORKS_DIR=\"${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}\"\nDYLIB_TARGET_PATH=\"${FRAMEWORKS_DIR}/libswift_Concurrency.dylib\"\n\nif [ ! -e \"${DYLIB_TARGET_PATH}\" ]; then\n\n DYLIB_PATH=$(find \"${TOOLCHAIN_DIR}/usr/lib\" -path \"*/${HOST_PLATFORM}/libswift_Concurrency.dylib\")\n echo \"copy ${DYLIB_PATH} to ${DYLIB_TARGET_PATH}\"\n\n mkdir -p \"${FRAMEWORKS_DIR}\"\n cp \"${DYLIB_PATH}\" \"${DYLIB_TARGET_PATH}\" || exit 1\n\nelse\n echo \"${DYLIB_TARGET_PATH} exists 👌\"\nfi\n"; }; - B6E6BA192BA2D8BE008AA7E1 /* Run swiftlint */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "Run swiftlint"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "if [ \"$CONFIGURATION\" != \"Debug\" ] || [ \"$ENABLE_PREVIEWS\" = \"YES\" ]; then exit 0; fi\n${BUILT_PRODUCTS_DIR}/SwiftLintTool\n"; - }; B6F2C8722A7A4C7D000498CF /* Make /Applications symlink, remove app on Clean build */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -9436,6 +9499,7 @@ 1DDD3EC52B84F96B004CBF2B /* CookiePopupProtectionPreferences.swift in Sources */, 3706FAE0293F65D500E42796 /* BookmarkSidebarTreeController.swift in Sources */, 3706FAE1293F65D500E42796 /* HomePageFavoritesModel.swift in Sources */, + 7B4D8A222BDA857300852966 /* VPNOperationErrorRecorder.swift in Sources */, 3706FAE2293F65D500E42796 /* SequenceExtensions.swift in Sources */, 3706FAE3293F65D500E42796 /* ChromiumDataImporter.swift in Sources */, 3706FAE5293F65D500E42796 /* BackForwardListItemViewModel.swift in Sources */, @@ -9682,6 +9746,7 @@ B66260E129AC6EBD00E9E3EE /* HistoryTabExtension.swift in Sources */, 3706FB8E293F65D500E42796 /* FirefoxEncryptionKeyReader.swift in Sources */, 3706FB8F293F65D500E42796 /* BookmarkManagementSplitViewController.swift in Sources */, + 316913272BD2B76F0051B46D /* DataBrokerPrerequisitesStatusVerifier.swift in Sources */, 3706FB90293F65D500E42796 /* CookieManagedNotificationContainerView.swift in Sources */, 3706FB91293F65D500E42796 /* FileManagerExtension.swift in Sources */, 3706FB92293F65D500E42796 /* PermissionModel.swift in Sources */, @@ -9710,12 +9775,14 @@ 98779A0129999B64005D8EB6 /* Bookmark.xcdatamodeld in Sources */, 3706FB9E293F65D500E42796 /* AboutModel.swift in Sources */, 3706FB9F293F65D500E42796 /* PasswordManagementCreditCardItemView.swift in Sources */, + 9F9C4A022BC7F36D0099738D /* BookmarkAllTabsDialogCoordinatorViewModel.swift in Sources */, 3706FBA0293F65D500E42796 /* NSTextFieldExtension.swift in Sources */, 9FA173E82B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift in Sources */, 3706FBA1293F65D500E42796 /* FireproofDomainsContainer.swift in Sources */, 3706FBA2293F65D500E42796 /* GeolocationService.swift in Sources */, 4B4D60C42A0C849600BCD287 /* NetworkProtectionInvitePresenter.swift in Sources */, 3706FBA3293F65D500E42796 /* FireproofingURLExtensions.swift in Sources */, + 3169132A2BD2C7570051B46D /* DataBrokerProtectionErrorViewController.swift in Sources */, 1DDD3EC12B84F5D5004CBF2B /* PreferencesCookiePopupProtectionView.swift in Sources */, 3706FBA4293F65D500E42796 /* ContentOverlayPopover.swift in Sources */, 3706FBA5293F65D500E42796 /* TabShadowView.swift in Sources */, @@ -9771,6 +9838,7 @@ 3706FBCC293F65D500E42796 /* CustomRoundedCornersShape.swift in Sources */, 3706FBCD293F65D500E42796 /* LocaleExtension.swift in Sources */, 3706FBCE293F65D500E42796 /* SavePaymentMethodViewController.swift in Sources */, + 9FA5A0A62BC8F34900153786 /* UserDefaultsBookmarkFoldersStore.swift in Sources */, 1DDC85002B835BC000670238 /* SearchPreferences.swift in Sources */, 3706FBD0293F65D500E42796 /* WebKitVersionProvider.swift in Sources */, 3706FBD1293F65D500E42796 /* NSCoderExtensions.swift in Sources */, @@ -9966,6 +10034,7 @@ 3706FC51293F65D500E42796 /* RecentlyVisitedView.swift in Sources */, B6E3E5552BBFCEE300A41922 /* NoDownloadsCellView.swift in Sources */, B645D8F729FA95440024461F /* WKProcessPoolExtension.swift in Sources */, + 9F9C49FE2BC7E9830099738D /* BookmarkAllTabsDialogViewModel.swift in Sources */, 9F514F922B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift in Sources */, 3706FC52293F65D500E42796 /* MouseOverAnimationButton.swift in Sources */, B60293E72BA19ECD0033186B /* NetPPopoverManagerMock.swift in Sources */, @@ -10018,9 +10087,10 @@ 7BFE955A2A9DF4550081ABE9 /* NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift in Sources */, 9FDA6C222B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */, C1372EF52BBC5BAD003F8793 /* SecureTextField.swift in Sources */, + 316913242BD2B6250051B46D /* DataBrokerProtectionPixelsHandler.swift in Sources */, 3706FC77293F65D500E42796 /* PageObserverUserScript.swift in Sources */, 4BF0E5132AD25A2600FFEC9E /* DuckDuckGoUserAgent.swift in Sources */, - 3706FC78293F65D500E42796 /* SecureVaultErrorReporter.swift in Sources */, + 3706FC78293F65D500E42796 /* SecureVaultReporter.swift in Sources */, 3706FC79293F65D500E42796 /* NSImageExtensions.swift in Sources */, 3706FEBD293F6EFF00E42796 /* BWCommand.swift in Sources */, 3706FC7B293F65D500E42796 /* PasswordManagementViewController.swift in Sources */, @@ -10053,7 +10123,6 @@ 3706FC8D293F65D500E42796 /* PreferencesGeneralView.swift in Sources */, 37197EA92942443D00394917 /* WebView.swift in Sources */, 3706FC8E293F65D500E42796 /* PinnedTabsView.swift in Sources */, - 3706FC92293F65D500E42796 /* NSStoryboardExtension.swift in Sources */, B6B5F58A2B03673B008DB58A /* BrowserImportMoreInfoView.swift in Sources */, 3706FC93293F65D500E42796 /* PreferencesViewController.swift in Sources */, 3706FC94293F65D500E42796 /* FireproofDomains.swift in Sources */, @@ -10075,6 +10144,7 @@ 3706FCA0293F65D500E42796 /* ContiguousBytesExtension.swift in Sources */, B602E8172A1E2570006D261F /* URL+NetworkProtection.swift in Sources */, 3706FCA1293F65D500E42796 /* AdjacentItemEnumerator.swift in Sources */, + 9F9C49FA2BC7BC970099738D /* BookmarkAllTabsDialogView.swift in Sources */, 3706FCA2293F65D500E42796 /* ChromiumKeychainPrompt.swift in Sources */, 3707C71E294B5D2900682A9F /* URLRequestExtension.swift in Sources */, 3706FCA3293F65D500E42796 /* WKProcessPool+GeolocationProvider.swift in Sources */, @@ -10089,6 +10159,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 9F0FFFB92BCCAE9C007C87DD /* AddEditBookmarkDialogViewModelMock.swift in Sources */, 3706FDDA293F661700E42796 /* EmbeddedTrackerDataTests.swift in Sources */, 3706FDDB293F661700E42796 /* AutofillPreferencesTests.swift in Sources */, 3706FDDC293F661700E42796 /* FileManagerExtensionTests.swift in Sources */, @@ -10121,6 +10192,7 @@ 3706FDF5293F661700E42796 /* StartupPreferencesTests.swift in Sources */, 3706FDF6293F661700E42796 /* DuckPlayerTests.swift in Sources */, 3706FDF7293F661700E42796 /* WebViewExtensionTests.swift in Sources */, + 9FA5A0AA2BC900FC00153786 /* BookmarkAllTabsDialogViewModelTests.swift in Sources */, 56534DEE29DF252C00121467 /* CapturingDefaultBrowserProvider.swift in Sources */, 3706FDF8293F661700E42796 /* FileStoreTests.swift in Sources */, 5603D90729B7B746007F9F01 /* MockTabViewItemDelegate.swift in Sources */, @@ -10178,7 +10250,9 @@ 857E44642A9F70F200ED77A7 /* CampaignVariantTests.swift in Sources */, 3706FE1E293F661700E42796 /* GeolocationProviderTests.swift in Sources */, 9F39106A2B68D87B00CB5112 /* ProgressExtensionTests.swift in Sources */, + 9FAD623B2BCFDB32007F3A65 /* WebsiteInfoHelpers.swift in Sources */, 3706FE1F293F661700E42796 /* AppStateChangePublisherTests.swift in Sources */, + 9FA5A0B12BC9039300153786 /* BookmarkFolderStoreMock.swift in Sources */, 3706FE20293F661700E42796 /* CLLocationManagerMock.swift in Sources */, B6656E0E2B29C733008798A1 /* FileImportViewLocalizationTests.swift in Sources */, B6C843DB2BA1CAB6006FDEC3 /* FilePresenterTests.swift in Sources */, @@ -10209,12 +10283,12 @@ B630E80129C887ED00363609 /* NSErrorAdditionalInfo.swift in Sources */, 3706FE31293F661700E42796 /* TabCollectionViewModelDelegateMock.swift in Sources */, 3706FE32293F661700E42796 /* BookmarksHTMLReaderTests.swift in Sources */, - 7BBE650E2BC67BA0008F4EE9 /* NetworkProtectionIPCTunnelControllerTests.swift in Sources */, + 9F0FFFBC2BCCAEC2007C87DD /* AddEditBookmarkFolderDialogViewModelMock.swift in Sources */, 3706FE33293F661700E42796 /* FireTests.swift in Sources */, B60C6F8229B1B4AD007BFAA8 /* TestRunHelper.swift in Sources */, - 7BBE65112BC67EED008F4EE9 /* NetworkProtectionTestingSupport.swift in Sources */, 567DA94029E8045D008AC5EE /* MockEmailStorage.swift in Sources */, 317295D32AF058D3002C3206 /* MockWaitlistTermsAndConditionsActionHandler.swift in Sources */, + 9F0FFFB52BCCAE37007C87DD /* BookmarkAllTabsDialogCoordinatorViewModelTests.swift in Sources */, 3706FE34293F661700E42796 /* PermissionStoreTests.swift in Sources */, 3706FE35293F661700E42796 /* ThirdPartyBrowserTests.swift in Sources */, 1DFAB5232A8983E100A0F7F6 /* SetExtensionTests.swift in Sources */, @@ -10222,6 +10296,7 @@ 3706FE37293F661700E42796 /* TabCollectionViewModelTests+WithoutPinnedTabsManager.swift in Sources */, 3706FE38293F661700E42796 /* SuggestionContainerTests.swift in Sources */, 3706FE39293F661700E42796 /* TabTests.swift in Sources */, + 9FAD623E2BD09DE5007F3A65 /* WebsiteInfoTests.swift in Sources */, 3706FE3A293F661700E42796 /* MockVariantManager.swift in Sources */, 3706FE3C293F661700E42796 /* FireproofDomainsStoreMock.swift in Sources */, 3706FE3D293F661700E42796 /* DataEncryptionTests.swift in Sources */, @@ -10244,6 +10319,7 @@ 3706FE46293F661700E42796 /* EncryptedValueTransformerTests.swift in Sources */, 9F3910632B68C35600CB5112 /* DownloadsTabExtensionTests.swift in Sources */, 3706FE47293F661700E42796 /* URLExtensionTests.swift in Sources */, + 9F8D57332BCCCB9A00AEA660 /* UserDefaultsBookmarkFoldersStoreTests.swift in Sources */, 1DB9617B29F1D06D00CF5568 /* InternalUserDeciderMock.swift in Sources */, 317295D52AF058D3002C3206 /* MockWaitlistFeatureSetupHandler.swift in Sources */, 3706FE49293F661700E42796 /* BookmarkNodePathTests.swift in Sources */, @@ -10269,6 +10345,7 @@ 566B196429CDB824007E38F4 /* MoreOptionsMenuTests.swift in Sources */, 9F0A2CF92B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift in Sources */, 3706FE56293F661700E42796 /* FaviconManagerMock.swift in Sources */, + 9F9C49F72BC786790099738D /* MoreOptionsMenu+BookmarksTests.swift in Sources */, 3706FE57293F661700E42796 /* LocalPinningManagerTests.swift in Sources */, 3706FE58293F661700E42796 /* HistoryStoreTests.swift in Sources */, 3706FE59293F661700E42796 /* EncryptionKeyGeneratorTests.swift in Sources */, @@ -10285,6 +10362,7 @@ 3706FE5F293F661700E42796 /* CrashReportTests.swift in Sources */, B60C6F7F29B1B41D007BFAA8 /* TestRunHelperInitializer.m in Sources */, 4B9DB0592A983B55000927DB /* MockNetworkProtectionCodeRedeemer.swift in Sources */, + 9F0FFFBF2BCCAF1F007C87DD /* BookmarkAllTabsDialogViewModelMock.swift in Sources */, 3706FE61293F661700E42796 /* PinnedTabsViewModelTests.swift in Sources */, 3706FE62293F661700E42796 /* PasswordManagementListSectionTests.swift in Sources */, 3706FE63293F661700E42796 /* RecentlyClosedCoordinatorMock.swift in Sources */, @@ -10303,6 +10381,7 @@ 1D9FDEBE2B9B5F0F0040B78C /* CookiePopupProtectionPreferencesTests.swift in Sources */, 028904212A7B25770028369C /* AppConfigurationURLProviderTests.swift in Sources */, 3706FE6F293F661700E42796 /* LocalStatisticsStoreTests.swift in Sources */, + 31DC2F232BD6E028001354EF /* DataBrokerPrerequisitesStatusVerifierTests.swift in Sources */, 3706FE70293F661700E42796 /* HistoryCoordinatorTests.swift in Sources */, 9F3344632BBFBDA40040CBEB /* BookmarksBarVisibilityManagerTests.swift in Sources */, 3706FE71293F661700E42796 /* SavedStateMock.swift in Sources */, @@ -10434,7 +10513,6 @@ 4B25377A2A11C01700610219 /* UserText+NetworkProtectionExtensions.swift in Sources */, B65DA5F42A77D3FA00CBEE8D /* BundleExtension.swift in Sources */, EE66418D2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift in Sources */, - 4B2D062D2A11C12300DE1F49 /* Logging.swift in Sources */, 7B2E52252A5FEC09000C6D39 /* NetworkProtectionAgentNotificationsPresenter.swift in Sources */, B602E8232A1E260E006D261F /* Bundle+NetworkProtectionExtensions.swift in Sources */, 4B2D062A2A11C0C900DE1F49 /* NetworkProtectionOptionKeyExtension.swift in Sources */, @@ -10471,6 +10549,7 @@ 7BA7CC5D2AD120C30042E5CE /* EventMapping+NetworkProtectionError.swift in Sources */, 7BA7CC4A2AD11EA00042E5CE /* NetworkProtectionTunnelController.swift in Sources */, EE3424602BA0853900173B1B /* VPNUninstaller.swift in Sources */, + 7B4D8A232BDA857300852966 /* VPNOperationErrorRecorder.swift in Sources */, 7BD1688E2AD4A4C400D24876 /* NetworkExtensionController.swift in Sources */, 7BA7CC3E2AD11E380042E5CE /* TunnelControllerIPCService.swift in Sources */, 7BA7CC402AD11E3D0042E5CE /* AppLauncher+DefaultInitializer.swift in Sources */, @@ -10507,6 +10586,7 @@ 7BA7CC582AD1203A0042E5CE /* UserText+NetworkProtection.swift in Sources */, 7BA7CC4B2AD11EC60042E5CE /* NetworkProtectionControllerErrorStore.swift in Sources */, EE3424612BA0853900173B1B /* VPNUninstaller.swift in Sources */, + 7B4D8A242BDA857300852966 /* VPNOperationErrorRecorder.swift in Sources */, 4BF0E5152AD25A2600FFEC9E /* DuckDuckGoUserAgent.swift in Sources */, 7BFE95592A9DF2AF0081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift in Sources */, 7BA7CC5C2AD120C30042E5CE /* EventMapping+NetworkProtectionError.swift in Sources */, @@ -10596,7 +10676,6 @@ 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 */, @@ -10652,11 +10731,13 @@ 31AA6B972B960B870025014E /* DataBrokerProtectionLoginItemPixels.swift in Sources */, B6BF5D852946FFDA006742B1 /* PrivacyDashboardTabExtension.swift in Sources */, B6E3E55B2BC0041900A41922 /* DownloadListStoreMock.swift in Sources */, + 9FA5A0A52BC8F34900153786 /* UserDefaultsBookmarkFoldersStore.swift in Sources */, B6C0B23026E61D630031CB7F /* DownloadListStore.swift in Sources */, 85799C1825DEBB3F0007EC87 /* Logging.swift in Sources */, AAC30A2E268F1EE300D2D9CD /* CrashReportPromptPresenter.swift in Sources */, 1D2DC00629016798008083A1 /* BWCredential.swift in Sources */, EEA3EEB12B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */, + 316913232BD2B6250051B46D /* DataBrokerProtectionPixelsHandler.swift in Sources */, 37AFCE8727DA334800471A10 /* PreferencesRootView.swift in Sources */, B684590825C9027900DC17B6 /* AppStateChangedPublisher.swift in Sources */, 4B92928F26670D1700AD2C21 /* BookmarkTableCellView.swift in Sources */, @@ -10795,6 +10876,7 @@ 4B379C2227BDBA29008A968E /* LocalAuthenticationService.swift in Sources */, 37CEFCA92A6737A2001EF741 /* CredentialsCleanupErrorHandling.swift in Sources */, 4BB99D0326FE191E001E4761 /* SafariBookmarksReader.swift in Sources */, + 316913292BD2C7570051B46D /* DataBrokerProtectionErrorViewController.swift in Sources */, 1DA6D0FD2A1FF9A100540406 /* HTTPCookie.swift in Sources */, AACF6FD626BC366D00CF09F9 /* SafariVersionReader.swift in Sources */, 4BE65485271FCD7B008D1D63 /* LoginFaviconView.swift in Sources */, @@ -11075,6 +11157,7 @@ 85D885B326A5A9DE0077C374 /* NSAlert+PasswordManager.swift in Sources */, 983DFB2528B67036006B7E34 /* UserContentUpdating.swift in Sources */, 1D9A4E5A2B43213B00F449E2 /* TabSnapshotExtension.swift in Sources */, + 316913262BD2B76F0051B46D /* DataBrokerPrerequisitesStatusVerifier.swift in Sources */, 4B7A57CF279A4EF300B1C70E /* ChromiumPreferences.swift in Sources */, AA6AD95B2704B6DB00159F8A /* FirePopoverViewController.swift in Sources */, 4BE4005327CF3DC3007D3161 /* SavePaymentMethodPopover.swift in Sources */, @@ -11156,6 +11239,7 @@ AAD86E52267A0DFF005C11BE /* UpdateController.swift in Sources */, 85A0118225AF60E700FA6A0C /* FindInPageModel.swift in Sources */, 7BA7CC4E2AD11F6F0042E5CE /* NetworkProtectionIPCTunnelController.swift in Sources */, + 9F9C49FD2BC7E9830099738D /* BookmarkAllTabsDialogViewModel.swift in Sources */, 4B9292A226670D2A00AD2C21 /* PseudoFolder.swift in Sources */, 1DDD3EC42B84F96B004CBF2B /* CookiePopupProtectionPreferences.swift in Sources */, 4BCF15D92ABB8A7F0083F6DF /* NetworkProtectionRemoteMessage.swift in Sources */, @@ -11223,6 +11307,7 @@ B69B503C2726A12500758A2B /* StatisticsStore.swift in Sources */, 3158B14A2B0BF74300AF130C /* DataBrokerProtectionDebugMenu.swift in Sources */, 4BBDEE9128FC14760092FAA6 /* BWInstallationService.swift in Sources */, + 7B4D8A212BDA857300852966 /* VPNOperationErrorRecorder.swift in Sources */, 859F30642A72A7BB00C20372 /* BookmarksBarPromptPopover.swift in Sources */, 4B4D60CA2A0C849600BCD287 /* NetworkProtectionInvitePresenter.swift in Sources */, B693955426F04BEC0015B914 /* ColorView.swift in Sources */, @@ -11245,6 +11330,7 @@ B693955726F04BEC0015B914 /* MouseOverButton.swift in Sources */, AA61C0D02722159B00E6B681 /* FireInfoViewController.swift in Sources */, 9D9AE8692AA76CDC0026E7DC /* LoginItem+NetworkProtection.swift in Sources */, + 9F9C49F92BC7BC970099738D /* BookmarkAllTabsDialogView.swift in Sources */, B64C85422694590B0048FEBE /* PermissionButton.swift in Sources */, AAA0CC472533833C0079BC96 /* MoreOptionsMenu.swift in Sources */, B64C84E32692DC9F0048FEBE /* PermissionAuthorizationViewController.swift in Sources */, @@ -11340,7 +11426,8 @@ 1D2DC0072901679C008083A1 /* BWError.swift in Sources */, 853014D625E671A000FB8205 /* PageObserverUserScript.swift in Sources */, B677FC4F2B06376B0099EB04 /* ReportFeedbackView.swift in Sources */, - B642738227B65BAC0005DFD1 /* SecureVaultErrorReporter.swift in Sources */, + B642738227B65BAC0005DFD1 /* SecureVaultReporter.swift in Sources */, + 9F9C4A012BC7F36D0099738D /* BookmarkAllTabsDialogCoordinatorViewModel.swift in Sources */, 4B139AFD26B60BD800894F82 /* NSImageExtensions.swift in Sources */, B62B48392ADE46FC000DECE5 /* Application.swift in Sources */, 4B9DB02C2A983B24000927DB /* WaitlistKeychainStorage.swift in Sources */, @@ -11378,7 +11465,6 @@ AA8EDF2424923E980071C2E8 /* URLExtension.swift in Sources */, B634DBDF293C8F7F00C3C99E /* Tab+UIDelegate.swift in Sources */, B6F1B02E2BCE6B47005E863C /* TunnelControllerProvider.swift in Sources */, - 4BE0DF06267819A1006337B7 /* NSStoryboardExtension.swift in Sources */, 37AFCE8127DA2CA600471A10 /* PreferencesViewController.swift in Sources */, 4B02198A25E05FAC00ED7DEA /* FireproofDomains.swift in Sources */, 4B677442255DBEEA00025BD8 /* Database.swift in Sources */, @@ -11427,6 +11513,7 @@ 37D2377C287EBDA300BCE03B /* TabIndexTests.swift in Sources */, 37534CA52811987D002621E7 /* AdjacentItemEnumeratorTests.swift in Sources */, 56B234BF2A84EFD200F2A1CC /* NavigationBarUrlExtensionsTests.swift in Sources */, + 9FAD623A2BCFDB32007F3A65 /* WebsiteInfoHelpers.swift in Sources */, C13909F42B85FD79001626ED /* AutofillDeleteAllPasswordsExecutorTests.swift in Sources */, 37534C9E28104D9B002621E7 /* TabLazyLoaderTests.swift in Sources */, B6619EF62B10DFF700CD9186 /* InstructionsFormatParserTests.swift in Sources */, @@ -11436,6 +11523,7 @@ 4B9292BD2667103100AD2C21 /* BookmarkOutlineViewDataSourceTests.swift in Sources */, C17CA7B22B9B5317008EC3C1 /* MockAutofillPopoverPresenter.swift in Sources */, 4BF6961D28BE911100D402D4 /* RecentlyVisitedSiteModelTests.swift in Sources */, + 9FA5A0A92BC900FC00153786 /* BookmarkAllTabsDialogViewModelTests.swift in Sources */, B6619F062B17138D00CD9186 /* DataImportSourceViewModelTests.swift in Sources */, 4BBF0917282DD6EF00EE1418 /* TemporaryFileHandlerTests.swift in Sources */, B6A5A27925B93FFF00AA7ADA /* StateRestorationManagerTests.swift in Sources */, @@ -11484,6 +11572,7 @@ 858A798826A99DBE00A75A42 /* PasswordManagementItemListModelTests.swift in Sources */, 566B196529CDB828007E38F4 /* CapturingOptionsButtonMenuDelegate.swift in Sources */, 4B8AD0B127A86D9200AE44D6 /* WKWebsiteDataStoreExtensionTests.swift in Sources */, + 9FA5A0B02BC9039200153786 /* BookmarkFolderStoreMock.swift in Sources */, B69B50472726C5C200758A2B /* VariantManagerTests.swift in Sources */, 8546DE6225C03056000CA5E1 /* UserAgentTests.swift in Sources */, 9F26060B2B85C20A00819292 /* AddEditBookmarkDialogViewModelTests.swift in Sources */, @@ -11500,9 +11589,11 @@ 85AC3B4925DAC9BD00C7D2AA /* ConfigurationStorageTests.swift in Sources */, 9F180D122B69C665000D695F /* DownloadsTabExtensionMock.swift in Sources */, AA91F83927076F1900771A0D /* PrivacyIconViewModelTests.swift in Sources */, + 9F9C49F62BC786790099738D /* MoreOptionsMenu+BookmarksTests.swift in Sources */, 4B723E0726B0003E00E14D75 /* CSVImporterTests.swift in Sources */, 4BCF15EC2ABB9AF80083F6DF /* NetworkProtectionRemoteMessageTests.swift in Sources */, B62EB47C25BAD3BB005745C6 /* WKWebViewPrivateMethodsAvailabilityTests.swift in Sources */, + 9F0FFFBB2BCCAEC2007C87DD /* AddEditBookmarkFolderDialogViewModelMock.swift in Sources */, 4BBC16A527C488C900E00A38 /* DeviceAuthenticatorTests.swift in Sources */, 4B3F641E27A8D3BD00E0C118 /* BrowserProfileTests.swift in Sources */, B6106BA026A7BE0B0013B453 /* PermissionManagerTests.swift in Sources */, @@ -11535,6 +11626,7 @@ 142879DC24CE1185005419BB /* SuggestionContainerViewModelTests.swift in Sources */, 566B195D29CDB692007E38F4 /* MoreOptionsMenuTests.swift in Sources */, AA0877B826D5160D00B05660 /* SafariVersionReaderTests.swift in Sources */, + 31DC2F222BD6DE6C001354EF /* DataBrokerPrerequisitesStatusVerifierTests.swift in Sources */, B69B50452726C5C200758A2B /* AtbParserTests.swift in Sources */, 1D8C2FED2B70F5D0005E4BBD /* MockViewSnapshotRenderer.swift in Sources */, B6106BAF26A7C6180013B453 /* PermissionStoreMock.swift in Sources */, @@ -11555,6 +11647,7 @@ B6106BB126A7D8720013B453 /* PermissionStoreTests.swift in Sources */, 4BF4951826C08395000547B8 /* ThirdPartyBrowserTests.swift in Sources */, 4B98D27C28D960DD003C2B6F /* FirefoxFaviconsReaderTests.swift in Sources */, + 9F0FFFBE2BCCAF1F007C87DD /* BookmarkAllTabsDialogViewModelMock.swift in Sources */, B60C6F7E29B1B41D007BFAA8 /* TestRunHelperInitializer.m in Sources */, 37479F152891BC8300302FE2 /* TabCollectionViewModelTests+WithoutPinnedTabsManager.swift in Sources */, AA63745424C9BF9A00AB2AC4 /* SuggestionContainerTests.swift in Sources */, @@ -11577,7 +11670,6 @@ 1D3B1AC22936B816006F4388 /* BWMessageIdGeneratorTests.swift in Sources */, B6C2C9F62760B659005B7F0A /* TestDataModel.xcdatamodeld in Sources */, 1DA6D1022A1FFA3700540406 /* HTTPCookieTests.swift in Sources */, - 7BBE65102BC67EED008F4EE9 /* NetworkProtectionTestingSupport.swift in Sources */, 1D9FDEC02B9B5FEA0040B78C /* AccessibilityPreferencesTests.swift in Sources */, B68172AE269EB43F006D1092 /* GeolocationServiceTests.swift in Sources */, B6AE74342609AFCE005B9B1A /* ProgressEstimationTests.swift in Sources */, @@ -11607,7 +11699,9 @@ AABAF59C260A7D130085060C /* FaviconManagerMock.swift in Sources */, 1DFAB5222A8983DE00A0F7F6 /* SetExtensionTests.swift in Sources */, 4BF6962028BEEE8B00D402D4 /* LocalPinningManagerTests.swift in Sources */, + 9F0FFFB82BCCAE9C007C87DD /* AddEditBookmarkDialogViewModelMock.swift in Sources */, AAEC74B82642E43800C2EFBC /* HistoryStoreTests.swift in Sources */, + 9FAD623D2BD09DE5007F3A65 /* WebsiteInfoTests.swift in Sources */, 9F180D0F2B69C553000D695F /* Tab+WKUIDelegateTests.swift in Sources */, 4BA1A6E6258C270800F6F690 /* EncryptionKeyGeneratorTests.swift in Sources */, B6106BB326A7F4AA0013B453 /* GeolocationServiceMock.swift in Sources */, @@ -11616,6 +11710,7 @@ 4BBF09232830812900EE1418 /* FileSystemDSL.swift in Sources */, 4B9DB05C2A983B55000927DB /* WaitlistViewModelTests.swift in Sources */, 1D1C36E329FAE8DA001FA40C /* FaviconManagerTests.swift in Sources */, + 9F0FFFB42BCCAE37007C87DD /* BookmarkAllTabsDialogCoordinatorViewModelTests.swift in Sources */, 4B723E0526B0003E00E14D75 /* DataImportMocks.swift in Sources */, 4B70C00227B0793D000386ED /* CrashReportTests.swift in Sources */, B6656E0D2B29C733008798A1 /* FileImportViewLocalizationTests.swift in Sources */, @@ -11650,6 +11745,7 @@ 9FBD84562BB3ACFD00220859 /* AttributionOriginFileProviderTests.swift in Sources */, 31E163BA293A56F400963C10 /* BrokenSiteReportingReferenceTests.swift in Sources */, 1D9FDEC32B9B63C90040B78C /* DataClearingPreferencesTests.swift in Sources */, + 9F8D57322BCCCB9A00AEA660 /* UserDefaultsBookmarkFoldersStoreTests.swift in Sources */, 4B723E0826B0003E00E14D75 /* MockSecureVault.swift in Sources */, 37CD54B527F1AC1300F1F7B9 /* PreferencesSidebarModelTests.swift in Sources */, B6CA4824298CDC2E0067ECCE /* AdClickAttributionTabExtensionTests.swift in Sources */, @@ -11657,7 +11753,6 @@ 37CD54B927F1F8AC00F1F7B9 /* AppearancePreferencesTests.swift in Sources */, EEF53E182950CED5002D78F4 /* JSAlertViewModelTests.swift in Sources */, 376C4DB928A1A48A00CC0F5B /* FirePopoverViewModelTests.swift in Sources */, - 7BBE650D2BC67BA0008F4EE9 /* NetworkProtectionIPCTunnelControllerTests.swift in Sources */, AAEC74B62642CC6A00C2EFBC /* HistoryStoringMock.swift in Sources */, AA652CB125DD825B009059CC /* LocalBookmarkStoreTests.swift in Sources */, B630794226731F5400DCEE41 /* WKDownloadMock.swift in Sources */, @@ -11773,14 +11868,6 @@ target = 4B2537592A11BE7300610219 /* NetworkProtectionSystemExtension */; targetProxy = 7BEC18302AD5DA3300D30536 /* PBXContainerItemProxy */; }; - B637D1BB2BC6AE5600C7DCA7 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - productRef = B637D1BA2BC6AE5600C7DCA7 /* SwiftLintTool */; - }; - B637D1BD2BC6AE6200C7DCA7 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - productRef = B637D1BC2BC6AE6200C7DCA7 /* SwiftLintTool */; - }; B6AEB5552BA3042300781A09 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = B6E6B9F22BA1FD90008AA7E1 /* sandbox-test-tool */; @@ -12640,6 +12727,22 @@ version = 3.1.4000; }; }; + 3FFD51CF7C19ACBDB9687474 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; + requirement = { + kind = exactVersion; + version = 140.0.3; + }; + }; + 4311906792B7676CE9535D76 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; + requirement = { + kind = revision; + revision = c06709ba8a586f6a40190bacaaaaa96b2d55e540; + }; + }; 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; @@ -12704,13 +12807,21 @@ version = 0.1.0; }; }; + FAE06B199CA1F209B55B34E9 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; + requirement = { + kind = exactVersion; + version = 137.0.0; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 1E46E19F2BD029BD0007273A /* Subscription */ = { + 08D4923DC968236E22E373E2 /* Crashes */ = { isa = XCSwiftPackageProductDependency; - package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; - productName = Subscription; + package = FAE06B199CA1F209B55B34E9 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Crashes; }; 1E950E3E2912A10D0051A99B /* ContentBlocking */ = { isa = XCSwiftPackageProductDependency; @@ -12833,10 +12944,6 @@ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = DDGSync; }; - 373FB4B02B4D6C42004C88D6 /* PreferencesViews */ = { - isa = XCSwiftPackageProductDependency; - productName = PreferencesViews; - }; 373FB4B22B4D6C4B004C88D6 /* PreferencesViews */ = { isa = XCSwiftPackageProductDependency; productName = PreferencesViews; @@ -12862,16 +12969,6 @@ isa = XCSwiftPackageProductDependency; productName = SyncUI; }; - 37CF91582BB416A500BADCAE /* Crashes */ = { - isa = XCSwiftPackageProductDependency; - package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; - productName = Crashes; - }; - 37CF915A2BB416AC00BADCAE /* Crashes */ = { - isa = XCSwiftPackageProductDependency; - package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; - productName = Crashes; - }; 37DF000429F9C056002B7D3E /* SyncDataProviders */ = { isa = XCSwiftPackageProductDependency; package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; @@ -12887,10 +12984,6 @@ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = Navigation; }; - 4B2537762A11BFE100610219 /* PixelKit */ = { - isa = XCSwiftPackageProductDependency; - productName = PixelKit; - }; 4B2D062B2A11C0E100DE1F49 /* Networking */ = { isa = XCSwiftPackageProductDependency; package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; @@ -12912,10 +13005,6 @@ isa = XCSwiftPackageProductDependency; productName = NetworkProtection; }; - 4B4D60972A0B2A5C00BCD287 /* PixelKit */ = { - isa = XCSwiftPackageProductDependency; - productName = PixelKit; - }; 4B4D60AE2A0C837F00BCD287 /* Networking */ = { isa = XCSwiftPackageProductDependency; package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; @@ -12933,14 +13022,6 @@ isa = XCSwiftPackageProductDependency; productName = "plugin:InputFilesChecker"; }; - 4B81AD342B29512B00706C96 /* PixelKitTestingUtilities */ = { - isa = XCSwiftPackageProductDependency; - productName = PixelKitTestingUtilities; - }; - 4B81AD362B29513100706C96 /* PixelKitTestingUtilities */ = { - isa = XCSwiftPackageProductDependency; - productName = PixelKitTestingUtilities; - }; 4BA7C4DC2B3F64E500AFE511 /* LoginItems */ = { isa = XCSwiftPackageProductDependency; productName = LoginItems; @@ -12972,6 +13053,11 @@ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = NetworkProtection; }; + 537FC71EA5115A983FAF3170 /* Crashes */ = { + isa = XCSwiftPackageProductDependency; + package = 4311906792B7676CE9535D76 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Crashes; + }; 7B00997C2B6508B700FE7C31 /* NetworkProtectionProxy */ = { isa = XCSwiftPackageProductDependency; productName = NetworkProtectionProxy; @@ -12997,14 +13083,6 @@ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = Subscription; }; - 7B5DD6992AE51FFA001DE99C /* PixelKit */ = { - isa = XCSwiftPackageProductDependency; - productName = PixelKit; - }; - 7B5F9A742AE2BE4E002AEBC0 /* PixelKit */ = { - isa = XCSwiftPackageProductDependency; - productName = PixelKit; - }; 7B624F162BA25C1F00A6C544 /* NetworkProtectionUI */ = { isa = XCSwiftPackageProductDependency; productName = NetworkProtectionUI; @@ -13023,10 +13101,6 @@ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = Common; }; - 7B97CD612B7E0C4B004FEF43 /* PixelKit */ = { - isa = XCSwiftPackageProductDependency; - productName = PixelKit; - }; 7BA076BA2B65D61400D7FB72 /* NetworkProtectionProxy */ = { isa = XCSwiftPackageProductDependency; productName = NetworkProtectionProxy; @@ -13065,14 +13139,6 @@ isa = XCSwiftPackageProductDependency; productName = NetworkProtectionUI; }; - 7BFCB74D2ADE7E1A00DA3EA7 /* PixelKit */ = { - isa = XCSwiftPackageProductDependency; - productName = PixelKit; - }; - 7BFCB74F2ADE7E2300DA3EA7 /* PixelKit */ = { - isa = XCSwiftPackageProductDependency; - productName = PixelKit; - }; 85D44B852BA08D29001B4AB5 /* Suggestions */ = { isa = XCSwiftPackageProductDependency; package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; @@ -13113,14 +13179,6 @@ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = Persistence; }; - 9D6983F82AC773C3002C02FC /* PixelKit */ = { - isa = XCSwiftPackageProductDependency; - productName = PixelKit; - }; - 9D6983FA2AC773C8002C02FC /* PixelKit */ = { - isa = XCSwiftPackageProductDependency; - productName = PixelKit; - }; 9D9AE8F82AAA3AD00026E7DC /* DataBrokerProtection */ = { isa = XCSwiftPackageProductDependency; productName = DataBrokerProtection; @@ -13129,10 +13187,6 @@ isa = XCSwiftPackageProductDependency; productName = DataBrokerProtection; }; - 9DB6E7232AA0DC5800A17F3C /* LoginItems */ = { - isa = XCSwiftPackageProductDependency; - productName = LoginItems; - }; 9DC70B192AA1FA5B005A844B /* LoginItems */ = { isa = XCSwiftPackageProductDependency; productName = LoginItems; @@ -13157,16 +13211,6 @@ package = AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */; productName = Sparkle; }; - B637D1BA2BC6AE5600C7DCA7 /* SwiftLintTool */ = { - isa = XCSwiftPackageProductDependency; - package = B6F997B92B8F352500476735 /* XCRemoteSwiftPackageReference "apple-toolbox" */; - productName = SwiftLintTool; - }; - B637D1BC2BC6AE6200C7DCA7 /* SwiftLintTool */ = { - isa = XCSwiftPackageProductDependency; - package = B6F997B92B8F352500476735 /* XCRemoteSwiftPackageReference "apple-toolbox" */; - productName = SwiftLintTool; - }; B65CD8CA2B316DF100A595BB /* SnapshotTesting */ = { isa = XCSwiftPackageProductDependency; package = B65CD8C92B316DF100A595BB /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */; @@ -13252,6 +13296,11 @@ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = Configuration; }; + DC3F73D49B2D44464AFEFCD8 /* Subscription */ = { + isa = XCSwiftPackageProductDependency; + package = 3FFD51CF7C19ACBDB9687474 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Subscription; + }; EE02D41F2BB460C000DBE6B3 /* BrowserServicesKit */ = { isa = XCSwiftPackageProductDependency; package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; @@ -13287,6 +13336,61 @@ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = NetworkProtection; }; + F116A7C22BD1924B00F3FCF7 /* PixelKitTestingUtilities */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = PixelKitTestingUtilities; + }; + F116A7C62BD1925500F3FCF7 /* PixelKitTestingUtilities */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = PixelKitTestingUtilities; + }; + F116A7C82BD1929000F3FCF7 /* PixelKitTestingUtilities */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = PixelKitTestingUtilities; + }; + F198C7112BD18A28000BF24D /* PixelKit */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = PixelKit; + }; + F198C7132BD18A30000BF24D /* PixelKit */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = PixelKit; + }; + F198C7152BD18A44000BF24D /* PixelKit */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = PixelKit; + }; + F198C7172BD18A4C000BF24D /* PixelKit */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = PixelKit; + }; + F198C7192BD18A5B000BF24D /* PixelKit */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = PixelKit; + }; + F198C71B2BD18A61000BF24D /* PixelKit */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = PixelKit; + }; + F198C71D2BD18D88000BF24D /* SwiftLintTool */ = { + isa = XCSwiftPackageProductDependency; + package = B6F997B92B8F352500476735 /* XCRemoteSwiftPackageReference "apple-toolbox" */; + productName = SwiftLintTool; + }; + F198C71F2BD18D92000BF24D /* SwiftLintTool */ = { + isa = XCSwiftPackageProductDependency; + package = B6F997B92B8F352500476735 /* XCRemoteSwiftPackageReference "apple-toolbox" */; + productName = SwiftLintTool; + }; F1D43AF22B98E47800BAB743 /* BareBonesBrowserKit */ = { isa = XCSwiftPackageProductDependency; package = F1D43AF12B98E47800BAB743 /* XCRemoteSwiftPackageReference "BareBonesBrowser" */; @@ -13297,6 +13401,10 @@ package = F1D43AF12B98E47800BAB743 /* XCRemoteSwiftPackageReference "BareBonesBrowser" */; productName = BareBonesBrowserKit; }; + F1DF95E62BD188B60045E591 /* LoginItems */ = { + isa = XCSwiftPackageProductDependency; + productName = LoginItems; + }; /* End XCSwiftPackageProductDependency section */ /* Begin XCVersionGroup section */ diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser App Store.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser App Store.xcscheme index 86fd422c11..e343e06df9 100644 --- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser App Store.xcscheme +++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser App Store.xcscheme @@ -157,16 +157,6 @@ ReferencedContainer = "container:LocalPackages/SyncUI"> - - - - - - - - - - - - - - - - + shouldUseLaunchSchemeArgsEnv = "YES"> @@ -55,6 +54,12 @@ + + + + @@ -65,6 +70,11 @@ BlueprintName = "UI Tests" ReferencedContainer = "container:DuckDuckGo.xcodeproj"> + + + + diff --git a/DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Chevron-Right-12-light.svg b/DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Chevron-Right-12-light.svg new file mode 100644 index 0000000000..2b4a602355 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Chevron-Right-12-light.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Chevron-Right-12.svg b/DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Chevron-Right-12.svg new file mode 100644 index 0000000000..4faab69801 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Chevron-Right-12.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Contents.json new file mode 100644 index 0000000000..7fea6d5282 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "Chevron-Right-12.svg", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "Chevron-Right-12-light.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtectionWaitlist/Contents.json b/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/Contents.json similarity index 100% rename from DuckDuckGo/Assets.xcassets/Images/DataBrokerProtectionWaitlist/Contents.json rename to DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/Contents.json diff --git a/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtectionWaitlist/DBP-Icon.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/DBP-Icon.imageset/Contents.json similarity index 100% rename from DuckDuckGo/Assets.xcassets/Images/DataBrokerProtectionWaitlist/DBP-Icon.imageset/Contents.json rename to DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/DBP-Icon.imageset/Contents.json diff --git a/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtectionWaitlist/DBP-Icon.imageset/DBP-Icon.pdf b/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/DBP-Icon.imageset/DBP-Icon.pdf similarity index 100% rename from DuckDuckGo/Assets.xcassets/Images/DataBrokerProtectionWaitlist/DBP-Icon.imageset/DBP-Icon.pdf rename to DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/DBP-Icon.imageset/DBP-Icon.pdf diff --git a/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtectionWaitlist/DBP-Information-Remover.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/DBP-Information-Remover.imageset/Contents.json similarity index 100% rename from DuckDuckGo/Assets.xcassets/Images/DataBrokerProtectionWaitlist/DBP-Information-Remover.imageset/Contents.json rename to DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/DBP-Information-Remover.imageset/Contents.json diff --git a/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtectionWaitlist/DBP-Information-Remover.imageset/DBP-Information-Remover.svg b/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/DBP-Information-Remover.imageset/DBP-Information-Remover.svg similarity index 100% rename from DuckDuckGo/Assets.xcassets/Images/DataBrokerProtectionWaitlist/DBP-Information-Remover.imageset/DBP-Information-Remover.svg rename to DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/DBP-Information-Remover.imageset/DBP-Information-Remover.svg diff --git a/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/dbp-error-info.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/dbp-error-info.imageset/Contents.json new file mode 100644 index 0000000000..9869fe984d --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/dbp-error-info.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "dbp-error-info.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/dbp-error-info.imageset/dbp-error-info.pdf b/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/dbp-error-info.imageset/dbp-error-info.pdf new file mode 100644 index 0000000000..c9486753e5 Binary files /dev/null and b/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/dbp-error-info.imageset/dbp-error-info.pdf differ diff --git a/DuckDuckGo/Assets.xcassets/Images/Identity-Theft-Restoration-Multicolor-16.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Identity-Theft-Restoration-Multicolor-16.imageset/Contents.json new file mode 100644 index 0000000000..c878d4b14f --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Identity-Theft-Restoration-Multicolor-16.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Identity-Theft-Restoration-Multicolor-16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Identity-Theft-Restoration-Multicolor-16.imageset/Identity-Theft-Restoration-Multicolor-16.pdf b/DuckDuckGo/Assets.xcassets/Images/Identity-Theft-Restoration-Multicolor-16.imageset/Identity-Theft-Restoration-Multicolor-16.pdf new file mode 100644 index 0000000000..30563fa583 Binary files /dev/null and b/DuckDuckGo/Assets.xcassets/Images/Identity-Theft-Restoration-Multicolor-16.imageset/Identity-Theft-Restoration-Multicolor-16.pdf differ diff --git a/DuckDuckGo/Assets.xcassets/Images/PersonalInformationRemoval-Multicolor-16.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/PersonalInformationRemoval-Multicolor-16.imageset/Contents.json new file mode 100644 index 0000000000..a30ef3d53e --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/PersonalInformationRemoval-Multicolor-16.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "PersonalInformationRemoval-Multicolor-16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/PersonalInformationRemoval-Multicolor-16.imageset/PersonalInformationRemoval-Multicolor-16.pdf b/DuckDuckGo/Assets.xcassets/Images/PersonalInformationRemoval-Multicolor-16.imageset/PersonalInformationRemoval-Multicolor-16.pdf new file mode 100644 index 0000000000..fbabea4523 Binary files /dev/null and b/DuckDuckGo/Assets.xcassets/Images/PersonalInformationRemoval-Multicolor-16.imageset/PersonalInformationRemoval-Multicolor-16.pdf differ diff --git a/DuckDuckGo/Assets.xcassets/Images/Settings-Multicolor-16.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Settings-Multicolor-16.imageset/Contents.json new file mode 100644 index 0000000000..d4b2052646 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Settings-Multicolor-16.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Settings-Multicolor-16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Settings-Multicolor-16.imageset/Settings-Multicolor-16.pdf b/DuckDuckGo/Assets.xcassets/Images/Settings-Multicolor-16.imageset/Settings-Multicolor-16.pdf new file mode 100644 index 0000000000..b3b41002c2 Binary files /dev/null and b/DuckDuckGo/Assets.xcassets/Images/Settings-Multicolor-16.imageset/Settings-Multicolor-16.pdf differ diff --git a/DuckDuckGo/Autoconsent/AutoconsentUserScript.swift b/DuckDuckGo/Autoconsent/AutoconsentUserScript.swift index c43f7dc27c..2000073692 100644 --- a/DuckDuckGo/Autoconsent/AutoconsentUserScript.swift +++ b/DuckDuckGo/Autoconsent/AutoconsentUserScript.swift @@ -202,7 +202,7 @@ extension AutoconsentUserScript { return } - guard [.http, .https].contains(url.navigationalScheme) else { + guard url.navigationalScheme?.isHypertextScheme == true else { // ignore special schemes os_log("Ignoring special URL scheme: %s", log: .autoconsent, type: .debug, messageData.url) replyHandler([ "type": "ok" ], nil) // this is just to prevent a Promise rejection diff --git a/DuckDuckGo/Autoconsent/autoconsent-bundle.js b/DuckDuckGo/Autoconsent/autoconsent-bundle.js index d7ccb86ad9..85e242339a 100644 --- a/DuckDuckGo/Autoconsent/autoconsent-bundle.js +++ b/DuckDuckGo/Autoconsent/autoconsent-bundle.js @@ -1 +1 @@ -!function(){"use strict";var e=class e{static setBase(t){e.base=t}static findElement(t,o=null,c=!1){let i=null;return i=null!=o?Array.from(o.querySelectorAll(t.selector)):null!=e.base?Array.from(e.base.querySelectorAll(t.selector)):Array.from(document.querySelectorAll(t.selector)),null!=t.textFilter&&(i=i.filter((e=>{const o=e.textContent.toLowerCase();if(Array.isArray(t.textFilter)){let e=!1;for(const c of t.textFilter)if(-1!==o.indexOf(c.toLowerCase())){e=!0;break}return e}if(null!=t.textFilter)return-1!==o.indexOf(t.textFilter.toLowerCase())}))),null!=t.styleFilters&&(i=i.filter((e=>{const o=window.getComputedStyle(e);let c=!0;for(const e of t.styleFilters){const t=o[e.option];c=e.negated?c&&t!==e.value:c&&t===e.value}return c}))),null!=t.displayFilter&&(i=i.filter((e=>t.displayFilter?0!==e.offsetHeight:0===e.offsetHeight))),null!=t.iframeFilter&&(i=i.filter((()=>t.iframeFilter?window.location!==window.parent.location:window.location===window.parent.location))),null!=t.childFilter&&(i=i.filter((o=>{const c=e.base;e.setBase(o);const i=e.find(t.childFilter);return e.setBase(c),null!=i.target}))),c?i:(i.length>1&&console.warn("Multiple possible targets: ",i,t,o),i[0])}static find(t,o=!1){const c=[];if(null!=t.parent){const i=e.findElement(t.parent,null,o);if(null!=i){if(i instanceof Array)return i.forEach((i=>{const n=e.findElement(t.target,i,o);n instanceof Array?n.forEach((e=>{c.push({parent:i,target:e})})):c.push({parent:i,target:n})})),c;{const n=e.findElement(t.target,i,o);n instanceof Array?n.forEach((e=>{c.push({parent:i,target:e})})):c.push({parent:i,target:n})}}}else{const i=e.findElement(t.target,null,o);i instanceof Array?i.forEach((e=>{c.push({parent:null,target:e})})):c.push({parent:null,target:i})}return 0===c.length&&c.push({parent:null,target:null}),o?c:(1!==c.length&&console.warn("Multiple results found, even though multiple false",c),c[0])}};e.base=null;var t=e;function o(e){const o=t.find(e);return"css"===e.type?!!o.target:"checkbox"===e.type?!!o.target&&o.target.checked:void 0}async function c(e,n){switch(e.type){case"click":return async function(e){const o=t.find(e);null!=o.target&&o.target.click();return i(0)}(e);case"list":return async function(e,t){for(const o of e.actions)await c(o,t)}(e,n);case"consent":return async function(e,t){for(const i of e.consents){const e=-1!==t.indexOf(i.type);if(i.matcher&&i.toggleAction){o(i.matcher)!==e&&await c(i.toggleAction)}else e?await c(i.trueAction):await c(i.falseAction)}}(e,n);case"ifcss":return async function(e,o){const i=t.find(e);i.target?e.falseAction&&await c(e.falseAction,o):e.trueAction&&await c(e.trueAction,o)}(e,n);case"waitcss":return async function(e){await new Promise((o=>{let c=e.retries||10;const i=e.waitTime||250,n=()=>{const a=t.find(e);(e.negated&&a.target||!e.negated&&!a.target)&&c>0?(c-=1,setTimeout(n,i)):o()};n()}))}(e);case"foreach":return async function(e,o){const i=t.find(e,!0),n=t.base;for(const n of i)n.target&&(t.setBase(n.target),await c(e.action,o));t.setBase(n)}(e,n);case"hide":return async function(e){const o=t.find(e);o.target&&o.target.classList.add("Autoconsent-Hidden")}(e);case"slide":return async function(e){const o=t.find(e),c=t.find(e.dragTarget);if(o.target){const e=o.target.getBoundingClientRect(),t=c.target.getBoundingClientRect();let i=t.top-e.top,n=t.left-e.left;"y"===this.config.axis.toLowerCase()&&(n=0),"x"===this.config.axis.toLowerCase()&&(i=0);const a=window.screenX+e.left+e.width/2,s=window.screenY+e.top+e.height/2,r=e.left+e.width/2,l=e.top+e.height/2,p=document.createEvent("MouseEvents");p.initMouseEvent("mousedown",!0,!0,window,0,a,s,r,l,!1,!1,!1,!1,0,o.target);const d=document.createEvent("MouseEvents");d.initMouseEvent("mousemove",!0,!0,window,0,a+n,s+i,r+n,l+i,!1,!1,!1,!1,0,o.target);const u=document.createEvent("MouseEvents");u.initMouseEvent("mouseup",!0,!0,window,0,a+n,s+i,r+n,l+i,!1,!1,!1,!1,0,o.target),o.target.dispatchEvent(p),await this.waitTimeout(10),o.target.dispatchEvent(d),await this.waitTimeout(10),o.target.dispatchEvent(u)}}(e);case"close":return async function(){window.close()}();case"wait":return async function(e){await i(e.waitTime)}(e);case"eval":return async function(e){return console.log("eval!",e.code),new Promise((t=>{try{e.async?(window.eval(e.code),setTimeout((()=>{t(window.eval("window.__consentCheckResult"))}),e.timeout||250)):t(window.eval(e.code))}catch(o){console.warn("eval error",o,e.code),t(!1)}}))}(e);default:throw"Unknown action type: "+e.type}}function i(e){return new Promise((t=>{setTimeout((()=>{t()}),e)}))}function n(){return crypto&&void 0!==crypto.randomUUID?crypto.randomUUID():Math.random().toString()}var a={pending:new Map,sendContentMessage:null};function s(e,t){const o=n();a.sendContentMessage({type:"eval",id:o,code:e,snippetId:t});const c=new class{constructor(e,t=1e3){this.id=e,this.promise=new Promise(((e,t)=>{this.resolve=e,this.reject=t})),this.timer=window.setTimeout((()=>{this.reject(new Error("timeout"))}),t)}}(o);return a.pending.set(c.id,c),c.promise}var r={EVAL_0:()=>console.log(1),EVAL_CONSENTMANAGER_1:()=>window.__cmp&&"object"==typeof __cmp("getCMPData"),EVAL_CONSENTMANAGER_2:()=>!__cmp("consentStatus").userChoiceExists,EVAL_CONSENTMANAGER_3:()=>__cmp("setConsent",0),EVAL_CONSENTMANAGER_4:()=>__cmp("setConsent",1),EVAL_CONSENTMANAGER_5:()=>__cmp("consentStatus").userChoiceExists,EVAL_COOKIEBOT_1:()=>!!window.Cookiebot,EVAL_COOKIEBOT_2:()=>!window.Cookiebot.hasResponse&&!0===window.Cookiebot.dialog?.visible,EVAL_COOKIEBOT_3:()=>window.Cookiebot.withdraw()||!0,EVAL_COOKIEBOT_4:()=>window.Cookiebot.hide()||!0,EVAL_COOKIEBOT_5:()=>!0===window.Cookiebot.declined,EVAL_KLARO_1:()=>{const e=globalThis.klaroConfig||globalThis.klaro?.getManager&&globalThis.klaro.getManager().config;if(!e)return!0;const t=(e.services||e.apps).filter((e=>!e.required)).map((e=>e.name));if(klaro&&klaro.getManager){const e=klaro.getManager();return t.every((t=>!e.consents[t]))}if(klaroConfig&&"cookie"===klaroConfig.storageMethod){const e=klaroConfig.cookieName||klaroConfig.storageName,o=JSON.parse(decodeURIComponent(document.cookie.split(";").find((t=>t.trim().startsWith(e))).split("=")[1]));return Object.keys(o).filter((e=>t.includes(e))).every((e=>!1===o[e]))}},EVAL_ONETRUST_1:()=>window.OnetrustActiveGroups.split(",").filter((e=>e.length>0)).length<=1,EVAL_TRUSTARC_TOP:()=>window&&window.truste&&"0"===window.truste.eu.bindMap.prefCookie,EVAL_ADROLL_0:()=>!document.cookie.includes("__adroll_fpc"),EVAL_ALMACMP_0:()=>document.cookie.includes('"name":"Google","consent":false'),EVAL_AFFINITY_SERIF_COM_0:()=>document.cookie.includes("serif_manage_cookies_viewed")&&!document.cookie.includes("serif_allow_analytics"),EVAL_ARBEITSAGENTUR_TEST:()=>document.cookie.includes("cookie_consent=denied"),EVAL_AXEPTIO_0:()=>document.cookie.includes("axeptio_authorized_vendors=%2C%2C"),EVAL_BAHN_TEST:()=>1===utag.gdpr.getSelectedCategories().length,EVAL_BING_0:()=>document.cookie.includes("AL=0")&&document.cookie.includes("AD=0")&&document.cookie.includes("SM=0"),EVAL_BLOCKSY_0:()=>document.cookie.includes("blocksy_cookies_consent_accepted=no"),EVAL_BORLABS_0:()=>!JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>-1!==e.indexOf("borlabs-cookie"))).split("=",2)[1])).consents.statistics,EVAL_BUNDESREGIERUNG_DE_0:()=>document.cookie.match("cookie-allow-tracking=0"),EVAL_CANVA_0:()=>!document.cookie.includes("gtm_fpc_engagement_event"),EVAL_CC_BANNER2_0:()=>!!document.cookie.match(/sncc=[^;]+D%3Dtrue/),EVAL_CLICKIO_0:()=>document.cookie.includes("__lxG__consent__v2_daisybit="),EVAL_CLINCH_0:()=>document.cookie.includes("ctc_rejected=1"),EVAL_COOKIECONSENT2_TEST:()=>document.cookie.includes("cc_cookie="),EVAL_COOKIECONSENT3_TEST:()=>document.cookie.includes("cc_cookie="),EVAL_COINBASE_0:()=>JSON.parse(decodeURIComponent(document.cookie.match(/cm_(eu|default)_preferences=([0-9a-zA-Z\\{\\}\\[\\]%:]*);?/)[2])).consent.length<=1,EVAL_COMPLIANZ_BANNER_0:()=>document.cookie.includes("cmplz_banner-status=dismissed"),EVAL_COOKIE_LAW_INFO_0:()=>CLI.disableAllCookies()||CLI.reject_close()||!0,EVAL_COOKIE_LAW_INFO_1:()=>-1===document.cookie.indexOf("cookielawinfo-checkbox-non-necessary=yes"),EVAL_COOKIE_LAW_INFO_DETECT:()=>!!window.CLI,EVAL_COOKIE_MANAGER_POPUP_0:()=>!1===JSON.parse(document.cookie.split(";").find((e=>e.trim().startsWith("CookieLevel"))).split("=")[1]).social,EVAL_COOKIEALERT_0:()=>document.querySelector("body").removeAttribute("style")||!0,EVAL_COOKIEALERT_1:()=>document.querySelector("body").removeAttribute("style")||!0,EVAL_COOKIEALERT_2:()=>!0===window.CookieConsent.declined,EVAL_COOKIEFIRST_0:()=>{return!1===(e=JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>-1!==e.indexOf("cookiefirst"))).trim()).split("=")[1])).performance&&!1===e.functional&&!1===e.advertising;var e},EVAL_COOKIEFIRST_1:()=>document.querySelectorAll("button[data-cookiefirst-accent-color=true][role=checkbox]:not([disabled])").forEach((e=>"true"==e.getAttribute("aria-checked")&&e.click()))||!0,EVAL_COOKIEINFORMATION_0:()=>CookieInformation.declineAllCategories()||!0,EVAL_COOKIEINFORMATION_1:()=>CookieInformation.submitAllCategories()||!0,EVAL_COOKIEINFORMATION_2:()=>document.cookie.includes("CookieInformationConsent="),EVAL_COOKIEYES_0:()=>document.cookie.includes("advertisement:no"),EVAL_DAILYMOTION_0:()=>!!document.cookie.match("dm-euconsent-v2"),EVAL_DNDBEYOND_TEST:()=>document.cookie.includes("cookie-consent=denied"),EVAL_DSGVO_0:()=>!document.cookie.includes("sp_dsgvo_cookie_settings"),EVAL_DUNELM_0:()=>document.cookie.includes("cc_functional=0")&&document.cookie.includes("cc_targeting=0"),EVAL_ETSY_0:()=>document.querySelectorAll(".gdpr-overlay-body input").forEach((e=>{e.checked=!1}))||!0,EVAL_ETSY_1:()=>document.querySelector(".gdpr-overlay-view button[data-wt-overlay-close]").click()||!0,EVAL_EU_COOKIE_COMPLIANCE_0:()=>-1===document.cookie.indexOf("cookie-agreed=2"),EVAL_EU_COOKIE_LAW_0:()=>!document.cookie.includes("euCookie"),EVAL_EZOIC_0:()=>ezCMP.handleAcceptAllClick(),EVAL_EZOIC_1:()=>!!document.cookie.match(/ez-consent-tcf/),EVAL_GOOGLE_0:()=>!!document.cookie.match(/SOCS=CAE/),EVAL_HEMA_TEST_0:()=>document.cookie.includes("cookies_rejected=1"),EVAL_IUBENDA_0:()=>document.querySelectorAll(".purposes-item input[type=checkbox]:not([disabled])").forEach((e=>{e.checked&&e.click()}))||!0,EVAL_IUBENDA_1:()=>!!document.cookie.match(/_iub_cs-\d+=/),EVAL_IWINK_TEST:()=>document.cookie.includes("cookie_permission_granted=no"),EVAL_JQUERY_COOKIEBAR_0:()=>!document.cookie.includes("cookies-state=accepted"),EVAL_MEDIAVINE_0:()=>document.querySelectorAll('[data-name="mediavine-gdpr-cmp"] input[type=checkbox]').forEach((e=>e.checked&&e.click()))||!0,EVAL_MICROSOFT_0:()=>Array.from(document.querySelectorAll("div > button")).filter((e=>e.innerText.match("Reject|Ablehnen")))[0].click()||!0,EVAL_MICROSOFT_1:()=>Array.from(document.querySelectorAll("div > button")).filter((e=>e.innerText.match("Accept|Annehmen")))[0].click()||!0,EVAL_MICROSOFT_2:()=>!!document.cookie.match("MSCC|GHCC"),EVAL_MOOVE_0:()=>document.querySelectorAll("#moove_gdpr_cookie_modal input").forEach((e=>{e.disabled||(e.checked="moove_gdpr_strict_cookies"===e.name||"moove_gdpr_strict_cookies"===e.id)}))||!0,EVAL_ONENINETWO_0:()=>document.cookie.includes("CC_ADVERTISING=NO")&&document.cookie.includes("CC_ANALYTICS=NO"),EVAL_OPERA_0:()=>document.cookie.includes("cookie_consent_essential=true")&&!document.cookie.includes("cookie_consent_marketing=true"),EVAL_PAYPAL_0:()=>!0===document.cookie.includes("cookie_prefs"),EVAL_PRIMEBOX_0:()=>!document.cookie.includes("cb-enabled=accepted"),EVAL_PUBTECH_0:()=>document.cookie.includes("euconsent-v2")&&(document.cookie.match(/.YAAAAAAAAAAA/)||document.cookie.match(/.aAAAAAAAAAAA/)||document.cookie.match(/.YAAACFgAAAAA/)),EVAL_REDDIT_0:()=>document.cookie.includes("eu_cookie={%22opted%22:true%2C%22nonessential%22:false}"),EVAL_SIBBO_0:()=>!!window.localStorage.getItem("euconsent-v2"),EVAL_SIRDATA_UNBLOCK_SCROLL:()=>(document.documentElement.classList.forEach((e=>{e.startsWith("sd-cmp-")&&document.documentElement.classList.remove(e)})),!0),EVAL_SNIGEL_0:()=>!!document.cookie.match("snconsent"),EVAL_STEAMPOWERED_0:()=>2===JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>e.trim().startsWith("cookieSettings"))).split("=")[1])).preference_state,EVAL_SVT_TEST:()=>document.cookie.includes('cookie-consent-1={"optedIn":true,"functionality":false,"statistics":false}'),EVAL_TAKEALOT_0:()=>document.body.classList.remove("freeze")||(document.body.style="")||!0,EVAL_TARTEAUCITRON_0:()=>tarteaucitron.userInterface.respondAll(!1)||!0,EVAL_TARTEAUCITRON_1:()=>tarteaucitron.userInterface.respondAll(!0)||!0,EVAL_TARTEAUCITRON_2:()=>document.cookie.match(/tarteaucitron=[^;]*/)[0].includes("false"),EVAL_TAUNTON_TEST:()=>document.cookie.includes("taunton_user_consent_submitted=true"),EVAL_TEALIUM_0:()=>void 0!==window.utag&&"object"==typeof utag.gdpr,EVAL_TEALIUM_1:()=>utag.gdpr.setConsentValue(!1)||!0,EVAL_TEALIUM_DONOTSELL:()=>utag.gdpr.dns?.setDnsState(!1)||!0,EVAL_TEALIUM_2:()=>utag.gdpr.setConsentValue(!0)||!0,EVAL_TEALIUM_3:()=>1!==utag.gdpr.getConsentState(),EVAL_TEALIUM_DONOTSELL_CHECK:()=>1!==utag.gdpr.dns?.getDnsState(),EVAL_TESTCMP_0:()=>"button_clicked"===window.results.results[0],EVAL_TESTCMP_COSMETIC_0:()=>"banner_hidden"===window.results.results[0],EVAL_THEFREEDICTIONARY_0:()=>cmpUi.showPurposes()||cmpUi.rejectAll()||!0,EVAL_THEFREEDICTIONARY_1:()=>cmpUi.allowAll()||!0,EVAL_THEVERGE_0:()=>document.cookie.includes("_duet_gdpr_acknowledged=1"),EVAL_UBUNTU_COM_0:()=>document.cookie.includes("_cookies_accepted=essential"),EVAL_UK_COOKIE_CONSENT_0:()=>!document.cookie.includes("catAccCookies"),EVAL_USERCENTRICS_API_0:()=>"object"==typeof UC_UI,EVAL_USERCENTRICS_API_1:()=>!!UC_UI.closeCMP(),EVAL_USERCENTRICS_API_2:()=>!!UC_UI.denyAllConsents(),EVAL_USERCENTRICS_API_3:()=>!!UC_UI.acceptAllConsents(),EVAL_USERCENTRICS_API_4:()=>!!UC_UI.closeCMP(),EVAL_USERCENTRICS_API_5:()=>!0===UC_UI.areAllConsentsAccepted(),EVAL_USERCENTRICS_API_6:()=>!1===UC_UI.areAllConsentsAccepted(),EVAL_USERCENTRICS_BUTTON_0:()=>JSON.parse(localStorage.getItem("usercentrics")).consents.every((e=>e.isEssential||!e.consentStatus)),EVAL_WAITROSE_0:()=>Array.from(document.querySelectorAll("label[id$=cookies-deny-label]")).forEach((e=>e.click()))||!0,EVAL_WAITROSE_1:()=>document.cookie.includes("wtr_cookies_advertising=0")&&document.cookie.includes("wtr_cookies_analytics=0"),EVAL_WP_COOKIE_NOTICE_0:()=>document.cookie.includes("wpl_viewed_cookie=no"),EVAL_XE_TEST:()=>document.cookie.includes("xeConsentState={%22performance%22:false%2C%22marketing%22:false%2C%22compliance%22:false}"),EVAL_XING_0:()=>document.cookie.includes("userConsent=%7B%22marketing%22%3Afalse"),EVAL_YOUTUBE_DESKTOP_0:()=>!!document.cookie.match(/SOCS=CAE/),EVAL_YOUTUBE_MOBILE_0:()=>!!document.cookie.match(/SOCS=CAE/)};var l={main:!0,frame:!1,urlPattern:""},p=class{constructor(e){this.runContext=l,this.autoconsent=e}get hasSelfTest(){throw new Error("Not Implemented")}get isIntermediate(){throw new Error("Not Implemented")}get isCosmetic(){throw new Error("Not Implemented")}mainWorldEval(e){const t=r[e];if(!t)return console.warn("Snippet not found",e),Promise.resolve(!1);const o=this.autoconsent.config.logs;if(this.autoconsent.config.isMainWorld){o.evals&&console.log("inline eval:",e,t);let c=!1;try{c=!!t.call(globalThis)}catch(t){o.evals&&console.error("error evaluating rule",e,t)}return Promise.resolve(c)}const c=`(${t.toString()})()`;return o.evals&&console.log("async eval:",e,c),s(c,e).catch((t=>(o.evals&&console.error("error evaluating rule",e,t),!1)))}checkRunContext(){const e={...l,...this.runContext},t=window.top===window;return!(t&&!e.main)&&(!(!t&&!e.frame)&&!(e.urlPattern&&!window.location.href.match(e.urlPattern)))}detectCmp(){throw new Error("Not Implemented")}async detectPopup(){return!1}optOut(){throw new Error("Not Implemented")}optIn(){throw new Error("Not Implemented")}openCmp(){throw new Error("Not Implemented")}async test(){return Promise.resolve(!0)}click(e,t=!1){return this.autoconsent.domActions.click(e,t)}elementExists(e){return this.autoconsent.domActions.elementExists(e)}elementVisible(e,t){return this.autoconsent.domActions.elementVisible(e,t)}waitForElement(e,t){return this.autoconsent.domActions.waitForElement(e,t)}waitForVisible(e,t,o){return this.autoconsent.domActions.waitForVisible(e,t,o)}waitForThenClick(e,t,o){return this.autoconsent.domActions.waitForThenClick(e,t,o)}wait(e){return this.autoconsent.domActions.wait(e)}hide(e,t){return this.autoconsent.domActions.hide(e,t)}prehide(e){return this.autoconsent.domActions.prehide(e)}undoPrehide(){return this.autoconsent.domActions.undoPrehide()}querySingleReplySelector(e,t){return this.autoconsent.domActions.querySingleReplySelector(e,t)}querySelectorChain(e){return this.autoconsent.domActions.querySelectorChain(e)}elementSelector(e){return this.autoconsent.domActions.elementSelector(e)}},d=class extends p{constructor(e,t){super(t),this.rule=e,this.name=e.name,this.runContext=e.runContext||l}get hasSelfTest(){return!!this.rule.test}get isIntermediate(){return!!this.rule.intermediate}get isCosmetic(){return!!this.rule.cosmetic}get prehideSelectors(){return this.rule.prehideSelectors}async detectCmp(){return!!this.rule.detectCmp&&this._runRulesParallel(this.rule.detectCmp)}async detectPopup(){return!!this.rule.detectPopup&&this._runRulesSequentially(this.rule.detectPopup)}async optOut(){const e=this.autoconsent.config.logs;return!!this.rule.optOut&&(e.lifecycle&&console.log("Initiated optOut()",this.rule.optOut),this._runRulesSequentially(this.rule.optOut))}async optIn(){const e=this.autoconsent.config.logs;return!!this.rule.optIn&&(e.lifecycle&&console.log("Initiated optIn()",this.rule.optIn),this._runRulesSequentially(this.rule.optIn))}async openCmp(){return!!this.rule.openCmp&&this._runRulesSequentially(this.rule.openCmp)}async test(){return this.hasSelfTest?this._runRulesSequentially(this.rule.test):super.test()}async evaluateRuleStep(e){const t=[],o=this.autoconsent.config.logs;if(e.exists&&t.push(this.elementExists(e.exists)),e.visible&&t.push(this.elementVisible(e.visible,e.check)),e.eval){const o=this.mainWorldEval(e.eval);t.push(o)}if(e.waitFor&&t.push(this.waitForElement(e.waitFor,e.timeout)),e.waitForVisible&&t.push(this.waitForVisible(e.waitForVisible,e.timeout,e.check)),e.click&&t.push(this.click(e.click,e.all)),e.waitForThenClick&&t.push(this.waitForThenClick(e.waitForThenClick,e.timeout,e.all)),e.wait&&t.push(this.wait(e.wait)),e.hide&&t.push(this.hide(e.hide,e.method)),e.if){if(!e.if.exists&&!e.if.visible)return console.error("invalid conditional rule",e.if),!1;const c=await this.evaluateRuleStep(e.if);o.rulesteps&&console.log("Condition is",c),c?t.push(this._runRulesSequentially(e.then)):e.else?t.push(this._runRulesSequentially(e.else)):t.push(!0)}if(e.any){for(const t of e.any)if(await this.evaluateRuleStep(t))return!0;return!1}if(0===t.length)return o.errors&&console.warn("Unrecognized rule",e),!1;return(await Promise.all(t)).reduce(((e,t)=>e&&t),!0)}async _runRulesParallel(e){const t=e.map((e=>this.evaluateRuleStep(e)));return(await Promise.all(t)).every((e=>!!e))}async _runRulesSequentially(e){const t=this.autoconsent.config.logs;for(const o of e){t.rulesteps&&console.log("Running rule...",o);const e=await this.evaluateRuleStep(o);if(t.rulesteps&&console.log("...rule result",e),!e&&!o.optional)return!1}return!0}};function u(e="autoconsent-css-rules"){const t=`style#${e}`,o=document.querySelector(t);if(o&&o instanceof HTMLStyleElement)return o;{const t=document.head||document.getElementsByTagName("head")[0]||document.documentElement,o=document.createElement("style");return o.id=e,t.appendChild(o),o}}function m(e,t,o="display"){const c=`${t} { ${"opacity"===o?"opacity: 0":"display: none"} !important; z-index: -1 !important; pointer-events: none !important; } `;return e instanceof HTMLStyleElement&&(e.innerText+=c,t.length>0)}async function h(e,t,o){const c=await e();return!c&&t>0?new Promise((c=>{setTimeout((async()=>{c(h(e,t-1,o))}),o)})):Promise.resolve(c)}function k(e){if(!e)return!1;if(null!==e.offsetParent)return!0;{const t=window.getComputedStyle(e);if("fixed"===t.position&&"none"!==t.display)return!0}return!1}function b(e){const t={enabled:!0,autoAction:"optOut",disabledCmps:[],enablePrehide:!0,enableCosmeticRules:!0,detectRetries:20,isMainWorld:!1,prehideTimeout:2e3,logs:{lifecycle:!1,rulesteps:!1,evals:!1,errors:!0,messages:!1}},o=(c=t,globalThis.structuredClone?structuredClone(c):JSON.parse(JSON.stringify(c)));var c;for(const c of Object.keys(t))void 0!==e[c]&&(o[c]=e[c]);return o}var _="#truste-show-consent",g="#truste-consent-track",y=[class extends p{constructor(e){super(e),this.name="TrustArc-top",this.prehideSelectors=[".trustarc-banner-container",`.truste_popframe,.truste_overlay,.truste_box_overlay,${g}`],this.runContext={main:!0,frame:!1},this._shortcutButton=null,this._optInDone=!1}get hasSelfTest(){return!1}get isIntermediate(){return!this._optInDone&&!this._shortcutButton}get isCosmetic(){return!1}async detectCmp(){const e=this.elementExists(`${_},${g}`);return e&&(this._shortcutButton=document.querySelector("#truste-consent-required")),e}async detectPopup(){return this.elementVisible(`#truste-consent-content,#trustarc-banner-overlay,${g}`,"all")}openFrame(){this.click(_)}async optOut(){return this._shortcutButton?(this._shortcutButton.click(),!0):(m(u(),`.truste_popframe, .truste_overlay, .truste_box_overlay, ${g}`),this.click(_),setTimeout((()=>{u().remove()}),1e4),!0)}async optIn(){return this._optInDone=!0,this.click("#truste-consent-button")}async openCmp(){return!0}async test(){return await this.mainWorldEval("EVAL_TRUSTARC_TOP")}},class extends p{constructor(){super(...arguments),this.name="TrustArc-frame",this.runContext={main:!1,frame:!0,urlPattern:"^https://consent-pref\\.trustarc\\.com/\\?"}}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return!0}async detectPopup(){return this.elementVisible("#defaultpreferencemanager","any")&&this.elementVisible(".mainContent","any")}async navigateToSettings(){return await h((async()=>this.elementExists(".shp")||this.elementVisible(".advance","any")||this.elementExists(".switch span:first-child")),10,500),this.elementExists(".shp")&&this.click(".shp"),await this.waitForElement(".prefPanel",5e3),this.elementVisible(".advance","any")&&this.click(".advance"),await h((()=>this.elementVisible(".switch span:first-child","any")),5,1e3)}async optOut(){return await h((()=>"complete"===document.readyState),20,100),await this.waitForElement(".mainContent[aria-hidden=false]",5e3),!!this.click(".rejectAll")||(this.elementExists(".prefPanel")&&await this.waitForElement('.prefPanel[style="visibility: visible;"]',3e3),this.click("#catDetails0")?(this.click(".submit"),this.waitForThenClick("#gwt-debug-close_id",5e3),!0):this.click(".required")?(this.waitForThenClick("#gwt-debug-close_id",5e3),!0):(await this.navigateToSettings(),this.click(".switch span:nth-child(1):not(.active)",!0),this.click(".submit"),this.waitForThenClick("#gwt-debug-close_id",3e5),!0))}async optIn(){return this.click(".call")||(await this.navigateToSettings(),this.click(".switch span:nth-child(2)",!0),this.click(".submit"),this.waitForElement("#gwt-debug-close_id",3e5).then((()=>{this.click("#gwt-debug-close_id")}))),!0}},class extends p{constructor(){super(...arguments),this.name="Cybotcookiebot",this.prehideSelectors=["#CybotCookiebotDialog,#CybotCookiebotDialogBodyUnderlay,#dtcookie-container,#cookiebanner,#cb-cookieoverlay,.modal--cookie-banner,#cookiebanner_outer,#CookieBanner"]}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return await this.mainWorldEval("EVAL_COOKIEBOT_1")}async detectPopup(){return this.mainWorldEval("EVAL_COOKIEBOT_2")}async optOut(){await this.wait(500);let e=await this.mainWorldEval("EVAL_COOKIEBOT_3");return await this.wait(500),e=e&&await this.mainWorldEval("EVAL_COOKIEBOT_4"),e}async optIn(){return this.elementExists("#dtcookie-container")?this.click(".h-dtcookie-accept"):(this.click(".CybotCookiebotDialogBodyLevelButton:not(:checked):enabled",!0),this.click("#CybotCookiebotDialogBodyLevelButtonAccept"),this.click("#CybotCookiebotDialogBodyButtonAccept"),!0)}async test(){return await this.wait(500),await this.mainWorldEval("EVAL_COOKIEBOT_5")}},class extends p{constructor(){super(...arguments),this.name="Sourcepoint-frame",this.prehideSelectors=["div[id^='sp_message_container_'],.message-overlay","#sp_privacy_manager_container"],this.ccpaNotice=!1,this.ccpaPopup=!1,this.runContext={main:!0,frame:!0}}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){const e=new URL(location.href);return e.searchParams.has("message_id")&&"ccpa-notice.sp-prod.net"===e.hostname?(this.ccpaNotice=!0,!0):"ccpa-pm.sp-prod.net"===e.hostname?(this.ccpaPopup=!0,!0):("/index.html"===e.pathname||"/privacy-manager/index.html"===e.pathname||"/ccpa_pm/index.html"===e.pathname)&&(e.searchParams.has("message_id")||e.searchParams.has("requestUUID")||e.searchParams.has("consentUUID"))}async detectPopup(){return!!this.ccpaNotice||(this.ccpaPopup?await this.waitForElement(".priv-save-btn",2e3):(await this.waitForElement(".sp_choice_type_11,.sp_choice_type_12,.sp_choice_type_13,.sp_choice_type_ACCEPT_ALL,.sp_choice_type_SAVE_AND_EXIT",2e3),!this.elementExists(".sp_choice_type_9")))}async optIn(){return await this.waitForElement(".sp_choice_type_11,.sp_choice_type_ACCEPT_ALL",2e3),!!this.click(".sp_choice_type_11")||!!this.click(".sp_choice_type_ACCEPT_ALL")}isManagerOpen(){return"/privacy-manager/index.html"===location.pathname||"/ccpa_pm/index.html"===location.pathname}async optOut(){const e=this.autoconsent.config.logs;if(this.ccpaPopup){const e=document.querySelectorAll(".priv-purpose-container .sp-switch-arrow-block a.neutral.on .right");for(const t of e)t.click();const t=document.querySelectorAll(".priv-purpose-container .sp-switch-arrow-block a.switch-bg.on");for(const e of t)e.click();return this.click(".priv-save-btn")}if(!this.isManagerOpen()){if(!await this.waitForElement(".sp_choice_type_12,.sp_choice_type_13"))return!1;if(!this.elementExists(".sp_choice_type_12"))return this.click(".sp_choice_type_13");this.click(".sp_choice_type_12"),await h((()=>this.isManagerOpen()),200,100)}await this.waitForElement(".type-modal",2e4),this.waitForThenClick(".ccpa-stack .pm-switch[aria-checked=true] .slider",500,!0);try{const e=".sp_choice_type_REJECT_ALL",t=".reject-toggle",o=await Promise.race([this.waitForElement(e,2e3).then((e=>e?0:-1)),this.waitForElement(t,2e3).then((e=>e?1:-1)),this.waitForElement(".pm-features",2e3).then((e=>e?2:-1))]);if(0===o)return await this.wait(1500),this.click(e);1===o?this.click(t):2===o&&(await this.waitForElement(".pm-features",1e4),this.click(".checked > span",!0),this.click(".chevron"))}catch(t){e.errors&&console.warn(t)}return this.click(".sp_choice_type_SAVE_AND_EXIT")}},class extends p{constructor(){super(...arguments),this.name="consentmanager.net",this.prehideSelectors=["#cmpbox,#cmpbox2"],this.apiAvailable=!1}get hasSelfTest(){return this.apiAvailable}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.apiAvailable=await this.mainWorldEval("EVAL_CONSENTMANAGER_1"),!!this.apiAvailable||this.elementExists("#cmpbox")}async detectPopup(){return this.apiAvailable?(await this.wait(500),await this.mainWorldEval("EVAL_CONSENTMANAGER_2")):this.elementVisible("#cmpbox .cmpmore","any")}async optOut(){return await this.wait(500),this.apiAvailable?await this.mainWorldEval("EVAL_CONSENTMANAGER_3"):!!this.click(".cmpboxbtnno")||(this.elementExists(".cmpwelcomeprpsbtn")?(this.click(".cmpwelcomeprpsbtn > a[aria-checked=true]",!0),this.click(".cmpboxbtnsave"),!0):(this.click(".cmpboxbtncustom"),await this.waitForElement(".cmptblbox",2e3),this.click(".cmptdchoice > a[aria-checked=true]",!0),this.click(".cmpboxbtnyescustomchoices"),this.hide("#cmpwrapper,#cmpbox","display"),!0))}async optIn(){return this.apiAvailable?await this.mainWorldEval("EVAL_CONSENTMANAGER_4"):this.click(".cmpboxbtnyes")}async test(){if(this.apiAvailable)return await this.mainWorldEval("EVAL_CONSENTMANAGER_5")}},class extends p{constructor(){super(...arguments),this.name="Evidon"}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("#_evidon_banner")}async detectPopup(){return this.elementVisible("#_evidon_banner","any")}async optOut(){return this.click("#_evidon-decline-button")||(m(u(),"#evidon-prefdiag-overlay,#evidon-prefdiag-background"),this.click("#_evidon-option-button"),await this.waitForElement("#evidon-prefdiag-overlay",5e3),this.click("#evidon-prefdiag-decline")),!0}async optIn(){return this.click("#_evidon-accept-button")}},class extends p{constructor(){super(...arguments),this.name="Onetrust",this.prehideSelectors=["#onetrust-banner-sdk,#onetrust-consent-sdk,.onetrust-pc-dark-filter,.js-consent-banner"],this.runContext={urlPattern:"^(?!.*https://www\\.nba\\.com/)"}}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("#onetrust-banner-sdk,#onetrust-pc-sdk")}async detectPopup(){return this.elementVisible("#onetrust-banner-sdk,#onetrust-pc-sdk","any")}async optOut(){return this.elementVisible("#onetrust-reject-all-handler,.ot-pc-refuse-all-handler,.js-reject-cookies","any")?this.click("#onetrust-reject-all-handler,.ot-pc-refuse-all-handler,.js-reject-cookies"):(this.elementExists("#onetrust-pc-btn-handler")?this.click("#onetrust-pc-btn-handler"):this.click(".ot-sdk-show-settings,button.js-cookie-settings"),await this.waitForElement("#onetrust-consent-sdk",2e3),await this.wait(1e3),this.click("#onetrust-consent-sdk input.category-switch-handler:checked,.js-editor-toggle-state:checked",!0),await this.wait(1e3),await this.waitForElement(".save-preference-btn-handler,.js-consent-save",2e3),this.click(".save-preference-btn-handler,.js-consent-save"),await this.waitForVisible("#onetrust-banner-sdk",5e3,"none"),!0)}async optIn(){return this.click("#onetrust-accept-btn-handler,#accept-recommended-btn-handler,.js-accept-cookies")}async test(){return await h((()=>this.mainWorldEval("EVAL_ONETRUST_1")),10,500)}},class extends p{constructor(){super(...arguments),this.name="Klaro",this.prehideSelectors=[".klaro"],this.settingsOpen=!1}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".klaro > .cookie-modal")?(this.settingsOpen=!0,!0):this.elementExists(".klaro > .cookie-notice")}async detectPopup(){return this.elementVisible(".klaro > .cookie-notice,.klaro > .cookie-modal","any")}async optOut(){return!!this.click(".klaro .cn-decline")||(this.settingsOpen||(this.click(".klaro .cn-learn-more,.klaro .cm-button-manage"),await this.waitForElement(".klaro > .cookie-modal",2e3),this.settingsOpen=!0),!!this.click(".klaro .cn-decline")||(this.click(".cm-purpose:not(.cm-toggle-all) > input:not(.half-checked,.required,.only-required),.cm-purpose:not(.cm-toggle-all) > div > input:not(.half-checked,.required,.only-required)",!0),this.click(".cm-btn-accept,.cm-button")))}async optIn(){return!!this.click(".klaro .cm-btn-accept-all")||(this.settingsOpen?(this.click(".cm-purpose:not(.cm-toggle-all) > input.half-checked",!0),this.click(".cm-btn-accept")):this.click(".klaro .cookie-notice .cm-btn-success"))}async test(){return await this.mainWorldEval("EVAL_KLARO_1")}},class extends p{constructor(){super(...arguments),this.name="Uniconsent"}get prehideSelectors(){return[".unic",".modal:has(.unic)"]}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".unic .unic-box,.unic .unic-bar,.unic .unic-modal")}async detectPopup(){return this.elementVisible(".unic .unic-box,.unic .unic-bar,.unic .unic-modal","any")}async optOut(){if(await this.waitForElement(".unic button",1e3),document.querySelectorAll(".unic button").forEach((e=>{const t=e.textContent;(t.includes("Manage Options")||t.includes("Optionen verwalten"))&&e.click()})),await this.waitForElement(".unic input[type=checkbox]",1e3)){await this.waitForElement(".unic button",1e3),document.querySelectorAll(".unic input[type=checkbox]").forEach((e=>{e.checked&&e.click()}));for(const e of document.querySelectorAll(".unic button")){const t=e.textContent;for(const o of["Confirm Choices","Save Choices","Auswahl speichern"])if(t.includes(o))return e.click(),await this.wait(500),!0}}return!1}async optIn(){return this.waitForThenClick(".unic #unic-agree")}async test(){await this.wait(1e3);return!this.elementExists(".unic .unic-box,.unic .unic-bar")}},class extends p{constructor(){super(...arguments),this.prehideSelectors=[".cmp-root"],this.name="Conversant"}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".cmp-root .cmp-receptacle")}async detectPopup(){return this.elementVisible(".cmp-root .cmp-receptacle","any")}async optOut(){if(!await this.waitForThenClick(".cmp-main-button:not(.cmp-main-button--primary)"))return!1;if(!await this.waitForElement(".cmp-view-tab-tabs"))return!1;await this.waitForThenClick(".cmp-view-tab-tabs > :first-child"),await this.waitForThenClick(".cmp-view-tab-tabs > .cmp-view-tab--active:first-child");for(const e of Array.from(document.querySelectorAll(".cmp-accordion-item"))){e.querySelector(".cmp-accordion-item-title").click(),await h((()=>!!e.querySelector(".cmp-accordion-item-content.cmp-active")),10,50);const t=e.querySelector(".cmp-accordion-item-content.cmp-active");t.querySelectorAll(".cmp-toggle-actions .cmp-toggle-deny:not(.cmp-toggle-deny--active)").forEach((e=>e.click())),t.querySelectorAll(".cmp-toggle-actions .cmp-toggle-checkbox:not(.cmp-toggle-checkbox--active)").forEach((e=>e.click()))}return await this.click(".cmp-main-button:not(.cmp-main-button--primary)"),!0}async optIn(){return this.waitForThenClick(".cmp-main-button.cmp-main-button--primary")}async test(){return document.cookie.includes("cmp-data=0")}},class extends p{constructor(){super(...arguments),this.name="tiktok.com",this.runContext={urlPattern:"tiktok"}}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}getShadowRoot(){const e=document.querySelector("tiktok-cookie-banner");return e?e.shadowRoot:null}async detectCmp(){return this.elementExists("tiktok-cookie-banner")}async detectPopup(){return k(this.getShadowRoot().querySelector(".tiktok-cookie-banner"))}async optOut(){const e=this.autoconsent.config.logs,t=this.getShadowRoot().querySelector(".button-wrapper button:first-child");return t?(e.rulesteps&&console.log("[clicking]",t),t.click(),!0):(e.errors&&console.log("no decline button found"),!1)}async optIn(){const e=this.autoconsent.config.logs,t=this.getShadowRoot().querySelector(".button-wrapper button:last-child");return t?(e.rulesteps&&console.log("[clicking]",t),t.click(),!0):(e.errors&&console.log("no accept button found"),!1)}async test(){const e=document.cookie.match(/cookie-consent=([^;]+)/);if(!e)return!1;const t=JSON.parse(decodeURIComponent(e[1]));return Object.values(t).every((e=>"boolean"!=typeof e||!1===e))}},class extends p{constructor(){super(...arguments),this.runContext={urlPattern:"^https://(www\\.)?airbnb\\.[^/]+/"},this.prehideSelectors=["div[data-testid=main-cookies-banner-container]",'div:has(> div:first-child):has(> div:last-child):has(> section [data-testid="strictly-necessary-cookies"])']}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("div[data-testid=main-cookies-banner-container]")}async detectPopup(){return this.elementVisible("div[data-testid=main-cookies-banner-container","any")}async optOut(){let e;for(await this.waitForThenClick("div[data-testid=main-cookies-banner-container] button._snbhip0");e=document.querySelector("[data-testid=modal-container] button[aria-checked=true]:not([disabled])");)e.click();return this.waitForThenClick("button[data-testid=save-btn]")}async optIn(){return this.waitForThenClick("div[data-testid=main-cookies-banner-container] button._148dgdpk")}async test(){return await h((()=>!!document.cookie.match("OptanonAlertBoxClosed")),20,200)}},class extends p{constructor(){super(...arguments),this.name="tumblr-com",this.runContext={urlPattern:"^https://(www\\.)?tumblr\\.com/"}}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}get prehideSelectors(){return["#cmp-app-container"]}async detectCmp(){return this.elementExists("#cmp-app-container")}async detectPopup(){return this.elementVisible("#cmp-app-container","any")}async optOut(){let e=document.querySelector("#cmp-app-container iframe"),t=e.contentDocument?.querySelector(".cmp-components-button.is-secondary");return!!t&&(t.click(),await h((()=>!!document.querySelector("#cmp-app-container iframe").contentDocument?.querySelector(".cmp__dialog input")),5,500),e=document.querySelector("#cmp-app-container iframe"),t=e.contentDocument?.querySelector(".cmp-components-button.is-secondary"),!!t&&(t.click(),!0))}async optIn(){const e=document.querySelector("#cmp-app-container iframe").contentDocument.querySelector(".cmp-components-button.is-primary");return!!e&&(e.click(),!0)}}];var w=[{name:"192.com",detectCmp:[{exists:".ont-cookies"}],detectPopup:[{visible:".ont-cookies"}],optIn:[{click:".ont-btn-main.ont-cookies-btn.js-ont-btn-ok2"}],optOut:[{click:".ont-cookes-btn-manage"},{click:".ont-btn-main.ont-cookies-btn.js-ont-btn-choose"}],test:[{eval:"EVAL_ONENINETWO_0"}]},{name:"1password-com",cosmetic:!0,prehideSelectors:['footer #footer-root [aria-label="Cookie Consent"]'],detectCmp:[{exists:'footer #footer-root [aria-label="Cookie Consent"]'}],detectPopup:[{visible:'footer #footer-root [aria-label="Cookie Consent"]'}],optIn:[{click:'footer #footer-root [aria-label="Cookie Consent"] button'}],optOut:[{hide:'footer #footer-root [aria-label="Cookie Consent"]'}]},{name:"abconcerts.be",vendorUrl:"https://unknown",intermediate:!1,prehideSelectors:["dialog.cookie-consent"],detectCmp:[{exists:"dialog.cookie-consent form.cookie-consent__form"}],detectPopup:[{visible:"dialog.cookie-consent form.cookie-consent__form"}],optIn:[{waitForThenClick:"dialog.cookie-consent form.cookie-consent__form button[value=yes]"}],optOut:[{if:{exists:"dialog.cookie-consent form.cookie-consent__form button[value=no]"},then:[{click:"dialog.cookie-consent form.cookie-consent__form button[value=no]"}],else:[{click:"dialog.cookie-consent form.cookie-consent__form button.cookie-consent__options-toggle"},{waitForThenClick:'dialog.cookie-consent form.cookie-consent__form button[value="save_options"]'}]}]},{name:"activobank.pt",runContext:{urlPattern:"^https://(www\\.)?activobank\\.pt"},prehideSelectors:["aside#cookies,.overlay-cookies"],detectCmp:[{exists:"#cookies .cookies-btn"}],detectPopup:[{visible:"#cookies #submitCookies"}],optIn:[{waitForThenClick:"#cookies #submitCookies"}],optOut:[{waitForThenClick:"#cookies #rejectCookies"}]},{name:"Adroll",prehideSelectors:["#adroll_consent_container"],detectCmp:[{exists:"#adroll_consent_container"}],detectPopup:[{visible:"#adroll_consent_container"}],optIn:[{waitForThenClick:"#adroll_consent_accept"}],optOut:[{waitForThenClick:"#adroll_consent_reject"}],test:[{eval:"EVAL_ADROLL_0"}]},{name:"affinity.serif.com",detectCmp:[{exists:".c-cookie-banner button[data-qa='allow-all-cookies']"}],detectPopup:[{visible:".c-cookie-banner"}],optIn:[{click:'button[data-qa="allow-all-cookies"]'}],optOut:[{click:'button[data-qa="manage-cookies"]'},{waitFor:'.c-cookie-banner ~ [role="dialog"]'},{waitForThenClick:'.c-cookie-banner ~ [role="dialog"] input[type="checkbox"][value="true"]',all:!0},{click:'.c-cookie-banner ~ [role="dialog"] .c-modal__action button'}],test:[{wait:500},{eval:"EVAL_AFFINITY_SERIF_COM_0"}]},{name:"agolde.com",cosmetic:!0,prehideSelectors:["#modal-1 div[data-micromodal-close]"],detectCmp:[{exists:"#modal-1 div[aria-labelledby=modal-1-title]"}],detectPopup:[{exists:"#modal-1 div[data-micromodal-close]"}],optIn:[{click:'button[aria-label="Close modal"]'}],optOut:[{hide:"#modal-1 div[data-micromodal-close]"}]},{name:"aliexpress",vendorUrl:"https://aliexpress.com/",runContext:{urlPattern:"^https://.*\\.aliexpress\\.com/"},prehideSelectors:["#gdpr-new-container"],detectCmp:[{exists:"#gdpr-new-container"}],detectPopup:[{visible:"#gdpr-new-container"}],optIn:[{waitForThenClick:"#gdpr-new-container .btn-accept"}],optOut:[{waitForThenClick:"#gdpr-new-container .btn-more"},{waitFor:"#gdpr-new-container .gdpr-dialog-switcher"},{click:"#gdpr-new-container .switcher-on",all:!0,optional:!0},{click:"#gdpr-new-container .btn-save"}]},{name:"almacmp",prehideSelectors:["#alma-cmpv2-container"],detectCmp:[{exists:"#alma-cmpv2-container"}],detectPopup:[{visible:"#alma-cmpv2-container #almacmp-modal-layer1"}],optIn:[{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer1 #almacmp-modalConfirmBtn"}],optOut:[{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer1 #almacmp-modalSettingBtn"},{waitFor:"#alma-cmpv2-container #almacmp-modal-layer2"},{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer2 #almacmp-reject-all-layer2"}],test:[{eval:"EVAL_ALMACMP_0"}]},{name:"altium.com",cosmetic:!0,prehideSelectors:[".altium-privacy-bar"],detectCmp:[{exists:".altium-privacy-bar"}],detectPopup:[{exists:".altium-privacy-bar"}],optIn:[{click:"a.altium-privacy-bar__btn"}],optOut:[{hide:".altium-privacy-bar"}]},{name:"amazon.com",prehideSelectors:['span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'],detectCmp:[{exists:'span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'}],detectPopup:[{visible:'span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'}],optIn:[{waitForVisible:"#sp-cc-accept"},{wait:500},{click:"#sp-cc-accept"}],optOut:[{waitForVisible:"#sp-cc-rejectall-link"},{wait:500},{click:"#sp-cc-rejectall-link"}]},{name:"aquasana.com",cosmetic:!0,prehideSelectors:["#consent-tracking"],detectCmp:[{exists:"#consent-tracking"}],detectPopup:[{exists:"#consent-tracking"}],optIn:[{click:"#accept_consent"}],optOut:[{hide:"#consent-tracking"}]},{name:"arbeitsagentur",vendorUrl:"https://www.arbeitsagentur.de/",prehideSelectors:[".modal-open bahf-cookie-disclaimer-dpl3"],detectCmp:[{exists:"bahf-cookie-disclaimer-dpl3"}],detectPopup:[{visible:"bahf-cookie-disclaimer-dpl3"}],optIn:[{waitForThenClick:["bahf-cookie-disclaimer-dpl3","bahf-cd-modal-dpl3 .ba-btn-primary"]}],optOut:[{waitForThenClick:["bahf-cookie-disclaimer-dpl3","bahf-cd-modal-dpl3 .ba-btn-contrast"]}],test:[{eval:"EVAL_ARBEITSAGENTUR_TEST"}]},{name:"asus",vendorUrl:"https://www.asus.com/",runContext:{urlPattern:"^https://www\\.asus\\.com/"},prehideSelectors:["#cookie-policy-info,#cookie-policy-info-bg"],detectCmp:[{exists:"#cookie-policy-info"}],detectPopup:[{visible:"#cookie-policy-info"}],optIn:[{waitForThenClick:'#cookie-policy-info [data-agree="Accept Cookies"]'}],optOut:[{if:{exists:"#cookie-policy-info .btn-reject"},then:[{waitForThenClick:"#cookie-policy-info .btn-reject"}],else:[{waitForThenClick:"#cookie-policy-info .btn-setting"},{waitForThenClick:'#cookie-policy-lightbox-wrapper [data-agree="Save Settings"]'}]}]},{name:"athlinks-com",runContext:{urlPattern:"^https://(www\\.)?athlinks\\.com/"},cosmetic:!0,prehideSelectors:["#footer-container ~ div"],detectCmp:[{exists:"#footer-container ~ div"}],detectPopup:[{visible:"#footer-container > div"}],optIn:[{click:"#footer-container ~ div button"}],optOut:[{hide:"#footer-container ~ div"}]},{name:"ausopen.com",cosmetic:!0,detectCmp:[{exists:".gdpr-popup__message"}],detectPopup:[{visible:".gdpr-popup__message"}],optOut:[{hide:".gdpr-popup__message"}],optIn:[{click:".gdpr-popup__message button"}]},{name:"automattic-cmp-optout",prehideSelectors:['form[class*="cookie-banner"][method="post"]'],detectCmp:[{exists:'form[class*="cookie-banner"][method="post"]'}],detectPopup:[{visible:'form[class*="cookie-banner"][method="post"]'}],optIn:[{click:'a[class*="accept-all-button"]'}],optOut:[{click:'form[class*="cookie-banner"] div[class*="simple-options"] a[class*="customize-button"]'},{waitForThenClick:"input[type=checkbox][checked]:not([disabled])",all:!0},{click:'a[class*="accept-selection-button"]'}]},{name:"aws.amazon.com",prehideSelectors:["#awsccc-cb-content","#awsccc-cs-container","#awsccc-cs-modalOverlay","#awsccc-cs-container-inner"],detectCmp:[{exists:"#awsccc-cb-content"}],detectPopup:[{visible:"#awsccc-cb-content"}],optIn:[{click:"button[data-id=awsccc-cb-btn-accept"}],optOut:[{click:"button[data-id=awsccc-cb-btn-customize]"},{waitFor:"input[aria-checked]"},{click:"input[aria-checked=true]",all:!0,optional:!0},{click:"button[data-id=awsccc-cs-btn-save]"}]},{name:"axeptio",prehideSelectors:[".axeptio_widget"],detectCmp:[{exists:".axeptio_widget"}],detectPopup:[{visible:".axeptio_widget"}],optIn:[{waitFor:".axeptio-widget--open"},{click:"button#axeptio_btn_acceptAll"}],optOut:[{waitFor:".axeptio-widget--open"},{click:"button#axeptio_btn_dismiss"}],test:[{eval:"EVAL_AXEPTIO_0"}]},{name:"baden-wuerttemberg.de",prehideSelectors:[".cookie-alert.t-dark"],cosmetic:!0,detectCmp:[{exists:".cookie-alert.t-dark"}],detectPopup:[{visible:".cookie-alert.t-dark"}],optIn:[{click:".cookie-alert__form input:not([disabled]):not([checked])"},{click:".cookie-alert__button button"}],optOut:[{hide:".cookie-alert.t-dark"}]},{name:"bahn-de",vendorUrl:"https://www.bahn.de/",cosmetic:!1,runContext:{main:!0,frame:!1,urlPattern:"^https://(www\\.)?bahn\\.de/"},intermediate:!1,prehideSelectors:[],detectCmp:[{exists:["body > div:first-child","#consent-layer"]}],detectPopup:[{visible:["body > div:first-child","#consent-layer"]}],optIn:[{waitForThenClick:["body > div:first-child","#consent-layer .js-accept-all-cookies"]}],optOut:[{waitForThenClick:["body > div:first-child","#consent-layer .js-accept-essential-cookies"]}],test:[{eval:"EVAL_BAHN_TEST"}]},{name:"bbb.org",runContext:{urlPattern:"^https://www\\.bbb\\.org/"},cosmetic:!0,prehideSelectors:['div[aria-label="use of cookies on bbb.org"]'],detectCmp:[{exists:'div[aria-label="use of cookies on bbb.org"]'}],detectPopup:[{visible:'div[aria-label="use of cookies on bbb.org"]'}],optIn:[{click:'div[aria-label="use of cookies on bbb.org"] button.bds-button-unstyled span.visually-hidden'}],optOut:[{hide:'div[aria-label="use of cookies on bbb.org"]'}]},{name:"bing.com",prehideSelectors:["#bnp_container"],detectCmp:[{exists:"#bnp_cookie_banner"}],detectPopup:[{visible:"#bnp_cookie_banner"}],optIn:[{click:"#bnp_btn_accept"}],optOut:[{click:"#bnp_btn_preference"},{click:"#mcp_savesettings"}],test:[{eval:"EVAL_BING_0"}]},{name:"blocksy",vendorUrl:"https://creativethemes.com/blocksy/docs/extensions/cookies-consent/",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,prehideSelectors:[".cookie-notification"],detectCmp:[{exists:"#blocksy-ext-cookies-consent-styles-css"}],detectPopup:[{visible:".cookie-notification"}],optIn:[{click:".cookie-notification .ct-cookies-decline-button"}],optOut:[{waitForThenClick:".cookie-notification .ct-cookies-decline-button"}],test:[{eval:"EVAL_BLOCKSY_0"}]},{name:"borlabs",detectCmp:[{exists:"._brlbs-block-content"}],detectPopup:[{visible:"._brlbs-bar-wrap,._brlbs-box-wrap"}],optIn:[{click:"a[data-cookie-accept-all]"}],optOut:[{click:"a[data-cookie-individual]"},{waitForVisible:".cookie-preference"},{click:"input[data-borlabs-cookie-checkbox]:checked",all:!0,optional:!0},{click:"#CookiePrefSave"},{wait:500}],prehideSelectors:["#BorlabsCookieBox"],test:[{eval:"EVAL_BORLABS_0"}]},{name:"bundesregierung.de",prehideSelectors:[".bpa-cookie-banner"],detectCmp:[{exists:".bpa-cookie-banner"}],detectPopup:[{visible:".bpa-cookie-banner .bpa-module-full-hero"}],optIn:[{click:".bpa-accept-all-button"}],optOut:[{wait:500,comment:"click is not immediately recognized"},{waitForThenClick:".bpa-close-button"}],test:[{eval:"EVAL_BUNDESREGIERUNG_DE_0"}]},{name:"burpee.com",cosmetic:!0,prehideSelectors:["#notice-cookie-block"],detectCmp:[{exists:"#notice-cookie-block"}],detectPopup:[{exists:"#html-body #notice-cookie-block"}],optIn:[{click:"#btn-cookie-allow"}],optOut:[{hide:"#html-body #notice-cookie-block, #notice-cookie"}]},{name:"canva.com",prehideSelectors:['div[role="dialog"] a[data-anchor-id="cookie-policy"]'],detectCmp:[{exists:'div[role="dialog"] a[data-anchor-id="cookie-policy"]'}],detectPopup:[{exists:'div[role="dialog"] a[data-anchor-id="cookie-policy"]'}],optIn:[{click:'div[role="dialog"] button:nth-child(1)'}],optOut:[{if:{exists:'div[role="dialog"] button:nth-child(3)'},then:[{click:'div[role="dialog"] button:nth-child(2)'}],else:[{click:'div[role="dialog"] button:nth-child(2)'},{waitFor:'div[role="dialog"] a[data-anchor-id="cookie-policy"]'},{waitFor:'div[role="dialog"] button[role=switch]'},{click:'div[role="dialog"] button:nth-child(2):not([role])'},{click:'div[role="dialog"] div:last-child button:only-child'}]}],test:[{eval:"EVAL_CANVA_0"}]},{name:"canyon.com",runContext:{urlPattern:"^https://www\\.canyon\\.com/"},prehideSelectors:["div.modal.cookiesModal.is-open"],detectCmp:[{exists:"div.modal.cookiesModal.is-open"}],detectPopup:[{visible:"div.modal.cookiesModal.is-open"}],optIn:[{click:'div.cookiesModal__buttonWrapper > button[data-closecause="close-by-submit"]'}],optOut:[{click:'div.cookiesModal__buttonWrapper > button[data-closecause="close-by-manage-cookies"]'},{waitForThenClick:"button#js-manage-data-privacy-save-button"}]},{name:"cc-banner-springer",prehideSelectors:[".cc-banner[data-cc-banner]"],detectCmp:[{exists:".cc-banner[data-cc-banner]"}],detectPopup:[{visible:".cc-banner[data-cc-banner]"}],optIn:[{waitForThenClick:".cc-banner[data-cc-banner] button[data-cc-action=accept]"}],optOut:[{if:{exists:".cc-banner[data-cc-banner] button[data-cc-action=reject]"},then:[{click:".cc-banner[data-cc-banner] button[data-cc-action=reject]"}],else:[{waitForThenClick:".cc-banner[data-cc-banner] button[data-cc-action=preferences]"},{waitFor:".cc-preferences[data-cc-preferences]"},{click:".cc-preferences[data-cc-preferences] input[type=radio][data-cc-action=toggle-category][value=off]",all:!0,optional:!0},{if:{exists:".cc-preferences[data-cc-preferences] button[data-cc-action=reject]"},then:[{click:".cc-preferences[data-cc-preferences] button[data-cc-action=reject]"}],else:[{click:".cc-preferences[data-cc-preferences] button[data-cc-action=save]"}]}]}],test:[{eval:"EVAL_CC_BANNER2_0"}]},{name:"cc_banner",cosmetic:!0,prehideSelectors:[".cc_banner-wrapper"],detectCmp:[{exists:".cc_banner-wrapper"}],detectPopup:[{visible:".cc_banner"}],optIn:[{click:".cc_btn_accept_all"}],optOut:[{hide:".cc_banner-wrapper"}]},{name:"ciaopeople.it",prehideSelectors:["#cp-gdpr-choices"],detectCmp:[{exists:"#cp-gdpr-choices"}],detectPopup:[{visible:"#cp-gdpr-choices"}],optIn:[{waitForThenClick:".gdpr-btm__right > button:nth-child(2)"}],optOut:[{waitForThenClick:".gdpr-top-content > button"},{waitFor:".gdpr-top-back"},{waitForThenClick:".gdpr-btm__right > button:nth-child(1)"}],test:[{visible:"#cp-gdpr-choices",check:"none"}]},{vendorUrl:"https://www.civicuk.com/cookie-control/",name:"civic-cookie-control",prehideSelectors:["#ccc-module,#ccc-overlay"],detectCmp:[{exists:"#ccc-module"}],detectPopup:[{visible:"#ccc"},{visible:"#ccc-module"}],optOut:[{click:"#ccc-reject-settings"}],optIn:[{click:"#ccc-recommended-settings"}]},{name:"click.io",prehideSelectors:["#cl-consent"],detectCmp:[{exists:"#cl-consent"}],detectPopup:[{visible:"#cl-consent"}],optIn:[{waitForThenClick:'#cl-consent [data-role="b_agree"]'}],optOut:[{waitFor:'#cl-consent [data-role="b_options"]'},{wait:500},{click:'#cl-consent [data-role="b_options"]'},{waitFor:'.cl-consent-popup.cl-consent-visible [data-role="alloff"]'},{click:'.cl-consent-popup.cl-consent-visible [data-role="alloff"]',all:!0},{click:'[data-role="b_save"]'}],test:[{eval:"EVAL_CLICKIO_0",comment:"TODO: this only checks if we interacted at all"}]},{name:"clinch",intermediate:!1,runContext:{frame:!1,main:!0},prehideSelectors:[".consent-modal[role=dialog]"],detectCmp:[{exists:".consent-modal[role=dialog]"}],detectPopup:[{visible:".consent-modal[role=dialog]"}],optIn:[{click:"#consent_agree"}],optOut:[{if:{exists:"#consent_reject"},then:[{click:"#consent_reject"}],else:[{click:"#manage_cookie_preferences"},{click:"#cookie_consent_preferences input:checked",all:!0,optional:!0},{click:"#consent_save"}]}],test:[{eval:"EVAL_CLINCH_0"}]},{name:"clustrmaps.com",runContext:{urlPattern:"^https://(www\\.)?clustrmaps\\.com/"},cosmetic:!0,prehideSelectors:["#gdpr-cookie-message"],detectCmp:[{exists:"#gdpr-cookie-message"}],detectPopup:[{visible:"#gdpr-cookie-message"}],optIn:[{click:"button#gdpr-cookie-accept"}],optOut:[{hide:"#gdpr-cookie-message"}]},{name:"coinbase",intermediate:!1,runContext:{frame:!0,main:!0,urlPattern:"^https://(www|help)\\.coinbase\\.com"},prehideSelectors:[],detectCmp:[{exists:"div[class^=CookieBannerContent__Container]"}],detectPopup:[{visible:"div[class^=CookieBannerContent__Container]"}],optIn:[{click:"div[class^=CookieBannerContent__CTA] :nth-last-child(1)"}],optOut:[{click:"button[class^=CookieBannerContent__Settings]"},{click:"div[class^=CookiePreferencesModal__CategoryContainer] input:checked",all:!0,optional:!0},{click:"div[class^=CookiePreferencesModal__ButtonContainer] > button"}],test:[{eval:"EVAL_COINBASE_0"}]},{name:"Complianz banner",prehideSelectors:["#cmplz-cookiebanner-container"],detectCmp:[{exists:"#cmplz-cookiebanner-container .cmplz-cookiebanner"}],detectPopup:[{visible:"#cmplz-cookiebanner-container .cmplz-cookiebanner",check:"any"}],optIn:[{waitForThenClick:".cmplz-cookiebanner .cmplz-accept"}],optOut:[{waitForThenClick:".cmplz-cookiebanner .cmplz-deny"}],test:[{eval:"EVAL_COMPLIANZ_BANNER_0"}]},{name:"Complianz categories",prehideSelectors:['.cc-type-categories[aria-describedby="cookieconsent:desc"]'],detectCmp:[{exists:'.cc-type-categories[aria-describedby="cookieconsent:desc"]'}],detectPopup:[{visible:'.cc-type-categories[aria-describedby="cookieconsent:desc"]'}],optIn:[{any:[{click:".cc-accept-all"},{click:".cc-allow-all"},{click:".cc-allow"},{click:".cc-dismiss"}]}],optOut:[{if:{exists:'.cc-type-categories[aria-describedby="cookieconsent:desc"] .cc-dismiss'},then:[{click:".cc-dismiss"}],else:[{click:".cc-type-categories input[type=checkbox]:not([disabled]):checked",all:!0,optional:!0},{click:".cc-save"}]}]},{name:"Complianz notice",prehideSelectors:['.cc-type-info[aria-describedby="cookieconsent:desc"]'],cosmetic:!0,detectCmp:[{exists:'.cc-type-info[aria-describedby="cookieconsent:desc"] .cc-compliance .cc-btn'}],detectPopup:[{visible:'.cc-type-info[aria-describedby="cookieconsent:desc"] .cc-compliance .cc-btn'}],optIn:[{click:".cc-accept-all",optional:!0},{click:".cc-allow",optional:!0},{click:".cc-dismiss",optional:!0}],optOut:[{if:{exists:".cc-deny"},then:[{click:".cc-deny"}],else:[{hide:'[aria-describedby="cookieconsent:desc"]'}]}]},{name:"Complianz opt-both",prehideSelectors:['[aria-describedby="cookieconsent:desc"] .cc-type-opt-both'],detectCmp:[{exists:'[aria-describedby="cookieconsent:desc"] .cc-type-opt-both'}],detectPopup:[{visible:'[aria-describedby="cookieconsent:desc"] .cc-type-opt-both'}],optIn:[{click:".cc-accept-all",optional:!0},{click:".cc-allow",optional:!0},{click:".cc-dismiss",optional:!0}],optOut:[{waitForThenClick:".cc-deny"}]},{name:"Complianz opt-out",prehideSelectors:['[aria-describedby="cookieconsent:desc"].cc-type-opt-out'],detectCmp:[{exists:'[aria-describedby="cookieconsent:desc"].cc-type-opt-out'}],detectPopup:[{visible:'[aria-describedby="cookieconsent:desc"].cc-type-opt-out'}],optIn:[{click:".cc-accept-all",optional:!0},{click:".cc-allow",optional:!0},{click:".cc-dismiss",optional:!0}],optOut:[{if:{exists:".cc-deny"},then:[{click:".cc-deny"}],else:[{hide:'[aria-describedby="cookieconsent:desc"]'}]}]},{name:"Complianz optin",prehideSelectors:['.cc-type-opt-in[aria-describedby="cookieconsent:desc"]'],detectCmp:[{exists:'.cc-type-opt-in[aria-describedby="cookieconsent:desc"]'}],detectPopup:[{visible:'.cc-type-opt-in[aria-describedby="cookieconsent:desc"]'}],optIn:[{any:[{click:".cc-accept-all"},{click:".cc-allow"},{click:".cc-dismiss"}]}],optOut:[{if:{visible:".cc-deny"},then:[{click:".cc-deny"}],else:[{if:{visible:".cc-settings"},then:[{waitForThenClick:".cc-settings"},{waitForVisible:".cc-settings-view"},{click:".cc-settings-view input[type=checkbox]:not([disabled]):checked",all:!0,optional:!0},{click:".cc-settings-view .cc-btn-accept-selected"}],else:[{click:".cc-dismiss"}]}]}]},{name:"cookie-law-info",prehideSelectors:["#cookie-law-info-bar"],detectCmp:[{exists:"#cookie-law-info-bar"},{eval:"EVAL_COOKIE_LAW_INFO_DETECT"}],detectPopup:[{visible:"#cookie-law-info-bar"}],optIn:[{click:'[data-cli_action="accept_all"]'}],optOut:[{hide:"#cookie-law-info-bar"},{eval:"EVAL_COOKIE_LAW_INFO_0"}],test:[{eval:"EVAL_COOKIE_LAW_INFO_1"}]},{name:"cookie-manager-popup",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,detectCmp:[{exists:"#notice-cookie-block #allow-functional-cookies, #notice-cookie-block #btn-cookie-settings"}],detectPopup:[{visible:"#notice-cookie-block"}],optIn:[{click:"#btn-cookie-allow"}],optOut:[{if:{exists:"#allow-functional-cookies"},then:[{click:"#allow-functional-cookies"}],else:[{waitForThenClick:"#btn-cookie-settings"},{waitForVisible:".modal-body"},{click:'.modal-body input:checked, .switch[data-switch="on"]',all:!0,optional:!0},{click:'[role="dialog"] .modal-footer button'}]}],prehideSelectors:["#btn-cookie-settings"],test:[{eval:"EVAL_COOKIE_MANAGER_POPUP_0"}]},{name:"cookie-notice",prehideSelectors:["#cookie-notice"],cosmetic:!0,detectCmp:[{visible:"#cookie-notice .cookie-notice-container"}],detectPopup:[{visible:"#cookie-notice"}],optIn:[{click:"#cn-accept-cookie"}],optOut:[{hide:"#cookie-notice"}]},{name:"cookie-script",vendorUrl:"https://cookie-script.com/",prehideSelectors:["#cookiescript_injected"],detectCmp:[{exists:"#cookiescript_injected"}],detectPopup:[{visible:"#cookiescript_injected"}],optOut:[{if:{exists:"#cookiescript_reject"},then:[{wait:100},{click:"#cookiescript_reject"}],else:[{click:"#cookiescript_manage"},{waitForVisible:".cookiescript_fsd_main"},{waitForThenClick:"#cookiescript_reject"}]}],optIn:[{click:"#cookiescript_accept"}]},{name:"cookieacceptbar",vendorUrl:"https://unknown",cosmetic:!0,prehideSelectors:["#cookieAcceptBar.cookieAcceptBar"],detectCmp:[{exists:"#cookieAcceptBar.cookieAcceptBar"}],detectPopup:[{visible:"#cookieAcceptBar.cookieAcceptBar"}],optIn:[{waitForThenClick:"#cookieAcceptBarConfirm"}],optOut:[{hide:"#cookieAcceptBar.cookieAcceptBar"}]},{name:"cookiealert",intermediate:!1,prehideSelectors:[],runContext:{frame:!0,main:!0},detectCmp:[{exists:".cookie-alert-extended"}],detectPopup:[{visible:".cookie-alert-extended-modal"}],optIn:[{click:"button[data-controller='cookie-alert/extended/button/accept']"},{eval:"EVAL_COOKIEALERT_0"}],optOut:[{click:"a[data-controller='cookie-alert/extended/detail-link']"},{click:".cookie-alert-configuration-input:checked",all:!0,optional:!0},{click:"button[data-controller='cookie-alert/extended/button/configuration']"},{eval:"EVAL_COOKIEALERT_0"}],test:[{eval:"EVAL_COOKIEALERT_2"}]},{name:"cookieconsent2",vendorUrl:"https://www.github.com/orestbida/cookieconsent",comment:"supports v2.x.x of the library",prehideSelectors:["#cc--main"],detectCmp:[{exists:"#cc--main"}],detectPopup:[{visible:"#cm"},{exists:"#s-all-bn"}],optIn:[{waitForThenClick:"#s-all-bn"}],optOut:[{waitForThenClick:"#s-rall-bn"}],test:[{eval:"EVAL_COOKIECONSENT2_TEST"}]},{name:"cookieconsent3",vendorUrl:"https://www.github.com/orestbida/cookieconsent",comment:"supports v3.x.x of the library",prehideSelectors:["#cc-main"],detectCmp:[{exists:"#cc-main"}],detectPopup:[{visible:"#cc-main .cm-wrapper"}],optIn:[{waitForThenClick:".cm__btn[data-role=all]"}],optOut:[{waitForThenClick:".cm__btn[data-role=necessary]"}],test:[{eval:"EVAL_COOKIECONSENT3_TEST"}]},{name:"cookiefirst.com",prehideSelectors:["#cookiefirst-root,.cookiefirst-root,[aria-labelledby=cookie-preference-panel-title]"],detectCmp:[{exists:"#cookiefirst-root,.cookiefirst-root"}],detectPopup:[{visible:"#cookiefirst-root,.cookiefirst-root"}],optIn:[{click:"button[data-cookiefirst-action=accept]"}],optOut:[{if:{exists:"button[data-cookiefirst-action=adjust]"},then:[{click:"button[data-cookiefirst-action=adjust]"},{waitForVisible:"[data-cookiefirst-widget=modal]",timeout:1e3},{eval:"EVAL_COOKIEFIRST_1"},{wait:1e3},{click:"button[data-cookiefirst-action=save]"}],else:[{click:"button[data-cookiefirst-action=reject]"}]}],test:[{eval:"EVAL_COOKIEFIRST_0"}]},{name:"Cookie Information Banner",prehideSelectors:["#cookie-information-template-wrapper"],detectCmp:[{exists:"#cookie-information-template-wrapper"}],detectPopup:[{visible:"#cookie-information-template-wrapper"}],optIn:[{eval:"EVAL_COOKIEINFORMATION_1"}],optOut:[{hide:"#cookie-information-template-wrapper",comment:"some templates don't hide the banner automatically"},{eval:"EVAL_COOKIEINFORMATION_0"}],test:[{eval:"EVAL_COOKIEINFORMATION_2"}]},{name:"cookieyes",prehideSelectors:[".cky-overlay,.cky-consent-container"],detectCmp:[{exists:".cky-consent-container"}],detectPopup:[{visible:".cky-consent-container"}],optIn:[{waitForThenClick:".cky-consent-container [data-cky-tag=accept-button]"}],optOut:[{if:{exists:".cky-consent-container [data-cky-tag=reject-button]"},then:[{waitForThenClick:".cky-consent-container [data-cky-tag=reject-button]"}],else:[{if:{exists:".cky-consent-container [data-cky-tag=settings-button]"},then:[{click:".cky-consent-container [data-cky-tag=settings-button]"},{waitFor:".cky-modal-open input[type=checkbox]"},{click:".cky-modal-open input[type=checkbox]:checked",all:!0,optional:!0},{waitForThenClick:".cky-modal [data-cky-tag=detail-save-button]"}],else:[{hide:".cky-consent-container,.cky-overlay"}]}]}],test:[{eval:"EVAL_COOKIEYES_0"}]},{name:"corona-in-zahlen.de",prehideSelectors:[".cookiealert"],detectCmp:[{exists:".cookiealert"}],detectPopup:[{visible:".cookiealert"}],optOut:[{click:".configurecookies"},{click:".confirmcookies"}],optIn:[{click:".acceptcookies"}]},{name:"crossfit-com",cosmetic:!0,prehideSelectors:['body #modal > div > div[class^="_wrapper_"]'],detectCmp:[{exists:'body #modal > div > div[class^="_wrapper_"]'}],detectPopup:[{visible:'body #modal > div > div[class^="_wrapper_"]'}],optIn:[{click:'button[aria-label="accept cookie policy"]'}],optOut:[{hide:'body #modal > div > div[class^="_wrapper_"]'}]},{name:"csu-landtag-de",runContext:{urlPattern:"^https://(www\\.|)?csu-landtag\\.de"},prehideSelectors:["#cookie-disclaimer"],detectCmp:[{exists:"#cookie-disclaimer"}],detectPopup:[{visible:"#cookie-disclaimer"}],optIn:[{click:"#cookieall"}],optOut:[{click:"#cookiesel"}]},{name:"dailymotion-us",cosmetic:!0,prehideSelectors:['div[class*="CookiePopup__desktopContainer"]:has(div[class*="CookiePopup"])'],detectCmp:[{exists:'div[class*="CookiePopup__desktopContainer"]'}],detectPopup:[{visible:'div[class*="CookiePopup__desktopContainer"]'}],optIn:[{click:'div[class*="CookiePopup__desktopContainer"] > button > span'}],optOut:[{hide:'div[class*="CookiePopup__desktopContainer"]'}]},{name:"dailymotion.com",runContext:{urlPattern:"^https://(www\\.)?dailymotion\\.com/"},prehideSelectors:['div[class*="Overlay__container"]:has(div[class*="TCF2Popup"])'],detectCmp:[{exists:'div[class*="TCF2Popup"]'}],detectPopup:[{visible:'[class*="TCF2Popup"] a[href^="https://www.dailymotion.com/legal/cookiemanagement"]'}],optIn:[{waitForThenClick:'button[class*="TCF2Popup__button"]:not([class*="TCF2Popup__personalize"])'}],optOut:[{waitForThenClick:'button[class*="TCF2ContinueWithoutAcceptingButton"]'}],test:[{eval:"EVAL_DAILYMOTION_0"}]},{name:"deepl.com",prehideSelectors:[".dl_cookieBanner_container"],detectCmp:[{exists:".dl_cookieBanner_container"}],detectPopup:[{visible:".dl_cookieBanner_container"}],optOut:[{click:".dl_cookieBanner--buttonSelected"}],optIn:[{click:".dl_cookieBanner--buttonAll"}]},{name:"delta.com",runContext:{urlPattern:"^https://www\\.delta\\.com/"},cosmetic:!0,prehideSelectors:["ngc-cookie-banner"],detectCmp:[{exists:"div.cookie-footer-container"}],detectPopup:[{visible:"div.cookie-footer-container"}],optIn:[{click:" button.cookie-close-icon"}],optOut:[{hide:"div.cookie-footer-container"}]},{name:"dmgmedia-us",prehideSelectors:["#mol-ads-cmp-iframe, div.mol-ads-cmp > form > div"],detectCmp:[{exists:"div.mol-ads-cmp > form > div"}],detectPopup:[{waitForVisible:"div.mol-ads-cmp > form > div"}],optIn:[{waitForThenClick:"button.mol-ads-cmp--btn-primary"}],optOut:[{waitForThenClick:"div.mol-ads-ccpa--message > u > a"},{waitForVisible:".mol-ads-cmp--modal-dialog"},{waitForThenClick:"a.mol-ads-cmp-footer-privacy"},{waitForThenClick:"button.mol-ads-cmp--btn-secondary"}]},{name:"dmgmedia",prehideSelectors:['[data-project="mol-fe-cmp"]'],detectCmp:[{exists:'[data-project="mol-fe-cmp"]'}],detectPopup:[{visible:'[data-project="mol-fe-cmp"]'}],optIn:[{waitForThenClick:'[data-project="mol-fe-cmp"] button[class*=primary]'}],optOut:[{waitForThenClick:'[data-project="mol-fe-cmp"] button[class*=basic]'},{waitForVisible:'[data-project="mol-fe-cmp"] div[class*="tabContent"]'},{waitForThenClick:'[data-project="mol-fe-cmp"] div[class*="toggle"][class*="enabled"]',all:!0},{waitForThenClick:'[data-project="mol-fe-cmp"] button[class*=white]'}]},{name:"dndbeyond",vendorUrl:"https://www.dndbeyond.com/",runContext:{urlPattern:"^https://(www\\.)?dndbeyond\\.com/"},prehideSelectors:["[id^=cookie-consent-banner]"],detectCmp:[{exists:"[id^=cookie-consent-banner]"}],detectPopup:[{visible:"[id^=cookie-consent-banner]"}],optIn:[{waitForThenClick:"#cookie-consent-granted"}],optOut:[{waitForThenClick:"#cookie-consent-denied"}],test:[{eval:"EVAL_DNDBEYOND_TEST"}]},{name:"Drupal",detectCmp:[{exists:"#drupalorg-crosssite-gdpr"}],detectPopup:[{visible:"#drupalorg-crosssite-gdpr"}],optOut:[{click:".no"}],optIn:[{click:".yes"}]},{name:"WP DSGVO Tools",link:"https://wordpress.org/plugins/shapepress-dsgvo/",prehideSelectors:[".sp-dsgvo"],cosmetic:!0,detectCmp:[{exists:".sp-dsgvo.sp-dsgvo-popup-overlay"}],detectPopup:[{visible:".sp-dsgvo.sp-dsgvo-popup-overlay",check:"any"}],optIn:[{click:".sp-dsgvo-privacy-btn-accept-all",all:!0}],optOut:[{hide:".sp-dsgvo.sp-dsgvo-popup-overlay"}],test:[{eval:"EVAL_DSGVO_0"}]},{name:"dunelm.com",prehideSelectors:["div[data-testid=cookie-consent-modal-backdrop]"],detectCmp:[{exists:"div[data-testid=cookie-consent-message-contents]"}],detectPopup:[{visible:"div[data-testid=cookie-consent-message-contents]"}],optIn:[{click:'[data-testid="cookie-consent-allow-all"]'}],optOut:[{click:"button[data-testid=cookie-consent-adjust-settings]"},{click:"button[data-testid=cookie-consent-preferences-save]"}],test:[{eval:"EVAL_DUNELM_0"}]},{name:"ecosia",vendorUrl:"https://www.ecosia.org/",runContext:{urlPattern:"^https://www\\.ecosia\\.org/"},prehideSelectors:[".cookie-wrapper"],detectCmp:[{exists:".cookie-wrapper > .cookie-notice"}],detectPopup:[{visible:".cookie-wrapper > .cookie-notice"}],optIn:[{waitForThenClick:"[data-test-id=cookie-notice-accept]"}],optOut:[{waitForThenClick:"[data-test-id=cookie-notice-reject]"}]},{name:"Ensighten ensModal",prehideSelectors:[".ensModal"],detectCmp:[{exists:".ensModal"}],detectPopup:[{visible:".ensModal"}],optIn:[{waitForThenClick:"#modalAcceptButton"}],optOut:[{waitForThenClick:".ensCheckbox:checked",all:!0},{waitForThenClick:"#ensSave"}]},{name:"Ensighten ensNotifyBanner",prehideSelectors:["#ensNotifyBanner"],detectCmp:[{exists:"#ensNotifyBanner"}],detectPopup:[{visible:"#ensNotifyBanner"}],optIn:[{waitForThenClick:"#ensCloseBanner"}],optOut:[{waitForThenClick:"#ensRejectAll,#rejectAll,#ensRejectBanner"}]},{name:"etsy",prehideSelectors:["#gdpr-single-choice-overlay","#gdpr-privacy-settings"],detectCmp:[{exists:"#gdpr-single-choice-overlay"}],detectPopup:[{visible:"#gdpr-single-choice-overlay"}],optOut:[{click:"button[data-gdpr-open-full-settings]"},{waitForVisible:".gdpr-overlay-body input",timeout:3e3},{wait:1e3},{eval:"EVAL_ETSY_0"},{eval:"EVAL_ETSY_1"}],optIn:[{click:"button[data-gdpr-single-choice-accept]"}]},{name:"eu-cookie-compliance-banner",detectCmp:[{exists:"body.eu-cookie-compliance-popup-open"}],detectPopup:[{exists:"body.eu-cookie-compliance-popup-open"}],optIn:[{click:".agree-button"}],optOut:[{if:{visible:".decline-button,.eu-cookie-compliance-save-preferences-button"},then:[{click:".decline-button,.eu-cookie-compliance-save-preferences-button"}]},{hide:".eu-cookie-compliance-banner-info, #sliding-popup"}],test:[{eval:"EVAL_EU_COOKIE_COMPLIANCE_0"}]},{name:"EU Cookie Law",prehideSelectors:[".pea_cook_wrapper,.pea_cook_more_info_popover"],cosmetic:!0,detectCmp:[{exists:".pea_cook_wrapper"}],detectPopup:[{wait:500},{visible:".pea_cook_wrapper"}],optIn:[{click:"#pea_cook_btn"}],optOut:[{hide:".pea_cook_wrapper"}],test:[{eval:"EVAL_EU_COOKIE_LAW_0"}]},{name:"europa-eu",vendorUrl:"https://ec.europa.eu/",runContext:{urlPattern:"^https://[^/]*europa\\.eu/"},prehideSelectors:["#cookie-consent-banner"],detectCmp:[{exists:".cck-container"}],detectPopup:[{visible:".cck-container"}],optIn:[{waitForThenClick:'.cck-actions-button[href="#accept"]'}],optOut:[{waitForThenClick:'.cck-actions-button[href="#refuse"]',hide:".cck-container"}]},{name:"EZoic",prehideSelectors:["#ez-cookie-dialog-wrapper"],detectCmp:[{exists:"#ez-cookie-dialog-wrapper"}],detectPopup:[{visible:"#ez-cookie-dialog-wrapper"}],optIn:[{click:"#ez-accept-all",optional:!0},{eval:"EVAL_EZOIC_0",optional:!0}],optOut:[{wait:500},{click:"#ez-manage-settings"},{waitFor:"#ez-cookie-dialog input[type=checkbox]"},{click:"#ez-cookie-dialog input[type=checkbox]:checked",all:!0},{click:"#ez-save-settings"}],test:[{eval:"EVAL_EZOIC_1"}]},{name:"facebook",runContext:{urlPattern:"^https://([a-z0-9-]+\\.)?facebook\\.com/"},prehideSelectors:['div[data-testid="cookie-policy-manage-dialog"]'],detectCmp:[{exists:'div[data-testid="cookie-policy-manage-dialog"]'}],detectPopup:[{visible:'div[data-testid="cookie-policy-manage-dialog"]'}],optIn:[{waitForThenClick:'button[data-cookiebanner="accept_button"]'},{waitForVisible:'div[data-testid="cookie-policy-manage-dialog"]',check:"none"}],optOut:[{waitForThenClick:'button[data-cookiebanner="accept_only_essential_button"]'},{waitForVisible:'div[data-testid="cookie-policy-manage-dialog"]',check:"none"}]},{name:"fides",vendorUrl:"https://github.com/ethyca/fides",prehideSelectors:["#fides-overlay"],detectCmp:[{exists:"#fides-overlay #fides-banner"}],detectPopup:[{visible:"#fides-overlay #fides-banner"}],optIn:[{waitForThenClick:'#fides-banner [data-testid="Accept all-btn"]'}],optOut:[{waitForThenClick:'#fides-banner [data-testid="Reject all-btn"]'}]},{name:"funding-choices",prehideSelectors:[".fc-consent-root,.fc-dialog-container,.fc-dialog-overlay,.fc-dialog-content"],detectCmp:[{exists:".fc-consent-root"}],detectPopup:[{exists:".fc-dialog-container"}],optOut:[{click:".fc-cta-do-not-consent,.fc-cta-manage-options"},{click:".fc-preference-consent:checked,.fc-preference-legitimate-interest:checked",all:!0,optional:!0},{click:".fc-confirm-choices",optional:!0}],optIn:[{click:".fc-cta-consent"}]},{name:"geeks-for-geeks",runContext:{urlPattern:"^https://www\\.geeksforgeeks\\.org/"},cosmetic:!0,prehideSelectors:[".cookie-consent"],detectCmp:[{exists:".cookie-consent"}],detectPopup:[{visible:".cookie-consent"}],optIn:[{click:".cookie-consent button.consent-btn"}],optOut:[{hide:".cookie-consent"}]},{name:"generic-cosmetic",cosmetic:!0,prehideSelectors:["#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"],detectCmp:[{exists:"#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"}],detectPopup:[{visible:"#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"}],optIn:[],optOut:[{hide:"#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"}]},{name:"google-consent-standalone",prehideSelectors:[],detectCmp:[{exists:'a[href^="https://policies.google.com/technologies/cookies"'},{exists:'form[action^="https://consent.google."][action$=".com/save"]'}],detectPopup:[{visible:'a[href^="https://policies.google.com/technologies/cookies"'}],optIn:[{waitForThenClick:'form[action^="https://consent.google."][action$=".com/save"]:has(input[name=set_eom][value=false]) button'}],optOut:[{waitForThenClick:'form[action^="https://consent.google."][action$=".com/save"]:has(input[name=set_eom][value=true]) button'}]},{name:"google.com",prehideSelectors:[".HTjtHe#xe7COe"],detectCmp:[{exists:".HTjtHe#xe7COe"},{exists:'.HTjtHe#xe7COe a[href^="https://policies.google.com/technologies/cookies"]'}],detectPopup:[{visible:".HTjtHe#xe7COe button#W0wltc"}],optIn:[{waitForThenClick:".HTjtHe#xe7COe button#L2AGLb"}],optOut:[{waitForThenClick:".HTjtHe#xe7COe button#W0wltc"}],test:[{eval:"EVAL_GOOGLE_0"}]},{name:"gov.uk",detectCmp:[{exists:"#global-cookie-message"}],detectPopup:[{exists:"#global-cookie-message"}],optIn:[{click:"button[data-accept-cookies=true]"}],optOut:[{click:"button[data-reject-cookies=true],#reject-cookies"},{click:"button[data-hide-cookie-banner=true],#hide-cookie-decision"}]},{name:"hashicorp",vendorUrl:"https://hashicorp.com/",runContext:{urlPattern:"^https://[^.]*\\.hashicorp\\.com/"},prehideSelectors:["[data-testid=consent-banner]"],detectCmp:[{exists:"[data-testid=consent-banner]"}],detectPopup:[{visible:"[data-testid=consent-banner]"}],optIn:[{waitForThenClick:"[data-testid=accept]"}],optOut:[{waitForThenClick:"[data-testid=manage-preferences]"},{waitForThenClick:"[data-testid=consent-mgr-dialog] [data-ga-button=save-preferences]"}]},{name:"healthline-media",prehideSelectors:["#modal-host > div.no-hash > div.window-wrapper"],detectCmp:[{exists:"#modal-host > div.no-hash > div.window-wrapper, div[data-testid=qualtrics-container]"}],detectPopup:[{exists:"#modal-host > div.no-hash > div.window-wrapper, div[data-testid=qualtrics-container]"}],optIn:[{click:"#modal-host > div.no-hash > div.window-wrapper > div:last-child button"}],optOut:[{if:{exists:'#modal-host > div.no-hash > div.window-wrapper > div:last-child a[href="/privacy-settings"]'},then:[{click:'#modal-host > div.no-hash > div.window-wrapper > div:last-child a[href="/privacy-settings"]'}],else:[{waitForVisible:"div#__next"},{click:"#__next div:nth-child(1) > button:first-child"}]}]},{name:"hema",prehideSelectors:[".cookie-modal"],detectCmp:[{visible:".cookie-modal .cookie-accept-btn"}],detectPopup:[{visible:".cookie-modal .cookie-accept-btn"}],optIn:[{waitForThenClick:".cookie-modal .cookie-accept-btn"}],optOut:[{waitForThenClick:".cookie-modal .js-cookie-reject-btn"}],test:[{eval:"EVAL_HEMA_TEST_0"}]},{name:"hetzner.com",runContext:{urlPattern:"^https://www\\.hetzner\\.com/"},prehideSelectors:["#CookieConsent"],detectCmp:[{exists:"#CookieConsent"}],detectPopup:[{visible:"#CookieConsent"}],optIn:[{click:"#CookieConsentGiven"}],optOut:[{click:"#CookieConsentDeclined"}]},{name:"hl.co.uk",prehideSelectors:[".cookieModalContent","#cookie-banner-overlay"],detectCmp:[{exists:"#cookie-banner-overlay"}],detectPopup:[{exists:"#cookie-banner-overlay"}],optIn:[{click:"#acceptCookieButton"}],optOut:[{click:"#manageCookie"},{hide:".cookieSettingsModal"},{waitFor:"#AOCookieToggle"},{click:"#AOCookieToggle[aria-pressed=true]",optional:!0},{waitFor:"#TPCookieToggle"},{click:"#TPCookieToggle[aria-pressed=true]",optional:!0},{click:"#updateCookieButton"}]},{name:"hu-manity",vendorUrl:"https://hu-manity.co/",prehideSelectors:["#hu.hu-wrapper"],detectCmp:[{exists:"#hu.hu-visible"}],detectPopup:[{visible:"#hu.hu-visible"}],optIn:[{waitForThenClick:"[data-hu-action=cookies-notice-consent-choices-3]"},{waitForThenClick:"#hu-cookies-save"}],optOut:[{waitForThenClick:"#hu-cookies-save"}]},{name:"hubspot",detectCmp:[{exists:"#hs-eu-cookie-confirmation"}],detectPopup:[{visible:"#hs-eu-cookie-confirmation"}],optIn:[{click:"#hs-eu-confirmation-button"}],optOut:[{click:"#hs-eu-decline-button"}]},{name:"indeed.com",cosmetic:!0,prehideSelectors:["#CookiePrivacyNotice"],detectCmp:[{exists:"#CookiePrivacyNotice"}],detectPopup:[{visible:"#CookiePrivacyNotice"}],optIn:[{click:"#CookiePrivacyNotice button[data-gnav-element-name=CookiePrivacyNoticeOk]"}],optOut:[{hide:"#CookiePrivacyNotice"}]},{name:"ing.de",runContext:{urlPattern:"^https://www\\.ing\\.de/"},cosmetic:!0,prehideSelectors:['div[slot="backdrop"]'],detectCmp:[{exists:'[data-tag-name="ing-cc-dialog-frame"]'}],detectPopup:[{visible:'[data-tag-name="ing-cc-dialog-frame"]'}],optIn:[{click:['[data-tag-name="ing-cc-dialog-level0"]','[data-tag-name="ing-cc-button"][class*="accept"]']}],optOut:[{click:['[data-tag-name="ing-cc-dialog-level0"]','[data-tag-name="ing-cc-button"][class*="more"]']}]},{name:"instagram",vendorUrl:"https://instagram.com",runContext:{urlPattern:"^https://www\\.instagram\\.com/"},prehideSelectors:[".x78zum5.xdt5ytf.xg6iff7.x1n2onr6"],detectCmp:[{exists:".x1qjc9v5.x9f619.x78zum5.xdt5ytf.x1iyjqo2.xl56j7k"}],detectPopup:[{visible:".x1qjc9v5.x9f619.x78zum5.xdt5ytf.x1iyjqo2.xl56j7k"}],optIn:[{waitForThenClick:"._a9--._a9_0"}],optOut:[{waitForThenClick:"._a9--._a9_1"},{wait:2e3}]},{name:"ionos.de",prehideSelectors:[".privacy-consent--backdrop",".privacy-consent--modal"],detectCmp:[{exists:".privacy-consent--modal"}],detectPopup:[{visible:".privacy-consent--modal"}],optIn:[{click:"#selectAll"}],optOut:[{click:".footer-config-link"},{click:"#confirmSelection"}]},{name:"itopvpn.com",cosmetic:!0,prehideSelectors:[".pop-cookie"],detectCmp:[{exists:".pop-cookie"}],detectPopup:[{exists:".pop-cookie"}],optIn:[{click:"#_pcookie"}],optOut:[{hide:".pop-cookie"}]},{name:"iubenda",prehideSelectors:["#iubenda-cs-banner"],detectCmp:[{exists:"#iubenda-cs-banner"}],detectPopup:[{visible:".iubenda-cs-accept-btn"}],optIn:[{click:".iubenda-cs-accept-btn"}],optOut:[{click:".iubenda-cs-customize-btn"},{eval:"EVAL_IUBENDA_0"},{click:"#iubFooterBtn"}],test:[{eval:"EVAL_IUBENDA_1"}]},{name:"iWink",prehideSelectors:["body.cookies-request #cookie-bar"],detectCmp:[{exists:"body.cookies-request #cookie-bar"}],detectPopup:[{visible:"body.cookies-request #cookie-bar"}],optIn:[{waitForThenClick:"body.cookies-request #cookie-bar .allow-cookies"}],optOut:[{waitForThenClick:"body.cookies-request #cookie-bar .disallow-cookies"}],test:[{eval:"EVAL_IWINK_TEST"}]},{name:"jdsports",vendorUrl:"https://www.jdsports.co.uk/",runContext:{urlPattern:"^https://(www|m)\\.jdsports\\."},prehideSelectors:[".miniConsent,#PrivacyPolicyBanner"],detectCmp:[{exists:".miniConsent,#PrivacyPolicyBanner"}],detectPopup:[{visible:".miniConsent,#PrivacyPolicyBanner"}],optIn:[{waitForThenClick:".miniConsent .accept-all-cookies"}],optOut:[{if:{exists:"#PrivacyPolicyBanner"},then:[{hide:"#PrivacyPolicyBanner"}],else:[{waitForThenClick:"#cookie-settings"},{waitForThenClick:"#reject-all-cookies"}]}]},{name:"johnlewis.com",prehideSelectors:["div[class^=pecr-cookie-banner-]"],detectCmp:[{exists:"div[class^=pecr-cookie-banner-]"}],detectPopup:[{exists:"div[class^=pecr-cookie-banner-]"}],optOut:[{click:"button[data-test^=manage-cookies]"},{wait:"500"},{click:"label[data-test^=toggle][class*=checked]:not([class*=disabled])",all:!0,optional:!0},{click:"button[data-test=save-preferences]"}],optIn:[{click:"button[data-test=allow-all]"}]},{name:"jquery.cookieBar",vendorUrl:"https://github.com/kovarp/jquery.cookieBar",prehideSelectors:[".cookie-bar"],cosmetic:!0,detectCmp:[{exists:".cookie-bar .cookie-bar__message,.cookie-bar .cookie-bar__buttons"}],detectPopup:[{visible:".cookie-bar .cookie-bar__message,.cookie-bar .cookie-bar__buttons",check:"any"}],optIn:[{click:".cookie-bar .cookie-bar__btn"}],optOut:[{hide:".cookie-bar"}],test:[{visible:".cookie-bar .cookie-bar__message,.cookie-bar .cookie-bar__buttons",check:"none"},{eval:"EVAL_JQUERY_COOKIEBAR_0"}]},{name:"justwatch.com",prehideSelectors:[".consent-banner"],detectCmp:[{exists:".consent-banner .consent-banner__actions"}],detectPopup:[{visible:".consent-banner .consent-banner__actions"}],optIn:[{click:".consent-banner__actions button.basic-button.primary"}],optOut:[{click:".consent-banner__actions button.basic-button.secondary"},{waitForThenClick:".consent-modal__footer button.basic-button.secondary"},{waitForThenClick:".consent-modal ion-content > div > a:nth-child(9)"},{click:"label.consent-switch input[type=checkbox]:checked",all:!0,optional:!0},{waitForVisible:".consent-modal__footer button.basic-button.primary"},{click:".consent-modal__footer button.basic-button.primary"}]},{name:"ketch",vendorUrl:"https://www.ketch.com",runContext:{frame:!1,main:!0},intermediate:!1,prehideSelectors:["#lanyard_root div[role='dialog']"],detectCmp:[{exists:"#lanyard_root div[role='dialog']"}],detectPopup:[{visible:"#lanyard_root div[role='dialog']"}],optIn:[{if:{exists:"#lanyard_root button[class='confirmButton']"},then:[{waitForThenClick:"#lanyard_root div[class*=buttons] > :nth-child(2)"},{click:"#lanyard_root button[class='confirmButton']"}],else:[{waitForThenClick:"#lanyard_root div[class*=buttons] > :nth-child(2)"}]}],optOut:[{if:{exists:"#lanyard_root [aria-describedby=banner-description]"},then:[{waitForThenClick:"#lanyard_root div[class*=buttons] > button[class*=secondaryButton]",comment:"can be either settings or reject button"}]},{waitFor:"#lanyard_root [aria-describedby=preference-description],#lanyard_root [aria-describedby=modal-description]",timeout:1e3,optional:!0},{if:{exists:"#lanyard_root [aria-describedby=preference-description],#lanyard_root [aria-describedby=modal-description]"},then:[{waitForThenClick:"#lanyard_root button[class*=rejectButton]"},{click:"#lanyard_root button[class*=confirmButton],#lanyard_root div[class*=actions_] > button:nth-child(1)"}]}]},{name:"kleinanzeigen-de",runContext:{urlPattern:"^https?://(www\\.)?kleinanzeigen\\.de"},prehideSelectors:["#gdpr-banner-container"],detectCmp:[{any:[{exists:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-cmp-button]"},{exists:"#ConsentManagementPage"}]}],detectPopup:[{any:[{visible:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-cmp-button]"},{visible:"#ConsentManagementPage"}]}],optIn:[{if:{exists:"#gdpr-banner-container #gdpr-banner"},then:[{click:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-accept]"}],else:[{click:"#ConsentManagementPage .Button-primary"}]}],optOut:[{if:{exists:"#gdpr-banner-container #gdpr-banner"},then:[{click:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-cmp-button]"}],else:[{click:"#ConsentManagementPage .Button-secondary"}]}]},{name:"lightbox",prehideSelectors:[".darken-layer.open,.lightbox.lightbox--cookie-consent"],detectCmp:[{exists:"body.cookie-consent-is-active div.lightbox--cookie-consent > div.lightbox__content > div.cookie-consent[data-jsb]"}],detectPopup:[{visible:"body.cookie-consent-is-active div.lightbox--cookie-consent > div.lightbox__content > div.cookie-consent[data-jsb]"}],optOut:[{click:".cookie-consent__footer > button[type='submit']:not([data-button='selectAll'])"}],optIn:[{click:".cookie-consent__footer > button[type='submit'][data-button='selectAll']"}]},{name:"lineagrafica",vendorUrl:"https://addons.prestashop.com/en/legal/8734-eu-cookie-law-gdpr-banner-blocker.html",cosmetic:!0,prehideSelectors:["#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"],detectCmp:[{exists:"#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"}],detectPopup:[{exists:"#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"}],optIn:[{waitForThenClick:"#lgcookieslaw_accept"}],optOut:[{hide:"#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"}]},{name:"linkedin.com",prehideSelectors:[".artdeco-global-alert[type=COOKIE_CONSENT]"],detectCmp:[{exists:".artdeco-global-alert[type=COOKIE_CONSENT]"}],detectPopup:[{visible:".artdeco-global-alert[type=COOKIE_CONSENT]"}],optIn:[{waitForVisible:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=ACCEPT]"},{wait:500},{waitForThenClick:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=ACCEPT]"}],optOut:[{waitForVisible:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=DENY]"},{wait:500},{waitForThenClick:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=DENY]"}],test:[{waitForVisible:".artdeco-global-alert[type=COOKIE_CONSENT]",check:"none"}]},{name:"livejasmin",vendorUrl:"https://www.livejasmin.com/",runContext:{urlPattern:"^https://(m|www)\\.livejasmin\\.com/"},prehideSelectors:["#consent_modal"],detectCmp:[{exists:"#consent_modal"}],detectPopup:[{visible:"#consent_modal"}],optIn:[{waitForThenClick:"#consent_modal button[data-testid=ButtonStyledButton]:first-of-type"}],optOut:[{waitForThenClick:"#consent_modal button[data-testid=ButtonStyledButton]:nth-of-type(2)"},{waitForVisible:"[data-testid=PrivacyPreferenceCenterWithConsentCookieContent]"},{click:"[data-testid=PrivacyPreferenceCenterWithConsentCookieContent] input[data-testid=PrivacyPreferenceCenterWithConsentCookieSwitch]:checked",optional:!0,all:!0},{waitForThenClick:"[data-testid=PrivacyPreferenceCenterWithConsentCookieContent] button[data-testid=ButtonStyledButton]:last-child"}]},{name:"macpaw.com",cosmetic:!0,prehideSelectors:['div[data-banner="cookies"]'],detectCmp:[{exists:'div[data-banner="cookies"]'}],detectPopup:[{exists:'div[data-banner="cookies"]'}],optIn:[{click:'button[data-banner-close="cookies"]'}],optOut:[{hide:'div[data-banner="cookies"]'}]},{name:"marksandspencer.com",cosmetic:!0,detectCmp:[{exists:".navigation-cookiebbanner"}],detectPopup:[{visible:".navigation-cookiebbanner"}],optOut:[{hide:".navigation-cookiebbanner"}],optIn:[{click:".navigation-cookiebbanner__submit"}]},{name:"mediamarkt.de",prehideSelectors:["div[aria-labelledby=pwa-consent-layer-title]","div[class^=StyledConsentLayerWrapper-]"],detectCmp:[{exists:"div[aria-labelledby^=pwa-consent-layer-title]"}],detectPopup:[{exists:"div[aria-labelledby^=pwa-consent-layer-title]"}],optOut:[{click:"button[data-test^=pwa-consent-layer-deny-all]"}],optIn:[{click:"button[data-test^=pwa-consent-layer-accept-all"}]},{name:"Mediavine",prehideSelectors:['[data-name="mediavine-gdpr-cmp"]'],detectCmp:[{exists:'[data-name="mediavine-gdpr-cmp"]'}],detectPopup:[{wait:500},{visible:'[data-name="mediavine-gdpr-cmp"]'}],optIn:[{waitForThenClick:'[data-name="mediavine-gdpr-cmp"] [format="primary"]'}],optOut:[{waitForThenClick:'[data-name="mediavine-gdpr-cmp"] [data-view="manageSettings"]'},{waitFor:'[data-name="mediavine-gdpr-cmp"] input[type=checkbox]'},{eval:"EVAL_MEDIAVINE_0",optional:!0},{click:'[data-name="mediavine-gdpr-cmp"] [format="secondary"]'}]},{name:"microsoft.com",prehideSelectors:["#wcpConsentBannerCtrl"],detectCmp:[{exists:"#wcpConsentBannerCtrl"}],detectPopup:[{exists:"#wcpConsentBannerCtrl"}],optOut:[{eval:"EVAL_MICROSOFT_0"}],optIn:[{eval:"EVAL_MICROSOFT_1"}],test:[{eval:"EVAL_MICROSOFT_2"}]},{name:"midway-usa",runContext:{urlPattern:"^https://www\\.midwayusa\\.com/"},cosmetic:!0,prehideSelectors:["#cookie-container"],detectCmp:[{exists:['div[aria-label="Cookie Policy Banner"]']}],detectPopup:[{visible:"#cookie-container"}],optIn:[{click:"button#cookie-btn"}],optOut:[{hide:'div[aria-label="Cookie Policy Banner"]'}]},{name:"moneysavingexpert.com",detectCmp:[{exists:"dialog[data-testid=accept-our-cookies-dialog]"}],detectPopup:[{visible:"dialog[data-testid=accept-our-cookies-dialog]"}],optIn:[{click:"#banner-accept"}],optOut:[{click:"#banner-manage"},{click:"#pc-confirm"}]},{name:"monzo.com",prehideSelectors:[".cookie-alert, cookie-alert__content"],detectCmp:[{exists:'div.cookie-alert[role="dialog"]'},{exists:'a[href*="monzo"]'}],detectPopup:[{visible:".cookie-alert__content"}],optIn:[{click:".js-accept-cookie-policy"}],optOut:[{click:".js-decline-cookie-policy"}]},{name:"Moove",prehideSelectors:["#moove_gdpr_cookie_info_bar"],detectCmp:[{exists:"#moove_gdpr_cookie_info_bar"}],detectPopup:[{visible:"#moove_gdpr_cookie_info_bar:not(.moove-gdpr-info-bar-hidden)"}],optIn:[{waitForThenClick:".moove-gdpr-infobar-allow-all"}],optOut:[{if:{exists:"#moove_gdpr_cookie_info_bar .change-settings-button"},then:[{click:"#moove_gdpr_cookie_info_bar .change-settings-button"},{waitForVisible:"#moove_gdpr_cookie_modal"},{eval:"EVAL_MOOVE_0"},{click:".moove-gdpr-modal-save-settings"}],else:[{hide:"#moove_gdpr_cookie_info_bar"}]}],test:[{visible:"#moove_gdpr_cookie_info_bar",check:"none"}]},{name:"national-lottery.co.uk",detectCmp:[{exists:".cuk_cookie_consent"}],detectPopup:[{visible:".cuk_cookie_consent",check:"any"}],optOut:[{click:".cuk_cookie_consent_manage_pref"},{click:".cuk_cookie_consent_save_pref"},{click:".cuk_cookie_consent_close"}],optIn:[{click:".cuk_cookie_consent_accept_all"}]},{name:"nba.com",runContext:{urlPattern:"^https://(www\\.)?nba.com/"},cosmetic:!0,prehideSelectors:["#onetrust-banner-sdk"],detectCmp:[{exists:"#onetrust-banner-sdk"}],detectPopup:[{visible:"#onetrust-banner-sdk"}],optIn:[{click:"#onetrust-accept-btn-handler"}],optOut:[{hide:"#onetrust-banner-sdk"}]},{name:"netflix.de",detectCmp:[{exists:"#cookie-disclosure"}],detectPopup:[{visible:".cookie-disclosure-message",check:"any"}],optIn:[{click:".btn-accept"}],optOut:[{hide:"#cookie-disclosure"},{click:".btn-reject"}]},{name:"nhs.uk",prehideSelectors:["#nhsuk-cookie-banner"],detectCmp:[{exists:"#nhsuk-cookie-banner"}],detectPopup:[{exists:"#nhsuk-cookie-banner"}],optOut:[{click:"#nhsuk-cookie-banner__link_accept"}],optIn:[{click:"#nhsuk-cookie-banner__link_accept_analytics"}]},{name:"notice-cookie",prehideSelectors:[".button--notice"],cosmetic:!0,detectCmp:[{exists:".notice--cookie"}],detectPopup:[{visible:".notice--cookie"}],optIn:[{click:".button--notice"}],optOut:[{hide:".notice--cookie"}]},{name:"nrk.no",cosmetic:!0,prehideSelectors:[".nrk-masthead__info-banner--cookie"],detectCmp:[{exists:".nrk-masthead__info-banner--cookie"}],detectPopup:[{exists:".nrk-masthead__info-banner--cookie"}],optIn:[{click:"div.nrk-masthead__info-banner--cookie button > span:has(+ svg.nrk-close)"}],optOut:[{hide:".nrk-masthead__info-banner--cookie"}]},{name:"obi.de",prehideSelectors:[".disc-cp--active"],detectCmp:[{exists:".disc-cp-modal__modal"}],detectPopup:[{visible:".disc-cp-modal__modal"}],optIn:[{click:".js-disc-cp-accept-all"}],optOut:[{click:".js-disc-cp-deny-all"}]},{name:"om",vendorUrl:"https://olli-machts.de/en/extension/cookie-manager",prehideSelectors:[".tx-om-cookie-consent"],detectCmp:[{exists:".tx-om-cookie-consent .active[data-omcookie-panel]"}],detectPopup:[{exists:".tx-om-cookie-consent .active[data-omcookie-panel]"}],optIn:[{waitForThenClick:"[data-omcookie-panel-save=all]"}],optOut:[{if:{exists:"[data-omcookie-panel-save=min]"},then:[{waitForThenClick:"[data-omcookie-panel-save=min]"}],else:[{click:"input[data-omcookie-panel-grp]:checked:not(:disabled)",all:!0,optional:!0},{waitForThenClick:"[data-omcookie-panel-save=save]"}]}]},{name:"onlyFans.com",runContext:{urlPattern:"^https://onlyfans\\.com/"},prehideSelectors:["div.b-cookies-informer"],detectCmp:[{exists:"div.b-cookies-informer"}],detectPopup:[{exists:"div.b-cookies-informer"}],optIn:[{click:"div.b-cookies-informer__nav > button:nth-child(2)"}],optOut:[{click:"div.b-cookies-informer__nav > button:nth-child(1)"},{if:{exists:"div.b-cookies-informer__switchers"},then:[{click:"div.b-cookies-informer__switchers input:not([disabled])",all:!0},{click:"div.b-cookies-informer__nav > button"}]}]},{name:"openli",vendorUrl:"https://openli.com",prehideSelectors:[".legalmonster-cleanslate"],detectCmp:[{exists:".legalmonster-cleanslate"}],detectPopup:[{visible:".legalmonster-cleanslate #lm-cookie-wall-container",check:"any"}],optIn:[{waitForThenClick:"#lm-accept-all"}],optOut:[{waitForThenClick:"#lm-accept-necessary"}]},{name:"opera.com",vendorUrl:"https://unknown",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,prehideSelectors:[],detectCmp:[{exists:"#cookie-consent .manage-cookies__btn"}],detectPopup:[{visible:"#cookie-consent .cookie-basic-consent__btn"}],optIn:[{waitForThenClick:"#cookie-consent .cookie-basic-consent__btn"}],optOut:[{waitForThenClick:"#cookie-consent .manage-cookies__btn"},{waitForThenClick:"#cookie-consent .active.marketing_option_switch.cookie-consent__switch",all:!0},{waitForThenClick:"#cookie-consent .cookie-selection__btn"}],test:[{eval:"EVAL_OPERA_0"}]},{name:"osano",prehideSelectors:[".osano-cm-window,.osano-cm-dialog"],detectCmp:[{exists:".osano-cm-window"}],detectPopup:[{visible:".osano-cm-dialog"}],optIn:[{click:".osano-cm-accept-all",optional:!0}],optOut:[{waitForThenClick:".osano-cm-denyAll"}]},{name:"otto.de",prehideSelectors:[".cookieBanner--visibility"],detectCmp:[{exists:".cookieBanner--visibility"}],detectPopup:[{visible:".cookieBanner__wrapper"}],optIn:[{click:".js_cookieBannerPermissionButton"}],optOut:[{click:".js_cookieBannerProhibitionButton"}]},{name:"ourworldindata",vendorUrl:"https://ourworldindata.org/",runContext:{urlPattern:"^https://ourworldindata\\.org/"},prehideSelectors:[".cookie-manager"],detectCmp:[{exists:".cookie-manager"}],detectPopup:[{visible:".cookie-manager .cookie-notice.open"}],optIn:[{waitForThenClick:".cookie-notice [data-test=accept]"}],optOut:[{waitForThenClick:".cookie-notice [data-test=reject]"}]},{name:"pabcogypsum",vendorUrl:"https://unknown",prehideSelectors:[".js-cookie-notice:has(#cookie_settings-form)"],detectCmp:[{exists:".js-cookie-notice #cookie_settings-form"}],detectPopup:[{visible:".js-cookie-notice #cookie_settings-form"}],optIn:[{waitForThenClick:".js-cookie-notice button[value=allow]"}],optOut:[{waitForThenClick:".js-cookie-notice button[value=disable]"}]},{name:"paypal-us",prehideSelectors:["#ccpaCookieContent_wrapper, article.ppvx_modal--overpanel"],detectCmp:[{exists:"#ccpaCookieBanner, .privacy-sheet-content"}],detectPopup:[{exists:"#ccpaCookieBanner, .privacy-sheet-content"}],optIn:[{click:"#acceptAllButton"}],optOut:[{if:{exists:"a#manageCookiesLink"},then:[{click:"a#manageCookiesLink"}],else:[{waitForVisible:".privacy-sheet-content #formContent"},{click:"#formContent .cookiepref-11m2iee-checkbox_base input:checked",all:!0,optional:!0},{click:".confirmCookie #submitCookiesBtn"}]}]},{name:"paypal.com",prehideSelectors:["#gdprCookieBanner"],detectCmp:[{exists:"#gdprCookieBanner"}],detectPopup:[{visible:"#gdprCookieContent_wrapper"}],optIn:[{click:"#acceptAllButton"}],optOut:[{wait:200},{click:".gdprCookieBanner_decline-button"}],test:[{wait:500},{eval:"EVAL_PAYPAL_0"}]},{name:"pinetools.com",cosmetic:!0,prehideSelectors:["#aviso_cookies"],detectCmp:[{exists:"#aviso_cookies"}],detectPopup:[{exists:".lang_en #aviso_cookies"}],optIn:[{click:"#aviso_cookies .a_boton_cerrar"}],optOut:[{hide:"#aviso_cookies"}]},{name:"pmc",cosmetic:!0,prehideSelectors:["#pmc-pp-tou--notice"],detectCmp:[{exists:"#pmc-pp-tou--notice"}],detectPopup:[{visible:"#pmc-pp-tou--notice"}],optIn:[{click:"span.pmc-pp-tou--notice-close-btn"}],optOut:[{hide:"#pmc-pp-tou--notice"}]},{name:"pornhub.com",runContext:{urlPattern:"^https://(www\\.)?pornhub\\.com/"},cosmetic:!0,prehideSelectors:[".cookiesBanner"],detectCmp:[{exists:".cookiesBanner"}],detectPopup:[{visible:".cookiesBanner"}],optIn:[{click:".cookiesBanner .okButton"}],optOut:[{hide:".cookiesBanner"}]},{name:"pornpics.com",cosmetic:!0,prehideSelectors:["#cookie-contract"],detectCmp:[{exists:"#cookie-contract"}],detectPopup:[{visible:"#cookie-contract"}],optIn:[{click:"#cookie-contract .icon-cross"}],optOut:[{hide:"#cookie-contract"}]},{name:"PrimeBox CookieBar",prehideSelectors:["#cookie-bar"],detectCmp:[{exists:"#cookie-bar .cb-enable,#cookie-bar .cb-disable,#cookie-bar .cb-policy"}],detectPopup:[{visible:"#cookie-bar .cb-enable,#cookie-bar .cb-disable,#cookie-bar .cb-policy",check:"any"}],optIn:[{waitForThenClick:"#cookie-bar .cb-enable"}],optOut:[{click:"#cookie-bar .cb-disable",optional:!0},{hide:"#cookie-bar"}],test:[{eval:"EVAL_PRIMEBOX_0"}]},{name:"privacymanager.io",prehideSelectors:["#gdpr-consent-tool-wrapper",'iframe[src^="https://cmp-consent-tool.privacymanager.io"]'],runContext:{urlPattern:"^https://cmp-consent-tool\\.privacymanager\\.io/",main:!1,frame:!0},detectCmp:[{exists:"button#save"}],detectPopup:[{visible:"button#save"}],optIn:[{click:"button#save"}],optOut:[{if:{exists:"#denyAll"},then:[{click:"#denyAll"},{waitForThenClick:".okButton"}],else:[{waitForThenClick:"#manageSettings"},{waitFor:".purposes-overview-list"},{waitFor:"button#saveAndExit"},{click:"span[role=checkbox][aria-checked=true]",all:!0,optional:!0},{click:"button#saveAndExit"}]}]},{name:"productz.com",vendorUrl:"https://productz.com/",runContext:{urlPattern:"^https://productz\\.com/"},prehideSelectors:[],detectCmp:[{exists:".c-modal.is-active"}],detectPopup:[{visible:".c-modal.is-active"}],optIn:[{waitForThenClick:".c-modal.is-active .is-accept"}],optOut:[{waitForThenClick:".c-modal.is-active .is-dismiss"}]},{name:"pubtech",prehideSelectors:["#pubtech-cmp"],detectCmp:[{exists:"#pubtech-cmp"}],detectPopup:[{visible:"#pubtech-cmp #pt-actions"}],optIn:[{if:{exists:"#pt-accept-all"},then:[{click:"#pubtech-cmp #pt-actions #pt-accept-all"}],else:[{click:"#pubtech-cmp #pt-actions button:nth-of-type(2)"}]}],optOut:[{click:"#pubtech-cmp #pt-close"}],test:[{eval:"EVAL_PUBTECH_0"}]},{name:"quantcast",prehideSelectors:["#qc-cmp2-main,#qc-cmp2-container"],detectCmp:[{exists:"#qc-cmp2-container"}],detectPopup:[{visible:"#qc-cmp2-ui"}],optOut:[{click:'.qc-cmp2-summary-buttons > button[mode="secondary"]'},{waitFor:"#qc-cmp2-ui"},{click:'.qc-cmp2-toggle-switch > button[aria-checked="true"]',all:!0,optional:!0},{click:'.qc-cmp2-main button[aria-label="REJECT ALL"]',optional:!0},{waitForThenClick:'.qc-cmp2-main button[aria-label="SAVE & EXIT"],.qc-cmp2-buttons-desktop > button[mode="primary"]',timeout:5e3}],optIn:[{click:'.qc-cmp2-summary-buttons > button[mode="primary"]'}]},{name:"reddit.com",runContext:{urlPattern:"^https://www\\.reddit\\.com/"},prehideSelectors:["[bundlename=reddit_cookie_banner]"],detectCmp:[{exists:"reddit-cookie-banner"}],detectPopup:[{visible:"reddit-cookie-banner"}],optIn:[{waitForThenClick:["reddit-cookie-banner","#accept-all-cookies-button > button"]}],optOut:[{waitForThenClick:["reddit-cookie-banner","#reject-nonessential-cookies-button > button"]}],test:[{eval:"EVAL_REDDIT_0"}]},{name:"rog-forum.asus.com",runContext:{urlPattern:"^https://rog-forum\\.asus\\.com/"},prehideSelectors:["#cookie-policy-info"],detectCmp:[{exists:"#cookie-policy-info"}],detectPopup:[{visible:"#cookie-policy-info"}],optIn:[{click:'div.cookie-btn-box > div[aria-label="Accept"]'}],optOut:[{click:'div.cookie-btn-box > div[aria-label="Reject"]'},{waitForThenClick:'.cookie-policy-lightbox-bottom > div[aria-label="Save Settings"]'}]},{name:"roofingmegastore.co.uk",runContext:{urlPattern:"^https://(www\\.)?roofingmegastore\\.co\\.uk"},prehideSelectors:["#m-cookienotice"],detectCmp:[{exists:"#m-cookienotice"}],detectPopup:[{visible:"#m-cookienotice"}],optIn:[{click:"#accept-cookies"}],optOut:[{click:"#manage-cookies"},{waitForThenClick:"#accept-selected"}]},{name:"samsung.com",runContext:{urlPattern:"^https://www\\.samsung\\.com/"},cosmetic:!0,prehideSelectors:["div.cookie-bar"],detectCmp:[{exists:"div.cookie-bar"}],detectPopup:[{visible:"div.cookie-bar"}],optIn:[{click:"div.cookie-bar__manage > a"}],optOut:[{hide:"div.cookie-bar"}]},{name:"setapp.com",vendorUrl:"https://setapp.com/",cosmetic:!0,runContext:{urlPattern:"^https://setapp\\.com/"},prehideSelectors:[],detectCmp:[{exists:".cookie-banner.js-cookie-banner"}],detectPopup:[{visible:".cookie-banner.js-cookie-banner"}],optIn:[{waitForThenClick:".cookie-banner.js-cookie-banner button"}],optOut:[{hide:".cookie-banner.js-cookie-banner"}]},{name:"sibbo",prehideSelectors:["sibbo-cmp-layout"],detectCmp:[{exists:"sibbo-cmp-layout"}],detectPopup:[{visible:"sibbo-cmp-layout"}],optIn:[{click:"sibbo-cmp-layout [data-accept-all]"}],optOut:[{click:'.sibbo-panel__aside__buttons a[data-nav="purposes"]'},{click:'.sibbo-panel__main__header__actions a[data-focusable="reject-all"]'},{if:{exists:"[data-view=purposes] .sibbo-panel__main__footer__actions [data-save-and-exit]"},then:[],else:[{waitFor:'.sibbo-panel__main__footer__actions a[data-focusable="next"]:not(.sibbo-cmp-button--disabled)'},{click:'.sibbo-panel__main__footer__actions a[data-focusable="next"]'},{click:'.sibbo-panel__main div[data-view="purposesLegInt"] a[data-focusable="reject-all"]'}]},{waitFor:".sibbo-panel__main__footer__actions [data-save-and-exit]:not(.sibbo-cmp-button--disabled)"},{click:".sibbo-panel__main__footer__actions [data-save-and-exit]:not(.sibbo-cmp-button--disabled)"}],test:[{eval:"EVAL_SIBBO_0"}]},{name:"similarweb.com",cosmetic:!0,prehideSelectors:[".app-cookies-notification"],detectCmp:[{exists:".app-cookies-notification"}],detectPopup:[{exists:".app-layout .app-cookies-notification"}],optIn:[{click:"button.app-cookies-notification__dismiss"}],optOut:[{hide:".app-layout .app-cookies-notification"}]},{name:"Sirdata",cosmetic:!1,prehideSelectors:["#sd-cmp"],detectCmp:[{exists:"#sd-cmp"}],detectPopup:[{visible:"#sd-cmp"}],optIn:[{waitForThenClick:"#sd-cmp .sd-cmp-3cRQ2"}],optOut:[{waitForThenClick:["#sd-cmp","xpath///span[contains(., 'Do not accept') or contains(., 'Acceptera inte') or contains(., 'No aceptar') or contains(., 'Ikke acceptere') or contains(., 'Nicht akzeptieren') or contains(., 'Не приемам') or contains(., 'Να μην γίνει αποδοχή') or contains(., 'Niet accepteren') or contains(., 'Nepřijímat') or contains(., 'Nie akceptuj') or contains(., 'Nu acceptați') or contains(., 'Não aceitar') or contains(., 'Continuer sans accepter') or contains(., 'Non accettare') or contains(., 'Nem fogad el')]"]}]},{name:"snigel",detectCmp:[{exists:".snigel-cmp-framework"}],detectPopup:[{visible:".snigel-cmp-framework"}],optOut:[{click:"#sn-b-custom"},{click:"#sn-b-save"}],test:[{eval:"EVAL_SNIGEL_0"}],optIn:[{click:".snigel-cmp-framework #accept-choices"}]},{name:"steampowered.com",detectCmp:[{exists:".cookiepreferences_popup"},{visible:".cookiepreferences_popup"}],detectPopup:[{visible:".cookiepreferences_popup"}],optOut:[{click:"#rejectAllButton"}],optIn:[{click:"#acceptAllButton"}],test:[{wait:1e3},{eval:"EVAL_STEAMPOWERED_0"}]},{name:"strato.de",prehideSelectors:[".consent__wrapper"],runContext:{urlPattern:"^https://www\\.strato\\.de/"},detectCmp:[{exists:".consent"}],detectPopup:[{visible:".consent"}],optIn:[{click:"button.consentAgree"}],optOut:[{click:"button.consentSettings"},{waitForThenClick:"button#consentSubmit"}]},{name:"svt.se",vendorUrl:"https://www.svt.se/",runContext:{urlPattern:"^https://www\\.svt\\.se/"},prehideSelectors:["[class*=CookieConsent__root___]"],detectCmp:[{exists:"[class*=CookieConsent__root___]"}],detectPopup:[{visible:"[class*=CookieConsent__modal___]"}],optIn:[{waitForThenClick:"[class*=CookieConsent__modal___] > div > button[class*=primary]"}],optOut:[{waitForThenClick:"[class*=CookieConsent__modal___] > div > button[class*=secondary]:nth-child(2)"}],test:[{eval:"EVAL_SVT_TEST"}]},{name:"takealot.com",cosmetic:!0,prehideSelectors:['div[class^="cookies-banner-module_"]'],detectCmp:[{exists:'div[class^="cookies-banner-module_cookie-banner_"]'}],detectPopup:[{exists:'div[class^="cookies-banner-module_cookie-banner_"]'}],optIn:[{click:'button[class*="cookies-banner-module_dismiss-button_"]'}],optOut:[{hide:'div[class^="cookies-banner-module_"]'},{if:{exists:'div[class^="cookies-banner-module_small-cookie-banner_"]'},then:[{eval:"EVAL_TAKEALOT_0"}],else:[]}]},{name:"tarteaucitron.js",prehideSelectors:["#tarteaucitronRoot"],detectCmp:[{exists:"#tarteaucitronRoot"}],detectPopup:[{visible:"#tarteaucitronRoot #tarteaucitronAlertSmall,#tarteaucitronRoot #tarteaucitronAlertBig",check:"any"}],optIn:[{eval:"EVAL_TARTEAUCITRON_1"}],optOut:[{eval:"EVAL_TARTEAUCITRON_0"}],test:[{eval:"EVAL_TARTEAUCITRON_2",comment:"sometimes there are required categories, so we check that at least something is false"}]},{name:"taunton",vendorUrl:"https://www.taunton.com/",prehideSelectors:["#taunton-user-consent__overlay"],detectCmp:[{exists:"#taunton-user-consent__overlay"}],detectPopup:[{exists:"#taunton-user-consent__overlay:not([aria-hidden=true])"}],optIn:[{click:"#taunton-user-consent__toolbar input[type=checkbox]:not(:checked)"},{click:"#taunton-user-consent__toolbar button[type=submit]"}],optOut:[{click:"#taunton-user-consent__toolbar input[type=checkbox]:checked",optional:!0,all:!0},{click:"#taunton-user-consent__toolbar button[type=submit]"}],test:[{eval:"EVAL_TAUNTON_TEST"}]},{name:"Tealium",prehideSelectors:["#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs,#__tealiumImplicitmodal,#consent-layer"],detectCmp:[{exists:"#__tealiumGDPRecModal *,#__tealiumGDPRcpPrefs *,#__tealiumImplicitmodal *"},{eval:"EVAL_TEALIUM_0"}],detectPopup:[{visible:"#__tealiumGDPRecModal *,#__tealiumGDPRcpPrefs *,#__tealiumImplicitmodal *",check:"any"}],optOut:[{eval:"EVAL_TEALIUM_1"},{eval:"EVAL_TEALIUM_DONOTSELL"},{hide:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs,#__tealiumImplicitmodal"},{waitForThenClick:"#cm-acceptNone,.js-accept-essential-cookies",timeout:1e3,optional:!0}],optIn:[{hide:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs"},{eval:"EVAL_TEALIUM_2"}],test:[{eval:"EVAL_TEALIUM_3"},{eval:"EVAL_TEALIUM_DONOTSELL_CHECK"},{visible:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs",check:"none"}]},{name:"temu",vendorUrl:"https://temu.com",runContext:{urlPattern:"^https://[^/]*temu\\.com/"},prehideSelectors:["._2d-8vq-W,._1UdBUwni"],detectCmp:[{exists:"._3YCsmIaS"}],detectPopup:[{visible:"._3YCsmIaS"}],optIn:[{waitForThenClick:"._3fKiu5wx._3zN5SumS._3tAK973O.IYOfhWEs.VGNGF1pA"}],optOut:[{waitForThenClick:"._3fKiu5wx._1_XToJBF._3tAK973O.IYOfhWEs.VGNGF1pA"}]},{name:"Termly",prehideSelectors:["#termly-code-snippet-support"],detectCmp:[{exists:"#termly-code-snippet-support"}],detectPopup:[{visible:"#termly-code-snippet-support div"}],optIn:[{waitForThenClick:'[data-tid="banner-accept"]'}],optOut:[{if:{exists:'[data-tid="banner-decline"]'},then:[{click:'[data-tid="banner-decline"]'}],else:[{click:".t-preference-button"},{wait:500},{if:{exists:".t-declineAllButton"},then:[{click:".t-declineAllButton"}],else:[{waitForThenClick:".t-preference-modal input[type=checkbox][checked]:not([disabled])",all:!0},{waitForThenClick:".t-saveButton"}]}]}]},{name:"termsfeed",vendorUrl:"https://termsfeed.com",comment:"v4.x.x",prehideSelectors:[".termsfeed-com---nb"],detectCmp:[{exists:".termsfeed-com---nb"}],detectPopup:[{visible:".termsfeed-com---nb"}],optIn:[{waitForThenClick:".cc-nb-okagree"}],optOut:[{waitForThenClick:".cc-nb-reject"}]},{name:"termsfeed3",vendorUrl:"https://termsfeed.com",comment:"v3.x.x",cosmetic:!0,prehideSelectors:[".cc_dialog.cc_css_reboot"],detectCmp:[{exists:".cc_dialog.cc_css_reboot"}],detectPopup:[{visible:".cc_dialog.cc_css_reboot"}],optIn:[{waitForThenClick:".cc_dialog.cc_css_reboot .cc_b_ok"}],optOut:[{hide:".cc_dialog.cc_css_reboot"}]},{name:"Test page cosmetic CMP",cosmetic:!0,prehideSelectors:["#privacy-test-page-cmp-test-prehide"],detectCmp:[{exists:"#privacy-test-page-cmp-test-banner"}],detectPopup:[{visible:"#privacy-test-page-cmp-test-banner"}],optIn:[{waitFor:"#accept-all"},{click:"#accept-all"}],optOut:[{hide:"#privacy-test-page-cmp-test-banner"}],test:[{wait:500},{eval:"EVAL_TESTCMP_COSMETIC_0"}]},{name:"Test page CMP",prehideSelectors:["#reject-all"],detectCmp:[{exists:"#privacy-test-page-cmp-test"}],detectPopup:[{visible:"#privacy-test-page-cmp-test"}],optIn:[{waitFor:"#accept-all"},{click:"#accept-all"}],optOut:[{waitFor:"#reject-all"},{click:"#reject-all"}],test:[{eval:"EVAL_TESTCMP_0"}]},{name:"thalia.de",prehideSelectors:[".consent-banner-box"],detectCmp:[{exists:"consent-banner[component=consent-banner]"}],detectPopup:[{visible:".consent-banner-box"}],optIn:[{click:".button-zustimmen"}],optOut:[{click:"button[data-consent=disagree]"}]},{name:"thefreedictionary.com",prehideSelectors:["#cmpBanner"],detectCmp:[{exists:"#cmpBanner"}],detectPopup:[{visible:"#cmpBanner"}],optIn:[{eval:"EVAL_THEFREEDICTIONARY_1"}],optOut:[{eval:"EVAL_THEFREEDICTIONARY_0"}]},{name:"theverge",runContext:{frame:!1,main:!0,urlPattern:"^https://(www)?\\.theverge\\.com"},intermediate:!1,prehideSelectors:[".duet--cta--cookie-banner"],detectCmp:[{exists:".duet--cta--cookie-banner"}],detectPopup:[{visible:".duet--cta--cookie-banner"}],optIn:[{click:".duet--cta--cookie-banner button.tracking-12",all:!1}],optOut:[{click:".duet--cta--cookie-banner button.tracking-12 > span"}],test:[{eval:"EVAL_THEVERGE_0"}]},{name:"tidbits-com",cosmetic:!0,prehideSelectors:["#eu_cookie_law_widget-2"],detectCmp:[{exists:"#eu_cookie_law_widget-2"}],detectPopup:[{visible:"#eu_cookie_law_widget-2"}],optIn:[{click:"#eu-cookie-law form > input.accept"}],optOut:[{hide:"#eu_cookie_law_widget-2"}]},{name:"tractor-supply",runContext:{urlPattern:"^https://www\\.tractorsupply\\.com/"},cosmetic:!0,prehideSelectors:[".tsc-cookie-banner"],detectCmp:[{exists:".tsc-cookie-banner"}],detectPopup:[{visible:".tsc-cookie-banner"}],optIn:[{click:"#cookie-banner-cancel"}],optOut:[{hide:".tsc-cookie-banner"}]},{name:"trader-joes-com",cosmetic:!0,prehideSelectors:['div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'],detectCmp:[{exists:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}],detectPopup:[{visible:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}],optIn:[{click:'div[class^="CookiesAlert_cookiesAlert__container__"] button'}],optOut:[{hide:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}]},{name:"transcend",vendorUrl:"https://unknown",cosmetic:!0,prehideSelectors:["#transcend-consent-manager"],detectCmp:[{exists:"#transcend-consent-manager"}],detectPopup:[{visible:"#transcend-consent-manager"}],optIn:[{waitForThenClick:["#transcend-consent-manager","#consentManagerMainDialog .inner-container button"]}],optOut:[{hide:"#transcend-consent-manager"}]},{name:"transip-nl",runContext:{urlPattern:"^https://www\\.transip\\.nl/"},prehideSelectors:["#consent-modal"],detectCmp:[{any:[{exists:"#consent-modal"},{exists:"#privacy-settings-content"}]}],detectPopup:[{any:[{visible:"#consent-modal"},{visible:"#privacy-settings-content"}]}],optIn:[{click:'button[type="submit"]'}],optOut:[{if:{exists:"#privacy-settings-content"},then:[{click:'button[type="submit"]'}],else:[{click:"div.one-modal__action-footer-column--secondary > a"}]}]},{name:"tropicfeel-com",prehideSelectors:["#shopify-section-cookies-controller"],detectCmp:[{exists:"#shopify-section-cookies-controller"}],detectPopup:[{visible:"#shopify-section-cookies-controller #cookies-controller-main-pane",check:"any"}],optIn:[{waitForThenClick:"#cookies-controller-main-pane form[data-form-allow-all] button"}],optOut:[{click:"#cookies-controller-main-pane a[data-tab-target=manage-cookies]"},{waitFor:"#manage-cookies-pane.active"},{click:"#manage-cookies-pane.active input[type=checkbox][checked]:not([disabled])",all:!0},{click:"#manage-cookies-pane.active button[type=submit]"}],test:[]},{name:"true-car",runContext:{urlPattern:"^https://www\\.truecar\\.com/"},cosmetic:!0,prehideSelectors:[['div[aria-labelledby="cookie-banner-heading"]']],detectCmp:[{exists:'div[aria-labelledby="cookie-banner-heading"]'}],detectPopup:[{visible:'div[aria-labelledby="cookie-banner-heading"]'}],optIn:[{click:'div[aria-labelledby="cookie-banner-heading"] > button[aria-label="Close"]'}],optOut:[{hide:'div[aria-labelledby="cookie-banner-heading"]'}]},{name:"truyo",prehideSelectors:["#truyo-consent-module"],detectCmp:[{exists:"#truyo-cookieBarContent"}],detectPopup:[{visible:"#truyo-consent-module"}],optIn:[{click:"button#acceptAllCookieButton"}],optOut:[{click:"button#declineAllCookieButton"}]},{name:"twitch-mobile",vendorUrl:"https://m.twitch.tv/",cosmetic:!0,runContext:{urlPattern:"^https?://m\\.twitch\\.tv"},prehideSelectors:[],detectCmp:[{exists:'.ReactModal__Overlay [href="https://www.twitch.tv/p/cookie-policy"]'}],detectPopup:[{visible:'.ReactModal__Overlay [href="https://www.twitch.tv/p/cookie-policy"]'}],optIn:[{waitForThenClick:'.ReactModal__Overlay:has([href="https://www.twitch.tv/p/cookie-policy"]) button'}],optOut:[{hide:'.ReactModal__Overlay:has([href="https://www.twitch.tv/p/cookie-policy"])'}]},{name:"twitch.tv",runContext:{urlPattern:"^https?://(www\\.)?twitch\\.tv"},prehideSelectors:["div:has(> .consent-banner .consent-banner__content--gdpr-v2),.ReactModalPortal:has([data-a-target=consent-modal-save])"],detectCmp:[{exists:".consent-banner .consent-banner__content--gdpr-v2"}],detectPopup:[{visible:".consent-banner .consent-banner__content--gdpr-v2"}],optIn:[{click:'button[data-a-target="consent-banner-accept"]'}],optOut:[{hide:"div:has(> .consent-banner .consent-banner__content--gdpr-v2)"},{click:'button[data-a-target="consent-banner-manage-preferences"]'},{waitFor:"input[type=checkbox][data-a-target=tw-checkbox]"},{click:"input[type=checkbox][data-a-target=tw-checkbox][checked]:not([disabled])",all:!0,optional:!0},{waitForThenClick:"[data-a-target=consent-modal-save]"},{waitForVisible:".ReactModalPortal:has([data-a-target=consent-modal-save])",check:"none"}]},{name:"twitter",runContext:{urlPattern:"^https://([a-z0-9-]+\\.)?twitter\\.com/"},prehideSelectors:['[data-testid="BottomBar"]'],detectCmp:[{exists:'[data-testid="BottomBar"] div'}],detectPopup:[{visible:'[data-testid="BottomBar"] div'}],optIn:[{waitForThenClick:'[data-testid="BottomBar"] > div:has(>div:first-child>div:last-child>span[role=button]) > div:last-child > div[role=button]:first-child'}],optOut:[{waitForThenClick:'[data-testid="BottomBar"] > div:has(>div:first-child>div:last-child>span[role=button]) > div:last-child > div[role=button]:last-child'}],TODOtest:[{eval:"EVAL_document.cookie.includes('d_prefs=MjoxLGNvbnNlbnRfdmVyc2lvbjoy')"}]},{name:"ubuntu.com",prehideSelectors:["dialog.cookie-policy"],detectCmp:[{any:[{exists:"dialog.cookie-policy header"},{exists:'xpath///*[@id="modal"]/div/header'}]}],detectPopup:[{any:[{visible:"dialog header"},{visible:'xpath///*[@id="modal"]/div/header'}]}],optIn:[{any:[{waitForThenClick:"#cookie-policy-button-accept"},{waitForThenClick:'xpath///*[@id="cookie-policy-button-accept"]'}]}],optOut:[{any:[{waitForThenClick:"button.js-manage"},{waitForThenClick:'xpath///*[@id="cookie-policy-content"]/p[4]/button[2]'}]},{waitForThenClick:"dialog.cookie-policy .p-switch__input:checked",optional:!0,all:!0,timeout:500},{any:[{waitForThenClick:"dialog.cookie-policy .js-save-preferences"},{waitForThenClick:'xpath///*[@id="modal"]/div/button'}]}],test:[{eval:"EVAL_UBUNTU_COM_0"}]},{name:"UK Cookie Consent",prehideSelectors:["#catapult-cookie-bar"],cosmetic:!0,detectCmp:[{exists:"#catapult-cookie-bar"}],detectPopup:[{exists:".has-cookie-bar #catapult-cookie-bar"}],optIn:[{click:"#catapultCookie"}],optOut:[{hide:"#catapult-cookie-bar"}],test:[{eval:"EVAL_UK_COOKIE_CONSENT_0"}]},{name:"urbanarmorgear-com",cosmetic:!0,prehideSelectors:['div[class^="Layout__CookieBannerContainer-"]'],detectCmp:[{exists:'div[class^="Layout__CookieBannerContainer-"]'}],detectPopup:[{visible:'div[class^="Layout__CookieBannerContainer-"]'}],optIn:[{click:'button[class^="CookieBanner__AcceptButton"]'}],optOut:[{hide:'div[class^="Layout__CookieBannerContainer-"]'}]},{name:"usercentrics-api",detectCmp:[{exists:"#usercentrics-root"}],detectPopup:[{eval:"EVAL_USERCENTRICS_API_0"},{exists:["#usercentrics-root","[data-testid=uc-container]"]},{waitForVisible:"#usercentrics-root",timeout:2e3}],optIn:[{eval:"EVAL_USERCENTRICS_API_3"},{eval:"EVAL_USERCENTRICS_API_1"},{eval:"EVAL_USERCENTRICS_API_5"}],optOut:[{eval:"EVAL_USERCENTRICS_API_1"},{eval:"EVAL_USERCENTRICS_API_2"}],test:[{eval:"EVAL_USERCENTRICS_API_6"}]},{name:"usercentrics-button",detectCmp:[{exists:"#usercentrics-button"}],detectPopup:[{visible:"#usercentrics-button #uc-btn-accept-banner"}],optIn:[{click:"#usercentrics-button #uc-btn-accept-banner"}],optOut:[{click:"#usercentrics-button #uc-btn-deny-banner"}],test:[{eval:"EVAL_USERCENTRICS_BUTTON_0"}]},{name:"uswitch.com",prehideSelectors:["#cookie-banner-wrapper"],detectCmp:[{exists:"#cookie-banner-wrapper"}],detectPopup:[{visible:"#cookie-banner-wrapper"}],optIn:[{click:"#cookie_banner_accept_mobile"}],optOut:[{click:"#cookie_banner_save"}]},{name:"vodafone.de",runContext:{urlPattern:"^https://www\\.vodafone\\.de/"},prehideSelectors:[".dip-consent,.dip-consent-container"],detectCmp:[{exists:".dip-consent-container"}],detectPopup:[{visible:".dip-consent-content"}],optOut:[{click:'.dip-consent-btn[tabindex="2"]'}],optIn:[{click:'.dip-consent-btn[tabindex="1"]'}]},{name:"waitrose.com",prehideSelectors:["div[aria-labelledby=CookieAlertModalHeading]","section[data-test=initial-waitrose-cookie-consent-banner]","section[data-test=cookie-consent-modal]"],detectCmp:[{exists:"section[data-test=initial-waitrose-cookie-consent-banner]"}],detectPopup:[{visible:"section[data-test=initial-waitrose-cookie-consent-banner]"}],optIn:[{click:"button[data-test=accept-all]"}],optOut:[{click:"button[data-test=manage-cookies]"},{wait:200},{eval:"EVAL_WAITROSE_0"},{click:"button[data-test=submit]"}],test:[{eval:"EVAL_WAITROSE_1"}]},{name:"webflow",vendorUrl:"https://webflow.com/",prehideSelectors:[".fs-cc-components"],detectCmp:[{exists:".fs-cc-components"}],detectPopup:[{visible:".fs-cc-components"},{visible:"[fs-cc=banner]"}],optIn:[{wait:500},{waitForThenClick:"[fs-cc=banner] [fs-cc=allow]"}],optOut:[{wait:500},{waitForThenClick:"[fs-cc=banner] [fs-cc=deny]"}]},{name:"wetransfer.com",detectCmp:[{exists:".welcome__cookie-notice"}],detectPopup:[{visible:".welcome__cookie-notice"}],optIn:[{click:".welcome__button--accept"}],optOut:[{click:".welcome__button--decline"}]},{name:"whitepages.com",runContext:{urlPattern:"^https://www\\.whitepages\\.com/"},cosmetic:!0,prehideSelectors:[".cookie-wrapper, .cookie-overlay"],detectCmp:[{exists:".cookie-wrapper"}],detectPopup:[{visible:".cookie-overlay"}],optIn:[{click:'button[aria-label="Got it"]'}],optOut:[{hide:".cookie-wrapper"}]},{name:"wolframalpha",vendorUrl:"https://www.wolframalpha.com",prehideSelectors:[],cosmetic:!0,runContext:{urlPattern:"^https://www\\.wolframalpha\\.com/"},detectCmp:[{exists:"section._a_yb"}],detectPopup:[{visible:"section._a_yb"}],optIn:[{waitForThenClick:"section._a_yb button"}],optOut:[{hide:"section._a_yb"}]},{name:"woo-commerce-com",prehideSelectors:[".wccom-comp-privacy-banner .wccom-privacy-banner"],detectCmp:[{exists:".wccom-comp-privacy-banner .wccom-privacy-banner"}],detectPopup:[{exists:".wccom-comp-privacy-banner .wccom-privacy-banner"}],optIn:[{click:".wccom-privacy-banner__content-buttons button.is-primary"}],optOut:[{click:".wccom-privacy-banner__content-buttons button.is-secondary"},{waitForThenClick:"input[type=checkbox][checked]:not([disabled])",all:!0},{click:"div.wccom-modal__footer > button"}]},{name:"WP Cookie Notice for GDPR",vendorUrl:"https://wordpress.org/plugins/gdpr-cookie-consent/",prehideSelectors:["#gdpr-cookie-consent-bar"],detectCmp:[{exists:"#gdpr-cookie-consent-bar"}],detectPopup:[{visible:"#gdpr-cookie-consent-bar"}],optIn:[{waitForThenClick:"#gdpr-cookie-consent-bar #cookie_action_accept"}],optOut:[{waitForThenClick:"#gdpr-cookie-consent-bar #cookie_action_reject"}],test:[{eval:"EVAL_WP_COOKIE_NOTICE_0"}]},{name:"wpcc",cosmetic:!0,prehideSelectors:[".wpcc-container"],detectCmp:[{exists:".wpcc-container"}],detectPopup:[{exists:".wpcc-container .wpcc-message"}],optIn:[{click:".wpcc-compliance .wpcc-btn"}],optOut:[{hide:".wpcc-container"}]},{name:"xe.com",vendorUrl:"https://www.xe.com/",runContext:{urlPattern:"^https://www\\.xe\\.com/"},prehideSelectors:["[class*=ConsentBanner]"],detectCmp:[{exists:"[class*=ConsentBanner]"}],detectPopup:[{visible:"[class*=ConsentBanner]"}],optIn:[{waitForThenClick:"[class*=ConsentBanner] .egnScw"}],optOut:[{wait:1e3},{waitForThenClick:"[class*=ConsentBanner] .frDWEu"},{waitForThenClick:"[class*=ConsentBanner] .hXIpFU"}],test:[{eval:"EVAL_XE_TEST"}]},{name:"xhamster-eu",prehideSelectors:[".cookies-modal"],detectCmp:[{exists:".cookies-modal"}],detectPopup:[{exists:".cookies-modal"}],optIn:[{click:"button.cmd-button-accept-all"}],optOut:[{click:"button.cmd-button-reject-all"}]},{name:"xhamster-us",runContext:{urlPattern:"^https://(www\\.)?xhamster\\d?\\.com"},cosmetic:!0,prehideSelectors:[".cookie-announce"],detectCmp:[{exists:".cookie-announce"}],detectPopup:[{visible:".cookie-announce .announce-text"}],optIn:[{click:".cookie-announce button.xh-button"}],optOut:[{hide:".cookie-announce"}]},{name:"xing.com",detectCmp:[{exists:"div[class^=cookie-consent-CookieConsent]"}],detectPopup:[{exists:"div[class^=cookie-consent-CookieConsent]"}],optIn:[{click:"#consent-accept-button"}],optOut:[{click:"#consent-settings-button"},{click:".consent-banner-button-accept-overlay"}],test:[{eval:"EVAL_XING_0"}]},{name:"xnxx-com",cosmetic:!0,prehideSelectors:["#cookies-use-alert"],detectCmp:[{exists:"#cookies-use-alert"}],detectPopup:[{visible:"#cookies-use-alert"}],optIn:[{click:"#cookies-use-alert .close"}],optOut:[{hide:"#cookies-use-alert"}]},{name:"xvideos",vendorUrl:"https://xvideos.com",runContext:{urlPattern:"^https://[^/]*xvideos\\.com/"},prehideSelectors:[],detectCmp:[{exists:".disclaimer-opened #disclaimer-cookies"}],detectPopup:[{visible:".disclaimer-opened #disclaimer-cookies"}],optIn:[{waitForThenClick:"#disclaimer-accept_cookies"}],optOut:[{waitForThenClick:"#disclaimer-reject_cookies"}]},{name:"Yahoo",runContext:{urlPattern:"^https://consent\\.yahoo\\.com/v2/"},prehideSelectors:["#reject-all"],detectCmp:[{exists:"#consent-page"}],detectPopup:[{visible:"#consent-page"}],optIn:[{waitForThenClick:"#consent-page button[value=agree]"}],optOut:[{waitForThenClick:"#consent-page button[value=reject]"}]},{name:"youporn.com",cosmetic:!0,prehideSelectors:[".euCookieModal, #js_euCookieModal"],detectCmp:[{exists:".euCookieModal"}],detectPopup:[{exists:".euCookieModal, #js_euCookieModal"}],optIn:[{click:'button[name="user_acceptCookie"]'}],optOut:[{hide:".euCookieModal"}]},{name:"youtube-desktop",prehideSelectors:["tp-yt-iron-overlay-backdrop.opened","ytd-consent-bump-v2-lightbox"],detectCmp:[{exists:"ytd-consent-bump-v2-lightbox tp-yt-paper-dialog"},{exists:'ytd-consent-bump-v2-lightbox tp-yt-paper-dialog a[href^="https://consent.youtube.com/"]'}],detectPopup:[{visible:"ytd-consent-bump-v2-lightbox tp-yt-paper-dialog"}],optIn:[{waitForThenClick:"ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:last-child #button,ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:last-child button"},{wait:500}],optOut:[{waitForThenClick:"ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:first-child #button,ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:first-child button"},{wait:500}],test:[{wait:500},{eval:"EVAL_YOUTUBE_DESKTOP_0"}]},{name:"youtube-mobile",prehideSelectors:[".consent-bump-v2-lightbox"],detectCmp:[{exists:"ytm-consent-bump-v2-renderer"}],detectPopup:[{visible:"ytm-consent-bump-v2-renderer"}],optIn:[{waitForThenClick:"ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons c3-material-button:first-child button, ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons ytm-button-renderer:first-child button"},{wait:500}],optOut:[{waitForThenClick:"ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons c3-material-button:nth-child(2) button, ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons ytm-button-renderer:nth-child(2) button"},{wait:500}],test:[{wait:500},{eval:"EVAL_YOUTUBE_MOBILE_0"}]},{name:"zdf",prehideSelectors:["#zdf-cmp-banner-sdk"],detectCmp:[{exists:"#zdf-cmp-banner-sdk"}],detectPopup:[{visible:"#zdf-cmp-main.zdf-cmp-show"}],optIn:[{waitForThenClick:"#zdf-cmp-main #zdf-cmp-accept-btn"}],optOut:[{waitForThenClick:"#zdf-cmp-main #zdf-cmp-deny-btn"}],test:[]}],C={"didomi.io":{detectors:[{presentMatcher:{target:{selector:"#didomi-host, #didomi-notice"},type:"css"},showingMatcher:{target:{selector:"body.didomi-popup-open, .didomi-notice-banner"},type:"css"}}],methods:[{action:{target:{selector:".didomi-popup-notice-buttons .didomi-button:not(.didomi-button-highlight), .didomi-notice-banner .didomi-learn-more-button"},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{retries:50,target:{selector:"#didomi-purpose-cookies"},type:"waitcss",waitTime:50},{consents:[{description:"Share (everything) with others",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-share_whith_others]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-share_whith_others]:last-child"},type:"click"},type:"X"},{description:"Information storage and access",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-cookies]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-cookies]:last-child"},type:"click"},type:"D"},{description:"Content selection, offers and marketing",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-CL-T1Rgm7]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-CL-T1Rgm7]:last-child"},type:"click"},type:"E"},{description:"Analytics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-analytics]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-analytics]:last-child"},type:"click"},type:"B"},{description:"Analytics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-M9NRHJe3G]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-M9NRHJe3G]:last-child"},type:"click"},type:"B"},{description:"Ad and content selection",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-advertising_personalization]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-advertising_personalization]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection",falseAction:{parent:{childFilter:{target:{selector:"#didomi-purpose-pub-ciblee"}},selector:".didomi-consent-popup-data-processing, .didomi-components-accordion-label-container"},target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-pub-ciblee]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-pub-ciblee]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - basics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-q4zlJqdcD]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-q4zlJqdcD]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - partners and subsidiaries",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-partenaire-cAsDe8jC]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-partenaire-cAsDe8jC]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-p4em9a8m]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-p4em9a8m]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - others",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-autres-pub]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-autres-pub]:last-child"},type:"click"},type:"F"},{description:"Social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-reseauxsociaux]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-reseauxsociaux]:last-child"},type:"click"},type:"A"},{description:"Social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-social_media]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-social_media]:last-child"},type:"click"},type:"A"},{description:"Content selection",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-content_personalization]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-content_personalization]:last-child"},type:"click"},type:"E"},{description:"Ad delivery",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-ad_delivery]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-ad_delivery]:last-child"},type:"click"},type:"F"}],type:"consent"},{action:{consents:[{matcher:{childFilter:{target:{selector:":not(.didomi-components-radio__option--selected)"}},type:"css"},trueAction:{target:{selector:":nth-child(2)"},type:"click"},falseAction:{target:{selector:":first-child"},type:"click"},type:"X"}],type:"consent"},target:{selector:".didomi-components-radio"},type:"foreach"}],type:"list"},name:"DO_CONSENT"},{action:{parent:{selector:".didomi-consent-popup-footer .didomi-consent-popup-actions"},target:{selector:".didomi-components-button:first-child"},type:"click"},name:"SAVE_CONSENT"}]},oil:{detectors:[{presentMatcher:{target:{selector:".as-oil-content-overlay"},type:"css"},showingMatcher:{target:{selector:".as-oil-content-overlay"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".as-js-advanced-settings"},type:"click"},{retries:"10",target:{selector:".as-oil-cpc__purpose-container"},type:"waitcss",waitTime:"250"}],type:"list"},name:"OPEN_OPTIONS"},{action:{actions:[{consents:[{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Information storage and access","Opbevaring af og adgang til oplysninger på din enhed"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Information storage and access","Opbevaring af og adgang til oplysninger på din enhed"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"D"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personlige annoncer","Personalisation"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personlige annoncer","Personalisation"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"E"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Annoncevalg, levering og rapportering","Ad selection, delivery, reporting"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Annoncevalg, levering og rapportering","Ad selection, delivery, reporting"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"F"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personalisering af indhold","Content selection, delivery, reporting"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personalisering af indhold","Content selection, delivery, reporting"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"E"},{matcher:{parent:{childFilter:{target:{selector:".as-oil-cpc__purpose-header",textFilter:["Måling","Measurement"]}},selector:".as-oil-cpc__purpose-container"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{childFilter:{target:{selector:".as-oil-cpc__purpose-header",textFilter:["Måling","Measurement"]}},selector:".as-oil-cpc__purpose-container"},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"B"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:"Google"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:"Google"},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"F"}],type:"consent"}],type:"list"},name:"DO_CONSENT"},{action:{target:{selector:".as-oil__btn-optin"},type:"click"},name:"SAVE_CONSENT"},{action:{target:{selector:"div.as-oil"},type:"hide"},name:"HIDE_CMP"}]},optanon:{detectors:[{presentMatcher:{target:{selector:"#optanon-menu, .optanon-alert-box-wrapper"},type:"css"},showingMatcher:{target:{displayFilter:!0,selector:".optanon-alert-box-wrapper"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".optanon-alert-box-wrapper .optanon-toggle-display, a[onclick*='OneTrust.ToggleInfoDisplay()'], a[onclick*='Optanon.ToggleInfoDisplay()']"},type:"click"}],type:"list"},name:"OPEN_OPTIONS"},{action:{actions:[{target:{selector:".preference-menu-item #Your-privacy"},type:"click"},{target:{selector:"#optanon-vendor-consent-text"},type:"click"},{action:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},target:{selector:"#optanon-vendor-consent-list .vendor-item"},type:"foreach"},{target:{selector:".vendor-consent-back-link"},type:"click"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-performance"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-performance"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-functional"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-functional"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-advertising"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-advertising"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-social"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-social"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Social Media Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Social Media Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalisation"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalisation"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Site monitoring cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Site monitoring cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Third party privacy-enhanced content"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Third party privacy-enhanced content"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Performance & Advertising Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Performance & Advertising Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Information storage and access"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Information storage and access"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"D"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad selection, delivery, reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad selection, delivery, reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content selection, delivery, reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content selection, delivery, reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Measurement"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Measurement"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Recommended Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Recommended Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Unclassified Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Unclassified Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Analytical Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Analytical Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Marketing Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Marketing Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalization"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalization"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad Selection, Delivery & Reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad Selection, Delivery & Reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content Selection, Delivery & Reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content Selection, Delivery & Reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"}],type:"list"},name:"DO_CONSENT"},{action:{parent:{selector:".optanon-save-settings-button"},target:{selector:".optanon-white-button-middle"},type:"click"},name:"SAVE_CONSENT"},{action:{actions:[{target:{selector:"#optanon-popup-wrapper"},type:"hide"},{target:{selector:"#optanon-popup-bg"},type:"hide"},{target:{selector:".optanon-alert-box-wrapper"},type:"hide"}],type:"list"},name:"HIDE_CMP"}]},quantcast2:{detectors:[{presentMatcher:{target:{selector:"[data-tracking-opt-in-overlay]"},type:"css"},showingMatcher:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-learn-more]"},type:"css"}}],methods:[{action:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-learn-more]"},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{type:"wait",waitTime:500},{action:{actions:[{target:{selector:"div",textFilter:["Information storage and access"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"D"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Personalization"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"F"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Ad selection, delivery, reporting"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"F"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Content selection, delivery, reporting"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"E"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Measurement"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"B"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Other Partners"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},type:"ifcss"}],type:"list"},parent:{childFilter:{target:{selector:"input"}},selector:"[data-tracking-opt-in-overlay] > div > div"},target:{childFilter:{target:{selector:"input"}},selector:":scope > div"},type:"foreach"}],type:"list"},name:"DO_CONSENT"},{action:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-save]"},type:"click"},name:"SAVE_CONSENT"}]},springer:{detectors:[{presentMatcher:{parent:null,target:{selector:".cmp-app_gdpr"},type:"css"},showingMatcher:{parent:null,target:{displayFilter:!0,selector:".cmp-popup_popup"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".cmp-intro_rejectAll"},type:"click"},{type:"wait",waitTime:250},{target:{selector:".cmp-purposes_purposeItem:not(.cmp-purposes_selectedPurpose)"},type:"click"}],type:"list"},name:"OPEN_OPTIONS"},{action:{consents:[{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Przechowywanie informacji na urządzeniu lub dostęp do nich",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Przechowywanie informacji na urządzeniu lub dostęp do nich",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"D"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór podstawowych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór podstawowych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"F"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"F"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"E"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"E"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Stosowanie badań rynkowych w celu generowania opinii odbiorców",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Stosowanie badań rynkowych w celu generowania opinii odbiorców",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"X"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Opracowywanie i ulepszanie produktów",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Opracowywanie i ulepszanie produktów",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"X"}],type:"consent"},name:"DO_CONSENT"},{action:{target:{selector:".cmp-details_save"},type:"click"},name:"SAVE_CONSENT"}]},wordpressgdpr:{detectors:[{presentMatcher:{parent:null,target:{selector:".wpgdprc-consent-bar"},type:"css"},showingMatcher:{parent:null,target:{displayFilter:!0,selector:".wpgdprc-consent-bar"},type:"css"}}],methods:[{action:{parent:null,target:{selector:".wpgdprc-consent-bar .wpgdprc-consent-bar__settings",textFilter:null},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{target:{selector:".wpgdprc-consent-modal .wpgdprc-button",textFilter:"Eyeota"},type:"click"},{consents:[{description:"Eyeota Cookies",matcher:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Eyeota"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Eyeota"},target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},{target:{selector:".wpgdprc-consent-modal .wpgdprc-button",textFilter:"Advertising"},type:"click"},{consents:[{description:"Advertising Cookies",matcher:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Advertising"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Advertising"},target:{selector:"label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},name:"DO_CONSENT"},{action:{parent:null,target:{selector:".wpgdprc-button",textFilter:"Save my settings"},type:"click"},name:"SAVE_CONSENT"}]}},v={autoconsent:w,consentomatic:C},f=Object.freeze({__proto__:null,autoconsent:w,consentomatic:C,default:v});const A=new class{constructor(e,t=null,o=null){if(this.id=n(),this.rules=[],this.foundCmp=null,this.state={lifecycle:"loading",prehideOn:!1,findCmpAttempts:0,detectedCmps:[],detectedPopups:[],selfTest:null},a.sendContentMessage=e,this.sendContentMessage=e,this.rules=[],this.updateState({lifecycle:"loading"}),this.addDynamicRules(),t)this.initialize(t,o);else{o&&this.parseDeclarativeRules(o);e({type:"init",url:window.location.href}),this.updateState({lifecycle:"waitingForInitResponse"})}this.domActions=new class{constructor(e){this.autoconsentInstance=e}click(e,t=!1){const o=this.elementSelector(e);return this.autoconsentInstance.config.logs.rulesteps&&console.log("[click]",e,t,o),o.length>0&&(t?o.forEach((e=>e.click())):o[0].click()),o.length>0}elementExists(e){return this.elementSelector(e).length>0}elementVisible(e,t){const o=this.elementSelector(e),c=new Array(o.length);return o.forEach(((e,t)=>{c[t]=k(e)})),"none"===t?c.every((e=>!e)):0!==c.length&&("any"===t?c.some((e=>e)):c.every((e=>e)))}waitForElement(e,t=1e4){const o=Math.ceil(t/200);return this.autoconsentInstance.config.logs.rulesteps&&console.log("[waitForElement]",e),h((()=>this.elementSelector(e).length>0),o,200)}waitForVisible(e,t=1e4,o="any"){return h((()=>this.elementVisible(e,o)),Math.ceil(t/200),200)}async waitForThenClick(e,t=1e4,o=!1){return await this.waitForElement(e,t),this.click(e,o)}wait(e){return new Promise((t=>{setTimeout((()=>{t(!0)}),e)}))}hide(e,t){return m(u(),e,t)}prehide(e){const t=u("autoconsent-prehide");return this.autoconsentInstance.config.logs.lifecycle&&console.log("[prehide]",t,location.href),m(t,e,"opacity")}undoPrehide(){const e=u("autoconsent-prehide");return this.autoconsentInstance.config.logs.lifecycle&&console.log("[undoprehide]",e,location.href),e&&e.remove(),!!e}querySingleReplySelector(e,t=document){if(e.startsWith("aria/"))return[];if(e.startsWith("xpath/")){const o=e.slice(6),c=document.evaluate(o,t,null,XPathResult.ANY_TYPE,null);let i=null;const n=[];for(;i=c.iterateNext();)n.push(i);return n}return e.startsWith("text/")||e.startsWith("pierce/")?[]:t.shadowRoot?Array.from(t.shadowRoot.querySelectorAll(e)):Array.from(t.querySelectorAll(e))}querySelectorChain(e){let t,o=document;for(const c of e){if(t=this.querySingleReplySelector(c,o),0===t.length)return[];o=t[0]}return t}elementSelector(e){return"string"==typeof e?this.querySingleReplySelector(e):this.querySelectorChain(e)}}(this)}initialize(e,t){const o=b(e);if(o.logs.lifecycle&&console.log("autoconsent init",window.location.href),this.config=o,o.enabled){if(t&&this.parseDeclarativeRules(t),this.rules=function(e,t){return e.filter((e=>(!t.disabledCmps||!t.disabledCmps.includes(e.name))&&(t.enableCosmeticRules||!e.isCosmetic)))}(this.rules,o),e.enablePrehide)if(document.documentElement)this.prehideElements();else{const e=()=>{window.removeEventListener("DOMContentLoaded",e),this.prehideElements()};window.addEventListener("DOMContentLoaded",e)}if("loading"===document.readyState){const e=()=>{window.removeEventListener("DOMContentLoaded",e),this.start()};window.addEventListener("DOMContentLoaded",e)}else this.start();this.updateState({lifecycle:"initialized"})}else o.logs.lifecycle&&console.log("autoconsent is disabled")}addDynamicRules(){y.forEach((e=>{this.rules.push(new e(this))}))}parseDeclarativeRules(e){Object.keys(e.consentomatic).forEach((t=>{this.addConsentomaticCMP(t,e.consentomatic[t])})),e.autoconsent.forEach((e=>{this.addDeclarativeCMP(e)}))}addDeclarativeCMP(e){this.rules.push(new d(e,this))}addConsentomaticCMP(e,t){this.rules.push(new class{constructor(e,t){this.name=e,this.config=t,this.methods=new Map,this.runContext=l,this.isCosmetic=!1,t.methods.forEach((e=>{e.action&&this.methods.set(e.name,e.action)})),this.hasSelfTest=!1}get isIntermediate(){return!1}checkRunContext(){return!0}async detectCmp(){return this.config.detectors.map((e=>o(e.presentMatcher))).some((e=>!!e))}async detectPopup(){return this.config.detectors.map((e=>o(e.showingMatcher))).some((e=>!!e))}async executeAction(e,t){return!this.methods.has(e)||c(this.methods.get(e),t)}async optOut(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),await this.executeAction("HIDE_CMP"),await this.executeAction("DO_CONSENT",[]),await this.executeAction("SAVE_CONSENT"),!0}async optIn(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),await this.executeAction("HIDE_CMP"),await this.executeAction("DO_CONSENT",["D","A","B","E","F","X"]),await this.executeAction("SAVE_CONSENT"),!0}async openCmp(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),!0}async test(){return!0}}(`com_${e}`,t))}start(){window.requestIdleCallback?window.requestIdleCallback((()=>this._start()),{timeout:500}):this._start()}async _start(){const e=this.config.logs;e.lifecycle&&console.log(`Detecting CMPs on ${window.location.href}`),this.updateState({lifecycle:"started"});const t=await this.findCmp(this.config.detectRetries);if(this.updateState({detectedCmps:t.map((e=>e.name))}),0===t.length)return e.lifecycle&&console.log("no CMP found",location.href),this.config.enablePrehide&&this.undoPrehide(),this.updateState({lifecycle:"nothingDetected"}),!1;this.updateState({lifecycle:"cmpDetected"});const o=[],c=[];for(const e of t)e.isCosmetic?c.push(e):o.push(e);let i=!1,n=await this.detectPopups(o,(async e=>{i=await this.handlePopup(e)}));if(0===n.length&&(n=await this.detectPopups(c,(async e=>{i=await this.handlePopup(e)}))),0===n.length)return e.lifecycle&&console.log("no popup found"),this.config.enablePrehide&&this.undoPrehide(),!1;if(n.length>1){const t={msg:"Found multiple CMPs, check the detection rules.",cmps:n.map((e=>e.name))};e.errors&&console.warn(t.msg,t.cmps),this.sendContentMessage({type:"autoconsentError",details:t})}return i}async findCmp(e){const t=this.config.logs;this.updateState({findCmpAttempts:this.state.findCmpAttempts+1});const o=[];for(const e of this.rules)try{if(!e.checkRunContext())continue;await e.detectCmp()&&(t.lifecycle&&console.log(`Found CMP: ${e.name} ${window.location.href}`),this.sendContentMessage({type:"cmpDetected",url:location.href,cmp:e.name}),o.push(e))}catch(o){t.errors&&console.warn(`error detecting ${e.name}`,o)}return 0===o.length&&e>0?(await this.domActions.wait(500),this.findCmp(e-1)):o}async detectPopup(e){if(await this.waitForPopup(e).catch((t=>(this.config.logs.errors&&console.warn(`error waiting for a popup for ${e.name}`,t),!1))))return this.updateState({detectedPopups:this.state.detectedPopups.concat([e.name])}),this.sendContentMessage({type:"popupFound",cmp:e.name,url:location.href}),e;throw new Error("Popup is not shown")}async detectPopups(e,t){const o=e.map((e=>this.detectPopup(e)));await Promise.any(o).then((e=>{t(e)})).catch((()=>null));const c=await Promise.allSettled(o),i=[];for(const e of c)"fulfilled"===e.status&&i.push(e.value);return i}async handlePopup(e){return this.updateState({lifecycle:"openPopupDetected"}),this.config.enablePrehide&&!this.state.prehideOn&&this.prehideElements(),this.foundCmp=e,"optOut"===this.config.autoAction?await this.doOptOut():"optIn"===this.config.autoAction?await this.doOptIn():(this.config.logs.lifecycle&&console.log("waiting for opt-out signal...",location.href),!0)}async doOptOut(){const e=this.config.logs;let t;return this.updateState({lifecycle:"runningOptOut"}),this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: opt out on ${window.location.href}`),t=await this.foundCmp.optOut(),e.lifecycle&&console.log(`${this.foundCmp.name}: opt out result ${t}`)):(e.errors&&console.log("no CMP to opt out"),t=!1),this.config.enablePrehide&&this.undoPrehide(),this.sendContentMessage({type:"optOutResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,scheduleSelfTest:this.foundCmp&&this.foundCmp.hasSelfTest,url:location.href}),t&&!this.foundCmp.isIntermediate?(this.sendContentMessage({type:"autoconsentDone",cmp:this.foundCmp.name,isCosmetic:this.foundCmp.isCosmetic,url:location.href}),this.updateState({lifecycle:"done"})):this.updateState({lifecycle:t?"optOutSucceeded":"optOutFailed"}),t}async doOptIn(){const e=this.config.logs;let t;return this.updateState({lifecycle:"runningOptIn"}),this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: opt in on ${window.location.href}`),t=await this.foundCmp.optIn(),e.lifecycle&&console.log(`${this.foundCmp.name}: opt in result ${t}`)):(e.errors&&console.log("no CMP to opt in"),t=!1),this.config.enablePrehide&&this.undoPrehide(),this.sendContentMessage({type:"optInResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,scheduleSelfTest:!1,url:location.href}),t&&!this.foundCmp.isIntermediate?(this.sendContentMessage({type:"autoconsentDone",cmp:this.foundCmp.name,isCosmetic:this.foundCmp.isCosmetic,url:location.href}),this.updateState({lifecycle:"done"})):this.updateState({lifecycle:t?"optInSucceeded":"optInFailed"}),t}async doSelfTest(){const e=this.config.logs;let t;return this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: self-test on ${window.location.href}`),t=await this.foundCmp.test()):(e.errors&&console.log("no CMP to self test"),t=!1),this.sendContentMessage({type:"selfTestResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,url:location.href}),this.updateState({selfTest:t}),t}async waitForPopup(e,t=5,o=500){const c=this.config.logs;c.lifecycle&&console.log("checking if popup is open...",e.name);const i=await e.detectPopup().catch((t=>(c.errors&&console.warn(`error detecting popup for ${e.name}`,t),!1)));return!i&&t>0?(await this.domActions.wait(o),this.waitForPopup(e,t-1,o)):(c.lifecycle&&console.log(e.name,"popup is "+(i?"open":"not open")),i)}prehideElements(){const e=this.config.logs,t=this.rules.filter((e=>e.prehideSelectors&&e.checkRunContext())).reduce(((e,t)=>[...e,...t.prehideSelectors]),["#didomi-popup,.didomi-popup-container,.didomi-popup-notice,.didomi-consent-popup-preferences,#didomi-notice,.didomi-popup-backdrop,.didomi-screen-medium"]);return this.updateState({prehideOn:!0}),setTimeout((()=>{this.config.enablePrehide&&this.state.prehideOn&&!["runningOptOut","runningOptIn"].includes(this.state.lifecycle)&&(e.lifecycle&&console.log("Process is taking too long, unhiding elements"),this.undoPrehide())}),this.config.prehideTimeout||2e3),this.domActions.prehide(t.join(","))}undoPrehide(){return this.updateState({prehideOn:!1}),this.domActions.undoPrehide()}updateState(e){Object.assign(this.state,e),this.sendContentMessage({type:"report",instanceId:this.id,url:window.location.href,mainFrame:window.top===window.self,state:this.state})}async receiveMessageCallback(e){const t=this.config?.logs;switch(t?.messages&&console.log("received from background",e,window.location.href),e.type){case"initResp":this.initialize(e.config,e.rules);break;case"optIn":await this.doOptIn();break;case"optOut":await this.doOptOut();break;case"selfTest":await this.doSelfTest();break;case"evalResp":!function(e,t){const o=a.pending.get(e);o?(a.pending.delete(e),o.timer&&window.clearTimeout(o.timer),o.resolve(t)):console.warn("no eval #",e)}(e.id,e.result)}}}((e=>{window.webkit.messageHandlers[e.type]&&window.webkit.messageHandlers[e.type].postMessage(e).then((e=>{A.receiveMessageCallback(e)}))}),null,f);window.autoconsentMessageCallback=e=>{A.receiveMessageCallback(e)}}(); +!function(){"use strict";var e=class e{static setBase(t){e.base=t}static findElement(t,o=null,c=!1){let i=null;return i=null!=o?Array.from(o.querySelectorAll(t.selector)):null!=e.base?Array.from(e.base.querySelectorAll(t.selector)):Array.from(document.querySelectorAll(t.selector)),null!=t.textFilter&&(i=i.filter((e=>{const o=e.textContent.toLowerCase();if(Array.isArray(t.textFilter)){let e=!1;for(const c of t.textFilter)if(-1!==o.indexOf(c.toLowerCase())){e=!0;break}return e}if(null!=t.textFilter)return-1!==o.indexOf(t.textFilter.toLowerCase())}))),null!=t.styleFilters&&(i=i.filter((e=>{const o=window.getComputedStyle(e);let c=!0;for(const e of t.styleFilters){const t=o[e.option];c=e.negated?c&&t!==e.value:c&&t===e.value}return c}))),null!=t.displayFilter&&(i=i.filter((e=>t.displayFilter?0!==e.offsetHeight:0===e.offsetHeight))),null!=t.iframeFilter&&(i=i.filter((()=>t.iframeFilter?window.location!==window.parent.location:window.location===window.parent.location))),null!=t.childFilter&&(i=i.filter((o=>{const c=e.base;e.setBase(o);const i=e.find(t.childFilter);return e.setBase(c),null!=i.target}))),c?i:(i.length>1&&console.warn("Multiple possible targets: ",i,t,o),i[0])}static find(t,o=!1){const c=[];if(null!=t.parent){const i=e.findElement(t.parent,null,o);if(null!=i){if(i instanceof Array)return i.forEach((i=>{const n=e.findElement(t.target,i,o);n instanceof Array?n.forEach((e=>{c.push({parent:i,target:e})})):c.push({parent:i,target:n})})),c;{const n=e.findElement(t.target,i,o);n instanceof Array?n.forEach((e=>{c.push({parent:i,target:e})})):c.push({parent:i,target:n})}}}else{const i=e.findElement(t.target,null,o);i instanceof Array?i.forEach((e=>{c.push({parent:null,target:e})})):c.push({parent:null,target:i})}return 0===c.length&&c.push({parent:null,target:null}),o?c:(1!==c.length&&console.warn("Multiple results found, even though multiple false",c),c[0])}};e.base=null;var t=e;function o(e){const o=t.find(e);return"css"===e.type?!!o.target:"checkbox"===e.type?!!o.target&&o.target.checked:void 0}async function c(e,n){switch(e.type){case"click":return async function(e){const o=t.find(e);null!=o.target&&o.target.click();return i(0)}(e);case"list":return async function(e,t){for(const o of e.actions)await c(o,t)}(e,n);case"consent":return async function(e,t){for(const i of e.consents){const e=-1!==t.indexOf(i.type);if(i.matcher&&i.toggleAction){o(i.matcher)!==e&&await c(i.toggleAction)}else e?await c(i.trueAction):await c(i.falseAction)}}(e,n);case"ifcss":return async function(e,o){const i=t.find(e);i.target?e.falseAction&&await c(e.falseAction,o):e.trueAction&&await c(e.trueAction,o)}(e,n);case"waitcss":return async function(e){await new Promise((o=>{let c=e.retries||10;const i=e.waitTime||250,n=()=>{const a=t.find(e);(e.negated&&a.target||!e.negated&&!a.target)&&c>0?(c-=1,setTimeout(n,i)):o()};n()}))}(e);case"foreach":return async function(e,o){const i=t.find(e,!0),n=t.base;for(const n of i)n.target&&(t.setBase(n.target),await c(e.action,o));t.setBase(n)}(e,n);case"hide":return async function(e){const o=t.find(e);o.target&&o.target.classList.add("Autoconsent-Hidden")}(e);case"slide":return async function(e){const o=t.find(e),c=t.find(e.dragTarget);if(o.target){const e=o.target.getBoundingClientRect(),t=c.target.getBoundingClientRect();let i=t.top-e.top,n=t.left-e.left;"y"===this.config.axis.toLowerCase()&&(n=0),"x"===this.config.axis.toLowerCase()&&(i=0);const a=window.screenX+e.left+e.width/2,s=window.screenY+e.top+e.height/2,r=e.left+e.width/2,l=e.top+e.height/2,p=document.createEvent("MouseEvents");p.initMouseEvent("mousedown",!0,!0,window,0,a,s,r,l,!1,!1,!1,!1,0,o.target);const d=document.createEvent("MouseEvents");d.initMouseEvent("mousemove",!0,!0,window,0,a+n,s+i,r+n,l+i,!1,!1,!1,!1,0,o.target);const u=document.createEvent("MouseEvents");u.initMouseEvent("mouseup",!0,!0,window,0,a+n,s+i,r+n,l+i,!1,!1,!1,!1,0,o.target),o.target.dispatchEvent(p),await this.waitTimeout(10),o.target.dispatchEvent(d),await this.waitTimeout(10),o.target.dispatchEvent(u)}}(e);case"close":return async function(){window.close()}();case"wait":return async function(e){await i(e.waitTime)}(e);case"eval":return async function(e){return console.log("eval!",e.code),new Promise((t=>{try{e.async?(window.eval(e.code),setTimeout((()=>{t(window.eval("window.__consentCheckResult"))}),e.timeout||250)):t(window.eval(e.code))}catch(o){console.warn("eval error",o,e.code),t(!1)}}))}(e);default:throw"Unknown action type: "+e.type}}function i(e){return new Promise((t=>{setTimeout((()=>{t()}),e)}))}function n(){return crypto&&void 0!==crypto.randomUUID?crypto.randomUUID():Math.random().toString()}var a={pending:new Map,sendContentMessage:null};function s(e,t){const o=n();a.sendContentMessage({type:"eval",id:o,code:e,snippetId:t});const c=new class{constructor(e,t=1e3){this.id=e,this.promise=new Promise(((e,t)=>{this.resolve=e,this.reject=t})),this.timer=window.setTimeout((()=>{this.reject(new Error("timeout"))}),t)}}(o);return a.pending.set(c.id,c),c.promise}var r={EVAL_0:()=>console.log(1),EVAL_CONSENTMANAGER_1:()=>window.__cmp&&"object"==typeof __cmp("getCMPData"),EVAL_CONSENTMANAGER_2:()=>!__cmp("consentStatus").userChoiceExists,EVAL_CONSENTMANAGER_3:()=>__cmp("setConsent",0),EVAL_CONSENTMANAGER_4:()=>__cmp("setConsent",1),EVAL_CONSENTMANAGER_5:()=>__cmp("consentStatus").userChoiceExists,EVAL_COOKIEBOT_1:()=>!!window.Cookiebot,EVAL_COOKIEBOT_2:()=>!window.Cookiebot.hasResponse&&!0===window.Cookiebot.dialog?.visible,EVAL_COOKIEBOT_3:()=>window.Cookiebot.withdraw()||!0,EVAL_COOKIEBOT_4:()=>window.Cookiebot.hide()||!0,EVAL_COOKIEBOT_5:()=>!0===window.Cookiebot.declined,EVAL_KLARO_1:()=>{const e=globalThis.klaroConfig||globalThis.klaro?.getManager&&globalThis.klaro.getManager().config;if(!e)return!0;const t=(e.services||e.apps).filter((e=>!e.required)).map((e=>e.name));if(klaro&&klaro.getManager){const e=klaro.getManager();return t.every((t=>!e.consents[t]))}if(klaroConfig&&"cookie"===klaroConfig.storageMethod){const e=klaroConfig.cookieName||klaroConfig.storageName,o=JSON.parse(decodeURIComponent(document.cookie.split(";").find((t=>t.trim().startsWith(e))).split("=")[1]));return Object.keys(o).filter((e=>t.includes(e))).every((e=>!1===o[e]))}},EVAL_KLARO_OPEN_POPUP:()=>{klaro.show(void 0,!0)},EVAL_KLARO_TRY_API_OPT_OUT:()=>{if(window.klaro&&"function"==typeof klaro.show&&"function"==typeof klaro.getManager)try{return klaro.getManager().changeAll(!1),klaro.getManager().saveAndApplyConsents(),!0}catch(e){return console.warn(e),!1}return!1},EVAL_ONETRUST_1:()=>window.OnetrustActiveGroups.split(",").filter((e=>e.length>0)).length<=1,EVAL_TRUSTARC_TOP:()=>window&&window.truste&&"0"===window.truste.eu.bindMap.prefCookie,EVAL_ADROLL_0:()=>!document.cookie.includes("__adroll_fpc"),EVAL_ALMACMP_0:()=>document.cookie.includes('"name":"Google","consent":false'),EVAL_AFFINITY_SERIF_COM_0:()=>document.cookie.includes("serif_manage_cookies_viewed")&&!document.cookie.includes("serif_allow_analytics"),EVAL_ARBEITSAGENTUR_TEST:()=>document.cookie.includes("cookie_consent=denied"),EVAL_AXEPTIO_0:()=>document.cookie.includes("axeptio_authorized_vendors=%2C%2C"),EVAL_BAHN_TEST:()=>1===utag.gdpr.getSelectedCategories().length,EVAL_BING_0:()=>document.cookie.includes("AL=0")&&document.cookie.includes("AD=0")&&document.cookie.includes("SM=0"),EVAL_BLOCKSY_0:()=>document.cookie.includes("blocksy_cookies_consent_accepted=no"),EVAL_BORLABS_0:()=>!JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>-1!==e.indexOf("borlabs-cookie"))).split("=",2)[1])).consents.statistics,EVAL_BUNDESREGIERUNG_DE_0:()=>document.cookie.match("cookie-allow-tracking=0"),EVAL_CANVA_0:()=>!document.cookie.includes("gtm_fpc_engagement_event"),EVAL_CC_BANNER2_0:()=>!!document.cookie.match(/sncc=[^;]+D%3Dtrue/),EVAL_CLICKIO_0:()=>document.cookie.includes("__lxG__consent__v2_daisybit="),EVAL_CLINCH_0:()=>document.cookie.includes("ctc_rejected=1"),EVAL_COOKIECONSENT2_TEST:()=>document.cookie.includes("cc_cookie="),EVAL_COOKIECONSENT3_TEST:()=>document.cookie.includes("cc_cookie="),EVAL_COINBASE_0:()=>JSON.parse(decodeURIComponent(document.cookie.match(/cm_(eu|default)_preferences=([0-9a-zA-Z\\{\\}\\[\\]%:]*);?/)[2])).consent.length<=1,EVAL_COMPLIANZ_BANNER_0:()=>document.cookie.includes("cmplz_banner-status=dismissed"),EVAL_COOKIE_LAW_INFO_0:()=>CLI.disableAllCookies()||CLI.reject_close()||!0,EVAL_COOKIE_LAW_INFO_1:()=>-1===document.cookie.indexOf("cookielawinfo-checkbox-non-necessary=yes"),EVAL_COOKIE_LAW_INFO_DETECT:()=>!!window.CLI,EVAL_COOKIE_MANAGER_POPUP_0:()=>!1===JSON.parse(document.cookie.split(";").find((e=>e.trim().startsWith("CookieLevel"))).split("=")[1]).social,EVAL_COOKIEALERT_0:()=>document.querySelector("body").removeAttribute("style")||!0,EVAL_COOKIEALERT_1:()=>document.querySelector("body").removeAttribute("style")||!0,EVAL_COOKIEALERT_2:()=>!0===window.CookieConsent.declined,EVAL_COOKIEFIRST_0:()=>{return!1===(e=JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>-1!==e.indexOf("cookiefirst"))).trim()).split("=")[1])).performance&&!1===e.functional&&!1===e.advertising;var e},EVAL_COOKIEFIRST_1:()=>document.querySelectorAll("button[data-cookiefirst-accent-color=true][role=checkbox]:not([disabled])").forEach((e=>"true"==e.getAttribute("aria-checked")&&e.click()))||!0,EVAL_COOKIEINFORMATION_0:()=>CookieInformation.declineAllCategories()||!0,EVAL_COOKIEINFORMATION_1:()=>CookieInformation.submitAllCategories()||!0,EVAL_COOKIEINFORMATION_2:()=>document.cookie.includes("CookieInformationConsent="),EVAL_COOKIEYES_0:()=>document.cookie.includes("advertisement:no"),EVAL_DAILYMOTION_0:()=>!!document.cookie.match("dm-euconsent-v2"),EVAL_DNDBEYOND_TEST:()=>document.cookie.includes("cookie-consent=denied"),EVAL_DSGVO_0:()=>!document.cookie.includes("sp_dsgvo_cookie_settings"),EVAL_DUNELM_0:()=>document.cookie.includes("cc_functional=0")&&document.cookie.includes("cc_targeting=0"),EVAL_ETSY_0:()=>document.querySelectorAll(".gdpr-overlay-body input").forEach((e=>{e.checked=!1}))||!0,EVAL_ETSY_1:()=>document.querySelector(".gdpr-overlay-view button[data-wt-overlay-close]").click()||!0,EVAL_EU_COOKIE_COMPLIANCE_0:()=>-1===document.cookie.indexOf("cookie-agreed=2"),EVAL_EU_COOKIE_LAW_0:()=>!document.cookie.includes("euCookie"),EVAL_EZOIC_0:()=>ezCMP.handleAcceptAllClick(),EVAL_EZOIC_1:()=>!!document.cookie.match(/ez-consent-tcf/),EVAL_GOOGLE_0:()=>!!document.cookie.match(/SOCS=CAE/),EVAL_HEMA_TEST_0:()=>document.cookie.includes("cookies_rejected=1"),EVAL_IUBENDA_0:()=>document.querySelectorAll(".purposes-item input[type=checkbox]:not([disabled])").forEach((e=>{e.checked&&e.click()}))||!0,EVAL_IUBENDA_1:()=>!!document.cookie.match(/_iub_cs-\d+=/),EVAL_IWINK_TEST:()=>document.cookie.includes("cookie_permission_granted=no"),EVAL_JQUERY_COOKIEBAR_0:()=>!document.cookie.includes("cookies-state=accepted"),EVAL_MEDIAVINE_0:()=>document.querySelectorAll('[data-name="mediavine-gdpr-cmp"] input[type=checkbox]').forEach((e=>e.checked&&e.click()))||!0,EVAL_MICROSOFT_0:()=>Array.from(document.querySelectorAll("div > button")).filter((e=>e.innerText.match("Reject|Ablehnen")))[0].click()||!0,EVAL_MICROSOFT_1:()=>Array.from(document.querySelectorAll("div > button")).filter((e=>e.innerText.match("Accept|Annehmen")))[0].click()||!0,EVAL_MICROSOFT_2:()=>!!document.cookie.match("MSCC|GHCC"),EVAL_MOOVE_0:()=>document.querySelectorAll("#moove_gdpr_cookie_modal input").forEach((e=>{e.disabled||(e.checked="moove_gdpr_strict_cookies"===e.name||"moove_gdpr_strict_cookies"===e.id)}))||!0,EVAL_ONENINETWO_0:()=>document.cookie.includes("CC_ADVERTISING=NO")&&document.cookie.includes("CC_ANALYTICS=NO"),EVAL_OPERA_0:()=>document.cookie.includes("cookie_consent_essential=true")&&!document.cookie.includes("cookie_consent_marketing=true"),EVAL_PAYPAL_0:()=>!0===document.cookie.includes("cookie_prefs"),EVAL_PRIMEBOX_0:()=>!document.cookie.includes("cb-enabled=accepted"),EVAL_PUBTECH_0:()=>document.cookie.includes("euconsent-v2")&&(document.cookie.match(/.YAAAAAAAAAAA/)||document.cookie.match(/.aAAAAAAAAAAA/)||document.cookie.match(/.YAAACFgAAAAA/)),EVAL_REDDIT_0:()=>document.cookie.includes("eu_cookie={%22opted%22:true%2C%22nonessential%22:false}"),EVAL_SIBBO_0:()=>!!window.localStorage.getItem("euconsent-v2"),EVAL_SIRDATA_UNBLOCK_SCROLL:()=>(document.documentElement.classList.forEach((e=>{e.startsWith("sd-cmp-")&&document.documentElement.classList.remove(e)})),!0),EVAL_SNIGEL_0:()=>!!document.cookie.match("snconsent"),EVAL_STEAMPOWERED_0:()=>2===JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>e.trim().startsWith("cookieSettings"))).split("=")[1])).preference_state,EVAL_SVT_TEST:()=>document.cookie.includes('cookie-consent-1={"optedIn":true,"functionality":false,"statistics":false}'),EVAL_TAKEALOT_0:()=>document.body.classList.remove("freeze")||(document.body.style="")||!0,EVAL_TARTEAUCITRON_0:()=>tarteaucitron.userInterface.respondAll(!1)||!0,EVAL_TARTEAUCITRON_1:()=>tarteaucitron.userInterface.respondAll(!0)||!0,EVAL_TARTEAUCITRON_2:()=>document.cookie.match(/tarteaucitron=[^;]*/)?.[0].includes("false"),EVAL_TAUNTON_TEST:()=>document.cookie.includes("taunton_user_consent_submitted=true"),EVAL_TEALIUM_0:()=>void 0!==window.utag&&"object"==typeof utag.gdpr,EVAL_TEALIUM_1:()=>utag.gdpr.setConsentValue(!1)||!0,EVAL_TEALIUM_DONOTSELL:()=>utag.gdpr.dns?.setDnsState(!1)||!0,EVAL_TEALIUM_2:()=>utag.gdpr.setConsentValue(!0)||!0,EVAL_TEALIUM_3:()=>1!==utag.gdpr.getConsentState(),EVAL_TEALIUM_DONOTSELL_CHECK:()=>1!==utag.gdpr.dns?.getDnsState(),EVAL_TESTCMP_0:()=>"button_clicked"===window.results.results[0],EVAL_TESTCMP_COSMETIC_0:()=>"banner_hidden"===window.results.results[0],EVAL_THEFREEDICTIONARY_0:()=>cmpUi.showPurposes()||cmpUi.rejectAll()||!0,EVAL_THEFREEDICTIONARY_1:()=>cmpUi.allowAll()||!0,EVAL_THEVERGE_0:()=>document.cookie.includes("_duet_gdpr_acknowledged=1"),EVAL_UBUNTU_COM_0:()=>document.cookie.includes("_cookies_accepted=essential"),EVAL_UK_COOKIE_CONSENT_0:()=>!document.cookie.includes("catAccCookies"),EVAL_USERCENTRICS_API_0:()=>"object"==typeof UC_UI,EVAL_USERCENTRICS_API_1:()=>!!UC_UI.closeCMP(),EVAL_USERCENTRICS_API_2:()=>!!UC_UI.denyAllConsents(),EVAL_USERCENTRICS_API_3:()=>!!UC_UI.acceptAllConsents(),EVAL_USERCENTRICS_API_4:()=>!!UC_UI.closeCMP(),EVAL_USERCENTRICS_API_5:()=>!0===UC_UI.areAllConsentsAccepted(),EVAL_USERCENTRICS_API_6:()=>!1===UC_UI.areAllConsentsAccepted(),EVAL_USERCENTRICS_BUTTON_0:()=>JSON.parse(localStorage.getItem("usercentrics")).consents.every((e=>e.isEssential||!e.consentStatus)),EVAL_WAITROSE_0:()=>Array.from(document.querySelectorAll("label[id$=cookies-deny-label]")).forEach((e=>e.click()))||!0,EVAL_WAITROSE_1:()=>document.cookie.includes("wtr_cookies_advertising=0")&&document.cookie.includes("wtr_cookies_analytics=0"),EVAL_WP_COOKIE_NOTICE_0:()=>document.cookie.includes("wpl_viewed_cookie=no"),EVAL_XE_TEST:()=>document.cookie.includes("xeConsentState={%22performance%22:false%2C%22marketing%22:false%2C%22compliance%22:false}"),EVAL_XING_0:()=>document.cookie.includes("userConsent=%7B%22marketing%22%3Afalse"),EVAL_YOUTUBE_DESKTOP_0:()=>!!document.cookie.match(/SOCS=CAE/),EVAL_YOUTUBE_MOBILE_0:()=>!!document.cookie.match(/SOCS=CAE/)};var l={main:!0,frame:!1,urlPattern:""},p=class{constructor(e){this.runContext=l,this.autoconsent=e}get hasSelfTest(){throw new Error("Not Implemented")}get isIntermediate(){throw new Error("Not Implemented")}get isCosmetic(){throw new Error("Not Implemented")}mainWorldEval(e){const t=r[e];if(!t)return console.warn("Snippet not found",e),Promise.resolve(!1);const o=this.autoconsent.config.logs;if(this.autoconsent.config.isMainWorld){o.evals&&console.log("inline eval:",e,t);let c=!1;try{c=!!t.call(globalThis)}catch(t){o.evals&&console.error("error evaluating rule",e,t)}return Promise.resolve(c)}const c=`(${t.toString()})()`;return o.evals&&console.log("async eval:",e,c),s(c,e).catch((t=>(o.evals&&console.error("error evaluating rule",e,t),!1)))}checkRunContext(){const e={...l,...this.runContext},t=window.top===window;return!(t&&!e.main)&&(!(!t&&!e.frame)&&!(e.urlPattern&&!window.location.href.match(e.urlPattern)))}detectCmp(){throw new Error("Not Implemented")}async detectPopup(){return!1}optOut(){throw new Error("Not Implemented")}optIn(){throw new Error("Not Implemented")}openCmp(){throw new Error("Not Implemented")}async test(){return Promise.resolve(!0)}click(e,t=!1){return this.autoconsent.domActions.click(e,t)}elementExists(e){return this.autoconsent.domActions.elementExists(e)}elementVisible(e,t){return this.autoconsent.domActions.elementVisible(e,t)}waitForElement(e,t){return this.autoconsent.domActions.waitForElement(e,t)}waitForVisible(e,t,o){return this.autoconsent.domActions.waitForVisible(e,t,o)}waitForThenClick(e,t,o){return this.autoconsent.domActions.waitForThenClick(e,t,o)}wait(e){return this.autoconsent.domActions.wait(e)}hide(e,t){return this.autoconsent.domActions.hide(e,t)}prehide(e){return this.autoconsent.domActions.prehide(e)}undoPrehide(){return this.autoconsent.domActions.undoPrehide()}querySingleReplySelector(e,t){return this.autoconsent.domActions.querySingleReplySelector(e,t)}querySelectorChain(e){return this.autoconsent.domActions.querySelectorChain(e)}elementSelector(e){return this.autoconsent.domActions.elementSelector(e)}},d=class extends p{constructor(e,t){super(t),this.rule=e,this.name=e.name,this.runContext=e.runContext||l}get hasSelfTest(){return!!this.rule.test}get isIntermediate(){return!!this.rule.intermediate}get isCosmetic(){return!!this.rule.cosmetic}get prehideSelectors(){return this.rule.prehideSelectors}async detectCmp(){return!!this.rule.detectCmp&&this._runRulesParallel(this.rule.detectCmp)}async detectPopup(){return!!this.rule.detectPopup&&this._runRulesSequentially(this.rule.detectPopup)}async optOut(){const e=this.autoconsent.config.logs;return!!this.rule.optOut&&(e.lifecycle&&console.log("Initiated optOut()",this.rule.optOut),this._runRulesSequentially(this.rule.optOut))}async optIn(){const e=this.autoconsent.config.logs;return!!this.rule.optIn&&(e.lifecycle&&console.log("Initiated optIn()",this.rule.optIn),this._runRulesSequentially(this.rule.optIn))}async openCmp(){return!!this.rule.openCmp&&this._runRulesSequentially(this.rule.openCmp)}async test(){return this.hasSelfTest?this._runRulesSequentially(this.rule.test):super.test()}async evaluateRuleStep(e){const t=[],o=this.autoconsent.config.logs;if(e.exists&&t.push(this.elementExists(e.exists)),e.visible&&t.push(this.elementVisible(e.visible,e.check)),e.eval){const o=this.mainWorldEval(e.eval);t.push(o)}if(e.waitFor&&t.push(this.waitForElement(e.waitFor,e.timeout)),e.waitForVisible&&t.push(this.waitForVisible(e.waitForVisible,e.timeout,e.check)),e.click&&t.push(this.click(e.click,e.all)),e.waitForThenClick&&t.push(this.waitForThenClick(e.waitForThenClick,e.timeout,e.all)),e.wait&&t.push(this.wait(e.wait)),e.hide&&t.push(this.hide(e.hide,e.method)),e.if){if(!e.if.exists&&!e.if.visible)return console.error("invalid conditional rule",e.if),!1;const c=await this.evaluateRuleStep(e.if);o.rulesteps&&console.log("Condition is",c),c?t.push(this._runRulesSequentially(e.then)):e.else?t.push(this._runRulesSequentially(e.else)):t.push(!0)}if(e.any){for(const t of e.any)if(await this.evaluateRuleStep(t))return!0;return!1}if(0===t.length)return o.errors&&console.warn("Unrecognized rule",e),!1;return(await Promise.all(t)).reduce(((e,t)=>e&&t),!0)}async _runRulesParallel(e){const t=e.map((e=>this.evaluateRuleStep(e)));return(await Promise.all(t)).every((e=>!!e))}async _runRulesSequentially(e){const t=this.autoconsent.config.logs;for(const o of e){t.rulesteps&&console.log("Running rule...",o);const e=await this.evaluateRuleStep(o);if(t.rulesteps&&console.log("...rule result",e),!e&&!o.optional)return!1}return!0}};function u(e="autoconsent-css-rules"){const t=`style#${e}`,o=document.querySelector(t);if(o&&o instanceof HTMLStyleElement)return o;{const t=document.head||document.getElementsByTagName("head")[0]||document.documentElement,o=document.createElement("style");return o.id=e,t.appendChild(o),o}}function m(e,t,o="display"){const c=`${t} { ${"opacity"===o?"opacity: 0":"display: none"} !important; z-index: -1 !important; pointer-events: none !important; } `;return e instanceof HTMLStyleElement&&(e.innerText+=c,t.length>0)}async function h(e,t,o){const c=await e();return!c&&t>0?new Promise((c=>{setTimeout((async()=>{c(h(e,t-1,o))}),o)})):Promise.resolve(c)}function k(e){if(!e)return!1;if(null!==e.offsetParent)return!0;{const t=window.getComputedStyle(e);if("fixed"===t.position&&"none"!==t.display)return!0}return!1}function b(e){const t={enabled:!0,autoAction:"optOut",disabledCmps:[],enablePrehide:!0,enableCosmeticRules:!0,detectRetries:20,isMainWorld:!1,prehideTimeout:2e3,logs:{lifecycle:!1,rulesteps:!1,evals:!1,errors:!0,messages:!1}},o=(c=t,globalThis.structuredClone?structuredClone(c):JSON.parse(JSON.stringify(c)));var c;for(const c of Object.keys(t))void 0!==e[c]&&(o[c]=e[c]);return o}var _="#truste-show-consent",g="#truste-consent-track",y=[class extends p{constructor(e){super(e),this.name="TrustArc-top",this.prehideSelectors=[".trustarc-banner-container",`.truste_popframe,.truste_overlay,.truste_box_overlay,${g}`],this.runContext={main:!0,frame:!1},this._shortcutButton=null,this._optInDone=!1}get hasSelfTest(){return!1}get isIntermediate(){return!this._optInDone&&!this._shortcutButton}get isCosmetic(){return!1}async detectCmp(){const e=this.elementExists(`${_},${g}`);return e&&(this._shortcutButton=document.querySelector("#truste-consent-required")),e}async detectPopup(){return this.elementVisible(`#truste-consent-content,#trustarc-banner-overlay,${g}`,"all")}openFrame(){this.click(_)}async optOut(){return this._shortcutButton?(this._shortcutButton.click(),!0):(m(u(),`.truste_popframe, .truste_overlay, .truste_box_overlay, ${g}`),this.click(_),setTimeout((()=>{u().remove()}),1e4),!0)}async optIn(){return this._optInDone=!0,this.click("#truste-consent-button")}async openCmp(){return!0}async test(){return await this.mainWorldEval("EVAL_TRUSTARC_TOP")}},class extends p{constructor(){super(...arguments),this.name="TrustArc-frame",this.runContext={main:!1,frame:!0,urlPattern:"^https://consent-pref\\.trustarc\\.com/\\?"}}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return!0}async detectPopup(){return this.elementVisible("#defaultpreferencemanager","any")&&this.elementVisible(".mainContent","any")}async navigateToSettings(){return await h((async()=>this.elementExists(".shp")||this.elementVisible(".advance","any")||this.elementExists(".switch span:first-child")),10,500),this.elementExists(".shp")&&this.click(".shp"),await this.waitForElement(".prefPanel",5e3),this.elementVisible(".advance","any")&&this.click(".advance"),await h((()=>this.elementVisible(".switch span:first-child","any")),5,1e3)}async optOut(){return await h((()=>"complete"===document.readyState),20,100),await this.waitForElement(".mainContent[aria-hidden=false]",5e3),!!this.click(".rejectAll")||(this.elementExists(".prefPanel")&&await this.waitForElement('.prefPanel[style="visibility: visible;"]',3e3),this.click("#catDetails0")?(this.click(".submit"),this.waitForThenClick("#gwt-debug-close_id",5e3),!0):this.click(".required")?(this.waitForThenClick("#gwt-debug-close_id",5e3),!0):(await this.navigateToSettings(),this.click(".switch span:nth-child(1):not(.active)",!0),this.click(".submit"),this.waitForThenClick("#gwt-debug-close_id",3e5),!0))}async optIn(){return this.click(".call")||(await this.navigateToSettings(),this.click(".switch span:nth-child(2)",!0),this.click(".submit"),this.waitForElement("#gwt-debug-close_id",3e5).then((()=>{this.click("#gwt-debug-close_id")}))),!0}},class extends p{constructor(){super(...arguments),this.name="Cybotcookiebot",this.prehideSelectors=["#CybotCookiebotDialog,#CybotCookiebotDialogBodyUnderlay,#dtcookie-container,#cookiebanner,#cb-cookieoverlay,.modal--cookie-banner,#cookiebanner_outer,#CookieBanner"]}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return await this.mainWorldEval("EVAL_COOKIEBOT_1")}async detectPopup(){return this.mainWorldEval("EVAL_COOKIEBOT_2")}async optOut(){await this.wait(500);let e=await this.mainWorldEval("EVAL_COOKIEBOT_3");return await this.wait(500),e=e&&await this.mainWorldEval("EVAL_COOKIEBOT_4"),e}async optIn(){return this.elementExists("#dtcookie-container")?this.click(".h-dtcookie-accept"):(this.click(".CybotCookiebotDialogBodyLevelButton:not(:checked):enabled",!0),this.click("#CybotCookiebotDialogBodyLevelButtonAccept"),this.click("#CybotCookiebotDialogBodyButtonAccept"),!0)}async test(){return await this.wait(500),await this.mainWorldEval("EVAL_COOKIEBOT_5")}},class extends p{constructor(){super(...arguments),this.name="Sourcepoint-frame",this.prehideSelectors=["div[id^='sp_message_container_'],.message-overlay","#sp_privacy_manager_container"],this.ccpaNotice=!1,this.ccpaPopup=!1,this.runContext={main:!0,frame:!0}}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){const e=new URL(location.href);return e.searchParams.has("message_id")&&"ccpa-notice.sp-prod.net"===e.hostname?(this.ccpaNotice=!0,!0):"ccpa-pm.sp-prod.net"===e.hostname?(this.ccpaPopup=!0,!0):("/index.html"===e.pathname||"/privacy-manager/index.html"===e.pathname||"/ccpa_pm/index.html"===e.pathname)&&(e.searchParams.has("message_id")||e.searchParams.has("requestUUID")||e.searchParams.has("consentUUID"))}async detectPopup(){return!!this.ccpaNotice||(this.ccpaPopup?await this.waitForElement(".priv-save-btn",2e3):(await this.waitForElement(".sp_choice_type_11,.sp_choice_type_12,.sp_choice_type_13,.sp_choice_type_ACCEPT_ALL,.sp_choice_type_SAVE_AND_EXIT",2e3),!this.elementExists(".sp_choice_type_9")))}async optIn(){return await this.waitForElement(".sp_choice_type_11,.sp_choice_type_ACCEPT_ALL",2e3),!!this.click(".sp_choice_type_11")||!!this.click(".sp_choice_type_ACCEPT_ALL")}isManagerOpen(){return"/privacy-manager/index.html"===location.pathname||"/ccpa_pm/index.html"===location.pathname}async optOut(){const e=this.autoconsent.config.logs;if(this.ccpaPopup){const e=document.querySelectorAll(".priv-purpose-container .sp-switch-arrow-block a.neutral.on .right");for(const t of e)t.click();const t=document.querySelectorAll(".priv-purpose-container .sp-switch-arrow-block a.switch-bg.on");for(const e of t)e.click();return this.click(".priv-save-btn")}if(!this.isManagerOpen()){if(!await this.waitForElement(".sp_choice_type_12,.sp_choice_type_13"))return!1;if(!this.elementExists(".sp_choice_type_12"))return this.click(".sp_choice_type_13");this.click(".sp_choice_type_12"),await h((()=>this.isManagerOpen()),200,100)}await this.waitForElement(".type-modal",2e4),this.waitForThenClick(".ccpa-stack .pm-switch[aria-checked=true] .slider",500,!0);try{const e=".sp_choice_type_REJECT_ALL",t=".reject-toggle",o=await Promise.race([this.waitForElement(e,2e3).then((e=>e?0:-1)),this.waitForElement(t,2e3).then((e=>e?1:-1)),this.waitForElement(".pm-features",2e3).then((e=>e?2:-1))]);if(0===o)return await this.wait(1500),this.click(e);1===o?this.click(t):2===o&&(await this.waitForElement(".pm-features",1e4),this.click(".checked > span",!0),this.click(".chevron"))}catch(t){e.errors&&console.warn(t)}return this.click(".sp_choice_type_SAVE_AND_EXIT")}},class extends p{constructor(){super(...arguments),this.name="consentmanager.net",this.prehideSelectors=["#cmpbox,#cmpbox2"],this.apiAvailable=!1}get hasSelfTest(){return this.apiAvailable}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.apiAvailable=await this.mainWorldEval("EVAL_CONSENTMANAGER_1"),!!this.apiAvailable||this.elementExists("#cmpbox")}async detectPopup(){return this.apiAvailable?(await this.wait(500),await this.mainWorldEval("EVAL_CONSENTMANAGER_2")):this.elementVisible("#cmpbox .cmpmore","any")}async optOut(){return await this.wait(500),this.apiAvailable?await this.mainWorldEval("EVAL_CONSENTMANAGER_3"):!!this.click(".cmpboxbtnno")||(this.elementExists(".cmpwelcomeprpsbtn")?(this.click(".cmpwelcomeprpsbtn > a[aria-checked=true]",!0),this.click(".cmpboxbtnsave"),!0):(this.click(".cmpboxbtncustom"),await this.waitForElement(".cmptblbox",2e3),this.click(".cmptdchoice > a[aria-checked=true]",!0),this.click(".cmpboxbtnyescustomchoices"),this.hide("#cmpwrapper,#cmpbox","display"),!0))}async optIn(){return this.apiAvailable?await this.mainWorldEval("EVAL_CONSENTMANAGER_4"):this.click(".cmpboxbtnyes")}async test(){if(this.apiAvailable)return await this.mainWorldEval("EVAL_CONSENTMANAGER_5")}},class extends p{constructor(){super(...arguments),this.name="Evidon"}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("#_evidon_banner")}async detectPopup(){return this.elementVisible("#_evidon_banner","any")}async optOut(){return this.click("#_evidon-decline-button")||(m(u(),"#evidon-prefdiag-overlay,#evidon-prefdiag-background"),this.click("#_evidon-option-button"),await this.waitForElement("#evidon-prefdiag-overlay",5e3),this.click("#evidon-prefdiag-decline")),!0}async optIn(){return this.click("#_evidon-accept-button")}},class extends p{constructor(){super(...arguments),this.name="Onetrust",this.prehideSelectors=["#onetrust-banner-sdk,#onetrust-consent-sdk,.onetrust-pc-dark-filter,.js-consent-banner"],this.runContext={urlPattern:"^(?!.*https://www\\.nba\\.com/)"}}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("#onetrust-banner-sdk,#onetrust-pc-sdk")}async detectPopup(){return this.elementVisible("#onetrust-banner-sdk,#onetrust-pc-sdk","any")}async optOut(){return this.elementVisible("#onetrust-reject-all-handler,.ot-pc-refuse-all-handler,.js-reject-cookies","any")?this.click("#onetrust-reject-all-handler,.ot-pc-refuse-all-handler,.js-reject-cookies"):(this.elementExists("#onetrust-pc-btn-handler")?this.click("#onetrust-pc-btn-handler"):this.click(".ot-sdk-show-settings,button.js-cookie-settings"),await this.waitForElement("#onetrust-consent-sdk",2e3),await this.wait(1e3),this.click("#onetrust-consent-sdk input.category-switch-handler:checked,.js-editor-toggle-state:checked",!0),await this.wait(1e3),await this.waitForElement(".save-preference-btn-handler,.js-consent-save",2e3),this.click(".save-preference-btn-handler,.js-consent-save"),await this.waitForVisible("#onetrust-banner-sdk",5e3,"none"),!0)}async optIn(){return this.click("#onetrust-accept-btn-handler,#accept-recommended-btn-handler,.js-accept-cookies")}async test(){return await h((()=>this.mainWorldEval("EVAL_ONETRUST_1")),10,500)}},class extends p{constructor(){super(...arguments),this.name="Klaro",this.prehideSelectors=[".klaro"],this.settingsOpen=!1}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".klaro > .cookie-modal")?(this.settingsOpen=!0,!0):this.elementExists(".klaro > .cookie-notice")}async detectPopup(){return this.elementVisible(".klaro > .cookie-notice,.klaro > .cookie-modal","any")}async optOut(){return!!await this.mainWorldEval("EVAL_KLARO_TRY_API_OPT_OUT")||(!!this.click(".klaro .cn-decline")||(await this.mainWorldEval("EVAL_KLARO_OPEN_POPUP"),!!this.click(".klaro .cn-decline")||(this.click(".cm-purpose:not(.cm-toggle-all) > input:not(.half-checked,.required,.only-required),.cm-purpose:not(.cm-toggle-all) > div > input:not(.half-checked,.required,.only-required)",!0),this.click(".cm-btn-accept,.cm-button"))))}async optIn(){return!!this.click(".klaro .cm-btn-accept-all")||(this.settingsOpen?(this.click(".cm-purpose:not(.cm-toggle-all) > input.half-checked",!0),this.click(".cm-btn-accept")):this.click(".klaro .cookie-notice .cm-btn-success"))}async test(){return await this.mainWorldEval("EVAL_KLARO_1")}},class extends p{constructor(){super(...arguments),this.name="Uniconsent"}get prehideSelectors(){return[".unic",".modal:has(.unic)"]}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".unic .unic-box,.unic .unic-bar,.unic .unic-modal")}async detectPopup(){return this.elementVisible(".unic .unic-box,.unic .unic-bar,.unic .unic-modal","any")}async optOut(){if(await this.waitForElement(".unic button",1e3),document.querySelectorAll(".unic button").forEach((e=>{const t=e.textContent;(t.includes("Manage Options")||t.includes("Optionen verwalten"))&&e.click()})),await this.waitForElement(".unic input[type=checkbox]",1e3)){await this.waitForElement(".unic button",1e3),document.querySelectorAll(".unic input[type=checkbox]").forEach((e=>{e.checked&&e.click()}));for(const e of document.querySelectorAll(".unic button")){const t=e.textContent;for(const o of["Confirm Choices","Save Choices","Auswahl speichern"])if(t.includes(o))return e.click(),await this.wait(500),!0}}return!1}async optIn(){return this.waitForThenClick(".unic #unic-agree")}async test(){await this.wait(1e3);return!this.elementExists(".unic .unic-box,.unic .unic-bar")}},class extends p{constructor(){super(...arguments),this.prehideSelectors=[".cmp-root"],this.name="Conversant"}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".cmp-root .cmp-receptacle")}async detectPopup(){return this.elementVisible(".cmp-root .cmp-receptacle","any")}async optOut(){if(!await this.waitForThenClick(".cmp-main-button:not(.cmp-main-button--primary)"))return!1;if(!await this.waitForElement(".cmp-view-tab-tabs"))return!1;await this.waitForThenClick(".cmp-view-tab-tabs > :first-child"),await this.waitForThenClick(".cmp-view-tab-tabs > .cmp-view-tab--active:first-child");for(const e of Array.from(document.querySelectorAll(".cmp-accordion-item"))){e.querySelector(".cmp-accordion-item-title").click(),await h((()=>!!e.querySelector(".cmp-accordion-item-content.cmp-active")),10,50);const t=e.querySelector(".cmp-accordion-item-content.cmp-active");t.querySelectorAll(".cmp-toggle-actions .cmp-toggle-deny:not(.cmp-toggle-deny--active)").forEach((e=>e.click())),t.querySelectorAll(".cmp-toggle-actions .cmp-toggle-checkbox:not(.cmp-toggle-checkbox--active)").forEach((e=>e.click()))}return await this.click(".cmp-main-button:not(.cmp-main-button--primary)"),!0}async optIn(){return this.waitForThenClick(".cmp-main-button.cmp-main-button--primary")}async test(){return document.cookie.includes("cmp-data=0")}},class extends p{constructor(){super(...arguments),this.name="tiktok.com",this.runContext={urlPattern:"tiktok"}}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}getShadowRoot(){const e=document.querySelector("tiktok-cookie-banner");return e?e.shadowRoot:null}async detectCmp(){return this.elementExists("tiktok-cookie-banner")}async detectPopup(){return k(this.getShadowRoot().querySelector(".tiktok-cookie-banner"))}async optOut(){const e=this.autoconsent.config.logs,t=this.getShadowRoot().querySelector(".button-wrapper button:first-child");return t?(e.rulesteps&&console.log("[clicking]",t),t.click(),!0):(e.errors&&console.log("no decline button found"),!1)}async optIn(){const e=this.autoconsent.config.logs,t=this.getShadowRoot().querySelector(".button-wrapper button:last-child");return t?(e.rulesteps&&console.log("[clicking]",t),t.click(),!0):(e.errors&&console.log("no accept button found"),!1)}async test(){const e=document.cookie.match(/cookie-consent=([^;]+)/);if(!e)return!1;const t=JSON.parse(decodeURIComponent(e[1]));return Object.values(t).every((e=>"boolean"!=typeof e||!1===e))}},class extends p{constructor(){super(...arguments),this.runContext={urlPattern:"^https://(www\\.)?airbnb\\.[^/]+/"},this.prehideSelectors=["div[data-testid=main-cookies-banner-container]",'div:has(> div:first-child):has(> div:last-child):has(> section [data-testid="strictly-necessary-cookies"])']}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("div[data-testid=main-cookies-banner-container]")}async detectPopup(){return this.elementVisible("div[data-testid=main-cookies-banner-container","any")}async optOut(){let e;for(await this.waitForThenClick("div[data-testid=main-cookies-banner-container] button._snbhip0");e=document.querySelector("[data-testid=modal-container] button[aria-checked=true]:not([disabled])");)e.click();return this.waitForThenClick("button[data-testid=save-btn]")}async optIn(){return this.waitForThenClick("div[data-testid=main-cookies-banner-container] button._148dgdpk")}async test(){return await h((()=>!!document.cookie.match("OptanonAlertBoxClosed")),20,200)}},class extends p{constructor(){super(...arguments),this.name="tumblr-com",this.runContext={urlPattern:"^https://(www\\.)?tumblr\\.com/"}}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}get prehideSelectors(){return["#cmp-app-container"]}async detectCmp(){return this.elementExists("#cmp-app-container")}async detectPopup(){return this.elementVisible("#cmp-app-container","any")}async optOut(){let e=document.querySelector("#cmp-app-container iframe"),t=e.contentDocument?.querySelector(".cmp-components-button.is-secondary");return!!t&&(t.click(),await h((()=>!!document.querySelector("#cmp-app-container iframe").contentDocument?.querySelector(".cmp__dialog input")),5,500),e=document.querySelector("#cmp-app-container iframe"),t=e.contentDocument?.querySelector(".cmp-components-button.is-secondary"),!!t&&(t.click(),!0))}async optIn(){const e=document.querySelector("#cmp-app-container iframe").contentDocument.querySelector(".cmp-components-button.is-primary");return!!e&&(e.click(),!0)}}];var w=[{name:"192.com",detectCmp:[{exists:".ont-cookies"}],detectPopup:[{visible:".ont-cookies"}],optIn:[{click:".ont-btn-main.ont-cookies-btn.js-ont-btn-ok2"}],optOut:[{click:".ont-cookes-btn-manage"},{click:".ont-btn-main.ont-cookies-btn.js-ont-btn-choose"}],test:[{eval:"EVAL_ONENINETWO_0"}]},{name:"1password-com",cosmetic:!0,prehideSelectors:['footer #footer-root [aria-label="Cookie Consent"]'],detectCmp:[{exists:'footer #footer-root [aria-label="Cookie Consent"]'}],detectPopup:[{visible:'footer #footer-root [aria-label="Cookie Consent"]'}],optIn:[{click:'footer #footer-root [aria-label="Cookie Consent"] button'}],optOut:[{hide:'footer #footer-root [aria-label="Cookie Consent"]'}]},{name:"abconcerts.be",vendorUrl:"https://unknown",intermediate:!1,prehideSelectors:["dialog.cookie-consent"],detectCmp:[{exists:"dialog.cookie-consent form.cookie-consent__form"}],detectPopup:[{visible:"dialog.cookie-consent form.cookie-consent__form"}],optIn:[{waitForThenClick:"dialog.cookie-consent form.cookie-consent__form button[value=yes]"}],optOut:[{if:{exists:"dialog.cookie-consent form.cookie-consent__form button[value=no]"},then:[{click:"dialog.cookie-consent form.cookie-consent__form button[value=no]"}],else:[{click:"dialog.cookie-consent form.cookie-consent__form button.cookie-consent__options-toggle"},{waitForThenClick:'dialog.cookie-consent form.cookie-consent__form button[value="save_options"]'}]}]},{name:"activobank.pt",runContext:{urlPattern:"^https://(www\\.)?activobank\\.pt"},prehideSelectors:["aside#cookies,.overlay-cookies"],detectCmp:[{exists:"#cookies .cookies-btn"}],detectPopup:[{visible:"#cookies #submitCookies"}],optIn:[{waitForThenClick:"#cookies #submitCookies"}],optOut:[{waitForThenClick:"#cookies #rejectCookies"}]},{name:"Adroll",prehideSelectors:["#adroll_consent_container"],detectCmp:[{exists:"#adroll_consent_container"}],detectPopup:[{visible:"#adroll_consent_container"}],optIn:[{waitForThenClick:"#adroll_consent_accept"}],optOut:[{waitForThenClick:"#adroll_consent_reject"}],test:[{eval:"EVAL_ADROLL_0"}]},{name:"affinity.serif.com",detectCmp:[{exists:".c-cookie-banner button[data-qa='allow-all-cookies']"}],detectPopup:[{visible:".c-cookie-banner"}],optIn:[{click:'button[data-qa="allow-all-cookies"]'}],optOut:[{click:'button[data-qa="manage-cookies"]'},{waitFor:'.c-cookie-banner ~ [role="dialog"]'},{waitForThenClick:'.c-cookie-banner ~ [role="dialog"] input[type="checkbox"][value="true"]',all:!0},{click:'.c-cookie-banner ~ [role="dialog"] .c-modal__action button'}],test:[{wait:500},{eval:"EVAL_AFFINITY_SERIF_COM_0"}]},{name:"agolde.com",cosmetic:!0,prehideSelectors:["#modal-1 div[data-micromodal-close]"],detectCmp:[{exists:"#modal-1 div[aria-labelledby=modal-1-title]"}],detectPopup:[{exists:"#modal-1 div[data-micromodal-close]"}],optIn:[{click:'button[aria-label="Close modal"]'}],optOut:[{hide:"#modal-1 div[data-micromodal-close]"}]},{name:"aliexpress",vendorUrl:"https://aliexpress.com/",runContext:{urlPattern:"^https://.*\\.aliexpress\\.com/"},prehideSelectors:["#gdpr-new-container"],detectCmp:[{exists:"#gdpr-new-container"}],detectPopup:[{visible:"#gdpr-new-container"}],optIn:[{waitForThenClick:"#gdpr-new-container .btn-accept"}],optOut:[{waitForThenClick:"#gdpr-new-container .btn-more"},{waitFor:"#gdpr-new-container .gdpr-dialog-switcher"},{click:"#gdpr-new-container .switcher-on",all:!0,optional:!0},{click:"#gdpr-new-container .btn-save"}]},{name:"almacmp",prehideSelectors:["#alma-cmpv2-container"],detectCmp:[{exists:"#alma-cmpv2-container"}],detectPopup:[{visible:"#alma-cmpv2-container #almacmp-modal-layer1"}],optIn:[{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer1 #almacmp-modalConfirmBtn"}],optOut:[{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer1 #almacmp-modalSettingBtn"},{waitFor:"#alma-cmpv2-container #almacmp-modal-layer2"},{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer2 #almacmp-reject-all-layer2"}],test:[{eval:"EVAL_ALMACMP_0"}]},{name:"altium.com",cosmetic:!0,prehideSelectors:[".altium-privacy-bar"],detectCmp:[{exists:".altium-privacy-bar"}],detectPopup:[{exists:".altium-privacy-bar"}],optIn:[{click:"a.altium-privacy-bar__btn"}],optOut:[{hide:".altium-privacy-bar"}]},{name:"amazon.com",prehideSelectors:['span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'],detectCmp:[{exists:'span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'}],detectPopup:[{visible:'span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'}],optIn:[{waitForVisible:"#sp-cc-accept"},{wait:500},{click:"#sp-cc-accept"}],optOut:[{waitForVisible:"#sp-cc-rejectall-link"},{wait:500},{click:"#sp-cc-rejectall-link"}]},{name:"aquasana.com",cosmetic:!0,prehideSelectors:["#consent-tracking"],detectCmp:[{exists:"#consent-tracking"}],detectPopup:[{exists:"#consent-tracking"}],optIn:[{click:"#accept_consent"}],optOut:[{hide:"#consent-tracking"}]},{name:"arbeitsagentur",vendorUrl:"https://www.arbeitsagentur.de/",prehideSelectors:[".modal-open bahf-cookie-disclaimer-dpl3"],detectCmp:[{exists:"bahf-cookie-disclaimer-dpl3"}],detectPopup:[{visible:"bahf-cookie-disclaimer-dpl3"}],optIn:[{waitForThenClick:["bahf-cookie-disclaimer-dpl3","bahf-cd-modal-dpl3 .ba-btn-primary"]}],optOut:[{waitForThenClick:["bahf-cookie-disclaimer-dpl3","bahf-cd-modal-dpl3 .ba-btn-contrast"]}],test:[{eval:"EVAL_ARBEITSAGENTUR_TEST"}]},{name:"asus",vendorUrl:"https://www.asus.com/",runContext:{urlPattern:"^https://www\\.asus\\.com/"},prehideSelectors:["#cookie-policy-info,#cookie-policy-info-bg"],detectCmp:[{exists:"#cookie-policy-info"}],detectPopup:[{visible:"#cookie-policy-info"}],optIn:[{waitForThenClick:'#cookie-policy-info [data-agree="Accept Cookies"]'}],optOut:[{if:{exists:"#cookie-policy-info .btn-reject"},then:[{waitForThenClick:"#cookie-policy-info .btn-reject"}],else:[{waitForThenClick:"#cookie-policy-info .btn-setting"},{waitForThenClick:'#cookie-policy-lightbox-wrapper [data-agree="Save Settings"]'}]}]},{name:"athlinks-com",runContext:{urlPattern:"^https://(www\\.)?athlinks\\.com/"},cosmetic:!0,prehideSelectors:["#footer-container ~ div"],detectCmp:[{exists:"#footer-container ~ div"}],detectPopup:[{visible:"#footer-container > div"}],optIn:[{click:"#footer-container ~ div button"}],optOut:[{hide:"#footer-container ~ div"}]},{name:"ausopen.com",cosmetic:!0,detectCmp:[{exists:".gdpr-popup__message"}],detectPopup:[{visible:".gdpr-popup__message"}],optOut:[{hide:".gdpr-popup__message"}],optIn:[{click:".gdpr-popup__message button"}]},{name:"automattic-cmp-optout",prehideSelectors:['form[class*="cookie-banner"][method="post"]'],detectCmp:[{exists:'form[class*="cookie-banner"][method="post"]'}],detectPopup:[{visible:'form[class*="cookie-banner"][method="post"]'}],optIn:[{click:'a[class*="accept-all-button"]'}],optOut:[{click:'form[class*="cookie-banner"] div[class*="simple-options"] a[class*="customize-button"]'},{waitForThenClick:"input[type=checkbox][checked]:not([disabled])",all:!0},{click:'a[class*="accept-selection-button"]'}]},{name:"aws.amazon.com",prehideSelectors:["#awsccc-cb-content","#awsccc-cs-container","#awsccc-cs-modalOverlay","#awsccc-cs-container-inner"],detectCmp:[{exists:"#awsccc-cb-content"}],detectPopup:[{visible:"#awsccc-cb-content"}],optIn:[{click:"button[data-id=awsccc-cb-btn-accept"}],optOut:[{click:"button[data-id=awsccc-cb-btn-customize]"},{waitFor:"input[aria-checked]"},{click:"input[aria-checked=true]",all:!0,optional:!0},{click:"button[data-id=awsccc-cs-btn-save]"}]},{name:"axeptio",prehideSelectors:[".axeptio_widget"],detectCmp:[{exists:".axeptio_widget"}],detectPopup:[{visible:".axeptio_widget"}],optIn:[{waitFor:".axeptio-widget--open"},{click:"button#axeptio_btn_acceptAll"}],optOut:[{waitFor:".axeptio-widget--open"},{click:"button#axeptio_btn_dismiss"}],test:[{eval:"EVAL_AXEPTIO_0"}]},{name:"baden-wuerttemberg.de",prehideSelectors:[".cookie-alert.t-dark"],cosmetic:!0,detectCmp:[{exists:".cookie-alert.t-dark"}],detectPopup:[{visible:".cookie-alert.t-dark"}],optIn:[{click:".cookie-alert__form input:not([disabled]):not([checked])"},{click:".cookie-alert__button button"}],optOut:[{hide:".cookie-alert.t-dark"}]},{name:"bahn-de",vendorUrl:"https://www.bahn.de/",cosmetic:!1,runContext:{main:!0,frame:!1,urlPattern:"^https://(www\\.)?bahn\\.de/"},intermediate:!1,prehideSelectors:[],detectCmp:[{exists:["body > div:first-child","#consent-layer"]}],detectPopup:[{visible:["body > div:first-child","#consent-layer"]}],optIn:[{waitForThenClick:["body > div:first-child","#consent-layer .js-accept-all-cookies"]}],optOut:[{waitForThenClick:["body > div:first-child","#consent-layer .js-accept-essential-cookies"]}],test:[{eval:"EVAL_BAHN_TEST"}]},{name:"bbb.org",runContext:{urlPattern:"^https://www\\.bbb\\.org/"},cosmetic:!0,prehideSelectors:['div[aria-label="use of cookies on bbb.org"]'],detectCmp:[{exists:'div[aria-label="use of cookies on bbb.org"]'}],detectPopup:[{visible:'div[aria-label="use of cookies on bbb.org"]'}],optIn:[{click:'div[aria-label="use of cookies on bbb.org"] button.bds-button-unstyled span.visually-hidden'}],optOut:[{hide:'div[aria-label="use of cookies on bbb.org"]'}]},{name:"bing.com",prehideSelectors:["#bnp_container"],detectCmp:[{exists:"#bnp_cookie_banner"}],detectPopup:[{visible:"#bnp_cookie_banner"}],optIn:[{click:"#bnp_btn_accept"}],optOut:[{click:"#bnp_btn_preference"},{click:"#mcp_savesettings"}],test:[{eval:"EVAL_BING_0"}]},{name:"blocksy",vendorUrl:"https://creativethemes.com/blocksy/docs/extensions/cookies-consent/",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,prehideSelectors:[".cookie-notification"],detectCmp:[{exists:"#blocksy-ext-cookies-consent-styles-css"}],detectPopup:[{visible:".cookie-notification"}],optIn:[{click:".cookie-notification .ct-cookies-decline-button"}],optOut:[{waitForThenClick:".cookie-notification .ct-cookies-decline-button"}],test:[{eval:"EVAL_BLOCKSY_0"}]},{name:"borlabs",detectCmp:[{exists:"._brlbs-block-content"}],detectPopup:[{visible:"._brlbs-bar-wrap,._brlbs-box-wrap"}],optIn:[{click:"a[data-cookie-accept-all]"}],optOut:[{click:"a[data-cookie-individual]"},{waitForVisible:".cookie-preference"},{click:"input[data-borlabs-cookie-checkbox]:checked",all:!0,optional:!0},{click:"#CookiePrefSave"},{wait:500}],prehideSelectors:["#BorlabsCookieBox"],test:[{eval:"EVAL_BORLABS_0"}]},{name:"bundesregierung.de",prehideSelectors:[".bpa-cookie-banner"],detectCmp:[{exists:".bpa-cookie-banner"}],detectPopup:[{visible:".bpa-cookie-banner .bpa-module-full-hero"}],optIn:[{click:".bpa-accept-all-button"}],optOut:[{wait:500,comment:"click is not immediately recognized"},{waitForThenClick:".bpa-close-button"}],test:[{eval:"EVAL_BUNDESREGIERUNG_DE_0"}]},{name:"burpee.com",cosmetic:!0,prehideSelectors:["#notice-cookie-block"],detectCmp:[{exists:"#notice-cookie-block"}],detectPopup:[{exists:"#html-body #notice-cookie-block"}],optIn:[{click:"#btn-cookie-allow"}],optOut:[{hide:"#html-body #notice-cookie-block, #notice-cookie"}]},{name:"canva.com",prehideSelectors:['div[role="dialog"] a[data-anchor-id="cookie-policy"]'],detectCmp:[{exists:'div[role="dialog"] a[data-anchor-id="cookie-policy"]'}],detectPopup:[{exists:'div[role="dialog"] a[data-anchor-id="cookie-policy"]'}],optIn:[{click:'div[role="dialog"] button:nth-child(1)'}],optOut:[{if:{exists:'div[role="dialog"] button:nth-child(3)'},then:[{click:'div[role="dialog"] button:nth-child(2)'}],else:[{click:'div[role="dialog"] button:nth-child(2)'},{waitFor:'div[role="dialog"] a[data-anchor-id="cookie-policy"]'},{waitFor:'div[role="dialog"] button[role=switch]'},{click:'div[role="dialog"] button:nth-child(2):not([role])'},{click:'div[role="dialog"] div:last-child button:only-child'}]}],test:[{eval:"EVAL_CANVA_0"}]},{name:"canyon.com",runContext:{urlPattern:"^https://www\\.canyon\\.com/"},prehideSelectors:["div.modal.cookiesModal.is-open"],detectCmp:[{exists:"div.modal.cookiesModal.is-open"}],detectPopup:[{visible:"div.modal.cookiesModal.is-open"}],optIn:[{click:'div.cookiesModal__buttonWrapper > button[data-closecause="close-by-submit"]'}],optOut:[{click:'div.cookiesModal__buttonWrapper > button[data-closecause="close-by-manage-cookies"]'},{waitForThenClick:"button#js-manage-data-privacy-save-button"}]},{name:"cc-banner-springer",prehideSelectors:[".cc-banner[data-cc-banner]"],detectCmp:[{exists:".cc-banner[data-cc-banner]"}],detectPopup:[{visible:".cc-banner[data-cc-banner]"}],optIn:[{waitForThenClick:".cc-banner[data-cc-banner] button[data-cc-action=accept]"}],optOut:[{if:{exists:".cc-banner[data-cc-banner] button[data-cc-action=reject]"},then:[{click:".cc-banner[data-cc-banner] button[data-cc-action=reject]"}],else:[{waitForThenClick:".cc-banner[data-cc-banner] button[data-cc-action=preferences]"},{waitFor:".cc-preferences[data-cc-preferences]"},{click:".cc-preferences[data-cc-preferences] input[type=radio][data-cc-action=toggle-category][value=off]",all:!0,optional:!0},{if:{exists:".cc-preferences[data-cc-preferences] button[data-cc-action=reject]"},then:[{click:".cc-preferences[data-cc-preferences] button[data-cc-action=reject]"}],else:[{click:".cc-preferences[data-cc-preferences] button[data-cc-action=save]"}]}]}],test:[{eval:"EVAL_CC_BANNER2_0"}]},{name:"cc_banner",cosmetic:!0,prehideSelectors:[".cc_banner-wrapper"],detectCmp:[{exists:".cc_banner-wrapper"}],detectPopup:[{visible:".cc_banner"}],optIn:[{click:".cc_btn_accept_all"}],optOut:[{hide:".cc_banner-wrapper"}]},{name:"ciaopeople.it",prehideSelectors:["#cp-gdpr-choices"],detectCmp:[{exists:"#cp-gdpr-choices"}],detectPopup:[{visible:"#cp-gdpr-choices"}],optIn:[{waitForThenClick:".gdpr-btm__right > button:nth-child(2)"}],optOut:[{waitForThenClick:".gdpr-top-content > button"},{waitFor:".gdpr-top-back"},{waitForThenClick:".gdpr-btm__right > button:nth-child(1)"}],test:[{visible:"#cp-gdpr-choices",check:"none"}]},{vendorUrl:"https://www.civicuk.com/cookie-control/",name:"civic-cookie-control",prehideSelectors:["#ccc-module,#ccc-overlay"],detectCmp:[{exists:"#ccc-module"}],detectPopup:[{visible:"#ccc"},{visible:"#ccc-module"}],optOut:[{click:"#ccc-reject-settings"}],optIn:[{click:"#ccc-recommended-settings"}]},{name:"click.io",prehideSelectors:["#cl-consent"],detectCmp:[{exists:"#cl-consent"}],detectPopup:[{visible:"#cl-consent"}],optIn:[{waitForThenClick:'#cl-consent [data-role="b_agree"]'}],optOut:[{waitFor:'#cl-consent [data-role="b_options"]'},{wait:500},{click:'#cl-consent [data-role="b_options"]'},{waitFor:'.cl-consent-popup.cl-consent-visible [data-role="alloff"]'},{click:'.cl-consent-popup.cl-consent-visible [data-role="alloff"]',all:!0},{click:'[data-role="b_save"]'}],test:[{eval:"EVAL_CLICKIO_0",comment:"TODO: this only checks if we interacted at all"}]},{name:"clinch",intermediate:!1,runContext:{frame:!1,main:!0},prehideSelectors:[".consent-modal[role=dialog]"],detectCmp:[{exists:".consent-modal[role=dialog]"}],detectPopup:[{visible:".consent-modal[role=dialog]"}],optIn:[{click:"#consent_agree"}],optOut:[{if:{exists:"#consent_reject"},then:[{click:"#consent_reject"}],else:[{click:"#manage_cookie_preferences"},{click:"#cookie_consent_preferences input:checked",all:!0,optional:!0},{click:"#consent_save"}]}],test:[{eval:"EVAL_CLINCH_0"}]},{name:"clustrmaps.com",runContext:{urlPattern:"^https://(www\\.)?clustrmaps\\.com/"},cosmetic:!0,prehideSelectors:["#gdpr-cookie-message"],detectCmp:[{exists:"#gdpr-cookie-message"}],detectPopup:[{visible:"#gdpr-cookie-message"}],optIn:[{click:"button#gdpr-cookie-accept"}],optOut:[{hide:"#gdpr-cookie-message"}]},{name:"coinbase",intermediate:!1,runContext:{frame:!0,main:!0,urlPattern:"^https://(www|help)\\.coinbase\\.com"},prehideSelectors:[],detectCmp:[{exists:"div[class^=CookieBannerContent__Container]"}],detectPopup:[{visible:"div[class^=CookieBannerContent__Container]"}],optIn:[{click:"div[class^=CookieBannerContent__CTA] :nth-last-child(1)"}],optOut:[{click:"button[class^=CookieBannerContent__Settings]"},{click:"div[class^=CookiePreferencesModal__CategoryContainer] input:checked",all:!0,optional:!0},{click:"div[class^=CookiePreferencesModal__ButtonContainer] > button"}],test:[{eval:"EVAL_COINBASE_0"}]},{name:"Complianz banner",prehideSelectors:["#cmplz-cookiebanner-container"],detectCmp:[{exists:"#cmplz-cookiebanner-container .cmplz-cookiebanner"}],detectPopup:[{visible:"#cmplz-cookiebanner-container .cmplz-cookiebanner",check:"any"}],optIn:[{waitForThenClick:".cmplz-cookiebanner .cmplz-accept"}],optOut:[{waitForThenClick:".cmplz-cookiebanner .cmplz-deny"}],test:[{eval:"EVAL_COMPLIANZ_BANNER_0"}]},{name:"Complianz categories",prehideSelectors:['.cc-type-categories[aria-describedby="cookieconsent:desc"]'],detectCmp:[{exists:'.cc-type-categories[aria-describedby="cookieconsent:desc"]'}],detectPopup:[{visible:'.cc-type-categories[aria-describedby="cookieconsent:desc"]'}],optIn:[{any:[{click:".cc-accept-all"},{click:".cc-allow-all"},{click:".cc-allow"},{click:".cc-dismiss"}]}],optOut:[{if:{exists:'.cc-type-categories[aria-describedby="cookieconsent:desc"] .cc-dismiss'},then:[{click:".cc-dismiss"}],else:[{click:".cc-type-categories input[type=checkbox]:not([disabled]):checked",all:!0,optional:!0},{click:".cc-save"}]}]},{name:"Complianz notice",prehideSelectors:['.cc-type-info[aria-describedby="cookieconsent:desc"]'],cosmetic:!0,detectCmp:[{exists:'.cc-type-info[aria-describedby="cookieconsent:desc"] .cc-compliance .cc-btn'}],detectPopup:[{visible:'.cc-type-info[aria-describedby="cookieconsent:desc"] .cc-compliance .cc-btn'}],optIn:[{click:".cc-accept-all",optional:!0},{click:".cc-allow",optional:!0},{click:".cc-dismiss",optional:!0}],optOut:[{if:{exists:".cc-deny"},then:[{click:".cc-deny"}],else:[{hide:'[aria-describedby="cookieconsent:desc"]'}]}]},{name:"Complianz opt-both",prehideSelectors:['[aria-describedby="cookieconsent:desc"] .cc-type-opt-both'],detectCmp:[{exists:'[aria-describedby="cookieconsent:desc"] .cc-type-opt-both'}],detectPopup:[{visible:'[aria-describedby="cookieconsent:desc"] .cc-type-opt-both'}],optIn:[{click:".cc-accept-all",optional:!0},{click:".cc-allow",optional:!0},{click:".cc-dismiss",optional:!0}],optOut:[{waitForThenClick:".cc-deny"}]},{name:"Complianz opt-out",prehideSelectors:['[aria-describedby="cookieconsent:desc"].cc-type-opt-out'],detectCmp:[{exists:'[aria-describedby="cookieconsent:desc"].cc-type-opt-out'}],detectPopup:[{visible:'[aria-describedby="cookieconsent:desc"].cc-type-opt-out'}],optIn:[{click:".cc-accept-all",optional:!0},{click:".cc-allow",optional:!0},{click:".cc-dismiss",optional:!0}],optOut:[{if:{exists:".cc-deny"},then:[{click:".cc-deny"}],else:[{hide:'[aria-describedby="cookieconsent:desc"]'}]}]},{name:"Complianz optin",prehideSelectors:['.cc-type-opt-in[aria-describedby="cookieconsent:desc"]'],detectCmp:[{exists:'.cc-type-opt-in[aria-describedby="cookieconsent:desc"]'}],detectPopup:[{visible:'.cc-type-opt-in[aria-describedby="cookieconsent:desc"]'}],optIn:[{any:[{click:".cc-accept-all"},{click:".cc-allow"},{click:".cc-dismiss"}]}],optOut:[{if:{visible:".cc-deny"},then:[{click:".cc-deny"}],else:[{if:{visible:".cc-settings"},then:[{waitForThenClick:".cc-settings"},{waitForVisible:".cc-settings-view"},{click:".cc-settings-view input[type=checkbox]:not([disabled]):checked",all:!0,optional:!0},{click:".cc-settings-view .cc-btn-accept-selected"}],else:[{click:".cc-dismiss"}]}]}]},{name:"cookie-law-info",prehideSelectors:["#cookie-law-info-bar"],detectCmp:[{exists:"#cookie-law-info-bar"},{eval:"EVAL_COOKIE_LAW_INFO_DETECT"}],detectPopup:[{visible:"#cookie-law-info-bar"}],optIn:[{click:'[data-cli_action="accept_all"]'}],optOut:[{hide:"#cookie-law-info-bar"},{eval:"EVAL_COOKIE_LAW_INFO_0"}],test:[{eval:"EVAL_COOKIE_LAW_INFO_1"}]},{name:"cookie-manager-popup",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,detectCmp:[{exists:"#notice-cookie-block #allow-functional-cookies, #notice-cookie-block #btn-cookie-settings"}],detectPopup:[{visible:"#notice-cookie-block"}],optIn:[{click:"#btn-cookie-allow"}],optOut:[{if:{exists:"#allow-functional-cookies"},then:[{click:"#allow-functional-cookies"}],else:[{waitForThenClick:"#btn-cookie-settings"},{waitForVisible:".modal-body"},{click:'.modal-body input:checked, .switch[data-switch="on"]',all:!0,optional:!0},{click:'[role="dialog"] .modal-footer button'}]}],prehideSelectors:["#btn-cookie-settings"],test:[{eval:"EVAL_COOKIE_MANAGER_POPUP_0"}]},{name:"cookie-notice",prehideSelectors:["#cookie-notice"],cosmetic:!0,detectCmp:[{visible:"#cookie-notice .cookie-notice-container"}],detectPopup:[{visible:"#cookie-notice"}],optIn:[{click:"#cn-accept-cookie"}],optOut:[{hide:"#cookie-notice"}]},{name:"cookie-script",vendorUrl:"https://cookie-script.com/",prehideSelectors:["#cookiescript_injected"],detectCmp:[{exists:"#cookiescript_injected"}],detectPopup:[{visible:"#cookiescript_injected"}],optOut:[{if:{exists:"#cookiescript_reject"},then:[{wait:100},{click:"#cookiescript_reject"}],else:[{click:"#cookiescript_manage"},{waitForVisible:".cookiescript_fsd_main"},{waitForThenClick:"#cookiescript_reject"}]}],optIn:[{click:"#cookiescript_accept"}]},{name:"cookieacceptbar",vendorUrl:"https://unknown",cosmetic:!0,prehideSelectors:["#cookieAcceptBar.cookieAcceptBar"],detectCmp:[{exists:"#cookieAcceptBar.cookieAcceptBar"}],detectPopup:[{visible:"#cookieAcceptBar.cookieAcceptBar"}],optIn:[{waitForThenClick:"#cookieAcceptBarConfirm"}],optOut:[{hide:"#cookieAcceptBar.cookieAcceptBar"}]},{name:"cookiealert",intermediate:!1,prehideSelectors:[],runContext:{frame:!0,main:!0},detectCmp:[{exists:".cookie-alert-extended"}],detectPopup:[{visible:".cookie-alert-extended-modal"}],optIn:[{click:"button[data-controller='cookie-alert/extended/button/accept']"},{eval:"EVAL_COOKIEALERT_0"}],optOut:[{click:"a[data-controller='cookie-alert/extended/detail-link']"},{click:".cookie-alert-configuration-input:checked",all:!0,optional:!0},{click:"button[data-controller='cookie-alert/extended/button/configuration']"},{eval:"EVAL_COOKIEALERT_0"}],test:[{eval:"EVAL_COOKIEALERT_2"}]},{name:"cookieconsent2",vendorUrl:"https://www.github.com/orestbida/cookieconsent",comment:"supports v2.x.x of the library",prehideSelectors:["#cc--main"],detectCmp:[{exists:"#cc--main"}],detectPopup:[{visible:"#cm"},{exists:"#s-all-bn"}],optIn:[{waitForThenClick:"#s-all-bn"}],optOut:[{waitForThenClick:"#s-rall-bn"}],test:[{eval:"EVAL_COOKIECONSENT2_TEST"}]},{name:"cookieconsent3",vendorUrl:"https://www.github.com/orestbida/cookieconsent",comment:"supports v3.x.x of the library",prehideSelectors:["#cc-main"],detectCmp:[{exists:"#cc-main"}],detectPopup:[{visible:"#cc-main .cm-wrapper"}],optIn:[{waitForThenClick:".cm__btn[data-role=all]"}],optOut:[{waitForThenClick:".cm__btn[data-role=necessary]"}],test:[{eval:"EVAL_COOKIECONSENT3_TEST"}]},{name:"cookiefirst.com",prehideSelectors:["#cookiefirst-root,.cookiefirst-root,[aria-labelledby=cookie-preference-panel-title]"],detectCmp:[{exists:"#cookiefirst-root,.cookiefirst-root"}],detectPopup:[{visible:"#cookiefirst-root,.cookiefirst-root"}],optIn:[{click:"button[data-cookiefirst-action=accept]"}],optOut:[{if:{exists:"button[data-cookiefirst-action=adjust]"},then:[{click:"button[data-cookiefirst-action=adjust]"},{waitForVisible:"[data-cookiefirst-widget=modal]",timeout:1e3},{eval:"EVAL_COOKIEFIRST_1"},{wait:1e3},{click:"button[data-cookiefirst-action=save]"}],else:[{click:"button[data-cookiefirst-action=reject]"}]}],test:[{eval:"EVAL_COOKIEFIRST_0"}]},{name:"Cookie Information Banner",prehideSelectors:["#cookie-information-template-wrapper"],detectCmp:[{exists:"#cookie-information-template-wrapper"}],detectPopup:[{visible:"#cookie-information-template-wrapper"}],optIn:[{eval:"EVAL_COOKIEINFORMATION_1"}],optOut:[{hide:"#cookie-information-template-wrapper",comment:"some templates don't hide the banner automatically"},{eval:"EVAL_COOKIEINFORMATION_0"}],test:[{eval:"EVAL_COOKIEINFORMATION_2"}]},{name:"cookieyes",prehideSelectors:[".cky-overlay,.cky-consent-container"],detectCmp:[{exists:".cky-consent-container"}],detectPopup:[{visible:".cky-consent-container"}],optIn:[{waitForThenClick:".cky-consent-container [data-cky-tag=accept-button]"}],optOut:[{if:{exists:".cky-consent-container [data-cky-tag=reject-button]"},then:[{waitForThenClick:".cky-consent-container [data-cky-tag=reject-button]"}],else:[{if:{exists:".cky-consent-container [data-cky-tag=settings-button]"},then:[{click:".cky-consent-container [data-cky-tag=settings-button]"},{waitFor:".cky-modal-open input[type=checkbox]"},{click:".cky-modal-open input[type=checkbox]:checked",all:!0,optional:!0},{waitForThenClick:".cky-modal [data-cky-tag=detail-save-button]"}],else:[{hide:".cky-consent-container,.cky-overlay"}]}]}],test:[{eval:"EVAL_COOKIEYES_0"}]},{name:"corona-in-zahlen.de",prehideSelectors:[".cookiealert"],detectCmp:[{exists:".cookiealert"}],detectPopup:[{visible:".cookiealert"}],optOut:[{click:".configurecookies"},{click:".confirmcookies"}],optIn:[{click:".acceptcookies"}]},{name:"crossfit-com",cosmetic:!0,prehideSelectors:['body #modal > div > div[class^="_wrapper_"]'],detectCmp:[{exists:'body #modal > div > div[class^="_wrapper_"]'}],detectPopup:[{visible:'body #modal > div > div[class^="_wrapper_"]'}],optIn:[{click:'button[aria-label="accept cookie policy"]'}],optOut:[{hide:'body #modal > div > div[class^="_wrapper_"]'}]},{name:"csu-landtag-de",runContext:{urlPattern:"^https://(www\\.|)?csu-landtag\\.de"},prehideSelectors:["#cookie-disclaimer"],detectCmp:[{exists:"#cookie-disclaimer"}],detectPopup:[{visible:"#cookie-disclaimer"}],optIn:[{click:"#cookieall"}],optOut:[{click:"#cookiesel"}]},{name:"dailymotion-us",cosmetic:!0,prehideSelectors:['div[class*="CookiePopup__desktopContainer"]:has(div[class*="CookiePopup"])'],detectCmp:[{exists:'div[class*="CookiePopup__desktopContainer"]'}],detectPopup:[{visible:'div[class*="CookiePopup__desktopContainer"]'}],optIn:[{click:'div[class*="CookiePopup__desktopContainer"] > button > span'}],optOut:[{hide:'div[class*="CookiePopup__desktopContainer"]'}]},{name:"dailymotion.com",runContext:{urlPattern:"^https://(www\\.)?dailymotion\\.com/"},prehideSelectors:['div[class*="Overlay__container"]:has(div[class*="TCF2Popup"])'],detectCmp:[{exists:'div[class*="TCF2Popup"]'}],detectPopup:[{visible:'[class*="TCF2Popup"] a[href^="https://www.dailymotion.com/legal/cookiemanagement"]'}],optIn:[{waitForThenClick:'button[class*="TCF2Popup__button"]:not([class*="TCF2Popup__personalize"])'}],optOut:[{waitForThenClick:'button[class*="TCF2ContinueWithoutAcceptingButton"]'}],test:[{eval:"EVAL_DAILYMOTION_0"}]},{name:"deepl.com",prehideSelectors:[".dl_cookieBanner_container"],detectCmp:[{exists:".dl_cookieBanner_container"}],detectPopup:[{visible:".dl_cookieBanner_container"}],optOut:[{click:".dl_cookieBanner--buttonSelected"}],optIn:[{click:".dl_cookieBanner--buttonAll"}]},{name:"delta.com",runContext:{urlPattern:"^https://www\\.delta\\.com/"},cosmetic:!0,prehideSelectors:["ngc-cookie-banner"],detectCmp:[{exists:"div.cookie-footer-container"}],detectPopup:[{visible:"div.cookie-footer-container"}],optIn:[{click:" button.cookie-close-icon"}],optOut:[{hide:"div.cookie-footer-container"}]},{name:"dmgmedia-us",prehideSelectors:["#mol-ads-cmp-iframe, div.mol-ads-cmp > form > div"],detectCmp:[{exists:"div.mol-ads-cmp > form > div"}],detectPopup:[{waitForVisible:"div.mol-ads-cmp > form > div"}],optIn:[{waitForThenClick:"button.mol-ads-cmp--btn-primary"}],optOut:[{waitForThenClick:"div.mol-ads-ccpa--message > u > a"},{waitForVisible:".mol-ads-cmp--modal-dialog"},{waitForThenClick:"a.mol-ads-cmp-footer-privacy"},{waitForThenClick:"button.mol-ads-cmp--btn-secondary"}]},{name:"dmgmedia",prehideSelectors:['[data-project="mol-fe-cmp"]'],detectCmp:[{exists:'[data-project="mol-fe-cmp"]'}],detectPopup:[{visible:'[data-project="mol-fe-cmp"]'}],optIn:[{waitForThenClick:'[data-project="mol-fe-cmp"] button[class*=primary]'}],optOut:[{waitForThenClick:'[data-project="mol-fe-cmp"] button[class*=basic]'},{waitForVisible:'[data-project="mol-fe-cmp"] div[class*="tabContent"]'},{waitForThenClick:'[data-project="mol-fe-cmp"] div[class*="toggle"][class*="enabled"]',all:!0},{waitForThenClick:'[data-project="mol-fe-cmp"] button[class*=white]'}]},{name:"dndbeyond",vendorUrl:"https://www.dndbeyond.com/",runContext:{urlPattern:"^https://(www\\.)?dndbeyond\\.com/"},prehideSelectors:["[id^=cookie-consent-banner]"],detectCmp:[{exists:"[id^=cookie-consent-banner]"}],detectPopup:[{visible:"[id^=cookie-consent-banner]"}],optIn:[{waitForThenClick:"#cookie-consent-granted"}],optOut:[{waitForThenClick:"#cookie-consent-denied"}],test:[{eval:"EVAL_DNDBEYOND_TEST"}]},{name:"Drupal",detectCmp:[{exists:"#drupalorg-crosssite-gdpr"}],detectPopup:[{visible:"#drupalorg-crosssite-gdpr"}],optOut:[{click:".no"}],optIn:[{click:".yes"}]},{name:"WP DSGVO Tools",link:"https://wordpress.org/plugins/shapepress-dsgvo/",prehideSelectors:[".sp-dsgvo"],cosmetic:!0,detectCmp:[{exists:".sp-dsgvo.sp-dsgvo-popup-overlay"}],detectPopup:[{visible:".sp-dsgvo.sp-dsgvo-popup-overlay",check:"any"}],optIn:[{click:".sp-dsgvo-privacy-btn-accept-all",all:!0}],optOut:[{hide:".sp-dsgvo.sp-dsgvo-popup-overlay"}],test:[{eval:"EVAL_DSGVO_0"}]},{name:"dunelm.com",prehideSelectors:["div[data-testid=cookie-consent-modal-backdrop]"],detectCmp:[{exists:"div[data-testid=cookie-consent-message-contents]"}],detectPopup:[{visible:"div[data-testid=cookie-consent-message-contents]"}],optIn:[{click:'[data-testid="cookie-consent-allow-all"]'}],optOut:[{click:"button[data-testid=cookie-consent-adjust-settings]"},{click:"button[data-testid=cookie-consent-preferences-save]"}],test:[{eval:"EVAL_DUNELM_0"}]},{name:"ecosia",vendorUrl:"https://www.ecosia.org/",runContext:{urlPattern:"^https://www\\.ecosia\\.org/"},prehideSelectors:[".cookie-wrapper"],detectCmp:[{exists:".cookie-wrapper > .cookie-notice"}],detectPopup:[{visible:".cookie-wrapper > .cookie-notice"}],optIn:[{waitForThenClick:"[data-test-id=cookie-notice-accept]"}],optOut:[{waitForThenClick:"[data-test-id=cookie-notice-reject]"}]},{name:"Ensighten ensModal",prehideSelectors:[".ensModal"],detectCmp:[{exists:".ensModal"}],detectPopup:[{visible:".ensModal"}],optIn:[{waitForThenClick:"#modalAcceptButton"}],optOut:[{waitForThenClick:".ensCheckbox:checked",all:!0},{waitForThenClick:"#ensSave"}]},{name:"Ensighten ensNotifyBanner",prehideSelectors:["#ensNotifyBanner"],detectCmp:[{exists:"#ensNotifyBanner"}],detectPopup:[{visible:"#ensNotifyBanner"}],optIn:[{waitForThenClick:"#ensCloseBanner"}],optOut:[{waitForThenClick:"#ensRejectAll,#rejectAll,#ensRejectBanner"}]},{name:"etsy",prehideSelectors:["#gdpr-single-choice-overlay","#gdpr-privacy-settings"],detectCmp:[{exists:"#gdpr-single-choice-overlay"}],detectPopup:[{visible:"#gdpr-single-choice-overlay"}],optOut:[{click:"button[data-gdpr-open-full-settings]"},{waitForVisible:".gdpr-overlay-body input",timeout:3e3},{wait:1e3},{eval:"EVAL_ETSY_0"},{eval:"EVAL_ETSY_1"}],optIn:[{click:"button[data-gdpr-single-choice-accept]"}]},{name:"eu-cookie-compliance-banner",detectCmp:[{exists:"body.eu-cookie-compliance-popup-open"}],detectPopup:[{exists:"body.eu-cookie-compliance-popup-open"}],optIn:[{click:".agree-button"}],optOut:[{if:{visible:".decline-button,.eu-cookie-compliance-save-preferences-button"},then:[{click:".decline-button,.eu-cookie-compliance-save-preferences-button"}]},{hide:".eu-cookie-compliance-banner-info, #sliding-popup"}],test:[{eval:"EVAL_EU_COOKIE_COMPLIANCE_0"}]},{name:"EU Cookie Law",prehideSelectors:[".pea_cook_wrapper,.pea_cook_more_info_popover"],cosmetic:!0,detectCmp:[{exists:".pea_cook_wrapper"}],detectPopup:[{wait:500},{visible:".pea_cook_wrapper"}],optIn:[{click:"#pea_cook_btn"}],optOut:[{hide:".pea_cook_wrapper"}],test:[{eval:"EVAL_EU_COOKIE_LAW_0"}]},{name:"europa-eu",vendorUrl:"https://ec.europa.eu/",runContext:{urlPattern:"^https://[^/]*europa\\.eu/"},prehideSelectors:["#cookie-consent-banner"],detectCmp:[{exists:".cck-container"}],detectPopup:[{visible:".cck-container"}],optIn:[{waitForThenClick:'.cck-actions-button[href="#accept"]'}],optOut:[{waitForThenClick:'.cck-actions-button[href="#refuse"]',hide:".cck-container"}]},{name:"EZoic",prehideSelectors:["#ez-cookie-dialog-wrapper"],detectCmp:[{exists:"#ez-cookie-dialog-wrapper"}],detectPopup:[{visible:"#ez-cookie-dialog-wrapper"}],optIn:[{click:"#ez-accept-all",optional:!0},{eval:"EVAL_EZOIC_0",optional:!0}],optOut:[{wait:500},{click:"#ez-manage-settings"},{waitFor:"#ez-cookie-dialog input[type=checkbox]"},{click:"#ez-cookie-dialog input[type=checkbox]:checked",all:!0},{click:"#ez-save-settings"}],test:[{eval:"EVAL_EZOIC_1"}]},{name:"facebook",runContext:{urlPattern:"^https://([a-z0-9-]+\\.)?facebook\\.com/"},prehideSelectors:['div[data-testid="cookie-policy-manage-dialog"]'],detectCmp:[{exists:'div[data-testid="cookie-policy-manage-dialog"]'}],detectPopup:[{visible:'div[data-testid="cookie-policy-manage-dialog"]'}],optIn:[{waitForThenClick:'button[data-cookiebanner="accept_button"]'},{waitForVisible:'div[data-testid="cookie-policy-manage-dialog"]',check:"none"}],optOut:[{waitForThenClick:'button[data-cookiebanner="accept_only_essential_button"]'},{waitForVisible:'div[data-testid="cookie-policy-manage-dialog"]',check:"none"}]},{name:"fides",vendorUrl:"https://github.com/ethyca/fides",prehideSelectors:["#fides-overlay"],detectCmp:[{exists:"#fides-overlay #fides-banner"}],detectPopup:[{visible:"#fides-overlay #fides-banner"}],optIn:[{waitForThenClick:'#fides-banner [data-testid="Accept all-btn"]'}],optOut:[{waitForThenClick:'#fides-banner [data-testid="Reject all-btn"]'}]},{name:"funding-choices",prehideSelectors:[".fc-consent-root,.fc-dialog-container,.fc-dialog-overlay,.fc-dialog-content"],detectCmp:[{exists:".fc-consent-root"}],detectPopup:[{exists:".fc-dialog-container"}],optOut:[{click:".fc-cta-do-not-consent,.fc-cta-manage-options"},{click:".fc-preference-consent:checked,.fc-preference-legitimate-interest:checked",all:!0,optional:!0},{click:".fc-confirm-choices",optional:!0}],optIn:[{click:".fc-cta-consent"}]},{name:"geeks-for-geeks",runContext:{urlPattern:"^https://www\\.geeksforgeeks\\.org/"},cosmetic:!0,prehideSelectors:[".cookie-consent"],detectCmp:[{exists:".cookie-consent"}],detectPopup:[{visible:".cookie-consent"}],optIn:[{click:".cookie-consent button.consent-btn"}],optOut:[{hide:".cookie-consent"}]},{name:"generic-cosmetic",cosmetic:!0,prehideSelectors:["#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"],detectCmp:[{exists:"#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"}],detectPopup:[{visible:"#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"}],optIn:[],optOut:[{hide:"#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"}]},{name:"google-consent-standalone",prehideSelectors:[],detectCmp:[{exists:'a[href^="https://policies.google.com/technologies/cookies"'},{exists:'form[action^="https://consent.google."][action$=".com/save"]'}],detectPopup:[{visible:'a[href^="https://policies.google.com/technologies/cookies"'}],optIn:[{waitForThenClick:'form[action^="https://consent.google."][action$=".com/save"]:has(input[name=set_eom][value=false]) button'}],optOut:[{waitForThenClick:'form[action^="https://consent.google."][action$=".com/save"]:has(input[name=set_eom][value=true]) button'}]},{name:"google.com",prehideSelectors:[".HTjtHe#xe7COe"],detectCmp:[{exists:".HTjtHe#xe7COe"},{exists:'.HTjtHe#xe7COe a[href^="https://policies.google.com/technologies/cookies"]'}],detectPopup:[{visible:".HTjtHe#xe7COe button#W0wltc"}],optIn:[{waitForThenClick:".HTjtHe#xe7COe button#L2AGLb"}],optOut:[{waitForThenClick:".HTjtHe#xe7COe button#W0wltc"}],test:[{eval:"EVAL_GOOGLE_0"}]},{name:"gov.uk",detectCmp:[{exists:"#global-cookie-message"}],detectPopup:[{exists:"#global-cookie-message"}],optIn:[{click:"button[data-accept-cookies=true]"}],optOut:[{click:"button[data-reject-cookies=true],#reject-cookies"},{click:"button[data-hide-cookie-banner=true],#hide-cookie-decision"}]},{name:"hashicorp",vendorUrl:"https://hashicorp.com/",runContext:{urlPattern:"^https://[^.]*\\.hashicorp\\.com/"},prehideSelectors:["[data-testid=consent-banner]"],detectCmp:[{exists:"[data-testid=consent-banner]"}],detectPopup:[{visible:"[data-testid=consent-banner]"}],optIn:[{waitForThenClick:"[data-testid=accept]"}],optOut:[{waitForThenClick:"[data-testid=manage-preferences]"},{waitForThenClick:"[data-testid=consent-mgr-dialog] [data-ga-button=save-preferences]"}]},{name:"healthline-media",prehideSelectors:["#modal-host > div.no-hash > div.window-wrapper"],detectCmp:[{exists:"#modal-host > div.no-hash > div.window-wrapper, div[data-testid=qualtrics-container]"}],detectPopup:[{exists:"#modal-host > div.no-hash > div.window-wrapper, div[data-testid=qualtrics-container]"}],optIn:[{click:"#modal-host > div.no-hash > div.window-wrapper > div:last-child button"}],optOut:[{if:{exists:'#modal-host > div.no-hash > div.window-wrapper > div:last-child a[href="/privacy-settings"]'},then:[{click:'#modal-host > div.no-hash > div.window-wrapper > div:last-child a[href="/privacy-settings"]'}],else:[{waitForVisible:"div#__next"},{click:"#__next div:nth-child(1) > button:first-child"}]}]},{name:"hema",prehideSelectors:[".cookie-modal"],detectCmp:[{visible:".cookie-modal .cookie-accept-btn"}],detectPopup:[{visible:".cookie-modal .cookie-accept-btn"}],optIn:[{waitForThenClick:".cookie-modal .cookie-accept-btn"}],optOut:[{waitForThenClick:".cookie-modal .js-cookie-reject-btn"}],test:[{eval:"EVAL_HEMA_TEST_0"}]},{name:"hetzner.com",runContext:{urlPattern:"^https://www\\.hetzner\\.com/"},prehideSelectors:["#CookieConsent"],detectCmp:[{exists:"#CookieConsent"}],detectPopup:[{visible:"#CookieConsent"}],optIn:[{click:"#CookieConsentGiven"}],optOut:[{click:"#CookieConsentDeclined"}]},{name:"hl.co.uk",prehideSelectors:[".cookieModalContent","#cookie-banner-overlay"],detectCmp:[{exists:"#cookie-banner-overlay"}],detectPopup:[{exists:"#cookie-banner-overlay"}],optIn:[{click:"#acceptCookieButton"}],optOut:[{click:"#manageCookie"},{hide:".cookieSettingsModal"},{waitFor:"#AOCookieToggle"},{click:"#AOCookieToggle[aria-pressed=true]",optional:!0},{waitFor:"#TPCookieToggle"},{click:"#TPCookieToggle[aria-pressed=true]",optional:!0},{click:"#updateCookieButton"}]},{name:"hu-manity",vendorUrl:"https://hu-manity.co/",prehideSelectors:["#hu.hu-wrapper"],detectCmp:[{exists:"#hu.hu-visible"}],detectPopup:[{visible:"#hu.hu-visible"}],optIn:[{waitForThenClick:"[data-hu-action=cookies-notice-consent-choices-3]"},{waitForThenClick:"#hu-cookies-save"}],optOut:[{waitForThenClick:"#hu-cookies-save"}]},{name:"hubspot",detectCmp:[{exists:"#hs-eu-cookie-confirmation"}],detectPopup:[{visible:"#hs-eu-cookie-confirmation"}],optIn:[{click:"#hs-eu-confirmation-button"}],optOut:[{click:"#hs-eu-decline-button"}]},{name:"indeed.com",cosmetic:!0,prehideSelectors:["#CookiePrivacyNotice"],detectCmp:[{exists:"#CookiePrivacyNotice"}],detectPopup:[{visible:"#CookiePrivacyNotice"}],optIn:[{click:"#CookiePrivacyNotice button[data-gnav-element-name=CookiePrivacyNoticeOk]"}],optOut:[{hide:"#CookiePrivacyNotice"}]},{name:"ing.de",runContext:{urlPattern:"^https://www\\.ing\\.de/"},cosmetic:!0,prehideSelectors:['div[slot="backdrop"]'],detectCmp:[{exists:'[data-tag-name="ing-cc-dialog-frame"]'}],detectPopup:[{visible:'[data-tag-name="ing-cc-dialog-frame"]'}],optIn:[{click:['[data-tag-name="ing-cc-dialog-level0"]','[data-tag-name="ing-cc-button"][class*="accept"]']}],optOut:[{click:['[data-tag-name="ing-cc-dialog-level0"]','[data-tag-name="ing-cc-button"][class*="more"]']}]},{name:"instagram",vendorUrl:"https://instagram.com",runContext:{urlPattern:"^https://www\\.instagram\\.com/"},prehideSelectors:[".x78zum5.xdt5ytf.xg6iff7.x1n2onr6"],detectCmp:[{exists:".x1qjc9v5.x9f619.x78zum5.xdt5ytf.x1iyjqo2.xl56j7k"}],detectPopup:[{visible:".x1qjc9v5.x9f619.x78zum5.xdt5ytf.x1iyjqo2.xl56j7k"}],optIn:[{waitForThenClick:"._a9--._a9_0"}],optOut:[{waitForThenClick:"._a9--._a9_1"},{wait:2e3}]},{name:"ionos.de",prehideSelectors:[".privacy-consent--backdrop",".privacy-consent--modal"],detectCmp:[{exists:".privacy-consent--modal"}],detectPopup:[{visible:".privacy-consent--modal"}],optIn:[{click:"#selectAll"}],optOut:[{click:".footer-config-link"},{click:"#confirmSelection"}]},{name:"itopvpn.com",cosmetic:!0,prehideSelectors:[".pop-cookie"],detectCmp:[{exists:".pop-cookie"}],detectPopup:[{exists:".pop-cookie"}],optIn:[{click:"#_pcookie"}],optOut:[{hide:".pop-cookie"}]},{name:"iubenda",prehideSelectors:["#iubenda-cs-banner"],detectCmp:[{exists:"#iubenda-cs-banner"}],detectPopup:[{visible:".iubenda-cs-accept-btn"}],optIn:[{click:".iubenda-cs-accept-btn"}],optOut:[{click:".iubenda-cs-customize-btn"},{eval:"EVAL_IUBENDA_0"},{click:"#iubFooterBtn"}],test:[{eval:"EVAL_IUBENDA_1"}]},{name:"iWink",prehideSelectors:["body.cookies-request #cookie-bar"],detectCmp:[{exists:"body.cookies-request #cookie-bar"}],detectPopup:[{visible:"body.cookies-request #cookie-bar"}],optIn:[{waitForThenClick:"body.cookies-request #cookie-bar .allow-cookies"}],optOut:[{waitForThenClick:"body.cookies-request #cookie-bar .disallow-cookies"}],test:[{eval:"EVAL_IWINK_TEST"}]},{name:"jdsports",vendorUrl:"https://www.jdsports.co.uk/",runContext:{urlPattern:"^https://(www|m)\\.jdsports\\."},prehideSelectors:[".miniConsent,#PrivacyPolicyBanner"],detectCmp:[{exists:".miniConsent,#PrivacyPolicyBanner"}],detectPopup:[{visible:".miniConsent,#PrivacyPolicyBanner"}],optIn:[{waitForThenClick:".miniConsent .accept-all-cookies"}],optOut:[{if:{exists:"#PrivacyPolicyBanner"},then:[{hide:"#PrivacyPolicyBanner"}],else:[{waitForThenClick:"#cookie-settings"},{waitForThenClick:"#reject-all-cookies"}]}]},{name:"johnlewis.com",prehideSelectors:["div[class^=pecr-cookie-banner-]"],detectCmp:[{exists:"div[class^=pecr-cookie-banner-]"}],detectPopup:[{exists:"div[class^=pecr-cookie-banner-]"}],optOut:[{click:"button[data-test^=manage-cookies]"},{wait:"500"},{click:"label[data-test^=toggle][class*=checked]:not([class*=disabled])",all:!0,optional:!0},{click:"button[data-test=save-preferences]"}],optIn:[{click:"button[data-test=allow-all]"}]},{name:"jquery.cookieBar",vendorUrl:"https://github.com/kovarp/jquery.cookieBar",prehideSelectors:[".cookie-bar"],cosmetic:!0,detectCmp:[{exists:".cookie-bar .cookie-bar__message,.cookie-bar .cookie-bar__buttons"}],detectPopup:[{visible:".cookie-bar .cookie-bar__message,.cookie-bar .cookie-bar__buttons",check:"any"}],optIn:[{click:".cookie-bar .cookie-bar__btn"}],optOut:[{hide:".cookie-bar"}],test:[{visible:".cookie-bar .cookie-bar__message,.cookie-bar .cookie-bar__buttons",check:"none"},{eval:"EVAL_JQUERY_COOKIEBAR_0"}]},{name:"justwatch.com",prehideSelectors:[".consent-banner"],detectCmp:[{exists:".consent-banner .consent-banner__actions"}],detectPopup:[{visible:".consent-banner .consent-banner__actions"}],optIn:[{click:".consent-banner__actions button.basic-button.primary"}],optOut:[{click:".consent-banner__actions button.basic-button.secondary"},{waitForThenClick:".consent-modal__footer button.basic-button.secondary"},{waitForThenClick:".consent-modal ion-content > div > a:nth-child(9)"},{click:"label.consent-switch input[type=checkbox]:checked",all:!0,optional:!0},{waitForVisible:".consent-modal__footer button.basic-button.primary"},{click:".consent-modal__footer button.basic-button.primary"}]},{name:"ketch",vendorUrl:"https://www.ketch.com",runContext:{frame:!1,main:!0},intermediate:!1,prehideSelectors:["#lanyard_root div[role='dialog']"],detectCmp:[{exists:"#lanyard_root div[role='dialog']"}],detectPopup:[{visible:"#lanyard_root div[role='dialog']"}],optIn:[{if:{exists:"#lanyard_root button[class='confirmButton']"},then:[{waitForThenClick:"#lanyard_root div[class*=buttons] > :nth-child(2)"},{click:"#lanyard_root button[class='confirmButton']"}],else:[{waitForThenClick:"#lanyard_root div[class*=buttons] > :nth-child(2)"}]}],optOut:[{if:{exists:"#lanyard_root [aria-describedby=banner-description]"},then:[{waitForThenClick:"#lanyard_root div[class*=buttons] > button[class*=secondaryButton]",comment:"can be either settings or reject button"}]},{waitFor:"#lanyard_root [aria-describedby=preference-description],#lanyard_root [aria-describedby=modal-description]",timeout:1e3,optional:!0},{if:{exists:"#lanyard_root [aria-describedby=preference-description],#lanyard_root [aria-describedby=modal-description]"},then:[{waitForThenClick:"#lanyard_root button[class*=rejectButton]"},{click:"#lanyard_root button[class*=confirmButton],#lanyard_root div[class*=actions_] > button:nth-child(1)"}]}]},{name:"kleinanzeigen-de",runContext:{urlPattern:"^https?://(www\\.)?kleinanzeigen\\.de"},prehideSelectors:["#gdpr-banner-container"],detectCmp:[{any:[{exists:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-cmp-button]"},{exists:"#ConsentManagementPage"}]}],detectPopup:[{any:[{visible:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-cmp-button]"},{visible:"#ConsentManagementPage"}]}],optIn:[{if:{exists:"#gdpr-banner-container #gdpr-banner"},then:[{click:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-accept]"}],else:[{click:"#ConsentManagementPage .Button-primary"}]}],optOut:[{if:{exists:"#gdpr-banner-container #gdpr-banner"},then:[{click:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-cmp-button]"}],else:[{click:"#ConsentManagementPage .Button-secondary"}]}]},{name:"lightbox",prehideSelectors:[".darken-layer.open,.lightbox.lightbox--cookie-consent"],detectCmp:[{exists:"body.cookie-consent-is-active div.lightbox--cookie-consent > div.lightbox__content > div.cookie-consent[data-jsb]"}],detectPopup:[{visible:"body.cookie-consent-is-active div.lightbox--cookie-consent > div.lightbox__content > div.cookie-consent[data-jsb]"}],optOut:[{click:".cookie-consent__footer > button[type='submit']:not([data-button='selectAll'])"}],optIn:[{click:".cookie-consent__footer > button[type='submit'][data-button='selectAll']"}]},{name:"lineagrafica",vendorUrl:"https://addons.prestashop.com/en/legal/8734-eu-cookie-law-gdpr-banner-blocker.html",cosmetic:!0,prehideSelectors:["#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"],detectCmp:[{exists:"#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"}],detectPopup:[{exists:"#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"}],optIn:[{waitForThenClick:"#lgcookieslaw_accept"}],optOut:[{hide:"#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"}]},{name:"linkedin.com",prehideSelectors:[".artdeco-global-alert[type=COOKIE_CONSENT]"],detectCmp:[{exists:".artdeco-global-alert[type=COOKIE_CONSENT]"}],detectPopup:[{visible:".artdeco-global-alert[type=COOKIE_CONSENT]"}],optIn:[{waitForVisible:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=ACCEPT]"},{wait:500},{waitForThenClick:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=ACCEPT]"}],optOut:[{waitForVisible:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=DENY]"},{wait:500},{waitForThenClick:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=DENY]"}],test:[{waitForVisible:".artdeco-global-alert[type=COOKIE_CONSENT]",check:"none"}]},{name:"livejasmin",vendorUrl:"https://www.livejasmin.com/",runContext:{urlPattern:"^https://(m|www)\\.livejasmin\\.com/"},prehideSelectors:["#consent_modal"],detectCmp:[{exists:"#consent_modal"}],detectPopup:[{visible:"#consent_modal"}],optIn:[{waitForThenClick:"#consent_modal button[data-testid=ButtonStyledButton]:first-of-type"}],optOut:[{waitForThenClick:"#consent_modal button[data-testid=ButtonStyledButton]:nth-of-type(2)"},{waitForVisible:"[data-testid=PrivacyPreferenceCenterWithConsentCookieContent]"},{click:"[data-testid=PrivacyPreferenceCenterWithConsentCookieContent] input[data-testid=PrivacyPreferenceCenterWithConsentCookieSwitch]:checked",optional:!0,all:!0},{waitForThenClick:"[data-testid=PrivacyPreferenceCenterWithConsentCookieContent] button[data-testid=ButtonStyledButton]:last-child"}]},{name:"macpaw.com",cosmetic:!0,prehideSelectors:['div[data-banner="cookies"]'],detectCmp:[{exists:'div[data-banner="cookies"]'}],detectPopup:[{exists:'div[data-banner="cookies"]'}],optIn:[{click:'button[data-banner-close="cookies"]'}],optOut:[{hide:'div[data-banner="cookies"]'}]},{name:"marksandspencer.com",cosmetic:!0,detectCmp:[{exists:".navigation-cookiebbanner"}],detectPopup:[{visible:".navigation-cookiebbanner"}],optOut:[{hide:".navigation-cookiebbanner"}],optIn:[{click:".navigation-cookiebbanner__submit"}]},{name:"mediamarkt.de",prehideSelectors:["div[aria-labelledby=pwa-consent-layer-title]","div[class^=StyledConsentLayerWrapper-]"],detectCmp:[{exists:"div[aria-labelledby^=pwa-consent-layer-title]"}],detectPopup:[{exists:"div[aria-labelledby^=pwa-consent-layer-title]"}],optOut:[{click:"button[data-test^=pwa-consent-layer-deny-all]"}],optIn:[{click:"button[data-test^=pwa-consent-layer-accept-all"}]},{name:"Mediavine",prehideSelectors:['[data-name="mediavine-gdpr-cmp"]'],detectCmp:[{exists:'[data-name="mediavine-gdpr-cmp"]'}],detectPopup:[{wait:500},{visible:'[data-name="mediavine-gdpr-cmp"]'}],optIn:[{waitForThenClick:'[data-name="mediavine-gdpr-cmp"] [format="primary"]'}],optOut:[{waitForThenClick:'[data-name="mediavine-gdpr-cmp"] [data-view="manageSettings"]'},{waitFor:'[data-name="mediavine-gdpr-cmp"] input[type=checkbox]'},{eval:"EVAL_MEDIAVINE_0",optional:!0},{click:'[data-name="mediavine-gdpr-cmp"] [format="secondary"]'}]},{name:"microsoft.com",prehideSelectors:["#wcpConsentBannerCtrl"],detectCmp:[{exists:"#wcpConsentBannerCtrl"}],detectPopup:[{exists:"#wcpConsentBannerCtrl"}],optOut:[{eval:"EVAL_MICROSOFT_0"}],optIn:[{eval:"EVAL_MICROSOFT_1"}],test:[{eval:"EVAL_MICROSOFT_2"}]},{name:"midway-usa",runContext:{urlPattern:"^https://www\\.midwayusa\\.com/"},cosmetic:!0,prehideSelectors:["#cookie-container"],detectCmp:[{exists:['div[aria-label="Cookie Policy Banner"]']}],detectPopup:[{visible:"#cookie-container"}],optIn:[{click:"button#cookie-btn"}],optOut:[{hide:'div[aria-label="Cookie Policy Banner"]'}]},{name:"moneysavingexpert.com",detectCmp:[{exists:"dialog[data-testid=accept-our-cookies-dialog]"}],detectPopup:[{visible:"dialog[data-testid=accept-our-cookies-dialog]"}],optIn:[{click:"#banner-accept"}],optOut:[{click:"#banner-manage"},{click:"#pc-confirm"}]},{name:"monzo.com",prehideSelectors:[".cookie-alert, cookie-alert__content"],detectCmp:[{exists:'div.cookie-alert[role="dialog"]'},{exists:'a[href*="monzo"]'}],detectPopup:[{visible:".cookie-alert__content"}],optIn:[{click:".js-accept-cookie-policy"}],optOut:[{click:".js-decline-cookie-policy"}]},{name:"Moove",prehideSelectors:["#moove_gdpr_cookie_info_bar"],detectCmp:[{exists:"#moove_gdpr_cookie_info_bar"}],detectPopup:[{visible:"#moove_gdpr_cookie_info_bar:not(.moove-gdpr-info-bar-hidden)"}],optIn:[{waitForThenClick:".moove-gdpr-infobar-allow-all"}],optOut:[{if:{exists:"#moove_gdpr_cookie_info_bar .change-settings-button"},then:[{click:"#moove_gdpr_cookie_info_bar .change-settings-button"},{waitForVisible:"#moove_gdpr_cookie_modal"},{eval:"EVAL_MOOVE_0"},{click:".moove-gdpr-modal-save-settings"}],else:[{hide:"#moove_gdpr_cookie_info_bar"}]}],test:[{visible:"#moove_gdpr_cookie_info_bar",check:"none"}]},{name:"national-lottery.co.uk",detectCmp:[{exists:".cuk_cookie_consent"}],detectPopup:[{visible:".cuk_cookie_consent",check:"any"}],optOut:[{click:".cuk_cookie_consent_manage_pref"},{click:".cuk_cookie_consent_save_pref"},{click:".cuk_cookie_consent_close"}],optIn:[{click:".cuk_cookie_consent_accept_all"}]},{name:"nba.com",runContext:{urlPattern:"^https://(www\\.)?nba.com/"},cosmetic:!0,prehideSelectors:["#onetrust-banner-sdk"],detectCmp:[{exists:"#onetrust-banner-sdk"}],detectPopup:[{visible:"#onetrust-banner-sdk"}],optIn:[{click:"#onetrust-accept-btn-handler"}],optOut:[{hide:"#onetrust-banner-sdk"}]},{name:"netflix.de",detectCmp:[{exists:"#cookie-disclosure"}],detectPopup:[{visible:".cookie-disclosure-message",check:"any"}],optIn:[{click:".btn-accept"}],optOut:[{hide:"#cookie-disclosure"},{click:".btn-reject"}]},{name:"nhs.uk",prehideSelectors:["#nhsuk-cookie-banner"],detectCmp:[{exists:"#nhsuk-cookie-banner"}],detectPopup:[{exists:"#nhsuk-cookie-banner"}],optOut:[{click:"#nhsuk-cookie-banner__link_accept"}],optIn:[{click:"#nhsuk-cookie-banner__link_accept_analytics"}]},{name:"notice-cookie",prehideSelectors:[".button--notice"],cosmetic:!0,detectCmp:[{exists:".notice--cookie"}],detectPopup:[{visible:".notice--cookie"}],optIn:[{click:".button--notice"}],optOut:[{hide:".notice--cookie"}]},{name:"nrk.no",cosmetic:!0,prehideSelectors:[".nrk-masthead__info-banner--cookie"],detectCmp:[{exists:".nrk-masthead__info-banner--cookie"}],detectPopup:[{exists:".nrk-masthead__info-banner--cookie"}],optIn:[{click:"div.nrk-masthead__info-banner--cookie button > span:has(+ svg.nrk-close)"}],optOut:[{hide:".nrk-masthead__info-banner--cookie"}]},{name:"obi.de",prehideSelectors:[".disc-cp--active"],detectCmp:[{exists:".disc-cp-modal__modal"}],detectPopup:[{visible:".disc-cp-modal__modal"}],optIn:[{click:".js-disc-cp-accept-all"}],optOut:[{click:".js-disc-cp-deny-all"}]},{name:"om",vendorUrl:"https://olli-machts.de/en/extension/cookie-manager",prehideSelectors:[".tx-om-cookie-consent"],detectCmp:[{exists:".tx-om-cookie-consent .active[data-omcookie-panel]"}],detectPopup:[{exists:".tx-om-cookie-consent .active[data-omcookie-panel]"}],optIn:[{waitForThenClick:"[data-omcookie-panel-save=all]"}],optOut:[{if:{exists:"[data-omcookie-panel-save=min]"},then:[{waitForThenClick:"[data-omcookie-panel-save=min]"}],else:[{click:"input[data-omcookie-panel-grp]:checked:not(:disabled)",all:!0,optional:!0},{waitForThenClick:"[data-omcookie-panel-save=save]"}]}]},{name:"onlyFans.com",runContext:{urlPattern:"^https://onlyfans\\.com/"},prehideSelectors:["div.b-cookies-informer"],detectCmp:[{exists:"div.b-cookies-informer"}],detectPopup:[{exists:"div.b-cookies-informer"}],optIn:[{click:"div.b-cookies-informer__nav > button:nth-child(2)"}],optOut:[{click:"div.b-cookies-informer__nav > button:nth-child(1)"},{if:{exists:"div.b-cookies-informer__switchers"},then:[{click:"div.b-cookies-informer__switchers input:not([disabled])",all:!0},{click:"div.b-cookies-informer__nav > button"}]}]},{name:"openli",vendorUrl:"https://openli.com",prehideSelectors:[".legalmonster-cleanslate"],detectCmp:[{exists:".legalmonster-cleanslate"}],detectPopup:[{visible:".legalmonster-cleanslate #lm-cookie-wall-container",check:"any"}],optIn:[{waitForThenClick:"#lm-accept-all"}],optOut:[{waitForThenClick:"#lm-accept-necessary"}]},{name:"opera.com",vendorUrl:"https://unknown",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,prehideSelectors:[],detectCmp:[{exists:"#cookie-consent .manage-cookies__btn"}],detectPopup:[{visible:"#cookie-consent .cookie-basic-consent__btn"}],optIn:[{waitForThenClick:"#cookie-consent .cookie-basic-consent__btn"}],optOut:[{waitForThenClick:"#cookie-consent .manage-cookies__btn"},{waitForThenClick:"#cookie-consent .active.marketing_option_switch.cookie-consent__switch",all:!0},{waitForThenClick:"#cookie-consent .cookie-selection__btn"}],test:[{eval:"EVAL_OPERA_0"}]},{name:"osano",prehideSelectors:[".osano-cm-window,.osano-cm-dialog"],detectCmp:[{exists:".osano-cm-window"}],detectPopup:[{visible:".osano-cm-dialog"}],optIn:[{click:".osano-cm-accept-all",optional:!0}],optOut:[{waitForThenClick:".osano-cm-denyAll"}]},{name:"otto.de",prehideSelectors:[".cookieBanner--visibility"],detectCmp:[{exists:".cookieBanner--visibility"}],detectPopup:[{visible:".cookieBanner__wrapper"}],optIn:[{click:".js_cookieBannerPermissionButton"}],optOut:[{click:".js_cookieBannerProhibitionButton"}]},{name:"ourworldindata",vendorUrl:"https://ourworldindata.org/",runContext:{urlPattern:"^https://ourworldindata\\.org/"},prehideSelectors:[".cookie-manager"],detectCmp:[{exists:".cookie-manager"}],detectPopup:[{visible:".cookie-manager .cookie-notice.open"}],optIn:[{waitForThenClick:".cookie-notice [data-test=accept]"}],optOut:[{waitForThenClick:".cookie-notice [data-test=reject]"}]},{name:"pabcogypsum",vendorUrl:"https://unknown",prehideSelectors:[".js-cookie-notice:has(#cookie_settings-form)"],detectCmp:[{exists:".js-cookie-notice #cookie_settings-form"}],detectPopup:[{visible:".js-cookie-notice #cookie_settings-form"}],optIn:[{waitForThenClick:".js-cookie-notice button[value=allow]"}],optOut:[{waitForThenClick:".js-cookie-notice button[value=disable]"}]},{name:"paypal-us",prehideSelectors:["#ccpaCookieContent_wrapper, article.ppvx_modal--overpanel"],detectCmp:[{exists:"#ccpaCookieBanner, .privacy-sheet-content"}],detectPopup:[{exists:"#ccpaCookieBanner, .privacy-sheet-content"}],optIn:[{click:"#acceptAllButton"}],optOut:[{if:{exists:"a#manageCookiesLink"},then:[{click:"a#manageCookiesLink"}],else:[{waitForVisible:".privacy-sheet-content #formContent"},{click:"#formContent .cookiepref-11m2iee-checkbox_base input:checked",all:!0,optional:!0},{click:".confirmCookie #submitCookiesBtn"}]}]},{name:"paypal.com",prehideSelectors:["#gdprCookieBanner"],detectCmp:[{exists:"#gdprCookieBanner"}],detectPopup:[{visible:"#gdprCookieContent_wrapper"}],optIn:[{click:"#acceptAllButton"}],optOut:[{wait:200},{click:".gdprCookieBanner_decline-button"}],test:[{wait:500},{eval:"EVAL_PAYPAL_0"}]},{name:"pinetools.com",cosmetic:!0,prehideSelectors:["#aviso_cookies"],detectCmp:[{exists:"#aviso_cookies"}],detectPopup:[{exists:".lang_en #aviso_cookies"}],optIn:[{click:"#aviso_cookies .a_boton_cerrar"}],optOut:[{hide:"#aviso_cookies"}]},{name:"pmc",cosmetic:!0,prehideSelectors:["#pmc-pp-tou--notice"],detectCmp:[{exists:"#pmc-pp-tou--notice"}],detectPopup:[{visible:"#pmc-pp-tou--notice"}],optIn:[{click:"span.pmc-pp-tou--notice-close-btn"}],optOut:[{hide:"#pmc-pp-tou--notice"}]},{name:"pornhub.com",runContext:{urlPattern:"^https://(www\\.)?pornhub\\.com/"},cosmetic:!0,prehideSelectors:[".cookiesBanner"],detectCmp:[{exists:".cookiesBanner"}],detectPopup:[{visible:".cookiesBanner"}],optIn:[{click:".cookiesBanner .okButton"}],optOut:[{hide:".cookiesBanner"}]},{name:"pornpics.com",cosmetic:!0,prehideSelectors:["#cookie-contract"],detectCmp:[{exists:"#cookie-contract"}],detectPopup:[{visible:"#cookie-contract"}],optIn:[{click:"#cookie-contract .icon-cross"}],optOut:[{hide:"#cookie-contract"}]},{name:"PrimeBox CookieBar",prehideSelectors:["#cookie-bar"],detectCmp:[{exists:"#cookie-bar .cb-enable,#cookie-bar .cb-disable,#cookie-bar .cb-policy"}],detectPopup:[{visible:"#cookie-bar .cb-enable,#cookie-bar .cb-disable,#cookie-bar .cb-policy",check:"any"}],optIn:[{waitForThenClick:"#cookie-bar .cb-enable"}],optOut:[{click:"#cookie-bar .cb-disable",optional:!0},{hide:"#cookie-bar"}],test:[{eval:"EVAL_PRIMEBOX_0"}]},{name:"privacymanager.io",prehideSelectors:["#gdpr-consent-tool-wrapper",'iframe[src^="https://cmp-consent-tool.privacymanager.io"]'],runContext:{urlPattern:"^https://cmp-consent-tool\\.privacymanager\\.io/",main:!1,frame:!0},detectCmp:[{exists:"button#save"}],detectPopup:[{visible:"button#save"}],optIn:[{click:"button#save"}],optOut:[{if:{exists:"#denyAll"},then:[{click:"#denyAll"},{waitForThenClick:".okButton"}],else:[{waitForThenClick:"#manageSettings"},{waitFor:".purposes-overview-list"},{waitFor:"button#saveAndExit"},{click:"span[role=checkbox][aria-checked=true]",all:!0,optional:!0},{click:"button#saveAndExit"}]}]},{name:"productz.com",vendorUrl:"https://productz.com/",runContext:{urlPattern:"^https://productz\\.com/"},prehideSelectors:[],detectCmp:[{exists:".c-modal.is-active"}],detectPopup:[{visible:".c-modal.is-active"}],optIn:[{waitForThenClick:".c-modal.is-active .is-accept"}],optOut:[{waitForThenClick:".c-modal.is-active .is-dismiss"}]},{name:"pubtech",prehideSelectors:["#pubtech-cmp"],detectCmp:[{exists:"#pubtech-cmp"}],detectPopup:[{visible:"#pubtech-cmp #pt-actions"}],optIn:[{if:{exists:"#pt-accept-all"},then:[{click:"#pubtech-cmp #pt-actions #pt-accept-all"}],else:[{click:"#pubtech-cmp #pt-actions button:nth-of-type(2)"}]}],optOut:[{click:"#pubtech-cmp #pt-close"}],test:[{eval:"EVAL_PUBTECH_0"}]},{name:"quantcast",prehideSelectors:["#qc-cmp2-main,#qc-cmp2-container"],detectCmp:[{exists:"#qc-cmp2-container"}],detectPopup:[{visible:"#qc-cmp2-ui"}],optOut:[{click:'.qc-cmp2-summary-buttons > button[mode="secondary"]'},{waitFor:"#qc-cmp2-ui"},{click:'.qc-cmp2-toggle-switch > button[aria-checked="true"]',all:!0,optional:!0},{click:'.qc-cmp2-main button[aria-label="REJECT ALL"]',optional:!0},{waitForThenClick:'.qc-cmp2-main button[aria-label="SAVE & EXIT"],.qc-cmp2-buttons-desktop > button[mode="primary"]',timeout:5e3}],optIn:[{click:'.qc-cmp2-summary-buttons > button[mode="primary"]'}]},{name:"reddit.com",runContext:{urlPattern:"^https://www\\.reddit\\.com/"},prehideSelectors:["[bundlename=reddit_cookie_banner]"],detectCmp:[{exists:"reddit-cookie-banner"}],detectPopup:[{visible:"reddit-cookie-banner"}],optIn:[{waitForThenClick:["reddit-cookie-banner","#accept-all-cookies-button > button"]}],optOut:[{waitForThenClick:["reddit-cookie-banner","#reject-nonessential-cookies-button > button"]}],test:[{eval:"EVAL_REDDIT_0"}]},{name:"rog-forum.asus.com",runContext:{urlPattern:"^https://rog-forum\\.asus\\.com/"},prehideSelectors:["#cookie-policy-info"],detectCmp:[{exists:"#cookie-policy-info"}],detectPopup:[{visible:"#cookie-policy-info"}],optIn:[{click:'div.cookie-btn-box > div[aria-label="Accept"]'}],optOut:[{click:'div.cookie-btn-box > div[aria-label="Reject"]'},{waitForThenClick:'.cookie-policy-lightbox-bottom > div[aria-label="Save Settings"]'}]},{name:"roofingmegastore.co.uk",runContext:{urlPattern:"^https://(www\\.)?roofingmegastore\\.co\\.uk"},prehideSelectors:["#m-cookienotice"],detectCmp:[{exists:"#m-cookienotice"}],detectPopup:[{visible:"#m-cookienotice"}],optIn:[{click:"#accept-cookies"}],optOut:[{click:"#manage-cookies"},{waitForThenClick:"#accept-selected"}]},{name:"samsung.com",runContext:{urlPattern:"^https://www\\.samsung\\.com/"},cosmetic:!0,prehideSelectors:["div.cookie-bar"],detectCmp:[{exists:"div.cookie-bar"}],detectPopup:[{visible:"div.cookie-bar"}],optIn:[{click:"div.cookie-bar__manage > a"}],optOut:[{hide:"div.cookie-bar"}]},{name:"setapp.com",vendorUrl:"https://setapp.com/",cosmetic:!0,runContext:{urlPattern:"^https://setapp\\.com/"},prehideSelectors:[],detectCmp:[{exists:".cookie-banner.js-cookie-banner"}],detectPopup:[{visible:".cookie-banner.js-cookie-banner"}],optIn:[{waitForThenClick:".cookie-banner.js-cookie-banner button"}],optOut:[{hide:".cookie-banner.js-cookie-banner"}]},{name:"sibbo",prehideSelectors:["sibbo-cmp-layout"],detectCmp:[{exists:"sibbo-cmp-layout"}],detectPopup:[{visible:"sibbo-cmp-layout"}],optIn:[{click:"sibbo-cmp-layout [data-accept-all]"}],optOut:[{click:'.sibbo-panel__aside__buttons a[data-nav="purposes"]'},{click:'.sibbo-panel__main__header__actions a[data-focusable="reject-all"]'},{if:{exists:"[data-view=purposes] .sibbo-panel__main__footer__actions [data-save-and-exit]"},then:[],else:[{waitFor:'.sibbo-panel__main__footer__actions a[data-focusable="next"]:not(.sibbo-cmp-button--disabled)'},{click:'.sibbo-panel__main__footer__actions a[data-focusable="next"]'},{click:'.sibbo-panel__main div[data-view="purposesLegInt"] a[data-focusable="reject-all"]'}]},{waitFor:".sibbo-panel__main__footer__actions [data-save-and-exit]:not(.sibbo-cmp-button--disabled)"},{click:".sibbo-panel__main__footer__actions [data-save-and-exit]:not(.sibbo-cmp-button--disabled)"}],test:[{eval:"EVAL_SIBBO_0"}]},{name:"similarweb.com",cosmetic:!0,prehideSelectors:[".app-cookies-notification"],detectCmp:[{exists:".app-cookies-notification"}],detectPopup:[{exists:".app-layout .app-cookies-notification"}],optIn:[{click:"button.app-cookies-notification__dismiss"}],optOut:[{hide:".app-layout .app-cookies-notification"}]},{name:"Sirdata",cosmetic:!1,prehideSelectors:["#sd-cmp"],detectCmp:[{exists:"#sd-cmp"}],detectPopup:[{visible:"#sd-cmp"}],optIn:[{waitForThenClick:"#sd-cmp .sd-cmp-3cRQ2"}],optOut:[{waitForThenClick:["#sd-cmp","xpath///span[contains(., 'Do not accept') or contains(., 'Acceptera inte') or contains(., 'No aceptar') or contains(., 'Ikke acceptere') or contains(., 'Nicht akzeptieren') or contains(., 'Не приемам') or contains(., 'Να μην γίνει αποδοχή') or contains(., 'Niet accepteren') or contains(., 'Nepřijímat') or contains(., 'Nie akceptuj') or contains(., 'Nu acceptați') or contains(., 'Não aceitar') or contains(., 'Continuer sans accepter') or contains(., 'Non accettare') or contains(., 'Nem fogad el')]"]}]},{name:"snigel",detectCmp:[{exists:".snigel-cmp-framework"}],detectPopup:[{visible:".snigel-cmp-framework"}],optOut:[{click:"#sn-b-custom"},{click:"#sn-b-save"}],test:[{eval:"EVAL_SNIGEL_0"}],optIn:[{click:".snigel-cmp-framework #accept-choices"}]},{name:"steampowered.com",detectCmp:[{exists:".cookiepreferences_popup"},{visible:".cookiepreferences_popup"}],detectPopup:[{visible:".cookiepreferences_popup"}],optOut:[{click:"#rejectAllButton"}],optIn:[{click:"#acceptAllButton"}],test:[{wait:1e3},{eval:"EVAL_STEAMPOWERED_0"}]},{name:"strato.de",prehideSelectors:[".consent__wrapper"],runContext:{urlPattern:"^https://www\\.strato\\.de/"},detectCmp:[{exists:".consent"}],detectPopup:[{visible:".consent"}],optIn:[{click:"button.consentAgree"}],optOut:[{click:"button.consentSettings"},{waitForThenClick:"button#consentSubmit"}]},{name:"svt.se",vendorUrl:"https://www.svt.se/",runContext:{urlPattern:"^https://www\\.svt\\.se/"},prehideSelectors:["[class*=CookieConsent__root___]"],detectCmp:[{exists:"[class*=CookieConsent__root___]"}],detectPopup:[{visible:"[class*=CookieConsent__modal___]"}],optIn:[{waitForThenClick:"[class*=CookieConsent__modal___] > div > button[class*=primary]"}],optOut:[{waitForThenClick:"[class*=CookieConsent__modal___] > div > button[class*=secondary]:nth-child(2)"}],test:[{eval:"EVAL_SVT_TEST"}]},{name:"takealot.com",cosmetic:!0,prehideSelectors:['div[class^="cookies-banner-module_"]'],detectCmp:[{exists:'div[class^="cookies-banner-module_cookie-banner_"]'}],detectPopup:[{exists:'div[class^="cookies-banner-module_cookie-banner_"]'}],optIn:[{click:'button[class*="cookies-banner-module_dismiss-button_"]'}],optOut:[{hide:'div[class^="cookies-banner-module_"]'},{if:{exists:'div[class^="cookies-banner-module_small-cookie-banner_"]'},then:[{eval:"EVAL_TAKEALOT_0"}],else:[]}]},{name:"tarteaucitron.js",prehideSelectors:["#tarteaucitronRoot"],detectCmp:[{exists:"#tarteaucitronRoot"}],detectPopup:[{visible:"#tarteaucitronRoot #tarteaucitronAlertBig",check:"any"}],optIn:[{eval:"EVAL_TARTEAUCITRON_1"}],optOut:[{eval:"EVAL_TARTEAUCITRON_0"}],test:[{eval:"EVAL_TARTEAUCITRON_2",comment:"sometimes there are required categories, so we check that at least something is false"}]},{name:"taunton",vendorUrl:"https://www.taunton.com/",prehideSelectors:["#taunton-user-consent__overlay"],detectCmp:[{exists:"#taunton-user-consent__overlay"}],detectPopup:[{exists:"#taunton-user-consent__overlay:not([aria-hidden=true])"}],optIn:[{click:"#taunton-user-consent__toolbar input[type=checkbox]:not(:checked)"},{click:"#taunton-user-consent__toolbar button[type=submit]"}],optOut:[{click:"#taunton-user-consent__toolbar input[type=checkbox]:checked",optional:!0,all:!0},{click:"#taunton-user-consent__toolbar button[type=submit]"}],test:[{eval:"EVAL_TAUNTON_TEST"}]},{name:"Tealium",prehideSelectors:["#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs,#__tealiumImplicitmodal,#consent-layer"],detectCmp:[{exists:"#__tealiumGDPRecModal *,#__tealiumGDPRcpPrefs *,#__tealiumImplicitmodal *"},{eval:"EVAL_TEALIUM_0"}],detectPopup:[{visible:"#__tealiumGDPRecModal *,#__tealiumGDPRcpPrefs *,#__tealiumImplicitmodal *",check:"any"}],optOut:[{eval:"EVAL_TEALIUM_1"},{eval:"EVAL_TEALIUM_DONOTSELL"},{hide:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs,#__tealiumImplicitmodal"},{waitForThenClick:"#cm-acceptNone,.js-accept-essential-cookies",timeout:1e3,optional:!0}],optIn:[{hide:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs"},{eval:"EVAL_TEALIUM_2"}],test:[{eval:"EVAL_TEALIUM_3"},{eval:"EVAL_TEALIUM_DONOTSELL_CHECK"},{visible:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs",check:"none"}]},{name:"temu",vendorUrl:"https://temu.com",runContext:{urlPattern:"^https://[^/]*temu\\.com/"},prehideSelectors:["._2d-8vq-W,._1UdBUwni"],detectCmp:[{exists:"._3YCsmIaS"}],detectPopup:[{visible:"._3YCsmIaS"}],optIn:[{waitForThenClick:"._3fKiu5wx._3zN5SumS._3tAK973O.IYOfhWEs.VGNGF1pA"}],optOut:[{waitForThenClick:"._3fKiu5wx._1_XToJBF._3tAK973O.IYOfhWEs.VGNGF1pA"}]},{name:"Termly",prehideSelectors:["#termly-code-snippet-support"],detectCmp:[{exists:"#termly-code-snippet-support"}],detectPopup:[{visible:"#termly-code-snippet-support div"}],optIn:[{waitForThenClick:'[data-tid="banner-accept"]'}],optOut:[{if:{exists:'[data-tid="banner-decline"]'},then:[{click:'[data-tid="banner-decline"]'}],else:[{click:".t-preference-button"},{wait:500},{if:{exists:".t-declineAllButton"},then:[{click:".t-declineAllButton"}],else:[{waitForThenClick:".t-preference-modal input[type=checkbox][checked]:not([disabled])",all:!0},{waitForThenClick:".t-saveButton"}]}]}]},{name:"termsfeed",vendorUrl:"https://termsfeed.com",comment:"v4.x.x",prehideSelectors:[".termsfeed-com---nb"],detectCmp:[{exists:".termsfeed-com---nb"}],detectPopup:[{visible:".termsfeed-com---nb"}],optIn:[{waitForThenClick:".cc-nb-okagree"}],optOut:[{waitForThenClick:".cc-nb-reject"}]},{name:"termsfeed3",vendorUrl:"https://termsfeed.com",comment:"v3.x.x",cosmetic:!0,prehideSelectors:[".cc_dialog.cc_css_reboot"],detectCmp:[{exists:".cc_dialog.cc_css_reboot"}],detectPopup:[{visible:".cc_dialog.cc_css_reboot"}],optIn:[{waitForThenClick:".cc_dialog.cc_css_reboot .cc_b_ok"}],optOut:[{hide:".cc_dialog.cc_css_reboot"}]},{name:"Test page cosmetic CMP",cosmetic:!0,prehideSelectors:["#privacy-test-page-cmp-test-prehide"],detectCmp:[{exists:"#privacy-test-page-cmp-test-banner"}],detectPopup:[{visible:"#privacy-test-page-cmp-test-banner"}],optIn:[{waitFor:"#accept-all"},{click:"#accept-all"}],optOut:[{hide:"#privacy-test-page-cmp-test-banner"}],test:[{wait:500},{eval:"EVAL_TESTCMP_COSMETIC_0"}]},{name:"Test page CMP",prehideSelectors:["#reject-all"],detectCmp:[{exists:"#privacy-test-page-cmp-test"}],detectPopup:[{visible:"#privacy-test-page-cmp-test"}],optIn:[{waitFor:"#accept-all"},{click:"#accept-all"}],optOut:[{waitFor:"#reject-all"},{click:"#reject-all"}],test:[{eval:"EVAL_TESTCMP_0"}]},{name:"thalia.de",prehideSelectors:[".consent-banner-box"],detectCmp:[{exists:"consent-banner[component=consent-banner]"}],detectPopup:[{visible:".consent-banner-box"}],optIn:[{click:".button-zustimmen"}],optOut:[{click:"button[data-consent=disagree]"}]},{name:"thefreedictionary.com",prehideSelectors:["#cmpBanner"],detectCmp:[{exists:"#cmpBanner"}],detectPopup:[{visible:"#cmpBanner"}],optIn:[{eval:"EVAL_THEFREEDICTIONARY_1"}],optOut:[{eval:"EVAL_THEFREEDICTIONARY_0"}]},{name:"theverge",runContext:{frame:!1,main:!0,urlPattern:"^https://(www)?\\.theverge\\.com"},intermediate:!1,prehideSelectors:[".duet--cta--cookie-banner"],detectCmp:[{exists:".duet--cta--cookie-banner"}],detectPopup:[{visible:".duet--cta--cookie-banner"}],optIn:[{click:".duet--cta--cookie-banner button.tracking-12",all:!1}],optOut:[{click:".duet--cta--cookie-banner button.tracking-12 > span"}],test:[{eval:"EVAL_THEVERGE_0"}]},{name:"tidbits-com",cosmetic:!0,prehideSelectors:["#eu_cookie_law_widget-2"],detectCmp:[{exists:"#eu_cookie_law_widget-2"}],detectPopup:[{visible:"#eu_cookie_law_widget-2"}],optIn:[{click:"#eu-cookie-law form > input.accept"}],optOut:[{hide:"#eu_cookie_law_widget-2"}]},{name:"tractor-supply",runContext:{urlPattern:"^https://www\\.tractorsupply\\.com/"},cosmetic:!0,prehideSelectors:[".tsc-cookie-banner"],detectCmp:[{exists:".tsc-cookie-banner"}],detectPopup:[{visible:".tsc-cookie-banner"}],optIn:[{click:"#cookie-banner-cancel"}],optOut:[{hide:".tsc-cookie-banner"}]},{name:"trader-joes-com",cosmetic:!0,prehideSelectors:['div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'],detectCmp:[{exists:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}],detectPopup:[{visible:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}],optIn:[{click:'div[class^="CookiesAlert_cookiesAlert__container__"] button'}],optOut:[{hide:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}]},{name:"transcend",vendorUrl:"https://unknown",cosmetic:!0,prehideSelectors:["#transcend-consent-manager"],detectCmp:[{exists:"#transcend-consent-manager"}],detectPopup:[{visible:"#transcend-consent-manager"}],optIn:[{waitForThenClick:["#transcend-consent-manager","#consentManagerMainDialog .inner-container button"]}],optOut:[{hide:"#transcend-consent-manager"}]},{name:"transip-nl",runContext:{urlPattern:"^https://www\\.transip\\.nl/"},prehideSelectors:["#consent-modal"],detectCmp:[{any:[{exists:"#consent-modal"},{exists:"#privacy-settings-content"}]}],detectPopup:[{any:[{visible:"#consent-modal"},{visible:"#privacy-settings-content"}]}],optIn:[{click:'button[type="submit"]'}],optOut:[{if:{exists:"#privacy-settings-content"},then:[{click:'button[type="submit"]'}],else:[{click:"div.one-modal__action-footer-column--secondary > a"}]}]},{name:"tropicfeel-com",prehideSelectors:["#shopify-section-cookies-controller"],detectCmp:[{exists:"#shopify-section-cookies-controller"}],detectPopup:[{visible:"#shopify-section-cookies-controller #cookies-controller-main-pane",check:"any"}],optIn:[{waitForThenClick:"#cookies-controller-main-pane form[data-form-allow-all] button"}],optOut:[{click:"#cookies-controller-main-pane a[data-tab-target=manage-cookies]"},{waitFor:"#manage-cookies-pane.active"},{click:"#manage-cookies-pane.active input[type=checkbox][checked]:not([disabled])",all:!0},{click:"#manage-cookies-pane.active button[type=submit]"}],test:[]},{name:"true-car",runContext:{urlPattern:"^https://www\\.truecar\\.com/"},cosmetic:!0,prehideSelectors:[['div[aria-labelledby="cookie-banner-heading"]']],detectCmp:[{exists:'div[aria-labelledby="cookie-banner-heading"]'}],detectPopup:[{visible:'div[aria-labelledby="cookie-banner-heading"]'}],optIn:[{click:'div[aria-labelledby="cookie-banner-heading"] > button[aria-label="Close"]'}],optOut:[{hide:'div[aria-labelledby="cookie-banner-heading"]'}]},{name:"truyo",prehideSelectors:["#truyo-consent-module"],detectCmp:[{exists:"#truyo-cookieBarContent"}],detectPopup:[{visible:"#truyo-consent-module"}],optIn:[{click:"button#acceptAllCookieButton"}],optOut:[{click:"button#declineAllCookieButton"}]},{name:"twitch-mobile",vendorUrl:"https://m.twitch.tv/",cosmetic:!0,runContext:{urlPattern:"^https?://m\\.twitch\\.tv"},prehideSelectors:[],detectCmp:[{exists:'.ReactModal__Overlay [href="https://www.twitch.tv/p/cookie-policy"]'}],detectPopup:[{visible:'.ReactModal__Overlay [href="https://www.twitch.tv/p/cookie-policy"]'}],optIn:[{waitForThenClick:'.ReactModal__Overlay:has([href="https://www.twitch.tv/p/cookie-policy"]) button'}],optOut:[{hide:'.ReactModal__Overlay:has([href="https://www.twitch.tv/p/cookie-policy"])'}]},{name:"twitch.tv",runContext:{urlPattern:"^https?://(www\\.)?twitch\\.tv"},prehideSelectors:["div:has(> .consent-banner .consent-banner__content--gdpr-v2),.ReactModalPortal:has([data-a-target=consent-modal-save])"],detectCmp:[{exists:".consent-banner .consent-banner__content--gdpr-v2"}],detectPopup:[{visible:".consent-banner .consent-banner__content--gdpr-v2"}],optIn:[{click:'button[data-a-target="consent-banner-accept"]'}],optOut:[{hide:"div:has(> .consent-banner .consent-banner__content--gdpr-v2)"},{click:'button[data-a-target="consent-banner-manage-preferences"]'},{waitFor:"input[type=checkbox][data-a-target=tw-checkbox]"},{click:"input[type=checkbox][data-a-target=tw-checkbox][checked]:not([disabled])",all:!0,optional:!0},{waitForThenClick:"[data-a-target=consent-modal-save]"},{waitForVisible:".ReactModalPortal:has([data-a-target=consent-modal-save])",check:"none"}]},{name:"twitter",runContext:{urlPattern:"^https://([a-z0-9-]+\\.)?twitter\\.com/"},prehideSelectors:['[data-testid="BottomBar"]'],detectCmp:[{exists:'[data-testid="BottomBar"] div'}],detectPopup:[{visible:'[data-testid="BottomBar"] div'}],optIn:[{waitForThenClick:'[data-testid="BottomBar"] > div:has(>div:first-child>div:last-child>span[role=button]) > div:last-child > div[role=button]:first-child'}],optOut:[{waitForThenClick:'[data-testid="BottomBar"] > div:has(>div:first-child>div:last-child>span[role=button]) > div:last-child > div[role=button]:last-child'}],TODOtest:[{eval:"EVAL_document.cookie.includes('d_prefs=MjoxLGNvbnNlbnRfdmVyc2lvbjoy')"}]},{name:"ubuntu.com",prehideSelectors:["dialog.cookie-policy"],detectCmp:[{any:[{exists:"dialog.cookie-policy header"},{exists:'xpath///*[@id="modal"]/div/header'}]}],detectPopup:[{any:[{visible:"dialog header"},{visible:'xpath///*[@id="modal"]/div/header'}]}],optIn:[{any:[{waitForThenClick:"#cookie-policy-button-accept"},{waitForThenClick:'xpath///*[@id="cookie-policy-button-accept"]'}]}],optOut:[{any:[{waitForThenClick:"button.js-manage"},{waitForThenClick:'xpath///*[@id="cookie-policy-content"]/p[4]/button[2]'}]},{waitForThenClick:"dialog.cookie-policy .p-switch__input:checked",optional:!0,all:!0,timeout:500},{any:[{waitForThenClick:"dialog.cookie-policy .js-save-preferences"},{waitForThenClick:'xpath///*[@id="modal"]/div/button'}]}],test:[{eval:"EVAL_UBUNTU_COM_0"}]},{name:"UK Cookie Consent",prehideSelectors:["#catapult-cookie-bar"],cosmetic:!0,detectCmp:[{exists:"#catapult-cookie-bar"}],detectPopup:[{exists:".has-cookie-bar #catapult-cookie-bar"}],optIn:[{click:"#catapultCookie"}],optOut:[{hide:"#catapult-cookie-bar"}],test:[{eval:"EVAL_UK_COOKIE_CONSENT_0"}]},{name:"urbanarmorgear-com",cosmetic:!0,prehideSelectors:['div[class^="Layout__CookieBannerContainer-"]'],detectCmp:[{exists:'div[class^="Layout__CookieBannerContainer-"]'}],detectPopup:[{visible:'div[class^="Layout__CookieBannerContainer-"]'}],optIn:[{click:'button[class^="CookieBanner__AcceptButton"]'}],optOut:[{hide:'div[class^="Layout__CookieBannerContainer-"]'}]},{name:"usercentrics-api",detectCmp:[{exists:"#usercentrics-root"}],detectPopup:[{eval:"EVAL_USERCENTRICS_API_0"},{exists:["#usercentrics-root","[data-testid=uc-container]"]},{waitForVisible:"#usercentrics-root",timeout:2e3}],optIn:[{eval:"EVAL_USERCENTRICS_API_3"},{eval:"EVAL_USERCENTRICS_API_1"},{eval:"EVAL_USERCENTRICS_API_5"}],optOut:[{eval:"EVAL_USERCENTRICS_API_1"},{eval:"EVAL_USERCENTRICS_API_2"}],test:[{eval:"EVAL_USERCENTRICS_API_6"}]},{name:"usercentrics-button",detectCmp:[{exists:"#usercentrics-button"}],detectPopup:[{visible:"#usercentrics-button #uc-btn-accept-banner"}],optIn:[{click:"#usercentrics-button #uc-btn-accept-banner"}],optOut:[{click:"#usercentrics-button #uc-btn-deny-banner"}],test:[{eval:"EVAL_USERCENTRICS_BUTTON_0"}]},{name:"uswitch.com",prehideSelectors:["#cookie-banner-wrapper"],detectCmp:[{exists:"#cookie-banner-wrapper"}],detectPopup:[{visible:"#cookie-banner-wrapper"}],optIn:[{click:"#cookie_banner_accept_mobile"}],optOut:[{click:"#cookie_banner_save"}]},{name:"vodafone.de",runContext:{urlPattern:"^https://www\\.vodafone\\.de/"},prehideSelectors:[".dip-consent,.dip-consent-container"],detectCmp:[{exists:".dip-consent-container"}],detectPopup:[{visible:".dip-consent-content"}],optOut:[{click:'.dip-consent-btn[tabindex="2"]'}],optIn:[{click:'.dip-consent-btn[tabindex="1"]'}]},{name:"waitrose.com",prehideSelectors:["div[aria-labelledby=CookieAlertModalHeading]","section[data-test=initial-waitrose-cookie-consent-banner]","section[data-test=cookie-consent-modal]"],detectCmp:[{exists:"section[data-test=initial-waitrose-cookie-consent-banner]"}],detectPopup:[{visible:"section[data-test=initial-waitrose-cookie-consent-banner]"}],optIn:[{click:"button[data-test=accept-all]"}],optOut:[{click:"button[data-test=manage-cookies]"},{wait:200},{eval:"EVAL_WAITROSE_0"},{click:"button[data-test=submit]"}],test:[{eval:"EVAL_WAITROSE_1"}]},{name:"webflow",vendorUrl:"https://webflow.com/",prehideSelectors:[".fs-cc-components"],detectCmp:[{exists:".fs-cc-components"}],detectPopup:[{visible:".fs-cc-components"},{visible:"[fs-cc=banner]"}],optIn:[{wait:500},{waitForThenClick:"[fs-cc=banner] [fs-cc=allow]"}],optOut:[{wait:500},{waitForThenClick:"[fs-cc=banner] [fs-cc=deny]"}]},{name:"wetransfer.com",detectCmp:[{exists:".welcome__cookie-notice"}],detectPopup:[{visible:".welcome__cookie-notice"}],optIn:[{click:".welcome__button--accept"}],optOut:[{click:".welcome__button--decline"}]},{name:"whitepages.com",runContext:{urlPattern:"^https://www\\.whitepages\\.com/"},cosmetic:!0,prehideSelectors:[".cookie-wrapper, .cookie-overlay"],detectCmp:[{exists:".cookie-wrapper"}],detectPopup:[{visible:".cookie-overlay"}],optIn:[{click:'button[aria-label="Got it"]'}],optOut:[{hide:".cookie-wrapper"}]},{name:"wolframalpha",vendorUrl:"https://www.wolframalpha.com",prehideSelectors:[],cosmetic:!0,runContext:{urlPattern:"^https://www\\.wolframalpha\\.com/"},detectCmp:[{exists:"section._a_yb"}],detectPopup:[{visible:"section._a_yb"}],optIn:[{waitForThenClick:"section._a_yb button"}],optOut:[{hide:"section._a_yb"}]},{name:"woo-commerce-com",prehideSelectors:[".wccom-comp-privacy-banner .wccom-privacy-banner"],detectCmp:[{exists:".wccom-comp-privacy-banner .wccom-privacy-banner"}],detectPopup:[{exists:".wccom-comp-privacy-banner .wccom-privacy-banner"}],optIn:[{click:".wccom-privacy-banner__content-buttons button.is-primary"}],optOut:[{click:".wccom-privacy-banner__content-buttons button.is-secondary"},{waitForThenClick:"input[type=checkbox][checked]:not([disabled])",all:!0},{click:"div.wccom-modal__footer > button"}]},{name:"WP Cookie Notice for GDPR",vendorUrl:"https://wordpress.org/plugins/gdpr-cookie-consent/",prehideSelectors:["#gdpr-cookie-consent-bar"],detectCmp:[{exists:"#gdpr-cookie-consent-bar"}],detectPopup:[{visible:"#gdpr-cookie-consent-bar"}],optIn:[{waitForThenClick:"#gdpr-cookie-consent-bar #cookie_action_accept"}],optOut:[{waitForThenClick:"#gdpr-cookie-consent-bar #cookie_action_reject"}],test:[{eval:"EVAL_WP_COOKIE_NOTICE_0"}]},{name:"wpcc",cosmetic:!0,prehideSelectors:[".wpcc-container"],detectCmp:[{exists:".wpcc-container"}],detectPopup:[{exists:".wpcc-container .wpcc-message"}],optIn:[{click:".wpcc-compliance .wpcc-btn"}],optOut:[{hide:".wpcc-container"}]},{name:"xe.com",vendorUrl:"https://www.xe.com/",runContext:{urlPattern:"^https://www\\.xe\\.com/"},prehideSelectors:["[class*=ConsentBanner]"],detectCmp:[{exists:"[class*=ConsentBanner]"}],detectPopup:[{visible:"[class*=ConsentBanner]"}],optIn:[{waitForThenClick:"[class*=ConsentBanner] .egnScw"}],optOut:[{wait:1e3},{waitForThenClick:"[class*=ConsentBanner] .frDWEu"},{waitForThenClick:"[class*=ConsentBanner] .hXIpFU"}],test:[{eval:"EVAL_XE_TEST"}]},{name:"xhamster-eu",prehideSelectors:[".cookies-modal"],detectCmp:[{exists:".cookies-modal"}],detectPopup:[{exists:".cookies-modal"}],optIn:[{click:"button.cmd-button-accept-all"}],optOut:[{click:"button.cmd-button-reject-all"}]},{name:"xhamster-us",runContext:{urlPattern:"^https://(www\\.)?xhamster\\d?\\.com"},cosmetic:!0,prehideSelectors:[".cookie-announce"],detectCmp:[{exists:".cookie-announce"}],detectPopup:[{visible:".cookie-announce .announce-text"}],optIn:[{click:".cookie-announce button.xh-button"}],optOut:[{hide:".cookie-announce"}]},{name:"xing.com",detectCmp:[{exists:"div[class^=cookie-consent-CookieConsent]"}],detectPopup:[{exists:"div[class^=cookie-consent-CookieConsent]"}],optIn:[{click:"#consent-accept-button"}],optOut:[{click:"#consent-settings-button"},{click:".consent-banner-button-accept-overlay"}],test:[{eval:"EVAL_XING_0"}]},{name:"xnxx-com",cosmetic:!0,prehideSelectors:["#cookies-use-alert"],detectCmp:[{exists:"#cookies-use-alert"}],detectPopup:[{visible:"#cookies-use-alert"}],optIn:[{click:"#cookies-use-alert .close"}],optOut:[{hide:"#cookies-use-alert"}]},{name:"xvideos",vendorUrl:"https://xvideos.com",runContext:{urlPattern:"^https://[^/]*xvideos\\.com/"},prehideSelectors:[],detectCmp:[{exists:".disclaimer-opened #disclaimer-cookies"}],detectPopup:[{visible:".disclaimer-opened #disclaimer-cookies"}],optIn:[{waitForThenClick:"#disclaimer-accept_cookies"}],optOut:[{waitForThenClick:"#disclaimer-reject_cookies"}]},{name:"Yahoo",runContext:{urlPattern:"^https://consent\\.yahoo\\.com/v2/"},prehideSelectors:["#reject-all"],detectCmp:[{exists:"#consent-page"}],detectPopup:[{visible:"#consent-page"}],optIn:[{waitForThenClick:"#consent-page button[value=agree]"}],optOut:[{waitForThenClick:"#consent-page button[value=reject]"}]},{name:"youporn.com",cosmetic:!0,prehideSelectors:[".euCookieModal, #js_euCookieModal"],detectCmp:[{exists:".euCookieModal"}],detectPopup:[{exists:".euCookieModal, #js_euCookieModal"}],optIn:[{click:'button[name="user_acceptCookie"]'}],optOut:[{hide:".euCookieModal"}]},{name:"youtube-desktop",prehideSelectors:["tp-yt-iron-overlay-backdrop.opened","ytd-consent-bump-v2-lightbox"],detectCmp:[{exists:"ytd-consent-bump-v2-lightbox tp-yt-paper-dialog"},{exists:'ytd-consent-bump-v2-lightbox tp-yt-paper-dialog a[href^="https://consent.youtube.com/"]'}],detectPopup:[{visible:"ytd-consent-bump-v2-lightbox tp-yt-paper-dialog"}],optIn:[{waitForThenClick:"ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:last-child #button,ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:last-child button"},{wait:500}],optOut:[{waitForThenClick:"ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:first-child #button,ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:first-child button"},{wait:500}],test:[{wait:500},{eval:"EVAL_YOUTUBE_DESKTOP_0"}]},{name:"youtube-mobile",prehideSelectors:[".consent-bump-v2-lightbox"],detectCmp:[{exists:"ytm-consent-bump-v2-renderer"}],detectPopup:[{visible:"ytm-consent-bump-v2-renderer"}],optIn:[{waitForThenClick:"ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons c3-material-button:first-child button, ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons ytm-button-renderer:first-child button"},{wait:500}],optOut:[{waitForThenClick:"ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons c3-material-button:nth-child(2) button, ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons ytm-button-renderer:nth-child(2) button"},{wait:500}],test:[{wait:500},{eval:"EVAL_YOUTUBE_MOBILE_0"}]},{name:"zdf",prehideSelectors:["#zdf-cmp-banner-sdk"],detectCmp:[{exists:"#zdf-cmp-banner-sdk"}],detectPopup:[{visible:"#zdf-cmp-main.zdf-cmp-show"}],optIn:[{waitForThenClick:"#zdf-cmp-main #zdf-cmp-accept-btn"}],optOut:[{waitForThenClick:"#zdf-cmp-main #zdf-cmp-deny-btn"}],test:[]}],C={"didomi.io":{detectors:[{presentMatcher:{target:{selector:"#didomi-host, #didomi-notice"},type:"css"},showingMatcher:{target:{selector:"body.didomi-popup-open, .didomi-notice-banner"},type:"css"}}],methods:[{action:{target:{selector:".didomi-popup-notice-buttons .didomi-button:not(.didomi-button-highlight), .didomi-notice-banner .didomi-learn-more-button"},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{retries:50,target:{selector:"#didomi-purpose-cookies"},type:"waitcss",waitTime:50},{consents:[{description:"Share (everything) with others",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-share_whith_others]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-share_whith_others]:last-child"},type:"click"},type:"X"},{description:"Information storage and access",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-cookies]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-cookies]:last-child"},type:"click"},type:"D"},{description:"Content selection, offers and marketing",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-CL-T1Rgm7]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-CL-T1Rgm7]:last-child"},type:"click"},type:"E"},{description:"Analytics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-analytics]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-analytics]:last-child"},type:"click"},type:"B"},{description:"Analytics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-M9NRHJe3G]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-M9NRHJe3G]:last-child"},type:"click"},type:"B"},{description:"Ad and content selection",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-advertising_personalization]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-advertising_personalization]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection",falseAction:{parent:{childFilter:{target:{selector:"#didomi-purpose-pub-ciblee"}},selector:".didomi-consent-popup-data-processing, .didomi-components-accordion-label-container"},target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-pub-ciblee]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-pub-ciblee]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - basics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-q4zlJqdcD]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-q4zlJqdcD]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - partners and subsidiaries",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-partenaire-cAsDe8jC]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-partenaire-cAsDe8jC]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-p4em9a8m]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-p4em9a8m]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - others",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-autres-pub]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-autres-pub]:last-child"},type:"click"},type:"F"},{description:"Social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-reseauxsociaux]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-reseauxsociaux]:last-child"},type:"click"},type:"A"},{description:"Social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-social_media]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-social_media]:last-child"},type:"click"},type:"A"},{description:"Content selection",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-content_personalization]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-content_personalization]:last-child"},type:"click"},type:"E"},{description:"Ad delivery",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-ad_delivery]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-ad_delivery]:last-child"},type:"click"},type:"F"}],type:"consent"},{action:{consents:[{matcher:{childFilter:{target:{selector:":not(.didomi-components-radio__option--selected)"}},type:"css"},trueAction:{target:{selector:":nth-child(2)"},type:"click"},falseAction:{target:{selector:":first-child"},type:"click"},type:"X"}],type:"consent"},target:{selector:".didomi-components-radio"},type:"foreach"}],type:"list"},name:"DO_CONSENT"},{action:{parent:{selector:".didomi-consent-popup-footer .didomi-consent-popup-actions"},target:{selector:".didomi-components-button:first-child"},type:"click"},name:"SAVE_CONSENT"}]},oil:{detectors:[{presentMatcher:{target:{selector:".as-oil-content-overlay"},type:"css"},showingMatcher:{target:{selector:".as-oil-content-overlay"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".as-js-advanced-settings"},type:"click"},{retries:"10",target:{selector:".as-oil-cpc__purpose-container"},type:"waitcss",waitTime:"250"}],type:"list"},name:"OPEN_OPTIONS"},{action:{actions:[{consents:[{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Information storage and access","Opbevaring af og adgang til oplysninger på din enhed"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Information storage and access","Opbevaring af og adgang til oplysninger på din enhed"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"D"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personlige annoncer","Personalisation"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personlige annoncer","Personalisation"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"E"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Annoncevalg, levering og rapportering","Ad selection, delivery, reporting"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Annoncevalg, levering og rapportering","Ad selection, delivery, reporting"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"F"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personalisering af indhold","Content selection, delivery, reporting"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personalisering af indhold","Content selection, delivery, reporting"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"E"},{matcher:{parent:{childFilter:{target:{selector:".as-oil-cpc__purpose-header",textFilter:["Måling","Measurement"]}},selector:".as-oil-cpc__purpose-container"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{childFilter:{target:{selector:".as-oil-cpc__purpose-header",textFilter:["Måling","Measurement"]}},selector:".as-oil-cpc__purpose-container"},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"B"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:"Google"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:"Google"},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"F"}],type:"consent"}],type:"list"},name:"DO_CONSENT"},{action:{target:{selector:".as-oil__btn-optin"},type:"click"},name:"SAVE_CONSENT"},{action:{target:{selector:"div.as-oil"},type:"hide"},name:"HIDE_CMP"}]},optanon:{detectors:[{presentMatcher:{target:{selector:"#optanon-menu, .optanon-alert-box-wrapper"},type:"css"},showingMatcher:{target:{displayFilter:!0,selector:".optanon-alert-box-wrapper"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".optanon-alert-box-wrapper .optanon-toggle-display, a[onclick*='OneTrust.ToggleInfoDisplay()'], a[onclick*='Optanon.ToggleInfoDisplay()']"},type:"click"}],type:"list"},name:"OPEN_OPTIONS"},{action:{actions:[{target:{selector:".preference-menu-item #Your-privacy"},type:"click"},{target:{selector:"#optanon-vendor-consent-text"},type:"click"},{action:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},target:{selector:"#optanon-vendor-consent-list .vendor-item"},type:"foreach"},{target:{selector:".vendor-consent-back-link"},type:"click"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-performance"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-performance"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-functional"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-functional"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-advertising"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-advertising"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-social"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-social"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Social Media Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Social Media Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalisation"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalisation"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Site monitoring cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Site monitoring cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Third party privacy-enhanced content"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Third party privacy-enhanced content"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Performance & Advertising Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Performance & Advertising Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Information storage and access"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Information storage and access"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"D"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad selection, delivery, reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad selection, delivery, reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content selection, delivery, reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content selection, delivery, reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Measurement"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Measurement"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Recommended Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Recommended Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Unclassified Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Unclassified Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Analytical Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Analytical Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Marketing Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Marketing Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalization"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalization"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad Selection, Delivery & Reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad Selection, Delivery & Reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content Selection, Delivery & Reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content Selection, Delivery & Reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"}],type:"list"},name:"DO_CONSENT"},{action:{parent:{selector:".optanon-save-settings-button"},target:{selector:".optanon-white-button-middle"},type:"click"},name:"SAVE_CONSENT"},{action:{actions:[{target:{selector:"#optanon-popup-wrapper"},type:"hide"},{target:{selector:"#optanon-popup-bg"},type:"hide"},{target:{selector:".optanon-alert-box-wrapper"},type:"hide"}],type:"list"},name:"HIDE_CMP"}]},quantcast2:{detectors:[{presentMatcher:{target:{selector:"[data-tracking-opt-in-overlay]"},type:"css"},showingMatcher:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-learn-more]"},type:"css"}}],methods:[{action:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-learn-more]"},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{type:"wait",waitTime:500},{action:{actions:[{target:{selector:"div",textFilter:["Information storage and access"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"D"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Personalization"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"F"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Ad selection, delivery, reporting"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"F"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Content selection, delivery, reporting"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"E"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Measurement"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"B"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Other Partners"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},type:"ifcss"}],type:"list"},parent:{childFilter:{target:{selector:"input"}},selector:"[data-tracking-opt-in-overlay] > div > div"},target:{childFilter:{target:{selector:"input"}},selector:":scope > div"},type:"foreach"}],type:"list"},name:"DO_CONSENT"},{action:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-save]"},type:"click"},name:"SAVE_CONSENT"}]},springer:{detectors:[{presentMatcher:{parent:null,target:{selector:".cmp-app_gdpr"},type:"css"},showingMatcher:{parent:null,target:{displayFilter:!0,selector:".cmp-popup_popup"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".cmp-intro_rejectAll"},type:"click"},{type:"wait",waitTime:250},{target:{selector:".cmp-purposes_purposeItem:not(.cmp-purposes_selectedPurpose)"},type:"click"}],type:"list"},name:"OPEN_OPTIONS"},{action:{consents:[{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Przechowywanie informacji na urządzeniu lub dostęp do nich",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Przechowywanie informacji na urządzeniu lub dostęp do nich",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"D"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór podstawowych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór podstawowych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"F"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"F"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"E"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"E"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Stosowanie badań rynkowych w celu generowania opinii odbiorców",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Stosowanie badań rynkowych w celu generowania opinii odbiorców",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"X"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Opracowywanie i ulepszanie produktów",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Opracowywanie i ulepszanie produktów",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"X"}],type:"consent"},name:"DO_CONSENT"},{action:{target:{selector:".cmp-details_save"},type:"click"},name:"SAVE_CONSENT"}]},wordpressgdpr:{detectors:[{presentMatcher:{parent:null,target:{selector:".wpgdprc-consent-bar"},type:"css"},showingMatcher:{parent:null,target:{displayFilter:!0,selector:".wpgdprc-consent-bar"},type:"css"}}],methods:[{action:{parent:null,target:{selector:".wpgdprc-consent-bar .wpgdprc-consent-bar__settings",textFilter:null},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{target:{selector:".wpgdprc-consent-modal .wpgdprc-button",textFilter:"Eyeota"},type:"click"},{consents:[{description:"Eyeota Cookies",matcher:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Eyeota"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Eyeota"},target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},{target:{selector:".wpgdprc-consent-modal .wpgdprc-button",textFilter:"Advertising"},type:"click"},{consents:[{description:"Advertising Cookies",matcher:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Advertising"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Advertising"},target:{selector:"label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},name:"DO_CONSENT"},{action:{parent:null,target:{selector:".wpgdprc-button",textFilter:"Save my settings"},type:"click"},name:"SAVE_CONSENT"}]}},v={autoconsent:w,consentomatic:C},f=Object.freeze({__proto__:null,autoconsent:w,consentomatic:C,default:v});const A=new class{constructor(e,t=null,o=null){if(this.id=n(),this.rules=[],this.foundCmp=null,this.state={lifecycle:"loading",prehideOn:!1,findCmpAttempts:0,detectedCmps:[],detectedPopups:[],selfTest:null},a.sendContentMessage=e,this.sendContentMessage=e,this.rules=[],this.updateState({lifecycle:"loading"}),this.addDynamicRules(),t)this.initialize(t,o);else{o&&this.parseDeclarativeRules(o);e({type:"init",url:window.location.href}),this.updateState({lifecycle:"waitingForInitResponse"})}this.domActions=new class{constructor(e){this.autoconsentInstance=e}click(e,t=!1){const o=this.elementSelector(e);return this.autoconsentInstance.config.logs.rulesteps&&console.log("[click]",e,t,o),o.length>0&&(t?o.forEach((e=>e.click())):o[0].click()),o.length>0}elementExists(e){return this.elementSelector(e).length>0}elementVisible(e,t){const o=this.elementSelector(e),c=new Array(o.length);return o.forEach(((e,t)=>{c[t]=k(e)})),"none"===t?c.every((e=>!e)):0!==c.length&&("any"===t?c.some((e=>e)):c.every((e=>e)))}waitForElement(e,t=1e4){const o=Math.ceil(t/200);return this.autoconsentInstance.config.logs.rulesteps&&console.log("[waitForElement]",e),h((()=>this.elementSelector(e).length>0),o,200)}waitForVisible(e,t=1e4,o="any"){return h((()=>this.elementVisible(e,o)),Math.ceil(t/200),200)}async waitForThenClick(e,t=1e4,o=!1){return await this.waitForElement(e,t),this.click(e,o)}wait(e){return new Promise((t=>{setTimeout((()=>{t(!0)}),e)}))}hide(e,t){return m(u(),e,t)}prehide(e){const t=u("autoconsent-prehide");return this.autoconsentInstance.config.logs.lifecycle&&console.log("[prehide]",t,location.href),m(t,e,"opacity")}undoPrehide(){const e=u("autoconsent-prehide");return this.autoconsentInstance.config.logs.lifecycle&&console.log("[undoprehide]",e,location.href),e&&e.remove(),!!e}querySingleReplySelector(e,t=document){if(e.startsWith("aria/"))return[];if(e.startsWith("xpath/")){const o=e.slice(6),c=document.evaluate(o,t,null,XPathResult.ANY_TYPE,null);let i=null;const n=[];for(;i=c.iterateNext();)n.push(i);return n}return e.startsWith("text/")||e.startsWith("pierce/")?[]:t.shadowRoot?Array.from(t.shadowRoot.querySelectorAll(e)):Array.from(t.querySelectorAll(e))}querySelectorChain(e){let t,o=document;for(const c of e){if(t=this.querySingleReplySelector(c,o),0===t.length)return[];o=t[0]}return t}elementSelector(e){return"string"==typeof e?this.querySingleReplySelector(e):this.querySelectorChain(e)}}(this)}initialize(e,t){const o=b(e);if(o.logs.lifecycle&&console.log("autoconsent init",window.location.href),this.config=o,o.enabled){if(t&&this.parseDeclarativeRules(t),this.rules=function(e,t){return e.filter((e=>(!t.disabledCmps||!t.disabledCmps.includes(e.name))&&(t.enableCosmeticRules||!e.isCosmetic)))}(this.rules,o),e.enablePrehide)if(document.documentElement)this.prehideElements();else{const e=()=>{window.removeEventListener("DOMContentLoaded",e),this.prehideElements()};window.addEventListener("DOMContentLoaded",e)}if("loading"===document.readyState){const e=()=>{window.removeEventListener("DOMContentLoaded",e),this.start()};window.addEventListener("DOMContentLoaded",e)}else this.start();this.updateState({lifecycle:"initialized"})}else o.logs.lifecycle&&console.log("autoconsent is disabled")}addDynamicRules(){y.forEach((e=>{this.rules.push(new e(this))}))}parseDeclarativeRules(e){Object.keys(e.consentomatic).forEach((t=>{this.addConsentomaticCMP(t,e.consentomatic[t])})),e.autoconsent.forEach((e=>{this.addDeclarativeCMP(e)}))}addDeclarativeCMP(e){this.rules.push(new d(e,this))}addConsentomaticCMP(e,t){this.rules.push(new class{constructor(e,t){this.name=e,this.config=t,this.methods=new Map,this.runContext=l,this.isCosmetic=!1,t.methods.forEach((e=>{e.action&&this.methods.set(e.name,e.action)})),this.hasSelfTest=!1}get isIntermediate(){return!1}checkRunContext(){return!0}async detectCmp(){return this.config.detectors.map((e=>o(e.presentMatcher))).some((e=>!!e))}async detectPopup(){return this.config.detectors.map((e=>o(e.showingMatcher))).some((e=>!!e))}async executeAction(e,t){return!this.methods.has(e)||c(this.methods.get(e),t)}async optOut(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),await this.executeAction("HIDE_CMP"),await this.executeAction("DO_CONSENT",[]),await this.executeAction("SAVE_CONSENT"),!0}async optIn(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),await this.executeAction("HIDE_CMP"),await this.executeAction("DO_CONSENT",["D","A","B","E","F","X"]),await this.executeAction("SAVE_CONSENT"),!0}async openCmp(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),!0}async test(){return!0}}(`com_${e}`,t))}start(){window.requestIdleCallback?window.requestIdleCallback((()=>this._start()),{timeout:500}):this._start()}async _start(){const e=this.config.logs;e.lifecycle&&console.log(`Detecting CMPs on ${window.location.href}`),this.updateState({lifecycle:"started"});const t=await this.findCmp(this.config.detectRetries);if(this.updateState({detectedCmps:t.map((e=>e.name))}),0===t.length)return e.lifecycle&&console.log("no CMP found",location.href),this.config.enablePrehide&&this.undoPrehide(),this.updateState({lifecycle:"nothingDetected"}),!1;this.updateState({lifecycle:"cmpDetected"});const o=[],c=[];for(const e of t)e.isCosmetic?c.push(e):o.push(e);let i=!1,n=await this.detectPopups(o,(async e=>{i=await this.handlePopup(e)}));if(0===n.length&&(n=await this.detectPopups(c,(async e=>{i=await this.handlePopup(e)}))),0===n.length)return e.lifecycle&&console.log("no popup found"),this.config.enablePrehide&&this.undoPrehide(),!1;if(n.length>1){const t={msg:"Found multiple CMPs, check the detection rules.",cmps:n.map((e=>e.name))};e.errors&&console.warn(t.msg,t.cmps),this.sendContentMessage({type:"autoconsentError",details:t})}return i}async findCmp(e){const t=this.config.logs;this.updateState({findCmpAttempts:this.state.findCmpAttempts+1});const o=[];for(const e of this.rules)try{if(!e.checkRunContext())continue;await e.detectCmp()&&(t.lifecycle&&console.log(`Found CMP: ${e.name} ${window.location.href}`),this.sendContentMessage({type:"cmpDetected",url:location.href,cmp:e.name}),o.push(e))}catch(o){t.errors&&console.warn(`error detecting ${e.name}`,o)}return 0===o.length&&e>0?(await this.domActions.wait(500),this.findCmp(e-1)):o}async detectPopup(e){if(await this.waitForPopup(e).catch((t=>(this.config.logs.errors&&console.warn(`error waiting for a popup for ${e.name}`,t),!1))))return this.updateState({detectedPopups:this.state.detectedPopups.concat([e.name])}),this.sendContentMessage({type:"popupFound",cmp:e.name,url:location.href}),e;throw new Error("Popup is not shown")}async detectPopups(e,t){const o=e.map((e=>this.detectPopup(e)));await Promise.any(o).then((e=>{t(e)})).catch((()=>null));const c=await Promise.allSettled(o),i=[];for(const e of c)"fulfilled"===e.status&&i.push(e.value);return i}async handlePopup(e){return this.updateState({lifecycle:"openPopupDetected"}),this.config.enablePrehide&&!this.state.prehideOn&&this.prehideElements(),this.foundCmp=e,"optOut"===this.config.autoAction?await this.doOptOut():"optIn"===this.config.autoAction?await this.doOptIn():(this.config.logs.lifecycle&&console.log("waiting for opt-out signal...",location.href),!0)}async doOptOut(){const e=this.config.logs;let t;return this.updateState({lifecycle:"runningOptOut"}),this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: opt out on ${window.location.href}`),t=await this.foundCmp.optOut(),e.lifecycle&&console.log(`${this.foundCmp.name}: opt out result ${t}`)):(e.errors&&console.log("no CMP to opt out"),t=!1),this.config.enablePrehide&&this.undoPrehide(),this.sendContentMessage({type:"optOutResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,scheduleSelfTest:this.foundCmp&&this.foundCmp.hasSelfTest,url:location.href}),t&&!this.foundCmp.isIntermediate?(this.sendContentMessage({type:"autoconsentDone",cmp:this.foundCmp.name,isCosmetic:this.foundCmp.isCosmetic,url:location.href}),this.updateState({lifecycle:"done"})):this.updateState({lifecycle:t?"optOutSucceeded":"optOutFailed"}),t}async doOptIn(){const e=this.config.logs;let t;return this.updateState({lifecycle:"runningOptIn"}),this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: opt in on ${window.location.href}`),t=await this.foundCmp.optIn(),e.lifecycle&&console.log(`${this.foundCmp.name}: opt in result ${t}`)):(e.errors&&console.log("no CMP to opt in"),t=!1),this.config.enablePrehide&&this.undoPrehide(),this.sendContentMessage({type:"optInResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,scheduleSelfTest:!1,url:location.href}),t&&!this.foundCmp.isIntermediate?(this.sendContentMessage({type:"autoconsentDone",cmp:this.foundCmp.name,isCosmetic:this.foundCmp.isCosmetic,url:location.href}),this.updateState({lifecycle:"done"})):this.updateState({lifecycle:t?"optInSucceeded":"optInFailed"}),t}async doSelfTest(){const e=this.config.logs;let t;return this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: self-test on ${window.location.href}`),t=await this.foundCmp.test()):(e.errors&&console.log("no CMP to self test"),t=!1),this.sendContentMessage({type:"selfTestResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,url:location.href}),this.updateState({selfTest:t}),t}async waitForPopup(e,t=5,o=500){const c=this.config.logs;c.lifecycle&&console.log("checking if popup is open...",e.name);const i=await e.detectPopup().catch((t=>(c.errors&&console.warn(`error detecting popup for ${e.name}`,t),!1)));return!i&&t>0?(await this.domActions.wait(o),this.waitForPopup(e,t-1,o)):(c.lifecycle&&console.log(e.name,"popup is "+(i?"open":"not open")),i)}prehideElements(){const e=this.config.logs,t=this.rules.filter((e=>e.prehideSelectors&&e.checkRunContext())).reduce(((e,t)=>[...e,...t.prehideSelectors]),["#didomi-popup,.didomi-popup-container,.didomi-popup-notice,.didomi-consent-popup-preferences,#didomi-notice,.didomi-popup-backdrop,.didomi-screen-medium"]);return this.updateState({prehideOn:!0}),setTimeout((()=>{this.config.enablePrehide&&this.state.prehideOn&&!["runningOptOut","runningOptIn"].includes(this.state.lifecycle)&&(e.lifecycle&&console.log("Process is taking too long, unhiding elements"),this.undoPrehide())}),this.config.prehideTimeout||2e3),this.domActions.prehide(t.join(","))}undoPrehide(){return this.updateState({prehideOn:!1}),this.domActions.undoPrehide()}updateState(e){Object.assign(this.state,e),this.sendContentMessage({type:"report",instanceId:this.id,url:window.location.href,mainFrame:window.top===window.self,state:this.state})}async receiveMessageCallback(e){const t=this.config?.logs;switch(t?.messages&&console.log("received from background",e,window.location.href),e.type){case"initResp":this.initialize(e.config,e.rules);break;case"optIn":await this.doOptIn();break;case"optOut":await this.doOptOut();break;case"selfTest":await this.doSelfTest();break;case"evalResp":!function(e,t){const o=a.pending.get(e);o?(a.pending.delete(e),o.timer&&window.clearTimeout(o.timer),o.resolve(t)):console.warn("no eval #",e)}(e.id,e.result)}}}((e=>{window.webkit.messageHandlers[e.type]&&window.webkit.messageHandlers[e.type].postMessage(e).then((e=>{A.receiveMessageCallback(e)}))}),null,f);window.autoconsentMessageCallback=e=>{A.receiveMessageCallback(e)}}(); diff --git a/DuckDuckGo/Autofill/AutofillActionBuilder.swift b/DuckDuckGo/Autofill/AutofillActionBuilder.swift index de48f8d6b3..cbf46a19e2 100644 --- a/DuckDuckGo/Autofill/AutofillActionBuilder.swift +++ b/DuckDuckGo/Autofill/AutofillActionBuilder.swift @@ -35,7 +35,7 @@ extension AutofillActionBuilder { struct AutofillDeleteAllPasswordsBuilder: AutofillActionBuilder { @MainActor func buildExecutor() -> AutofillActionExecutor? { - guard let secureVault = try? AutofillSecureVaultFactory.makeVault(errorReporter: SecureVaultErrorReporter.shared), + guard let secureVault = try? AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter.shared), let syncService = NSApp.delegateTyped.syncService else { return nil } return AutofillDeleteAllPasswordsExecutor(userAuthenticator: DeviceAuthenticator.shared, diff --git a/DuckDuckGo/Autofill/ContentOverlayViewController.swift b/DuckDuckGo/Autofill/ContentOverlayViewController.swift index 9e325ef8b0..5bfdf2ca85 100644 --- a/DuckDuckGo/Autofill/ContentOverlayViewController.swift +++ b/DuckDuckGo/Autofill/ContentOverlayViewController.swift @@ -309,8 +309,8 @@ extension ContentOverlayViewController: SecureVaultManagerDelegate { } } - public func secureVaultInitFailed(_ error: SecureStorageError) { - SecureVaultErrorReporter.shared.secureVaultInitFailed(error) + public func secureVaultError(_ error: SecureStorageError) { + SecureVaultReporter.shared.secureVaultError(error) } public func secureVaultManager(_: BrowserServicesKit.SecureVaultManager, didReceivePixel pixel: AutofillUserScript.JSPixel) { diff --git a/DuckDuckGo/Bookmarks/Model/BookmarkManager.swift b/DuckDuckGo/Bookmarks/Model/BookmarkManager.swift index b3172da78e..1220f5eabb 100644 --- a/DuckDuckGo/Bookmarks/Model/BookmarkManager.swift +++ b/DuckDuckGo/Bookmarks/Model/BookmarkManager.swift @@ -28,8 +28,10 @@ protocol BookmarkManager: AnyObject { func allHosts() -> Set func getBookmark(for url: URL) -> Bookmark? func getBookmark(forUrl url: String) -> Bookmark? + func getBookmarkFolder(withId id: String) -> BookmarkFolder? @discardableResult func makeBookmark(for url: URL, title: String, isFavorite: Bool) -> Bookmark? @discardableResult func makeBookmark(for url: URL, title: String, isFavorite: Bool, index: Int?, parent: BookmarkFolder?) -> Bookmark? + func makeBookmarks(for websitesInfo: [WebsiteInfo], inNewFolderNamed folderName: String, withinParentFolder parent: ParentFolderType) func makeFolder(for title: String, parent: BookmarkFolder?, completion: @escaping (BookmarkFolder) -> Void) func remove(bookmark: Bookmark) func remove(folder: BookmarkFolder) @@ -46,7 +48,6 @@ protocol BookmarkManager: AnyObject { func move(objectUUIDs: [String], toIndex: Int?, withinParentFolder: ParentFolderType, completion: @escaping (Error?) -> Void) func moveFavorites(with objectUUIDs: [String], toIndex: Int?, completion: @escaping (Error?) -> Void) func importBookmarks(_ bookmarks: ImportedBookmarks, source: BookmarkImportSource) -> BookmarksImportSummary - func handleFavoritesAfterDisablingSync() // Wrapper definition in a protocol is not supported yet @@ -138,6 +139,10 @@ final class LocalBookmarkManager: BookmarkManager { return list?[url] } + func getBookmarkFolder(withId id: String) -> BookmarkFolder? { + bookmarkStore.bookmarkFolder(withId: id) + } + @discardableResult func makeBookmark(for url: URL, title: String, isFavorite: Bool) -> Bookmark? { makeBookmark(for: url, title: title, isFavorite: isFavorite, index: nil, parent: nil) } @@ -167,6 +172,12 @@ final class LocalBookmarkManager: BookmarkManager { return bookmark } + func makeBookmarks(for websitesInfo: [WebsiteInfo], inNewFolderNamed folderName: String, withinParentFolder parent: ParentFolderType) { + bookmarkStore.saveBookmarks(for: websitesInfo, inNewFolderNamed: folderName, withinParentFolder: parent) + loadBookmarks() + requestSync() + } + func remove(bookmark: Bookmark) { guard list != nil else { return } guard let latestBookmark = getBookmark(forUrl: bookmark.url) else { diff --git a/DuckDuckGo/Bookmarks/Model/WebsiteInfo.swift b/DuckDuckGo/Bookmarks/Model/WebsiteInfo.swift index fc393b256d..a4d146abd7 100644 --- a/DuckDuckGo/Bookmarks/Model/WebsiteInfo.swift +++ b/DuckDuckGo/Bookmarks/Model/WebsiteInfo.swift @@ -18,15 +18,17 @@ import Foundation -struct WebsiteInfo { +struct WebsiteInfo: Equatable { let url: URL - let title: String? + /// Returns the title of the website if available, otherwise returns the domain of the URL. + /// If both title and and domain are nil, it returns the absolute string representation of the URL. + let title: String init?(_ tab: Tab) { guard case let .url(url, _, _) = tab.content else { return nil } self.url = url - self.title = tab.title + self.title = tab.title ?? url.host ?? url.absoluteString } } diff --git a/DuckDuckGo/Bookmarks/Services/BookmarkStore.swift b/DuckDuckGo/Bookmarks/Services/BookmarkStore.swift index 3466d1a7fa..8c8f4a6d06 100644 --- a/DuckDuckGo/Bookmarks/Services/BookmarkStore.swift +++ b/DuckDuckGo/Bookmarks/Services/BookmarkStore.swift @@ -48,9 +48,11 @@ protocol BookmarkStore { func loadAll(type: BookmarkStoreFetchPredicateType, completion: @escaping ([BaseBookmarkEntity]?, Error?) -> Void) func save(bookmark: Bookmark, parent: BookmarkFolder?, index: Int?, completion: @escaping (Bool, Error?) -> Void) + func saveBookmarks(for websitesInfo: [WebsiteInfo], inNewFolderNamed folderName: String, withinParentFolder parent: ParentFolderType) func save(folder: BookmarkFolder, parent: BookmarkFolder?, completion: @escaping (Bool, Error?) -> Void) func remove(objectsWithUUIDs: [String], completion: @escaping (Bool, Error?) -> Void) func update(bookmark: Bookmark) + func bookmarkFolder(withId id: String) -> BookmarkFolder? func update(folder: BookmarkFolder) func update(folder: BookmarkFolder, andMoveToParent parent: ParentFolderType) func add(objectsWithUUIDs: [String], to parent: BookmarkFolder?, completion: @escaping (Error?) -> Void) @@ -59,6 +61,5 @@ protocol BookmarkStore { func move(objectUUIDs: [String], toIndex: Int?, withinParentFolder: ParentFolderType, completion: @escaping (Error?) -> Void) func moveFavorites(with objectUUIDs: [String], toIndex: Int?, completion: @escaping (Error?) -> Void) func importBookmarks(_ bookmarks: ImportedBookmarks, source: BookmarkImportSource) -> BookmarksImportSummary - func handleFavoritesAfterDisablingSync() } diff --git a/DuckDuckGo/Bookmarks/Services/BookmarkStoreMock.swift b/DuckDuckGo/Bookmarks/Services/BookmarkStoreMock.swift index 2ab57158a9..003ba7bb59 100644 --- a/DuckDuckGo/Bookmarks/Services/BookmarkStoreMock.swift +++ b/DuckDuckGo/Bookmarks/Services/BookmarkStoreMock.swift @@ -104,6 +104,15 @@ public final class BookmarkStoreMock: BookmarkStore { capturedBookmark = bookmark } + var bookmarkFolderWithIdCalled = false + var capturedFolderId: String? + var bookmarkFolder: BookmarkFolder? + func bookmarkFolder(withId id: String) -> BookmarkFolder? { + bookmarkFolderWithIdCalled = true + capturedFolderId = id + return bookmarkFolder + } + var updateFolderCalled = false func update(folder: BookmarkFolder) { updateFolderCalled = true @@ -133,6 +142,16 @@ public final class BookmarkStoreMock: BookmarkStore { return BookmarksImportSummary(successful: 0, duplicates: 0, failed: 0) } + var saveBookmarksInNewFolderNamedCalled = false + var capturedWebsitesInfo: [WebsiteInfo]? + var capturedNewFolderName: String? + func saveBookmarks(for websitesInfo: [WebsiteInfo], inNewFolderNamed folderName: String, withinParentFolder parent: ParentFolderType) { + saveBookmarksInNewFolderNamedCalled = true + capturedWebsitesInfo = websitesInfo + capturedNewFolderName = folderName + capturedParentFolderType = parent + } + var canMoveObjectWithUUIDCalled = false func canMoveObjectWithUUID(objectUUID uuid: String, to parent: BookmarkFolder) -> Bool { canMoveObjectWithUUIDCalled = true diff --git a/DuckDuckGo/Bookmarks/Services/LocalBookmarkStore.swift b/DuckDuckGo/Bookmarks/Services/LocalBookmarkStore.swift index fb21f3222f..2a480e8333 100644 --- a/DuckDuckGo/Bookmarks/Services/LocalBookmarkStore.swift +++ b/DuckDuckGo/Bookmarks/Services/LocalBookmarkStore.swift @@ -65,6 +65,7 @@ final class LocalBookmarkStore: BookmarkStore { case missingRoot case missingFavoritesRoot case saveLoopError(Error?) + case badModelMapping } private(set) var favoritesDisplayMode: FavoritesDisplayMode @@ -339,6 +340,23 @@ final class LocalBookmarkStore: BookmarkStore { }) } + func saveBookmarks(for websitesInfo: [WebsiteInfo], inNewFolderNamed folderName: String, withinParentFolder parent: ParentFolderType) { + do { + try applyChangesAndSave { context in + // Fetch Parent folder + let parentFolder = try bookmarkEntity(for: parent, in: context) + // Create new Folder for all bookmarks + let newFolderMO = BookmarkEntity.makeFolder(title: folderName, parent: parentFolder, context: context) + // Save the bookmarks + websitesInfo.forEach { info in + _ = BookmarkEntity.makeBookmark(title: info.title, url: info.url.absoluteString, parent: newFolderMO, context: context) + } + } + } catch { + commonOnSaveErrorHandler(error) + } + } + func remove(objectsWithUUIDs identifiers: [String], completion: @escaping (Bool, Error?) -> Void) { applyChangesAndSave(changes: { [weak self] context in @@ -390,6 +408,38 @@ final class LocalBookmarkStore: BookmarkStore { } } + func bookmarkFolder(withId id: String) -> BookmarkFolder? { + let context = makeContext() + + var bookmarkFolderToReturn: BookmarkFolder? + let favoritesDisplayMode = self.favoritesDisplayMode + + context.performAndWait { + let folderFetchRequest = BaseBookmarkEntity.singleEntity(with: id) + do { + let folderFetchRequestResult = try context.fetch(folderFetchRequest) + guard let bookmarkFolderManagedObject = folderFetchRequestResult.first else { return } + + guard let bookmarkFolder = BaseBookmarkEntity.from( + managedObject: bookmarkFolderManagedObject, + parentFolderUUID: bookmarkFolderManagedObject.parent?.uuid, + favoritesDisplayMode: favoritesDisplayMode + ) as? BookmarkFolder + else { + throw BookmarkStoreError.badModelMapping + } + bookmarkFolderToReturn = bookmarkFolder + + } catch BookmarkStoreError.badModelMapping { + os_log("Failed to map BookmarkEntity to BookmarkFolder, with error: %s", log: .bookmarks, type: .error) + } catch { + os_log("Failed to fetch last saved folder for bookmarks all tabs, with error: %s", log: .bookmarks, type: .error, error.localizedDescription) + } + } + + return bookmarkFolderToReturn + } + func update(folder: BookmarkFolder) { do { _ = try applyChangesAndSave(changes: { [weak self] context in @@ -998,32 +1048,38 @@ private extension LocalBookmarkStore { } func move(entities: [BookmarkEntity], toIndex index: Int?, withinParentFolderType type: ParentFolderType, in context: NSManagedObjectContext) throws { + let newParentFolder = try bookmarkEntity(for: type, in: context) + + if let index = index, index < newParentFolder.childrenArray.count { + self.move(entities: entities, to: index, within: newParentFolder) + } else { + for bookmarkManagedObject in entities { + bookmarkManagedObject.parent = nil + newParentFolder.addToChildren(bookmarkManagedObject) + } + } + } + + func bookmarkEntity(for parentFolderType: ParentFolderType, in context: NSManagedObjectContext) throws -> BookmarkEntity { guard let rootFolder = bookmarksRoot(in: context) else { throw BookmarkStoreError.missingRoot } - let newParentFolder: BookmarkEntity + let parentFolder: BookmarkEntity - switch type { - case .root: newParentFolder = rootFolder - case .parent(let newParentUUID): - let bookmarksFetchRequest = BaseBookmarkEntity.singleEntity(with: newParentUUID) + switch parentFolderType { + case .root: + parentFolder = rootFolder + case let .parent(parentUUID): + let bookmarksFetchRequest = BaseBookmarkEntity.singleEntity(with: parentUUID) if let fetchedParent = try context.fetch(bookmarksFetchRequest).first, fetchedParent.isFolder { - newParentFolder = fetchedParent + parentFolder = fetchedParent } else { throw BookmarkStoreError.missingEntity } } - - if let index = index, index < newParentFolder.childrenArray.count { - self.move(entities: entities, to: index, within: newParentFolder) - } else { - for bookmarkManagedObject in entities { - bookmarkManagedObject.parent = nil - newParentFolder.addToChildren(bookmarkManagedObject) - } - } + return parentFolder } } @@ -1041,6 +1097,7 @@ extension LocalBookmarkStore.BookmarkStoreError: CustomNSError { case .missingRoot: return 7 case .missingFavoritesRoot: return 8 case .saveLoopError: return 9 + case .badModelMapping: return 10 } } diff --git a/DuckDuckGo/Bookmarks/Services/UserDefaultsBookmarkFoldersStore.swift b/DuckDuckGo/Bookmarks/Services/UserDefaultsBookmarkFoldersStore.swift new file mode 100644 index 0000000000..7fb3187329 --- /dev/null +++ b/DuckDuckGo/Bookmarks/Services/UserDefaultsBookmarkFoldersStore.swift @@ -0,0 +1,48 @@ +// +// UserDefaultsBookmarkFoldersStore.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 + +/// A type used to provide the ID of the folder where all tabs were last saved. +protocol BookmarkFoldersStore: AnyObject { + /// The ID of the folder where all bookmarks from the last session were saved. + var lastBookmarkAllTabsFolderIdUsed: String? { get set } +} + +final class UserDefaultsBookmarkFoldersStore: BookmarkFoldersStore { + + enum Keys { + static let bookmarkAllTabsFolderUsedKey = "bookmarks.all-tabs.last-used-folder" + } + + private let userDefaults: UserDefaults + + init(userDefaults: UserDefaults = .standard) { + self.userDefaults = userDefaults + } + + var lastBookmarkAllTabsFolderIdUsed: String? { + get { + userDefaults.string(forKey: Keys.bookmarkAllTabsFolderUsedKey) + } + set { + userDefaults.set(newValue, forKey: Keys.bookmarkAllTabsFolderUsedKey) + } + } + +} diff --git a/DuckDuckGo/Bookmarks/View/AddBookmarkFolderPopoverView.swift b/DuckDuckGo/Bookmarks/View/AddBookmarkFolderPopoverView.swift index f15465df36..8ce41c17f2 100644 --- a/DuckDuckGo/Bookmarks/View/AddBookmarkFolderPopoverView.swift +++ b/DuckDuckGo/Bookmarks/View/AddBookmarkFolderPopoverView.swift @@ -36,7 +36,6 @@ struct AddBookmarkFolderPopoverView: ModalView { isDefaultActionDisabled: model.isDefaultActionButtonDisabled, defaultAction: { _ in model.addFolder() } ) - .padding(.vertical, 16.0) .font(.system(size: 13)) .frame(width: 320) } diff --git a/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift b/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift index fba84b44bc..1faa240398 100644 --- a/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift +++ b/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift @@ -55,7 +55,6 @@ struct AddBookmarkPopoverView: View { isDefaultActionDisabled: model.isDefaultActionButtonDisabled, defaultAction: model.doneButtonAction ) - .padding(.vertical, 16.0) .font(.system(size: 13)) .frame(width: 320) } diff --git a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift index 2c0256bba4..78cdc6efcd 100644 --- a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift +++ b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift @@ -55,7 +55,7 @@ struct AddEditBookmarkDialogView: ModalView { isDefaultActionDisabled: viewModel.bookmarkModel.isDefaultActionDisabled, defaultAction: viewModel.bookmarkModel.addOrSave ) - .frame(width: 448, height: 288) + .frame(width: 448) } private var addFolderView: some View { diff --git a/DuckDuckGo/Bookmarks/View/Dialog/BookmarkAllTabsDialogView.swift b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkAllTabsDialogView.swift new file mode 100644 index 0000000000..ad920b10c6 --- /dev/null +++ b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkAllTabsDialogView.swift @@ -0,0 +1,141 @@ +// +// BookmarkAllTabsDialogView.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 SwiftUI +import SwiftUIExtensions + +struct BookmarkAllTabsDialogView: ModalView { + @ObservedObject private var viewModel: BookmarkAllTabsDialogCoordinatorViewModel + + init(viewModel: BookmarkAllTabsDialogCoordinatorViewModel) { + self.viewModel = viewModel + } + + var body: some View { + Group { + switch viewModel.viewState { + case .bookmarkAllTabs: + bookmarkAllTabsView + case .addFolder: + addFolderView + } + } + .font(.system(size: 13)) + } + + private var bookmarkAllTabsView: some View { + BookmarkDialogContainerView( + title: viewModel.bookmarkModel.title, + middleSection: { + Text(viewModel.bookmarkModel.educationalMessage) + .multilineText() + .multilineTextAlignment(.leading) + .foregroundColor(.secondary) + .font(.system(size: 11)) + BookmarkDialogStackedContentView( + .init( + title: UserText.Bookmarks.Dialog.Field.folderName, + content: TextField("", text: $viewModel.bookmarkModel.folderName) + .focusedOnAppear() + .textFieldStyle(RoundedBorderTextFieldStyle()) + .font(.system(size: 14)) + ), + .init( + title: UserText.Bookmarks.Dialog.Field.location, + content: BookmarkDialogFolderManagementView( + folders: viewModel.bookmarkModel.folders, + selectedFolder: $viewModel.bookmarkModel.selectedFolder, + onActionButton: viewModel.addFolderAction + ) + ) + ) + }, + bottomSection: { + BookmarkDialogButtonsView( + viewState: .init(.compressed), + otherButtonAction: .init( + title: viewModel.bookmarkModel.cancelActionTitle, + isDisabled: viewModel.bookmarkModel.isOtherActionDisabled, + action: viewModel.bookmarkModel.cancel + ), + defaultButtonAction: .init( + title: viewModel.bookmarkModel.defaultActionTitle, + keyboardShortCut: .defaultAction, + isDisabled: viewModel.bookmarkModel.isDefaultActionDisabled, + action: viewModel.bookmarkModel.addOrSave + ) + ) + } + + ) + .frame(width: 448) + } + + private var addFolderView: some View { + AddEditBookmarkFolderView( + title: viewModel.folderModel.title, + buttonsState: .compressed, + folders: viewModel.folderModel.folders, + folderName: $viewModel.folderModel.folderName, + selectedFolder: $viewModel.folderModel.selectedFolder, + cancelActionTitle: viewModel.folderModel.cancelActionTitle, + isCancelActionDisabled: viewModel.folderModel.isOtherActionDisabled, + cancelAction: { _ in + viewModel.dismissAction() + }, + defaultActionTitle: viewModel.folderModel.defaultActionTitle, + isDefaultActionDisabled: viewModel.folderModel.isDefaultActionDisabled, + defaultAction: { _ in + viewModel.folderModel.addOrSave { + viewModel.dismissAction() + } + } + ) + .frame(width: 448, height: 210) + } +} + +#if DEBUG +#Preview("Bookmark All Tabs - Light") { + let parentFolder = BookmarkFolder(id: "7", title: "DuckDuckGo") + let bookmark = Bookmark(id: "1", url: "www.duckduckgo.com", title: "DuckDuckGo", isFavorite: true, parentFolderUUID: "7") + let bookmarkManager = LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(bookmarks: [bookmark, parentFolder])) + bookmarkManager.loadBookmarks() + let websitesInfo: [WebsiteInfo] = [ + .init(.init(content: .url(URL.duckDuckGo, credential: nil, source: .ui)))!, + .init(.init(content: .url(URL.duckDuckGoEmail, credential: nil, source: .ui)))!, + ] + + return BookmarksDialogViewFactory.makeBookmarkAllOpenTabsView(websitesInfo: websitesInfo, bookmarkManager: bookmarkManager) + .preferredColorScheme(.light) +} + +#Preview("Bookmark All Tabs - Dark") { + let parentFolder = BookmarkFolder(id: "7", title: "DuckDuckGo") + let bookmark = Bookmark(id: "1", url: "www.duckduckgo.com", title: "DuckDuckGo", isFavorite: true, parentFolderUUID: "7") + let bookmarkManager = LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(bookmarks: [bookmark, parentFolder])) + bookmarkManager.loadBookmarks() + let websitesInfo: [WebsiteInfo] = [ + .init(.init(content: .url(URL.duckDuckGo, credential: nil, source: .ui)))!, + .init(.init(content: .url(URL.duckDuckGoEmail, credential: nil, source: .ui)))!, + ] + + return BookmarksDialogViewFactory.makeBookmarkAllOpenTabsView(websitesInfo: websitesInfo, bookmarkManager: bookmarkManager) + .preferredColorScheme(.dark) +} +#endif diff --git a/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogContainerView.swift b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogContainerView.swift index ea49712abb..120869ea4f 100644 --- a/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogContainerView.swift +++ b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogContainerView.swift @@ -42,9 +42,13 @@ struct BookmarkDialogContainerView: View { Text(title) .foregroundColor(.primary) .fontWeight(.semibold) + .padding(.top, 20) }, center: middleSection, - bottom: bottomSection + bottom: { + bottomSection() + .padding(.bottom, 16.0) + } ) } } diff --git a/DuckDuckGo/Bookmarks/View/Dialog/BookmarksDialogViewFactory.swift b/DuckDuckGo/Bookmarks/View/Dialog/BookmarksDialogViewFactory.swift index b29b50bbbb..3bff7ff4af 100644 --- a/DuckDuckGo/Bookmarks/View/Dialog/BookmarksDialogViewFactory.swift +++ b/DuckDuckGo/Bookmarks/View/Dialog/BookmarksDialogViewFactory.swift @@ -80,6 +80,18 @@ enum BookmarksDialogViewFactory { return makeAddEditBookmarkDialogView(viewModel: viewModel, bookmarkManager: bookmarkManager) } + /// Creates an instance of AddEditBookmarkDialogView for adding Bookmarks for all the open Tabs. + /// - Parameters: + /// - websitesInfo: A list of websites to add as bookmarks. + /// - bookmarkManager: An instance of `BookmarkManager`. This should be used for `#previews` only. + /// - Returns: An instance of BookmarkAllTabsDialogView + static func makeBookmarkAllOpenTabsView(websitesInfo: [WebsiteInfo], bookmarkManager: LocalBookmarkManager = .shared) -> BookmarkAllTabsDialogView { + let addFolderViewModel = AddEditBookmarkFolderDialogViewModel(mode: .add(parentFolder: nil), bookmarkManager: bookmarkManager) + let bookmarkAllTabsViewModel = BookmarkAllTabsDialogViewModel(websites: websitesInfo, foldersStore: UserDefaultsBookmarkFoldersStore(), bookmarkManager: bookmarkManager) + let viewModel = BookmarkAllTabsDialogCoordinatorViewModel(bookmarkModel: bookmarkAllTabsViewModel, folderModel: addFolderViewModel) + return BookmarkAllTabsDialogView(viewModel: viewModel) + } + } private extension BookmarksDialogViewFactory { diff --git a/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkFolderDialogViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkFolderDialogViewModel.swift index 62c1e0356c..48815ebc8d 100644 --- a/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkFolderDialogViewModel.swift +++ b/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkFolderDialogViewModel.swift @@ -42,8 +42,9 @@ final class AddEditBookmarkFolderDialogViewModel: BookmarkFolderDialogEditing { @Published var folderName: String @Published var selectedFolder: BookmarkFolder? + @Published private(set) var folders: [FolderViewModel] - let folders: [FolderViewModel] + private var folderCancellable: AnyCancellable? var title: String { mode.title @@ -77,14 +78,20 @@ final class AddEditBookmarkFolderDialogViewModel: BookmarkFolderDialogEditing { folderName = mode.folderName folders = .init(bookmarkManager.list) selectedFolder = mode.parentFolder + + bind() } func cancel(dismiss: () -> Void) { + reset() dismiss() } func addOrSave(dismiss: () -> Void) { - defer { dismiss() } + defer { + reset() + dismiss() + } guard !folderName.isEmpty else { assertionFailure("folderName is empty, button should be disabled") @@ -110,6 +117,14 @@ final class AddEditBookmarkFolderDialogViewModel: BookmarkFolderDialogEditing { private extension AddEditBookmarkFolderDialogViewModel { + func bind() { + folderCancellable = bookmarkManager.listPublisher + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] bookmarkList in + self?.folders = .init(bookmarkList) + }) + } + func update(folder: BookmarkFolder, originalParent: BookmarkFolder?, newParent: BookmarkFolder?) { // If the original location of the folder changed move it to the new folder. if selectedFolder?.id != originalParent?.id { @@ -129,6 +144,10 @@ private extension AddEditBookmarkFolderDialogViewModel { } } + func reset() { + self.folderName = "" + } + } // MARK: - AddEditBookmarkFolderDialogViewModel.Mode diff --git a/DuckDuckGo/Bookmarks/ViewModel/BookmarkAllTabsDialogCoordinatorViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/BookmarkAllTabsDialogCoordinatorViewModel.swift new file mode 100644 index 0000000000..975c8811b6 --- /dev/null +++ b/DuckDuckGo/Bookmarks/ViewModel/BookmarkAllTabsDialogCoordinatorViewModel.swift @@ -0,0 +1,74 @@ +// +// BookmarkAllTabsDialogCoordinatorViewModel.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 SwiftUI +import Combine + +final class BookmarkAllTabsDialogCoordinatorViewModel: ObservableObject { + @ObservedObject var bookmarkModel: BookmarkViewModel + @ObservedObject var folderModel: AddFolderViewModel + @Published var viewState: ViewState + + private var cancellables: Set = [] + + init(bookmarkModel: BookmarkViewModel, folderModel: AddFolderViewModel) { + self.bookmarkModel = bookmarkModel + self.folderModel = folderModel + viewState = .bookmarkAllTabs + bind() + } + + func dismissAction() { + viewState = .bookmarkAllTabs + } + + func addFolderAction() { + folderModel.selectedFolder = bookmarkModel.selectedFolder + viewState = .addFolder + } + + private func bind() { + bookmarkModel.objectWillChange + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.objectWillChange.send() + } + .store(in: &cancellables) + + folderModel.objectWillChange + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.objectWillChange.send() + } + .store(in: &cancellables) + + folderModel.addFolderPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] bookmarkFolder in + self?.bookmarkModel.selectedFolder = bookmarkFolder + } + .store(in: &cancellables) + } +} + +extension BookmarkAllTabsDialogCoordinatorViewModel { + enum ViewState { + case bookmarkAllTabs + case addFolder + } +} diff --git a/DuckDuckGo/Bookmarks/ViewModel/BookmarkAllTabsDialogViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/BookmarkAllTabsDialogViewModel.swift new file mode 100644 index 0000000000..daf6018403 --- /dev/null +++ b/DuckDuckGo/Bookmarks/ViewModel/BookmarkAllTabsDialogViewModel.swift @@ -0,0 +1,127 @@ +// +// BookmarkAllTabsDialogViewModel.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 Combine + +@MainActor +protocol BookmarkAllTabsDialogEditing: BookmarksDialogViewModel { + var folderName: String { get set } + var educationalMessage: String { get } + var folderNameFieldTitle: String { get } + var locationFieldTitle: String { get } +} + +final class BookmarkAllTabsDialogViewModel: BookmarkAllTabsDialogEditing { + private static let dateFormatter: ISO8601DateFormatter = { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withFullDate, .withDashSeparatorInDate] + return formatter + }() + + private let websites: [WebsiteInfo] + private let foldersStore: BookmarkFoldersStore + private let bookmarkManager: BookmarkManager + + private var folderCancellable: AnyCancellable? + + @Published private(set) var folders: [FolderViewModel] + @Published var selectedFolder: BookmarkFolder? + @Published var folderName: String + + var title: String { + String(format: UserText.Bookmarks.Dialog.Title.bookmarkOpenTabs, websites.count) + } + let cancelActionTitle = UserText.cancel + let defaultActionTitle = UserText.Bookmarks.Dialog.Action.addAllBookmarks + let educationalMessage = UserText.Bookmarks.Dialog.Message.bookmarkOpenTabsEducational + let folderNameFieldTitle = UserText.Bookmarks.Dialog.Field.folderName + let locationFieldTitle = UserText.Bookmarks.Dialog.Field.location + let isOtherActionDisabled = false + + var isDefaultActionDisabled: Bool { + folderName.trimmingWhitespace().isEmpty + } + + init( + websites: [WebsiteInfo], + foldersStore: BookmarkFoldersStore, + bookmarkManager: BookmarkManager = LocalBookmarkManager.shared, + dateFormatterConfigurationProvider: () -> DateFormatterConfiguration = DateFormatterConfiguration.defaultConfiguration + ) { + self.websites = websites + self.foldersStore = foldersStore + self.bookmarkManager = bookmarkManager + + folders = .init(bookmarkManager.list) + selectedFolder = foldersStore.lastBookmarkAllTabsFolderIdUsed.flatMap(bookmarkManager.getBookmarkFolder(withId:)) + folderName = Self.folderName(configuration: dateFormatterConfigurationProvider(), websitesNumber: websites.count) + bind() + } + + func cancel(dismiss: () -> Void) { + dismiss() + } + + func addOrSave(dismiss: () -> Void) { + // Save last used folder + foldersStore.lastBookmarkAllTabsFolderIdUsed = selectedFolder?.id + + // Save all bookmarks + let parentFolder: ParentFolderType = selectedFolder.flatMap { .parent(uuid: $0.id) } ?? .root + bookmarkManager.makeBookmarks(for: websites, inNewFolderNamed: folderName, withinParentFolder: parentFolder) + + // Dismiss the view + dismiss() + } +} + +// MARK: - Private + +private extension BookmarkAllTabsDialogViewModel { + + static func folderName(configuration: DateFormatterConfiguration, websitesNumber: Int) -> String { + Self.dateFormatter.timeZone = configuration.timeZone + let dateString = Self.dateFormatter.string(from: configuration.date) + return String(format: UserText.Bookmarks.Dialog.Value.folderName, dateString, websitesNumber) + } + + func bind() { + folderCancellable = bookmarkManager.listPublisher + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] bookmarkList in + self?.folders = .init(bookmarkList) + }) + } + +} + +// MARK: - DateConfiguration + +extension BookmarkAllTabsDialogViewModel { + + struct DateFormatterConfiguration { + let date: Date + let timeZone: TimeZone + + static func defaultConfiguration() -> DateFormatterConfiguration { + .init(date: Date(), timeZone: .current) + } + } + +} diff --git a/DuckDuckGo/Common/Extensions/NSAttributedStringExtension.swift b/DuckDuckGo/Common/Extensions/NSAttributedStringExtension.swift index 01f2d1c255..58a1cd7fb0 100644 --- a/DuckDuckGo/Common/Extensions/NSAttributedStringExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSAttributedStringExtension.swift @@ -18,6 +18,7 @@ import AppKit +typealias NSAttributedStringBuilder = ArrayBuilder extension NSAttributedString { /// These values come from Figma. Click on the text in Figma and choose Code > iOS to see the values. @@ -31,6 +32,31 @@ extension NSAttributedString { ]) } + convenience init(image: NSImage, rect: CGRect) { + let attachment = NSTextAttachment() + attachment.image = image + attachment.bounds = rect + self.init(attachment: attachment) + } + + convenience init(@NSAttributedStringBuilder components: () -> [NSAttributedString]) { + let components = components() + guard !components.isEmpty else { + self.init() + return + } + guard components.count > 1 else { + self.init(attributedString: components[0]) + return + } + let result = NSMutableAttributedString(attributedString: components[0]) + for component in components[1...] { + result.append(component) + } + + self.init(attributedString: result) + } + } extension NSMutableAttributedString { @@ -48,12 +74,3 @@ extension NSMutableAttributedString { } } - -extension NSTextAttachment { - func setImageHeight(height: CGFloat, offset: CGPoint = .zero) { - guard let image = image else { return } - let ratio = image.size.width / image.size.height - - bounds = CGRect(x: bounds.origin.x + offset.x, y: bounds.origin.y + offset.y, width: ratio * height, height: height) - } -} diff --git a/DuckDuckGo/Common/Extensions/NSStoryboardExtension.swift b/DuckDuckGo/Common/Extensions/NSStoryboardExtension.swift deleted file mode 100644 index 9e50fa79ce..0000000000 --- a/DuckDuckGo/Common/Extensions/NSStoryboardExtension.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// NSStoryboardExtension.swift -// -// 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 AppKit - -extension NSStoryboard { - - static var bookmarks = NSStoryboard(name: "Bookmarks", bundle: .main) - -} diff --git a/DuckDuckGo/Common/Extensions/NSViewExtension.swift b/DuckDuckGo/Common/Extensions/NSViewExtension.swift index b82095c1f0..333dce57cf 100644 --- a/DuckDuckGo/Common/Extensions/NSViewExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSViewExtension.swift @@ -79,6 +79,11 @@ extension NSView { return self } + var isShown: Bool { + get { !isHidden } + set { isHidden = !newValue } + } + func makeMeFirstResponder() { guard let window = window else { os_log("%s: Window not available", type: .error, className) diff --git a/DuckDuckGo/Common/Extensions/URLExtension.swift b/DuckDuckGo/Common/Extensions/URLExtension.swift index 62b6c8fc2b..07683d29cc 100644 --- a/DuckDuckGo/Common/Extensions/URLExtension.swift +++ b/DuckDuckGo/Common/Extensions/URLExtension.swift @@ -30,6 +30,11 @@ extension URL.NavigationalScheme { return [.http, .https, .file] } + /// HTTP or HTTPS + var isHypertextScheme: Bool { + Self.hypertextSchemes.contains(self) + } + } extension URL { @@ -137,12 +142,16 @@ extension URL { // 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")! + static let dataBrokerProtection = URL(string: "duck://personal-information-removal")! #if !SANDBOX_TEST_TOOL static func settingsPane(_ pane: PreferencePaneIdentifier) -> URL { return settings.appendingPathComponent(pane.rawValue) } + + var isSettingsURL: Bool { + isChild(of: .settings) && (pathComponents.isEmpty || PreferencePaneIdentifier(url: self) != nil) + } #endif enum Invalid { @@ -404,6 +413,10 @@ extension URL { return false } + var isEmailProtection: Bool { + self.isChild(of: .duckDuckGoEmailLogin) || self == .duckDuckGoEmail + } + enum DuckDuckGoParameters: String { case search = "q" case ia @@ -547,7 +560,7 @@ extension URL { return false } - func stripUnsupportedCredentials() -> String { + func strippingUnsupportedCredentials() -> String { if self.absoluteString.firstIndex(of: "@") != nil { let authPattern = "([^:]+):\\/\\/[^\\/]*@" let strippedURL = self.absoluteString.replacingOccurrences(of: authPattern, with: "$1://", options: .regularExpression) @@ -558,7 +571,14 @@ extension URL { } public func isChild(of parentURL: URL) -> Bool { - guard let parentURLHost = parentURL.host, self.isPart(ofDomain: parentURLHost) else { return false } - return pathComponents.starts(with: parentURL.pathComponents) + if scheme == parentURL.scheme, + port == parentURL.port, + let parentURLHost = parentURL.host, + self.isPart(ofDomain: parentURLHost), + pathComponents.starts(with: parentURL.pathComponents) { + return true + } else { + return false + } } } diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 9cb9493ebf..a201afff72 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -212,6 +212,7 @@ struct UserText { static let addFolder = NSLocalizedString("menu.add.folder", value: "Add Folder…", comment: "Menu item to add a folder") static let tabHomeTitle = NSLocalizedString("tab.home.title", value: "New Tab", comment: "Tab home title") + static let tabUntitledTitle = NSLocalizedString("tab.empty.title", value: "Untitled", comment: "Title for an empty tab without a title") 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") @@ -464,7 +465,8 @@ struct UserText { static let addFavorite = NSLocalizedString("add.favorite", value: "Add Favorite", comment: "Button for adding a favorite bookmark") static let editFavorite = NSLocalizedString("edit.favorite", value: "Edit Favorite", comment: "Header of the view that edits a favorite bookmark") static let removeFromFavorites = NSLocalizedString("remove.from.favorites", value: "Remove from Favorites", comment: "Button for removing bookmarks from favorites") - static let bookmarkThisPage = NSLocalizedString("bookmark.this.page", value: "Bookmark This Page", comment: "Menu item for bookmarking current page") + static let bookmarkThisPage = NSLocalizedString("bookmark.this.page", value: "Bookmark This Page…", comment: "Menu item for bookmarking current page") + static let bookmarkAllTabs = NSLocalizedString("bookmark.all.tabs", value: "Bookmark All Tabs…", comment: "Menu item for bookmarking all the open tabs") static let bookmarksShowToolbarPanel = NSLocalizedString("bookmarks.show-toolbar-panel", value: "Open Bookmarks Panel", comment: "Menu item for opening the bookmarks panel") static let bookmarksManageBookmarks = NSLocalizedString("bookmarks.manage-bookmarks", value: "Manage Bookmarks", comment: "Menu item for opening the bookmarks management interface") static let bookmarkImportedFromFolder = NSLocalizedString("bookmarks.imported.from.folder", value: "Imported from", comment: "Name of the folder the imported bookmarks are saved into") @@ -1104,15 +1106,24 @@ struct UserText { static let editBookmark = NSLocalizedString("bookmarks.dialog.title.edit", value: "Edit Bookmark", comment: "Bookmark edit dialog title") static let addFolder = NSLocalizedString("bookmarks.dialog.folder.title.add", value: "Add Folder", comment: "Bookmark folder creation dialog title") static let editFolder = NSLocalizedString("bookmarks.dialog.folder.title.edit", value: "Edit Folder", comment: "Bookmark folder edit dialog title") + static let bookmarkOpenTabs = NSLocalizedString("bookmarks.dialog.allTabs.title.add", value: "Bookmark Open Tabs (%d)", comment: "Title of dialog to bookmark all open tabs. E.g. 'Bookmark Open Tabs (42)'") + } + enum Message { + static let bookmarkOpenTabsEducational = NSLocalizedString("bookmarks.dialog.allTabs.message.add", value: "These bookmarks will be saved in a new folder:", comment: "Bookmark creation for all open tabs dialog title") } enum Field { static let name = NSLocalizedString("bookmarks.dialog.field.name", value: "Name", comment: "Name field label for Bookmark or Folder") static let url = NSLocalizedString("bookmarks.dialog.field.url", value: "URL", comment: "URL field label for Bookmar") static let location = NSLocalizedString("bookmarks.dialog.field.location", value: "Location", comment: "Location field label for Bookmark folder") + static let folderName = NSLocalizedString("bookmarks.dialog.field.folderName", value: "Folder Name", comment: "Folder name field label for Bookmarks folder") + } + enum Value { + static let folderName = NSLocalizedString("bookmarks.dialog.field.folderName.value", value: "%@ - Tabs (%d)", comment: "The suggested name of the folder that will contain the bookmark tabs. Eg. 2024-02-12 - Tabs (42)") } enum Action { static let addBookmark = NSLocalizedString("bookmarks.dialog.action.addBookmark", value: "Add Bookmark", comment: "CTA title for adding a Bookmark") static let addFolder = NSLocalizedString("bookmarks.dialog.action.addFolder", value: "Add Folder", comment: "CTA title for adding a Folder") + static let addAllBookmarks = NSLocalizedString("bookmarks.dialog.action.addAllBookmarks", value: "Save Bookmarks", comment: "CTA title for saving multiple Bookmarks at once") } } } @@ -1138,4 +1149,14 @@ struct UserText { // Key: "subscription.progress.view.completing.purchase" // Comment: "Progress view title when completing the purchase" static let completingPurchaseTitle = "Completing purchase..." + + // MARK: - DBP Error pages + + static let dbpErrorPageBadPathTitle = "Move DuckDuckGo App to Applications" + static let dbpErrorPageBadPathMessage = "To use Personal Information Removal, the DuckDuckGo app needs to be in the Applications folder on your Mac. You can move the app yourself and restart the browser, or we can do it for you." + static let dbpErrorPageBadPathCTA = "Move App for Me..." + + static let dbpErrorPageNoPermissionTitle = "Change System Setting" + static let dbpErrorPageNoPermissionMessage = "Open System Settings and allow DuckDuckGo Personal Information Removal to run in the background." + static let dbpErrorPageNoPermissionCTA = "Open System Settings..." } diff --git a/DuckDuckGo/Common/View/AppKit/LoadingProgressView.swift b/DuckDuckGo/Common/View/AppKit/LoadingProgressView.swift index 56f6b2de2b..9c444e511b 100644 --- a/DuckDuckGo/Common/View/AppKit/LoadingProgressView.swift +++ b/DuckDuckGo/Common/View/AppKit/LoadingProgressView.swift @@ -31,7 +31,7 @@ final class LoadingProgressView: NSView, CAAnimationDelegate { private var targetProgress: Double = 0.0 private var targetTime: CFTimeInterval = 0.0 - var isShown: Bool { + var isProgressShown: Bool { progressMask.opacity == 1.0 } diff --git a/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift b/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift index 504b70a61f..e4add5a17e 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 = "\"a482727f0d20b29eabd1e22fde2d54cf\"" - public static let embeddedDataSHA = "993aa84559944a8866e40cebbce02beee2b1597f86b63f998d000d2a0e5d617a" + public static let embeddedDataETag = "\"7cf7b71adb62c3cbcbf8b84c61a0004d\"" + public static let embeddedDataSHA = "20e9b59e7e60ccc9ae52853935ebe3d74227234fcf8b46da5a66cff3adc7e6c7" } var embeddedDataEtag: String { diff --git a/DuckDuckGo/ContentBlocker/macos-config.json b/DuckDuckGo/ContentBlocker/macos-config.json index 7c20612900..39163097c1 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": 1713140318814, + "version": 1713542334045, "features": { "adClickAttribution": { "readme": "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/#3rd-party-tracker-loading-protection", @@ -279,6 +279,15 @@ { "domain": "condell-ltd.com" }, + { + "domain": "leefgemeenschapzilt.nl" + }, + { + "domain": "healthline.com" + }, + { + "domain": "sporthoj.com" + }, { "domain": "marvel.com" }, @@ -290,13 +299,12 @@ "disabledCMPs": [ "generic-cosmetic", "termsfeed3", - "strato.de", "healthline-media", "tarteaucitron.js" ] }, "state": "enabled", - "hash": "0eaff8b64b6d3e8a59879f3b4ab6c0ba" + "hash": "f35e24cf85485b441cb9a76146e77e17" }, "autofill": { "exceptions": [ @@ -2843,6 +2851,27 @@ } ] }, + { + "domain": "independent.co.uk", + "rules": [ + { + "selector": "#partners", + "type": "hide-empty" + }, + { + "selector": "#top-banner-wrapper", + "type": "hide-empty" + }, + { + "selector": "[data-mpu1=true]", + "type": "hide-empty" + }, + { + "selector": "#stickyFooterRoot", + "type": "hide" + } + ] + }, { "domain": "indiatimes.com", "rules": [ @@ -4231,7 +4260,7 @@ ] }, "state": "enabled", - "hash": "c0fa0dfbc6231be31492023b623ac99b" + "hash": "765e789c939c6e3307f576bc698fbb9e" }, "exceptionHandler": { "exceptions": [ @@ -4884,18 +4913,11 @@ } }, "toggleReports": { - "state": "enabled", - "rollout": { - "steps": [ - { - "percent": 10 - } - ] - } + "state": "enabled" } }, "state": "enabled", - "hash": "b337f9c7cf15e7e4807ef232befaa999" + "hash": "66968d9b69520975185476473cc11824" }, "privacyPro": { "state": "enabled", @@ -5218,7 +5240,8 @@ { "rule": "static.adsafeprotected.com/iasPET.1.js", "domains": [ - "corriere.it" + "corriere.it", + "independent.co.uk" ] }, { @@ -6894,7 +6917,8 @@ "domains": [ "andieswim.com", "footweartruth.com", - "kmail-lists.com" + "kmail-lists.com", + "usafacts.org" ] } ] @@ -7107,7 +7131,8 @@ { "rule": "npttech.com/advertising.js", "domains": [ - "blick.ch" + "blick.ch", + "independent.co.uk" ] } ] @@ -8195,7 +8220,7 @@ "domain": "sundancecatalog.com" } ], - "hash": "2d627140b59bca8b8edbc236e79cd46e" + "hash": "2d5ce26ddae089bcb61e4f4a0b1ae487" }, "trackingCookies1p": { "settings": { diff --git a/DuckDuckGo/DBP/DBPHomeViewController.swift b/DuckDuckGo/DBP/DBPHomeViewController.swift index 30f4a4c0ce..cf9e05e900 100644 --- a/DuckDuckGo/DBP/DBPHomeViewController.swift +++ b/DuckDuckGo/DBP/DBPHomeViewController.swift @@ -34,8 +34,15 @@ final class DBPHomeViewController: NSViewController { private var presentedWindowController: NSWindowController? private let dataBrokerProtectionManager: DataBrokerProtectionManager private let pixelHandler: EventMapping = DataBrokerProtectionPixelsHandler() + private var currentChildViewController: NSViewController? + private var observer: NSObjectProtocol? - lazy var dataBrokerProtectionViewController: DataBrokerProtectionViewController = { + private let prerequisiteVerifier: DataBrokerPrerequisitesStatusVerifier + private lazy var errorViewController: DataBrokerProtectionErrorViewController = { + DataBrokerProtectionErrorViewController() + }() + + private lazy var dataBrokerProtectionViewController: DataBrokerProtectionViewController = { let privacyConfigurationManager = PrivacyFeatures.contentBlocking.privacyConfigurationManager let features = ContentScopeFeatureToggles(emailProtection: false, emailProtectionIncontextSignup: false, @@ -64,8 +71,9 @@ final class DBPHomeViewController: NSViewController { }) }() - init(dataBrokerProtectionManager: DataBrokerProtectionManager) { + init(dataBrokerProtectionManager: DataBrokerProtectionManager, prerequisiteVerifier: DataBrokerPrerequisitesStatusVerifier = DefaultDataBrokerPrerequisitesStatusVerifier()) { self.dataBrokerProtectionManager = dataBrokerProtectionManager + self.prerequisiteVerifier = prerequisiteVerifier super.init(nibName: nil, bundle: nil) } @@ -80,9 +88,8 @@ final class DBPHomeViewController: NSViewController { override func viewDidLoad() { super.viewDidLoad() - if !dataBrokerProtectionManager.shouldAskForInviteCode() { - attachDataBrokerContainerView() - } + setupUI() + setupObserver() do { if try dataBrokerProtectionManager.dataManager.fetchProfile() != nil { @@ -95,22 +102,31 @@ final class DBPHomeViewController: NSViewController { } } - private func attachDataBrokerContainerView() { - addChild(dataBrokerProtectionViewController) - view.addSubview(dataBrokerProtectionViewController.view) - } - override func viewDidAppear() { super.viewDidAppear() - if dataBrokerProtectionManager.shouldAskForInviteCode() { + if shouldAskForInviteCode() { presentInviteCodeFlow() } } override func viewDidLayout() { super.viewDidLayout() - dataBrokerProtectionViewController.view.frame = view.bounds + if let currentChildViewController = currentChildViewController { + currentChildViewController.view.frame = view.bounds + } + } + + private func setupUI() { + if !shouldAskForInviteCode() { + setupUIWithCurrentStatus() + } + } + + private func setupObserver() { + observer = NotificationCenter.default.addObserver(forName: NSApplication.didBecomeActiveNotification, object: nil, queue: nil) { [weak self] _ in + self?.setupUI() + } } private func presentInviteCodeFlow() { @@ -128,13 +144,54 @@ final class DBPHomeViewController: NSViewController { } parentWindowController.window?.beginSheet(newWindow) } + + private func setupUIWithCurrentStatus() { + setupUIWithStatus(prerequisiteVerifier.checkStatus()) + } + + private func setupUIWithStatus(_ status: DataBrokerPrerequisitesStatus) { + switch status { + case .invalidDirectory: + displayWrongDirectoryErrorUI() + pixelHandler.fire(.homeViewShowBadPathError) + case .invalidSystemPermission: + displayWrongPermissionsErrorUI() + pixelHandler.fire(.homeViewShowNoPermissionError) + case .valid: + displayDBPUI() + pixelHandler.fire(.homeViewShowWebUI) + } + } + + private func shouldAskForInviteCode() -> Bool { + prerequisiteVerifier.checkStatus() == .valid && dataBrokerProtectionManager.shouldAskForInviteCode() + } + + private func displayDBPUI() { + replaceChildController(dataBrokerProtectionViewController) + } + + private func replaceChildController(_ childViewController: NSViewController) { + if let child = currentChildViewController { + child.removeCompletely() + } + + addAndLayoutChild(childViewController) + self.currentChildViewController = childViewController + } + + deinit { + if let observer = observer { + NotificationCenter.default.removeObserver(observer) + } + } } extension DBPHomeViewController: DataBrokerProtectionInviteDialogsViewModelDelegate { func dataBrokerProtectionInviteDialogsViewModelDidReedemSuccessfully(_ viewModel: DataBrokerProtectionInviteDialogsViewModel) { presentedWindowController?.window?.close() presentedWindowController = nil - attachDataBrokerContainerView() + setupUIWithCurrentStatus() } func dataBrokerProtectionInviteDialogsViewModelDidCancel(_ viewModel: DataBrokerProtectionInviteDialogsViewModel) { @@ -144,78 +201,54 @@ extension DBPHomeViewController: DataBrokerProtectionInviteDialogsViewModelDeleg } } -public class DataBrokerProtectionPixelsHandler: EventMapping { - - // swiftlint:disable:next function_body_length - public init() { - super.init { event, _, _, _ in - switch event { - case .error(let error, _): - PixelKit.fire(DebugEvent(event, error: error)) - case .generalError(let error, _), - .secureVaultInitError(let error), - .secureVaultError(let error): - PixelKit.fire(DebugEvent(event, error: error)) - case .ipcServerOptOutAllBrokersCompletion(error: let error), - .ipcServerScanAllBrokersCompletion(error: let error), - .ipcServerRunQueuedOperationsCompletion(error: let error): - PixelKit.fire(DebugEvent(event, error: error)) - case .parentChildMatches, - .optOutStart, - .optOutEmailGenerate, - .optOutCaptchaParse, - .optOutCaptchaSend, - .optOutCaptchaSolve, - .optOutSubmit, - .optOutEmailReceive, - .optOutEmailConfirm, - .optOutValidate, - .optOutFinish, - .optOutSubmitSuccess, - .optOutFillForm, - .optOutSuccess, - .optOutFailure, - .backgroundAgentStarted, - .backgroundAgentRunOperationsAndStartSchedulerIfPossible, - .backgroundAgentRunOperationsAndStartSchedulerIfPossibleNoSavedProfile, - .backgroundAgentRunOperationsAndStartSchedulerIfPossibleRunQueuedOperationsCallbackStartScheduler, - .backgroundAgentStartedStoppingDueToAnotherInstanceRunning, - .ipcServerStartScheduler, - .ipcServerStopScheduler, - .ipcServerOptOutAllBrokers, - .ipcServerScanAllBrokers, - .ipcServerRunQueuedOperations, - .ipcServerRunAllOperations, - .scanSuccess, - .scanFailed, - .scanError, - .dataBrokerProtectionNotificationSentFirstScanComplete, - .dataBrokerProtectionNotificationOpenedFirstScanComplete, - .dataBrokerProtectionNotificationSentFirstRemoval, - .dataBrokerProtectionNotificationOpenedFirstRemoval, - .dataBrokerProtectionNotificationScheduled2WeeksCheckIn, - .dataBrokerProtectionNotificationOpened2WeeksCheckIn, - .dataBrokerProtectionNotificationSentAllRecordsRemoved, - .dataBrokerProtectionNotificationOpenedAllRecordsRemoved, - .dailyActiveUser, - .weeklyActiveUser, - .monthlyActiveUser, - .weeklyReportScanning, - .weeklyReportRemovals, - .scanningEventNewMatch, - .scanningEventReAppearance, - .webUILoadingFailed, - .webUILoadingStarted, - .webUILoadingSuccess, - .emptyAccessTokenDaily, - .generateEmailHTTPErrorDaily: - PixelKit.fire(event) - } +// MARK: - Error UI + +extension DBPHomeViewController { + private func displayWrongDirectoryErrorUI() { + let errorViewModel = DataBrokerProtectionErrorViewModel(title: UserText.dbpErrorPageBadPathTitle, + message: UserText.dbpErrorPageBadPathMessage, + ctaText: UserText.dbpErrorPageBadPathCTA, + ctaAction: { [weak self] in + self?.moveToApplicationFolder() + }) + + errorViewController.errorViewModel = errorViewModel + replaceChildController(errorViewController) + } + + private func displayWrongPermissionsErrorUI() { + let errorViewModel = DataBrokerProtectionErrorViewModel(title: UserText.dbpErrorPageNoPermissionTitle, + message: UserText.dbpErrorPageNoPermissionMessage, + ctaText: UserText.dbpErrorPageNoPermissionCTA, + ctaAction: { [weak self] in + self?.openLoginItemSettings() + }) + + errorViewController.errorViewModel = errorViewModel + replaceChildController(errorViewController) + } +} + +// MARK: - System configuration + +import ServiceManagement + +extension DBPHomeViewController { + func openLoginItemSettings() { + pixelHandler.fire(.homeViewCTAGrantPermissionClicked) + if #available(macOS 13.0, *) { + SMAppService.openSystemSettingsLoginItems() + } else { + let loginItemsURL = URL(string: "x-apple.systempreferences:com.apple.LoginItems-Settings.extension")! + NSWorkspace.shared.open(loginItemsURL) } } - override init(mapping: @escaping EventMapping.Mapping) { - fatalError("Use init()") + func moveToApplicationFolder() { + pixelHandler.fire(.homeViewCTAMoveApplicationClicked) + Task { @MainActor in + await AppLauncher(appBundleURL: Bundle.main.bundleURL).launchApp(withCommand: .moveAppToApplications) + } } } diff --git a/DuckDuckGo/DBP/DataBrokerPrerequisitesStatusVerifier.swift b/DuckDuckGo/DBP/DataBrokerPrerequisitesStatusVerifier.swift new file mode 100644 index 0000000000..f5f5c4d091 --- /dev/null +++ b/DuckDuckGo/DBP/DataBrokerPrerequisitesStatusVerifier.swift @@ -0,0 +1,50 @@ +// +// DataBrokerPrerequisitesStatusVerifier.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 Combine +import DataBrokerProtection +import LoginItems + +enum DataBrokerPrerequisitesStatus { + case invalidDirectory + case invalidSystemPermission + case valid +} + +protocol DataBrokerPrerequisitesStatusVerifier: AnyObject { + func checkStatus() -> DataBrokerPrerequisitesStatus +} + +final class DefaultDataBrokerPrerequisitesStatusVerifier: DataBrokerPrerequisitesStatusVerifier { + private let statusChecker: DBPLoginItemStatusChecker + + init(statusChecker: DBPLoginItemStatusChecker = LoginItem.dbpBackgroundAgent) { + self.statusChecker = statusChecker + } + + func checkStatus() -> DataBrokerPrerequisitesStatus { + if !statusChecker.doesHaveNecessaryPermissions() { + return .invalidSystemPermission + } else if !statusChecker.isInCorrectDirectory() { + return .invalidDirectory + } else { + return .valid + } + } +} diff --git a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift index bf1a08a800..48a458f3cf 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift @@ -48,6 +48,7 @@ final class DataBrokerProtectionDebugMenu: NSMenu { private let customURLLabelMenuItem = NSMenuItem(title: "") private let environmentMenu = NSMenu() + private let statusMenuIconMenu = NSMenuItem(title: "Show Status Menu Icon", action: #selector(DataBrokerProtectionDebugMenu.toggleShowStatusMenuItem)) private let webUISettings = DataBrokerProtectionWebUIURLSettings(.dbp) private let settings = DataBrokerProtectionSettings(defaults: .dbp) @@ -147,6 +148,8 @@ final class DataBrokerProtectionDebugMenu: NSMenu { NSMenuItem.separator() + statusMenuIconMenu.targetting(self) + NSMenuItem(title: "Show DB Browser", action: #selector(DataBrokerProtectionDebugMenu.showDatabaseBrowser)) .targetting(self) NSMenuItem(title: "Force Profile Removal", action: #selector(DataBrokerProtectionDebugMenu.showForceOptOutWindow)) @@ -172,6 +175,7 @@ final class DataBrokerProtectionDebugMenu: NSMenu { updateWaitlistItems() updateWebUIMenuItemsState() updateEnvironmentMenu() + updateShowStatusMenuIconMenu() } // MARK: - Menu functions @@ -223,7 +227,7 @@ final class DataBrokerProtectionDebugMenu: NSMenu { os_log("Running scan operations...", log: .dataBrokerProtection) let showWebView = sender.representedObject as? Bool ?? false - DataBrokerProtectionManager.shared.scheduler.scanAllBrokers(showWebView: showWebView) { errors in + DataBrokerProtectionManager.shared.scheduler.startManualScan(showWebView: showWebView) { errors in if let errors = errors { if let oneTimeError = errors.oneTimeError { os_log("scan operations finished, error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) @@ -366,6 +370,10 @@ final class DataBrokerProtectionDebugMenu: NSMenu { } } + @objc private func toggleShowStatusMenuItem() { + settings.showInMenuBar.toggle() + } + @objc func setSelectedEnvironment(_ menuItem: NSMenuItem) { let title = menuItem.title let selectedEnvironment: DataBrokerProtectionSettings.SelectedEnvironment @@ -448,6 +456,10 @@ final class DataBrokerProtectionDebugMenu: NSMenu { environmentMenu.items.first?.state = selectedEnvironment == .production ? .on: .off environmentMenu.items.last?.state = selectedEnvironment == .staging ? .on: .off } + + private func updateShowStatusMenuIconMenu() { + statusMenuIconMenu.state = settings.showInMenuBar ? .on : .off + } } extension DataBrokerProtectionDebugMenu: NSWindowDelegate { diff --git a/DuckDuckGo/DBP/DataBrokerProtectionLoginItemScheduler.swift b/DuckDuckGo/DBP/DataBrokerProtectionLoginItemScheduler.swift index 8cd8416bc4..cc0d841ee8 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionLoginItemScheduler.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionLoginItemScheduler.swift @@ -55,10 +55,10 @@ extension DataBrokerProtectionLoginItemScheduler: DataBrokerProtectionScheduler ipcScheduler.statusPublisher } - func scanAllBrokers(showWebView: Bool, - completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { + func startManualScan(showWebView: Bool, + completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { enableLoginItem() - ipcScheduler.scanAllBrokers(showWebView: showWebView, completion: completion) + ipcScheduler.startManualScan(showWebView: showWebView, completion: completion) } func startScheduler(showWebView: Bool) { diff --git a/DuckDuckGo/DBP/DataBrokerProtectionManager.swift b/DuckDuckGo/DBP/DataBrokerProtectionManager.swift index adb20e4aee..f3b8705674 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionManager.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionManager.swift @@ -41,7 +41,12 @@ public final class DataBrokerProtectionManager { return dataManager }() - private lazy var ipcClient = DataBrokerProtectionIPCClient(machServiceName: Bundle.main.dbpBackgroundAgentBundleId, pixelHandler: pixelHandler) + private lazy var ipcClient: DataBrokerProtectionIPCClient = { + let loginItemStatusChecker = LoginItem.dbpBackgroundAgent + return DataBrokerProtectionIPCClient(machServiceName: Bundle.main.dbpBackgroundAgentBundleId, + pixelHandler: pixelHandler, + loginItemStatusChecker: loginItemStatusChecker) + }() lazy var scheduler: DataBrokerProtectionLoginItemScheduler = { diff --git a/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift b/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift new file mode 100644 index 0000000000..08c5b74f18 --- /dev/null +++ b/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift @@ -0,0 +1,118 @@ +// +// DataBrokerProtectionPixelsHandler.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 DataBrokerProtection +import PixelKit +import Common + +public class DataBrokerProtectionPixelsHandler: EventMapping { + + // swiftlint:disable:next function_body_length + public init() { + super.init { event, _, _, _ in + switch event { + case .error(let error, _): + PixelKit.fire(DebugEvent(event, error: error)) + case .generalError(let error, _), + .secureVaultInitError(let error), + .secureVaultError(let error): + PixelKit.fire(DebugEvent(event, error: error)) + case .ipcServerStartSchedulerXPCError(error: let error), + .ipcServerStopSchedulerXPCError(error: let error), + .ipcServerScanAllBrokersXPCError(error: let error), + .ipcServerScanAllBrokersCompletedOnAgentWithError(error: let error), + .ipcServerScanAllBrokersCompletionCalledOnAppWithError(error: let error), + .ipcServerOptOutAllBrokersCompletion(error: let error), + .ipcServerRunQueuedOperationsCompletion(error: let error): + PixelKit.fire(DebugEvent(event, error: error), frequency: .dailyAndCount, includeAppVersionParameter: true) + case .ipcServerStartSchedulerCalledByApp, + .ipcServerStartSchedulerReceivedByAgent, + .ipcServerStopSchedulerCalledByApp, + .ipcServerStopSchedulerReceivedByAgent, + .ipcServerScanAllBrokersAttemptedToCallWithoutLoginItemPermissions, + .ipcServerScanAllBrokersAttemptedToCallInWrongDirectory, + .ipcServerScanAllBrokersCalledByApp, + .ipcServerScanAllBrokersReceivedByAgent, + .ipcServerScanAllBrokersCompletedOnAgentWithoutError, + .ipcServerScanAllBrokersCompletionCalledOnAppWithoutError, + .ipcServerScanAllBrokersInterruptedOnAgent, + .ipcServerScanAllBrokersCompletionCalledOnAppAfterInterruption: + PixelKit.fire(event, frequency: .dailyAndCount, includeAppVersionParameter: true) + case .parentChildMatches, + .optOutStart, + .optOutEmailGenerate, + .optOutCaptchaParse, + .optOutCaptchaSend, + .optOutCaptchaSolve, + .optOutSubmit, + .optOutEmailReceive, + .optOutEmailConfirm, + .optOutValidate, + .optOutFinish, + .optOutSubmitSuccess, + .optOutFillForm, + .optOutSuccess, + .optOutFailure, + .backgroundAgentStarted, + .backgroundAgentRunOperationsAndStartSchedulerIfPossible, + .backgroundAgentRunOperationsAndStartSchedulerIfPossibleNoSavedProfile, + .backgroundAgentRunOperationsAndStartSchedulerIfPossibleRunQueuedOperationsCallbackStartScheduler, + .backgroundAgentStartedStoppingDueToAnotherInstanceRunning, + .ipcServerOptOutAllBrokers, + .ipcServerRunQueuedOperations, + .ipcServerRunAllOperations, + .scanSuccess, + .scanFailed, + .scanError, + .dataBrokerProtectionNotificationSentFirstScanComplete, + .dataBrokerProtectionNotificationOpenedFirstScanComplete, + .dataBrokerProtectionNotificationSentFirstRemoval, + .dataBrokerProtectionNotificationOpenedFirstRemoval, + .dataBrokerProtectionNotificationScheduled2WeeksCheckIn, + .dataBrokerProtectionNotificationOpened2WeeksCheckIn, + .dataBrokerProtectionNotificationSentAllRecordsRemoved, + .dataBrokerProtectionNotificationOpenedAllRecordsRemoved, + .dailyActiveUser, + .weeklyActiveUser, + .monthlyActiveUser, + .weeklyReportScanning, + .weeklyReportRemovals, + .scanningEventNewMatch, + .scanningEventReAppearance, + .webUILoadingFailed, + .webUILoadingStarted, + .webUILoadingSuccess, + .emptyAccessTokenDaily, + .generateEmailHTTPErrorDaily: + PixelKit.fire(event) + + case .homeViewShowNoPermissionError, + .homeViewShowWebUI, + .homeViewShowBadPathError, + .homeViewCTAMoveApplicationClicked, + .homeViewCTAGrantPermissionClicked: + PixelKit.fire(event, frequency: .dailyAndCount) + } + } + } + + override init(mapping: @escaping EventMapping.Mapping) { + fatalError("Use init()") + } +} diff --git a/DuckDuckGo/DBP/ErrorView/DataBrokerProtectionErrorViewController.swift b/DuckDuckGo/DBP/ErrorView/DataBrokerProtectionErrorViewController.swift new file mode 100644 index 0000000000..fa628b686e --- /dev/null +++ b/DuckDuckGo/DBP/ErrorView/DataBrokerProtectionErrorViewController.swift @@ -0,0 +1,92 @@ +// +// DataBrokerProtectionErrorViewController.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 SwiftUI + +final class DataBrokerProtectionErrorViewController: NSViewController { + private var errorSubview: NSView? + + var errorViewModel: DataBrokerProtectionErrorViewModel? { + didSet { + guard let errorViewModel = errorViewModel else { return } + + errorSubview?.removeFromSuperview() + + let errorView = DataBrokerProtectionErrorView(viewModel: errorViewModel) + errorSubview = NSHostingView(rootView: errorView) + + if let errorSubview = errorSubview { + view.addAndLayout(errorSubview) + } + } + } +} + +struct DataBrokerProtectionErrorView: View { + var viewModel: DataBrokerProtectionErrorViewModel + + var body: some View { + VStack(alignment: .center, spacing: 16) { + + HStack { + Image("DaxLockScreenLogo") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 32, height: 32) + + Text("Privacy Pro") + .font(.title) + .fontWeight(.light) + } + .padding(.bottom, 25) + + HStack { + Image("dbp-error-info") + .resizable() + .frame(width: 24, height: 24) + + Text(viewModel.title) + .font(.title) + .fontWeight(.light) + } + + Text(viewModel.message) + .font(.body) + .fontWeight(.light) + .multilineTextAlignment(.center) + .padding(.bottom, 10) + + Button(action: { + viewModel.ctaAction() + }) { + Text(viewModel.ctaText) + } + + Spacer() + }.padding() + .frame(maxWidth: 500) + } +} + +struct DataBrokerProtectionErrorViewModel { + let title: String + let message: String + let ctaText: String + let ctaAction: () -> Void +} diff --git a/DuckDuckGo/DBP/LoginItem+DataBrokerProtection.swift b/DuckDuckGo/DBP/LoginItem+DataBrokerProtection.swift index cdbfa623a9..d6fbfa761f 100644 --- a/DuckDuckGo/DBP/LoginItem+DataBrokerProtection.swift +++ b/DuckDuckGo/DBP/LoginItem+DataBrokerProtection.swift @@ -18,6 +18,7 @@ import Foundation import LoginItems +import DataBrokerProtection #if DBP @@ -27,4 +28,28 @@ extension LoginItem { } +extension LoginItem: DBPLoginItemStatusChecker { + + public func doesHaveNecessaryPermissions() -> Bool { + return status != .requiresApproval + } + + public func isInCorrectDirectory() -> Bool { + guard let appPath = Bundle.main.resourceURL?.deletingLastPathComponent() else { return false } + let dirPaths = NSSearchPathForDirectoriesInDomains(.applicationDirectory, .localDomainMask, true) + for path in dirPaths { + let filePath: URL + if #available(macOS 13.0, *) { + filePath = URL(filePath: path) + } else { + filePath = URL(fileURLWithPath: path) + } + if appPath.absoluteString.hasPrefix(filePath.absoluteString) { + return true + } + } + return false + } +} + #endif diff --git a/DuckDuckGo/DataImport/Logins/SecureVault/SecureVaultLoginImporter.swift b/DuckDuckGo/DataImport/Logins/SecureVault/SecureVaultLoginImporter.swift index 17acb874e0..193806cef0 100644 --- a/DuckDuckGo/DataImport/Logins/SecureVault/SecureVaultLoginImporter.swift +++ b/DuckDuckGo/DataImport/Logins/SecureVault/SecureVaultLoginImporter.swift @@ -23,7 +23,7 @@ import SecureStorage final class SecureVaultLoginImporter: LoginImporter { func importLogins(_ logins: [ImportedLoginCredential], progressCallback: @escaping (Int) throws -> Void) throws -> DataImport.DataTypeSummary { - let vault = try AutofillSecureVaultFactory.makeVault(errorReporter: SecureVaultErrorReporter.shared) + let vault = try AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter.shared) var successful: [String] = [] var duplicates: [String] = [] diff --git a/DuckDuckGo/Feedback/View/FeedbackViewController.swift b/DuckDuckGo/Feedback/View/FeedbackViewController.swift index ff45c1a7e9..6bec572e75 100644 --- a/DuckDuckGo/Feedback/View/FeedbackViewController.swift +++ b/DuckDuckGo/Feedback/View/FeedbackViewController.swift @@ -75,7 +75,7 @@ final class FeedbackViewController: NSViewController { var currentTab: Tab? var currentTabUrl: URL? { - guard let url = currentTab?.content.url else { + guard let url = currentTab?.content.urlForWebView else { return nil } diff --git a/DuckDuckGo/Fire/Model/Fire.swift b/DuckDuckGo/Fire/Model/Fire.swift index 29c2ca70f0..4766d8b6e6 100644 --- a/DuckDuckGo/Fire/Model/Fire.swift +++ b/DuckDuckGo/Fire/Model/Fire.swift @@ -385,7 +385,7 @@ final class Fire { // MARK: - Favicons private func autofillDomains() -> Set { - guard let vault = try? secureVaultFactory.makeVault(errorReporter: SecureVaultErrorReporter.shared), + guard let vault = try? secureVaultFactory.makeVault(reporter: SecureVaultReporter.shared), let accounts = try? vault.accounts() else { return [] } diff --git a/DuckDuckGo/Fireproofing/Extensions/FireproofingURLExtensions.swift b/DuckDuckGo/Fireproofing/Extensions/FireproofingURLExtensions.swift index d75068166c..da4b9df331 100644 --- a/DuckDuckGo/Fireproofing/Extensions/FireproofingURLExtensions.swift +++ b/DuckDuckGo/Fireproofing/Extensions/FireproofingURLExtensions.swift @@ -52,7 +52,7 @@ extension URL { ] var canFireproof: Bool { - guard let host = self.host else { return false } + guard let host = self.host, self.navigationalScheme?.isHypertextScheme == true else { return false } return (host != Self.cookieDomain) } diff --git a/DuckDuckGo/HomePage/Model/DataImportStatusProviding.swift b/DuckDuckGo/HomePage/Model/DataImportStatusProviding.swift index 4d65b77c05..17efd1a64d 100644 --- a/DuckDuckGo/HomePage/Model/DataImportStatusProviding.swift +++ b/DuckDuckGo/HomePage/Model/DataImportStatusProviding.swift @@ -31,7 +31,7 @@ final class BookmarksAndPasswordsImportStatusProvider: DataImportStatusProviding let secureVault: (any AutofillSecureVault)? let bookmarkManager: BookmarkManager - init(secureVault: (any AutofillSecureVault)? = try? AutofillSecureVaultFactory.makeVault(errorReporter: SecureVaultErrorReporter.shared), + init(secureVault: (any AutofillSecureVault)? = try? AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter.shared), bookmarkManager: BookmarkManager = LocalBookmarkManager.shared) { self.secureVault = secureVault self.bookmarkManager = bookmarkManager diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index ef1f610c2e..2b07fcd86f 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -7474,6 +7474,66 @@ } } }, + "bookmark.all.tabs" : { + "comment" : "Menu item for bookmarking all the open tabs", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alle Tabs mit Lesezeichen versehen…" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Bookmark All Tabs…" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Añadir todas las pestañas a marcadores…" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ajouter tous les onglets aux signets…" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Crea un segnalibro con tutte le schede…" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alle tabbladen toevoegen als bladwijzer…" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dodaj wszystkie karty do zakładek…" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Marcar todos os separadores…" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Добавить все вкладки в закладки…" + } + } + } + }, "bookmark.dialog.add" : { "comment" : "Button to confim a bookmark creation", "extractionState" : "extracted_with_value", @@ -7601,55 +7661,55 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Diese Seite als Lesezeichen markieren" + "value" : "Diese Seite mit einem Lesezeichen versehen…" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "Bookmark This Page" + "value" : "Bookmark This Page…" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Marcar esta página" + "value" : "Marcar esta página…" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Ajouter cette page aux signets" + "value" : "Ajouter cette page aux signets…" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Crea un segnalibro per questa pagina" + "value" : "Crea un segnalibro per questa pagina…" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Bladwijzer toevoegen aan deze pagina" + "value" : "Bladwijzer toevoegen voor deze pagina…" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Dodaj tę stronę do zakładek" + "value" : "Dodaj tę stronę do zakładek…" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Marcar esta página" + "value" : "Marcar esta página…" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Сохранить в закладках" + "value" : "Сохранить страницу в закладках…" } } } @@ -8660,6 +8720,66 @@ } } }, + "bookmarks.dialog.action.addAllBookmarks" : { + "comment" : "CTA title for saving multiple Bookmarks at once", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lesezeichen speichern" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Save Bookmarks" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Guardar marcadores" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enregistrer les signets" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Salva segnalibri" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bladwijzers opslaan" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zapisz zakładki" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Guardar marcadores" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сохранение закладок" + } + } + } + }, "bookmarks.dialog.action.addBookmark" : { "comment" : "CTA title for adding a Bookmark", "extractionState" : "extracted_with_value", @@ -8780,6 +8900,246 @@ } } }, + "bookmarks.dialog.allTabs.message.add" : { + "comment" : "Bookmark creation for all open tabs dialog title", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Diese Lesezeichen werden in einem neuen Ordner gespeichert:" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "These bookmarks will be saved in a new folder:" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Estos marcadores se guardarán en una nueva carpeta:" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ces signets seront enregistrés dans un nouveau dossier:" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Questi segnalibri verranno salvati in una nuova cartella:" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deze bladwijzers worden opgeslagen in een nieuwe map:" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Te zakładki zostaną zapisane w nowym folderze:" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Estes marcadores serão guardados numa nova pasta:" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Эти закладки будут сохранены в новой папке:" + } + } + } + }, + "bookmarks.dialog.allTabs.title.add" : { + "comment" : "Title of dialog to bookmark all open tabs. E.g. 'Bookmark Open Tabs (42)'", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Offene Tabs mit Lesezeichen versehen (%d)" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Bookmark Open Tabs (%d)" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Añadir pestañas abiertas (%d)" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ajouter les onglets ouverts (%d) aux signets" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aggiungere ai segnalibri le schede aperte (%d)" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d geopende tabbladen toevoegen als bladwijzer" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dodaj otwarte karty (%d) do zakładek" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Marcar separadores abertos (%d)" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Добавить открытые вкладки (%d) в закладки" + } + } + } + }, + "bookmarks.dialog.field.folderName" : { + "comment" : "Folder name field label for Bookmarks folder", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ordnername" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Folder Name" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nombre de la carpeta" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nom du dossier" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nome della cartella" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Naam van de map" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nazwa folderu" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nome da pasta" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Название папки" + } + } + } + }, + "bookmarks.dialog.field.folderName.value" : { + "comment" : "The suggested name of the folder that will contain the bookmark tabs. Eg. 2024-02-12 - Tabs (42)", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ – Tabs (%2$d)" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@ - Tabs (%2$d)" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ - Pestañas (%2$d)" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ - Onglets (%2$d)" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ - Schede (%2$d)" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ - Tabbladen (%2$d)" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ - Karty (%2$d)" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ - Separadores (%2$d)" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ - Вкладки (%2$d)" + } + } + } + }, "bookmarks.dialog.field.location" : { "comment" : "Location field label for Bookmark folder", "extractionState" : "extracted_with_value", @@ -20060,6 +20420,7 @@ }, "Hide" : { "comment" : "Main Menu > View > Home Button > None item\n Preferences > Home Button > None item", + "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { @@ -45743,6 +46104,9 @@ } } } + }, + "Privacy Pro" : { + }, "private.search.explenation" : { "comment" : "feature explanation in settings", @@ -48009,6 +48373,7 @@ }, "Show left of the back button" : { "comment" : "Preferences > Home Button > left position item", + "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { @@ -48062,6 +48427,7 @@ }, "Show Left of the Back Button" : { "comment" : "Main Menu > View > Home Button > left position item", + "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { @@ -48327,6 +48693,7 @@ }, "Show right of the reload button" : { "comment" : "Preferences > Home Button > right position item", + "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { @@ -48380,6 +48747,7 @@ }, "Show Right of the Reload Button" : { "comment" : "Main Menu > View > Home Button > right position item", + "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { @@ -49868,6 +50236,66 @@ } } }, + "tab.empty.title" : { + "comment" : "Title for an empty tab without a title", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ohne Titel" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Untitled" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sin título" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sans titre" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ohne Titel" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Naamloos" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bez tytułu" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sem título" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Без названия" + } + } + } + }, "tab.error.title" : { "comment" : "Tab error title", "extractionState" : "extracted_with_value", diff --git a/DuckDuckGo/MainWindow/MainViewController.swift b/DuckDuckGo/MainWindow/MainViewController.swift index aab0bb6f42..118116eaad 100644 --- a/DuckDuckGo/MainWindow/MainViewController.swift +++ b/DuckDuckGo/MainWindow/MainViewController.swift @@ -303,13 +303,20 @@ final class MainViewController: NSViewController { } private func subscribeToTitleChange(of selectedTabViewModel: TabViewModel?) { - guard let window = self.view.window else { return } - selectedTabViewModel?.$title + guard let selectedTabViewModel else { return } + + // Only subscribe once the view is added to the window. + let windowPublisher = view.publisher(for: \.window).filter({ $0 != nil }).prefix(1).asVoid() + + windowPublisher + .combineLatest(selectedTabViewModel.$title) { $1 } .map { $0.truncated(length: MainMenu.Constants.maxTitleLength) } .receive(on: DispatchQueue.main) - .assign(to: \.title, onWeaklyHeld: window) + .sink { [weak self] title in + self?.view.window?.title = title + } .store(in: &tabViewModelCancellables) } diff --git a/DuckDuckGo/Menus/HistoryMenu.swift b/DuckDuckGo/Menus/HistoryMenu.swift index b99c7a867f..42fd97d990 100644 --- a/DuckDuckGo/Menus/HistoryMenu.swift +++ b/DuckDuckGo/Menus/HistoryMenu.swift @@ -53,7 +53,9 @@ final class HistoryMenu: NSMenu { reopenLastClosedMenuItem recentlyClosedMenuItem reopenAllWindowsFromLastSessionMenuItem - NSMenuItem.separator() + + clearAllHistorySeparator + clearAllHistoryMenuItem } reopenMenuItemKeyEquivalentManager.reopenLastClosedMenuItem = reopenLastClosedMenuItem diff --git a/DuckDuckGo/Menus/MainMenu.swift b/DuckDuckGo/Menus/MainMenu.swift index 132e25075d..66e026dc1d 100644 --- a/DuckDuckGo/Menus/MainMenu.swift +++ b/DuckDuckGo/Menus/MainMenu.swift @@ -291,6 +291,7 @@ import SubscriptionUI func buildBookmarksMenu() -> NSMenuItem { NSMenuItem(title: UserText.bookmarks).submenu(bookmarksMenu.buildItems { NSMenuItem(title: UserText.bookmarkThisPage, action: #selector(MainViewController.bookmarkThisPage), keyEquivalent: "d") + NSMenuItem(title: UserText.bookmarkAllTabs, action: #selector(MainViewController.bookmarkAllOpenTabs), keyEquivalent: [.command, .shift, "d"]) manageBookmarksMenuItem bookmarksMenuToggleBookmarksBarMenuItem NSMenuItem.separator() @@ -557,6 +558,14 @@ import SubscriptionUI let debugMenu = NSMenu(title: "Debug") { NSMenuItem(title: "Open Vanilla Browser", action: #selector(MainViewController.openVanillaBrowser)).withAccessibilityIdentifier("MainMenu.openVanillaBrowser") NSMenuItem.separator() + NSMenuItem(title: "Tab") { + NSMenuItem(title: "Append Tabs") { + NSMenuItem(title: "10 Tabs", action: #selector(MainViewController.addDebugTabs(_:)), representedObject: 10) + NSMenuItem(title: "50 Tabs", action: #selector(MainViewController.addDebugTabs(_:)), representedObject: 50) + NSMenuItem(title: "100 Tabs", action: #selector(MainViewController.addDebugTabs(_:)), representedObject: 100) + NSMenuItem(title: "150 Tabs", action: #selector(MainViewController.addDebugTabs(_:)), representedObject: 150) + } + } NSMenuItem(title: "Reset Data") { NSMenuItem(title: "Reset Default Browser Prompt", action: #selector(MainViewController.resetDefaultBrowserPrompt)) NSMenuItem(title: "Reset Default Grammar Checks", action: #selector(MainViewController.resetDefaultGrammarChecks)) diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index 5be8c2344c..eca7f8004d 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -215,7 +215,7 @@ extension AppDelegate { savePanel.beginSheetModal(for: window) { response in guard response == .OK, let selectedURL = savePanel.url else { return } - let vault = try? AutofillSecureVaultFactory.makeVault(errorReporter: SecureVaultErrorReporter.shared) + let vault = try? AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter.shared) let exporter = CSVLoginExporter(secureVault: vault!) do { try exporter.exportVaultLogins(to: selectedURL) @@ -507,6 +507,11 @@ extension MainViewController { .openBookmarkPopover(setFavorite: false, accessPoint: .init(sender: sender, default: .moreMenu)) } + @objc func bookmarkAllOpenTabs(_ sender: Any) { + let websitesInfo = tabCollectionViewModel.tabs.compactMap(WebsiteInfo.init) + BookmarksDialogViewFactory.makeBookmarkAllOpenTabsView(websitesInfo: websitesInfo).show() + } + @objc func favoriteThisPage(_ sender: Any) { guard let tabIndex = getActiveTabAndIndex()?.index else { return } if tabCollectionViewModel.selectedTabIndex != tabIndex { @@ -656,6 +661,14 @@ extension MainViewController { // MARK: - Debug + @objc func addDebugTabs(_ sender: AnyObject) { + let numberOfTabs = sender.representedObject as? Int ?? 1 + (1...numberOfTabs).forEach { _ in + let tab = Tab(content: .url(.duckDuckGo, credential: nil, source: .ui)) + tabCollectionViewModel.append(tab: tab) + } + } + @objc func resetDefaultBrowserPrompt(_ sender: Any?) { UserDefaultsWrapper.clear(.defaultBrowserDismissed) } @@ -670,7 +683,7 @@ extension MainViewController { } @objc func resetSecureVaultData(_ sender: Any?) { - let vault = try? AutofillSecureVaultFactory.makeVault(errorReporter: SecureVaultErrorReporter.shared) + let vault = try? AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter.shared) let accounts = (try? vault?.accounts()) ?? [] for accountID in accounts.compactMap(\.id) { @@ -925,8 +938,8 @@ extension MainViewController: NSMenuItemValidation { case #selector(findInPage), #selector(findInPageNext), #selector(findInPagePrevious): - return activeTabViewModel?.canReload == true // must have content loaded - && view.window?.isKeyWindow == true // disable in full screen + return activeTabViewModel?.canFindInPage == true // must have content loaded + && view.window?.isKeyWindow == true // disable in video full screen case #selector(findInPageDone): return getActiveTabAndIndex()?.tab.findInPage?.isActive == true @@ -944,6 +957,8 @@ extension MainViewController: NSMenuItemValidation { case #selector(MainViewController.bookmarkThisPage(_:)), #selector(MainViewController.favoriteThisPage(_:)): return activeTabViewModel?.canBeBookmarked == true + case #selector(MainViewController.bookmarkAllOpenTabs(_:)): + return tabCollectionViewModel.canBookmarkAllOpenTabs() case #selector(MainViewController.openBookmark(_:)), #selector(MainViewController.showManageBookmarks(_:)): return true @@ -1042,7 +1057,7 @@ extension AppDelegate: NSMenuItemValidation { } private var areTherePasswords: Bool { - let vault = try? AutofillSecureVaultFactory.makeVault(errorReporter: SecureVaultErrorReporter.shared) + let vault = try? AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter.shared) guard let vault else { return false } diff --git a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift index 566a7c413f..f277f679b6 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift @@ -275,11 +275,11 @@ final class AddressBarButtonsViewController: NSViewController { guard view.window?.isPopUpWindow == false else { return } bookmarkButton.setAccessibilityIdentifier("AddressBarButtonsViewController.bookmarkButton") let hasEmptyAddressBar = textFieldValue?.isEmpty ?? true - var showBookmarkButton: Bool { + var shouldShowBookmarkButton: Bool { guard let tabViewModel, tabViewModel.canBeBookmarked else { return false } var isUrlBookmarked = false - if let url = tabViewModel.tab.content.url, + if let url = tabViewModel.tab.content.userEditableUrl, bookmarkManager.isUrlBookmarked(url: url) { isUrlBookmarked = true } @@ -287,7 +287,7 @@ final class AddressBarButtonsViewController: NSViewController { return clearButton.isHidden && !hasEmptyAddressBar && (isMouseOverNavigationBar || bookmarkPopover?.isShown == true || isUrlBookmarked) } - bookmarkButton.isHidden = !showBookmarkButton + bookmarkButton.isShown = shouldShowBookmarkButton } func openBookmarkPopover(setFavorite: Bool, accessPoint: GeneralPixel.AccessPoint) { @@ -299,7 +299,7 @@ final class AddressBarButtonsViewController: NSViewController { let bookmarkPopover = bookmarkPopoverCreatingIfNeeded() if !bookmarkPopover.isShown { - bookmarkButton.isHidden = false + bookmarkButton.isShown = true bookmarkPopover.isNew = result.isNew bookmarkPopover.bookmark = bookmark bookmarkPopover.show(positionedBelow: bookmarkButton) @@ -319,7 +319,7 @@ final class AddressBarButtonsViewController: NSViewController { }() if query.permissions.contains(.camera) - || (query.permissions.contains(.microphone) && microphoneButton.isHidden && !cameraButton.isHidden) { + || (query.permissions.contains(.microphone) && microphoneButton.isHidden && cameraButton.isShown) { button = cameraButton } else { assert(query.permissions.count == 1) @@ -342,9 +342,7 @@ final class AddressBarButtonsViewController: NSViewController { return } } - guard !button.isHidden, - !permissionButtons.isHidden - else { return } + guard button.isShown, permissionButtons.isShown else { return } (popover.contentViewController as? PermissionAuthorizationViewController)?.query = query popover.show(relativeTo: button.bounds, of: button, preferredEdge: .maxY) @@ -389,7 +387,7 @@ final class AddressBarButtonsViewController: NSViewController { func updateButtons() { stopAnimationsAfterFocus() - clearButton.isHidden = !(isTextFieldEditorFirstResponder && !(textFieldValue?.isEmpty ?? true)) + clearButton.isShown = isTextFieldEditorFirstResponder && !textFieldValue.isEmpty updatePrivacyEntryPointButton() updateImageButton() @@ -413,7 +411,7 @@ final class AddressBarButtonsViewController: NSViewController { permissions.microphone = tabViewModel.usedPermissions.microphone } - let url = tabViewModel.tab.content.url ?? .empty + let url = tabViewModel.tab.content.urlForWebView ?? .empty let domain = url.isFileURL ? .localhost : (url.host ?? "") PermissionContextMenu(permissions: permissions.map { ($0, $1) }, domain: domain, delegate: self) @@ -432,7 +430,7 @@ final class AddressBarButtonsViewController: NSViewController { return } - let url = tabViewModel.tab.content.url ?? .empty + let url = tabViewModel.tab.content.urlForWebView ?? .empty let domain = url.isFileURL ? .localhost : (url.host ?? "") PermissionContextMenu(permissions: [(.microphone, state)], domain: domain, delegate: self) @@ -451,7 +449,7 @@ final class AddressBarButtonsViewController: NSViewController { return } - let url = tabViewModel.tab.content.url ?? .empty + let url = tabViewModel.tab.content.urlForWebView ?? .empty let domain = url.isFileURL ? .localhost : (url.host ?? "") PermissionContextMenu(permissions: [(.geolocation, state)], domain: domain, delegate: self) @@ -475,7 +473,7 @@ final class AddressBarButtonsViewController: NSViewController { $0.append( (.popups, .requested($1)) ) } } else { - let url = tabViewModel.tab.content.url ?? .empty + let url = tabViewModel.tab.content.urlForWebView ?? .empty domain = url.isFileURL ? .localhost : (url.host ?? "") permissions = [(.popups, state)] } @@ -499,7 +497,7 @@ final class AddressBarButtonsViewController: NSViewController { } permissions = [(permissionType, state)] - let url = tabViewModel.tab.content.url ?? .empty + let url = tabViewModel.tab.content.urlForWebView ?? .empty let domain = url.isFileURL ? .localhost : (url.host ?? "") PermissionContextMenu(permissions: permissions, domain: domain, delegate: self) @@ -690,15 +688,15 @@ final class AddressBarButtonsViewController: NSViewController { } private func updatePermissionButtons() { - permissionButtons.isHidden = isTextFieldEditorFirstResponder - || isAnyTrackerAnimationPlaying - || (tabViewModel?.isShowingErrorPage ?? true) + guard let tabViewModel else { return } + + permissionButtons.isShown = !isTextFieldEditorFirstResponder + && !isAnyTrackerAnimationPlaying + && !tabViewModel.isShowingErrorPage defer { showOrHidePermissionPopoverIfNeeded() } - guard let tabViewModel else { return } - geolocationButton.buttonState = tabViewModel.usedPermissions.geolocation let (camera, microphone) = PermissionState?.combineCamera(tabViewModel.usedPermissions.camera, @@ -733,7 +731,7 @@ final class AddressBarButtonsViewController: NSViewController { } private func updateBookmarkButtonImage(isUrlBookmarked: Bool = false) { - if let url = tabViewModel?.tab.content.url, + if let url = tabViewModel?.tab.content.userEditableUrl, isUrlBookmarked || bookmarkManager.isUrlBookmarked(url: url) { bookmarkButton.image = .bookmarkFilled @@ -770,22 +768,25 @@ final class AddressBarButtonsViewController: NSViewController { private func updatePrivacyEntryPointButton() { guard let tabViewModel else { return } - let urlScheme = tabViewModel.tab.content.url?.scheme - let isHypertextUrl = urlScheme == "http" || urlScheme == "https" + let url = tabViewModel.tab.content.userEditableUrl + let isNewTabOrOnboarding = [.newtab, .onboarding].contains(tabViewModel.tab.content) + let isHypertextUrl = url?.navigationalScheme?.isHypertextScheme == true && url?.isDuckPlayer == false let isEditingMode = controllerMode?.isEditing ?? false let isTextFieldValueText = textFieldValue?.isText ?? false - let isLocalUrl = tabViewModel.tab.content.url?.isLocalURL ?? false + let isLocalUrl = url?.isLocalURL ?? false // Privacy entry point button - privacyEntryPointButton.isHidden = isEditingMode - || isTextFieldEditorFirstResponder - || !isHypertextUrl - || tabViewModel.isShowingErrorPage - || isTextFieldValueText - || isLocalUrl - imageButtonWrapper.isHidden = view.window?.isPopUpWindow == true - || !privacyEntryPointButton.isHidden - || isAnyTrackerAnimationPlaying + privacyEntryPointButton.isShown = !isEditingMode + && !isTextFieldEditorFirstResponder + && isHypertextUrl + && !tabViewModel.isShowingErrorPage + && !isTextFieldValueText + && !isLocalUrl + + imageButtonWrapper.isShown = view.window?.isPopUpWindow != true + && (isHypertextUrl || isTextFieldEditorFirstResponder || isEditingMode || isNewTabOrOnboarding) + && privacyEntryPointButton.isHidden + && !isAnyTrackerAnimationPlaying } private func updatePrivacyEntryPointIcon() { @@ -796,7 +797,7 @@ final class AddressBarButtonsViewController: NSViewController { guard !isAnyShieldAnimationPlaying else { return } switch tabViewModel.tab.content { - case .url(let url, _, _): + case .url(let url, _, _), .identityTheftRestoration(let url), .subscription(let url): guard let host = url.host else { break } let isNotSecure = url.scheme == URL.NavigationalScheme.http.rawValue @@ -824,8 +825,7 @@ final class AddressBarButtonsViewController: NSViewController { let trackerAnimationImageProvider = TrackerAnimationImageProvider() private func animateTrackers() { - guard !privacyEntryPointButton.isHidden, - let tabViewModel else { return } + guard privacyEntryPointButton.isShown, let tabViewModel else { return } switch tabViewModel.tab.content { case .url(let url, _, _): @@ -835,7 +835,7 @@ final class AddressBarButtonsViewController: NSViewController { } var animationView: LottieAnimationView - if url.scheme == "http" { + if url.navigationalScheme == .http { animationView = shieldDotAnimationView } else { animationView = shieldAnimationView @@ -878,7 +878,7 @@ final class AddressBarButtonsViewController: NSViewController { shieldAnimations: Bool = true, badgeAnimations: Bool = true) { func stopAnimation(_ animationView: LottieAnimationView) { - if animationView.isAnimationPlaying || !animationView.isHidden { + if animationView.isAnimationPlaying || animationView.isShown { animationView.isHidden = true animationView.stop() } @@ -922,7 +922,7 @@ final class AddressBarButtonsViewController: NSViewController { private func bookmarkForCurrentUrl(setFavorite: Bool, accessPoint: GeneralPixel.AccessPoint) -> (bookmark: Bookmark?, isNew: Bool) { guard let tabViewModel, - let url = tabViewModel.tab.content.url else { + let url = tabViewModel.tab.content.userEditableUrl else { assertionFailure("No URL for bookmarking") return (nil, false) } diff --git a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift index 21bb3b0bfa..c3494d313d 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift @@ -233,7 +233,7 @@ final class AddressBarTextField: NSTextField { case .suggestion(let suggestionViewModel): let suggestion = suggestionViewModel.suggestion switch suggestion { - case .website, .bookmark, .historyEntry: + case .website, .bookmark, .historyEntry, .internalPage: restoreValue(Value(stringValue: suggestionViewModel.autocompletionString, userTyped: true)) case .phrase(phrase: let phase): restoreValue(Value.text(phase, userTyped: false)) @@ -259,7 +259,7 @@ final class AddressBarTextField: NSTextField { switch self.value { case .suggestion(let suggestionViewModel): switch suggestionViewModel.suggestion { - case .phrase, .website, .bookmark, .historyEntry: return false + case .phrase, .website, .bookmark, .historyEntry, .internalPage: return false case .unknown: return true } case .text(_, userTyped: true), .url(_, _, userTyped: true): return false @@ -275,7 +275,7 @@ final class AddressBarTextField: NSTextField { guard let selectedTabViewModel = selectedTabViewModel ?? tabCollectionViewModel.selectedTabViewModel else { return } let addressBarString = addressBarString ?? selectedTabViewModel.addressBarString - let isSearch = selectedTabViewModel.tab.content.url?.isDuckDuckGoSearch ?? false + let isSearch = selectedTabViewModel.tab.content.userEditableUrl?.isDuckDuckGoSearch ?? false self.value = Value(stringValue: addressBarString, userTyped: false, isSearch: isSearch) clearUndoManager() } @@ -418,7 +418,8 @@ final class AddressBarTextField: NSTextField { switch suggestion { case .bookmark(title: _, url: let url, isFavorite: _, allowedInTopHits: _), .historyEntry(title: _, url: let url, allowedInTopHits: _), - .website(url: let url): + .website(url: let url), + .internalPage(title: _, url: let url): finalUrl = url userEnteredValue = url.absoluteString case .phrase(phrase: let phrase), @@ -802,7 +803,8 @@ extension AddressBarTextField { self = Suffix.visit(host: host) case .bookmark(title: _, url: let url, isFavorite: _, allowedInTopHits: _), - .historyEntry(title: _, url: let url, allowedInTopHits: _): + .historyEntry(title: _, url: let url, allowedInTopHits: _), + .internalPage(title: _, url: let url): if let title = suggestionViewModel.title, !title.isEmpty, suggestionViewModel.autocompletionString != title { @@ -856,6 +858,11 @@ extension AddressBarTextField { } } +extension AddressBarTextField.Value? { + var isEmpty: Bool { + self?.isEmpty ?? true + } +} // MARK: - NSTextFieldDelegate extension AddressBarTextField: NSTextFieldDelegate { diff --git a/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift index 43ff508e66..7976bb9dc6 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift @@ -225,9 +225,9 @@ final class AddressBarViewController: NSViewController { passiveTextField.stringValue = "" return } - tabViewModel.$passiveAddressBarString + tabViewModel.$passiveAddressBarAttributedString .receive(on: DispatchQueue.main) - .assign(to: \.stringValue, onWeaklyHeld: passiveTextField) + .assign(to: \.attributedStringValue, onWeaklyHeld: passiveTextField) .store(in: &tabViewModelCancellables) } @@ -239,9 +239,9 @@ final class AddressBarViewController: NSViewController { func shouldShowLoadingIndicator(for tabViewModel: TabViewModel, isLoading: Bool, error: Error?) -> Bool { if isLoading, - let url = tabViewModel.tab.content.url, - [.http, .https].contains(url.navigationalScheme), - url.isDuckDuckGoSearch == false, + let url = tabViewModel.tab.content.urlForWebView, + url.navigationalScheme?.isHypertextScheme == true, + !url.isDuckDuckGoSearch, !url.isDuckPlayer, error == nil { return true } else { @@ -259,7 +259,7 @@ final class AddressBarViewController: NSViewController { .sink { [weak self] value in guard tabViewModel.isLoading, let progressIndicator = self?.progressIndicator, - progressIndicator.isShown + progressIndicator.isProgressShown else { return } progressIndicator.increaseProgress(to: value) @@ -274,7 +274,7 @@ final class AddressBarViewController: NSViewController { if shouldShowLoadingIndicator(for: tabViewModel, isLoading: isLoading, error: error) { progressIndicator.show(progress: tabViewModel.progress, startTime: tabViewModel.loadingStartTime) - } else if progressIndicator.isShown { + } else if progressIndicator.isProgressShown { progressIndicator.finishAndHide() } } @@ -364,7 +364,7 @@ final class AddressBarViewController: NSViewController { case .suggestion(let suggestionViewModel): switch suggestionViewModel.suggestion { case .phrase, .unknown: self.mode = .editing(isUrl: false) - case .website, .bookmark, .historyEntry: self.mode = .editing(isUrl: true) + case .website, .bookmark, .historyEntry, .internalPage: self.mode = .editing(isUrl: true) } } } diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift index 2638bc96bb..4d2dbb909a 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift @@ -28,6 +28,7 @@ import Subscription protocol OptionsButtonMenuDelegate: AnyObject { func optionsButtonMenuRequestedBookmarkThisPage(_ sender: NSMenuItem) + func optionsButtonMenuRequestedBookmarkAllOpenTabs(_ sender: NSMenuItem) func optionsButtonMenuRequestedBookmarkPopover(_ menu: NSMenu) func optionsButtonMenuRequestedBookmarkManagementInterface(_ menu: NSMenu) func optionsButtonMenuRequestedBookmarkImportInterface(_ menu: NSMenu) @@ -171,6 +172,10 @@ final class MoreOptionsMenu: NSMenu { actionDelegate?.optionsButtonMenuRequestedBookmarkThisPage(sender) } + @objc func bookmarkAllOpenTabs(_ sender: NSMenuItem) { + actionDelegate?.optionsButtonMenuRequestedBookmarkAllOpenTabs(sender) + } + @objc func openBookmarks(_ sender: NSMenuItem) { actionDelegate?.optionsButtonMenuRequestedBookmarkPopover(self) } @@ -402,10 +407,11 @@ final class MoreOptionsMenu: NSMenu { } private func addPageItems() { - guard let url = tabCollectionViewModel.selectedTabViewModel?.tab.content.url else { return } + guard let tabViewModel = tabCollectionViewModel.selectedTabViewModel, + let url = tabViewModel.tab.content.userEditableUrl else { return } + let oldItemsCount = items.count if url.canFireproof, let host = url.host { - let isFireproof = FireproofDomains.shared.isFireproof(fireproofDomain: host) let title = isFireproof ? UserText.removeFireproofing : UserText.fireproofSite let image: NSImage = isFireproof ? .burn : .fireproof @@ -413,25 +419,31 @@ final class MoreOptionsMenu: NSMenu { addItem(withTitle: title, action: #selector(toggleFireproofing(_:)), keyEquivalent: "") .targetting(self) .withImage(image) - } - addItem(withTitle: UserText.findInPageMenuItem, action: #selector(findInPage(_:)), keyEquivalent: "f") - .targetting(self) - .withImage(.findSearch) - .withAccessibilityIdentifier("MoreOptionsMenu.findInPage") - - addItem(withTitle: UserText.shareMenuItem, action: nil, keyEquivalent: "") - .targetting(self) - .withImage(.share) - .withSubmenu(sharingMenu) + if tabViewModel.canFindInPage { + addItem(withTitle: UserText.findInPageMenuItem, action: #selector(findInPage(_:)), keyEquivalent: "f") + .targetting(self) + .withImage(.findSearch) + .withAccessibilityIdentifier("MoreOptionsMenu.findInPage") + } - addItem(withTitle: UserText.printMenuItem, action: #selector(doPrint(_:)), keyEquivalent: "") - .targetting(self) - .withImage(.print) + if tabViewModel.canReload { + addItem(withTitle: UserText.shareMenuItem, action: nil, keyEquivalent: "") + .targetting(self) + .withImage(.share) + .withSubmenu(sharingMenu) + } - addItem(NSMenuItem.separator()) + if tabViewModel.canPrint { + addItem(withTitle: UserText.printMenuItem, action: #selector(doPrint(_:)), keyEquivalent: "") + .targetting(self) + .withImage(.print) + } + if items.count > oldItemsCount { + addItem(NSMenuItem.separator()) + } } private func makeNetworkProtectionItem() -> NSMenuItem { @@ -627,6 +639,12 @@ final class BookmarksSubMenu: NSMenu { bookmarkPageItem.isEnabled = tabCollectionViewModel.selectedTabViewModel?.canBeBookmarked == true + let bookmarkAllTabsItem = addItem(withTitle: UserText.bookmarkAllTabs, action: #selector(MoreOptionsMenu.bookmarkAllOpenTabs(_:)), keyEquivalent: "d") + .withModifierMask([.command, .shift]) + .targetting(target) + + bookmarkAllTabsItem.isEnabled = tabCollectionViewModel.canBookmarkAllOpenTabs() + addItem(NSMenuItem.separator()) addItem(withTitle: UserText.bookmarksShowToolbarPanel, action: #selector(MoreOptionsMenu.openBookmarks(_:)), keyEquivalent: "") diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index d27b62fd5d..ee17298593 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -644,15 +644,20 @@ final class NavigationBarViewController: NSViewController { private func subscribeToDownloads() { downloadListCoordinator.updates + .filter { update in + // filter download completion events only + if case .updated(let oldValue) = update.kind, + oldValue.progress != nil && update.item.progress == nil { + return true + } else { + return false + } + } .throttle(for: 1.0, scheduler: DispatchQueue.main, latest: true) .sink { [weak self] update in guard let self else { return } - if case .updated(let oldValue) = update.kind, - DownloadsPreferences.shared.shouldOpenPopupOnCompletion, - update.item.destinationURL != nil, - update.item.tempURL == nil, - oldValue.tempURL != nil, // download finished + if DownloadsPreferences.shared.shouldOpenPopupOnCompletion, !update.item.isBurner, WindowControllersManager.shared.lastKeyMainWindowController?.window === downloadsButton.window { @@ -661,11 +666,11 @@ final class NavigationBarViewController: NSViewController { downloadsDelegate: self) } else if update.item.isBurner { invalidateDownloadButtonHidingTimer() - updateDownloadsButton(updatingFromPinnedViewsNotification: false) } updateDownloadsButton() } .store(in: &downloadsCancellables) + downloadListCoordinator.progress.publisher(for: \.totalUnitCount) .combineLatest(downloadListCoordinator.progress.publisher(for: \.completedUnitCount)) .throttle(for: 0.2, scheduler: DispatchQueue.main, latest: true) @@ -693,7 +698,7 @@ final class NavigationBarViewController: NSViewController { passwordManagementButton.menu = menu passwordManagementButton.toolTip = UserText.autofillShortcutTooltip - let url = tabCollectionViewModel.selectedTabViewModel?.tab.content.url + let url = tabCollectionViewModel.selectedTabViewModel?.tab.content.userEditableUrl passwordManagementButton.image = .passwordManagement @@ -1011,6 +1016,11 @@ extension NavigationBarViewController: OptionsButtonMenuDelegate { .openBookmarkPopover(setFavorite: false, accessPoint: .init(sender: sender, default: .moreMenu)) } + func optionsButtonMenuRequestedBookmarkAllOpenTabs(_ sender: NSMenuItem) { + let websitesInfo = tabCollectionViewModel.tabs.compactMap(WebsiteInfo.init) + BookmarksDialogViewFactory.makeBookmarkAllOpenTabsView(websitesInfo: websitesInfo).show() + } + func optionsButtonMenuRequestedBookmarkPopover(_ menu: NSMenu) { popovers.showBookmarkListPopover(usingView: bookmarkListButton, withDelegate: self, diff --git a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift index 2446ab0ac5..0a29c23807 100644 --- a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift +++ b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift @@ -34,10 +34,18 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { case networkProtectionTunnelStartSuccess case networkProtectionTunnelStartFailure(_ error: Error) + case networkProtectionTunnelStopAttempt + case networkProtectionTunnelStopSuccess + case networkProtectionTunnelStopFailure(_ error: Error) + case networkProtectionTunnelUpdateAttempt case networkProtectionTunnelUpdateSuccess case networkProtectionTunnelUpdateFailure(_ error: Error) + case networkProtectionTunnelWakeAttempt + case networkProtectionTunnelWakeSuccess + case networkProtectionTunnelWakeFailure(_ error: Error) + case networkProtectionEnableAttemptConnecting case networkProtectionEnableAttemptSuccess case networkProtectionEnableAttemptFailure @@ -119,6 +127,15 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { case .networkProtectionTunnelStartFailure: return "netp_tunnel_start_failure" + case .networkProtectionTunnelStopAttempt: + return "netp_tunnel_stop_attempt" + + case .networkProtectionTunnelStopSuccess: + return "netp_tunnel_stop_success" + + case .networkProtectionTunnelStopFailure: + return "netp_tunnel_stop_failure" + case .networkProtectionTunnelUpdateAttempt: return "netp_tunnel_update_attempt" @@ -128,6 +145,15 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { case .networkProtectionTunnelUpdateFailure: return "netp_tunnel_update_failure" + case .networkProtectionTunnelWakeAttempt: + return "netp_tunnel_wake_attempt" + + case .networkProtectionTunnelWakeSuccess: + return "netp_tunnel_wake_success" + + case .networkProtectionTunnelWakeFailure: + return "netp_tunnel_wake_failure" + case .networkProtectionEnableAttemptConnecting: return "netp_ev_enable_attempt" @@ -300,9 +326,15 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { .networkProtectionTunnelStartAttempt, .networkProtectionTunnelStartSuccess, .networkProtectionTunnelStartFailure, + .networkProtectionTunnelStopAttempt, + .networkProtectionTunnelStopSuccess, + .networkProtectionTunnelStopFailure, .networkProtectionTunnelUpdateAttempt, .networkProtectionTunnelUpdateSuccess, .networkProtectionTunnelUpdateFailure, + .networkProtectionTunnelWakeAttempt, + .networkProtectionTunnelWakeSuccess, + .networkProtectionTunnelWakeFailure, .networkProtectionEnableAttemptConnecting, .networkProtectionEnableAttemptSuccess, .networkProtectionEnableAttemptFailure, @@ -343,7 +375,9 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { return error case .networkProtectionControllerStartFailure(let error), .networkProtectionTunnelStartFailure(let error), + .networkProtectionTunnelStopFailure(let error), .networkProtectionTunnelUpdateFailure(let error), + .networkProtectionTunnelWakeFailure(let error), .networkProtectionClientFailedToParseRedeemResponse(let error), .networkProtectionWireguardErrorCannotSetNetworkSettings(let error), .networkProtectionRekeyFailure(let error), @@ -356,8 +390,12 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { .networkProtectionControllerStartSuccess, .networkProtectionTunnelStartAttempt, .networkProtectionTunnelStartSuccess, + .networkProtectionTunnelStopAttempt, + .networkProtectionTunnelStopSuccess, .networkProtectionTunnelUpdateAttempt, .networkProtectionTunnelUpdateSuccess, + .networkProtectionTunnelWakeAttempt, + .networkProtectionTunnelWakeSuccess, .networkProtectionEnableAttemptConnecting, .networkProtectionEnableAttemptSuccess, .networkProtectionEnableAttemptFailure, diff --git a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/VPNOperationErrorRecorder.swift b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/VPNOperationErrorRecorder.swift new file mode 100644 index 0000000000..7fa58ab46d --- /dev/null +++ b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/VPNOperationErrorRecorder.swift @@ -0,0 +1,189 @@ +// +// VPNOperationErrorRecorder.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 NetworkProtectionIPC + +@objc +final class ErrorInformation: NSObject, Codable { + let domain: String + let code: Int + + init(_ error: Error) { + let nsError = error as NSError + + domain = nsError.domain + code = nsError.code + } +} + +/// This class provides information about VPN operation errors. +/// +/// To be used in combination with ``VPNOperationErrorRecorder`` +/// +final class VPNOperationErrorHistory { + + private let ipcClient: TunnelControllerIPCClient + private let defaults: UserDefaults + + init(ipcClient: TunnelControllerIPCClient, + defaults: UserDefaults = .netP) { + + self.ipcClient = ipcClient + self.defaults = defaults + } + + /// The earliest error is the one that best represents the latest failure + /// + var lastStartError: ErrorInformation? { + lastIPCStartError ?? lastControllerStartError + } + + var lastStartErrorDescription: String { + lastStartError.map { errorInformation in + "Error domain=\(errorInformation.domain) code=\(errorInformation.code)" + } ?? "none" + } + + private var lastIPCStartError: ErrorInformation? { + defaults.vpnIPCStartError + } + + private var lastControllerStartError: ErrorInformation? { + defaults.controllerStartError + } + + var lastTunnelError: ErrorInformation? { + get async { + await withCheckedContinuation { (continuation: CheckedContinuation) in + ipcClient.fetchLastError { error in + if let error { + continuation.resume(returning: ErrorInformation(error)) + } else { + continuation.resume(returning: nil) + } + } + } + } + } + + var lastTunnelErrorDescription: String { + get async { + await lastTunnelError.map { errorInformation in + "Error domain=\(errorInformation.domain) code=\(errorInformation.code)" + } ?? "none" + } + } +} + +/// This class records information about recent errors during VPN operation. +/// +/// To be used in combination with ``VPNOperationErrorHistory`` +/// +final class VPNOperationErrorRecorder { + + private let defaults: UserDefaults + + init(defaults: UserDefaults = .netP) { + self.defaults = defaults + } + + // IPC Errors + + func beginRecordingIPCStart() { + defaults.vpnIPCStartError = nil + } + + func recordIPCStartFailure(_ error: Error) { + defaults.vpnIPCStartError = ErrorInformation(error) + } + + // VPN Controller Errors + + func beginRecordingControllerStart() { + defaults.controllerStartError = nil + + // This needs a special note because it may be non-obvious. The thing is users + // can start the VPN directly from the menu app, and in this case we want IPC + // errors to be cleared because they have priority in the reporting. Additionally + // if the controller is starting the VPN we can safely assume there was no IPC + // error in the current start attempt, so resetting ipc start errors should be fine, + // regardless. + defaults.vpnIPCStartError = nil + } + + func recordControllerStartFailure(_ error: Error) { + defaults.controllerStartError = ErrorInformation(error) + } +} + +fileprivate extension UserDefaults { + private var vpnIPCStartErrorKey: String { + "vpnIPCStartError" + } + + @objc + dynamic var vpnIPCStartError: ErrorInformation? { + get { + guard let payload = data(forKey: vpnIPCStartErrorKey) else { + return nil + } + + return try? JSONDecoder().decode(ErrorInformation.self, from: payload) + } + + set { + guard let newValue, + let payload = try? JSONEncoder().encode(newValue) else { + + removeObject(forKey: vpnIPCStartErrorKey) + return + } + + set(payload, forKey: vpnIPCStartErrorKey) + } + } +} + +fileprivate extension UserDefaults { + private var controllerStartErrorKey: String { + "controllerStartError" + } + + @objc + dynamic var controllerStartError: ErrorInformation? { + get { + guard let payload = data(forKey: controllerStartErrorKey) else { + return nil + } + + return try? JSONDecoder().decode(ErrorInformation.self, from: payload) + } + + set { + guard let newValue, + let payload = try? JSONEncoder().encode(newValue) else { + + removeObject(forKey: controllerStartErrorKey) + return + } + + set(payload, forKey: controllerStartErrorKey) + } + } +} diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift index c2565dc405..b1bebcadbb 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift @@ -480,6 +480,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr /// Starts the VPN connection /// func start() async { + VPNOperationErrorRecorder().beginRecordingControllerStart() PixelKit.fire(NetworkProtectionPixelEvent.networkProtectionControllerStartAttempt, frequency: .dailyAndCount) controllerErrorStore.lastErrorMessage = nil @@ -525,6 +526,8 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr frequency: .dailyAndCount) } } catch { + VPNOperationErrorRecorder().recordControllerStartFailure(error) + PixelKit.fire( NetworkProtectionPixelEvent.networkProtectionControllerStartFailure(error), frequency: .dailyAndCount, includeAppVersionParameter: true ) diff --git a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift index 7e05352066..a39fcb6de4 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift @@ -51,16 +51,19 @@ final class NetworkProtectionIPCTunnelController { private let loginItemsManager: LoginItemsManaging private let ipcClient: NetworkProtectionIPCClient private let pixelKit: PixelFiring? + private let errorRecorder: VPNOperationErrorRecorder init(featureVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(), loginItemsManager: LoginItemsManaging = LoginItemsManager(), ipcClient: NetworkProtectionIPCClient, - pixelKit: PixelFiring? = PixelKit.shared) { + pixelKit: PixelFiring? = PixelKit.shared, + errorRecorder: VPNOperationErrorRecorder = VPNOperationErrorRecorder()) { self.featureVisibility = featureVisibility self.loginItemsManager = loginItemsManager self.ipcClient = ipcClient self.pixelKit = pixelKit + self.errorRecorder = errorRecorder } // MARK: - Login Items Manager @@ -84,9 +87,11 @@ extension NetworkProtectionIPCTunnelController: TunnelController { @MainActor func start() async { + errorRecorder.beginRecordingIPCStart() pixelKit?.fire(StartAttempt.begin) func handleFailure(_ error: Error) { + errorRecorder.recordIPCStartFailure(error) log(error) pixelKit?.fire(StartAttempt.failure(error), frequency: .dailyAndCount) } @@ -156,7 +161,7 @@ extension NetworkProtectionIPCTunnelController: TunnelController { } } -// MARK: - Start Attempts +// MARK: - Start Attempts: Pixels extension NetworkProtectionIPCTunnelController { diff --git a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift index 48add2199f..ecdc324254 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift @@ -17,6 +17,7 @@ // import Combine +import Common import Foundation import Subscription import NetworkProtection diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift index 842ff33ad8..3c71dc41a7 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift @@ -233,6 +233,24 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { frequency: .dailyAndCount, includeAppVersionParameter: true) } + case .tunnelStopAttempt(let step): + switch step { + case .begin: + PixelKit.fire( + NetworkProtectionPixelEvent.networkProtectionTunnelStopAttempt, + frequency: .standard, + includeAppVersionParameter: true) + case .failure(let error): + PixelKit.fire( + NetworkProtectionPixelEvent.networkProtectionTunnelStopFailure(error), + frequency: .dailyAndCount, + includeAppVersionParameter: true) + case .success: + PixelKit.fire( + NetworkProtectionPixelEvent.networkProtectionTunnelStopSuccess, + frequency: .dailyAndCount, + includeAppVersionParameter: true) + } case .tunnelUpdateAttempt(let step): switch step { case .begin: @@ -251,6 +269,24 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { frequency: .dailyAndCount, includeAppVersionParameter: true) } + case .tunnelWakeAttempt(let step): + switch step { + case .begin: + PixelKit.fire( + NetworkProtectionPixelEvent.networkProtectionTunnelWakeAttempt, + frequency: .dailyAndCount, + includeAppVersionParameter: true) + case .failure(let error): + PixelKit.fire( + NetworkProtectionPixelEvent.networkProtectionTunnelWakeFailure(error), + frequency: .dailyAndCount, + includeAppVersionParameter: true) + case .success: + PixelKit.fire( + NetworkProtectionPixelEvent.networkProtectionTunnelWakeSuccess, + frequency: .dailyAndCount, + includeAppVersionParameter: true) + } } } @@ -272,6 +308,9 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { #else let defaults = UserDefaults.netP #endif + + NetworkProtectionLastVersionRunStore(userDefaults: defaults).lastExtensionVersionRun = AppVersion.shared.versionAndBuildNumber + let settings = VPNSettings(defaults: defaults) let tunnelHealthStore = NetworkProtectionTunnelHealthStore(notificationCenter: notificationCenter) let controllerErrorStore = NetworkProtectionTunnelErrorStore(notificationCenter: notificationCenter) @@ -421,30 +460,6 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { try? loadDefaultPixelHeaders(from: options) } - // MARK: - Start/Stop Tunnel - - override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { - super.stopTunnel(with: reason) { - Task { - completionHandler() - - // From what I'm seeing in my tests the next call to start the tunnel is MUCH - // less likely to fail if we force this extension to exit when the tunnel is killed. - // - // Ref: https://app.asana.com/0/72649045549333/1204668639086684/f - // - exit(EXIT_SUCCESS) - } - } - } - - override func cancelTunnelWithError(_ error: Error?) { - Task { - super.cancelTunnelWithError(error) - exit(EXIT_SUCCESS) - } - } - // MARK: - Pixels private func setupPixels(defaultHeaders: [String: String] = [:]) { diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacTransparentProxyProvider.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacTransparentProxyProvider.swift index 5d9b0c0fa4..1c4993210e 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacTransparentProxyProvider.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacTransparentProxyProvider.swift @@ -27,7 +27,7 @@ import PixelKit final class MacTransparentProxyProvider: TransparentProxyProvider { - static var vpnProxyLogger = Logger(subsystem: OSLog.subsystem, category: "VPN Proxy") + static var vpnProxyLogger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "DuckDuckGo", category: "VPN Proxy") private var cancellables = Set() diff --git a/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift b/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift index 3e067f1187..c9a0c87f8b 100644 --- a/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift +++ b/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift @@ -224,7 +224,9 @@ struct PinnedTabInnerView: View { .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) { + } else if let domain = model.content.userEditableUrl?.host, + let eTLDplus1 = ContentBlocking.shared.tld.eTLDplus1(domain), + let firstLetter = eTLDplus1.capitalized.first.flatMap(String.init) { ZStack { Rectangle() .foregroundColor(.forString(eTLDplus1)) diff --git a/DuckDuckGo/Preferences/Model/PreferencesSection.swift b/DuckDuckGo/Preferences/Model/PreferencesSection.swift index bff6930013..777b1a54dd 100644 --- a/DuckDuckGo/Preferences/Model/PreferencesSection.swift +++ b/DuckDuckGo/Preferences/Model/PreferencesSection.swift @@ -98,7 +98,7 @@ enum PreferencesSectionIdentifier: Hashable, CaseIterable { } -enum PreferencePaneIdentifier: String, Equatable, Hashable, Identifiable { +enum PreferencePaneIdentifier: String, Equatable, Hashable, Identifiable, CaseIterable { case defaultBrowser case privateSearch case webTrackingProtection diff --git a/DuckDuckGo/PrivacyDashboard/PrivacyDashboardPermissionHandler.swift b/DuckDuckGo/PrivacyDashboard/PrivacyDashboardPermissionHandler.swift index f1734c4a6f..fe991eade4 100644 --- a/DuckDuckGo/PrivacyDashboard/PrivacyDashboardPermissionHandler.swift +++ b/DuckDuckGo/PrivacyDashboard/PrivacyDashboardPermissionHandler.swift @@ -60,7 +60,7 @@ final class PrivacyDashboardPermissionHandler { assertionFailure("PrivacyDashboardViewController: tabViewModel not set") return } - guard let domain = tabViewModel?.tab.content.url?.host else { + guard let domain = tabViewModel?.tab.content.urlForWebView?.host else { onPermissionChange?([]) return } diff --git a/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardViewController.swift b/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardViewController.swift index 41e1300195..befe61b87a 100644 --- a/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardViewController.swift +++ b/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardViewController.swift @@ -325,7 +325,7 @@ extension PrivacyDashboardViewController { // ⚠️ To limit privacy risk, site URL is trimmed to not include query and fragment guard let currentTab = tabViewModel?.tab, - let currentURL = currentTab.content.url?.trimmingQueryItemsAndFragment() else { + let currentURL = currentTab.content.urlForWebView?.trimmingQueryItemsAndFragment() else { throw BrokenSiteReportError.failedToFetchTheCurrentURL } let blockedTrackerDomains = currentTab.privacyInfo?.trackerInfo.trackersBlocked.compactMap { $0.domain } ?? [] @@ -335,7 +335,7 @@ extension PrivacyDashboardViewController { // current domain's protection status let configuration = ContentBlocking.shared.privacyConfigurationManager.privacyConfig - let protectionsState = configuration.isFeature(.contentBlocking, enabledForDomain: currentTab.content.url?.host) + let protectionsState = configuration.isFeature(.contentBlocking, enabledForDomain: currentTab.content.urlForWebView?.host) let webVitals = await calculateWebVitals(performanceMetrics: currentTab.brokenSiteInfo?.performanceMetrics, privacyConfig: configuration) diff --git a/DuckDuckGo/RecentlyClosed/Model/RecentlyClosedCacheItem.swift b/DuckDuckGo/RecentlyClosed/Model/RecentlyClosedCacheItem.swift index 3314b3da2a..2d313b152c 100644 --- a/DuckDuckGo/RecentlyClosed/Model/RecentlyClosedCacheItem.swift +++ b/DuckDuckGo/RecentlyClosed/Model/RecentlyClosedCacheItem.swift @@ -35,7 +35,7 @@ extension RecentlyClosedTab: RecentlyClosedCacheItemBurning { } private func contentContainsDomains(_ baseDomains: Set, tld: TLD) -> Bool { - if let host = tabContent.url?.host, let baseDomain = tld.eTLDplus1(host), baseDomains.contains(baseDomain) { + if let host = tabContent.urlForWebView?.host, let baseDomain = tld.eTLDplus1(host), baseDomains.contains(baseDomain) { return true } else { return false diff --git a/DuckDuckGo/RecentlyClosed/View/RecentlyClosedMenu.swift b/DuckDuckGo/RecentlyClosed/View/RecentlyClosedMenu.swift index cdbeec8739..798e5f153f 100644 --- a/DuckDuckGo/RecentlyClosed/View/RecentlyClosedMenu.swift +++ b/DuckDuckGo/RecentlyClosed/View/RecentlyClosedMenu.swift @@ -71,15 +71,15 @@ private extension NSMenuItem { image = TabViewModel.Favicon.home title = UserText.tabHomeTitle case .settings: - image = TabViewModel.Favicon.preferences + image = TabViewModel.Favicon.settings title = UserText.tabPreferencesTitle case .bookmarks: - image = TabViewModel.Favicon.preferences + image = TabViewModel.Favicon.bookmarks title = UserText.tabPreferencesTitle case .url, .subscription, .identityTheftRestoration: image = recentlyClosedTab.favicon image?.size = NSSize.faviconSize - title = recentlyClosedTab.title ?? recentlyClosedTab.tabContent.url?.absoluteString ?? "" + title = recentlyClosedTab.title ?? recentlyClosedTab.tabContent.userEditableUrl?.absoluteString ?? "" if title.count > MainMenu.Constants.maxTitleLength { title = String(title.truncated(length: MainMenu.Constants.maxTitleLength)) diff --git a/DuckDuckGo/SecureVault/Model/AutofillNeverPromptWebsitesManager.swift b/DuckDuckGo/SecureVault/Model/AutofillNeverPromptWebsitesManager.swift index 015f0606f3..0e69ffd6dc 100644 --- a/DuckDuckGo/SecureVault/Model/AutofillNeverPromptWebsitesManager.swift +++ b/DuckDuckGo/SecureVault/Model/AutofillNeverPromptWebsitesManager.swift @@ -28,7 +28,7 @@ final class AutofillNeverPromptWebsitesManager { private let secureVault: (any AutofillSecureVault)? - public init(secureVault: (any AutofillSecureVault)? = try? AutofillSecureVaultFactory.makeVault(errorReporter: SecureVaultErrorReporter.shared)) { + public init(secureVault: (any AutofillSecureVault)? = try? AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter.shared)) { self.secureVault = secureVault fetchNeverPromptWebsites() diff --git a/DuckDuckGo/SecureVault/SecureVaultReporter.swift b/DuckDuckGo/SecureVault/SecureVaultReporter.swift new file mode 100644 index 0000000000..af50ce3e84 --- /dev/null +++ b/DuckDuckGo/SecureVault/SecureVaultReporter.swift @@ -0,0 +1,65 @@ +// +// SecureVaultReporter.swift +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Common +import Foundation +import BrowserServicesKit +import PixelKit +import SecureStorage + +final class SecureVaultKeyStoreEventMapper: EventMapping { + public init() { + super.init { event, _, _, _ in + switch event { + case .l1KeyMigration: + PixelKit.fire(DebugEvent(GeneralPixel.secureVaultKeystoreEventL1KeyMigration)) + case .l2KeyMigration: + PixelKit.fire(DebugEvent(GeneralPixel.secureVaultKeystoreEventL2KeyMigration)) + case .l2KeyPasswordMigration: + PixelKit.fire(DebugEvent(GeneralPixel.secureVaultKeystoreEventL2KeyPasswordMigration)) + } + } + } + + override init(mapping: @escaping EventMapping.Mapping) { + fatalError("Use init()") + } +} + +final class SecureVaultReporter: SecureVaultReporting { + static let shared = SecureVaultReporter() + private var keyStoreMapper: SecureVaultKeyStoreEventMapper + private init(keyStoreMapper: SecureVaultKeyStoreEventMapper = SecureVaultKeyStoreEventMapper()) { + self.keyStoreMapper = keyStoreMapper + } + + func secureVaultError(_ error: SecureStorageError) { + guard NSApp.runType.requiresEnvironment else { return } + + switch error { + case .initFailed, .failedToOpenDatabase: + PixelKit.fire(DebugEvent(GeneralPixel.secureVaultInitError(error: error))) + default: + PixelKit.fire(DebugEvent(GeneralPixel.secureVaultError(error: error))) + } + } + + func secureVaultKeyStoreEvent(_ event: SecureStorageKeyStoreEvent) { + keyStoreMapper.fire(event) + } +} diff --git a/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift b/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift index b5fd2cf4fc..f18a2478de 100644 --- a/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift +++ b/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift @@ -153,7 +153,7 @@ final class PasswordManagementViewController: NSViewController { } var secureVault: (any AutofillSecureVault)? { - try? AutofillSecureVaultFactory.makeVault(errorReporter: SecureVaultErrorReporter.shared) + try? AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter.shared) } private let passwordManagerCoordinator: PasswordManagerCoordinating = PasswordManagerCoordinator.shared @@ -1074,7 +1074,7 @@ extension PasswordManagementViewController: NSMenuItemValidation { } private var haveDuckDuckGoPasswords: Bool { - guard let vault = try? AutofillSecureVaultFactory.makeVault(errorReporter: SecureVaultErrorReporter.shared) else { return false } + guard let vault = try? AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter.shared) else { return false } let accounts = (try? vault.accounts()) ?? [] return !accounts.isEmpty } diff --git a/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift b/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift index fa4da02ff7..02a5595240 100644 --- a/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift +++ b/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift @@ -222,7 +222,7 @@ final class SaveCredentialsViewController: NSViewController { } } } else { - _ = try AutofillSecureVaultFactory.makeVault(errorReporter: SecureVaultErrorReporter.shared).storeWebsiteCredentials(credentials) + _ = try AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter.shared).storeWebsiteCredentials(credentials) NSApp.delegateTyped.syncService?.scheduler.notifyDataChanged() os_log(.debug, log: OSLog.sync, "Requesting sync if enabled") } diff --git a/DuckDuckGo/SecureVault/View/SaveIdentityViewController.swift b/DuckDuckGo/SecureVault/View/SaveIdentityViewController.swift index 88006e5096..61ce88b628 100644 --- a/DuckDuckGo/SecureVault/View/SaveIdentityViewController.swift +++ b/DuckDuckGo/SecureVault/View/SaveIdentityViewController.swift @@ -71,7 +71,7 @@ final class SaveIdentityViewController: NSViewController { identity.title = UserText.pmDefaultIdentityAutofillTitle do { - try AutofillSecureVaultFactory.makeVault(errorReporter: SecureVaultErrorReporter.shared).storeIdentity(identity) + try AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter.shared).storeIdentity(identity) PixelKit.fire(GeneralPixel.autofillItemSaved(kind: .identity)) } catch { os_log("%s:%s: failed to store identity %s", type: .error, className, #function, error.localizedDescription) diff --git a/DuckDuckGo/SecureVault/View/SavePaymentMethodViewController.swift b/DuckDuckGo/SecureVault/View/SavePaymentMethodViewController.swift index 68275eeb5e..31c9d6a3b3 100644 --- a/DuckDuckGo/SecureVault/View/SavePaymentMethodViewController.swift +++ b/DuckDuckGo/SecureVault/View/SavePaymentMethodViewController.swift @@ -92,7 +92,7 @@ final class SavePaymentMethodViewController: NSViewController { paymentMethod.title = CreditCardValidation.type(for: paymentMethod.cardNumber).displayName do { - try AutofillSecureVaultFactory.makeVault(errorReporter: SecureVaultErrorReporter.shared).storeCreditCard(paymentMethod) + try AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter.shared).storeCreditCard(paymentMethod) } catch { os_log("%s:%s: failed to store payment method %s", type: .error, className, #function, error.localizedDescription) PixelKit.fire(DebugEvent(GeneralPixel.secureVaultError(error: error))) diff --git a/DuckDuckGo/Sharing/SharingMenu.swift b/DuckDuckGo/Sharing/SharingMenu.swift index 992ddb6103..bdb91d56bf 100644 --- a/DuckDuckGo/Sharing/SharingMenu.swift +++ b/DuckDuckGo/Sharing/SharingMenu.swift @@ -49,7 +49,7 @@ final class SharingMenu: NSMenu { guard let tabViewModel = WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.tabCollectionViewModel.selectedTabViewModel, tabViewModel.canReload, !tabViewModel.isShowingErrorPage, - let url = tabViewModel.tab.content.url else { return nil } + let url = tabViewModel.tab.content.userEditableUrl else { return nil } let sharingData = DuckPlayer.shared.sharingData(for: tabViewModel.title, url: url) ?? (tabViewModel.title, url) diff --git a/DuckDuckGo/Statistics/GeneralPixel.swift b/DuckDuckGo/Statistics/GeneralPixel.swift index 285a3bbdee..e2eeb8d192 100644 --- a/DuckDuckGo/Statistics/GeneralPixel.swift +++ b/DuckDuckGo/Statistics/GeneralPixel.swift @@ -311,6 +311,10 @@ enum GeneralPixel: PixelKitEventV2 { // Tracks installation without tracking retention. case installationAttribution + case secureVaultKeystoreEventL1KeyMigration + case secureVaultKeystoreEventL2KeyMigration + case secureVaultKeystoreEventL2KeyPasswordMigration + var name: String { switch self { @@ -723,7 +727,6 @@ enum GeneralPixel: PixelKitEventV2 { case .bookmarksMigrationCouldNotRemoveOldStore: return "bookmarks_migration_could_not_remove_old_store" case .bookmarksMigrationCouldNotPrepareMultipleFavoriteFolders: return "bookmarks_migration_could_not_prepare_multiple_favorite_folders" - case .syncSentUnauthenticatedRequest: return "sync_sent_unauthenticated_request" case .syncMetadataCouldNotLoadDatabase: return "sync_metadata_could_not_load_database" case .syncBookmarksProviderInitializationFailed: return "sync_bookmarks_provider_initialization_failed" @@ -764,6 +767,10 @@ enum GeneralPixel: PixelKitEventV2 { // Installation Attribution case .installationAttribution: return "m_mac_install" + + case .secureVaultKeystoreEventL1KeyMigration: return "m_mac_secure_vault_keystore_event_l1-key-migration" + case .secureVaultKeystoreEventL2KeyMigration: return "m_mac_secure_vault_keystore_event_l2-key-migration" + case .secureVaultKeystoreEventL2KeyPasswordMigration: return "m_mac_secure_vault_keystore_event_l2-key-password-migration" } } diff --git a/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift b/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift index 5d4435eb83..94f4a59e46 100644 --- a/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift +++ b/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift @@ -30,15 +30,17 @@ final class SuggestionContainer { private let historyCoordinating: HistoryCoordinating private let bookmarkManager: BookmarkManager + private let startupPreferences: StartupPreferences private let loading: SuggestionLoading private var latestQuery: Query? fileprivate let suggestionsURLSession = URLSession(configuration: .ephemeral) - init(suggestionLoading: SuggestionLoading, historyCoordinating: HistoryCoordinating, bookmarkManager: BookmarkManager) { + init(suggestionLoading: SuggestionLoading, historyCoordinating: HistoryCoordinating, bookmarkManager: BookmarkManager, startupPreferences: StartupPreferences = .shared) { self.bookmarkManager = bookmarkManager self.historyCoordinating = historyCoordinating + self.startupPreferences = startupPreferences self.loading = suggestionLoading self.loading.dataSource = self } @@ -91,7 +93,23 @@ extension SuggestionContainer: SuggestionLoadingDataSource { return historyCoordinating.history ?? [] } - func bookmarks(for suggestionLoading: SuggestionLoading) -> [Suggestions.Bookmark] { + @MainActor func internalPages(for suggestionLoading: Suggestions.SuggestionLoading) -> [Suggestions.InternalPage] { + [ + // suggestions for Bookmarks&Settings + .init(title: UserText.bookmarks, url: .bookmarks), + .init(title: UserText.settings, url: .settings), + ] + PreferencePaneIdentifier.allCases.map { + // preference panes URLs + .init(title: UserText.settings + " → " + $0.displayName, url: .settingsPane($0)) + } + { + guard startupPreferences.launchToCustomHomePage, + let homePage = URL(string: startupPreferences.formattedCustomHomePageURL) else { return [] } + // home page suggestion + return [.init(title: UserText.homePage, url: homePage)] + }() + } + + @MainActor func bookmarks(for suggestionLoading: SuggestionLoading) -> [Suggestions.Bookmark] { bookmarkManager.list?.bookmarks() ?? [] } diff --git a/DuckDuckGo/Suggestions/ViewModel/SuggestionViewModel.swift b/DuckDuckGo/Suggestions/ViewModel/SuggestionViewModel.swift index d7b8c5910c..c3a6d1aad7 100644 --- a/DuckDuckGo/Suggestions/ViewModel/SuggestionViewModel.swift +++ b/DuckDuckGo/Suggestions/ViewModel/SuggestionViewModel.swift @@ -94,7 +94,8 @@ struct SuggestionViewModel: Equatable { } else { return title ?? url.toString(forUserInput: userStringValue) } - case .bookmark(title: let title, url: _, isFavorite: _, allowedInTopHits: _): + case .bookmark(title: let title, url: _, isFavorite: _, allowedInTopHits: _), + .internalPage(title: let title, url: _): return title case .unknown(value: let value): return value @@ -113,7 +114,8 @@ struct SuggestionViewModel: Equatable { } else { return title } - case .bookmark(title: let title, url: _, isFavorite: _, allowedInTopHits: _): + case .bookmark(title: let title, url: _, isFavorite: _, allowedInTopHits: _), + .internalPage(title: let title, url: _): return title } } @@ -155,6 +157,8 @@ struct SuggestionViewModel: Equatable { dropScheme: true, dropTrailingSlash: true) } + case .internalPage: + return " – " + UserText.duckDuckGo } } @@ -174,6 +178,13 @@ struct SuggestionViewModel: Equatable { return .favoritedBookmarkSuggestion case .unknown: return .web + case .internalPage(title: _, url: let url) where url == .bookmarks: + return .bookmarksFolder + case .internalPage(title: _, url: let url) where url.isSettingsURL: + return .settingsMulticolor16 + case .internalPage(title: _, url: let url): + guard url == URL(string: StartupPreferences.shared.formattedCustomHomePageURL) else { return nil } + return .home16 } } diff --git a/DuckDuckGo/Sync/SyncCredentialsAdapter.swift b/DuckDuckGo/Sync/SyncCredentialsAdapter.swift index 571fa21afb..c9f495901e 100644 --- a/DuckDuckGo/Sync/SyncCredentialsAdapter.swift +++ b/DuckDuckGo/Sync/SyncCredentialsAdapter.swift @@ -44,7 +44,7 @@ final class SyncCredentialsAdapter { syncDidCompletePublisher = syncDidCompleteSubject.eraseToAnyPublisher() databaseCleaner = CredentialsDatabaseCleaner( secureVaultFactory: secureVaultFactory, - secureVaultErrorReporter: SecureVaultErrorReporter.shared, + secureVaultErrorReporter: SecureVaultReporter.shared, errorEvents: CredentialsCleanupErrorHandling(), log: .passwordManager ) @@ -71,7 +71,7 @@ final class SyncCredentialsAdapter { do { let provider = try CredentialsProvider( secureVaultFactory: secureVaultFactory, - secureVaultErrorReporter: SecureVaultErrorReporter.shared, + secureVaultErrorReporter: SecureVaultReporter.shared, metadataStore: metadataStore, metricsEvents: metricsEventsHandler, log: OSLog.sync, diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift index a799ebbb04..2ec1087956 100644 --- a/DuckDuckGo/Tab/Model/Tab.swift +++ b/DuckDuckGo/Tab/Model/Tab.swift @@ -461,12 +461,15 @@ protocol NewWindowPolicyDecisionMaker { if let url = webView.url { let content = TabContent.contentFromURL(url, source: .webViewUpdated) - if self.content.isUrl, self.content.url == url { + if self.content.isUrl, self.content.urlForWebView == url { // ignore content updates when tab.content has userEntered or credential set but equal url as it comes from the WebView url updated event } else if content != self.content { self.content = content } - } else if self.content.isUrl { + } else if self.content.isUrl, + // DuckURLSchemeHandler redirects duck:// address to a simulated request + // ignore webView.url temporarily switching to `nil` + self.content.urlForWebView?.isDuckPlayer != true { // when e.g. opening a download in new tab - web view restores `nil` after the navigation is interrupted // maybe it worths adding another content type like .interruptedLoad(URL) to display a URL in the address bar self.content = .none @@ -579,7 +582,7 @@ protocol NewWindowPolicyDecisionMaker { @MainActor var currentHistoryItem: BackForwardListItem? { webView.backForwardList.currentItem.map(BackForwardListItem.init) - ?? (content.url ?? navigationDelegate.currentNavigation?.url).map { url in + ?? (content.urlForWebView ?? navigationDelegate.currentNavigation?.url).map { url in BackForwardListItem(kind: .url(url), title: webView.title ?? title, identity: nil) } } @@ -608,7 +611,11 @@ protocol NewWindowPolicyDecisionMaker { let canGoBack = webView.canGoBack let canGoForward = webView.canGoForward - let canReload = self.content.userEditableUrl != nil + let canReload = if case .url(let url, credential: _, source: _) = content, !(url.isDuckPlayer || url.isDuckURLScheme) { + true + } else { + false + } if canGoBack != self.canGoBack { self.canGoBack = canGoBack @@ -721,7 +728,7 @@ protocol NewWindowPolicyDecisionMaker { if startupPreferences.launchToCustomHomePage, let customURL = URL(string: startupPreferences.formattedCustomHomePageURL) { - setContent(.url(customURL, credential: nil, source: .ui)) + setContent(.contentFromURL(customURL, source: .ui)) } else { setContent(.newtab) } @@ -895,9 +902,10 @@ protocol NewWindowPolicyDecisionMaker { } func requestFireproofToggle() { - guard let url = content.userEditableUrl, - let host = url.host - else { return } + guard case .url(let url, _, _) = content, + url.navigationalScheme?.isHypertextScheme == true, + !url.isDuckPlayer, + let host = url.host else { return } _ = FireproofDomains.shared.toggle(domain: host) } @@ -992,7 +1000,7 @@ protocol NewWindowPolicyDecisionMaker { if cachedFavicon != favicon { favicon = cachedFavicon } - } else if oldValue?.url?.host != url.host { + } else if oldValue?.urlForWebView?.host != url.host { // If the domain matches the previous value, just keep the same favicon favicon = nil } @@ -1031,7 +1039,7 @@ extension Tab: FaviconUserScriptDelegate { for documentUrl: URL) { guard documentUrl != .error else { return } faviconManagement.handleFaviconLinks(faviconLinks, documentUrl: documentUrl) { favicon in - guard documentUrl == self.content.url, let favicon = favicon else { + guard documentUrl == self.content.urlForWebView, let favicon = favicon else { return } self.favicon = favicon.image @@ -1098,7 +1106,7 @@ extension Tab/*: NavigationResponder*/ { // to be moved to Tab+Navigation.swift // credential is removed from the URL and set to TabContent to be used on next Challenge self.content = .url(navigationAction.url.removingBasicAuthCredential(), credential: credential, source: .webViewUpdated) // reload URL without credentialss - request.url = self.content.url! + request.url = self.content.urlForWebView! navigator.load(request) } } diff --git a/DuckDuckGo/Tab/Model/TabContent.swift b/DuckDuckGo/Tab/Model/TabContent.swift index d052a791b9..369a036ba2 100644 --- a/DuckDuckGo/Tab/Model/TabContent.swift +++ b/DuckDuckGo/Tab/Model/TabContent.swift @@ -129,6 +129,8 @@ extension TabContent { if let settingsPane = url.flatMap(PreferencePaneIdentifier.init(url:)) { return .settings(pane: settingsPane) + } else if url?.isDuckPlayer == true, let (videoId, timestamp) = url?.youtubeVideoParams { + return .url(.duckPlayer(videoId, timestamp: timestamp), credential: nil, source: source) } else if let url, let credential = url.basicAuthCredential { // when navigating to a URL with basic auth username/password, cache it and redirect to a trimmed URL return .url(url.removingBasicAuthCredential(), credential: credential, source: source) @@ -190,19 +192,20 @@ extension TabContent { } } - var url: URL? { - userEditableUrl - } + // !!! don‘t add `url` property to avoid ambiguity with the `.url` enum case + // use `userEditableUrl` or `urlForWebView` instead. + /// user-editable URL displayed in the address bar var userEditableUrl: URL? { - switch self { - case .url(let url, credential: _, source: _) where !(url.isDuckPlayer || url.isDuckURLScheme): - return url - default: - return nil + let url = urlForWebView + if let url, url.isDuckPlayer, + let (videoID, timestamp) = url.youtubeVideoParams { + return .duckPlayer(videoID, timestamp: timestamp) } + return url } + /// `real` URL loaded in the web view var urlForWebView: URL? { switch self { case .url(let url, credential: _, source: _): @@ -290,10 +293,10 @@ extension TabContent { var canBeBookmarked: Bool { switch self { - case .subscription, .identityTheftRestoration, .dataBrokerProtection: + case .newtab, .onboarding, .none: return false - default: - return isUrl + case .url, .settings, .bookmarks, .subscription, .identityTheftRestoration, .dataBrokerProtection: + return true } } diff --git a/DuckDuckGo/Tab/TabExtensions/AutofillTabExtension.swift b/DuckDuckGo/Tab/TabExtensions/AutofillTabExtension.swift index 867702bd7e..328b447ea3 100644 --- a/DuckDuckGo/Tab/TabExtensions/AutofillTabExtension.swift +++ b/DuckDuckGo/Tab/TabExtensions/AutofillTabExtension.swift @@ -163,8 +163,8 @@ extension AutofillTabExtension: SecureVaultManagerDelegate { } } - func secureVaultInitFailed(_ error: SecureStorageError) { - SecureVaultErrorReporter.shared.secureVaultInitFailed(error) + func secureVaultError(_ error: SecureStorageError) { + SecureVaultReporter.shared.secureVaultError(error) } public func secureVaultManager(_: BrowserServicesKit.SecureVaultManager, didReceivePixel pixel: AutofillUserScript.JSPixel) { diff --git a/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift b/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift index 7624a298b5..139c9c2b97 100644 --- a/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift +++ b/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift @@ -179,7 +179,7 @@ extension TabExtensionsBuilder { HistoryTabExtension(isBurner: args.isTabBurner, historyCoordinating: dependencies.historyCoordinating, trackersPublisher: contentBlocking.trackersPublisher, - urlPublisher: args.contentPublisher.map { content in content.isUrl ? content.url : nil }, + urlPublisher: args.contentPublisher.map { content in content.isUrl ? content.urlForWebView : nil }, titlePublisher: args.titlePublisher) } add { diff --git a/DuckDuckGo/Tab/TabExtensions/TabSnapshotExtension.swift b/DuckDuckGo/Tab/TabExtensions/TabSnapshotExtension.swift index 66f84f5ebd..4aaee77a92 100644 --- a/DuckDuckGo/Tab/TabExtensions/TabSnapshotExtension.swift +++ b/DuckDuckGo/Tab/TabExtensions/TabSnapshotExtension.swift @@ -151,7 +151,8 @@ final class TabSnapshotExtension { @MainActor func renderWebViewSnapshot() async { - guard let webView, let tabContent, let url = tabContent.url else { + guard let webView, let tabContent, + let url = tabContent.userEditableUrl, !url.isDuckURLScheme else { // Previews of native views are rendered in renderNativePreview() return } diff --git a/DuckDuckGo/Tab/TabLazyLoader/LazyLoadable.swift b/DuckDuckGo/Tab/TabLazyLoader/LazyLoadable.swift index 9d2d84628d..7fb5446cb5 100644 --- a/DuckDuckGo/Tab/TabLazyLoader/LazyLoadable.swift +++ b/DuckDuckGo/Tab/TabLazyLoader/LazyLoadable.swift @@ -37,7 +37,7 @@ protocol LazyLoadable: AnyObject, Identifiable { extension Tab: LazyLoadable { var isUrl: Bool { content.isUrl } - var url: URL? { content.url } + var url: URL? { content.urlForWebView } var loadingFinishedPublisher: AnyPublisher { navigationStatePublisher.compactMap { $0 } diff --git a/DuckDuckGo/Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Tab/View/BrowserTabViewController.swift index 327dc8975f..9c2f65619f 100644 --- a/DuckDuckGo/Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Tab/View/BrowserTabViewController.swift @@ -776,7 +776,7 @@ extension BrowserTabViewController: NSDraggingDestination { return true } - selectedTab.setContent(.url(url, source: .appOpenUrl)) + selectedTab.setContent(.contentFromURL(url, source: .appOpenUrl)) return true } diff --git a/DuckDuckGo/Tab/ViewModel/TabViewModel.swift b/DuckDuckGo/Tab/ViewModel/TabViewModel.swift index 1db6697d20..e9726a5dc0 100644 --- a/DuckDuckGo/Tab/ViewModel/TabViewModel.swift +++ b/DuckDuckGo/Tab/ViewModel/TabViewModel.swift @@ -26,12 +26,14 @@ final class TabViewModel { enum Favicon { static let home = NSImage.homeFavicon + static let duckPlayer = NSImage.duckPlayerSettings static let burnerHome = NSImage.burnerTabFavicon - static let preferences = NSImage.preferences - static let bookmarks = NSImage.bookmarks - static let dataBrokerProtection = NSImage.dbpIcon - static let subscription = NSImage.subscriptionIcon - static let identityTheftRestoration = NSImage.itrIcon + static let settings = NSImage.settingsMulticolor16 + static let bookmarks = NSImage.bookmarksFolder + static let emailProtection = NSImage.emailProtectionIcon + static let dataBrokerProtection = NSImage.personalInformationRemovalMulticolor16 + static let subscription = NSImage.privacyPro + static let identityTheftRestoration = NSImage.identityTheftRestorationMulticolor16 } private(set) var tab: Tab @@ -62,7 +64,8 @@ final class TabViewModel { var loadingStartTime: CFTimeInterval? @Published private(set) var addressBarString: String = "" - @Published private(set) var passiveAddressBarString: String = "" + @Published private(set) var passiveAddressBarAttributedString = NSAttributedString() + var lastAddressBarTextFieldValue: AddressBarTextField.Value? @Published private(set) var title: String = UserText.tabHomeTitle @@ -80,6 +83,19 @@ final class TabViewModel { !isShowingErrorPage && canReload && !tab.webView.isInFullScreenMode } + var canFindInPage: Bool { + guard !isShowingErrorPage else { return false } + switch tab.content { + case .url(let url, _, _): + return !(url.isDuckPlayer || url.isDuckURLScheme) + case .subscription, .identityTheftRestoration: + return true + + case .newtab, .settings, .bookmarks, .onboarding, .dataBrokerProtection, .none: + return false + } + } + init(tab: Tab, appearancePreferences: AppearancePreferences = .shared, accessibilityPreferences: AccessibilityPreferences = .shared) { @@ -117,7 +133,7 @@ final class TabViewModel { case .url(let url, _, source: .webViewUpdated), .url(let url, _, source: .link): - guard !url.isEmpty, url != .blankPage else { fallthrough } + guard !url.isEmpty, url != .blankPage, !url.isDuckPlayer else { fallthrough } // Only display the Tab content URL update matching its Security Origin // see https://github.com/mozilla-mobile/firefox-ios/wiki/WKWebView-navigation-and-security-considerations @@ -215,9 +231,8 @@ final class TabViewModel { } private func subscribeToPreferences() { - appearancePreferences.$showFullURL.dropFirst().sink { [weak self] newValue in - guard let self = self, let url = self.tabURL, let host = self.tabHostURL else { return } - self.updatePassiveAddressBarString(showURL: newValue, url: url, hostURL: host) + appearancePreferences.$showFullURL.dropFirst().sink { [weak self] showFullURL in + self?.updatePassiveAddressBarString(showFullURL: showFullURL) }.store(in: &cancellables) accessibilityPreferences.$defaultPageZoom.sink { [weak self] newValue in guard let self = self else { return } @@ -233,64 +248,70 @@ final class TabViewModel { } private func updateCanBeBookmarked() { - canBeBookmarked = !isShowingErrorPage && (tab.content.url ?? .blankPage) != .blankPage - } - - private var tabURL: URL? { - return tab.content.url + canBeBookmarked = !isShowingErrorPage && tab.content.canBeBookmarked } - private var tabHostURL: URL? { - return tabURL?.root + private func updateAddressBarStrings() { + updateAddressBarString() + updatePassiveAddressBarString() } - private func updateAddressBarStrings() { - guard tab.content.isUrl, let url = tabURL else { - addressBarString = "" - passiveAddressBarString = "" - return - } + private func updateAddressBarString() { + addressBarString = { + guard ![.none, .onboarding, .newtab].contains(tab.content), + let url = tab.content.userEditableUrl else { return "" } - if url.isFileURL { - addressBarString = url.absoluteString - passiveAddressBarString = url.absoluteString - return - } + if url.isBlobURL { + return url.strippingUnsupportedCredentials() + } + return url.absoluteString + }() + } - if url.isDataURL { - addressBarString = url.absoluteString - passiveAddressBarString = "data:" - return + private func updatePassiveAddressBarString(showFullURL: Bool? = nil) { + let showFullURL = showFullURL ?? appearancePreferences.showFullURL + passiveAddressBarAttributedString = switch tab.content { + case .newtab, .onboarding, .none: + .init() // empty + case .settings: + .settingsTrustedIndicator + case .bookmarks: + .bookmarksTrustedIndicator + case .dataBrokerProtection: + .dbpTrustedIndicator + case .subscription: + .subscriptionTrustedIndicator + case .identityTheftRestoration: + .identityTheftRestorationTrustedIndicator + case .url(let url, _, _) where url.isDuckPlayer: + .duckPlayerTrustedIndicator + case .url(let url, _, _) where url.isEmailProtection: + .emailProtectionTrustedIndicator + case .url(let url, _, _): + NSAttributedString(string: passiveAddressBarString(with: url, showFullURL: showFullURL)) } + } + private func passiveAddressBarString(with url: URL, showFullURL: Bool) -> String { if url.isBlobURL { - let strippedUrl = url.stripUnsupportedCredentials() - addressBarString = strippedUrl - passiveAddressBarString = strippedUrl - return - } + url.strippingUnsupportedCredentials() - guard let hostURL = tabHostURL else { - // also lands here for about:blank and about:home - addressBarString = "" - passiveAddressBarString = "" - return - } + } else if url.isDataURL { + "data:" - addressBarString = url.absoluteString - updatePassiveAddressBarString(showURL: appearancePreferences.showFullURL, url: url, hostURL: hostURL) - } + } else if !showFullURL && url.isFileURL { + url.lastPathComponent - private func updatePassiveAddressBarString(showURL: Bool, url: URL, hostURL: URL) { - if showURL { - passiveAddressBarString = url.toString(decodePunycode: true, dropScheme: false, dropTrailingSlash: true) - } else { - passiveAddressBarString = hostURL.toString(decodePunycode: true, dropScheme: true, dropTrailingSlash: true).droppingWwwPrefix() + } else if !showFullURL && url.host?.isEmpty == false { + url.root?.toString(decodePunycode: true, dropScheme: true, dropTrailingSlash: true).droppingWwwPrefix() ?? "" + + } else /* display full url */ { + url.toString(decodePunycode: true, dropScheme: false, dropTrailingSlash: true) } } private func updateTitle() { // swiftlint:disable:this cyclomatic_complexity - let title: String + var 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): @@ -324,46 +345,41 @@ final class TabViewModel { title = addressBarString } } + if title.isEmpty { + title = UserText.tabUntitledTitle + } if self.title != title { self.title = title } } + // swiftlint:disable:next cyclomatic_complexity private func updateFavicon(_ tabFavicon: NSImage?? = .none /* provided from .sink or taken from tab.favicon (optional) if .none */) { guard !isShowingErrorPage else { favicon = errorFaviconToShow(error: tab.error) return } - switch tab.content { + favicon = switch tab.content { case .dataBrokerProtection: - favicon = Favicon.dataBrokerProtection - return + Favicon.dataBrokerProtection + case .newtab where tab.burnerMode.isBurner: + Favicon.burnerHome case .newtab: - if tab.burnerMode.isBurner { - favicon = Favicon.burnerHome - } else { - favicon = Favicon.home - } - return + Favicon.home case .settings: - favicon = Favicon.preferences - return + Favicon.settings case .bookmarks: - favicon = Favicon.bookmarks - return + Favicon.bookmarks case .subscription: - favicon = Favicon.subscription - return + Favicon.subscription case .identityTheftRestoration: - favicon = Favicon.identityTheftRestoration - return - case .url, .onboarding, .none: break - } - - if let favicon: NSImage? = tabFavicon { - self.favicon = favicon - } else { - self.favicon = tab.favicon + Favicon.identityTheftRestoration + case .url(let url, _, _) where url.isDuckPlayer: + Favicon.duckPlayer + case .url(let url, _, _) where url.isEmailProtection: + Favicon.emailProtection + case .url, .onboarding, .none: + tabFavicon ?? tab.favicon } } @@ -423,3 +439,61 @@ extension TabViewModel: TabDataClearing { } } + +private extension NSAttributedString { + + private typealias Component = NSAttributedString + + private static let spacer = NSImage() // empty spacer image attachment for Attributed Strings below + + private static let iconBaselineOffset: CGFloat = -3 + private static let iconSize: CGFloat = 16 + private static let iconSpacing: CGFloat = 6 + private static let chevronSize: CGFloat = 12 + private static let chevronSpacing: CGFloat = 12 + + private static let duckDuckGoWithChevronAttributedString = NSAttributedString { + // logo + Component(image: .homeFavicon, rect: CGRect(x: 0, y: iconBaselineOffset, width: iconSize, height: iconSize)) + // spacing + Component(image: spacer, rect: CGRect(x: 0, y: 0, width: iconSpacing, height: 1)) + // DuckDuckGo + Component(string: UserText.duckDuckGo) + + // spacing (wide) + Component(image: spacer, rect: CGRect(x: 0, y: 0, width: chevronSpacing, height: 1)) + // chevron + Component(image: .chevronRight12, rect: CGRect(x: 0, y: -1, width: chevronSize, height: chevronSize)) + // spacing (wide) + Component(image: spacer, rect: CGRect(x: 0, y: 0, width: chevronSpacing, height: 1)) + } + + private static func trustedIndicatorAttributedString(with icon: NSImage, title: String) -> NSAttributedString { + NSAttributedString { + duckDuckGoWithChevronAttributedString + + // favicon + Component(image: icon, rect: CGRect(x: 0, y: iconBaselineOffset, width: icon.size.width, height: icon.size.height)) + // spacing + Component(image: spacer, rect: CGRect(x: 0, y: 0, width: iconSpacing, height: 1)) + // title + Component(string: title) + } + } + + static let settingsTrustedIndicator = trustedIndicatorAttributedString(with: .settingsMulticolor16, + title: UserText.settings) + static let bookmarksTrustedIndicator = trustedIndicatorAttributedString(with: .bookmarksFolder, + title: UserText.bookmarks) + static let dbpTrustedIndicator = trustedIndicatorAttributedString(with: .personalInformationRemovalMulticolor16, + title: UserText.tabDataBrokerProtectionTitle) + static let subscriptionTrustedIndicator = trustedIndicatorAttributedString(with: .privacyPro, + title: UserText.subscription) + static let identityTheftRestorationTrustedIndicator = trustedIndicatorAttributedString(with: .identityTheftRestorationMulticolor16, + title: UserText.identityTheftRestorationOptionsMenuItem) + static let duckPlayerTrustedIndicator = trustedIndicatorAttributedString(with: .duckPlayerSettings, + title: UserText.duckPlayer) + static let emailProtectionTrustedIndicator = trustedIndicatorAttributedString(with: .emailProtectionIcon, + title: UserText.emailProtectionPreferences) + +} diff --git a/DuckDuckGo/TabBar/View/TabBarViewController.swift b/DuckDuckGo/TabBar/View/TabBarViewController.swift index 8f0253224b..ecce54a428 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewController.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewController.swift @@ -1041,7 +1041,7 @@ extension TabBarViewController: TabBarViewItemDelegate { func tabBarViewItemBookmarkThisPageAction(_ tabBarViewItem: TabBarViewItem) { guard let indexPath = collectionView.indexPath(for: tabBarViewItem), let tabViewModel = tabCollectionViewModel.tabViewModel(at: indexPath.item), - let url = tabViewModel.tab.content.url else { + let url = tabViewModel.tab.content.userEditableUrl else { os_log("TabBarViewController: Failed to get index path of tab bar view item", type: .error) return } @@ -1049,6 +1049,15 @@ extension TabBarViewController: TabBarViewItemDelegate { bookmarkTab(with: url, title: tabViewModel.title) } + func tabBarViewAllItemsCanBeBookmarked(_ tabBarViewItem: TabBarViewItem) -> Bool { + tabCollectionViewModel.canBookmarkAllOpenTabs() + } + + func tabBarViewItemBookmarkAllOpenTabsAction(_ tabBarViewItem: TabBarViewItem) { + let websitesInfo = tabCollectionViewModel.tabs.compactMap(WebsiteInfo.init) + BookmarksDialogViewFactory.makeBookmarkAllOpenTabsView(websitesInfo: websitesInfo).show() + } + func tabBarViewItemCloseAction(_ tabBarViewItem: TabBarViewItem) { guard let indexPath = collectionView.indexPath(for: tabBarViewItem) else { assertionFailure("TabBarViewController: Failed to get index path of tab bar view item") diff --git a/DuckDuckGo/TabBar/View/TabBarViewItem.swift b/DuckDuckGo/TabBar/View/TabBarViewItem.swift index a92752aa56..b7e81cacc9 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewItem.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewItem.swift @@ -33,6 +33,7 @@ protocol TabBarViewItemDelegate: AnyObject { func tabBarViewItemCanBeDuplicated(_ tabBarViewItem: TabBarViewItem) -> Bool func tabBarViewItemCanBePinned(_ tabBarViewItem: TabBarViewItem) -> Bool func tabBarViewItemCanBeBookmarked(_ tabBarViewItem: TabBarViewItem) -> Bool + func tabBarViewAllItemsCanBeBookmarked(_ tabBarViewItem: TabBarViewItem) -> Bool func tabBarViewItemCloseAction(_ tabBarViewItem: TabBarViewItem) func tabBarViewItemTogglePermissionAction(_ tabBarViewItem: TabBarViewItem) @@ -41,6 +42,7 @@ protocol TabBarViewItemDelegate: AnyObject { func tabBarViewItemDuplicateAction(_ tabBarViewItem: TabBarViewItem) func tabBarViewItemPinAction(_ tabBarViewItem: TabBarViewItem) func tabBarViewItemBookmarkThisPageAction(_ tabBarViewItem: TabBarViewItem) + func tabBarViewItemBookmarkAllOpenTabsAction(_ tabBarViewItem: TabBarViewItem) func tabBarViewItemMoveToNewWindowAction(_ tabBarViewItem: TabBarViewItem) func tabBarViewItemMoveToNewBurnerWindowAction(_ tabBarViewItem: TabBarViewItem) func tabBarViewItemFireproofSite(_ tabBarViewItem: TabBarViewItem) @@ -196,6 +198,10 @@ final class TabBarViewItem: NSCollectionViewItem { delegate?.tabBarViewItemBookmarkThisPageAction(self) } + @objc func bookmarkAllOpenTabsAction(_ sender: Any) { + delegate?.tabBarViewItemBookmarkAllOpenTabsAction(self) + } + private var lastKnownIndexPath: IndexPath? @IBAction func closeButtonAction(_ sender: Any) { @@ -253,7 +259,7 @@ final class TabBarViewItem: NSCollectionViewItem { }.store(in: &cancellables) tabViewModel.tab.$content.sink { [weak self] content in - self?.currentURL = content.url + self?.currentURL = content.userEditableUrl }.store(in: &cancellables) tabViewModel.$usedPermissions.assign(to: \.usedPermissions, onWeaklyHeld: self).store(in: &cancellables) @@ -486,16 +492,19 @@ extension TabBarViewItem: NSMenuDelegate { // Section 1 addDuplicateMenuItem(to: menu) addPinMenuItem(to: menu) - menu.addItem(NSMenuItem.separator()) + addMuteUnmuteMenuItem(to: menu) + menu.addItem(.separator()) // Section 2 - addBookmarkMenuItem(to: menu) addFireproofMenuItem(to: menu) - - addMuteUnmuteMenuItem(to: menu) - menu.addItem(NSMenuItem.separator()) + addBookmarkMenuItem(to: menu) + menu.addItem(.separator()) // Section 3 + addBookmarkAllTabsMenuItem(to: menu) + menu.addItem(.separator()) + + // Section 4 addCloseMenuItem(to: menu) addCloseOtherMenuItem(to: menu, areThereOtherTabs: areThereOtherTabs) addCloseTabsToTheRightMenuItem(to: menu, areThereTabsToTheRight: otherItemsState.hasItemsToTheRight) @@ -525,6 +534,13 @@ extension TabBarViewItem: NSMenuDelegate { menu.addItem(bookmarkMenuItem) } + private func addBookmarkAllTabsMenuItem(to menu: NSMenu) { + let bookmarkMenuItem = NSMenuItem(title: UserText.bookmarkAllTabs, action: #selector(bookmarkAllOpenTabsAction(_:)), keyEquivalent: "") + bookmarkMenuItem.target = self + bookmarkMenuItem.isEnabled = delegate?.tabBarViewAllItemsCanBeBookmarked(self) ?? false + menu.addItem(bookmarkMenuItem) + } + private func addFireproofMenuItem(to menu: NSMenu) { var menuItem = NSMenuItem(title: UserText.fireproofSite, action: #selector(fireproofSiteAction(_:)), keyEquivalent: "") menuItem.isEnabled = false @@ -542,7 +558,6 @@ extension TabBarViewItem: NSMenuDelegate { private func addMuteUnmuteMenuItem(to menu: NSMenu) { guard let audioState = delegate?.tabBarViewItemAudioState(self) else { return } - menu.addItem(NSMenuItem.separator()) let menuItemTitle = audioState == .muted ? UserText.unmuteTab : UserText.muteTab let muteUnmuteMenuItem = NSMenuItem(title: menuItemTitle, action: #selector(muteUnmuteSiteAction(_:)), keyEquivalent: "") muteUnmuteMenuItem.target = self diff --git a/DuckDuckGo/TabBar/ViewModel/TabCollectionViewModel.swift b/DuckDuckGo/TabBar/ViewModel/TabCollectionViewModel.swift index 0317aed9b9..2467c1d900 100644 --- a/DuckDuckGo/TabBar/ViewModel/TabCollectionViewModel.swift +++ b/DuckDuckGo/TabBar/ViewModel/TabCollectionViewModel.swift @@ -763,3 +763,14 @@ extension TabCollectionViewModel { } } + +// MARK: - Bookmark All Open Tabs + +extension TabCollectionViewModel { + + func canBookmarkAllOpenTabs() -> Bool { + // At least two non pinned, non empty (URL only), and not showing an error tabs. + tabViewModels.values.filter(\.canBeBookmarked).count >= 2 + } + +} diff --git a/DuckDuckGo/TabPreview/TabPreviewViewController.swift b/DuckDuckGo/TabPreview/TabPreviewViewController.swift index 9e888b4146..cdf6d3e721 100644 --- a/DuckDuckGo/TabPreview/TabPreviewViewController.swift +++ b/DuckDuckGo/TabPreview/TabPreviewViewController.swift @@ -185,7 +185,7 @@ extension TabPreviewViewController { let title: String var tabContent: Tab.TabContent let shouldShowPreview: Bool - var addressBarString: String { tabContent.url?.absoluteString ?? "Default" } + var addressBarString: String { tabContent.userEditableUrl?.absoluteString ?? "Default" } var snapshot: NSImage? { let image = NSImage(size: size) diff --git a/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift b/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift index b4e463df17..1e9dd5d333 100644 --- a/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift +++ b/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift @@ -24,6 +24,7 @@ import NetworkProtection import NetworkExtension import NetworkProtectionIPC import NetworkProtectionUI +import Subscription struct VPNMetadata: Encodable { @@ -49,7 +50,8 @@ struct VPNMetadata: Encodable { struct VPNState: Encodable { let onboardingState: String let connectionState: String - let lastErrorMessage: String + let lastStartErrorDescription: String + let lastTunnelErrorDescription: String let connectedServer: String let connectedServerIP: String } @@ -72,12 +74,19 @@ struct VPNMetadata: Encodable { let notificationsAgentIsRunning: Bool } + struct PrivacyProInfo: Encodable { + let betaParticipant: Bool + let hasPrivacyProAccount: Bool + let hasVPNEntitlement: Bool + } + let appInfo: AppInfo let deviceInfo: DeviceInfo let networkInfo: NetworkInfo let vpnState: VPNState let vpnSettingsState: VPNSettingsState let loginItemState: LoginItemState + let privacyProInfo: PrivacyProInfo func toPrettyPrintedJSON() -> String? { let encoder = JSONEncoder() @@ -111,11 +120,16 @@ protocol VPNMetadataCollector { final class DefaultVPNMetadataCollector: VPNMetadataCollector { private let statusReporter: NetworkProtectionStatusReporter + private let ipcClient: TunnelControllerIPCClient + private let defaults: UserDefaults - init() { + init(defaults: UserDefaults = .netP) { let ipcClient = TunnelControllerIPCClient() ipcClient.register() + self.ipcClient = ipcClient + self.defaults = defaults + self.statusReporter = DefaultNetworkProtectionStatusReporter( statusObserver: ipcClient.connectionStatusObserver, serverInfoObserver: ipcClient.serverInfoObserver, @@ -138,6 +152,7 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { let vpnState = await collectVPNState() let vpnSettingsState = collectVPNSettingsState() let loginItemState = collectLoginItemState() + let privacyProInfo = await collectPrivacyProInfo() return VPNMetadata( appInfo: appInfoMetadata, @@ -145,7 +160,8 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { networkInfo: networkInfoMetadata, vpnState: vpnState, vpnSettingsState: vpnSettingsState, - loginItemState: loginItemState + loginItemState: loginItemState, + privacyProInfo: privacyProInfo ) } @@ -153,7 +169,7 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { private func collectAppInfoMetadata() -> VPNMetadata.AppInfo { let appVersion = AppVersion.shared.versionAndBuildNumber - let versionStore = NetworkProtectionLastVersionRunStore(userDefaults: .netP) + let versionStore = NetworkProtectionLastVersionRunStore(userDefaults: defaults) let isInternalUser = NSApp.delegateTyped.internalUserDecider.isInternalUser let isInApplicationsDirectory = Bundle.main.isInApplicationsDirectory @@ -222,7 +238,7 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { func collectVPNState() async -> VPNMetadata.VPNState { let onboardingState: String - switch UserDefaults.netP.networkProtectionOnboardingStatus { + switch defaults.networkProtectionOnboardingStatus { case .completed: onboardingState = "complete" case .isOnboarding(let step): @@ -234,13 +250,16 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { } } + let errorHistory = VPNOperationErrorHistory(ipcClient: ipcClient, defaults: defaults) + let connectionState = String(describing: statusReporter.statusObserver.recentValue) - let lastErrorMessage = statusReporter.connectionErrorObserver.recentValue ?? "none" + let lastTunnelErrorDescription = await errorHistory.lastTunnelErrorDescription let connectedServer = statusReporter.serverInfoObserver.recentValue.serverLocation?.serverLocation ?? "none" let connectedServerIP = statusReporter.serverInfoObserver.recentValue.serverAddress ?? "none" return .init(onboardingState: onboardingState, connectionState: connectionState, - lastErrorMessage: lastErrorMessage, + lastStartErrorDescription: errorHistory.lastStartErrorDescription, + lastTunnelErrorDescription: lastTunnelErrorDescription, connectedServer: connectedServer, connectedServerIP: connectedServerIP) } @@ -269,7 +288,7 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { } func collectVPNSettingsState() -> VPNMetadata.VPNSettingsState { - let settings = VPNSettings(defaults: .netP) + let settings = VPNSettings(defaults: defaults) return .init( connectOnLoginEnabled: settings.connectOnLogin, @@ -283,4 +302,20 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { ) } + func collectPrivacyProInfo() async -> VPNMetadata.PrivacyProInfo { + let accountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) + let waitlistStore = WaitlistKeychainStore( + waitlistIdentifier: NetworkProtectionWaitlist.identifier, + keychainAppGroup: NetworkProtectionWaitlist.keychainAppGroup + ) + + let hasVPNEntitlement = (try? await accountManager.hasEntitlement(for: .networkProtection).get()) ?? false + + return .init( + betaParticipant: waitlistStore.isInvited, + hasPrivacyProAccount: accountManager.isUserAuthenticated, + hasVPNEntitlement: hasVPNEntitlement + ) + } + } diff --git a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift b/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift index c792a3e788..11bf4a988e 100644 --- a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift +++ b/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift @@ -166,29 +166,11 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { } private var isWaitlistBetaActive: Bool { - switch featureOverrides.waitlistActive { - case .useRemoteValue: - guard privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(NetworkProtectionSubfeature.waitlistBetaActive) else { - return false - } - - return true - case .on: - return true - case .off: - return false - } + true } private var isWaitlistEnabled: Bool { - switch featureOverrides.waitlistEnabled { - case .useRemoteValue: - return privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(NetworkProtectionSubfeature.waitlist) - case .on: - return true - case .off: - return false - } + true } func disableForAllUsers() async { diff --git a/DuckDuckGo/Windows/View/WindowControllersManager.swift b/DuckDuckGo/Windows/View/WindowControllersManager.swift index 10758ce56d..1bea6538a8 100644 --- a/DuckDuckGo/Windows/View/WindowControllersManager.swift +++ b/DuckDuckGo/Windows/View/WindowControllersManager.swift @@ -174,12 +174,12 @@ extension WindowControllersManager { let firstTab = tabCollection.tabs.first, case .newtab = firstTab.content, !newTab { - firstTab.setContent(url.map { .url($0, source: source) } ?? .newtab) + firstTab.setContent(url.map { .contentFromURL($0, source: source) } ?? .newtab) } else if let tab = tabCollectionViewModel.selectedTabViewModel?.tab, !newTab { - tab.setContent(url.map { .url($0, source: source) } ?? .newtab) + tab.setContent(url.map { .contentFromURL($0, source: source) } ?? .newtab) } else { let newTab = Tab(content: url.map { .url($0, source: source) } ?? .newtab, shouldLoadInBackground: true, burnerMode: tabCollectionViewModel.burnerMode) - newTab.setContent(url.map { .url($0, source: source) } ?? .newtab) + newTab.setContent(url.map { .contentFromURL($0, source: source) } ?? .newtab) tabCollectionViewModel.append(tab: newTab) } } diff --git a/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppDelegate.swift b/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppDelegate.swift index 5e3bf42951..0a3e777138 100644 --- a/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppDelegate.swift +++ b/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppDelegate.swift @@ -72,15 +72,43 @@ final class DuckDuckGoDBPBackgroundAgentApplication: NSApplication { required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + } @main final class DuckDuckGoDBPBackgroundAgentAppDelegate: NSObject, NSApplicationDelegate { + private let settings = DataBrokerProtectionSettings() + private var cancellables = Set() + private var statusBarMenu: StatusBarMenu? + @MainActor func applicationDidFinishLaunching(_ aNotification: Notification) { os_log("DuckDuckGoAgent started", log: .dbpBackgroundAgent, type: .info) let manager = DataBrokerProtectionBackgroundManager.shared manager.runOperationsAndStartSchedulerIfPossible() + + setupStatusBarMenu() + } + + @MainActor + private func setupStatusBarMenu() { + statusBarMenu = StatusBarMenu() + + if settings.showInMenuBar { + statusBarMenu?.show() + } else { + statusBarMenu?.hide() + } + + settings.showInMenuBarPublisher.sink { [weak self] showInMenuBar in + Task { @MainActor in + if showInMenuBar { + self?.statusBarMenu?.show() + } else { + self?.statusBarMenu?.hide() + } + } + }.store(in: &cancellables) } } diff --git a/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift b/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift index 306af54cae..33a1fc1d77 100644 --- a/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift +++ b/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift @@ -66,12 +66,12 @@ extension IPCServiceManager: IPCServerInterface { } func startScheduler(showWebView: Bool) { - pixelHandler.fire(.ipcServerStartScheduler) + pixelHandler.fire(.ipcServerStartSchedulerReceivedByAgent) scheduler.startScheduler(showWebView: showWebView) } func stopScheduler() { - pixelHandler.fire(.ipcServerStopScheduler) + pixelHandler.fire(.ipcServerStopSchedulerReceivedByAgent) scheduler.stopScheduler() } @@ -84,11 +84,20 @@ extension IPCServiceManager: IPCServerInterface { } } - func scanAllBrokers(showWebView: Bool, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { - pixelHandler.fire(.ipcServerScanAllBrokers) - scheduler.scanAllBrokers(showWebView: showWebView) { errors in - self.pixelHandler.fire(.ipcServerScanAllBrokersCompletion(error: errors?.oneTimeError)) + func startManualScan(showWebView: Bool, + completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { + pixelHandler.fire(.ipcServerScanAllBrokersReceivedByAgent) + scheduler.startManualScan(showWebView: showWebView) { errors in + if let error = errors?.oneTimeError { + switch error { + case DataBrokerProtectionSchedulerError.operationsInterrupted: + self.pixelHandler.fire(.ipcServerScanAllBrokersInterruptedOnAgent) + default: + self.pixelHandler.fire(.ipcServerScanAllBrokersCompletedOnAgentWithError(error: error)) + } + } else { + self.pixelHandler.fire(.ipcServerScanAllBrokersCompletedOnAgentWithoutError) + } completion(errors) } } diff --git a/DuckDuckGoVPN/TunnelControllerIPCService.swift b/DuckDuckGoVPN/TunnelControllerIPCService.swift index 971dc24522..b88c3f20b8 100644 --- a/DuckDuckGoVPN/TunnelControllerIPCService.swift +++ b/DuckDuckGoVPN/TunnelControllerIPCService.swift @@ -127,6 +127,19 @@ extension TunnelControllerIPCService: IPCServerInterface { completion(nil) } + func fetchLastError(completion: @escaping (Error?) -> Void) { + Task { + guard #available(macOS 13.0, *), + let connection = await tunnelController.connection else { + + completion(nil) + return + } + + connection.fetchLastDisconnectError(completionHandler: completion) + } + } + func resetAll(uninstallSystemExtension: Bool) async { try? await networkExtensionController.deactivateSystemExtension() } diff --git a/IntegrationTests/NavigationProtection/NavigationProtectionIntegrationTests.swift b/IntegrationTests/NavigationProtection/NavigationProtectionIntegrationTests.swift index f06aa2ade3..9cbc7831bf 100644 --- a/IntegrationTests/NavigationProtection/NavigationProtectionIntegrationTests.swift +++ b/IntegrationTests/NavigationProtection/NavigationProtectionIntegrationTests.swift @@ -77,7 +77,7 @@ class NavigationProtectionIntegrationTests: XCTestCase { for i in 0.. private let vault: (any DataBrokerProtectionSecureVault)? - private let secureVaultErrorReporter: SecureVaultErrorReporting? + private let secureVaultErrorReporter: SecureVaultReporting? init(fakeBrokerFlag: DataBrokerDebugFlag = DataBrokerDebugFlagFakeBroker(), pixelHandler: EventMapping, vault: (any DataBrokerProtectionSecureVault)? = nil, - secureVaultErrorReporter: SecureVaultErrorReporting? = DataBrokerProtectionSecureVaultErrorReporter.shared) { + secureVaultErrorReporter: SecureVaultReporting? = DataBrokerProtectionSecureVaultErrorReporter.shared) { self.fakeBrokerFlag = fakeBrokerFlag self.pixelHandler = pixelHandler self.vault = vault @@ -69,7 +69,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func save(_ profile: DataBrokerProtectionProfile) async throws { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) if try vault.fetchProfile(with: Self.profileId) != nil { try await updateProfile(profile, vault: vault) @@ -85,7 +85,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { public func fetchProfile() throws -> DataBrokerProtectionProfile? { do { - let vault = try DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) + let vault = try DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) return try vault.fetchProfile(with: Self.profileId) } catch { os_log("Database error: fetchProfile, error: %{public}@", log: .error, error.localizedDescription) @@ -96,7 +96,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { public func deleteProfileData() throws { do { - let vault = try DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) + let vault = try DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) try vault.deleteProfileData() } catch { os_log("Database error: deleteProfileData, error: %{public}@", log: .error, error.localizedDescription) @@ -107,7 +107,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func fetchChildBrokers(for parentBroker: String) throws -> [DataBroker] { do { - let vault = try DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) + let vault = try DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) return try vault.fetchChildBrokers(for: parentBroker) } catch { os_log("Database error: fetchChildBrokers, error: %{public}@", log: .error, error.localizedDescription) @@ -118,7 +118,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func save(_ extractedProfile: ExtractedProfile, brokerId: Int64, profileQueryId: Int64) throws -> Int64 { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) return try vault.save(extractedProfile: extractedProfile, brokerId: brokerId, profileQueryId: profileQueryId) } catch { @@ -130,7 +130,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func brokerProfileQueryData(for brokerId: Int64, and profileQueryId: Int64) throws -> BrokerProfileQueryData? { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) guard let broker = try vault.fetchBroker(with: brokerId), let profileQuery = try vault.fetchProfileQuery(with: profileQueryId), let scanOperation = try vault.fetchScan(brokerId: brokerId, profileQueryId: profileQueryId) else { @@ -157,7 +157,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func fetchExtractedProfiles(for brokerId: Int64) throws -> [ExtractedProfile] { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) return try vault.fetchExtractedProfiles(for: brokerId) } catch { os_log("Database error: fetchExtractedProfiles, error: %{public}@", log: .error, error.localizedDescription) @@ -168,7 +168,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func updatePreferredRunDate(_ date: Date?, brokerId: Int64, profileQueryId: Int64) throws { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) try vault.updatePreferredRunDate(date, brokerId: brokerId, profileQueryId: profileQueryId) } catch { os_log("Database error: updatePreferredRunDate without extractedProfileID, error: %{public}@", log: .error, error.localizedDescription) @@ -179,7 +179,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func updatePreferredRunDate(_ date: Date?, brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64) throws { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) try vault.updatePreferredRunDate( date, @@ -195,7 +195,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func updateLastRunDate(_ date: Date?, brokerId: Int64, profileQueryId: Int64) throws { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) try vault.updateLastRunDate(date, brokerId: brokerId, profileQueryId: profileQueryId) } catch { os_log("Database error: updateLastRunDate without extractedProfileID, error: %{public}@", log: .error, error.localizedDescription) @@ -206,7 +206,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func updateLastRunDate(_ date: Date?, brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64) throws { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) try vault.updateLastRunDate( date, @@ -223,7 +223,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func updateRemovedDate(_ date: Date?, on extractedProfileId: Int64) throws { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) try vault.updateRemovedDate(for: extractedProfileId, with: date) } catch { os_log("Database error: updateRemovedDate, error: %{public}@", log: .error, error.localizedDescription) @@ -234,7 +234,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func add(_ historyEvent: HistoryEvent) throws { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) if let extractedProfileId = historyEvent.extractedProfileId { try vault.save(historyEvent: historyEvent, brokerId: historyEvent.brokerId, profileQueryId: historyEvent.profileQueryId, extractedProfileId: extractedProfileId) @@ -250,7 +250,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func fetchAllBrokerProfileQueryData() throws -> [BrokerProfileQueryData] { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) let brokers = try vault.fetchAllBrokers() let profileQueries = try vault.fetchAllProfileQueries(for: Self.profileId) var brokerProfileQueryDataList = [BrokerProfileQueryData]() @@ -283,7 +283,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func saveOptOutOperation(optOut: OptOutOperationData, extractedProfile: ExtractedProfile) throws { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) try vault.save(brokerId: optOut.brokerId, profileQueryId: optOut.profileQueryId, @@ -299,7 +299,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func fetchLastEvent(brokerId: Int64, profileQueryId: Int64) throws -> HistoryEvent? { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) let events = try vault.fetchEvents(brokerId: brokerId, profileQueryId: profileQueryId) return events.max(by: { $0.date < $1.date }) @@ -312,7 +312,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func hasMatches() throws -> Bool { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) return try vault.hasMatches() } catch { os_log("Database error: hasMatches, error: %{public}@", log: .error, error.localizedDescription) @@ -323,7 +323,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func fetchScanHistoryEvents(brokerId: Int64, profileQueryId: Int64) throws -> [HistoryEvent] { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: nil) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: nil) guard let scan = try vault.fetchScan(brokerId: brokerId, profileQueryId: profileQueryId) else { return [HistoryEvent]() } @@ -338,7 +338,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func fetchOptOutHistoryEvents(brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64) throws -> [HistoryEvent] { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: nil) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: nil) guard let optOut = try vault.fetchOptOut(brokerId: brokerId, profileQueryId: profileQueryId, extractedProfileId: extractedProfileId) else { return [HistoryEvent]() } @@ -353,7 +353,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func fetchAttemptInformation(for extractedProfileId: Int64) throws -> AttemptInformation? { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) return try vault.fetchAttemptInformation(for: extractedProfileId) } catch { os_log("Database error: fetchAttemptInformation, error: %{public}@", log: .error, error.localizedDescription) @@ -364,7 +364,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func addAttempt(extractedProfileId: Int64, attemptUUID: UUID, dataBroker: String, lastStageDate: Date, startTime: Date) throws { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) try vault.save(extractedProfileId: extractedProfileId, attemptUUID: attemptUUID, dataBroker: dataBroker, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionSecureVaultErrorReporter.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionSecureVaultErrorReporter.swift index 21cb003862..86421908fd 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionSecureVaultErrorReporter.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionSecureVaultErrorReporter.swift @@ -22,7 +22,7 @@ import SecureStorage import PixelKit import Common -final class DataBrokerProtectionSecureVaultErrorReporter: SecureVaultErrorReporting { +final class DataBrokerProtectionSecureVaultErrorReporter: SecureVaultReporting { static let shared = DataBrokerProtectionSecureVaultErrorReporter(pixelHandler: DataBrokerProtectionPixelsHandler()) let pixelHandler: EventMapping @@ -30,7 +30,7 @@ final class DataBrokerProtectionSecureVaultErrorReporter: SecureVaultErrorReport self.pixelHandler = pixelHandler } - func secureVaultInitFailed(_ error: SecureStorageError) { + func secureVaultError(_ error: SecureStorageError) { switch error { case .initFailed, .failedToOpenDatabase: pixelHandler.fire(.secureVaultInitError(error: error)) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift index 456b2930fa..f7b809ac9f 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift @@ -27,6 +27,11 @@ public protocol IPCClientInterface: AnyObject { func schedulerStatusChanges(_ status: DataBrokerProtectionSchedulerStatus) } +public protocol DBPLoginItemStatusChecker { + func doesHaveNecessaryPermissions() -> Bool + func isInCorrectDirectory() -> Bool +} + /// This is the XPC interface with parameters that can be packed properly @objc protocol XPCClientInterface: NSObjectProtocol { @@ -36,6 +41,7 @@ protocol XPCClientInterface: NSObjectProtocol { public final class DataBrokerProtectionIPCClient: NSObject { private let pixelHandler: EventMapping + private let loginItemStatusChecker: DBPLoginItemStatusChecker // MARK: - XPC Communication @@ -52,8 +58,9 @@ public final class DataBrokerProtectionIPCClient: NSObject { // MARK: - Initializers - public init(machServiceName: String, pixelHandler: EventMapping) { + public init(machServiceName: String, pixelHandler: EventMapping, loginItemStatusChecker: DBPLoginItemStatusChecker) { self.pixelHandler = pixelHandler + self.loginItemStatusChecker = loginItemStatusChecker let clientInterface = NSXPCInterface(with: XPCClientInterface.self) let serverInterface = NSXPCInterface(with: XPCServerInterface.self) @@ -94,20 +101,22 @@ extension DataBrokerProtectionIPCClient: IPCServerInterface { } public func startScheduler(showWebView: Bool) { - self.pixelHandler.fire(.ipcServerStartScheduler) + self.pixelHandler.fire(.ipcServerStartSchedulerCalledByApp) xpc.execute(call: { server in server.startScheduler(showWebView: showWebView) - }, xpcReplyErrorHandler: { _ in + }, xpcReplyErrorHandler: { error in + self.pixelHandler.fire(.ipcServerStartSchedulerXPCError(error: error)) // Intentional no-op as there's no completion block // If you add a completion block, please remember to call it here too! }) } public func stopScheduler() { - self.pixelHandler.fire(.ipcServerStopScheduler) + self.pixelHandler.fire(.ipcServerStopSchedulerCalledByApp) xpc.execute(call: { server in server.stopScheduler() - }, xpcReplyErrorHandler: { _ in + }, xpcReplyErrorHandler: { error in + self.pixelHandler.fire(.ipcServerStopSchedulerXPCError(error: error)) // Intentional no-op as there's no completion block // If you add a completion block, please remember to call it here too! }) @@ -127,16 +136,42 @@ extension DataBrokerProtectionIPCClient: IPCServerInterface { }) } - public func scanAllBrokers(showWebView: Bool, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { - self.pixelHandler.fire(.ipcServerScanAllBrokers) + public func startManualScan(showWebView: Bool, + completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { + self.pixelHandler.fire(.ipcServerScanAllBrokersCalledByApp) + + guard loginItemStatusChecker.doesHaveNecessaryPermissions() else { + self.pixelHandler.fire(.ipcServerScanAllBrokersAttemptedToCallWithoutLoginItemPermissions) + let errors = DataBrokerProtectionSchedulerErrorCollection(oneTimeError: DataBrokerProtectionSchedulerError.loginItemDoesNotHaveNecessaryPermissions) + completion(errors) + return + } + + guard loginItemStatusChecker.isInCorrectDirectory() else { + self.pixelHandler.fire(.ipcServerScanAllBrokersAttemptedToCallInWrongDirectory) + let errors = DataBrokerProtectionSchedulerErrorCollection(oneTimeError: DataBrokerProtectionSchedulerError.appInWrongDirectory) + completion(errors) + return + } + xpc.execute(call: { server in - server.scanAllBrokers(showWebView: showWebView) { errors in - self.pixelHandler.fire(.ipcServerScanAllBrokersCompletion(error: errors?.oneTimeError)) + server.startManualScan(showWebView: showWebView) { errors in + if let error = errors?.oneTimeError { + let nsError = error as NSError + let interruptedError = DataBrokerProtectionSchedulerError.operationsInterrupted as NSError + if nsError.domain == interruptedError.domain, + nsError.code == interruptedError.code { + self.pixelHandler.fire(.ipcServerScanAllBrokersCompletionCalledOnAppAfterInterruption) + } else { + self.pixelHandler.fire(.ipcServerScanAllBrokersCompletionCalledOnAppWithError(error: error)) + } + } else { + self.pixelHandler.fire(.ipcServerScanAllBrokersCompletionCalledOnAppWithoutError) + } completion(errors) } }, xpcReplyErrorHandler: { error in - self.pixelHandler.fire(.ipcServerScanAllBrokersCompletion(error: error)) + self.pixelHandler.fire(.ipcServerScanAllBrokersXPCError(error: error)) completion(DataBrokerProtectionSchedulerErrorCollection(oneTimeError: error)) }) } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCScheduler.swift index cfb11f5187..0763404514 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCScheduler.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCScheduler.swift @@ -51,10 +51,10 @@ public final class DataBrokerProtectionIPCScheduler: DataBrokerProtectionSchedul ipcClient.optOutAllBrokers(showWebView: showWebView, completion: completion) } - public func scanAllBrokers(showWebView: Bool, - completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { + public func startManualScan(showWebView: Bool, + completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { let completion = completion ?? { _ in } - ipcClient.scanAllBrokers(showWebView: showWebView, completion: completion) + ipcClient.startManualScan(showWebView: showWebView, completion: completion) } public func runQueuedOperations(showWebView: Bool, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift index 9027e1d275..dce168b8fe 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift @@ -94,8 +94,8 @@ public protocol IPCServerInterface: AnyObject { func optOutAllBrokers(showWebView: Bool, completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) - func scanAllBrokers(showWebView: Bool, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) + func startManualScan(showWebView: Bool, + completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) func runQueuedOperations(showWebView: Bool, completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) func runAllOperations(showWebView: Bool) @@ -135,8 +135,8 @@ protocol XPCServerInterface { func optOutAllBrokers(showWebView: Bool, completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) - func scanAllBrokers(showWebView: Bool, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) + func startManualScan(showWebView: Bool, + completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) func runQueuedOperations(showWebView: Bool, completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) func runAllOperations(showWebView: Bool) @@ -213,9 +213,9 @@ extension DataBrokerProtectionIPCServer: XPCServerInterface { serverDelegate?.optOutAllBrokers(showWebView: showWebView, completion: completion) } - func scanAllBrokers(showWebView: Bool, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { - serverDelegate?.scanAllBrokers(showWebView: showWebView, completion: completion) + func startManualScan(showWebView: Bool, + completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { + serverDelegate?.startManualScan(showWebView: showWebView, completion: completion) } func runQueuedOperations(showWebView: Bool, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUIViewModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUIViewModel.swift index 3ca4eeb399..df3cf6fd82 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUIViewModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUIViewModel.swift @@ -75,7 +75,7 @@ final class DBPUIViewModel { extension DBPUIViewModel: DBPUIScanOps { func startScan() -> Bool { - scheduler.scanAllBrokers() + scheduler.startManualScan() return true } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift index 78b76af560..81e3859c17 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift @@ -192,8 +192,6 @@ final class DataBrokerOperationsCollection: Operation { try await Task.sleep(nanoseconds: UInt64(sleepInterval) * 1_000_000_000) } - finish() - } catch { os_log("Error: %{public}@", log: .dataBrokerProtection, error.localizedDescription) self.error = error @@ -203,6 +201,8 @@ final class DataBrokerOperationsCollection: Operation { withDataBrokerName: brokerProfileQueriesData.first?.dataBroker.name) } } + + finish() } private func finish() { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift index 79020cde0b..9128f87178 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift @@ -118,7 +118,7 @@ public struct DataBrokerProtectionBrokerUpdater { } public static func provide() -> DataBrokerProtectionBrokerUpdater? { - if let vault = try? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: nil) { + if let vault = try? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: nil) { return DataBrokerProtectionBrokerUpdater(vault: vault) } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift index daa9194f41..d9de1179a7 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift @@ -100,12 +100,29 @@ public enum DataBrokerProtectionPixels { case backgroundAgentRunOperationsAndStartSchedulerIfPossibleRunQueuedOperationsCallbackStartScheduler // IPC server events - case ipcServerStartScheduler - case ipcServerStopScheduler + case ipcServerStartSchedulerCalledByApp + case ipcServerStartSchedulerReceivedByAgent + case ipcServerStartSchedulerXPCError(error: Error?) + + case ipcServerStopSchedulerCalledByApp + case ipcServerStopSchedulerReceivedByAgent + case ipcServerStopSchedulerXPCError(error: Error?) + + case ipcServerScanAllBrokersAttemptedToCallWithoutLoginItemPermissions + case ipcServerScanAllBrokersAttemptedToCallInWrongDirectory + case ipcServerScanAllBrokersCalledByApp + case ipcServerScanAllBrokersReceivedByAgent + case ipcServerScanAllBrokersXPCError(error: Error?) + + case ipcServerScanAllBrokersCompletedOnAgentWithoutError + case ipcServerScanAllBrokersCompletedOnAgentWithError(error: Error?) + case ipcServerScanAllBrokersCompletionCalledOnAppWithoutError + case ipcServerScanAllBrokersCompletionCalledOnAppWithError(error: Error?) + case ipcServerScanAllBrokersInterruptedOnAgent + case ipcServerScanAllBrokersCompletionCalledOnAppAfterInterruption + case ipcServerOptOutAllBrokers case ipcServerOptOutAllBrokersCompletion(error: Error?) - case ipcServerScanAllBrokers - case ipcServerScanAllBrokersCompletion(error: Error?) case ipcServerRunQueuedOperations case ipcServerRunQueuedOperationsCompletion(error: Error?) case ipcServerRunAllOperations @@ -144,6 +161,13 @@ public enum DataBrokerProtectionPixels { // Backend service errors case generateEmailHTTPErrorDaily(statusCode: Int, environment: String, wasOnWaitlist: Bool) case emptyAccessTokenDaily(environment: String, wasOnWaitlist: Bool, callSite: BackendServiceCallSite) + + // Home View + case homeViewShowNoPermissionError + case homeViewShowWebUI + case homeViewShowBadPathError + case homeViewCTAMoveApplicationClicked + case homeViewCTAGrantPermissionClicked } extension DataBrokerProtectionPixels: PixelKitEvent { @@ -187,12 +211,28 @@ extension DataBrokerProtectionPixels: PixelKitEvent { case .backgroundAgentRunOperationsAndStartSchedulerIfPossibleNoSavedProfile: return "m_mac_dbp_background-agent-run-operations-and-start-scheduler-if-possible_no-saved-profile" case .backgroundAgentRunOperationsAndStartSchedulerIfPossibleRunQueuedOperationsCallbackStartScheduler: return "m_mac_dbp_background-agent-run-operations-and-start-scheduler-if-possible_callback_start-scheduler" - case .ipcServerStartScheduler: return "m_mac_dbp_ipc-server_start-scheduler" - case .ipcServerStopScheduler: return "m_mac_dbp_ipc-server_stop-scheduler" + case .ipcServerStartSchedulerCalledByApp: return "m_mac_dbp_ipc-server_start-scheduler_called-by-app" + case .ipcServerStartSchedulerReceivedByAgent: return "m_mac_dbp_ipc-server_start-scheduler_received-by-agent" + case .ipcServerStartSchedulerXPCError: return "m_mac_dbp_ipc-server_start-scheduler_xpc-error" + + case .ipcServerStopSchedulerCalledByApp: return "m_mac_dbp_ipc-server_stop-scheduler_called-by-app" + case .ipcServerStopSchedulerReceivedByAgent: return "m_mac_dbp_ipc-server_stop-scheduler_received-by-agent" + case .ipcServerStopSchedulerXPCError: return "m_mac_dbp_ipc-server_stop-scheduler_xpc-error" + + case .ipcServerScanAllBrokersAttemptedToCallWithoutLoginItemPermissions: return "m_mac_dbp_ipc-server_scan-all-brokers_attempted-to-call-without-login-item-permissions" + case .ipcServerScanAllBrokersAttemptedToCallInWrongDirectory: return "m_mac_dbp_ipc-server_scan-all-brokers_attempted-to-call-in-wrong-directory" + case .ipcServerScanAllBrokersCalledByApp: return "m_mac_dbp_ipc-server_scan-all-brokers_called-by-app" + case .ipcServerScanAllBrokersReceivedByAgent: return "m_mac_dbp_ipc-server_scan-all-brokers_received-by-agent" + case .ipcServerScanAllBrokersXPCError: return "m_mac_dbp_ipc-server_scan-all-brokers_xpc-error" + case .ipcServerScanAllBrokersCompletedOnAgentWithoutError: return "m_mac_dbp_ipc-server_scan-all-brokers_completed-on-agent_without-error" + case .ipcServerScanAllBrokersCompletedOnAgentWithError: return "m_mac_dbp_ipc-server_scan-all-brokers_completed-on-agent_with-error" + case .ipcServerScanAllBrokersCompletionCalledOnAppWithoutError: return "m_mac_dbp_ipc-server_scan-all-brokers_completion-called-on-app_without-error" + case .ipcServerScanAllBrokersCompletionCalledOnAppWithError: return "m_mac_dbp_ipc-server_scan-all-brokers_completion-called-on-app_with-error" + case .ipcServerScanAllBrokersInterruptedOnAgent: return "m_mac_dbp_ipc-server_scan-all-brokers_interrupted-on-agent" + case .ipcServerScanAllBrokersCompletionCalledOnAppAfterInterruption: return "m_mac_dbp_ipc-server_scan-all-brokers_completion-called-on-app_after-interruption" + case .ipcServerOptOutAllBrokers: return "m_mac_dbp_ipc-server_opt-out-all-brokers" case .ipcServerOptOutAllBrokersCompletion: return "m_mac_dbp_ipc-server_opt-out-all-brokers_completion" - case .ipcServerScanAllBrokers: return "m_mac_dbp_ipc-server_scan-all-brokers" - case .ipcServerScanAllBrokersCompletion: return "m_mac_dbp_ipc-server_scan-all-brokers_completion" case .ipcServerRunQueuedOperations: return "m_mac_dbp_ipc-server_run-queued-operations" case .ipcServerRunQueuedOperationsCompletion: return "m_mac_dbp_ipc-server_run-queued-operations_completion" case .ipcServerRunAllOperations: return "m_mac_dbp_ipc-server_run-all-operations" @@ -232,6 +272,13 @@ extension DataBrokerProtectionPixels: PixelKitEvent { // Backend service errors case .generateEmailHTTPErrorDaily: return "m_mac_dbp_service_email-generate-http-error" case .emptyAccessTokenDaily: return "m_mac_dbp_service_empty-auth-token" + + // Home View + case .homeViewShowNoPermissionError: return "m_mac_dbp_home_view_show-no-permission-error" + case .homeViewShowWebUI: return "m_mac_dbp_home_view_show-web-ui" + case .homeViewShowBadPathError: return "m_mac_dbp_home_view_show-bad-path-error" + case .homeViewCTAMoveApplicationClicked: return "m_mac_dbp_home_view-cta-move-application-clicked" + case .homeViewCTAGrantPermissionClicked: return "m_mac_dbp_home_view-cta-grant-permission-clicked" } } @@ -324,16 +371,34 @@ extension DataBrokerProtectionPixels: PixelKitEvent { .scanningEventNewMatch, .scanningEventReAppearance, + .homeViewShowNoPermissionError, + .homeViewShowWebUI, + .homeViewShowBadPathError, + .homeViewCTAMoveApplicationClicked, + .homeViewCTAGrantPermissionClicked, .secureVaultInitError, .secureVaultError: return [:] - case .ipcServerStartScheduler, - .ipcServerStopScheduler, + case .ipcServerStartSchedulerCalledByApp, + .ipcServerStartSchedulerReceivedByAgent, + .ipcServerStartSchedulerXPCError, + .ipcServerStopSchedulerCalledByApp, + .ipcServerStopSchedulerReceivedByAgent, + .ipcServerStopSchedulerXPCError, + .ipcServerScanAllBrokersAttemptedToCallWithoutLoginItemPermissions, + .ipcServerScanAllBrokersAttemptedToCallInWrongDirectory, + .ipcServerScanAllBrokersCalledByApp, + .ipcServerScanAllBrokersReceivedByAgent, + .ipcServerScanAllBrokersXPCError, + .ipcServerScanAllBrokersCompletedOnAgentWithoutError, + .ipcServerScanAllBrokersCompletedOnAgentWithError, + .ipcServerScanAllBrokersCompletionCalledOnAppWithoutError, + .ipcServerScanAllBrokersCompletionCalledOnAppWithError, + .ipcServerScanAllBrokersInterruptedOnAgent, + .ipcServerScanAllBrokersCompletionCalledOnAppAfterInterruption, .ipcServerOptOutAllBrokers, .ipcServerOptOutAllBrokersCompletion, - .ipcServerScanAllBrokers, - .ipcServerScanAllBrokersCompletion, .ipcServerRunQueuedOperations, .ipcServerRunQueuedOperationsCompletion, .ipcServerRunAllOperations: @@ -373,10 +438,27 @@ public class DataBrokerProtectionPixelsHandler: EventMapping Void)?) { } - func runQueuedOperations(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { } - func scanAllBrokers(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { } + func runQueuedOperations(showWebView: Bool, + completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { } + func startManualScan(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { } func runAllOperations(showWebView: Bool) { } func getDebugMetadata(completion: (DBPBackgroundAgentMetadata?) -> Void) { } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift index f10ca642e5..7465df1771 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift @@ -55,8 +55,8 @@ final class DataBrokerProtectionProcessor { } // MARK: - Public functions - func runAllScanOperations(showWebView: Bool = false, - completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil) { + func startManualScans(showWebView: Bool = false, + completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil) { operationQueue.cancelAllOperations() runOperations(operationType: .scan, priorityDate: nil, @@ -114,7 +114,7 @@ final class DataBrokerProtectionProcessor { completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { // Before running new operations we check if there is any updates to the broker files. - if let vault = try? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: nil) { + if let vault = try? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: nil) { let brokerUpdater = DataBrokerProtectionBrokerUpdater(vault: vault, pixelHandler: pixelHandler) brokerUpdater.checkForUpdatesInBrokerJSONFiles() } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift index fdd8137ba9..e7a15bd1bb 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift @@ -27,6 +27,12 @@ public enum DataBrokerProtectionSchedulerStatus: Codable { case running } +public enum DataBrokerProtectionSchedulerError: Error { + case loginItemDoesNotHaveNecessaryPermissions + case appInWrongDirectory + case operationsInterrupted +} + @objc public class DataBrokerProtectionSchedulerErrorCollection: NSObject, NSSecureCoding { /* @@ -74,7 +80,7 @@ public protocol DataBrokerProtectionScheduler { func stopScheduler() func optOutAllBrokers(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) - func scanAllBrokers(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) + func startManualScan(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) func runQueuedOperations(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) func runAllOperations(showWebView: Bool) @@ -92,8 +98,8 @@ extension DataBrokerProtectionScheduler { runAllOperations(showWebView: false) } - public func scanAllBrokers() { - scanAllBrokers(showWebView: false, completion: nil) + public func startManualScan() { + startManualScan(showWebView: false, completion: nil) } } @@ -243,19 +249,21 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch } - public func scanAllBrokers(showWebView: Bool = false, - completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil) { + public func startManualScan(showWebView: Bool = false, + completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil) { stopScheduler() userNotificationService.requestNotificationPermission() os_log("Scanning all brokers...", log: .dataBrokerProtection) - dataBrokerProcessor.runAllScanOperations(showWebView: showWebView) { [weak self] errors in + dataBrokerProcessor.startManualScans(showWebView: showWebView) { [weak self] errors in guard let self = self else { return } self.startScheduler(showWebView: showWebView) - self.userNotificationService.sendFirstScanCompletedNotification() + if errors?.oneTimeError == nil { + self.userNotificationService.sendFirstScanCompletedNotification() + } if let hasMatches = try? self.dataManager.hasMatches(), hasMatches { @@ -264,12 +272,17 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch if let errors = errors { if let oneTimeError = errors.oneTimeError { - os_log("Error during DefaultDataBrokerProtectionScheduler.scanAllBrokers in dataBrokerProcessor.runAllScanOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) - self.pixelHandler.fire(.generalError(error: oneTimeError, functionOccurredIn: "DefaultDataBrokerProtectionScheduler.scanAllBrokers")) + switch oneTimeError { + case DataBrokerProtectionSchedulerError.operationsInterrupted: + os_log("Interrupted during DefaultDataBrokerProtectionScheduler.startManualScan in dataBrokerProcessor.runAllScanOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) + default: + os_log("Error during DefaultDataBrokerProtectionScheduler.startManualScan in dataBrokerProcessor.runAllScanOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) + self.pixelHandler.fire(.generalError(error: oneTimeError, functionOccurredIn: "DefaultDataBrokerProtectionScheduler.startManualScan")) + } } if let operationErrors = errors.operationErrors, operationErrors.count != 0 { - os_log("Operation error(s) during DefaultDataBrokerProtectionScheduler.scanAllBrokers in dataBrokerProcessor.runAllScanOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) + os_log("Operation error(s) during DefaultDataBrokerProtectionScheduler.startManualScan in dataBrokerProcessor.runAllScanOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/StatusItem/StatusBarMenu.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/StatusItem/StatusBarMenu.swift new file mode 100644 index 0000000000..369a42ee56 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/StatusItem/StatusBarMenu.swift @@ -0,0 +1,71 @@ +// +// StatusBarMenu.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 AppKit + +public final class StatusBarMenu: NSObject { + private let statusItem: NSStatusItem + private let popover: StatusBarPopover + + public override init() { + statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) + popover = StatusBarPopover() + popover.behavior = .transient + super.init() + + setupStatusItem() + } + + @objc + private func statusBarButtonTapped() { + togglePopover() + } + + private func setupStatusItem() { + statusItem.button?.target = self + statusItem.button?.image = NSImage(systemSymbolName: NSImage.Name("person.crop.circle.badge.minus"), accessibilityDescription: nil) + statusItem.button?.action = #selector(statusBarButtonTapped) + statusItem.button?.sendAction(on: [.leftMouseUp]) + } + + // MARK: - Popover + + private func togglePopover() { + if popover.isShown { + popover.close() + } else { + guard let button = statusItem.button else { + return + } + + popover.show(relativeTo: button.bounds, of: button, preferredEdge: .maxY) + popover.contentViewController?.view.window?.makeKey() + } + } + + // MARK: - Showing & Hiding the menu + + public func show() { + statusItem.isVisible = true + } + + public func hide() { + statusItem.isVisible = false + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/StatusItem/StatusBarMenuDebugInfoViewModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/StatusItem/StatusBarMenuDebugInfoViewModel.swift new file mode 100644 index 0000000000..9f3c15478c --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/StatusItem/StatusBarMenuDebugInfoViewModel.swift @@ -0,0 +1,37 @@ +// +// StatusBarMenuDebugInfoViewModel.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 + +public final class StatusBarMenuDebugInfoViewModel: ObservableObject { + + var bundlePath: String + var version: String + + public init(bundle: Bundle = .main) { + bundlePath = bundle.bundlePath + + // swiftlint:disable:next force_cast + let shortVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String + + // swiftlint:disable:next force_cast + let buildNumber = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as! String + + version = shortVersion + " (build: " + buildNumber + ")" + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/StatusItem/StatusBarPopover.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/StatusItem/StatusBarPopover.swift new file mode 100644 index 0000000000..84cc931d30 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/StatusItem/StatusBarPopover.swift @@ -0,0 +1,43 @@ +// +// StatusBarPopover.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 AppKit +import SwiftUI + +public final class StatusBarPopover: NSPopover { + + public required override init() { + super.init() + + self.animates = false + self.behavior = .transient + + setupContentController() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupContentController() { + let controller = NSHostingController(rootView: StatusBarPopoverView(viewModel: StatusBarMenuDebugInfoViewModel())) + contentViewController = controller + controller.view.frame = CGRect(origin: .zero, size: controller.view.intrinsicContentSize) + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/StatusItem/StatusBarPopoverView.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/StatusItem/StatusBarPopoverView.swift new file mode 100644 index 0000000000..2e9199dc21 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/StatusItem/StatusBarPopoverView.swift @@ -0,0 +1,64 @@ +// +// StatusBarPopoverView.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 SwiftUI +import Combine + +struct StatusBarPopoverView: View { + let viewModel: StatusBarMenuDebugInfoViewModel + + var body: some View { + VStack(alignment: .center, spacing: 10) { + HStack { + Text("Personal Information Removal") + .font(.headline) + .foregroundColor(.primary) + Spacer() + } + + informationRow(title: "Version", details: viewModel.version) + informationRow(title: "Bundle Path", details: viewModel.bundlePath) + } + .padding() + .frame(width: 350, height: 200) + } + + private func informationRow(title: String, details: String) -> some View { + VStack(spacing: 2) { + HStack { + Text(title) + .font(.headline) + .foregroundColor(.primary) + Spacer() + } + HStack { + + Text(details) + .font(.body) + .foregroundColor(.secondary) + .makeSelectable() + .lineLimit(nil) + Spacer() + } + } + } +} + +#Preview { + StatusBarPopoverView(viewModel: StatusBarMenuDebugInfoViewModel()) +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionSecureVault.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionSecureVault.swift index aa0e13a95e..c854055aab 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionSecureVault.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionSecureVault.swift @@ -26,7 +26,7 @@ typealias DataBrokerProtectionVaultFactory = SecureVaultFactory( makeCryptoProvider: { return DataBrokerProtectionCryptoProvider() - }, makeKeyStoreProvider: { + }, makeKeyStoreProvider: { _ in return DataBrokerProtectionKeyStoreProvider() }, makeDatabaseProvider: { key in return try DefaultDataBrokerProtectionDatabaseProvider(key: key) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionSettings.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionSettings.swift index 1d731fd0f4..78203fa40f 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionSettings.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionSettings.swift @@ -17,6 +17,7 @@ // import Foundation +import Combine public final class DataBrokerProtectionSettings { private let defaults: UserDefaults @@ -45,6 +46,8 @@ public final class DataBrokerProtectionSettings { self.init(defaults: .dbp) } + // MARK: - Environment + public var selectedEnvironment: SelectedEnvironment { get { defaults.dataBrokerProtectionSelectedEnvironment @@ -54,6 +57,22 @@ public final class DataBrokerProtectionSettings { defaults.dataBrokerProtectionSelectedEnvironment = newValue } } + + // MARK: - Show in Menu Bar + + public var showInMenuBarPublisher: AnyPublisher { + defaults.networkProtectionSettingShowInMenuBarPublisher + } + + public var showInMenuBar: Bool { + get { + defaults.dataBrokerProtectionShowMenuBarIcon + } + + set { + defaults.dataBrokerProtectionShowMenuBarIcon = newValue + } + } } extension UserDefaults { @@ -61,6 +80,13 @@ extension UserDefaults { "dataBrokerProtectionSelectedEnvironmentRawValue" } + static let showMenuBarIconDefaultValue = false + private var showMenuBarIconKey: String { + "dataBrokerProtectionShowMenuBarIcon" + } + + // MARK: - Environment + @objc dynamic var dataBrokerProtectionSelectedEnvironmentRawValue: String { get { @@ -81,4 +107,25 @@ extension UserDefaults { dataBrokerProtectionSelectedEnvironmentRawValue = newValue.rawValue } } + + // MARK: - Show in Menu Bar + + @objc + dynamic var dataBrokerProtectionShowMenuBarIcon: Bool { + get { + value(forKey: showMenuBarIconKey) as? Bool ?? Self.showMenuBarIconDefaultValue + } + + set { + guard newValue != dataBrokerProtectionShowMenuBarIcon else { + return + } + + set(newValue, forKey: showMenuBarIconKey) + } + } + + var networkProtectionSettingShowInMenuBarPublisher: AnyPublisher { + publisher(for: \.dataBrokerProtectionShowMenuBarIcon).eraseToAnyPublisher() + } } diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index c08127a1af..761b9691ca 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,11 +31,11 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "138.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "141.1.1"), + .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems"), - .package(path: "../PixelKit"), ], targets: [ // MARK: - NetworkProtectionIPC @@ -45,6 +45,7 @@ let package = Package( dependencies: [ .product(name: "NetworkProtection", package: "BrowserServicesKit"), .product(name: "XPCHelper", package: "XPCHelper"), + .product(name: "PixelKit", package: "BrowserServicesKit"), ], swiftSettings: [ .define("DEBUG", .when(configuration: .debug)) @@ -57,7 +58,7 @@ let package = Package( name: "NetworkProtectionProxy", dependencies: [ .product(name: "NetworkProtection", package: "BrowserServicesKit"), - .product(name: "PixelKit", package: "PixelKit"), + .product(name: "PixelKit", package: "BrowserServicesKit"), ], swiftSettings: [ .define("DEBUG", .when(configuration: .debug)) @@ -70,9 +71,10 @@ let package = Package( name: "NetworkProtectionUI", dependencies: [ .product(name: "NetworkProtection", package: "BrowserServicesKit"), + .product(name: "PixelKit", package: "BrowserServicesKit"), .product(name: "SwiftUIExtensions", package: "SwiftUIExtensions"), .product(name: "LoginItems", package: "LoginItems"), - .product(name: "PixelKit", package: "PixelKit"), + .product(name: "Lottie", package: "lottie-spm") ], resources: [ .copy("Resources/Assets.xcassets") @@ -88,7 +90,7 @@ let package = Package( "NetworkProtectionUI", .product(name: "NetworkProtectionTestUtils", package: "BrowserServicesKit"), .product(name: "LoginItems", package: "LoginItems"), - .product(name: "PixelKitTestingUtilities", package: "PixelKit"), + .product(name: "PixelKitTestingUtilities", package: "BrowserServicesKit"), ] ), ] diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift index 61996e3945..a3aafbbe11 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift @@ -172,6 +172,12 @@ extension TunnelControllerIPCClient: IPCServerInterface { }, xpcReplyErrorHandler: completion) } + public func fetchLastError(completion: @escaping (Error?) -> Void) { + xpc.execute(call: { server in + server.fetchLastError(completion: completion) + }, xpcReplyErrorHandler: completion) + } + public func debugCommand(_ command: DebugCommand) async throws { guard let payload = try? JSONEncoder().encode(command) else { return diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCServer.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCServer.swift index 0d72a0d7ce..ef5a8d015a 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCServer.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCServer.swift @@ -45,6 +45,10 @@ public protocol IPCServerInterface: AnyObject { /// func stop(completion: @escaping (Error?) -> Void) + /// Fetches the last error directly from the tunnel manager. + /// + func fetchLastError(completion: @escaping (Error?) -> Void) + /// Debug commands /// func debugCommand(_ command: DebugCommand) async throws @@ -71,6 +75,10 @@ protocol XPCServerInterface { /// func stop(completion: @escaping (Error?) -> Void) + /// Fetches the last error directly from the tunnel manager. + /// + func fetchLastError(completion: @escaping (Error?) -> Void) + /// Debug commands /// func debugCommand(_ payload: Data, completion: @escaping (Error?) -> Void) @@ -174,6 +182,10 @@ extension TunnelControllerIPCServer: XPCServerInterface { serverDelegate?.stop(completion: completion) } + func fetchLastError(completion: @escaping (Error?) -> Void) { + serverDelegate?.fetchLastError(completion: completion) + } + func debugCommand(_ payload: Data, completion: @escaping (Error?) -> Void) { guard let command = try? JSONDecoder().decode(DebugCommand.self, from: payload) else { completion(IPCError.cannotDecodeDebugCommand) diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/PromptActionView/PromptActionView.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/PromptActionView/PromptActionView.swift index ab497e050b..372f25aa52 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/PromptActionView/PromptActionView.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/PromptActionView/PromptActionView.swift @@ -18,6 +18,7 @@ import Foundation import SwiftUI +import SwiftUIExtensions fileprivate extension View { func applyStepTitleAttributes() -> some View { diff --git a/LocalPackages/PixelKit/.gitignore b/LocalPackages/PixelKit/.gitignore deleted file mode 100644 index 3b29812086..0000000000 --- a/LocalPackages/PixelKit/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -.DS_Store -/.build -/Packages -/*.xcodeproj -xcuserdata/ -DerivedData/ -.swiftpm/config/registries.json -.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata -.netrc diff --git a/LocalPackages/PixelKit/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/LocalPackages/PixelKit/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003d..0000000000 --- a/LocalPackages/PixelKit/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/LocalPackages/PixelKit/Package.swift b/LocalPackages/PixelKit/Package.swift deleted file mode 100644 index f6ca4e0701..0000000000 --- a/LocalPackages/PixelKit/Package.swift +++ /dev/null @@ -1,50 +0,0 @@ -// swift-tools-version: 5.7 -// The swift-tools-version declares the minimum version of Swift required to build this package. - -import PackageDescription - -let package = Package( - name: "PixelKit", - platforms: [ - .macOS("11.4") - ], - products: [ - // Products define the executables and libraries a package produces, and make them visible to other packages. - .library( - name: "PixelKit", - targets: ["PixelKit"] - ), - .library( - name: "PixelKitTestingUtilities", - targets: ["PixelKitTestingUtilities"] - ) - ], - dependencies: [ - ], - targets: [ - .target( - name: "PixelKit", - dependencies: [ - ], - swiftSettings: [ - .define("DEBUG", .when(configuration: .debug)), - ] - ), - .testTarget( - name: "PixelKitTests", - dependencies: [ - "PixelKit", - "PixelKitTestingUtilities", - ], - swiftSettings: [ - .define("DEBUG", .when(configuration: .debug)) - ] - ), - .target( - name: "PixelKitTestingUtilities", - dependencies: [ - "PixelKit", - ] - ) - ] -) diff --git a/LocalPackages/PixelKit/README.md b/LocalPackages/PixelKit/README.md deleted file mode 100644 index 9d5094a7d8..0000000000 --- a/LocalPackages/PixelKit/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# PixelKit - -This package is meant to provide basic support for firing pixel across different targets. - -This package was designed to not really know specific pixels. Those can be defined -individually by each target importing this package, or through more specialized -shared packages. - -This design decision is meant to make PixelKit lean and to make it possible to use it -for future apps we may decide to make, without it having to carry over all of the business -domain logic for any single app. diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift deleted file mode 100644 index b692271709..0000000000 --- a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift +++ /dev/null @@ -1,137 +0,0 @@ -// -// PixelKit+Parameters.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 extension PixelKit { - - enum Parameters: Hashable { - public static let duration = "duration" - public static let test = "test" - public static let appVersion = "appVersion" - public static let pixelSource = "pixelSource" - public static let osMajorVersion = "osMajorVersion" - - public static let errorCode = "e" - public static let errorDomain = "d" - public static let errorCount = "c" - public static let errorSource = "error_source" - public static let sourceBrowserVersion = "source_browser_version" - public static let underlyingErrorCode = "ue" - public static let underlyingErrorDomain = "ud" - public static let underlyingErrorSQLiteCode = "sqlrc" - public static let underlyingErrorSQLiteExtendedCode = "sqlerc" - - public static let keychainFieldName = "fieldName" - public static let keychainErrorCode = "keychain_error_code" - - public static let emailCohort = "cohort" - public static let emailLastUsed = "duck_address_last_used" - - public static let assertionMessage = "message" - public static let assertionFile = "file" - public static let assertionLine = "line" - - public static let function = "function" - public static let line = "line" - - public static let latency = "latency" - public static let server = "server" - public static let networkType = "net_type" - - // Pixel experiments - public static let experimentCohort = "cohort" - - // Dashboard - public static let dashboardTriggerOrigin = "trigger_origin" - - // VPN - public static let vpnBreakageCategory = "breakageCategory" - public static let vpnBreakageDescription = "breakageDescription" - public static let vpnBreakageMetadata = "breakageMetadata" - - public static let reason = "reason" - - public static let vpnCohort = "cohort" - } - - enum Values { - public static let test = "1" - } - -} - -public protocol ErrorWithPixelParameters { - - var errorParameters: [String: String] { get } - -} - -public extension Error { - - var pixelParameters: [String: String] { - var params = [String: String]() - - if let errorWithUserInfo = self as? ErrorWithPixelParameters { - params = errorWithUserInfo.errorParameters - } - - let nsError = self as NSError - - params[PixelKit.Parameters.errorCode] = "\(nsError.code)" - params[PixelKit.Parameters.errorDomain] = nsError.domain - - let underlyingErrorParameters = self.underlyingErrorParameters(for: nsError) - params.merge(underlyingErrorParameters) { first, _ in - return first - } - - if let sqlErrorCode = nsError.userInfo["SQLiteResultCode"] as? NSNumber { - params[PixelKit.Parameters.underlyingErrorSQLiteCode] = "\(sqlErrorCode.intValue)" - } - - if let sqlExtendedErrorCode = nsError.userInfo["SQLiteExtendedResultCode"] as? NSNumber { - params[PixelKit.Parameters.underlyingErrorSQLiteExtendedCode] = "\(sqlExtendedErrorCode.intValue)" - } - - return params - } - - /// Recursive call to add underlying error information - /// - func underlyingErrorParameters(for nsError: NSError, level: Int = 0) -> [String: String] { - if let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] as? NSError { - let errorCodeParameterName = PixelKit.Parameters.underlyingErrorCode + (level == 0 ? "" : String(level + 1)) - let errorDomainParameterName = PixelKit.Parameters.underlyingErrorDomain + (level == 0 ? "" : String(level + 1)) - - let currentUnderlyingErrorParameters = [ - errorCodeParameterName: "\(underlyingError.code)", - errorDomainParameterName: underlyingError.domain - ] - - // Check if the underlying error has an underlying error of its own - let additionalParameters = underlyingErrorParameters(for: underlyingError, level: level + 1) - - return currentUnderlyingErrorParameters.merging(additionalParameters) { first, _ in - return first // Doesn't really matter as there should be no conflict of parameters - } - } - - return [:] - } -} diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift deleted file mode 100644 index d647b28835..0000000000 --- a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift +++ /dev/null @@ -1,476 +0,0 @@ -// -// 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 -import os.log // swiftlint:disable:this enforce_os_log_wrapper - -public final class PixelKit { - /// `true` if a request is fired, `false` otherwise - public typealias CompletionBlock = (Bool, Error?) -> Void - - /// The frequency with which a pixel is sent to our endpoint. - public enum Frequency { - /// The default frequency for pixels. This fires pixels with the event names as-is. - case standard - - /// [Legacy] Used in Pixel.fire(...) as .unique but without the `_u` requirement in the name - case legacyInitial - - /// Sent only once ever. The timestamp for this pixel is stored. - /// Note: This is the only pixel that MUST end with `_u`, Name for pixels of this type must end with if it doesn't an assertion is fired. - case unique - - /// [Legacy] Used in Pixel.fire(...) as .daily but without the `_d` automatically added to the name - case legacyDaily - - /// Sent once per day. The last timestamp for this pixel is stored and compared to the current date. Pixels of this type will have `_d` appended to their name. - case daily - - /// Sent once per day with a `_d` suffix, in addition to every time it is called with a `_c` suffix. - /// This means a pixel will get sent twice the first time it is called per-day, and subsequent calls that day will only send the `_c` variant. - /// This is useful in situations where pixels receive spikes in volume, as the daily pixel can be used to determine how many users are actually affected. - case dailyAndCount - - fileprivate var description: String { - switch self { - case .standard: - "Standard" - case .legacyInitial: - "Legacy Initial" - case .unique: - "Unique" - case .legacyDaily: - "Legacy Daily" - case .daily: - "Daily" - case .dailyAndCount: - "Daily and Count" - } - } - } - - public enum Header { - public static let acceptEncoding = "Accept-Encoding" - public static let acceptLanguage = "Accept-Language" - public static let userAgent = "User-Agent" - public static let ifNoneMatch = "If-None-Match" - public static let moreInfo = "X-DuckDuckGo-MoreInfo" - public static let client = "X-DuckDuckGo-Client" - } - - public enum Source: String { - case macStore = "browser-appstore" - case macDMG = "browser-dmg" - case iOS = "phone" - case iPadOS = "tablet" - } - - /// A closure typealias to request sending pixels through the network. - public typealias FireRequest = ( - _ pixelName: String, - _ headers: [String: String], - _ parameters: [String: String], - _ allowedQueryReservedCharacters: CharacterSet?, - _ callBackOnMainThread: Bool, - _ onComplete: @escaping CompletionBlock) -> Void - - public typealias Event = PixelKitEvent - public static let duckDuckGoMorePrivacyInfo = URL(string: "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/atb/")! - private let defaults: UserDefaults - - private let logger = Logger(subsystem: "com.duckduckgo.PixelKit", category: "PixelKit") - - private static let defaultDailyPixelCalendar: Calendar = { - var calendar = Calendar.current - calendar.timeZone = TimeZone(secondsFromGMT: 0)! - return calendar - }() - - private static let weeksToCoalesceCohort = 6 - - private let dateGenerator: () -> Date - - public private(set) static var shared: PixelKit? - - private let appVersion: String - private let defaultHeaders: [String: String] - private let fireRequest: FireRequest - - /// Sets up PixelKit for the entire app. - /// - /// - Parameters: - /// - `dryRun`: if `true`, simulate requests and "send" them at an accelerated rate (once every 2 minutes instead of once a day) - /// - `source`: if set, adds a `pixelSource` parameter to the pixel call; this can be used to specify which target is sending the pixel - /// - `fireRequest`: this is not triggered when `dryRun` is `true` - public static func setUp(dryRun: Bool = false, - appVersion: String, - source: String? = nil, - defaultHeaders: [String: String], - dailyPixelCalendar: Calendar? = nil, - dateGenerator: @escaping () -> Date = Date.init, - defaults: UserDefaults, - fireRequest: @escaping FireRequest) { - shared = PixelKit(dryRun: dryRun, - appVersion: appVersion, - source: source, - defaultHeaders: defaultHeaders, - dailyPixelCalendar: dailyPixelCalendar, - dateGenerator: dateGenerator, - defaults: defaults, - fireRequest: fireRequest) - } - - public static func tearDown() { - shared = nil - } - - private var dryRun: Bool - private let source: String? - private let pixelCalendar: Calendar - - public init(dryRun: Bool, - appVersion: String, - source: String? = nil, - defaultHeaders: [String: String], - dailyPixelCalendar: Calendar? = nil, - dateGenerator: @escaping () -> Date = Date.init, - defaults: UserDefaults, - fireRequest: @escaping FireRequest) { - - self.dryRun = dryRun - self.appVersion = appVersion - self.source = source - self.defaultHeaders = defaultHeaders - self.pixelCalendar = dailyPixelCalendar ?? Self.defaultDailyPixelCalendar - self.dateGenerator = dateGenerator - self.defaults = defaults - self.fireRequest = fireRequest - logger.debug("👾 PixelKit initialised: dryRun: \(self.dryRun, privacy: .public) appVersion: \(self.appVersion, privacy: .public) source: \(self.source ?? "-", privacy: .public) defaultHeaders: \(self.defaultHeaders, privacy: .public) pixelCalendar: \(self.pixelCalendar, privacy: .public)") - } - - // swiftlint:disable:next function_body_length cyclomatic_complexity - private func fire(pixelNamed pixelName: String, - frequency: Frequency, - withHeaders headers: [String: String]?, - withAdditionalParameters params: [String: String]?, - withError error: Error?, - allowedQueryReservedCharacters: CharacterSet?, - includeAppVersionParameter: Bool, - onComplete: @escaping CompletionBlock) { - - var newParams = params ?? [:] - if includeAppVersionParameter { newParams[Parameters.appVersion] = appVersion } - if let source { newParams[Parameters.pixelSource] = source } - if let error { newParams.appendErrorPixelParams(error: error) } - - #if DEBUG - newParams[Parameters.test] = Values.test - #endif - - var headers = headers ?? defaultHeaders - headers[Header.moreInfo] = "See " + Self.duckDuckGoMorePrivacyInfo.absoluteString - headers[Header.client] = "macOS" - - switch frequency { - case .standard: - reportErrorIf(pixel: pixelName, endsWith: "_u") - reportErrorIf(pixel: pixelName, endsWith: "_d") - fireRequestWrapper(pixelName, headers, newParams, allowedQueryReservedCharacters, true, frequency, onComplete) - case .legacyInitial: - reportErrorIf(pixel: pixelName, endsWith: "_u") - reportErrorIf(pixel: pixelName, endsWith: "_d") - if !pixelHasBeenFiredEver(pixelName) { - fireRequestWrapper(pixelName, headers, newParams, allowedQueryReservedCharacters, true, frequency, onComplete) - updatePixelLastFireDate(pixelName: pixelName) - } else { - printDebugInfo(pixelName: pixelName, frequency: frequency, parameters: newParams, skipped: true) - } - case .unique: - reportErrorIf(pixel: pixelName, endsWith: "_d") - guard pixelName.hasSuffix("_u") else { - assertionFailure("Unique pixel: must end with _u") - return - } - if !pixelHasBeenFiredEver(pixelName) { - fireRequestWrapper(pixelName, headers, newParams, allowedQueryReservedCharacters, true, frequency, onComplete) - updatePixelLastFireDate(pixelName: pixelName) - } else { - printDebugInfo(pixelName: pixelName, frequency: frequency, parameters: newParams, skipped: true) - } - case .legacyDaily: - reportErrorIf(pixel: pixelName, endsWith: "_u") - reportErrorIf(pixel: pixelName, endsWith: "_d") - if !pixelHasBeenFiredToday(pixelName) { - fireRequestWrapper(pixelName, headers, newParams, allowedQueryReservedCharacters, true, frequency, onComplete) - updatePixelLastFireDate(pixelName: pixelName) - } else { - printDebugInfo(pixelName: pixelName, frequency: frequency, parameters: newParams, skipped: true) - } - case .daily: - reportErrorIf(pixel: pixelName, endsWith: "_u") - reportErrorIf(pixel: pixelName, endsWith: "_d") // Because is added automatically - if !pixelHasBeenFiredToday(pixelName) { - fireRequestWrapper(pixelName + "_d", headers, newParams, allowedQueryReservedCharacters, true, frequency, onComplete) - updatePixelLastFireDate(pixelName: pixelName) - } else { - printDebugInfo(pixelName: pixelName + "_d", frequency: frequency, parameters: newParams, skipped: true) - } - case .dailyAndCount: - reportErrorIf(pixel: pixelName, endsWith: "_u") - reportErrorIf(pixel: pixelName, endsWith: "_d") // Because is added automatically - reportErrorIf(pixel: pixelName, endsWith: "_c") // Because is added automatically - if !pixelHasBeenFiredToday(pixelName) { - fireRequestWrapper(pixelName + "_d", headers, newParams, allowedQueryReservedCharacters, true, frequency, onComplete) - updatePixelLastFireDate(pixelName: pixelName) - } else { - printDebugInfo(pixelName: pixelName + "_d", frequency: frequency, parameters: newParams, skipped: true) - } - - fireRequestWrapper(pixelName + "_c", headers, newParams, allowedQueryReservedCharacters, true, frequency, onComplete) - } - } - - /// If the pixel name ends with the forbiddenString then an error is logged or an assertion failure is fired in debug - func reportErrorIf(pixel: String, endsWith forbiddenString: String) { - if pixel.hasSuffix(forbiddenString) { - logger.error("Pixel \(pixel, privacy: .public) must not end with \(forbiddenString, privacy: .public)") - assertionFailure("Pixel \(pixel) must not end with \(forbiddenString)") - } - } - - private func printDebugInfo(pixelName: String, frequency: Frequency, parameters: [String: String], skipped: Bool = false) { - let params = parameters.filter { key, _ in !["test"].contains(key) } - logger.debug("👾[\(frequency.description, privacy: .public)-\(skipped ? "Skipped" : "Fired", privacy: .public)] \(pixelName, privacy: .public) \(params, privacy: .public)") - } - - private func fireRequestWrapper( - _ pixelName: String, - _ headers: [String: String], - _ parameters: [String: String], - _ allowedQueryReservedCharacters: CharacterSet?, - _ callBackOnMainThread: Bool, - _ frequency: Frequency, - _ onComplete: @escaping CompletionBlock) { - printDebugInfo(pixelName: pixelName, frequency: frequency, parameters: parameters, skipped: false) - guard !dryRun else { - // simulate server response time for Dry Run mode - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - onComplete(true, nil) - } - return - } - fireRequest(pixelName, headers, parameters, allowedQueryReservedCharacters, callBackOnMainThread, onComplete) - } - - private func prefixedName(for event: Event) -> String { - if event.name.hasPrefix("m_mac_") { - return event.name - } - - if let debugEvent = event as? DebugEvent { - return "m_mac_debug_\(debugEvent.name)" - } else { - return "m_mac_\(event.name)" - } - } - - public func fire(_ event: Event, - frequency: Frequency = .standard, - withHeaders headers: [String: String]? = nil, - withAdditionalParameters params: [String: String]? = nil, - withError error: Error? = nil, - allowedQueryReservedCharacters: CharacterSet? = nil, - includeAppVersionParameter: Bool = true, - onComplete: @escaping CompletionBlock = { _, _ in }) { - - let pixelName = prefixedName(for: event) - - if !dryRun { - if frequency == .daily, pixelHasBeenFiredToday(pixelName) { - onComplete(false, nil) - return - } else if frequency == .unique, pixelHasBeenFiredEver(pixelName) { - onComplete(false, nil) - return - } - } - - let newParams: [String: String]? - switch (event.parameters, params) { - case (.some(let parameters), .none): - newParams = parameters - case (.none, .some(let parameters)): - newParams = parameters - case (.some(let params1), .some(let params2)): - newParams = params1.merging(params2) { $1 } - case (.none, .none): - newParams = nil - } - - let newError: Error? - - if let event = event as? PixelKitEventV2, - let error = event.error { - - // 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 = error - } else { - newError = error - } - - fire(pixelNamed: pixelName, - frequency: frequency, - withHeaders: headers, - withAdditionalParameters: newParams, - withError: newError, - allowedQueryReservedCharacters: allowedQueryReservedCharacters, - includeAppVersionParameter: includeAppVersionParameter, - onComplete: onComplete) - } - - public static func fire(_ event: Event, - frequency: Frequency = .standard, - withHeaders headers: [String: String] = [:], - withAdditionalParameters parameters: [String: String]? = nil, - withError error: Error? = nil, - allowedQueryReservedCharacters: CharacterSet? = nil, - includeAppVersionParameter: Bool = true, - onComplete: @escaping CompletionBlock = { _, _ in }) { - - Self.shared?.fire(event, - frequency: frequency, - withHeaders: headers, - withAdditionalParameters: parameters, - withError: error, - allowedQueryReservedCharacters: allowedQueryReservedCharacters, - includeAppVersionParameter: includeAppVersionParameter, - onComplete: onComplete) - } - - private func cohort(from cohortLocalDate: Date?, dateGenerator: () -> Date = Date.init) -> String? { - guard let cohortLocalDate, - let baseDate = pixelCalendar.date(from: .init(year: 2023, month: 1, day: 1)), - let weeksSinceCohortAssigned = pixelCalendar.dateComponents([.weekOfYear], from: cohortLocalDate, to: dateGenerator()).weekOfYear, - let assignedCohort = pixelCalendar.dateComponents([.weekOfYear], from: baseDate, to: cohortLocalDate).weekOfYear else { - return nil - } - - if weeksSinceCohortAssigned > Self.weeksToCoalesceCohort { - return "" - } else { - return "week-" + String(assignedCohort + 1) - } - } - - public static func cohort(from cohortLocalDate: Date?, dateGenerator: () -> Date = Date.init) -> String { - Self.shared?.cohort(from: cohortLocalDate, dateGenerator: dateGenerator) ?? "" - } - - public static func pixelLastFireDate(event: Event) -> Date? { - Self.shared?.pixelLastFireDate(event: event) - } - - public func pixelLastFireDate(pixelName: String) -> Date? { - var date = defaults.object(forKey: userDefaultsKeyName(forPixelName: pixelName)) as? Date - if date == nil { - date = defaults.object(forKey: legacyUserDefaultsKeyName(forPixelName: pixelName)) as? Date - } - return date - } - - public func pixelLastFireDate(event: Event) -> Date? { - pixelLastFireDate(pixelName: prefixedName(for: event)) - } - - private func updatePixelLastFireDate(pixelName: String) { - defaults.set(dateGenerator(), forKey: userDefaultsKeyName(forPixelName: pixelName)) - } - - private func pixelHasBeenFiredToday(_ name: String) -> Bool { - guard !dryRun else { - if let lastFireDate = pixelLastFireDate(pixelName: name), - let twoMinsAgo = pixelCalendar.date(byAdding: .minute, value: -2, to: dateGenerator()) { - return lastFireDate >= twoMinsAgo - } - - return false - } - - if let lastFireDate = pixelLastFireDate(pixelName: name) { - return pixelCalendar.isDate(dateGenerator(), inSameDayAs: lastFireDate) - } - - return false - } - - private func pixelHasBeenFiredEver(_ name: String) -> Bool { - pixelLastFireDate(pixelName: name) != nil - } - - public func clearFrequencyHistoryFor(pixel: PixelKitEventV2) { - guard let name = Self.shared?.userDefaultsKeyName(forPixelName: pixel.name) else { - return - } - self.defaults.removeObject(forKey: name) - } - - public func clearFrequencyHistoryForAllPixels() { - for (key, _) in self.defaults.dictionaryRepresentation() { - if key.hasPrefix(Self.storageKeyPrefixLegacy) || key.hasPrefix(Self.storageKeyPrefix) { - self.defaults.removeObject(forKey: key) - self.logger.debug("🚮 Removing from storage \(key, privacy: .public)") - } - } - } - - static let storageKeyPrefixLegacy = "com.duckduckgo.network-protection.pixel." - static let storageKeyPrefix = "com.duckduckgo.network-protection.pixel." - - /// Initially PixelKit was configured only for serving netP so these very specific keys were used, now PixelKit serves the entire app so we need to move away from them. - /// NOTE: I would remove this 6 months after release - private func legacyUserDefaultsKeyName(forPixelName pixelName: String) -> String { - dryRun - ? "\(Self.storageKeyPrefixLegacy)\(pixelName).dry-run" - : "\(Self.storageKeyPrefixLegacy)\(pixelName)" - } - - private func userDefaultsKeyName(forPixelName pixelName: String) -> String { - return "\(Self.storageKeyPrefix)\(pixelName)\( dryRun ? ".dry-run" : "" )" - } -} - -extension Dictionary where Key == String, Value == String { - - mutating func appendErrorPixelParams(error: Error) { - self.merge(error.pixelParameters) { _, second in - return second - } - } -} - -internal extension PixelKit { - - /// [USE ONLY FOR TESTS] Sets the shared PixelKit.shared singleton - /// - Parameter pixelkit: A custom instance of PixelKit - static func setSharedForTesting(pixelKit: PixelKit) { - Self.shared = pixelKit - } -} diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEvent.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEvent.swift deleted file mode 100644 index ca352f3347..0000000000 --- a/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEvent.swift +++ /dev/null @@ -1,100 +0,0 @@ -// -// PixelKitEvent.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 - -/// An event that can be fired using PixelKit. -/// -public protocol PixelKitEvent { - var name: String { get } - var parameters: [String: String]? { get } -} - -/// Implementation of ``PixelKitEvent`` with specific logic for debug events. -/// -public final class DebugEvent: PixelKitEvent { - public enum EventType { - case assertionFailure(message: String, file: StaticString, line: UInt) - case custom(_ event: PixelKitEvent) - } - - public let eventType: EventType - public let error: Error? - - public init(eventType: EventType, error: Error? = nil) { - self.eventType = eventType - self.error = error - } - - public init(_ event: PixelKitEvent, error: Error? = nil) { - self.eventType = .custom(event) - self.error = error - } - - public var name: String { - switch eventType { - case .assertionFailure: - return "assertion_failure" - case .custom(let event): - return event.name - } - } - - public var parameters: [String: String]? { - var params: [String: String] - - if case let .custom(event) = eventType, - let eventParams = event.parameters { - params = eventParams - } else { - params = [String: String]() - } - - if let errorWithUserInfo = error as? ErrorWithPixelParameters { - params = errorWithUserInfo.errorParameters - } - - if case let .assertionFailure(message, file, line) = eventType { - params[PixelKit.Parameters.assertionMessage] = message - params[PixelKit.Parameters.assertionFile] = String(file) - params[PixelKit.Parameters.assertionLine] = String(line) - } - - if let error = error { - let nsError = error as NSError - - params[PixelKit.Parameters.errorCode] = "\(nsError.code)" - params[PixelKit.Parameters.errorDomain] = nsError.domain - - if let underlyingError = nsError.userInfo["NSUnderlyingError"] as? NSError { - params[PixelKit.Parameters.underlyingErrorCode] = "\(underlyingError.code)" - params[PixelKit.Parameters.underlyingErrorDomain] = underlyingError.domain - } - - if let sqlErrorCode = nsError.userInfo["SQLiteResultCode"] as? NSNumber { - params[PixelKit.Parameters.underlyingErrorSQLiteCode] = "\(sqlErrorCode.intValue)" - } - - if let sqlExtendedErrorCode = nsError.userInfo["SQLiteExtendedResultCode"] as? NSNumber { - params[PixelKit.Parameters.underlyingErrorSQLiteExtendedCode] = "\(sqlExtendedErrorCode.intValue)" - } - } - - return params - } -} diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEventV2.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEventV2.swift deleted file mode 100644 index dc641454c9..0000000000 --- a/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEventV2.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// 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 - -/// 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 specify an error without having to know about its parameterisation. -/// -/// 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 } -} - -/// Protocol to support mocking pixel firing. -/// -/// We're adding support for `PixelKitEventV2` events strategically because adding support for earlier pixels -/// would be more complicated and time consuming. The idea of V2 events is that fire calls should not include a lot -/// of parameters. Parameters should be provided by the `PixelKitEventV2` protocol (extending it if necessary) -/// and the call to `fire` should process those properties to serialize in the requests. -/// -public protocol PixelFiring { - func fire(_ event: PixelKitEventV2) - - func fire(_ event: PixelKitEventV2, - frequency: PixelKit.Frequency) -} - -extension PixelKit: PixelFiring { - public func fire(_ event: PixelKitEventV2) { - fire(event, frequency: .standard) - } - - public func fire(_ event: PixelKitEventV2, - frequency: PixelKit.Frequency) { - - fire(event, frequency: frequency, onComplete: { _, _ in }) - } -} diff --git a/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/PixelFireExpectations.swift b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/PixelFireExpectations.swift deleted file mode 100644 index 1a1d8c2f64..0000000000 --- a/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/PixelFireExpectations.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// 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 -import PixelKit - -/// 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 underlyingErrors: [Error] - var customFields: [String: String]? - - /// Convenience initializer for cleaner semantics - /// - public static func expect(pixelName: String, error: Error? = nil, underlyingErrors: [Error] = [], customFields: [String: String]? = nil) -> PixelFireExpectations { - - .init(pixelName: pixelName, error: error, underlyingErrors: underlyingErrors, customFields: customFields) - } - - public init(pixelName: String, error: Error? = nil, underlyingErrors: [Error] = [], customFields: [String: String]? = nil) { - self.pixelName = pixelName - self.error = error - self.underlyingErrors = underlyingErrors - self.customFields = customFields - } - - public var parameters: [String: String] { - var parameters = customFields ?? [String: String]() - - if let nsError = error as? NSError { - parameters[PixelKit.Parameters.errorCode] = String(nsError.code) - parameters[PixelKit.Parameters.errorDomain] = nsError.domain - } - - for (index, error) in underlyingErrors.enumerated() { - let errorCodeParameterName = PixelKit.Parameters.underlyingErrorCode + (index == 0 ? "" : String(index + 1)) - let errorDomainParameterName = PixelKit.Parameters.underlyingErrorDomain + (index == 0 ? "" : String(index + 1)) - let nsError = error as NSError - - parameters[errorCodeParameterName] = String(nsError.code) - parameters[errorDomainParameterName] = nsError.domain - } - - return parameters - } -} diff --git a/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/ValidatePixel.swift b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/ValidatePixel.swift deleted file mode 100644 index 00547deb5b..0000000000 --- a/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/ValidatePixel.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// ValidatePixel.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 PixelKit -import XCTest - -public final class PixelRequestValidator { - public init() {} - - public func validateBasicPixelParams( - expectedAppVersion: String, - expectedUserAgent: String, - requestParameters parameters: [String: String], - requestHeaders headers: [String: String]) { - - XCTAssertEqual(parameters[PixelKit.Parameters.test], "1") - XCTAssertEqual(parameters[PixelKit.Parameters.appVersion], expectedAppVersion) - - XCTAssertEqual(headers[PixelKit.Header.userAgent], expectedUserAgent) - XCTAssertEqual(headers[PixelKit.Header.acceptEncoding], "gzip;q=1.0, compress;q=0.5") - XCTAssertNotNil(headers[PixelKit.Header.acceptLanguage]) - XCTAssertNotNil(headers[PixelKit.Header.moreInfo], PixelKit.duckDuckGoMorePrivacyInfo.absoluteString) - } - - public func validateDebugPixelParams( - expectedError: Error?, - requestParameters parameters: [String: String]) { - - if let error = expectedError as? NSError { - XCTAssertEqual(parameters[PixelKit.Parameters.errorCode], "\(error.code)") - XCTAssertEqual(parameters[PixelKit.Parameters.errorDomain], error.domain) - } - } -} diff --git a/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift deleted file mode 100644 index 81232cd4bb..0000000000 --- a/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift +++ /dev/null @@ -1,169 +0,0 @@ -// -// 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 - ] - - /// List of underlying error pixel parameters - private static var underlyingErrorPixelParameters = [ - PixelKit.Parameters.underlyingErrorCode, - PixelKit.Parameters.underlyingErrorDomain - ] - - /// 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_" -#elseif os(iOS) - return "m_" -#endif - } - - /// These parameters are known to be expected just based on the event definition. - /// - /// They're not a complete list of parameters for the event, as the fire call may contain extra information - /// that results in additional parameters. Ideally we want most (if not all) that information to eventually - /// make part of the pixel definition. - func knownExpectedParameters(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 - - if let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] as? NSError { - expectedParameters[PixelKit.Parameters.underlyingErrorCode] = "\(underlyingError.code)" - expectedParameters[PixelKit.Parameters.underlyingErrorDomain] = underlyingError.domain - } - } - - return expectedParameters - } - - // MARK: - Misc Convenience - - private var userDefaults: UserDefaults { - UserDefaults(suiteName: "testing_\(UUID().uuidString)")! - } - - // MARK: - Pixel Firing Expectations - - func fire(_ event: PixelKitEventV2, frequency: PixelKit.Frequency, and expectations: PixelFireExpectations, file: StaticString, line: UInt) { - verifyThat(event, frequency: frequency, meets: expectations, file: file, line: line) - } - - /// 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, - frequency: PixelKit.Frequency, - meets expectations: PixelFireExpectations, - file: StaticString, - line: UInt) { - let expectedPixelNames: [String] = expectedPixelNames(originalName: event.name, frequency: frequency) - let knownExpectedParameters = knownExpectedParameters(for: event) - let callbackExecutedExpectation = expectation(description: "The PixelKit callback has been executed") - - if frequency == .dailyAndCount { - callbackExecutedExpectation.expectedFulfillmentCount = 2 - } - - // Ensure PixelKit is torn down before setting it back up, avoiding unit test race conditions: - PixelKit.tearDown() - - PixelKit.setUp(dryRun: false, - appVersion: "1.0.5", - source: "test-app", - defaultHeaders: [:], - defaults: userDefaults) { firedPixelName, _, firedParameters, _, _, completion in - callbackExecutedExpectation.fulfill() - - let firedParameters = Self.filterStandardPixelParameters(from: firedParameters) - - // Internal validations - XCTAssertTrue(expectedPixelNames.contains(firedPixelName), file: file, line: line) - XCTAssertTrue(knownExpectedParameters.allSatisfy { (key, value) in - firedParameters[key] == value - }) - - if frequency == .dailyAndCount { - XCTAssertTrue(firedPixelName.hasPrefix(expectations.pixelName)) - XCTAssertTrue(firedPixelName.hasSuffix("_c") || firedPixelName.hasSuffix("_d")) - XCTAssertEqual(firedPixelName.count, expectations.pixelName.count + 2) - let exp = self.expectedPixelNames(originalName: expectations.pixelName, frequency: frequency) - XCTAssertTrue(exp.contains(firedPixelName)) - } else { - XCTAssertEqual(expectations.pixelName, firedPixelName) - } - XCTAssertEqual(firedParameters, expectations.parameters) - - completion(true, nil) - } - - PixelKit.fire(event, frequency: frequency) - waitForExpectations(timeout: 0.1) - } - - func expectedPixelNames(originalName: String, frequency: PixelKit.Frequency) -> [String] { - let expectedPixelNameWithoutSuffix = originalName.hasPrefix(Self.pixelPlatformPrefix) ? originalName : Self.pixelPlatformPrefix + originalName - var expectedPixelNames: [String] = [] - - switch frequency { - case .standard: - expectedPixelNames.append(expectedPixelNameWithoutSuffix) - case .legacyInitial: - expectedPixelNames.append(expectedPixelNameWithoutSuffix) - case .unique: - expectedPixelNames.append(expectedPixelNameWithoutSuffix) - case .legacyDaily: - expectedPixelNames.append(expectedPixelNameWithoutSuffix) - case .daily: - expectedPixelNames.append(expectedPixelNameWithoutSuffix.appending("_d")) - case .dailyAndCount: - expectedPixelNames.append(expectedPixelNameWithoutSuffix.appending("_d")) - expectedPixelNames.append(expectedPixelNameWithoutSuffix.appending("_c")) - } - return expectedPixelNames - } -} diff --git a/LocalPackages/PixelKit/Tests/PixelKitTests/PixelKitParametersTests.swift b/LocalPackages/PixelKit/Tests/PixelKitTests/PixelKitParametersTests.swift deleted file mode 100644 index 4d661d3336..0000000000 --- a/LocalPackages/PixelKit/Tests/PixelKitTests/PixelKitParametersTests.swift +++ /dev/null @@ -1,75 +0,0 @@ -// -// PixelKitParametersTests.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 XCTest -@testable import PixelKit -import PixelKitTestingUtilities - -final class PixelKitParametersTests: XCTestCase { - - /// Test events for convenience - /// - private enum TestEvent: PixelKitEventV2 { - case errorEvent(error: Error) - - var name: String { - switch self { - case .errorEvent: - return "error_event" - } - } - - var parameters: [String: String]? { - nil - } - - var error: Error? { - switch self { - case .errorEvent(let error): - error - } - } - } - - /// Test that when firing pixels that include multiple levels of underlying error information, all levels - /// are properly included in the pixel. - /// - func testUnderlyingErrorInformationParameters() { - let underlyingError3 = NSError(domain: "test", code: 3) - let underlyingError2 = NSError( - domain: "test", - code: 2, - userInfo: [ - NSUnderlyingErrorKey: underlyingError3 as NSError - ]) - let topLevelError = NSError( - domain: "test", - code: 1, - userInfo: [ - NSUnderlyingErrorKey: underlyingError2 as NSError - ]) - - fire(TestEvent.errorEvent(error: topLevelError), - frequency: .standard, - and: .expect(pixelName: "m_mac_error_event", - error: topLevelError, - underlyingErrors: [underlyingError2, underlyingError3]), - file: #filePath, - line: #line) - } -} diff --git a/LocalPackages/PixelKit/Tests/PixelKitTests/PixelKitTests.swift b/LocalPackages/PixelKit/Tests/PixelKitTests/PixelKitTests.swift deleted file mode 100644 index fed5b7789d..0000000000 --- a/LocalPackages/PixelKit/Tests/PixelKitTests/PixelKitTests.swift +++ /dev/null @@ -1,385 +0,0 @@ -// -// PixelKitTests.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 XCTest -@testable import PixelKit -import os.log // swiftlint:disable:this enforce_os_log_wrapper - -final class PixelKitTests: XCTestCase { - - private func userDefaults() -> UserDefaults { - UserDefaults(suiteName: "testing_\(UUID().uuidString)")! - } - - /// Test events for convenience - /// - private enum TestEvent: String, PixelKitEvent { - case testEvent - case testEventWithoutParameters - case dailyEvent - case dailyEventWithoutParameters - case dailyAndContinuousEvent - case dailyAndContinuousEventWithoutParameters - case uniqueEvent - - var name: String { - switch self { - case .uniqueEvent: - return "\(rawValue)_u" - default: - return rawValue - } - } - - var parameters: [String: String]? { - switch self { - case .testEvent, .dailyEvent, .dailyAndContinuousEvent, .uniqueEvent: - return [ - "eventParam1": "eventParamValue1", - "eventParam2": "eventParamValue2" - ] - case .testEventWithoutParameters, .dailyEventWithoutParameters, .dailyAndContinuousEventWithoutParameters: - return nil - } - } - - var frequency: PixelKit.Frequency { - switch self { - case .testEvent, .testEventWithoutParameters: - return .standard - case .uniqueEvent: - return .unique - case .dailyEvent, .dailyEventWithoutParameters: - return .daily - case .dailyAndContinuousEvent, .dailyAndContinuousEventWithoutParameters: - return .dailyAndCount - } - } - } - - /// Test that a dry run won't execute the fire request callback. - /// - func testDryRunWontExecuteCallback() async { - let appVersion = "1.0.5" - let headers: [String: String] = [:] - - let pixelKit = PixelKit(dryRun: true, appVersion: appVersion, defaultHeaders: headers, dailyPixelCalendar: nil, defaults: userDefaults()) { _, _, _, _, _, _ in - - XCTFail("This callback should not be executed when doing a dry run") - } - - pixelKit.fire(TestEvent.testEvent) - } - - /// Tests firing a sample pixel and ensuring that all fields are properly set in the fire request callback. - /// - func testFiringASamplePixel() { - // Prepare test parameters - let appVersion = "1.0.5" - let headers = ["a": "2", "b": "3", "c": "2000"] - let event = TestEvent.testEvent - let userDefaults = userDefaults() - - // Set expectations - let expectedPixelName = "m_mac_\(event.name)" - let fireCallbackCalled = expectation(description: "Expect the pixel firing callback to be called") - - // Prepare mock to validate expectations - let pixelKit = PixelKit(dryRun: false, - appVersion: appVersion, - defaultHeaders: headers, - dailyPixelCalendar: nil, - defaults: userDefaults) { firedPixelName, firedHeaders, parameters, _, _, _ in - - fireCallbackCalled.fulfill() - - XCTAssertEqual(expectedPixelName, firedPixelName) - XCTAssertTrue(headers.allSatisfy({ key, value in - firedHeaders[key] == value - })) - - XCTAssertEqual(firedHeaders[PixelKit.Header.moreInfo], "See \(PixelKit.duckDuckGoMorePrivacyInfo)") - - XCTAssertEqual(parameters[PixelKit.Parameters.appVersion], appVersion) -#if DEBUG - XCTAssertEqual(parameters[PixelKit.Parameters.test], PixelKit.Values.test) -#else - XCTAssertNil(parameters[PixelKit.Parameters.test]) -#endif - } - - // Run test - pixelKit.fire(event) - - // Wait for expectations to be fulfilled - wait(for: [fireCallbackCalled], timeout: 0.5) - } - - /// We test firing a daily pixel for the first time executes the fire request callback with the right parameters - /// - func testFiringDailyPixelForTheFirstTime() { - // Prepare test parameters - let appVersion = "1.0.5" - let headers = ["a": "2", "b": "3", "c": "2000"] - let event = TestEvent.dailyEvent - let userDefaults = userDefaults() - - // Set expectations - let expectedPixelName = "m_mac_\(event.name)_d" - let expectedMoreInfoString = "See \(PixelKit.duckDuckGoMorePrivacyInfo)" - let fireCallbackCalled = expectation(description: "Expect the pixel firing callback to be called") - - // Prepare mock to validate expectations - let pixelKit = PixelKit(dryRun: false, - appVersion: appVersion, - defaultHeaders: headers, - dailyPixelCalendar: nil, - defaults: userDefaults) { firedPixelName, firedHeaders, parameters, _, _, _ in - - fireCallbackCalled.fulfill() - - XCTAssertEqual(expectedPixelName, firedPixelName) - XCTAssertTrue(headers.allSatisfy({ key, value in - firedHeaders[key] == value - })) - - XCTAssertEqual(firedHeaders[PixelKit.Header.moreInfo], expectedMoreInfoString) - XCTAssertEqual(parameters[PixelKit.Parameters.appVersion], appVersion) -#if DEBUG - XCTAssertEqual(parameters[PixelKit.Parameters.test], PixelKit.Values.test) -#else - XCTAssertNil(parameters[PixelKit.Parameters.test]) -#endif - } - - // Run test - pixelKit.fire(event, frequency: .daily) - - // Wait for expectations to be fulfilled - wait(for: [fireCallbackCalled], timeout: 0.5) - } - - /// We test firing a daily pixel a second time does not execute the fire request callback. - /// - func testDailyPixelDoubleFiringFrequency() { - // Prepare test parameters - let appVersion = "1.0.5" - let headers = ["a": "2", "b": "3", "c": "2000"] - let event = TestEvent.dailyEvent - let userDefaults = userDefaults() - - // Set expectations - let expectedPixelName = "m_mac_\(event.name)_d" - let expectedMoreInfoString = "See \(PixelKit.duckDuckGoMorePrivacyInfo)" - let fireCallbackCalled = expectation(description: "Expect the pixel firing callback to be called") - fireCallbackCalled.expectedFulfillmentCount = 1 - fireCallbackCalled.assertForOverFulfill = true - - // Prepare mock to validate expectations - let pixelKit = PixelKit(dryRun: false, - appVersion: appVersion, - defaultHeaders: headers, - dailyPixelCalendar: nil, - defaults: userDefaults) { firedPixelName, firedHeaders, parameters, _, _, _ in - - fireCallbackCalled.fulfill() - - XCTAssertEqual(expectedPixelName, firedPixelName) - XCTAssertTrue(headers.allSatisfy({ key, value in - firedHeaders[key] == value - })) - - XCTAssertEqual(firedHeaders[PixelKit.Header.moreInfo], expectedMoreInfoString) - XCTAssertEqual(parameters[PixelKit.Parameters.appVersion], appVersion) -#if DEBUG - XCTAssertEqual(parameters[PixelKit.Parameters.test], PixelKit.Values.test) -#else - XCTAssertNil(parameters[PixelKit.Parameters.test]) -#endif - } - - // Run test - pixelKit.fire(event, frequency: .daily) - pixelKit.fire(event, frequency: .daily) - - // Wait for expectations to be fulfilled - wait(for: [fireCallbackCalled], timeout: 0.5) - } - - /// Test firing a daily pixel a few times - func testDailyPixelFrequency() { - // Prepare test parameters - let appVersion = "1.0.5" - let headers = ["a": "2", "b": "3", "c": "2000"] - let event = TestEvent.dailyEvent - let userDefaults = userDefaults() - - let timeMachine = TimeMachine() - - // Set expectations - let fireCallbackCalled = expectation(description: "Expect the pixel firing callback to be called") - fireCallbackCalled.expectedFulfillmentCount = 3 - fireCallbackCalled.assertForOverFulfill = true - - // Prepare mock to validate expectations - let pixelKit = PixelKit(dryRun: false, - appVersion: appVersion, - defaultHeaders: headers, - dailyPixelCalendar: nil, - dateGenerator: timeMachine.now, - defaults: userDefaults) { _, _, _, _, _, _ in - fireCallbackCalled.fulfill() - } - - // Run test - pixelKit.fire(event, frequency: .daily) // Fired - timeMachine.travel(by: .hour, value: 2) - pixelKit.fire(event, frequency: .legacyDaily) // Skipped - - timeMachine.travel(by: .day, value: 1) - timeMachine.travel(by: .hour, value: 2) - pixelKit.fire(event, frequency: .legacyDaily) // Fired - - timeMachine.travel(by: .hour, value: 10) - pixelKit.fire(event, frequency: .legacyDaily) // Skipped - - timeMachine.travel(by: .day, value: 1) - pixelKit.fire(event, frequency: .legacyDaily) // Fired - - // Wait for expectations to be fulfilled - wait(for: [fireCallbackCalled], timeout: 0.5) - } - - /// Test firing a unique pixel - func testUniquePixel() { - // Prepare test parameters - let appVersion = "1.0.5" - let headers = ["a": "2", "b": "3", "c": "2000"] - let event = TestEvent.uniqueEvent - let userDefaults = userDefaults() - - let timeMachine = TimeMachine() - - // Set expectations - let fireCallbackCalled = expectation(description: "Expect the pixel firing callback to be called") - fireCallbackCalled.expectedFulfillmentCount = 1 - fireCallbackCalled.assertForOverFulfill = true - - let pixelKit = PixelKit(dryRun: false, - appVersion: appVersion, - defaultHeaders: headers, - dailyPixelCalendar: nil, - dateGenerator: timeMachine.now, - defaults: userDefaults) { _, _, _, _, _, _ in - fireCallbackCalled.fulfill() - } - - // Run test - pixelKit.fire(event, frequency: .unique) // Fired - timeMachine.travel(by: .hour, value: 2) - pixelKit.fire(event, frequency: .unique) // Skipped (already fired) - - timeMachine.travel(by: .day, value: 1) - timeMachine.travel(by: .hour, value: 2) - pixelKit.fire(event, frequency: .unique) // Skipped (already fired) - - timeMachine.travel(by: .hour, value: 10) - pixelKit.fire(event, frequency: .unique) // Skipped (already fired) - - timeMachine.travel(by: .day, value: 1) - pixelKit.fire(event, frequency: .unique) // Skipped (already fired) - - // Wait for expectations to be fulfilled - wait(for: [fireCallbackCalled], timeout: 0.5) - } - - func testVPNCohort() { - XCTAssertEqual(PixelKit.cohort(from: nil), "") - assertCohortEqual(.init(year: 2023, month: 1, day: 1), reportAs: "week-1") - assertCohortEqual(.init(year: 2024, month: 2, day: 24), reportAs: "week-60") - } - - private func assertCohortEqual(_ cohort: DateComponents, reportAs reportedCohort: String) { - var calendar = Calendar.current - calendar.timeZone = TimeZone(secondsFromGMT: 0)! - calendar.locale = Locale(identifier: "en_US_POSIX") - - let cohort = calendar.date(from: cohort) - let timeMachine = TimeMachine(calendar: calendar, date: cohort) - - PixelKit.setUp(appVersion: "test", - defaultHeaders: [:], - dailyPixelCalendar: calendar, - dateGenerator: timeMachine.now, - defaults: userDefaults()) { _, _, _, _, _, _ in } - - // 1st week - XCTAssertEqual(PixelKit.cohort(from: cohort, dateGenerator: timeMachine.now), reportedCohort) - - // 2nd week - timeMachine.travel(by: .weekOfYear, value: 1) - XCTAssertEqual(PixelKit.cohort(from: cohort, dateGenerator: timeMachine.now), reportedCohort) - - // 3rd week - timeMachine.travel(by: .weekOfYear, value: 1) - XCTAssertEqual(PixelKit.cohort(from: cohort, dateGenerator: timeMachine.now), reportedCohort) - - // 4th week - timeMachine.travel(by: .weekOfYear, value: 1) - XCTAssertEqual(PixelKit.cohort(from: cohort, dateGenerator: timeMachine.now), reportedCohort) - - // 5th week - timeMachine.travel(by: .weekOfYear, value: 1) - XCTAssertEqual(PixelKit.cohort(from: cohort, dateGenerator: timeMachine.now), reportedCohort) - - // 6th week - timeMachine.travel(by: .weekOfYear, value: 1) - XCTAssertEqual(PixelKit.cohort(from: cohort, dateGenerator: timeMachine.now), reportedCohort) - - // 7th week - timeMachine.travel(by: .weekOfYear, value: 1) - XCTAssertEqual(PixelKit.cohort(from: cohort, dateGenerator: timeMachine.now), reportedCohort) - - // 8th week - timeMachine.travel(by: .weekOfYear, value: 1) - XCTAssertEqual(PixelKit.cohort(from: cohort, dateGenerator: timeMachine.now), "") - } -} - -private class TimeMachine { - private var date: Date - private let calendar: Calendar - - init(calendar: Calendar? = nil, date: Date? = nil) { - self.calendar = calendar ?? { - var calendar = Calendar.current - calendar.timeZone = TimeZone(secondsFromGMT: 0)! - calendar.locale = Locale(identifier: "en_US_POSIX") - return calendar - }() - self.date = date ?? .init(timeIntervalSince1970: 0) - } - - func travel(by component: Calendar.Component, value: Int) { - date = calendar.date(byAdding: component, value: value, to: now())! - } - - func now() -> Date { - date - } -} diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 9092bb8c50..408f9cbfe2 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: "138.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "141.1.1"), .package(path: "../SwiftUIExtensions") ], targets: [ @@ -24,7 +24,11 @@ let package = Package( ], resources: [ .process("Resources") - ]), + ], + swiftSettings: [ + .define("DEBUG", .when(configuration: .debug)) + ] + ), .testTarget( name: "SubscriptionUITests", dependencies: ["SubscriptionUI"]), diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/SwiftUI/MultilineTextHeightFixer.swift b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/MultilineTextHeightFixer.swift similarity index 98% rename from LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/SwiftUI/MultilineTextHeightFixer.swift rename to LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/MultilineTextHeightFixer.swift index 43b81b54c9..9a6e693e43 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/SwiftUI/MultilineTextHeightFixer.swift +++ b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/MultilineTextHeightFixer.swift @@ -39,7 +39,7 @@ private struct MultilineTextHeightFixer: ViewModifier { } } -extension View { +public extension View { /// Meant to be used for multiline-text. This is currently only applying a modifier /// diff --git a/UITests/UI Tests.xctestplan b/UITests/UI Tests.xctestplan new file mode 100644 index 0000000000..e1394ca735 --- /dev/null +++ b/UITests/UI Tests.xctestplan @@ -0,0 +1,49 @@ +{ + "configurations" : [ + { + "id" : "3B3274F0-3353-49A0-B607-6F17F519C2E2", + "name" : "Test Scheme Action", + "options" : { + + } + } + ], + "defaultOptions" : { + "commandLineArgumentEntries" : [ + { + "argument" : "-NSConstraintBasedLayoutVisualizeMutuallyExclusiveConstraints YES" + }, + { + "argument" : "-com.apple.CoreData.ConcurrencyDebug 1" + } + ], + "environmentVariableEntries" : [ + { + "key" : "OS_ACTIVITY_DT_MODE", + "value" : "YES" + }, + { + "key" : "OS_ACTIVITY_MODE", + "value" : "debug" + } + ], + "targetForVariableExpansion" : { + "containerPath" : "container:DuckDuckGo.xcodeproj", + "identifier" : "AA585D7D248FD31100E9A3E2", + "name" : "DuckDuckGo Privacy Browser" + } + }, + "testTargets" : [ + { + "skippedTests" : [ + "PermissionsTests" + ], + "target" : { + "containerPath" : "container:DuckDuckGo.xcodeproj", + "identifier" : "7B4CE8D926F02108009134B1", + "name" : "UI Tests" + } + } + ], + "version" : 1 +} diff --git a/UnitTests/Autofill/Mocks/MockAutofillActionExecutor.swift b/UnitTests/Autofill/Mocks/MockAutofillActionExecutor.swift index bb7a4ec8bb..d94b13ccb0 100644 --- a/UnitTests/Autofill/Mocks/MockAutofillActionExecutor.swift +++ b/UnitTests/Autofill/Mocks/MockAutofillActionExecutor.swift @@ -27,7 +27,7 @@ final class MockAutofillActionBuilder: AutofillActionBuilder { var mockPresenter: MockAutofillActionPresenter? func buildExecutor() -> AutofillActionExecutor? { - guard let secureVault = try? MockSecureVaultFactory.makeVault(errorReporter: nil) else { return nil } + guard let secureVault = try? MockSecureVaultFactory.makeVault(reporter: nil) else { return nil } let syncService = MockDDGSyncing(authState: .inactive, scheduler: CapturingScheduler(), isSyncInProgress: false) let executor = MockAutofillActionExecutor(userAuthenticator: UserAuthenticatorMock(), secureVault: secureVault, syncService: syncService) self.mockExecutor = executor diff --git a/UnitTests/Autofill/Tests/AutofillDeleteAllPasswordsExecutorTests.swift b/UnitTests/Autofill/Tests/AutofillDeleteAllPasswordsExecutorTests.swift index f34960a14f..3b262f145d 100644 --- a/UnitTests/Autofill/Tests/AutofillDeleteAllPasswordsExecutorTests.swift +++ b/UnitTests/Autofill/Tests/AutofillDeleteAllPasswordsExecutorTests.swift @@ -30,7 +30,7 @@ final class AutofillDeleteAllPasswordsExecutorTests: XCTestCase { private var syncService: DDGSyncing! override func setUpWithError() throws { - secureVault = try MockSecureVaultFactory.makeVault(errorReporter: nil) + secureVault = try MockSecureVaultFactory.makeVault(reporter: nil) syncService = MockDDGSyncing(authState: .inactive, scheduler: scheduler, isSyncInProgress: false) sut = .init(userAuthenticator: mockAuthenticator, secureVault: secureVault, syncService: syncService) } diff --git a/LocalPackages/PixelKit/Sources/PixelKit/Extensions/URL+PixelKit.swift b/UnitTests/Bookmarks/Helpers/WebsiteInfoHelpers.swift similarity index 53% rename from LocalPackages/PixelKit/Sources/PixelKit/Extensions/URL+PixelKit.swift rename to UnitTests/Bookmarks/Helpers/WebsiteInfoHelpers.swift index 98bb77c2c4..9a87f91090 100644 --- a/LocalPackages/PixelKit/Sources/PixelKit/Extensions/URL+PixelKit.swift +++ b/UnitTests/Bookmarks/Helpers/WebsiteInfoHelpers.swift @@ -1,7 +1,7 @@ // -// URL+PixelKit.swift +// WebsiteInfoHelpers.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. @@ -17,14 +17,19 @@ // import Foundation +@testable import DuckDuckGo_Privacy_Browser -extension URL { +extension WebsiteInfo { - static let pixelBase = ProcessInfo.processInfo.environment["PIXEL_BASE_URL", default: "https://improving.duckduckgo.com"] - - public static func pixelUrl(forPixelNamed pixelName: String) -> URL { - let urlString = "\(Self.pixelBase)/t/\(pixelName)" - return URL(string: urlString)! + @MainActor + static func makeWebsitesInfo(url: URL, title: String? = nil, occurrences: Int = 1) -> [WebsiteInfo] { + (1...occurrences) + .map { _ in + let tab = Tab(content: .url(url, credential: nil, source: .ui)) + tab.title = title + return tab + } + .compactMap(WebsiteInfo.init) } } diff --git a/UnitTests/Bookmarks/Model/LocalBookmarkManagerTests.swift b/UnitTests/Bookmarks/Model/LocalBookmarkManagerTests.swift index 97ce23202d..79d3809847 100644 --- a/UnitTests/Bookmarks/Model/LocalBookmarkManagerTests.swift +++ b/UnitTests/Bookmarks/Model/LocalBookmarkManagerTests.swift @@ -178,6 +178,87 @@ final class LocalBookmarkManagerTests: XCTestCase { XCTAssertNotNil(bookmarkList) } + func testWhenGetBookmarkFolderIsCalledThenAskBookmarkStoreToRetrieveFolder() throws { + // GIVEN + let (bookmarkManager, bookmarkStoreMock) = LocalBookmarkManager.aManager + XCTAssertFalse(bookmarkStoreMock.bookmarkFolderWithIdCalled) + XCTAssertNil(bookmarkStoreMock.capturedFolderId) + + // WHEN + _ = bookmarkManager.getBookmarkFolder(withId: #function) + + // THEN + XCTAssertTrue(bookmarkStoreMock.bookmarkFolderWithIdCalled) + XCTAssertEqual(bookmarkStoreMock.capturedFolderId, #function) + } + + func testWhenGetBookmarkFolderIsCalledAndFolderExistsInStoreThenBookmarkStoreReturnsFolder() throws { + // GIVEN + let (bookmarkManager, bookmarkStoreMock) = LocalBookmarkManager.aManager + let folder = BookmarkFolder(id: "1", title: "Test") + bookmarkStoreMock.bookmarkFolder = folder + + // WHEN + let result = bookmarkManager.getBookmarkFolder(withId: #function) + + // THEN + XCTAssertEqual(result, folder) + } + + func testWhenGetBookmarkFolderIsCalledAndFolderDoesNotExistInStoreThenBookmarkStoreReturnsNil() throws { + // GIVEN + let (bookmarkManager, bookmarkStoreMock) = LocalBookmarkManager.aManager + bookmarkStoreMock.bookmarkFolder = nil + + // WHEN + let result = bookmarkManager.getBookmarkFolder(withId: #function) + + // THEN + XCTAssertNil(result) + } + + // MARK: - Save Multiple Bookmarks at once + + func testWhenMakeBookmarksForWebsitesInfoIsCalledThenBookmarkStoreIsAskedToCreateMultipleBookmarks() { + // GIVEN + let (sut, bookmarkStoreMock) = LocalBookmarkManager.aManager + let newFolderName = #function + let websitesInfo = [ + WebsiteInfo(url: URL.duckDuckGo, title: "Website 1"), + WebsiteInfo(url: URL.duckDuckGo, title: "Website 2"), + WebsiteInfo(url: URL.duckDuckGo, title: "Website 3"), + WebsiteInfo(url: URL.duckDuckGo, title: "Website 4"), + ].compactMap { $0 } + XCTAssertFalse(bookmarkStoreMock.saveBookmarksInNewFolderNamedCalled) + XCTAssertNil(bookmarkStoreMock.capturedWebsitesInfo) + XCTAssertNil(bookmarkStoreMock.capturedNewFolderName) + XCTAssertNil(bookmarkStoreMock.capturedParentFolderType) + + // WHEN + sut.makeBookmarks(for: websitesInfo, inNewFolderNamed: newFolderName, withinParentFolder: .root) + + // THEN + XCTAssertTrue(bookmarkStoreMock.saveBookmarksInNewFolderNamedCalled) + XCTAssertEqual(bookmarkStoreMock.capturedWebsitesInfo?.count, 4) + XCTAssertEqual(bookmarkStoreMock.capturedWebsitesInfo, websitesInfo) + XCTAssertEqual(bookmarkStoreMock.capturedNewFolderName, newFolderName) + XCTAssertEqual(bookmarkStoreMock.capturedParentFolderType, .root) + } + + func testWhenMakeBookmarksForWebsiteInfoIsCalledThenReloadAllBookmarks() { + // GIVEN + let (sut, bookmarkStoreMock) = LocalBookmarkManager.aManager + bookmarkStoreMock.loadAllCalled = false // Reset after load all bookmarks the first time + XCTAssertFalse(bookmarkStoreMock.loadAllCalled) + let websitesInfo = [WebsiteInfo(url: URL.duckDuckGo, title: "Website 1")].compactMap { $0 } + + // WHEN + sut.makeBookmarks(for: websitesInfo, inNewFolderNamed: "Test", withinParentFolder: .root) + + // THEN + XCTAssertTrue(bookmarkStoreMock.loadAllCalled) + } + } fileprivate extension LocalBookmarkManager { @@ -204,3 +285,14 @@ fileprivate extension Bookmark { isFavorite: false) } + +private extension WebsiteInfo { + + @MainActor + init?(url: URL, title: String) { + let tab = Tab(content: .url(url, credential: nil, source: .ui)) + tab.title = title + self.init(tab) + } + +} diff --git a/UnitTests/Bookmarks/Model/WebsiteInfoTests.swift b/UnitTests/Bookmarks/Model/WebsiteInfoTests.swift new file mode 100644 index 0000000000..7b1b8e8196 --- /dev/null +++ b/UnitTests/Bookmarks/Model/WebsiteInfoTests.swift @@ -0,0 +1,77 @@ +// +// WebsiteInfoTests.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 XCTest +@testable import DuckDuckGo_Privacy_Browser + +@MainActor +final class WebsiteInfoTests: XCTestCase { + + // MARK: - URL + + func testWhenInitWithTabThenSetURLWithTabURLValue() throws { + // GIVEN + let url = URL.duckDuckGo + let websiteInfo = try XCTUnwrap(WebsiteInfo.makeWebsitesInfo(url: url).first) + + // WHEN + let result = websiteInfo.url + + // THEN + XCTAssertEqual(result, url) + } + + // MARK: - Title + + func testWhenTitleIsNotNilThenDisplayTitleReturnsTitleValue() throws { + // GIVEN + let title = #function + let websiteInfo = try XCTUnwrap(WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo, title: title).first) + + // WHEN + let result = websiteInfo.title + + // THEN + XCTAssertEqual(result, title) + } + + func testWhenTitleIsNilAndURLConformsToRFC3986ThenDisplayTitleReturnsURLHost() throws { + // GIVEN + let url = URL.duckDuckGo + let websiteInfo = try XCTUnwrap(WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo, title: nil).first) + + // WHEN + let result = websiteInfo.title + + // THEN + XCTAssertEqual(result, url.host) + } + + func testWhenTitleIsNilAndURLDoesNotConformToRFC3986ThenDisplayTitleReturnsURLAbsoluteString() throws { + // GIVEN + let invalidURL = try XCTUnwrap(URL(string: "duckduckgo.com")) + let websiteInfo = try XCTUnwrap(WebsiteInfo.makeWebsitesInfo(url: invalidURL, title: nil).first) + + // WHEN + let result = websiteInfo.title + + // THEN + XCTAssertEqual(result, invalidURL.absoluteString) + } + +} diff --git a/LocalPackages/PixelKit/Sources/PixelKit/Extensions/String+StaticString.swift b/UnitTests/Bookmarks/Services/BookmarkFolderStoreMock.swift similarity index 70% rename from LocalPackages/PixelKit/Sources/PixelKit/Extensions/String+StaticString.swift rename to UnitTests/Bookmarks/Services/BookmarkFolderStoreMock.swift index 19f9b8b692..014282eff6 100644 --- a/LocalPackages/PixelKit/Sources/PixelKit/Extensions/String+StaticString.swift +++ b/UnitTests/Bookmarks/Services/BookmarkFolderStoreMock.swift @@ -1,7 +1,7 @@ // -// String+StaticString.swift +// BookmarkFolderStoreMock.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. @@ -17,11 +17,9 @@ // import Foundation +@testable import DuckDuckGo_Privacy_Browser + +final class BookmarkFolderStoreMock: BookmarkFoldersStore { + var lastBookmarkAllTabsFolderIdUsed: String? -extension String { - init(_ staticString: StaticString) { - self = staticString.withUTF8Buffer { - String(decoding: $0, as: UTF8.self) - } - } } diff --git a/UnitTests/Bookmarks/Services/LocalBookmarkStoreTests.swift b/UnitTests/Bookmarks/Services/LocalBookmarkStoreTests.swift index 2ebff62e7d..73d2f96e8d 100644 --- a/UnitTests/Bookmarks/Services/LocalBookmarkStoreTests.swift +++ b/UnitTests/Bookmarks/Services/LocalBookmarkStoreTests.swift @@ -261,6 +261,134 @@ final class LocalBookmarkStoreTests: XCTestCase { waitForExpectations(timeout: 3, handler: nil) } + @MainActor + func testWhenSaveMultipleWebsiteInfoToANewFolderInRootFolder_ThenTheNewFolderIsCreated_AndBoomarksAreAddedToTheFolder() async throws { + // GIVEN + let context = container.viewContext + let sut = LocalBookmarkStore(context: context) + let newFolderName = "Bookmark All Open Tabs" + let websites = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo, occurrences: 50) + var bookmarksEntity = try await sut.loadAll(type: .bookmarks).get() + var topLevelEntities = try await sut.loadAll(type: .topLevelEntities).get() + XCTAssertEqual(bookmarksEntity.count, 0) + XCTAssertEqual(topLevelEntities.count, 0) + + // WHEN + sut.saveBookmarks(for: websites, inNewFolderNamed: newFolderName, withinParentFolder: .root) + + // THEN + bookmarksEntity = try await sut.loadAll(type: .bookmarks).get() + topLevelEntities = try await sut.loadAll(type: .topLevelEntities).get() + let bookmarks = try XCTUnwrap(bookmarksEntity as? [Bookmark]) + let folders = try XCTUnwrap(topLevelEntities as? [BookmarkFolder]) + let folder = try XCTUnwrap(folders.first) + XCTAssertEqual(bookmarksEntity.count, 50) + XCTAssertEqual(folders.count, 1) + XCTAssertEqual(folder.parentFolderUUID, BookmarkEntity.Constants.rootFolderID) + XCTAssertEqual(folder.title, newFolderName) + XCTAssertEqual(Set(folder.children), Set(bookmarks)) + bookmarks.forEach { bookmark in + XCTAssertEqual(bookmark.parentFolderUUID, folder.id) + } + } + + @MainActor + func testWhenSaveMultipleWebsiteInfoToANewFolderInSubfolder_ThenTheNewFolderIsCreated_AndBoomarksAreAddedToTheFolder() async throws { + // GIVEN + let context = container.viewContext + let sut = LocalBookmarkStore(context: context) + let newFolderName = "Bookmark All Open Tabs" + let websites = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo, occurrences: 50) + let parentFolderToInsert = BookmarkFolder(id: "ABCDE", title: "Subfolder") + _ = await sut.save(folder: parentFolderToInsert, parent: nil) + var bookmarksEntity = try await sut.loadAll(type: .bookmarks).get() + var topLevelEntities = try await sut.loadAll(type: .topLevelEntities).get() + XCTAssertEqual(bookmarksEntity.count, 0) + XCTAssertEqual(topLevelEntities.count, 1) + XCTAssertEqual(topLevelEntities.first, parentFolderToInsert) + XCTAssertEqual((topLevelEntities.first as? BookmarkFolder)?.parentFolderUUID, BookmarkEntity.Constants.rootFolderID) + + // WHEN + sut.saveBookmarks(for: websites, inNewFolderNamed: newFolderName, withinParentFolder: .parent(uuid: parentFolderToInsert.id)) + + // THEN + bookmarksEntity = try await sut.loadAll(type: .bookmarks).get() + topLevelEntities = try await sut.loadAll(type: .topLevelEntities).get() + let bookmarks = try XCTUnwrap(bookmarksEntity as? [Bookmark]) + let folders = try XCTUnwrap(topLevelEntities as? [BookmarkFolder]) + let parentFolder = try XCTUnwrap(folders.first) + let subFolder = try XCTUnwrap(parentFolder.children.first as? BookmarkFolder) + XCTAssertEqual(bookmarksEntity.count, 50) + XCTAssertEqual(folders.count, 1) + XCTAssertEqual(parentFolder.title, parentFolderToInsert.title) + XCTAssertEqual(parentFolder.children.count, 1) + XCTAssertEqual(subFolder.title, newFolderName) + XCTAssertEqual(Set(subFolder.children), Set(bookmarks)) + bookmarks.forEach { bookmark in + XCTAssertEqual(bookmark.parentFolderUUID, subFolder.id) + } + } + + @MainActor + func testWhenSaveMultipleWebsiteInfo_AndTitleIsNotNil_ThenTitleIsUsedAsBookmarkTitle() async throws { + // GIVEN + let context = container.viewContext + let sut = LocalBookmarkStore(context: context) + let websiteName = "Test Website" + let websites = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo, title: websiteName, occurrences: 1) + var bookmarksEntity = try await sut.loadAll(type: .bookmarks).get() + XCTAssertEqual(bookmarksEntity.count, 0) + + // WHEN + sut.saveBookmarks(for: websites, inNewFolderNamed: "Saved Tabs", withinParentFolder: .root) + + // THEN + bookmarksEntity = try await sut.loadAll(type: .bookmarks).get() + let bookmark = try XCTUnwrap((bookmarksEntity as? [Bookmark])?.first) + XCTAssertEqual(bookmarksEntity.count, 1) + XCTAssertEqual(bookmark.title, websiteName) + } + + @MainActor + func testWhenSaveMultipleWebsiteInfo_AndTitleIsNil_ThenURLDomainIsUsedAsBookmarkTitle() async throws { + // GIVEN + let context = container.viewContext + let sut = LocalBookmarkStore(context: context) + let url = URL.duckDuckGo + let websites = WebsiteInfo.makeWebsitesInfo(url: url, title: nil, occurrences: 1) + var bookmarksEntity = try await sut.loadAll(type: .bookmarks).get() + XCTAssertEqual(bookmarksEntity.count, 0) + + // WHEN + sut.saveBookmarks(for: websites, inNewFolderNamed: "Saved Tabs", withinParentFolder: .root) + + // THEN + bookmarksEntity = try await sut.loadAll(type: .bookmarks).get() + let bookmark = try XCTUnwrap((bookmarksEntity as? [Bookmark])?.first) + XCTAssertEqual(bookmarksEntity.count, 1) + XCTAssertEqual(bookmark.title, url.host) + } + + @MainActor + func testWhenSaveMultipleWebsiteInfo_AndTitleIsNil_AndURLDoesNotConformToRFC3986_ThenURLAbsoluteStringIsUsedAsBookmarkTitle() async throws { + // GIVEN + let context = container.viewContext + let sut = LocalBookmarkStore(context: context) + let url = try XCTUnwrap(URL(string: "duckduckgo.com")) + let websites = WebsiteInfo.makeWebsitesInfo(url: url, title: nil, occurrences: 1) + var bookmarksEntity = try await sut.loadAll(type: .bookmarks).get() + XCTAssertEqual(bookmarksEntity.count, 0) + + // WHEN + sut.saveBookmarks(for: websites, inNewFolderNamed: "Saved Tabs", withinParentFolder: .root) + + // THEN + bookmarksEntity = try await sut.loadAll(type: .bookmarks).get() + let bookmark = try XCTUnwrap((bookmarksEntity as? [Bookmark])?.first) + XCTAssertEqual(bookmarksEntity.count, 1) + XCTAssertEqual(bookmark.title, url.absoluteString) + } + // MARK: Moving Bookmarks/Folders func testWhenMovingBookmarkWithinParentCollection_AndIndexIsValid_ThenBookmarkIsMoved() async { @@ -1071,6 +1199,63 @@ final class LocalBookmarkStoreTests: XCTestCase { } } + // MARK: - Retrieve Bookmark Folder + + func testWhenFetchingBookmarkFolderWithId_AndFolderExist_ThenFolderIsReturned() async { + // GIVEN + let context = container.viewContext + let sut = LocalBookmarkStore(context: context) + let folderId = "ABCDE" + let folder = BookmarkFolder(id: folderId, title: "Test") + _ = await sut.save(folder: folder, parent: nil) + + // WHEN + let result = sut.bookmarkFolder(withId: folderId) + + // THEN + XCTAssertEqual(result, folder) + } + + func testWhenFetchingBookmarkFolderWithId_AndFolderDoesNotExist_ThenNilIsReturned() { + // GIVEN + let context = container.viewContext + let sut = LocalBookmarkStore(context: context) + let folderId = "ABCDE" + + // WHEN + let result = sut.bookmarkFolder(withId: folderId) + + // THEN + XCTAssertNil(result) + } + + func testWhenFetchingBookmarkFolderWithId_AndFolderHasBeenMoved_ThenFolderIsStillReturned() async { + // GIVEN + let context = container.viewContext + let sut = LocalBookmarkStore(context: context) + let folderId = "ABCDE" + let folder1 = BookmarkFolder(id: UUID().uuidString, title: "Test") + let folder2 = BookmarkFolder(id: folderId, title: "Test") + let expectedFolder = BookmarkFolder(id: folderId, title: "Test", parentFolderUUID: folder1.id) + _ = await sut.save(folder: folder1, parent: nil) + _ = await sut.save(folder: folder2, parent: nil) + + // WHEN + let firstFetchResult = sut.bookmarkFolder(withId: folderId) + + // THEN + XCTAssertEqual(firstFetchResult, folder2) + + // Move folder + _ = await sut.move(objectUUIDs: [folder2.id], toIndex: nil, withinParentFolder: .parent(uuid: folder1.id)) + + // WHEN + let secondFetchResult = sut.bookmarkFolder(withId: folderId) + + // THEN + XCTAssertEqual(secondFetchResult, expectedFolder) + } + // MARK: Import func testWhenBookmarksAreImported_AndNoDuplicatesExist_ThenBookmarksAreImported() { diff --git a/UnitTests/Bookmarks/Services/UserDefaultsBookmarkFoldersStoreTests.swift b/UnitTests/Bookmarks/Services/UserDefaultsBookmarkFoldersStoreTests.swift new file mode 100644 index 0000000000..7020d8ec28 --- /dev/null +++ b/UnitTests/Bookmarks/Services/UserDefaultsBookmarkFoldersStoreTests.swift @@ -0,0 +1,62 @@ +// +// UserDefaultsBookmarkFoldersStoreTests.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 XCTest +@testable import DuckDuckGo_Privacy_Browser + +final class UserDefaultsBookmarkFoldersStoreTests: XCTestCase { + private static let suiteName = "testing_bookmark_folders_store" + private var userDefaults: UserDefaults! + private var sut: UserDefaultsBookmarkFoldersStore! + + override func setUpWithError() throws { + try super.setUpWithError() + userDefaults = UserDefaults(suiteName: Self.suiteName) + sut = UserDefaultsBookmarkFoldersStore(userDefaults: userDefaults) + } + + override func tearDownWithError() throws { + userDefaults.removePersistentDomain(forName: Self.suiteName) + userDefaults = nil + sut = nil + try super.tearDownWithError() + } + + func testReturnBookmarkAllTabsLastFolderIdUsedWhenUserDefaultsContainsValue() { + // GIVEN + let value = "12345" + userDefaults.set(value, forKey: UserDefaultsBookmarkFoldersStore.Keys.bookmarkAllTabsFolderUsedKey) + + // WHEN + let result = sut.lastBookmarkAllTabsFolderIdUsed + + // THEN + XCTAssertEqual(result, value) + } + + func testReturnNilForBookmarkAllTabsLastFolderIdUsedWhenUserDefaultsDoesNotContainValue() { + // GIVEN + userDefaults.set(nil, forKey: UserDefaultsBookmarkFoldersStore.Keys.bookmarkAllTabsFolderUsedKey) + + // WHEN + let result = sut.lastBookmarkAllTabsFolderIdUsed + + // THEN + XCTAssertNil(result) + } +} diff --git a/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogCoordinatorViewModelTests.swift b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogCoordinatorViewModelTests.swift index 53072513c4..c8b101e148 100644 --- a/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogCoordinatorViewModelTests.swift +++ b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogCoordinatorViewModelTests.swift @@ -125,46 +125,44 @@ final class AddEditBookmarkDialogCoordinatorViewModelTests: XCTestCase { XCTAssertEqual(bookmarkViewModelMock.selectedFolder, folder) } -} + // MARK: - Integration Test -final class AddEditBookmarkDialogViewModelMock: BookmarkDialogEditing { - var bookmarkName: String = "" - var bookmarkURLPath: String = "" - var isBookmarkFavorite: Bool = false - var isURLFieldHidden: Bool = false - var title: String = "" - var folders: [DuckDuckGo_Privacy_Browser.FolderViewModel] = [] - var selectedFolder: DuckDuckGo_Privacy_Browser.BookmarkFolder? { - didSet { - selectedFolderExpectation?.fulfill() + func testWhenAddFolderMultipleTimesThenFolderListIsUpdatedAndSelectedFolderIsNil() { + // GIVEN + let expectation = self.expectation(description: #function) + let folder = BookmarkFolder(id: "1", title: "Folder") + bookmarkViewModelMock.selectedFolder = folder + let bookmarkStoreMock = BookmarkStoreMock() + bookmarkStoreMock.bookmarks = [folder] + let bookmarkManager = LocalBookmarkManager(bookmarkStore: bookmarkStoreMock, faviconManagement: FaviconManagerMock()) + bookmarkManager.loadBookmarks() + let folderModel = AddEditBookmarkFolderDialogViewModel(mode: .add(parentFolder: nil), bookmarkManager: bookmarkManager) + let sut = AddEditBookmarkDialogCoordinatorViewModel(bookmarkModel: bookmarkViewModelMock, folderModel: folderModel) + let c = folderModel.$folders + .dropFirst(2) // Not interested in the first two events. 1.subscribing to $folders and 2. subscribing to $list. + .sink { folders in + expectation.fulfill() } - } - var cancelActionTitle: String = "" - var isOtherActionDisabled: Bool = false - var defaultActionTitle: String = "" - var isDefaultActionDisabled: Bool = false - func cancel(dismiss: () -> Void) {} - func addOrSave(dismiss: () -> Void) {} + XCTAssertNil(folderModel.selectedFolder) - var selectedFolderExpectation: XCTestExpectation? -} + // Tap Add Folder + sut.addFolderAction() + XCTAssertEqual(sut.viewState, .folder) + XCTAssertTrue(folderModel.folderName.isEmpty) + XCTAssertEqual(folderModel.selectedFolder, folder) -final class AddEditBookmarkFolderDialogViewModelMock: BookmarkFolderDialogEditing { - let subject = PassthroughSubject() + // Create a new folder + folderModel.folderName = #function + folderModel.addOrSave {} + + // Add folder again + sut.addFolderAction() - var addFolderPublisher: AnyPublisher { - subject.eraseToAnyPublisher() + // THEN + withExtendedLifetime(c) {} + waitForExpectations(timeout: 1.0) + XCTAssertEqual(sut.viewState, .folder) + XCTAssertTrue(folderModel.folderName.isEmpty) } - var folderName: String = "" - var title: String = "" - var folders: [DuckDuckGo_Privacy_Browser.FolderViewModel] = [] - var selectedFolder: DuckDuckGo_Privacy_Browser.BookmarkFolder? - var cancelActionTitle: String = "" - var isOtherActionDisabled: Bool = false - var defaultActionTitle: String = "" - var isDefaultActionDisabled: Bool = false - - func cancel(dismiss: () -> Void) {} - func addOrSave(dismiss: () -> Void) {} } diff --git a/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogViewModelTests.swift b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogViewModelTests.swift index b409df5b9c..615777c7d9 100644 --- a/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogViewModelTests.swift +++ b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogViewModelTests.swift @@ -119,25 +119,61 @@ final class AddEditBookmarkDialogViewModelTests: XCTestCase { XCTAssertTrue(result.isEmpty) } - func testShouldSetNameAndURLToValueWhenInitModeIsAddTabInfoIsNotNilAndURLIsNotAlreadyBookmarked() { + func testWhenInitModeIsAddAndTabInfoIsNotNilAndURLIsNotAlreadyBookmarkedThenSetURLToValue() { // GIVEN let tab = Tab(content: .url(URL.duckDuckGo, source: .link), title: "Test") let sut = AddEditBookmarkDialogViewModel(mode: .add(tabWebsite: WebsiteInfo(tab)), bookmarkManager: bookmarkManager) // WHEN - let name = sut.bookmarkName let url = sut.bookmarkURLPath // THEN - XCTAssertEqual(name, "Test") XCTAssertEqual(url, URL.duckDuckGo.absoluteString) } + func testWhenInitAndModeIsAddAndTabInfoTitleIsNotNilAndURLIsNotAlreadyBookmarkedThenSetBookmarkNameToTitle() { + // GIVEN + let tab = Tab(content: .url(URL.duckDuckGo, source: .link), title: "Test") + let sut = AddEditBookmarkDialogViewModel(mode: .add(tabWebsite: WebsiteInfo(tab)), bookmarkManager: bookmarkManager) + + // WHEN + let name = sut.bookmarkName + + // THEN + XCTAssertEqual(name, "Test") + } + + func testWhenInitAndModeIsAddAndTabInfoTitleIsNilAndURLIsNotAlreadyBookmarkedThenSetBookmarkNameToURLDomain() { + // GIVEN + let url = URL.duckDuckGo + let tab = Tab(content: .url(url, source: .link), title: nil) + let sut = AddEditBookmarkDialogViewModel(mode: .add(tabWebsite: WebsiteInfo(tab)), bookmarkManager: bookmarkManager) + + // WHEN + let name = sut.bookmarkName + + // THEN + XCTAssertEqual(name, url.host) + } + + func testWhenInitAndModeIsAddAndTabInfoTitleIsNilAndURLDoesNotConformToRFC3986AndURLIsNotAlreadyBookmarkedThenSetBookmarkNameToURLAbsoluteString() throws { + // GIVEN + let url = try XCTUnwrap(URL(string: "duckduckgo.com")) + let tab = Tab(content: .url(url, source: .link), title: nil) + let sut = AddEditBookmarkDialogViewModel(mode: .add(tabWebsite: WebsiteInfo(tab)), bookmarkManager: bookmarkManager) + + // WHEN + let name = sut.bookmarkName + + // THEN + XCTAssertEqual(name, url.absoluteString) + } + func testShouldSetNameAndURLToEmptyWhenInitModeIsAddTabInfoIsNotNilAndURLIsAlreadyBookmarked() throws { // GIVEN let tab = Tab(content: .url(URL.duckDuckGo, source: .link), title: "Test") let websiteInfo = try XCTUnwrap(WebsiteInfo(tab)) - let bookmark = Bookmark(id: "1", url: websiteInfo.url.absoluteString, title: websiteInfo.title ?? "", isFavorite: false) + let bookmark = Bookmark(id: "1", url: websiteInfo.url.absoluteString, title: websiteInfo.title, isFavorite: false) bookmarkStoreMock.bookmarks = [bookmark] bookmarkManager.loadBookmarks() let sut = AddEditBookmarkDialogViewModel(mode: .add(tabWebsite: WebsiteInfo(tab)), bookmarkManager: bookmarkManager) diff --git a/UnitTests/Bookmarks/ViewModels/AddEditBookmarkFolderDialogViewModelTests.swift b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkFolderDialogViewModelTests.swift index a48f18d2ab..6d54257048 100644 --- a/UnitTests/Bookmarks/ViewModels/AddEditBookmarkFolderDialogViewModelTests.swift +++ b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkFolderDialogViewModelTests.swift @@ -329,7 +329,7 @@ final class AddEditBookmarkFolderDialogViewModelTests: XCTestCase { // THEN XCTAssertFalse(bookmarkStoreMock.updateFolderCalled) XCTAssertTrue(bookmarkStoreMock.saveFolderCalled) - XCTAssertEqual(bookmarkStoreMock.capturedFolder?.title, sut.folderName) + XCTAssertEqual(bookmarkStoreMock.capturedFolder?.title, #function) XCTAssertEqual(bookmarkStoreMock.capturedParentFolder, folder) } @@ -346,7 +346,7 @@ final class AddEditBookmarkFolderDialogViewModelTests: XCTestCase { // THEN XCTAssertTrue(bookmarkStoreMock.updateFolderCalled) - XCTAssertEqual(bookmarkStoreMock.capturedFolder?.title, sut.folderName) + XCTAssertEqual(bookmarkStoreMock.capturedFolder?.title, "TEST") } func testShouldNotAskBookmarkStoreToUpdateFolderWhenNameIsNotChanged() { diff --git a/UnitTests/Bookmarks/ViewModels/BookmarkAllTabsDialogCoordinatorViewModelTests.swift b/UnitTests/Bookmarks/ViewModels/BookmarkAllTabsDialogCoordinatorViewModelTests.swift new file mode 100644 index 0000000000..d766d27f0d --- /dev/null +++ b/UnitTests/Bookmarks/ViewModels/BookmarkAllTabsDialogCoordinatorViewModelTests.swift @@ -0,0 +1,171 @@ +// +// BookmarkAllTabsDialogCoordinatorViewModelTests.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 XCTest +import Combine +@testable import DuckDuckGo_Privacy_Browser + +@MainActor +final class BookmarkAllTabsDialogCoordinatorViewModelTests: XCTestCase { + private var sut: BookmarkAllTabsDialogCoordinatorViewModel! + private var bookmarkAllTabsViewModelMock: BookmarkAllTabsDialogViewModelMock! + private var bookmarkFolderViewModelMock: AddEditBookmarkFolderDialogViewModelMock! + private var cancellables: Set! + + override func setUpWithError() throws { + try super.setUpWithError() + + cancellables = [] + bookmarkAllTabsViewModelMock = .init() + bookmarkFolderViewModelMock = .init() + sut = .init(bookmarkModel: bookmarkAllTabsViewModelMock, folderModel: bookmarkFolderViewModelMock) + } + + override func tearDownWithError() throws { + cancellables = nil + bookmarkAllTabsViewModelMock = nil + bookmarkFolderViewModelMock = nil + sut = nil + try super.tearDownWithError() + } + + func testWhenInitThenViewStateIsBookmarkAllTabs() { + XCTAssertEqual(sut.viewState, .bookmarkAllTabs) + } + + func testWhenDismissActionIsCalledThenViewStateIsBookmarkAllTabs() { + // GIVEN + sut.addFolderAction() + XCTAssertEqual(sut.viewState, .addFolder) + + // WHEN + sut.dismissAction() + + // THEN + XCTAssertEqual(sut.viewState, .bookmarkAllTabs) + + } + + func testWhenAddFolderActionIsCalledThenSetSelectedFolderOnFolderViewModelIsCalledAndReturnAddFolderViewState() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: "Folder") + bookmarkAllTabsViewModelMock.selectedFolder = folder + XCTAssertNil(bookmarkFolderViewModelMock.selectedFolder) + XCTAssertEqual(sut.viewState, .bookmarkAllTabs) + + // WHEN + sut.addFolderAction() + + // THEN + XCTAssertEqual(bookmarkFolderViewModelMock.selectedFolder, folder) + XCTAssertEqual(sut.viewState, .addFolder) + } + + func testWhenBookmarkModelChangesThenReceiveEvent() { + // GIVEN + let expectation = self.expectation(description: #function) + var didCallChangeValue = false + sut.objectWillChange.sink { _ in + didCallChangeValue = true + expectation.fulfill() + } + .store(in: &cancellables) + + // WHEN + sut.bookmarkModel.objectWillChange.send() + + // THEN + waitForExpectations(timeout: 1.0) + XCTAssertTrue(didCallChangeValue) + } + + func testWhenBookmarkFolderModelChangesThenReceiveEvent() { + // GIVEN + let expectation = self.expectation(description: #function) + var didCallChangeValue = false + sut.objectWillChange.sink { _ in + didCallChangeValue = true + expectation.fulfill() + } + .store(in: &cancellables) + + // WHEN + sut.folderModel.objectWillChange.send() + + // THEN + waitForExpectations(timeout: 1.0) + XCTAssertTrue(didCallChangeValue) + } + + func testWhenAddFolderPublisherSendsEventThenSelectedFolderOnBookmarkAllTabsViewModelIsSet() { + // GIVEN + let expectation = self.expectation(description: #function) + bookmarkAllTabsViewModelMock.selectedFolderExpectation = expectation + let folder = BookmarkFolder(id: "ABCDE", title: #function) + XCTAssertNil(bookmarkAllTabsViewModelMock.selectedFolder) + + // WHEN + sut.folderModel.subject.send(folder) + + // THEN + waitForExpectations(timeout: 1.0) + XCTAssertEqual(bookmarkAllTabsViewModelMock.selectedFolder, folder) + } + + // MARK: - Integration Test + + func testWhenAddFolderMultipleTimesThenFolderListIsUpdatedAndSelectedFolderIsNil() { + // GIVEN + let expectation = self.expectation(description: #function) + let folder = BookmarkFolder(id: "1", title: "Folder") + bookmarkAllTabsViewModelMock.selectedFolder = folder + let bookmarkStoreMock = BookmarkStoreMock() + bookmarkStoreMock.bookmarks = [folder] + let bookmarkManager = LocalBookmarkManager(bookmarkStore: bookmarkStoreMock, faviconManagement: FaviconManagerMock()) + bookmarkManager.loadBookmarks() + let folderModel = AddEditBookmarkFolderDialogViewModel(mode: .add(parentFolder: nil), bookmarkManager: bookmarkManager) + let sut = BookmarkAllTabsDialogCoordinatorViewModel(bookmarkModel: bookmarkAllTabsViewModelMock, folderModel: folderModel) + let c = folderModel.$folders + .dropFirst(2) // Not interested in the first two events. 1.subscribing to $folders and 2. subscribing to $list. + .sink { folders in + expectation.fulfill() + } + + XCTAssertNil(folderModel.selectedFolder) + + // Tap Add Folder + sut.addFolderAction() + XCTAssertEqual(sut.viewState, .addFolder) + XCTAssertTrue(folderModel.folderName.isEmpty) + XCTAssertEqual(folderModel.selectedFolder, folder) + + // Create a new folder + folderModel.folderName = #function + folderModel.addOrSave {} + + // Add folder again + sut.addFolderAction() + + // THEN + withExtendedLifetime(c) {} + waitForExpectations(timeout: 1.0) + XCTAssertEqual(sut.viewState, .addFolder) + XCTAssertTrue(folderModel.folderName.isEmpty) + } + +} diff --git a/UnitTests/Bookmarks/ViewModels/BookmarkAllTabsDialogViewModelTests.swift b/UnitTests/Bookmarks/ViewModels/BookmarkAllTabsDialogViewModelTests.swift new file mode 100644 index 0000000000..dd05e42238 --- /dev/null +++ b/UnitTests/Bookmarks/ViewModels/BookmarkAllTabsDialogViewModelTests.swift @@ -0,0 +1,382 @@ +// +// BookmarkAllTabsDialogViewModelTests.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 XCTest +@testable import DuckDuckGo_Privacy_Browser + +@MainActor +final class BookmarkAllTabsDialogViewModelTests: XCTestCase { + private var bookmarkManager: LocalBookmarkManager! + private var bookmarkStoreMock: BookmarkStoreMock! + private var foldersStoreMock: BookmarkFolderStoreMock! + + override func setUpWithError() throws { + try super.setUpWithError() + bookmarkStoreMock = BookmarkStoreMock() + bookmarkStoreMock.bookmarks = [BookmarkFolder.mock] + bookmarkManager = .init(bookmarkStore: bookmarkStoreMock, faviconManagement: FaviconManagerMock()) + bookmarkManager.loadBookmarks() + foldersStoreMock = .init() + } + + override func tearDownWithError() throws { + bookmarkStoreMock = nil + bookmarkManager = nil + foldersStoreMock = nil + try super.tearDownWithError() + } + + // MARK: - Copy + + func testWhenTitleIsCalledThenItReflectsThenNumberOfWebsites() { + // GIVEN + let websitesInfo = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo, occurrences: 10) + let sut = BookmarkAllTabsDialogViewModel(websites: websitesInfo, foldersStore: foldersStoreMock, bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.title + + // THEN + XCTAssertEqual(title, String(format: UserText.Bookmarks.Dialog.Title.bookmarkOpenTabs, websitesInfo.count)) + } + + func testWhenCancelActionTitleIsCalledThenItReturnsTheRightTitle() { + // GIVEN + let websitesInfo = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo) + let sut = BookmarkAllTabsDialogViewModel(websites: websitesInfo, foldersStore: foldersStoreMock, bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.cancelActionTitle + + // THEN + XCTAssertEqual(title, UserText.cancel) + } + + func testWhenEducationalMessageIsCalledThenItReturnsTheRightMessage() { + // GIVEN + let websitesInfo = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo) + let sut = BookmarkAllTabsDialogViewModel(websites: websitesInfo, foldersStore: foldersStoreMock, bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.educationalMessage + + // THEN + XCTAssertEqual(title, UserText.Bookmarks.Dialog.Message.bookmarkOpenTabsEducational) + } + + func testWhenDefaultActionTitleIsCalledThenItReturnsTheRightTitle() { + // GIVEN + let websitesInfo = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo) + let sut = BookmarkAllTabsDialogViewModel(websites: websitesInfo, foldersStore: foldersStoreMock, bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.defaultActionTitle + + // THEN + XCTAssertEqual(title, UserText.Bookmarks.Dialog.Action.addAllBookmarks) + } + + func testWhenFolderNameFieldTitleIsCalledThenItReturnsTheRightTitle() { + // GIVEN + let websitesInfo = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo) + let sut = BookmarkAllTabsDialogViewModel(websites: websitesInfo, foldersStore: foldersStoreMock, bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.folderNameFieldTitle + + // THEN + XCTAssertEqual(title, UserText.Bookmarks.Dialog.Field.folderName) + } + + func testWhenLocationFieldTitleIsCalledThenItReturnsTheRightTitle() { + // GIVEN + let websitesInfo = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo) + let sut = BookmarkAllTabsDialogViewModel(websites: websitesInfo, foldersStore: foldersStoreMock, bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.locationFieldTitle + + // THEN + XCTAssertEqual(title, UserText.Bookmarks.Dialog.Field.location) + } + + // MARK: - State + + func testWhenInitThenFolderNameIsSetToCurrentDateAndNumberOfWebsites() throws { + // GIVEN + let date = Date(timeIntervalSince1970: 1712902304) // 12th of April 2024 + let gmtTimeZone = try XCTUnwrap(TimeZone(identifier: "GMT")) + let websitesInfo = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo, occurrences: 5) + let sut = BookmarkAllTabsDialogViewModel( + websites: websitesInfo, + foldersStore: foldersStoreMock, + bookmarkManager: bookmarkManager, + dateFormatterConfigurationProvider: { + BookmarkAllTabsDialogViewModel.DateFormatterConfiguration(date: date, timeZone: gmtTimeZone) + } + ) + + // WHEN + let result = sut.folderName + + // THEN + XCTAssertEqual(result, String(format: UserText.Bookmarks.Dialog.Value.folderName, "2024-04-12", websitesInfo.count)) + } + + func testWhenInitAndTimeZoneIsPDTThenFolderNameIsSetToCurrentDateAndNumberOfWebsites() throws { + // GIVEN + let date = Date(timeIntervalSince1970: 1712902304) // 12th of April 2024 (GMT) + let pdtTimeZone = try XCTUnwrap(TimeZone(identifier: "America/Los_Angeles")) + let expectedDate = "2024-04-11" // Expected date in PDT TimeZone + let websitesInfo = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo, occurrences: 5) + let sut = BookmarkAllTabsDialogViewModel( + websites: websitesInfo, + foldersStore: foldersStoreMock, + bookmarkManager: bookmarkManager, + dateFormatterConfigurationProvider: { + BookmarkAllTabsDialogViewModel.DateFormatterConfiguration(date: date, timeZone: pdtTimeZone) + } + ) + + // WHEN + let result = sut.folderName + + // THEN + XCTAssertEqual(result, String(format: UserText.Bookmarks.Dialog.Value.folderName, expectedDate, websitesInfo.count)) + } + + func testWhenInitThenFoldersAreSetFromBookmarkList() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + bookmarkStoreMock.bookmarks = [folder] + bookmarkManager.loadBookmarks() + let websitesInfo = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo) + let sut = BookmarkAllTabsDialogViewModel(websites: websitesInfo, foldersStore: foldersStoreMock, bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.folders + + // THEN + XCTAssertEqual(result.count, 1) + XCTAssertEqual(result.first?.entity, folder) + } + + func testWhenInitAndFoldersStoreLastUsedFolderIsNilThenDoNotAskBookmarkStoreForBookmarkFolder() { + // GIVEN + foldersStoreMock.lastBookmarkAllTabsFolderIdUsed = nil + XCTAssertFalse(bookmarkStoreMock.bookmarkFolderWithIdCalled) + XCTAssertNil(bookmarkStoreMock.capturedFolderId) + + // WHEN + _ = BookmarkAllTabsDialogViewModel(websites: WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo), foldersStore: foldersStoreMock, bookmarkManager: bookmarkManager) + + // THEN + XCTAssertFalse(bookmarkStoreMock.bookmarkFolderWithIdCalled) + XCTAssertNil(bookmarkStoreMock.capturedFolderId) + } + + func testWhenInitAndFoldersStoreLastUsedFolderIsNotNilThenAskBookmarkStoreForBookmarkFolder() { + foldersStoreMock.lastBookmarkAllTabsFolderIdUsed = "1ABCDE" + XCTAssertFalse(bookmarkStoreMock.bookmarkFolderWithIdCalled) + XCTAssertNil(bookmarkStoreMock.capturedFolderId) + + // WHEN + _ = BookmarkAllTabsDialogViewModel(websites: WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo), foldersStore: foldersStoreMock, bookmarkManager: bookmarkManager) + + // THEN + XCTAssertTrue(bookmarkStoreMock.bookmarkFolderWithIdCalled) + XCTAssertEqual(bookmarkStoreMock.capturedFolderId, "1ABCDE") + } + + func testWhenFoldersStoreLastUsedFolderIsNotNilAndBookmarkStoreDoesNotContainFolderThenSelectedFolderIsNil() throws { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + foldersStoreMock.lastBookmarkAllTabsFolderIdUsed = "1" + bookmarkStoreMock.bookmarkFolder = nil + bookmarkStoreMock.bookmarks = [folder] + bookmarkManager.loadBookmarks() + let websitesInfo = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo) + let sut = BookmarkAllTabsDialogViewModel(websites: websitesInfo, foldersStore: foldersStoreMock, bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.selectedFolder + + // THEN + XCTAssertNil(result) + } + + func testWhenFoldersStoreLastUsedFolderIsNotNilThenSelectedFolderIsNotNil() throws { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + foldersStoreMock.lastBookmarkAllTabsFolderIdUsed = "1" + bookmarkStoreMock.bookmarkFolder = folder + bookmarkStoreMock.bookmarks = [folder] + bookmarkManager.loadBookmarks() + let websitesInfo = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo) + let sut = BookmarkAllTabsDialogViewModel(websites: websitesInfo, foldersStore: foldersStoreMock, bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.selectedFolder + + // THEN + XCTAssertEqual(result, folder) + } + + func testWhenFolderIsAddedThenFoldersListIsRefreshed() { + // GIVEN + let expectation = self.expectation(description: #function) + expectation.expectedFulfillmentCount = 2 + let folder = BookmarkFolder(id: "1", title: #function) + let folder2 = BookmarkFolder(id: "2", title: "Test") + bookmarkStoreMock.bookmarks = [folder] + bookmarkManager.loadBookmarks() + let websitesInfo = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo) + let sut = BookmarkAllTabsDialogViewModel(websites: websitesInfo, foldersStore: foldersStoreMock, bookmarkManager: bookmarkManager) + XCTAssertEqual(sut.folders.count, 1) + XCTAssertEqual(sut.folders.first?.entity, folder) + + // Simulate Bookmark store changing data set + bookmarkStoreMock.bookmarks = [folder, folder2] + var expectedFolder: [BookmarkFolder] = [] + let c = sut.$folders + .dropFirst() + .sink { folders in + expectedFolder = folders.map(\.entity) + expectation.fulfill() + } + + // WHEN + bookmarkManager.loadBookmarks() + + // THEN + withExtendedLifetime(c) {} + waitForExpectations(timeout: 1.0) + XCTAssertEqual(expectedFolder.count, 2) + XCTAssertEqual(expectedFolder.first, folder) + XCTAssertEqual(expectedFolder.last, folder2) + } + + // MARK: - Actions + + func testWhenIsOtherActionDisabledCalledThenReturnFalse() { + // GIVEN + let websitesInfo = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo) + let sut = BookmarkAllTabsDialogViewModel(websites: websitesInfo, foldersStore: foldersStoreMock, bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.isOtherActionDisabled + + // THEN + XCTAssertFalse(result) + } + + func testWhenFolderNameIsEmptyDefaultActionIsDisabled() { + // GIVEN + let websitesInfo = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo) + let sut = BookmarkAllTabsDialogViewModel(websites: websitesInfo, foldersStore: foldersStoreMock, bookmarkManager: bookmarkManager) + sut.folderName = "" + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertTrue(result) + } + + func testWhenFolderNameIsNotEmptyDefaultActionIsEnabled() { + // GIVEN + let websitesInfo = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo) + let sut = BookmarkAllTabsDialogViewModel(websites: websitesInfo, foldersStore: foldersStoreMock, bookmarkManager: bookmarkManager) + sut.folderName = "TEST" + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertFalse(result) + } + + func testWhenCancelIsCalledThenDismissIsCalled() { + // GIVEN + let websitesInfo = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo) + let sut = BookmarkAllTabsDialogViewModel(websites: websitesInfo, foldersStore: foldersStoreMock, bookmarkManager: bookmarkManager) + var didCallDismiss = false + + // WHEN + sut.cancel { + didCallDismiss = true + } + + // THEN + XCTAssertTrue(didCallDismiss) + } + + func testWhenAddOrSaveIsCalledAndSelectedFolderIsNilThenBookmarkStoreIsAskedToBookmarkWebsitesInfoInRootFolder() { + // GIVEN + let websitesInfo = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo) + let sut = BookmarkAllTabsDialogViewModel(websites: websitesInfo, foldersStore: foldersStoreMock, bookmarkManager: bookmarkManager) + sut.selectedFolder = nil + XCTAssertFalse(bookmarkStoreMock.saveBookmarksInNewFolderNamedCalled) + XCTAssertNil(bookmarkStoreMock.capturedWebsitesInfo) + XCTAssertNil(bookmarkStoreMock.capturedParentFolderType) + + // WHEN + sut.addOrSave(dismiss: {}) + + // THEN + XCTAssertTrue(bookmarkStoreMock.saveBookmarksInNewFolderNamedCalled) + XCTAssertEqual(bookmarkStoreMock.capturedWebsitesInfo, websitesInfo) + XCTAssertEqual(bookmarkStoreMock.capturedParentFolderType, .root) + + } + + func testWhenAddOrSaveIsCalledAndSelectedFolderIsNotNilThenBookmarkStoreIsAskedToBookmarkWebsitesInfoNotInRootFolder() { + // GIVEN + let folder = BookmarkFolder(id: "ABCDE", title: "Saved Tabs") + let websitesInfo = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo) + let sut = BookmarkAllTabsDialogViewModel(websites: websitesInfo, foldersStore: foldersStoreMock, bookmarkManager: bookmarkManager) + sut.selectedFolder = folder + XCTAssertFalse(bookmarkStoreMock.saveBookmarksInNewFolderNamedCalled) + XCTAssertNil(bookmarkStoreMock.capturedWebsitesInfo) + XCTAssertNil(bookmarkStoreMock.capturedParentFolderType) + + // WHEN + sut.addOrSave(dismiss: {}) + + // THEN + XCTAssertTrue(bookmarkStoreMock.saveBookmarksInNewFolderNamedCalled) + XCTAssertEqual(bookmarkStoreMock.capturedWebsitesInfo, websitesInfo) + XCTAssertEqual(bookmarkStoreMock.capturedParentFolderType, .parent(uuid: "ABCDE")) + } + + func testWhenAddOrSaveIsCalledThenDismissIsCalled() { + // GIVEN + let websitesInfo = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo) + let sut = BookmarkAllTabsDialogViewModel(websites: websitesInfo, foldersStore: foldersStoreMock, bookmarkManager: bookmarkManager) + var didCallDismiss = false + + // WHEN + sut.addOrSave { + didCallDismiss = true + } + + // THEN + XCTAssertTrue(didCallDismiss) + } + +} diff --git a/UnitTests/Bookmarks/ViewModels/Mocks/AddEditBookmarkDialogViewModelMock.swift b/UnitTests/Bookmarks/ViewModels/Mocks/AddEditBookmarkDialogViewModelMock.swift new file mode 100644 index 0000000000..6cd4175ac6 --- /dev/null +++ b/UnitTests/Bookmarks/ViewModels/Mocks/AddEditBookmarkDialogViewModelMock.swift @@ -0,0 +1,43 @@ +// +// AddEditBookmarkDialogViewModelMock.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 XCTest +@testable import DuckDuckGo_Privacy_Browser + +final class AddEditBookmarkDialogViewModelMock: BookmarkDialogEditing { + var bookmarkName: String = "" + var bookmarkURLPath: String = "" + var isBookmarkFavorite: Bool = false + var isURLFieldHidden: Bool = false + var title: String = "" + var folders: [DuckDuckGo_Privacy_Browser.FolderViewModel] = [] + var selectedFolder: DuckDuckGo_Privacy_Browser.BookmarkFolder? { + didSet { + selectedFolderExpectation?.fulfill() + } + } + var cancelActionTitle: String = "" + var isOtherActionDisabled: Bool = false + var defaultActionTitle: String = "" + var isDefaultActionDisabled: Bool = false + + func cancel(dismiss: () -> Void) {} + func addOrSave(dismiss: () -> Void) {} + + var selectedFolderExpectation: XCTestExpectation? +} diff --git a/UnitTests/Bookmarks/ViewModels/Mocks/AddEditBookmarkFolderDialogViewModelMock.swift b/UnitTests/Bookmarks/ViewModels/Mocks/AddEditBookmarkFolderDialogViewModelMock.swift new file mode 100644 index 0000000000..99317bf853 --- /dev/null +++ b/UnitTests/Bookmarks/ViewModels/Mocks/AddEditBookmarkFolderDialogViewModelMock.swift @@ -0,0 +1,40 @@ +// +// AddEditBookmarkFolderDialogViewModelMock.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 Combine +@testable import DuckDuckGo_Privacy_Browser + +final class AddEditBookmarkFolderDialogViewModelMock: BookmarkFolderDialogEditing { + let subject = PassthroughSubject() + + var addFolderPublisher: AnyPublisher { + subject.eraseToAnyPublisher() + } + var folderName: String = "" + var title: String = "" + var folders: [DuckDuckGo_Privacy_Browser.FolderViewModel] = [] + var selectedFolder: DuckDuckGo_Privacy_Browser.BookmarkFolder? + var cancelActionTitle: String = "" + var isOtherActionDisabled: Bool = false + var defaultActionTitle: String = "" + var isDefaultActionDisabled: Bool = false + + func cancel(dismiss: () -> Void) {} + func addOrSave(dismiss: () -> Void) {} +} diff --git a/UnitTests/Bookmarks/ViewModels/Mocks/BookmarkAllTabsDialogViewModelMock.swift b/UnitTests/Bookmarks/ViewModels/Mocks/BookmarkAllTabsDialogViewModelMock.swift new file mode 100644 index 0000000000..3d9e9ba3e7 --- /dev/null +++ b/UnitTests/Bookmarks/ViewModels/Mocks/BookmarkAllTabsDialogViewModelMock.swift @@ -0,0 +1,43 @@ +// +// BookmarkAllTabsDialogViewModelMock.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 XCTest +@testable import DuckDuckGo_Privacy_Browser + +final class BookmarkAllTabsDialogViewModelMock: BookmarkAllTabsDialogEditing { + var folderName: String = "" + var educationalMessage: String = "" + var folderNameFieldTitle: String = "" + var locationFieldTitle: String = "" + var title: String = "" + var folders: [DuckDuckGo_Privacy_Browser.FolderViewModel] = [] + var selectedFolder: DuckDuckGo_Privacy_Browser.BookmarkFolder? { + didSet { + selectedFolderExpectation?.fulfill() + } + } + var cancelActionTitle: String = "" + var isOtherActionDisabled: Bool = false + var defaultActionTitle: String = "" + var isDefaultActionDisabled: Bool = true + + func cancel(dismiss: () -> Void) {} + func addOrSave(dismiss: () -> Void) {} + + var selectedFolderExpectation: XCTestExpectation? +} diff --git a/UnitTests/DBP/DataBrokerPrerequisitesStatusVerifierTests.swift b/UnitTests/DBP/DataBrokerPrerequisitesStatusVerifierTests.swift new file mode 100644 index 0000000000..02c94e7feb --- /dev/null +++ b/UnitTests/DBP/DataBrokerPrerequisitesStatusVerifierTests.swift @@ -0,0 +1,73 @@ +// +// DataBrokerPrerequisitesStatusVerifierTests.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 XCTest +@testable import DuckDuckGo_Privacy_Browser +import DataBrokerProtection + +final class DataBrokerPrerequisitesStatusVerifierTests: XCTestCase { + private let statusChecker = MockDBPLoginItemStatusChecker() + + override func setUpWithError() throws { + } + + override func tearDownWithError() throws { + statusChecker.reset() + } + + func testIncorrectDirectory_thenReturnIncorrectDirectoryStatus() { + statusChecker.isInCorrectDirectoryValue = false + let result = DefaultDataBrokerPrerequisitesStatusVerifier(statusChecker: statusChecker).checkStatus() + XCTAssertEqual(result, DataBrokerPrerequisitesStatus.invalidDirectory) + } + + func testIncorrectPermission_thenReturnIncorrectPermissionStatus() { + statusChecker.doesHavePermissionValue = false + let result = DefaultDataBrokerPrerequisitesStatusVerifier(statusChecker: statusChecker).checkStatus() + XCTAssertEqual(result, DataBrokerPrerequisitesStatus.invalidSystemPermission) + } + + func testIncorrectDirectoryAndIncorrectPermission_thenReturnIncorrectPermissionStatus() { + statusChecker.isInCorrectDirectoryValue = false + statusChecker.doesHavePermissionValue = false + let result = DefaultDataBrokerPrerequisitesStatusVerifier(statusChecker: statusChecker).checkStatus() + XCTAssertEqual(result, DataBrokerPrerequisitesStatus.invalidSystemPermission) + } + + func testCorrectStatus_thenReturnValidStatus() { + let result = DefaultDataBrokerPrerequisitesStatusVerifier(statusChecker: statusChecker).checkStatus() + XCTAssertEqual(result, DataBrokerPrerequisitesStatus.valid) + } +} + +private final class MockDBPLoginItemStatusChecker: DBPLoginItemStatusChecker { + var doesHavePermissionValue = true + var isInCorrectDirectoryValue = true + + func doesHaveNecessaryPermissions() -> Bool { + return doesHavePermissionValue + } + func isInCorrectDirectory() -> Bool { + return isInCorrectDirectoryValue + } + + func reset() { + doesHavePermissionValue = true + isInCorrectDirectoryValue = true + } +} diff --git a/UnitTests/DataExport/CSVLoginExporterTests.swift b/UnitTests/DataExport/CSVLoginExporterTests.swift index 20d244968e..bd3c079383 100644 --- a/UnitTests/DataExport/CSVLoginExporterTests.swift +++ b/UnitTests/DataExport/CSVLoginExporterTests.swift @@ -25,7 +25,7 @@ class CSVLoginExporterTests: XCTestCase { func testWhenExportingLogins_ThenLoginsArePersistedToDisk() throws { let mockFileStore = FileStoreMock() - let vault = try MockSecureVaultFactory.makeVault(errorReporter: nil) + let vault = try MockSecureVaultFactory.makeVault(reporter: nil) vault.addWebsiteCredentials(identifiers: [1]) diff --git a/UnitTests/DataExport/MockSecureVault.swift b/UnitTests/DataExport/MockSecureVault.swift index c92a6f6189..d9d55a92fa 100644 --- a/UnitTests/DataExport/MockSecureVault.swift +++ b/UnitTests/DataExport/MockSecureVault.swift @@ -26,7 +26,7 @@ typealias MockVaultFactory = SecureVaultFactory( makeCryptoProvider: { return MockCryptoProvider() - }, makeKeyStoreProvider: { + }, makeKeyStoreProvider: { _ in let provider = MockKeyStoreProvider() provider._l1Key = "key".data(using: .utf8) return provider diff --git a/UnitTests/Fire/Model/FireTests.swift b/UnitTests/Fire/Model/FireTests.swift index 0afbbbbc7a..2e8e3e778e 100644 --- a/UnitTests/Fire/Model/FireTests.swift +++ b/UnitTests/Fire/Model/FireTests.swift @@ -94,7 +94,7 @@ final class FireTests: XCTestCase { waitForExpectations(timeout: 5, handler: nil) XCTAssertEqual(tabCollectionViewModel.tabCollection.tabs.count, 0) - XCTAssertEqual(pinnedTabsManager.tabCollection.tabs.map(\.content.url), pinnedTabs.map(\.content.url)) + XCTAssertEqual(pinnedTabsManager.tabCollection.tabs.map(\.content.userEditableUrl), pinnedTabs.map(\.content.userEditableUrl)) } func testWhenBurnAll_ThenAllWebsiteDataAreRemoved() { diff --git a/UnitTests/HomePage/DataImportProviderTests.swift b/UnitTests/HomePage/DataImportProviderTests.swift index 325f97e461..a67468dd72 100644 --- a/UnitTests/HomePage/DataImportProviderTests.swift +++ b/UnitTests/HomePage/DataImportProviderTests.swift @@ -55,7 +55,7 @@ final class DataImportProviderTests: XCTestCase { override func setUp() { UserDefaultsWrapper.clearAll() - vault = try! MockSecureVaultFactory.makeVault(errorReporter: nil) + vault = try! MockSecureVaultFactory.makeVault(reporter: nil) vault.storedAccounts = notImportedAccounts vault.storedIdentities = [] vault.storedCards = [] diff --git a/UnitTests/HomePage/Mocks/MockBookmarkManager.swift b/UnitTests/HomePage/Mocks/MockBookmarkManager.swift index 28f65fdf59..d1fafb18be 100644 --- a/UnitTests/HomePage/Mocks/MockBookmarkManager.swift +++ b/UnitTests/HomePage/Mocks/MockBookmarkManager.swift @@ -41,6 +41,10 @@ class MockBookmarkManager: BookmarkManager { return nil } + func getBookmarkFolder(withId id: String) -> DuckDuckGo_Privacy_Browser.BookmarkFolder? { + return nil + } + func makeBookmark(for url: URL, title: String, isFavorite: Bool) -> DuckDuckGo_Privacy_Browser.Bookmark? { return nil } @@ -49,6 +53,8 @@ class MockBookmarkManager: BookmarkManager { return nil } + func makeBookmarks(for websitesInfo: [DuckDuckGo_Privacy_Browser.WebsiteInfo], inNewFolderNamed folderName: String, withinParentFolder parent: DuckDuckGo_Privacy_Browser.ParentFolderType) {} + func makeFolder(for title: String, parent: DuckDuckGo_Privacy_Browser.BookmarkFolder?, completion: (DuckDuckGo_Privacy_Browser.BookmarkFolder) -> Void) {} func remove(bookmark: DuckDuckGo_Privacy_Browser.Bookmark) {} diff --git a/UnitTests/Menus/MainMenuTests.swift b/UnitTests/Menus/MainMenuTests.swift index 44ec9cdb11..b3b5f4275e 100644 --- a/UnitTests/Menus/MainMenuTests.swift +++ b/UnitTests/Menus/MainMenuTests.swift @@ -18,6 +18,7 @@ import XCTest import Combine +import BrowserServicesKit @testable import DuckDuckGo_Privacy_Browser class MainMenuTests: XCTestCase { @@ -87,4 +88,26 @@ class MainMenuTests: XCTestCase { XCTAssertEqual(manager.reopenLastClosedMenuItem?.keyEquivalent, ReopenMenuItemKeyEquivalentManager.Const.keyEquivalent) XCTAssertEqual(manager.reopenLastClosedMenuItem?.keyEquivalentModifierMask, ReopenMenuItemKeyEquivalentManager.Const.modifierMask) } + + // MARK: - Bookmarks + + @MainActor + func testWhenBookmarksMenuIsInitialized_ThenSecondItemIsBookmarkAllTabs() throws { + // GIVEN + let sut = MainMenu(featureFlagger: DummyFeatureFlagger(), bookmarkManager: MockBookmarkManager(), faviconManager: FaviconManagerMock(), copyHandler: CopyHandler()) + let bookmarksMenu = try XCTUnwrap(sut.item(withTitle: UserText.bookmarks)) + + // WHEN + let result = try XCTUnwrap(bookmarksMenu.submenu?.item(withTitle: UserText.bookmarkAllTabs)) + + // THEN + XCTAssertEqual(result.keyEquivalent, "d") + XCTAssertEqual(result.keyEquivalentModifierMask, [.command, .shift]) + } +} + +private class DummyFeatureFlagger: FeatureFlagger { + func isFeatureOn(forProvider: F) -> Bool { + false + } } diff --git a/UnitTests/Menus/Mocks/CapturingOptionsButtonMenuDelegate.swift b/UnitTests/Menus/Mocks/CapturingOptionsButtonMenuDelegate.swift index e37582c4fa..05e9c206e7 100644 --- a/UnitTests/Menus/Mocks/CapturingOptionsButtonMenuDelegate.swift +++ b/UnitTests/Menus/Mocks/CapturingOptionsButtonMenuDelegate.swift @@ -23,6 +23,7 @@ class CapturingOptionsButtonMenuDelegate: OptionsButtonMenuDelegate { var optionsButtonMenuRequestedPreferencesCalled = false var optionsButtonMenuRequestedAppearancePreferencesCalled = false + var optionsButtonMenuRequestedBookmarkAllOpenTabsCalled = false func optionsButtonMenuRequestedDataBrokerProtection(_ menu: NSMenu) { @@ -36,6 +37,10 @@ class CapturingOptionsButtonMenuDelegate: OptionsButtonMenuDelegate { } + func optionsButtonMenuRequestedBookmarkAllOpenTabs(_ sender: NSMenuItem) { + optionsButtonMenuRequestedBookmarkAllOpenTabsCalled = true + } + func optionsButtonMenuRequestedBookmarkPopover(_ menu: NSMenu) { } diff --git a/UnitTests/Menus/MoreOptionsMenu+BookmarksTests.swift b/UnitTests/Menus/MoreOptionsMenu+BookmarksTests.swift new file mode 100644 index 0000000000..3888c658a1 --- /dev/null +++ b/UnitTests/Menus/MoreOptionsMenu+BookmarksTests.swift @@ -0,0 +1,61 @@ +// +// MoreOptionsMenu+BookmarksTests.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 XCTest +@testable import DuckDuckGo_Privacy_Browser + +@MainActor +final class MoreOptionsMenu_BookmarksTests: XCTestCase { + + func testWhenBookmarkSubmenuIsInitThenBookmarkAllTabsKeyIsCmdShiftD() throws { + // GIVEN + let sut = BookmarksSubMenu(targetting: self, tabCollectionViewModel: .init()) + + // WHEN + let result = try XCTUnwrap(sut.item(withTitle: UserText.bookmarkAllTabs)) + + // THEN + XCTAssertEqual(result.keyEquivalent, "d") + XCTAssertEqual(result.keyEquivalentModifierMask, [.command, .shift]) + } + + func testWhenTabCollectionCanBookmarkAllTabsThenBookmarkAllTabsMenuItemIsEnabled() throws { + // GIVEN + let tab1 = Tab(content: .url(.duckDuckGo, credential: nil, source: .ui)) + let tab2 = Tab(content: .url(.duckDuckGoEmail, credential: nil, source: .ui)) + let sut = BookmarksSubMenu(targetting: self, tabCollectionViewModel: .init(tabCollection: .init(tabs: [tab1, tab2]))) + + // WHEN + let result = try XCTUnwrap(sut.item(withTitle: UserText.bookmarkAllTabs)) + + // THEN + XCTAssertTrue(result.isEnabled) + } + + func testWhenTabCollectionCannotBookmarkAllTabsThenBookmarkAllTabsMenuItemIsDisabled() throws { + // GIVEN + let sut = BookmarksSubMenu(targetting: self, tabCollectionViewModel: .init(tabCollection: .init(tabs: []))) + + // WHEN + let result = try XCTUnwrap(sut.item(withTitle: UserText.bookmarkAllTabs)) + + // THEN + XCTAssertFalse(result.isEnabled) + } + +} diff --git a/UnitTests/Menus/MoreOptionsMenuTests.swift b/UnitTests/Menus/MoreOptionsMenuTests.swift index 4b9994cb5e..209701e87a 100644 --- a/UnitTests/Menus/MoreOptionsMenuTests.swift +++ b/UnitTests/Menus/MoreOptionsMenuTests.swift @@ -155,6 +155,23 @@ final class MoreOptionsMenuTests: XCTestCase { XCTAssertTrue(capturingActionDelegate.optionsButtonMenuRequestedPreferencesCalled) } + // MARK: - Bookmarks + + @MainActor + func testWhenClickingOnBookmarkAllTabsMenuItemThenTheActionDelegateIsAlerted() throws { + // GIVEN + let bookmarksMenu = try XCTUnwrap(moreOptionMenu.item(at: 8)?.submenu) + let bookmarkAllTabsIndex = try XCTUnwrap(bookmarksMenu.indexOfItem(withTitle: UserText.bookmarkAllTabs)) + let bookmarkAllTabsMenuItem = try XCTUnwrap(bookmarksMenu.items[bookmarkAllTabsIndex]) + bookmarkAllTabsMenuItem.isEnabled = true + + // WHEN + bookmarksMenu.performActionForItem(at: bookmarkAllTabsIndex) + + // THEN + XCTAssertTrue(capturingActionDelegate.optionsButtonMenuRequestedBookmarkAllOpenTabsCalled) + } + } final class NetworkProtectionVisibilityMock: NetworkProtectionFeatureVisibility { diff --git a/UnitTests/Preferences/AutofillPreferencesModelTests.swift b/UnitTests/Preferences/AutofillPreferencesModelTests.swift index 79341abe79..9851f65f72 100644 --- a/UnitTests/Preferences/AutofillPreferencesModelTests.swift +++ b/UnitTests/Preferences/AutofillPreferencesModelTests.swift @@ -49,7 +49,7 @@ final class UserAuthenticatorMock: UserAuthenticating { final class AutofillPreferencesModelTests: XCTestCase { func neverPromptWebsitesManager() throws -> AutofillNeverPromptWebsitesManager { - try AutofillNeverPromptWebsitesManager(secureVault: MockSecureVaultFactory.makeVault(errorReporter: nil)) + try AutofillNeverPromptWebsitesManager(secureVault: MockSecureVaultFactory.makeVault(reporter: nil)) } @MainActor diff --git a/UnitTests/Suggestions/Model/SuggestionContainerTests.swift b/UnitTests/Suggestions/Model/SuggestionContainerTests.swift index db15131a1c..fbd8316ade 100644 --- a/UnitTests/Suggestions/Model/SuggestionContainerTests.swift +++ b/UnitTests/Suggestions/Model/SuggestionContainerTests.swift @@ -44,7 +44,7 @@ final class SuggestionContainerTests: XCTestCase { withExtendedLifetime(cancellable) { waitForExpectations(timeout: 1) } - XCTAssertEqual(suggestionContainer.result?.all, result.topHits + result.duckduckgoSuggestions + result.historyAndBookmarks) + XCTAssertEqual(suggestionContainer.result?.all, result.topHits + result.duckduckgoSuggestions + result.localSuggestions) } func testWhenStopGettingSuggestionsIsCalled_ThenNoSuggestionsArePublished() { diff --git a/UnitTests/Suggestions/ViewModel/SuggestionContainerViewModelTests.swift b/UnitTests/Suggestions/ViewModel/SuggestionContainerViewModelTests.swift index cee0f7bec3..42beab1350 100644 --- a/UnitTests/Suggestions/ViewModel/SuggestionContainerViewModelTests.swift +++ b/UnitTests/Suggestions/ViewModel/SuggestionContainerViewModelTests.swift @@ -154,7 +154,7 @@ extension SuggestionResult { ] return SuggestionResult(topHits: topHits, duckduckgoSuggestions: [], - historyAndBookmarks: []) + localSuggestions: []) } } diff --git a/UnitTests/Tab/Model/TabTests.swift b/UnitTests/Tab/Model/TabTests.swift index 5f44e88e02..5bbea5bee8 100644 --- a/UnitTests/Tab/Model/TabTests.swift +++ b/UnitTests/Tab/Model/TabTests.swift @@ -368,7 +368,7 @@ final class TabTests: XCTestCase { extension Tab { var url: URL? { get { - content.url + content.userEditableUrl } set { setContent(newValue.map { TabContent.url($0, source: .link) } ?? .newtab) diff --git a/UnitTests/Tab/ViewModel/TabViewModelTests.swift b/UnitTests/Tab/ViewModel/TabViewModelTests.swift index 9dbaa4a0fd..d06378f00f 100644 --- a/UnitTests/Tab/ViewModel/TabViewModelTests.swift +++ b/UnitTests/Tab/ViewModel/TabViewModelTests.swift @@ -74,10 +74,32 @@ final class TabViewModelTests: XCTestCase { } @MainActor - func testWhenURLIsFileURLThenAddressBarIsFilePath() { + func testWhenURLIsFileURLAndShowFullUrlIsDisabledThenAddressBarIsFileName() { let urlString = "file:///Users/Dax/file.txt" let url = URL.makeURL(from: urlString)! - let tabViewModel = TabViewModel.forTabWithURL(url) + let tab = Tab(content: .url(url, source: .link)) + let appearancePreferences = AppearancePreferences(persistor: AppearancePreferencesPersistorMock(showFullURL: false)) + let tabViewModel = TabViewModel(tab: tab, appearancePreferences: appearancePreferences) + + let addressBarStringExpectation = expectation(description: "Address bar string") + + tabViewModel.simulateLoadingCompletion(url, in: tabViewModel.tab.webView) + + tabViewModel.$addressBarString.debounce(for: 0.1, scheduler: RunLoop.main).sink { _ in + XCTAssertEqual(tabViewModel.addressBarString, urlString) + XCTAssertEqual(tabViewModel.passiveAddressBarAttributedString.string, url.lastPathComponent) + addressBarStringExpectation.fulfill() + } .store(in: &cancellables) + waitForExpectations(timeout: 1, handler: nil) + } + + @MainActor + func testWhenURLIsFileURLAndShowFullUrlIsEnabledThenAddressBarIsFilePath() { + let urlString = "file:///Users/Dax/file.txt" + let url = URL.makeURL(from: urlString)! + let tab = Tab(content: .url(url, source: .link)) + let appearancePreferences = AppearancePreferences(persistor: AppearancePreferencesPersistorMock(showFullURL: true)) + let tabViewModel = TabViewModel(tab: tab, appearancePreferences: appearancePreferences) let addressBarStringExpectation = expectation(description: "Address bar string") @@ -85,7 +107,7 @@ final class TabViewModelTests: XCTestCase { tabViewModel.$addressBarString.debounce(for: 0.1, scheduler: RunLoop.main).sink { _ in XCTAssertEqual(tabViewModel.addressBarString, urlString) - XCTAssertEqual(tabViewModel.passiveAddressBarString, urlString) + XCTAssertEqual(tabViewModel.passiveAddressBarAttributedString.string, urlString) addressBarStringExpectation.fulfill() } .store(in: &cancellables) waitForExpectations(timeout: 1, handler: nil) @@ -103,7 +125,7 @@ final class TabViewModelTests: XCTestCase { tabViewModel.$addressBarString.debounce(for: 0.1, scheduler: RunLoop.main).sink { _ in XCTAssertEqual(tabViewModel.addressBarString, urlString) - XCTAssertEqual(tabViewModel.passiveAddressBarString, "data:") + XCTAssertEqual(tabViewModel.passiveAddressBarAttributedString.string, "data:") addressBarStringExpectation.fulfill() } .store(in: &cancellables) waitForExpectations(timeout: 1, handler: nil) diff --git a/UnitTests/TabBar/View/MockTabViewItemDelegate.swift b/UnitTests/TabBar/View/MockTabViewItemDelegate.swift index e63dad9b0a..9157000830 100644 --- a/UnitTests/TabBar/View/MockTabViewItemDelegate.swift +++ b/UnitTests/TabBar/View/MockTabViewItemDelegate.swift @@ -23,9 +23,12 @@ class MockTabViewItemDelegate: TabBarViewItemDelegate { var mockedCurrentTab: Tab? + var canBookmarkAllOpenTabs = false var hasItemsToTheRight = false var audioState: WKWebView.AudioState? + private(set) var tabBarViewItemBookmarkAllOpenTabsActionCalled = false + func tabBarViewItem(_ tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem, isMouseOver: Bool) { } @@ -70,6 +73,14 @@ class MockTabViewItemDelegate: TabBarViewItemDelegate { } + func tabBarViewAllItemsCanBeBookmarked(_ tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem) -> Bool { + canBookmarkAllOpenTabs + } + + func tabBarViewItemBookmarkAllOpenTabsAction(_ tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem) { + tabBarViewItemBookmarkAllOpenTabsActionCalled = true + } + func tabBarViewItemMoveToNewWindowAction(_ tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem) { } diff --git a/UnitTests/TabBar/View/TabBarViewItemTests.swift b/UnitTests/TabBar/View/TabBarViewItemTests.swift index 208d73a29e..821ca0b5cc 100644 --- a/UnitTests/TabBar/View/TabBarViewItemTests.swift +++ b/UnitTests/TabBar/View/TabBarViewItemTests.swift @@ -45,31 +45,33 @@ final class TabBarViewItemTests: XCTestCase { XCTAssertEqual(menu.item(at: 0)?.title, UserText.duplicateTab) XCTAssertEqual(menu.item(at: 1)?.title, UserText.pinTab) XCTAssertTrue(menu.item(at: 2)?.isSeparatorItem ?? false) - XCTAssertEqual(menu.item(at: 3)?.title, UserText.bookmarkThisPage) - XCTAssertEqual(menu.item(at: 4)?.title, UserText.fireproofSite) + XCTAssertEqual(menu.item(at: 3)?.title, UserText.fireproofSite) + XCTAssertEqual(menu.item(at: 4)?.title, UserText.bookmarkThisPage) XCTAssertTrue(menu.item(at: 5)?.isSeparatorItem ?? false) - XCTAssertEqual(menu.item(at: 6)?.title, UserText.closeTab) - XCTAssertEqual(menu.item(at: 7)?.title, UserText.closeOtherTabs) - XCTAssertEqual(menu.item(at: 8)?.title, UserText.closeTabsToTheRight) - XCTAssertEqual(menu.item(at: 9)?.title, UserText.moveTabToNewWindow) + XCTAssertEqual(menu.item(at: 6)?.title, UserText.bookmarkAllTabs) + XCTAssertTrue(menu.item(at: 7)?.isSeparatorItem ?? false) + XCTAssertEqual(menu.item(at: 8)?.title, UserText.closeTab) + XCTAssertEqual(menu.item(at: 9)?.title, UserText.closeOtherTabs) + XCTAssertEqual(menu.item(at: 10)?.title, UserText.closeTabsToTheRight) + XCTAssertEqual(menu.item(at: 11)?.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) + XCTAssertFalse(menu.item(at: 1)?.isSeparatorItem ?? true) + XCTAssertEqual(menu.item(at: 2)?.title, UserText.muteTab) + XCTAssertTrue(menu.item(at: 3)?.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) + XCTAssertFalse(menu.item(at: 1)?.isSeparatorItem ?? true) + XCTAssertEqual(menu.item(at: 2)?.title, UserText.unmuteTab) + XCTAssertTrue(menu.item(at: 3)?.isSeparatorItem ?? false) } func testWhenOneTabCloseThenOtherTabsItemIsDisabled() { @@ -188,9 +190,44 @@ final class TabBarViewItemTests: XCTestCase { let pinItem = menu.items.first { $0.title == UserText.pinTab } XCTAssertFalse(pinItem?.isEnabled ?? true) + } - let bookmarkItem = menu.items.first { $0.title == UserText.bookmarkThisPage } - XCTAssertFalse(bookmarkItem?.isEnabled ?? true) + func testWhenCanBookmarkAllOpenTabsThenBookmarkAllOpenTabsItemIsEnabled() throws { + // GIVEN + delegate.canBookmarkAllOpenTabs = true + tabBarViewItem.menuNeedsUpdate(menu) + + // WHEN + let item = try XCTUnwrap(menu.item(withTitle: UserText.bookmarkAllTabs)) + + // THEN + XCTAssertTrue(item.isEnabled) + } + + func testWhenCannotBookmarkAllOpenTabsThenBookmarkAllOpenTabsItemIsDisabled() throws { + // GIVEN + delegate.canBookmarkAllOpenTabs = false + tabBarViewItem.menuNeedsUpdate(menu) + + // WHEN + let item = try XCTUnwrap(menu.item(withTitle: UserText.bookmarkAllTabs)) + + // THEN + XCTAssertFalse(item.isEnabled) + } + + func testWhenClickingOnBookmarkAllTabsThenTheActionDelegateIsNotified() throws { + // GIVEN + delegate.canBookmarkAllOpenTabs = true + tabBarViewItem.menuNeedsUpdate(menu) + let index = try XCTUnwrap(menu.indexOfItem(withTitle: UserText.bookmarkAllTabs)) + XCTAssertFalse(delegate.tabBarViewItemBookmarkAllOpenTabsActionCalled) + + // WHEN + menu.performActionForItem(at: index) + + // THEN + XCTAssertTrue(delegate.tabBarViewItemBookmarkAllOpenTabsActionCalled) } } diff --git a/UnitTests/TabBar/ViewModel/TabCollectionViewModelTests.swift b/UnitTests/TabBar/ViewModel/TabCollectionViewModelTests.swift index efc39feac0..3415fd4439 100644 --- a/UnitTests/TabBar/ViewModel/TabCollectionViewModelTests.swift +++ b/UnitTests/TabBar/ViewModel/TabCollectionViewModelTests.swift @@ -425,6 +425,79 @@ final class TabCollectionViewModelTests: XCTestCase { XCTAssertEqual(events.count, 1) XCTAssertIdentical(events[0], tabCollectionViewModel.selectedTabViewModel) } + + // MARK: - Bookmark All Open Tabs + + func testWhenOneEmptyTabOpenThenCanBookmarkAllOpenTabsIsFalse() throws { + // GIVEN + let sut = TabCollectionViewModel.aTabCollectionViewModel() + let firstTabViewModel = try XCTUnwrap(sut.tabViewModel(at: 0)) + XCTAssertEqual(sut.tabViewModels.count, 1) + XCTAssertEqual(firstTabViewModel.tabContent, .newtab) + + // WHEN + let result = sut.canBookmarkAllOpenTabs() + + // THEN + XCTAssertFalse(result) + } + + func testWhenOneURLTabOpenThenCanBookmarkAllOpenTabsIsFalse() throws { + // GIVEN + let sut = TabCollectionViewModel.aTabCollectionViewModel() + sut.replaceTab(at: .unpinned(0), with: .init(content: .url(.duckDuckGo, credential: nil, source: .ui))) + let firstTabViewModel = try XCTUnwrap(sut.tabViewModel(at: 0)) + XCTAssertEqual(sut.tabViewModels.count, 1) + XCTAssertEqual(firstTabViewModel.tabContent, .url(.duckDuckGo, credential: nil, source: .ui)) + + // WHEN + let result = sut.canBookmarkAllOpenTabs() + + // THEN + XCTAssertFalse(result) + } + + func testWhenOneURLTabAndOnePinnedTabOpenThenCanBookmarkAllOpenTabsIsFalse() { + // GIVEN + let sut = TabCollectionViewModel.aTabCollectionViewModel() + sut.replaceTab(at: .unpinned(0), with: .init(content: .url(.duckDuckGo, credential: nil, source: .ui))) + sut.append(tab: .init(content: .url(.duckDuckGoEmail, credential: nil, source: .ui))) + sut.pinTab(at: 0) + XCTAssertEqual(sut.pinnedTabs.count, 1) + XCTAssertEqual(sut.tabViewModels.count, 1) + + // WHEN + let result = sut.canBookmarkAllOpenTabs() + + // THEN + XCTAssertFalse(result) + } + + func testWhenAtLeastTwoURLTabsOpenThenCanBookmarkAllOpenTabsIsTrue() { + // GIVEN + let sut = TabCollectionViewModel.aTabCollectionViewModel() + let pinnedTab = Tab(content: .url(.aboutDuckDuckGo, credential: nil, source: .ui)) + sut.append(tabs: [ + pinnedTab, + .init(content: .url(.duckDuckGo, credential: nil, source: .ui)), + .init(content: .newtab), + .init(content: .bookmarks), + .init(content: .anySettingsPane), + .init(content: .url(.duckDuckGoEmail, credential: nil, source: .ui)), + ]) + sut.pinTab(at: 1) + XCTAssertEqual(sut.pinnedTabs.count, 1) + XCTAssertEqual(sut.tabViewModels.count, 6) + XCTAssertEqual(sut.pinnedTabs.first, pinnedTab) + XCTAssertNil(sut.tabViewModels[pinnedTab]) + + // WHEN + let result = sut.canBookmarkAllOpenTabs() + + // THEN + XCTAssertTrue(result) + } + } fileprivate extension TabCollectionViewModel { diff --git a/UnitTests/VPNFeedbackForm/VPNFeedbackFormViewModelTests.swift b/UnitTests/VPNFeedbackForm/VPNFeedbackFormViewModelTests.swift index f5f727456e..17b489a705 100644 --- a/UnitTests/VPNFeedbackForm/VPNFeedbackFormViewModelTests.swift +++ b/UnitTests/VPNFeedbackForm/VPNFeedbackFormViewModelTests.swift @@ -103,7 +103,8 @@ private class MockVPNMetadataCollector: VPNMetadataCollector { let vpnState = VPNMetadata.VPNState( onboardingState: "onboarded", connectionState: "connected", - lastErrorMessage: "none", + lastStartErrorDescription: "none", + lastTunnelErrorDescription: "none", connectedServer: "Paoli, PA", connectedServerIP: "123.123.123.123" ) @@ -126,13 +127,20 @@ private class MockVPNMetadataCollector: VPNMetadataCollector { notificationsAgentIsRunning: true ) + let privacyProInfo = VPNMetadata.PrivacyProInfo( + betaParticipant: false, + hasPrivacyProAccount: true, + hasVPNEntitlement: true + ) + return VPNMetadata( appInfo: appInfo, deviceInfo: deviceInfo, networkInfo: networkInfo, vpnState: vpnState, vpnSettingsState: vpnSettingsState, - loginItemState: loginItemState + loginItemState: loginItemState, + privacyProInfo: privacyProInfo ) } diff --git a/package-lock.json b/package-lock.json index b3d356236a..353201dd2a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "macos-browser", "version": "1.0.0", "dependencies": { - "@duckduckgo/autoconsent": "^10.5.0" + "@duckduckgo/autoconsent": "^10.6.1" }, "devDependencies": { "@rollup/plugin-json": "^4.1.0", @@ -53,9 +53,9 @@ } }, "node_modules/@duckduckgo/autoconsent": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/@duckduckgo/autoconsent/-/autoconsent-10.5.0.tgz", - "integrity": "sha512-4mdp9mwBiE+IKTvN84iRA8d7eSkJ5xMaQvhvbgw7XlD1VOJlfiJPhP8PJWV+wyc7DNVHMtcdUXiD+ICw/SJBRA==" + "version": "10.6.1", + "resolved": "https://registry.npmjs.org/@duckduckgo/autoconsent/-/autoconsent-10.6.1.tgz", + "integrity": "sha512-ptgT0sp4zmQTZHAyGR9TN/WJT9W7kTb/yvaF20FwwSIcLKd2xLe2jCDwbGTaLVSqAixWDKqzZ1Dg3l7HE159Sw==" }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.2", diff --git a/package.json b/package.json index dbae81bb89..78eb438846 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,6 @@ "rollup-plugin-terser": "^7.0.2" }, "dependencies": { - "@duckduckgo/autoconsent": "^10.5.0" + "@duckduckgo/autoconsent": "^10.6.1" } } diff --git a/scripts/appcast_manager/appcastManager.swift b/scripts/appcast_manager/appcastManager.swift index c3906a3cae..e7d28209e5 100755 --- a/scripts/appcast_manager/appcastManager.swift +++ b/scripts/appcast_manager/appcastManager.swift @@ -80,6 +80,9 @@ SYNOPSIS 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 --release-to-internal-channel --dmg --release-notes-html [--key ] + appcastManager --release-to-public-channel --version [--release-notes-html ] [--key ] + appcastManager --release-hotfix-to-public-channel --dmg --release-notes-html [--key ] appcastManager --help DESCRIPTION @@ -109,7 +112,13 @@ DESCRIPTION exit(0) case .releaseToInternalChannel, .releaseHotfixToPublicChannel: - guard let dmgPath = arguments.parameters["--dmg"], let releaseNotesPath = arguments.parameters["--release-notes"] else { + guard let dmgPath = arguments.parameters["--dmg"] else { + print("Missing required parameters") + exit(1) + } + let releaseNotesPath = arguments.parameters["--release-notes"] + let releaseNotesHTMLPath = arguments.parameters["--release-notes-html"] + guard releaseNotesPath != nil || releaseNotesHTMLPath != nil else { print("Missing required parameters") exit(1) } @@ -117,7 +126,11 @@ case .releaseToInternalChannel, .releaseHotfixToPublicChannel: print("➡️ Action: Add to internal channel") print("➡️ DMG Path: \(dmgPath)") - print("➡️ Release Notes Path: \(releaseNotesPath)") + if let releaseNotesPath { + print("➡️ Release Notes Path: \(releaseNotesPath)") + } else if let releaseNotesHTMLPath { + print("➡️ Release Notes HTML Path: \(releaseNotesHTMLPath)") + } if isCI, let keyFile { print("➡️ Key file: \(keyFile)") } @@ -130,7 +143,11 @@ case .releaseToInternalChannel, .releaseHotfixToPublicChannel: } // Handle release notes file - handleReleaseNotesFile(path: releaseNotesPath, updatesDirectoryURL: specificDir, dmgURL: dmgURL) + if let releaseNotesPath { + handleReleaseNotesFile(path: releaseNotesPath, updatesDirectoryURL: specificDir, dmgURL: dmgURL) + } else if let releaseNotesHTMLPath { + handleReleaseNotesHTML(path: releaseNotesHTMLPath, updatesDirectoryURL: specificDir, dmgURL: dmgURL) + } // Extract version number from DMG file name let versionNumber = getVersionNumberFromDMGFileName(dmgURL: dmgURL) @@ -170,6 +187,10 @@ case .releaseToPublicChannel: print("Release Notes Path: \(releaseNotesPath)") let dmgURLForPublic = specificDir.appendingPathComponent(dmgFileName) handleReleaseNotesFile(path: releaseNotesPath, updatesDirectoryURL: specificDir, dmgURL: dmgURLForPublic) + } else if let releaseNotesHTMLPath = arguments.parameters["--release-notes-html"] { + print("Release Notes Path: \(releaseNotesHTMLPath)") + let dmgURLForPublic = specificDir.appendingPathComponent(dmgFileName) + handleReleaseNotesHTML(path: releaseNotesHTMLPath, updatesDirectoryURL: specificDir, dmgURL: dmgURLForPublic) } else { print("👀 No new release notes provided. Keeping existing release notes.") } @@ -605,6 +626,27 @@ final class AppcastDownloader { // MARK: - Handling of Release Notes +func handleReleaseNotesHTML(path: String, updatesDirectoryURL: URL, dmgURL: URL) { + // Copy release notes file and rename it to match the dmg filename + let releaseNotesURL = URL(fileURLWithPath: path) + let destinationReleaseNotesURL = updatesDirectoryURL.appendingPathComponent(dmgURL.deletingPathExtension().lastPathComponent + ".html") + + do { + if FileManager.default.fileExists(atPath: destinationReleaseNotesURL.path) { + try FileManager.default.removeItem(at: destinationReleaseNotesURL) + print("Old release notes file removed.") + } + + // Save the converted release notes to the destination file + try FileManager.default.copyItem(at: releaseNotesURL, to: destinationReleaseNotesURL) + print("✅ New release notes HTML file copied to the updates directory.") + + } catch { + print("❌ Failed to copy and convert release notes HTML file: \(error).") + exit(1) + } +} + func handleReleaseNotesFile(path: String, updatesDirectoryURL: URL, dmgURL: URL) { // Copy release notes file and rename it to match the dmg filename let releaseNotesURL = URL(fileURLWithPath: path) diff --git a/scripts/extract_release_notes.sh b/scripts/extract_release_notes.sh index 9c7db812a6..f995620dfe 100755 --- a/scripts/extract_release_notes.sh +++ b/scripts/extract_release_notes.sh @@ -7,30 +7,133 @@ # start_marker="release notes" +pp_marker="^for privacy pro subscribers:?$" end_marker="this release includes:" +placeholder="add release notes here" is_capturing=0 +is_capturing_pp=0 has_content=0 +notes= +pp_notes= -if [[ "$1" == "-t" ]]; then - # capture included tasks instead of release notes - start_marker="this release includes:" - end_marker= -fi +output="html" + +case "$1" in + -a) + # Generate Asana rich text output + output="asana" + ;; + -r) + # Generate raw output instead of HTML + output="raw" + ;; + -t) + # Capture raw included tasks' URLs instead of release notes + output="tasks" + start_marker="this release includes:" + pp_marker= + end_marker= + ;; + *) + ;; +esac + +html_escape() { + local input="$1" + sed -e 's/&/\&/g' -e 's//\>/g' <<< "$input" +} + +make_links() { + local input="$1" + sed -E 's|(https://[^ ]*)|\1|' <<< "$input" +} + +lowercase() { + local input="$1" + tr '[:upper:]' '[:lower:]' <<< "$input" +} + +print_and_exit() { + echo -ne "$notes" + exit 0 +} + +add_to_notes() { + notes+="$1" + if [[ "$output" != "asana" ]]; then + notes+="\\n" + fi +} + +add_to_pp_notes() { + pp_notes+="$1" + if [[ "$output" != "asana" ]]; then + pp_notes+="\\n" + fi +} + +add_release_note() { + local release_note="$1" + local processed_release_note= + if [[ "$output" == "raw" || "$output" == "tasks" ]]; then + processed_release_note="$release_note" + else + processed_release_note="
  • $(make_links "$(html_escape "$release_note")")
  • " + fi + if [[ $is_capturing_pp -eq 1 ]]; then + add_to_pp_notes "$processed_release_note" + else + add_to_notes "$processed_release_note" + fi +} while read -r line do - if [[ $(tr '[:upper:]' '[:lower:]' <<< "$line") == "$start_marker" ]]; then - is_capturing=1 - elif [[ -n "$end_marker" && $(tr '[:upper:]' '[:lower:]' <<< "$line") == "$end_marker" ]]; then - exit 0 - elif [[ $is_capturing -eq 1 && -n "$line" ]]; then - has_content=1 - echo "$line" - fi + # Lowercase each line to compare with markers + lowercase_line="$(lowercase "$line")" + + if [[ "$lowercase_line" == "$start_marker" ]]; then + # Only start capturing here + is_capturing=1 + if [[ "$output" == "asana" ]]; then + add_to_notes "
      " + elif [[ "$output" == "html" ]]; then + # Add HTML header and start the list + add_to_notes "

      What's new

      " + add_to_notes "
        " + fi + elif [[ -n "$pp_marker" && "$lowercase_line" =~ $pp_marker ]]; then + is_capturing_pp=1 + if [[ "$output" == "asana" ]]; then + add_to_pp_notes "

      For Privacy Pro subscribers

        " + elif [[ "$output" == "html" ]]; then + # If we've reached the PP marker, end the list and start the PP list + add_to_pp_notes "
      " + add_to_pp_notes "

      For Privacy Pro subscribers

      " + add_to_pp_notes "
        " + else + add_to_pp_notes "$line" + fi + elif [[ -n "$end_marker" && "$lowercase_line" == "$end_marker" ]]; then + # If we've reached the end marker, check if PP notes are present and not a placeholder, and add them verbatim to notes + # shellcheck disable=SC2076 + if [[ -n "$pp_notes" && ! "$(lowercase "$pp_notes")" =~ "$placeholder" ]]; then + notes+="$pp_notes" # never add extra newline here (that's why we don't use `add_to_notes`) + fi + if [[ "$output" != "raw" ]]; then + # End the list on end marker + add_to_notes "
      " + fi + # Print output and exit + print_and_exit + elif [[ $is_capturing -eq 1 && -n "$line" ]]; then + has_content=1 + add_release_note "$line" + fi done if [[ $has_content -eq 0 ]]; then - exit 1 + exit 1 fi -exit 0 +print_and_exit diff --git a/scripts/tests/extract_release_notes/extract_release_notes.bats b/scripts/tests/extract_release_notes/extract_release_notes.bats new file mode 100644 index 0000000000..49f9a31e40 --- /dev/null +++ b/scripts/tests/extract_release_notes/extract_release_notes.bats @@ -0,0 +1,349 @@ +#!/usr/bin/env bats + +setup() { + DIR="$( cd "$( dirname "$BATS_TEST_FILENAME" )" >/dev/null 2>&1 && pwd )" + # make executables in ./../../ visible to PATH + PATH="$DIR/../..:$PATH" +} + +main() { + bash extract_release_notes.sh "$@" +} + +# +# Functions below define inputs and expected outputs for the tests +# + +# Placeholder release notes with placeholder Privacy Pro section +placeholder() { + local mode="${1:-input}" + case "$mode" in + input) + cat <<-EOF + Note: This task's description is managed automatically. + Only the Release notes section below should be modified manually. + Please do not adjust formatting. + + Release notes + + <-- Add release notes here --> + + For Privacy Pro subscribers + + <-- Add release notes here --> + + This release includes: + EOF + ;; + raw) + cat <<-EOF + <-- Add release notes here --> + EOF + ;; + html) + cat <<-EOF +

      What's new

      +
        +
      • <-- Add release notes here -->
      • +
      + EOF + ;; + asana) + cat <<-EOF +
      • <-- Add release notes here -->
      + EOF + ;; + esac +} + +# Non-empty release notes with non-empty Privacy Pro section +full() { + local mode="${1:-input}" + case "$mode" in + input) + cat <<-EOF + Note: This task's description is managed automatically. + Only the Release notes section below should be modified manually. + Please do not adjust formatting. + + Release notes + + You can now find browser windows listed in the "Window" app menu and in the Dock menu. + We also added "Duplicate Tab" to the app menu so you can use it as an action in Apple Shortcuts. + When watching videos in Duck Player, clicking endscreen recommendations will now open those videos in the same tab. + The bug that duplicated sites in your browsing history has been fixed, and the visual glitching that sometimes occurred during session restore and app launch has been addressed. + + For Privacy Pro subscribers + + VPN updates! More detailed connection info in the VPN dashboard, plus animations and usability improvements. + Visit https://duckduckgo.com/pro for more information. Privacy Pro is currently available to U.S. residents only. + + This release includes: + + https://app.asana.com/0/0/0/f/ + https://app.asana.com/0/0/0/f/ + https://app.asana.com/0/0/0/f/ + EOF + ;; + raw) + cat <<-EOF + You can now find browser windows listed in the "Window" app menu and in the Dock menu. + We also added "Duplicate Tab" to the app menu so you can use it as an action in Apple Shortcuts. + When watching videos in Duck Player, clicking endscreen recommendations will now open those videos in the same tab. + The bug that duplicated sites in your browsing history has been fixed, and the visual glitching that sometimes occurred during session restore and app launch has been addressed. + For Privacy Pro subscribers + VPN updates! More detailed connection info in the VPN dashboard, plus animations and usability improvements. + Visit https://duckduckgo.com/pro for more information. Privacy Pro is currently available to U.S. residents only. + EOF + ;; + html) + cat <<-EOF +

      What's new

      +
        +
      • You can now find browser windows listed in the "Window" app menu and in the Dock menu.
      • +
      • We also added "Duplicate Tab" to the app menu so you can use it as an action in Apple Shortcuts.
      • +
      • When watching videos in Duck Player, clicking endscreen recommendations will now open those videos in the same tab.
      • +
      • The bug that duplicated sites in your browsing history has been fixed, and the visual glitching that sometimes occurred during session restore and app launch has been addressed.
      • +
      +

      For Privacy Pro subscribers

      +
        +
      • VPN updates! More detailed connection info in the VPN dashboard, plus animations and usability improvements.
      • +
      • Visit https://duckduckgo.com/pro for more information. Privacy Pro is currently available to U.S. residents only.
      • +
      + EOF + ;; + asana) + cat <<-EOF +
      • You can now find browser windows listed in the "Window" app menu and in the Dock menu.
      • We also added "Duplicate Tab" to the app menu so you can use it as an action in Apple Shortcuts.
      • When watching videos in Duck Player, clicking endscreen recommendations will now open those videos in the same tab.
      • The bug that duplicated sites in your browsing history has been fixed, and the visual glitching that sometimes occurred during session restore and app launch has been addressed.

      For Privacy Pro subscribers

      • VPN updates! More detailed connection info in the VPN dashboard, plus animations and usability improvements.
      • Visit https://duckduckgo.com/pro for more information. Privacy Pro is currently available to U.S. residents only.
      + EOF + ;; + esac +} + +# Non-empty release notes and missing Privacy Pro section +without_privacy_pro_section() { + local mode="${1:-input}" + case "$mode" in + input) + cat <<-EOF + Note: This task's description is managed automatically. + Only the Release notes section below should be modified manually. + Please do not adjust formatting. + + Release notes + + You can now find browser windows listed in the "Window" app menu and in the Dock menu. + We also added "Duplicate Tab" to the app menu so you can use it as an action in Apple Shortcuts. + When watching videos in Duck Player, clicking endscreen recommendations will now open those videos in the same tab. + The bug that duplicated sites in your browsing history has been fixed, and the visual glitching that sometimes occurred during session restore and app launch has been addressed. + + This release includes: + + https://app.asana.com/0/0/0/f/ + https://app.asana.com/0/0/0/f/ + https://app.asana.com/0/0/0/f/ + EOF + ;; + raw) + cat <<-EOF + You can now find browser windows listed in the "Window" app menu and in the Dock menu. + We also added "Duplicate Tab" to the app menu so you can use it as an action in Apple Shortcuts. + When watching videos in Duck Player, clicking endscreen recommendations will now open those videos in the same tab. + The bug that duplicated sites in your browsing history has been fixed, and the visual glitching that sometimes occurred during session restore and app launch has been addressed. + EOF + ;; + html) + cat <<-EOF +

      What's new

      +
        +
      • You can now find browser windows listed in the "Window" app menu and in the Dock menu.
      • +
      • We also added "Duplicate Tab" to the app menu so you can use it as an action in Apple Shortcuts.
      • +
      • When watching videos in Duck Player, clicking endscreen recommendations will now open those videos in the same tab.
      • +
      • The bug that duplicated sites in your browsing history has been fixed, and the visual glitching that sometimes occurred during session restore and app launch has been addressed.
      • +
      + EOF + ;; + asana) + cat <<-EOF +
      • You can now find browser windows listed in the "Window" app menu and in the Dock menu.
      • We also added "Duplicate Tab" to the app menu so you can use it as an action in Apple Shortcuts.
      • When watching videos in Duck Player, clicking endscreen recommendations will now open those videos in the same tab.
      • The bug that duplicated sites in your browsing history has been fixed, and the visual glitching that sometimes occurred during session restore and app launch has been addressed.
      + EOF + ;; + esac +} + +# Non-empty release notes and a placeholder Privacy Pro section +placeholder_privacy_pro_section() { + local mode="${1:-input}" + case "$mode" in + input) + cat <<-EOF + Note: This task's description is managed automatically. + Only the Release notes section below should be modified manually. + Please do not adjust formatting. + + Release notes + + You can now find browser windows listed in the "Window" app menu and in the Dock menu. + We also added "Duplicate Tab" to the app menu so you can use it as an action in Apple Shortcuts. + When watching videos in Duck Player, clicking endscreen recommendations will now open those videos in the same tab. + The bug that duplicated sites in your browsing history has been fixed, and the visual glitching that sometimes occurred during session restore and app launch has been addressed. + + For Privacy Pro subscribers + + <-- Add release notes here --> + + This release includes: + + https://app.asana.com/0/0/0/f/ + https://app.asana.com/0/0/0/f/ + https://app.asana.com/0/0/0/f/ + EOF + ;; + *) + without_privacy_pro_section "$mode" + ;; + esac +} + +# Non-empty release notes and Privacy Pro release header as a bullet point inside regular release notes +# Privacy Pro section header should be recognized and interpreted as a separate section (like in the full example) +privacy_pro_in_regular_release_notes() { + local mode="${1:-input}" + case "$mode" in + input) + cat <<-EOF + Note: This task's description is managed automatically. + Only the Release notes section below should be modified manually. + Please do not adjust formatting. + + Release notes + + You can now find browser windows listed in the "Window" app menu and in the Dock menu. + We also added "Duplicate Tab" to the app menu so you can use it as an action in Apple Shortcuts. + When watching videos in Duck Player, clicking endscreen recommendations will now open those videos in the same tab. + The bug that duplicated sites in your browsing history has been fixed, and the visual glitching that sometimes occurred during session restore and app launch has been addressed. + For Privacy Pro subscribers + VPN updates! More detailed connection info in the VPN dashboard, plus animations and usability improvements. + Visit https://duckduckgo.com/pro for more information. Privacy Pro is currently available to U.S. residents only. + + This release includes: + + https://app.asana.com/0/0/0/f/ + https://app.asana.com/0/0/0/f/ + https://app.asana.com/0/0/0/f/ + EOF + ;; + *) + full "$mode" + ;; + esac +} + +# +# Test cases start here +# + +# bats test_tags=placeholder, raw +@test "input: placeholder | output: raw" { + run main -r <<< "$(placeholder)" + [ "$status" -eq 0 ] + [ "$output" == "$(placeholder raw)" ] +} + +# bats test_tags=placeholder, html +@test "input: placeholder | output: html" { + run main -h <<< "$(placeholder)" + [ "$status" -eq 0 ] + [ "$output" == "$(placeholder html)" ] +} + +# bats test_tags=placeholder, asana +@test "input: placeholder | output: asana" { + run main -a <<< "$(placeholder)" + [ "$status" -eq 0 ] + [ "$output" == "$(placeholder asana)" ] +} + +# bats test_tags=full, raw +@test "input: full | output: raw" { + run main -r <<< "$(full)" + [ "$status" -eq 0 ] + [ "$output" == "$(full raw)" ] +} + +# bats test_tags=full, html +@test "input: full | output: html" { + run main -h <<< "$(full)" + [ "$status" -eq 0 ] + [ "$output" == "$(full html)" ] +} + +# bats test_tags=full, asana +@test "input: full | output: asana" { + run main -a <<< "$(full)" + [ "$status" -eq 0 ] + [ "$output" == "$(full asana)" ] +} + +# bats test_tags=no-pp, raw +@test "input: without_privacy_pro_section | output: raw" { + run main -r <<< "$(without_privacy_pro_section)" + [ "$status" -eq 0 ] + [ "$output" == "$(without_privacy_pro_section raw)" ] +} + +# bats test_tags=no-pp, html +@test "input: without_privacy_pro_section | output: html" { + run main -h <<< "$(without_privacy_pro_section)" + [ "$status" -eq 0 ] + [ "$output" == "$(without_privacy_pro_section html)" ] +} + +# bats test_tags=no-pp, asana +@test "input: without_privacy_pro_section | output: asana" { + run main -a <<< "$(without_privacy_pro_section)" + [ "$status" -eq 0 ] + [ "$output" == "$(without_privacy_pro_section asana)" ] +} + +# bats test_tags=placeholder-pp, raw +@test "input: placeholder_privacy_pro_section | output: raw" { + run main -r <<< "$(placeholder_privacy_pro_section)" + [ "$status" -eq 0 ] + [ "$output" == "$(placeholder_privacy_pro_section raw)" ] +} + +# bats test_tags=placeholder-pp, html +@test "input: placeholder_privacy_pro_section | output: html" { + run main -h <<< "$(placeholder_privacy_pro_section)" + [ "$status" -eq 0 ] + [ "$output" == "$(placeholder_privacy_pro_section html)" ] +} + +# bats test_tags=placeholder-pp, asana +@test "input: placeholder_privacy_pro_section | output: asana" { + run main -a <<< "$(placeholder_privacy_pro_section)" + [ "$status" -eq 0 ] + [ "$output" == "$(placeholder_privacy_pro_section asana)" ] +} + +# bats test_tags=inline-pp, raw +@test "input: privacy_pro_in_regular_release_notes | output: raw" { + run main -r <<< "$(privacy_pro_in_regular_release_notes)" + [ "$status" -eq 0 ] + [ "$output" == "$(privacy_pro_in_regular_release_notes raw)" ] +} + +# bats test_tags=inline-pp, html +@test "input: privacy_pro_in_regular_release_notes | output: html" { + run main -h <<< "$(privacy_pro_in_regular_release_notes)" + [ "$status" -eq 0 ] + [ "$output" == "$(privacy_pro_in_regular_release_notes html)" ] +} + +# bats test_tags=inline-pp, asana +@test "input: privacy_pro_in_regular_release_notes | output: asana" { + run main -a <<< "$(privacy_pro_in_regular_release_notes)" + [ "$status" -eq 0 ] + [ "$output" == "$(privacy_pro_in_regular_release_notes asana)" ] +} diff --git a/scripts/update_asana_for_release.sh b/scripts/update_asana_for_release.sh index a0aef9c83e..b5610faed7 100755 --- a/scripts/update_asana_for_release.sh +++ b/scripts/update_asana_for_release.sh @@ -48,7 +48,7 @@ fetch_current_release_notes() { 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 + | "${cwd}"/extract_release_notes.sh -a } get_task_id() { @@ -58,19 +58,6 @@ get_task_id() { fi } -construct_release_notes() { - local escaped_release_note - - 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 -} - construct_this_release_includes() { if [[ -n "${task_ids[*]}" ]]; then printf '%s' '
        ' @@ -89,7 +76,7 @@ construct_release_task_description() { printf '%s' 'Please do not adjust formatting.' printf '%s' '

        Release notes

        ' - construct_release_notes + printf '%s' "$release_notes" printf '%s' '

        This release includes:

        ' construct_this_release_includes @@ -105,7 +92,7 @@ construct_release_announcement_task_description() { printf '%s' '
      \n
      ' printf '%s' '

      Release notes

      ' - construct_release_notes + printf '%s' "$release_notes" printf '%s' '\n' printf '%s' '

      This release includes:

      ' @@ -282,10 +269,8 @@ handle_internal_release() { done <<< "$(find_task_urls_in_git_log "$last_release_tag")" # 2. Fetch current release notes from Asana release task. - local release_notes=() - while read -r line; do - release_notes+=("$line") - done <<< "$(fetch_current_release_notes "${release_task_id}")" + local release_notes + release_notes="$(fetch_current_release_notes "${release_task_id}")" # 3. Construct new release task description local html_notes @@ -325,10 +310,8 @@ handle_public_release() { complete_tasks "${task_ids[@]}" # 5. Fetch current release notes from Asana release task. - local release_notes=() - while read -r line; do - release_notes+=("$line") - done <<< "$(fetch_current_release_notes "${release_task_id}")" + local release_notes + release_notes="$(fetch_current_release_notes "${release_task_id}")" # 6. Construct release announcement task description local html_notes