diff --git a/.github/actions/asana-create-action-item/action.yml b/.github/actions/asana-create-action-item/action.yml index 7f4ee3f3c2..b83f2dd1ad 100644 --- a/.github/actions/asana-create-action-item/action.yml +++ b/.github/actions/asana-create-action-item/action.yml @@ -45,7 +45,7 @@ runs: - id: get-asana-user-id if: github.event_name != 'schedule' - uses: duckduckgo/apple-infra/actions/asana-get-user-id-for-github-handle@main + uses: duckduckgo/apple-toolbox/actions/asana-get-user-id-for-github-handle@main with: access-token: ${{ inputs.access-token }} github-handle: ${{ github.actor }} diff --git a/.github/actions/asana-log-message/action.yml b/.github/actions/asana-log-message/action.yml index 288fd832ba..e00cbd2a6d 100644 --- a/.github/actions/asana-log-message/action.yml +++ b/.github/actions/asana-log-message/action.yml @@ -31,7 +31,7 @@ runs: - id: get-asana-user-id if: github.event_name != 'schedule' - uses: duckduckgo/apple-infra/actions/asana-get-user-id-for-github-handle@main + uses: duckduckgo/apple-toolbox/actions/asana-get-user-id-for-github-handle@main with: access-token: ${{ inputs.access-token }} github-handle: ${{ github.actor }} 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..5b7bc475b5 100644 --- a/.github/workflows/code_freeze.yml +++ b/.github/workflows/code_freeze.yml @@ -47,7 +47,7 @@ jobs: - name: Get Asana user ID id: get-asana-user-id - uses: duckduckgo/apple-infra/actions/asana-get-user-id-for-github-handle@main + uses: duckduckgo/apple-toolbox/actions/asana-get-user-id-for-github-handle@main with: access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} github-handle: ${{ github.actor }} @@ -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_variants.yml b/.github/workflows/create_variants.yml index c48424a947..eb3acf62ff 100644 --- a/.github/workflows/create_variants.yml +++ b/.github/workflows/create_variants.yml @@ -143,7 +143,7 @@ jobs: GH_TOKEN: ${{ github.token }} WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} run: | - curl -fLSs $(gh api https://api.github.com/repos/${{ github.repository }}/contents/scripts/assets/variants-release-mm-template.json?ref=${{ github.ref }} --jq .download_url) \ + curl -fLSs $(gh api https://api.github.com/repos/${{ github.repository }}/contents/scripts/assets/variants-release-mm-template.json --jq .download_url) \ --output message-template.json export MM_USER_HANDLE=$(base64 -d <<< ${{ secrets.MM_HANDLES_BASE64 }} | jq ".${{ github.actor }}" | tr -d '"') 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/pr_task_url.yml b/.github/workflows/pr_task_url.yml index 820b61ba14..b5e247e02d 100644 --- a/.github/workflows/pr_task_url.yml +++ b/.github/workflows/pr_task_url.yml @@ -2,7 +2,7 @@ name: Asana PR Task URL on: pull_request: - types: [opened, edited, closed, unlabeled, synchronize] + types: [opened, edited, closed, unlabeled, synchronize, review_requested] jobs: @@ -112,6 +112,24 @@ jobs: if: ${{ needs.assert-project-membership.outputs.task_id }} run: exit ${{ needs.assert-project-membership.outputs.failure }} + # When reviewer is assigned create a subtask in Asana if not existing already + create-asana-pr-subtask-if-needed: + + name: "Create the PR subtask in Asana" + + runs-on: ubuntu-latest + if: github.event.action == 'review_requested' + + needs: [assert-project-membership] + + steps: + - name: Create or Update PR Subtask + uses: duckduckgo/apple-toolbox/actions/asana-create-pr-subtask@main + with: + access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} + asana-task-id: ${{ needs.assert-project-membership.outputs.task_id }} + github-reviewer-user: ${{ github.event.requested_reviewer.login }} + # When a PR is merged, move the task to the Waiting for Release section of the App Board. mark-waiting-for-release: 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 59f3d0e6e2..e94261020d 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 172 +CURRENT_PROJECT_VERSION = 181 diff --git a/Configuration/Version.xcconfig b/Configuration/Version.xcconfig index b517e1e1fb..c23c40bdc0 100644 --- a/Configuration/Version.xcconfig +++ b/Configuration/Version.xcconfig @@ -1 +1 @@ -MARKETING_VERSION = 1.85.0 +MARKETING_VERSION = 1.86.0 diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 9c51811b85..d4a930cc07 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -128,6 +128,8 @@ 1DDF076328F815AD00EDFBE3 /* BWCommunicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDF075D28F815AD00EDFBE3 /* BWCommunicator.swift */; }; 1DDF076428F815AD00EDFBE3 /* BWManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDF075E28F815AD00EDFBE3 /* BWManager.swift */; }; 1DE03425298BC7F000CAB3D7 /* InternalUserDeciderStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D12F2E1298BC660009A65FD /* InternalUserDeciderStoreMock.swift */; }; + 1DEF3BAD2BD145A9004A2FBA /* AutoClearHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DEF3BAC2BD145A9004A2FBA /* AutoClearHandler.swift */; }; + 1DEF3BAE2BD145A9004A2FBA /* AutoClearHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DEF3BAC2BD145A9004A2FBA /* AutoClearHandler.swift */; }; 1DFAB51D2A8982A600A0F7F6 /* SetExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFAB51C2A8982A600A0F7F6 /* SetExtension.swift */; }; 1DFAB51E2A8982A600A0F7F6 /* SetExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFAB51C2A8982A600A0F7F6 /* SetExtension.swift */; }; 1DFAB5222A8983DE00A0F7F6 /* SetExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFAB51F2A89830D00A0F7F6 /* SetExtensionTests.swift */; }; @@ -637,7 +639,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 */; }; @@ -1082,6 +1083,12 @@ 4B1E6EEE27AB5E5100F51793 /* PasswordManagementListSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B1E6EEC27AB5E5100F51793 /* PasswordManagementListSection.swift */; }; 4B1E6EF127AB5E5D00F51793 /* NSPopUpButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B1E6EEF27AB5E5D00F51793 /* NSPopUpButtonView.swift */; }; 4B1E6EF227AB5E5D00F51793 /* PasswordManagementItemList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B1E6EF027AB5E5D00F51793 /* PasswordManagementItemList.swift */; }; + 4B1EFF1C2BD71EEF007CC84F /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 4B1EFF1B2BD71EEF007CC84F /* PixelKit */; }; + 4B1EFF1D2BD71FCA007CC84F /* UserDefaultsWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85C6A29525CC1FFD00EEB5F1 /* UserDefaultsWrapper.swift */; }; + 4B1EFF1E2BD72034007CC84F /* BundleExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6106B9D26A565DA0013B453 /* BundleExtension.swift */; }; + 4B1EFF1F2BD72170007CC84F /* OptionalExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B637273C26CCF0C200C8CB02 /* OptionalExtension.swift */; }; + 4B1EFF212BD72189007CC84F /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = 4B1EFF202BD72189007CC84F /* Networking */; }; + 4B1EFF222BD7223D007CC84F /* NetworkProtectionPixelEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BF0E5042AD2551A00FFEC9E /* NetworkProtectionPixelEvent.swift */; }; 4B25375B2A11BE7300610219 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B4D603E2A0B290200BCD287 /* NetworkExtension.framework */; }; 4B2537722A11BF8B00610219 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B25376F2A11BF8B00610219 /* main.swift */; }; 4B25377A2A11C01700610219 /* UserText+NetworkProtectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D607C2A0B29FA00BCD287 /* UserText+NetworkProtectionExtensions.swift */; }; @@ -1090,7 +1097,6 @@ 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 */; }; @@ -1162,11 +1168,6 @@ 4B4D60C12A0C848E00BCD287 /* NetworkProtectionControllerErrorStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D606A2A0B29FA00BCD287 /* NetworkProtectionControllerErrorStore.swift */; }; 4B4D60C22A0C849000BCD287 /* EventMapping+NetworkProtectionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60722A0B29FA00BCD287 /* EventMapping+NetworkProtectionError.swift */; }; 4B4D60C32A0C849100BCD287 /* EventMapping+NetworkProtectionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60722A0B29FA00BCD287 /* EventMapping+NetworkProtectionError.swift */; }; - 4B4D60C42A0C849600BCD287 /* NetworkProtectionInvitePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D606F2A0B29FA00BCD287 /* NetworkProtectionInvitePresenter.swift */; }; - 4B4D60C62A0C849600BCD287 /* NetworkProtectionInviteCodeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60702A0B29FA00BCD287 /* NetworkProtectionInviteCodeViewModel.swift */; }; - 4B4D60CA2A0C849600BCD287 /* NetworkProtectionInvitePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D606F2A0B29FA00BCD287 /* NetworkProtectionInvitePresenter.swift */; }; - 4B4D60CC2A0C849600BCD287 /* NetworkProtectionInviteCodeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60702A0B29FA00BCD287 /* NetworkProtectionInviteCodeViewModel.swift */; }; - 4B4D60CF2A0C849600BCD287 /* NetworkProtectionInviteDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D606C2A0B29FA00BCD287 /* NetworkProtectionInviteDialog.swift */; }; 4B4D60D32A0C84F700BCD287 /* UserText+NetworkProtection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60D22A0C84F700BCD287 /* UserText+NetworkProtection.swift */; }; 4B4D60D42A0C84F700BCD287 /* UserText+NetworkProtection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60D22A0C84F700BCD287 /* UserText+NetworkProtection.swift */; }; 4B4D60DD2A0C875E00BCD287 /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; @@ -1209,7 +1210,6 @@ 4B723E1226B0006E00E14D75 /* DataImport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B723DEB26B0002B00E14D75 /* DataImport.swift */; }; 4B723E1326B0007A00E14D75 /* CSVLoginExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B723DFD26B0002B00E14D75 /* CSVLoginExporter.swift */; }; 4B723E1926B000DC00E14D75 /* TemporaryFileCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B723E1726B000DC00E14D75 /* TemporaryFileCreator.swift */; }; - 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 */; }; 4B85A48028821CC500FC4C39 /* NSPasteboardItemExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B85A47F28821CC500FC4C39 /* NSPasteboardItemExtension.swift */; }; @@ -1272,8 +1272,6 @@ 4B9DB02A2A983B24000927DB /* WaitlistStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB00E2A983B24000927DB /* WaitlistStorage.swift */; }; 4B9DB02C2A983B24000927DB /* WaitlistKeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB00F2A983B24000927DB /* WaitlistKeychainStorage.swift */; }; 4B9DB02D2A983B24000927DB /* WaitlistKeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB00F2A983B24000927DB /* WaitlistKeychainStorage.swift */; }; - 4B9DB0322A983B24000927DB /* EnableWaitlistFeatureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB0132A983B24000927DB /* EnableWaitlistFeatureView.swift */; }; - 4B9DB0332A983B24000927DB /* EnableWaitlistFeatureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB0132A983B24000927DB /* EnableWaitlistFeatureView.swift */; }; 4B9DB0352A983B24000927DB /* WaitlistTermsAndConditionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB0142A983B24000927DB /* WaitlistTermsAndConditionsView.swift */; }; 4B9DB0362A983B24000927DB /* WaitlistTermsAndConditionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB0142A983B24000927DB /* WaitlistTermsAndConditionsView.swift */; }; 4B9DB0382A983B24000927DB /* JoinedWaitlistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB0152A983B24000927DB /* JoinedWaitlistView.swift */; }; @@ -1294,8 +1292,6 @@ 4B9DB0552A983B55000927DB /* MockWaitlistStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB04F2A983B55000927DB /* MockWaitlistStorage.swift */; }; 4B9DB0562A983B55000927DB /* MockNotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB0502A983B55000927DB /* MockNotificationService.swift */; }; 4B9DB0572A983B55000927DB /* MockNotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB0502A983B55000927DB /* MockNotificationService.swift */; }; - 4B9DB0582A983B55000927DB /* MockNetworkProtectionCodeRedeemer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB0512A983B55000927DB /* MockNetworkProtectionCodeRedeemer.swift */; }; - 4B9DB0592A983B55000927DB /* MockNetworkProtectionCodeRedeemer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB0512A983B55000927DB /* MockNetworkProtectionCodeRedeemer.swift */; }; 4B9DB05A2A983B55000927DB /* MockWaitlistRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB0522A983B55000927DB /* MockWaitlistRequest.swift */; }; 4B9DB05B2A983B55000927DB /* MockWaitlistRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB0522A983B55000927DB /* MockWaitlistRequest.swift */; }; 4B9DB05C2A983B55000927DB /* WaitlistViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB0532A983B55000927DB /* WaitlistViewModelTests.swift */; }; @@ -1361,7 +1357,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,6 +1478,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 */; }; + 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 */; }; @@ -1496,7 +1495,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 */; }; 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 */; }; @@ -1550,12 +1548,10 @@ 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 */; }; - 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 */; }; 7BFE95562A9DF29B0081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFE95532A9DF2930081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift */; }; 7BFE95592A9DF2AF0081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFE95532A9DF2930081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift */; }; - 7BFE955A2A9DF4550081ABE9 /* NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFE95512A9DF1CE0081ABE9 /* NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift */; }; 7BFF850F2B0C09DA00ECACA2 /* DuckDuckGo Personal Information Removal.app in Embed Login Items */ = {isa = PBXBuildFile; fileRef = 9D9AE8D12AAA39A70026E7DC /* DuckDuckGo Personal Information Removal.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 85012B0229133F9F003D0DCC /* NavigationBarPopovers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85012B0129133F9F003D0DCC /* NavigationBarPopovers.swift */; }; 850E8DFB2A6FEC5E00691187 /* BookmarksBarAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850E8DFA2A6FEC5E00691187 /* BookmarksBarAppearance.swift */; }; @@ -1694,6 +1690,14 @@ 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 */; }; @@ -1726,10 +1730,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 */; }; @@ -1740,8 +1754,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 */; }; @@ -2490,6 +2514,8 @@ EEA3EEB12B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA3EEB02B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift */; }; EEA3EEB32B24EC0600E8333A /* VPNLocationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA3EEB22B24EC0600E8333A /* VPNLocationViewModel.swift */; }; EEAD7A7C2A1D3E20002A24E7 /* AppLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD7A6E2A1D3E1F002A24E7 /* AppLauncher.swift */; }; + EEBCA0C62BD7CE2C004DF19C /* VPNFailureRecoveryPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEBCA0C52BD7CE2C004DF19C /* VPNFailureRecoveryPixel.swift */; }; + EEBCA0C72BD7CE2C004DF19C /* VPNFailureRecoveryPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEBCA0C52BD7CE2C004DF19C /* VPNFailureRecoveryPixel.swift */; }; EEBCE6822BA444FA00B9DF00 /* XCUIElementExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEBCE6812BA444FA00B9DF00 /* XCUIElementExtension.swift */; }; EEBCE6832BA463DD00B9DF00 /* NSImageExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B139AFC26B60BD800894F82 /* NSImageExtensions.swift */; }; EEBCE6842BA4643200B9DF00 /* NSSizeExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC5E4E325D6BA9C007F5990 /* NSSizeExtension.swift */; }; @@ -2797,6 +2823,7 @@ 1DDF075F28F815AD00EDFBE3 /* BWStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BWStatus.swift; sourceTree = ""; }; 1DDF076028F815AD00EDFBE3 /* BWError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BWError.swift; sourceTree = ""; }; 1DDF076128F815AD00EDFBE3 /* BWResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BWResponse.swift; sourceTree = ""; }; + 1DEF3BAC2BD145A9004A2FBA /* AutoClearHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutoClearHandler.swift; sourceTree = ""; }; 1DFAB51C2A8982A600A0F7F6 /* SetExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetExtension.swift; sourceTree = ""; }; 1DFAB51F2A89830D00A0F7F6 /* SetExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetExtensionTests.swift; sourceTree = ""; }; 1E0C72052ABC63BD00802009 /* SubscriptionPagesUserScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionPagesUserScript.swift; sourceTree = ""; }; @@ -3031,9 +3058,6 @@ 4B4D60652A0B29FA00BCD287 /* NetworkProtectionNavBarButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionNavBarButtonModel.swift; sourceTree = ""; }; 4B4D60692A0B29FA00BCD287 /* NetworkProtection+ConvenienceInitializers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NetworkProtection+ConvenienceInitializers.swift"; sourceTree = ""; }; 4B4D606A2A0B29FA00BCD287 /* NetworkProtectionControllerErrorStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionControllerErrorStore.swift; sourceTree = ""; }; - 4B4D606C2A0B29FA00BCD287 /* NetworkProtectionInviteDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionInviteDialog.swift; sourceTree = ""; }; - 4B4D606F2A0B29FA00BCD287 /* NetworkProtectionInvitePresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionInvitePresenter.swift; sourceTree = ""; }; - 4B4D60702A0B29FA00BCD287 /* NetworkProtectionInviteCodeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionInviteCodeViewModel.swift; sourceTree = ""; }; 4B4D60722A0B29FA00BCD287 /* EventMapping+NetworkProtectionError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EventMapping+NetworkProtectionError.swift"; sourceTree = ""; }; 4B4D60762A0B29FA00BCD287 /* NetworkProtectionUNNotificationsPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionUNNotificationsPresenter.swift; sourceTree = ""; }; 4B4D607C2A0B29FA00BCD287 /* UserText+NetworkProtectionExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserText+NetworkProtectionExtensions.swift"; sourceTree = ""; }; @@ -3128,7 +3152,6 @@ 4B9DB00C2A983B24000927DB /* WaitlistViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WaitlistViewModel.swift; sourceTree = ""; }; 4B9DB00E2A983B24000927DB /* WaitlistStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WaitlistStorage.swift; sourceTree = ""; }; 4B9DB00F2A983B24000927DB /* WaitlistKeychainStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WaitlistKeychainStorage.swift; sourceTree = ""; }; - 4B9DB0132A983B24000927DB /* EnableWaitlistFeatureView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnableWaitlistFeatureView.swift; sourceTree = ""; }; 4B9DB0142A983B24000927DB /* WaitlistTermsAndConditionsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WaitlistTermsAndConditionsView.swift; sourceTree = ""; }; 4B9DB0152A983B24000927DB /* JoinedWaitlistView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JoinedWaitlistView.swift; sourceTree = ""; }; 4B9DB0162A983B24000927DB /* InvitedToWaitlistView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InvitedToWaitlistView.swift; sourceTree = ""; }; @@ -3139,7 +3162,6 @@ 4B9DB01C2A983B24000927DB /* NotificationService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; 4B9DB04F2A983B55000927DB /* MockWaitlistStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockWaitlistStorage.swift; sourceTree = ""; }; 4B9DB0502A983B55000927DB /* MockNotificationService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockNotificationService.swift; sourceTree = ""; }; - 4B9DB0512A983B55000927DB /* MockNetworkProtectionCodeRedeemer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockNetworkProtectionCodeRedeemer.swift; sourceTree = ""; }; 4B9DB0522A983B55000927DB /* MockWaitlistRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockWaitlistRequest.swift; sourceTree = ""; }; 4B9DB0532A983B55000927DB /* WaitlistViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WaitlistViewModelTests.swift; sourceTree = ""; }; 4BA1A69A258B076900F6F690 /* FileStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileStore.swift; sourceTree = ""; }; @@ -3192,7 +3214,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 = ""; }; 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 = ""; }; @@ -3261,6 +3282,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 = ""; }; @@ -3302,7 +3324,6 @@ 7BEC20402B0F505F00243D3E /* AddBookmarkPopoverView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddBookmarkPopoverView.swift; sourceTree = ""; }; 7BEC20412B0F505F00243D3E /* AddBookmarkFolderPopoverView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddBookmarkFolderPopoverView.swift; sourceTree = ""; }; 7BF1A9D72AE054D300FCA683 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 7BFE95512A9DF1CE0081ABE9 /* NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift; sourceTree = ""; }; 7BFE95532A9DF2930081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UserDefaults+NetworkProtectionWaitlist.swift"; sourceTree = ""; }; 85012B0129133F9F003D0DCC /* NavigationBarPopovers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarPopovers.swift; sourceTree = ""; }; 850E8DFA2A6FEC5E00691187 /* BookmarksBarAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksBarAppearance.swift; sourceTree = ""; }; @@ -3419,6 +3440,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 = ""; }; @@ -3435,14 +3460,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 = ""; }; @@ -3979,6 +4014,7 @@ EEA3EEB02B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionVPNCountryLabelsModel.swift; sourceTree = ""; }; EEA3EEB22B24EC0600E8333A /* VPNLocationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VPNLocationViewModel.swift; sourceTree = ""; }; EEAD7A6E2A1D3E1F002A24E7 /* AppLauncher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppLauncher.swift; sourceTree = ""; }; + EEBCA0C52BD7CE2C004DF19C /* VPNFailureRecoveryPixel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VPNFailureRecoveryPixel.swift; sourceTree = ""; }; EEBCE6812BA444FA00B9DF00 /* XCUIElementExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XCUIElementExtension.swift; sourceTree = ""; }; EEC111E3294D06020086524F /* JSAlert.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = JSAlert.storyboard; sourceTree = ""; }; EEC111E5294D06290086524F /* JSAlertViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSAlertViewModel.swift; sourceTree = ""; }; @@ -4132,7 +4168,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 4B1EFF1C2BD71EEF007CC84F /* PixelKit in Frameworks */, 7B624F172BA25C1F00A6C544 /* NetworkProtectionUI in Frameworks */, + 4B1EFF212BD72189007CC84F /* Networking in Frameworks */, 37269F052B3332C2005E8E46 /* Common in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4621,6 +4659,7 @@ 566B195F29CDB7A9007E38F4 /* Mocks */, 378205FA283C277800D1D4AA /* MainMenuTests.swift */, 566B195C29CDB692007E38F4 /* MoreOptionsMenuTests.swift */, + 9F9C49F52BC786790099738D /* MoreOptionsMenu+BookmarksTests.swift */, ); path = Menus; sourceTree = ""; @@ -5035,7 +5074,6 @@ children = ( 4B4D60722A0B29FA00BCD287 /* EventMapping+NetworkProtectionError.swift */, BDE981DB2BBD110800645880 /* Assets */, - 4B4D606B2A0B29FA00BCD287 /* Invite */, 7BD3AF5C2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift */, 9D9AE8682AA76CDC0026E7DC /* LoginItem+NetworkProtection.swift */, B602E81C2A1E25B0006D261F /* NEOnDemandRuleExtension.swift */, @@ -5049,7 +5087,6 @@ 7B05829D2A812AC000AC3F7C /* NetworkProtectionOnboardingMenu.swift */, 7B430EA02A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift */, 4B8F52402A18326600BE7131 /* NetworkProtectionTunnelController.swift */, - 7BFE95512A9DF1CE0081ABE9 /* NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift */, EEA3EEAF2B24EB5100E8333A /* VPNLocation */, 7B934C3D2A866CFF00FC8F9C /* NetworkProtectionOnboardingMenu.swift */, B6F1B02D2BCE6B47005E863C /* TunnelControllerProvider.swift */, @@ -5057,16 +5094,6 @@ path = BothAppTargets; sourceTree = ""; }; - 4B4D606B2A0B29FA00BCD287 /* Invite */ = { - isa = PBXGroup; - children = ( - 4B4D606C2A0B29FA00BCD287 /* NetworkProtectionInviteDialog.swift */, - 4B4D606F2A0B29FA00BCD287 /* NetworkProtectionInvitePresenter.swift */, - 4B4D60702A0B29FA00BCD287 /* NetworkProtectionInviteCodeViewModel.swift */, - ); - path = Invite; - sourceTree = ""; - }; 4B4D60742A0B29FA00BCD287 /* NetworkExtensionTargets */ = { isa = PBXGroup; children = ( @@ -5105,6 +5132,7 @@ 4B4D607D2A0B29FA00BCD287 /* NetworkExtensionTargets */ = { isa = PBXGroup; children = ( + EEBCA0C12BD7CDDA004DF19C /* Pixels */, 4B41ED9F2B15437A001EEDF4 /* NetworkProtectionNotificationsPresenterFactory.swift */, EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */, 7B0099802B65C6B300FE7C31 /* MacTransparentProxyProvider.swift */, @@ -5421,7 +5449,6 @@ 4B9DB0122A983B24000927DB /* WaitlistSteps */ = { isa = PBXGroup; children = ( - 4B9DB0132A983B24000927DB /* EnableWaitlistFeatureView.swift */, 4B9DB0142A983B24000927DB /* WaitlistTermsAndConditionsView.swift */, 4B9DB0152A983B24000927DB /* JoinedWaitlistView.swift */, 4B9DB0162A983B24000927DB /* InvitedToWaitlistView.swift */, @@ -5454,7 +5481,6 @@ 317295D02AF058D3002C3206 /* MockWaitlistTermsAndConditionsActionHandler.swift */, 4B9DB04F2A983B55000927DB /* MockWaitlistStorage.swift */, 4B9DB0502A983B55000927DB /* MockNotificationService.swift */, - 4B9DB0512A983B55000927DB /* MockNetworkProtectionCodeRedeemer.swift */, 4B9DB0522A983B55000927DB /* MockWaitlistRequest.swift */, ); path = Mocks; @@ -6218,6 +6244,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 = ( @@ -6229,9 +6265,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 = ""; @@ -6249,6 +6288,7 @@ 9F56CFA82B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift */, 9FEE98642B846870002E44E8 /* AddEditBookmarkView.swift */, 9F56CFB02B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift */, + 9F9C49F82BC7BC970099738D /* BookmarkAllTabsDialogView.swift */, ); path = Dialog; sourceTree = ""; @@ -6261,6 +6301,14 @@ path = Factory; sourceTree = ""; }; + 9FAD62382BCFDB1D007F3A65 /* Helpers */ = { + isa = PBXGroup; + children = ( + 9FAD62392BCFDB32007F3A65 /* WebsiteInfoHelpers.swift */, + ); + path = Helpers; + sourceTree = ""; + }; AA0877B626D515EE00B05660 /* UserAgent */ = { isa = PBXGroup; children = ( @@ -6346,6 +6394,7 @@ 858A798226A8B75F00A75A42 /* CopyHandler.swift */, 1D36E65A298ACD2900AA485D /* AppIconChanger.swift */, CB24F70B29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift */, + 1DEF3BAC2BD145A9004A2FBA /* AutoClearHandler.swift */, ); path = Application; sourceTree = ""; @@ -6610,6 +6659,7 @@ AA652CAB25DD820D009059CC /* Bookmarks */ = { isa = PBXGroup; children = ( + 9FAD62382BCFDB1D007F3A65 /* Helpers */, 9F872D9B2B9058B000138637 /* Extensions */, 9FA75A3C2BA00DF500DA5FA6 /* Factory */, 9F982F102B82264400231028 /* ViewModels */, @@ -6636,6 +6686,7 @@ 98A95D87299A2DF900B9B81A /* BookmarkMigrationTests.swift */, 9F872D9F2B90644800138637 /* ContextualMenuTests.swift */, 9F0A2CF72B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift */, + 9FAD623C2BD09DE5007F3A65 /* WebsiteInfoTests.swift */, ); path = Model; sourceTree = ""; @@ -6645,6 +6696,8 @@ children = ( AA652CB025DD825B009059CC /* LocalBookmarkStoreTests.swift */, 986189E52A7CFB3E001B4519 /* LocalBookmarkStoreSavingTests.swift */, + 9FA5A0AC2BC9037A00153786 /* BookmarkFolderStoreMock.swift */, + 9F8D57312BCCCB9A00AEA660 /* UserDefaultsBookmarkFoldersStoreTests.swift */, ); path = Services; sourceTree = ""; @@ -7029,6 +7082,8 @@ 9F56CFAC2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift */, 9FEE98682B85B869002E44E8 /* BookmarksDialogViewModel.swift */, 9FEE986C2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift */, + 9F9C4A002BC7F36D0099738D /* BookmarkAllTabsDialogCoordinatorViewModel.swift */, + 9F9C49FC2BC7E9820099738D /* BookmarkAllTabsDialogViewModel.swift */, ); path = ViewModel; sourceTree = ""; @@ -7151,6 +7206,7 @@ B6DA06E52913F39400225DE2 /* MenuItemSelectors.swift */, AAC5E4D625D6A710007F5990 /* BookmarkStore.swift */, 987799F829999973005D8EB6 /* LocalBookmarkStore.swift */, + 9FA5A0A42BC8F34900153786 /* UserDefaultsBookmarkFoldersStore.swift */, ); path = Services; sourceTree = ""; @@ -7292,7 +7348,6 @@ B6B3E0DC2657E9CF0040E0A2 /* NSScreenExtension.swift */, AAC5E4E325D6BA9C007F5990 /* NSSizeExtension.swift */, 4B39AAF527D9B2C700A73FD5 /* NSStackViewExtension.swift */, - 4BE0DF0426781961006337B7 /* NSStoryboardExtension.swift */, AA5C8F58258FE21F00748EB7 /* NSTextFieldExtension.swift */, 858A798426A8BB5D00A75A42 /* NSTextViewExtension.swift */, 4B0511E0262CAA8600F6079C /* NSViewControllerExtension.swift */, @@ -8030,6 +8085,14 @@ path = JSAlert; sourceTree = ""; }; + EEBCA0C12BD7CDDA004DF19C /* Pixels */ = { + isa = PBXGroup; + children = ( + EEBCA0C52BD7CE2C004DF19C /* VPNFailureRecoveryPixel.swift */, + ); + path = Pixels; + sourceTree = ""; + }; EEBCE6802BA444FA00B9DF00 /* Common */ = { isa = PBXGroup; children = ( @@ -8046,6 +8109,7 @@ 4BF0E5042AD2551A00FFEC9E /* NetworkProtectionPixelEvent.swift */, 7B934C402A866DD400FC8F9C /* UserDefaults+NetworkProtectionShared.swift */, 7BFE95532A9DF2930081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift */, + 7B4D8A202BDA857300852966 /* VPNOperationErrorRecorder.swift */, ); path = AppAndExtensionAndAgentTargets; sourceTree = ""; @@ -8327,6 +8391,8 @@ packageProductDependencies = ( 37269F042B3332C2005E8E46 /* Common */, 7B624F162BA25C1F00A6C544 /* NetworkProtectionUI */, + 4B1EFF1B2BD71EEF007CC84F /* PixelKit */, + 4B1EFF202BD72189007CC84F /* Networking */, ); productName = DuckDuckGoNotifications; productReference = 4B4BEC202A11B4E2001D9AC5 /* DuckDuckGo Notifications.app */; @@ -9383,7 +9449,6 @@ 373D9B4929EEAC1B00381FDD /* SyncMetadataDatabase.swift in Sources */, 3706FAB8293F65D500E42796 /* FaviconImageCache.swift in Sources */, 3706FAB9293F65D500E42796 /* TabBarViewController.swift in Sources */, - 4B9DB0332A983B24000927DB /* EnableWaitlistFeatureView.swift in Sources */, 3706FABA293F65D500E42796 /* BookmarkOutlineViewDataSource.swift in Sources */, 3706FABB293F65D500E42796 /* PasswordManagementBitwardenItemView.swift in Sources */, 1D220BF92B86192200F8BBC6 /* PreferencesEmailProtectionView.swift in Sources */, @@ -9435,6 +9500,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 */, @@ -9565,7 +9631,6 @@ 1ED910D62B63BFB300936947 /* IdentityTheftRestorationPagesUserScript.swift in Sources */, 3706FB40293F65D500E42796 /* ContextualMenu.swift in Sources */, 3706FB41293F65D500E42796 /* NavigationBarViewController.swift in Sources */, - 4B7534CC2A1FD7EA00158A99 /* NetworkProtectionInviteDialog.swift in Sources */, 3707C71C294B5D1900682A9F /* TabExtensionsBuilder.swift in Sources */, 3706FB42293F65D500E42796 /* MainViewController.swift in Sources */, 3706FB43293F65D500E42796 /* DuckPlayer.swift in Sources */, @@ -9627,7 +9692,6 @@ 3706FB6C293F65D500E42796 /* FaviconStore.swift in Sources */, 3706FB6D293F65D500E42796 /* SuggestionListCharacteristics.swift in Sources */, 377D801F2AB48191002AF251 /* FavoritesDisplayModeSyncHandler.swift in Sources */, - 4B4D60C62A0C849600BCD287 /* NetworkProtectionInviteCodeViewModel.swift in Sources */, 3706FB6F293F65D500E42796 /* BookmarkListViewController.swift in Sources */, 3706FB70293F65D500E42796 /* SecureVaultLoginImporter.swift in Sources */, 3706FB72293F65D500E42796 /* RecentlyClosedCoordinator.swift in Sources */, @@ -9711,11 +9775,11 @@ 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 */, @@ -9773,6 +9837,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 */, @@ -9968,6 +10033,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,7 +10084,6 @@ 3706FC71293F65D500E42796 /* NSColorExtension.swift in Sources */, 1DB9618229F67F6100CF5568 /* FaviconNullStore.swift in Sources */, 3706FC73293F65D500E42796 /* AddressBarButtonsViewController.swift in Sources */, - 7BFE955A2A9DF4550081ABE9 /* NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift in Sources */, 9FDA6C222B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */, C1372EF52BBC5BAD003F8793 /* SecureTextField.swift in Sources */, 316913242BD2B6250051B46D /* DataBrokerProtectionPixelsHandler.swift in Sources */, @@ -10057,7 +10122,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 */, @@ -10066,6 +10130,7 @@ 3706FC96293F65D500E42796 /* HorizontallyCenteredLayout.swift in Sources */, 3706FC97293F65D500E42796 /* BookmarksOutlineView.swift in Sources */, 3706FC98293F65D500E42796 /* CountryList.swift in Sources */, + 1DEF3BAE2BD145A9004A2FBA /* AutoClearHandler.swift in Sources */, 4B37EE732B4CFF0800A89A61 /* HomePageRemoteMessagingStorage.swift in Sources */, 3706FC99293F65D500E42796 /* PreferencesSection.swift in Sources */, B6C8CAA82AD010DD0060E1CD /* YandexDataImporter.swift in Sources */, @@ -10079,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 */, @@ -10093,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 */, @@ -10125,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 */, @@ -10182,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 */, @@ -10213,10 +10283,12 @@ B630E80129C887ED00363609 /* NSErrorAdditionalInfo.swift in Sources */, 3706FE31293F661700E42796 /* TabCollectionViewModelDelegateMock.swift in Sources */, 3706FE32293F661700E42796 /* BookmarksHTMLReaderTests.swift in Sources */, + 9F0FFFBC2BCCAEC2007C87DD /* AddEditBookmarkFolderDialogViewModelMock.swift in Sources */, 3706FE33293F661700E42796 /* FireTests.swift in Sources */, B60C6F8229B1B4AD007BFAA8 /* TestRunHelper.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 */, @@ -10224,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 */, @@ -10246,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 */, @@ -10271,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 */, @@ -10286,7 +10361,7 @@ 3706FE5E293F661700E42796 /* DataImportMocks.swift in Sources */, 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 */, @@ -10437,7 +10512,7 @@ 4B25377A2A11C01700610219 /* UserText+NetworkProtectionExtensions.swift in Sources */, B65DA5F42A77D3FA00CBEE8D /* BundleExtension.swift in Sources */, EE66418D2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift in Sources */, - 4B2D062D2A11C12300DE1F49 /* Logging.swift in Sources */, + EEBCA0C72BD7CE2C004DF19C /* VPNFailureRecoveryPixel.swift in Sources */, 7B2E52252A5FEC09000C6D39 /* NetworkProtectionAgentNotificationsPresenter.swift in Sources */, B602E8232A1E260E006D261F /* Bundle+NetworkProtectionExtensions.swift in Sources */, 4B2D062A2A11C0C900DE1F49 /* NetworkProtectionOptionKeyExtension.swift in Sources */, @@ -10474,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 */, @@ -10510,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 */, @@ -10536,9 +10613,13 @@ 4B4BEC432A11B5C7001D9AC5 /* Bundle+VPN.swift in Sources */, 4B4BEC452A11B5EE001D9AC5 /* UserText+NetworkProtectionExtensions.swift in Sources */, 4B4BEC422A11B5C7001D9AC5 /* NetworkProtectionOptionKeyExtension.swift in Sources */, + 4B1EFF1D2BD71FCA007CC84F /* UserDefaultsWrapper.swift in Sources */, B602E8222A1E2603006D261F /* Bundle+NetworkProtectionExtensions.swift in Sources */, + 4B1EFF222BD7223D007CC84F /* NetworkProtectionPixelEvent.swift in Sources */, B602E81A2A1E2570006D261F /* URL+NetworkProtection.swift in Sources */, + 4B1EFF1F2BD72170007CC84F /* OptionalExtension.swift in Sources */, EEAD7A7C2A1D3E20002A24E7 /* AppLauncher.swift in Sources */, + 4B1EFF1E2BD72034007CC84F /* BundleExtension.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10551,6 +10632,7 @@ 4B4D609F2A0B2C7300BCD287 /* Logging.swift in Sources */, EE66418C2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift in Sources */, 7B7DFB202B7E736B009EA1A3 /* MacPacketTunnelProvider.swift in Sources */, + EEBCA0C62BD7CE2C004DF19C /* VPNFailureRecoveryPixel.swift in Sources */, 4B4D60A12A0B2D6100BCD287 /* NetworkProtectionOptionKeyExtension.swift in Sources */, B602E8182A1E2570006D261F /* URL+NetworkProtection.swift in Sources */, B65DA5F52A77D3FA00CBEE8D /* BundleExtension.swift in Sources */, @@ -10599,7 +10681,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 */, @@ -10655,6 +10736,7 @@ 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 */, @@ -10752,7 +10834,6 @@ 4B9DB03B2A983B24000927DB /* InvitedToWaitlistView.swift in Sources */, 8589063C267BCDC000D23B0D /* SaveCredentialsViewController.swift in Sources */, 4BBE0AA727B9B027003B37A8 /* PopUpButton.swift in Sources */, - 4B4D60CF2A0C849600BCD287 /* NetworkProtectionInviteDialog.swift in Sources */, AABEE6A524AA0A7F0043105B /* SuggestionViewController.swift in Sources */, 1D6216B229069BBF00386B2C /* BWKeyStorage.swift in Sources */, AA7E919F287872EA00AB6B62 /* VisitViewModel.swift in Sources */, @@ -10875,6 +10956,7 @@ 37054FCE2876472D00033B6F /* WebViewSnapshotView.swift in Sources */, 4BBC16A027C4859400E00A38 /* DeviceAuthenticationService.swift in Sources */, CB24F70C29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift in Sources */, + 1DEF3BAD2BD145A9004A2FBA /* AutoClearHandler.swift in Sources */, 37CBCA9A2A8966E60050218F /* SyncSettingsAdapter.swift in Sources */, EEC4A66D2B2C894F00F7C0AA /* VPNLocationPreferenceItemModel.swift in Sources */, 3776582F27F82E62009A6B35 /* AutofillPreferences.swift in Sources */, @@ -10921,7 +11003,6 @@ 3184AC6F288F2A1100C35E4B /* CookieNotificationAnimationModel.swift in Sources */, 4B9DB0382A983B24000927DB /* JoinedWaitlistView.swift in Sources */, B63ED0E526BB8FB900A9DAD1 /* SharingMenu.swift in Sources */, - 4B9DB0322A983B24000927DB /* EnableWaitlistFeatureView.swift in Sources */, AA4FF40C2624751A004E2377 /* GrammarFeaturesManager.swift in Sources */, 4B9DB0442A983B24000927DB /* WaitlistModalViewController.swift in Sources */, B6DA06E8291401D700225DE2 /* WKMenuItemIdentifier.swift in Sources */, @@ -11104,7 +11185,6 @@ AAE246F32709EF3B00BEEAEE /* FirePopoverCollectionViewItem.swift in Sources */, 4B41EDA32B1543B9001EEDF4 /* VPNPreferencesModel.swift in Sources */, AA61C0D22727F59B00E6B681 /* ArrayExtension.swift in Sources */, - 4B4D60CC2A0C849600BCD287 /* NetworkProtectionInviteCodeViewModel.swift in Sources */, 373A1AB02842C4EA00586521 /* BookmarkHTMLImporter.swift in Sources */, B6B5F57F2B024105008DB58A /* DataImportSummaryView.swift in Sources */, 31C3CE0228EDC1E70002C24A /* CustomRoundedCornersShape.swift in Sources */, @@ -11164,6 +11244,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 */, @@ -11231,8 +11312,8 @@ 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 */, AA5C1DD3285A217F0089850C /* RecentlyClosedCacheItem.swift in Sources */, B6BBF17427475B15004F850E /* PopupBlockedPopover.swift in Sources */, @@ -11253,6 +11334,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 */, @@ -11310,7 +11392,6 @@ B687B7CA2947A029001DEA6F /* ContentBlockingTabExtension.swift in Sources */, 85B7184C27677C6500B4277F /* OnboardingViewController.swift in Sources */, 4B379C1E27BDB7FF008A968E /* DeviceAuthenticator.swift in Sources */, - 7BFE95522A9DF1CE0081ABE9 /* NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift in Sources */, 1456D6E124EFCBC300775049 /* TabBarCollectionView.swift in Sources */, 4B4D60BF2A0C848A00BCD287 /* NetworkProtection+ConvenienceInitializers.swift in Sources */, 4B6B64842BA930420009FF9F /* WaitlistThankYouPromptPresenter.swift in Sources */, @@ -11349,6 +11430,7 @@ 853014D625E671A000FB8205 /* PageObserverUserScript.swift in Sources */, B677FC4F2B06376B0099EB04 /* ReportFeedbackView.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 */, @@ -11386,7 +11468,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 */, @@ -11435,6 +11516,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 */, @@ -11444,6 +11526,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 */, @@ -11492,6 +11575,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 */, @@ -11508,9 +11592,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 */, @@ -11530,7 +11616,6 @@ AA0F3DB7261A566C0077F2D9 /* SuggestionLoadingMock.swift in Sources */, B6E6BA162BA2CF5F008AA7E1 /* SandboxTestToolNotifications.swift in Sources */, B60C6F8129B1B4AD007BFAA8 /* TestRunHelper.swift in Sources */, - 4B9DB0582A983B55000927DB /* MockNetworkProtectionCodeRedeemer.swift in Sources */, 9F872DA02B90644800138637 /* ContextualMenuTests.swift in Sources */, 4B9292BE2667103100AD2C21 /* PasteboardFolderTests.swift in Sources */, 4B9292C52667104B00AD2C21 /* CoreDataTestUtilities.swift in Sources */, @@ -11564,6 +11649,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 */, @@ -11615,7 +11701,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 */, @@ -11624,6 +11712,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 */, @@ -11658,6 +11747,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 */, @@ -12644,7 +12734,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 138.0.0; + version = 140.0.3; }; }; 4311906792B7676CE9535D76 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */ = { @@ -12660,7 +12750,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 140.0.0; + version = 143.0.0; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { @@ -12896,6 +12986,16 @@ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = Navigation; }; + 4B1EFF1B2BD71EEF007CC84F /* PixelKit */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = PixelKit; + }; + 4B1EFF202BD72189007CC84F /* Networking */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Networking; + }; 4B2D062B2A11C0E100DE1F49 /* Networking */ = { isa = XCSwiftPackageProductDependency; package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index feaf63045b..150efe31c4 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "89442d067d2fcd77d487202b8d38be7e47ac0b5b", - "version" : "140.0.0" + "revision" : "7c41d69a93bbe80639fb7489e2018e5957ac2b5c", + "version" : "143.0.0" } }, { diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/sandbox-test-tool.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/sandbox-test-tool.xcscheme index 41730d7069..eb7e5e26bb 100644 --- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/sandbox-test-tool.xcscheme +++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/sandbox-test-tool.xcscheme @@ -1,7 +1,7 @@ + version = "1.8"> Void) { if response.actionIdentifier == UNNotificationDefaultActionIdentifier { - if response.notification.request.identifier == NetworkProtectionWaitlist.notificationIdentifier { - if NetworkProtectionWaitlist().readyToAcceptTermsAndConditions { - NetworkProtectionWaitlistViewControllerPresenter.show() - } - } - #if DBP if response.notification.request.identifier == DataBrokerProtectionWaitlist.notificationIdentifier { DataBrokerProtectionAppEvents().handleWaitlistInvitedNotification(source: .localPush) diff --git a/DuckDuckGo/Application/AutoClearHandler.swift b/DuckDuckGo/Application/AutoClearHandler.swift new file mode 100644 index 0000000000..f1044dae5f --- /dev/null +++ b/DuckDuckGo/Application/AutoClearHandler.swift @@ -0,0 +1,125 @@ +// +// AutoClearHandler.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 + +final class AutoClearHandler { + + private let preferences: DataClearingPreferences + private let fireViewModel: FireViewModel + private let stateRestorationManager: AppStateRestorationManager + + init(preferences: DataClearingPreferences, + fireViewModel: FireViewModel, + stateRestorationManager: AppStateRestorationManager) { + self.preferences = preferences + self.fireViewModel = fireViewModel + self.stateRestorationManager = stateRestorationManager + } + + @MainActor + func handleAppLaunch() { + burnOnStartIfNeeded() + restoreTabsIfNeeded() + resetTheCorrectTerminationFlag() + } + + var onAutoClearCompleted: (() -> Void)? + + @MainActor + func handleAppTermination() -> NSApplication.TerminateReply? { + guard preferences.isAutoClearEnabled else { return nil } + + if preferences.isWarnBeforeClearingEnabled { + switch confirmAutoClear() { + case .alertFirstButtonReturn: + // Clear and Quit + performAutoClear() + return .terminateLater + case .alertSecondButtonReturn: + // Quit without Clearing Data + appTerminationHandledCorrectly = true + restoreTabsOnStartup = true + return .terminateNow + default: + // Cancel + return .terminateCancel + } + } + + performAutoClear() + return .terminateLater + } + + func resetTheCorrectTerminationFlag() { + appTerminationHandledCorrectly = false + } + + // MARK: - Private + + private func confirmAutoClear() -> NSApplication.ModalResponse { + let alert = NSAlert.autoClearAlert() + let response = alert.runModal() + return response + } + + @MainActor + private func performAutoClear() { + fireViewModel.fire.burnAll { [weak self] in + self?.appTerminationHandledCorrectly = true + self?.onAutoClearCompleted?() + } + } + + // MARK: - Burn On Start + // Burning on quit wasn't successful + + @UserDefaultsWrapper(key: .appTerminationHandledCorrectly, defaultValue: false) + private var appTerminationHandledCorrectly: Bool + + @MainActor + @discardableResult + func burnOnStartIfNeeded() -> Bool { + let shouldBurnOnStart = preferences.isAutoClearEnabled && !appTerminationHandledCorrectly + guard shouldBurnOnStart else { return false } + + fireViewModel.fire.burnAll() + return true + } + + // MARK: - Burn without Clearing Data + + @UserDefaultsWrapper(key: .restoreTabsOnStartup, defaultValue: false) + private var restoreTabsOnStartup: Bool + + @MainActor + @discardableResult + func restoreTabsIfNeeded() -> Bool { + let isAutoClearEnabled = preferences.isAutoClearEnabled + let restoreTabsOnStartup = restoreTabsOnStartup + self.restoreTabsOnStartup = false + if isAutoClearEnabled && restoreTabsOnStartup { + stateRestorationManager.restoreLastSessionState(interactive: false) + return true + } + + return false + } + +} diff --git a/DuckDuckGo/Assets.xcassets/Images/BurnAlert.imageset/Burn-Original-large.pdf b/DuckDuckGo/Assets.xcassets/Images/BurnAlert.imageset/Burn-Original-large.pdf deleted file mode 100644 index 2208d56068..0000000000 Binary files a/DuckDuckGo/Assets.xcassets/Images/BurnAlert.imageset/Burn-Original-large.pdf and /dev/null differ diff --git a/DuckDuckGo/Assets.xcassets/Images/BurnAlert.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/BurnAlert.imageset/Contents.json index 48111f0d3f..3f5be128a3 100644 --- a/DuckDuckGo/Assets.xcassets/Images/BurnAlert.imageset/Contents.json +++ b/DuckDuckGo/Assets.xcassets/Images/BurnAlert.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "Burn-Original-large.pdf", + "filename" : "Fire-96x96.pdf", "idiom" : "universal" } ], diff --git a/DuckDuckGo/Assets.xcassets/Images/BurnAlert.imageset/Fire-96x96.pdf b/DuckDuckGo/Assets.xcassets/Images/BurnAlert.imageset/Fire-96x96.pdf new file mode 100644 index 0000000000..297359ff71 Binary files /dev/null and b/DuckDuckGo/Assets.xcassets/Images/BurnAlert.imageset/Fire-96x96.pdf differ 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/NetworkProtectionWaitlist/Shield-16.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Identity-Theft-Restoration-Multicolor-16.imageset/Contents.json similarity index 56% rename from DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Shield-16.imageset/Contents.json rename to DuckDuckGo/Assets.xcassets/Images/Identity-Theft-Restoration-Multicolor-16.imageset/Contents.json index 510acba745..c878d4b14f 100644 --- a/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Shield-16.imageset/Contents.json +++ b/DuckDuckGo/Assets.xcassets/Images/Identity-Theft-Restoration-Multicolor-16.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "Shield-16.pdf", + "filename" : "Identity-Theft-Restoration-Multicolor-16.pdf", "idiom" : "universal" } ], @@ -10,6 +10,6 @@ "version" : 1 }, "properties" : { - "template-rendering-intent" : "template" + "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/NetworkProtectionWaitlist/Card-16.imageset/Card-16.pdf b/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Card-16.imageset/Card-16.pdf deleted file mode 100644 index ead0016185..0000000000 Binary files a/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Card-16.imageset/Card-16.pdf and /dev/null differ diff --git a/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Rocket-16.imageset/Rocket-16.pdf b/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Rocket-16.imageset/Rocket-16.pdf deleted file mode 100644 index c7dd245c4f..0000000000 Binary files a/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Rocket-16.imageset/Rocket-16.pdf and /dev/null differ diff --git a/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Shield-16.imageset/Shield-16.pdf b/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Shield-16.imageset/Shield-16.pdf deleted file mode 100644 index 677edb434b..0000000000 Binary files a/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Shield-16.imageset/Shield-16.pdf and /dev/null differ diff --git a/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Card-16.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/PersonalInformationRemoval-Multicolor-16.imageset/Contents.json similarity index 54% rename from DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Card-16.imageset/Contents.json rename to DuckDuckGo/Assets.xcassets/Images/PersonalInformationRemoval-Multicolor-16.imageset/Contents.json index 0030995444..a30ef3d53e 100644 --- a/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Card-16.imageset/Contents.json +++ b/DuckDuckGo/Assets.xcassets/Images/PersonalInformationRemoval-Multicolor-16.imageset/Contents.json @@ -1,15 +1,12 @@ { "images" : [ { - "filename" : "Card-16.pdf", + "filename" : "PersonalInformationRemoval-Multicolor-16.pdf", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" } } 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/NetworkProtectionWaitlist/Rocket-16.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Settings-Multicolor-16.imageset/Contents.json similarity index 53% rename from DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Rocket-16.imageset/Contents.json rename to DuckDuckGo/Assets.xcassets/Images/Settings-Multicolor-16.imageset/Contents.json index b23b52c784..d4b2052646 100644 --- a/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Rocket-16.imageset/Contents.json +++ b/DuckDuckGo/Assets.xcassets/Images/Settings-Multicolor-16.imageset/Contents.json @@ -1,15 +1,12 @@ { "images" : [ { - "filename" : "Rocket-16.pdf", + "filename" : "Settings-Multicolor-16.pdf", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" } } 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/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..d8cdf7773f 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 @@ -605,21 +655,37 @@ final class LocalBookmarkStore: BookmarkStore { currentInsertionIndex > objectIndex { adjustedInsertionIndex -= 1 } + let nextInsertionIndex = adjustedInsertionIndex + 1 bookmarkManagedObject.parent = nil // Removing the bookmark from its current parent may have removed it from the collection it is about to be added to, so re-check // the bounds before adding it back. if adjustedInsertionIndex < newParentFolder.childrenArray.count { - newParentFolder.insertIntoChildren(bookmarkManagedObject, at: adjustedInsertionIndex) + // Handle stubs + let allChildren = (newParentFolder.children?.array as? [BookmarkEntity]) ?? [] + if newParentFolder.childrenArray.count != allChildren.count { + var correctedIndex = 0 + + while adjustedInsertionIndex > 0 && correctedIndex < allChildren.count { + if allChildren[correctedIndex].isStub == false { + adjustedInsertionIndex -= 1 + } + correctedIndex += 1 + } + newParentFolder.insertIntoChildren(bookmarkManagedObject, at: correctedIndex) + } else { + newParentFolder.insertIntoChildren(bookmarkManagedObject, at: adjustedInsertionIndex) + } } else { newParentFolder.addToChildren(bookmarkManagedObject) } - currentInsertionIndex = adjustedInsertionIndex + 1 + currentInsertionIndex = nextInsertionIndex } } + // swiftlint:disable:next function_body_length func moveFavorites(with objectUUIDs: [String], toIndex index: Int?, completion: @escaping (Error?) -> Void) { applyChangesAndSave(changes: { [weak self] context in @@ -642,27 +708,45 @@ final class LocalBookmarkStore: BookmarkStore { return (try? context.fetch(entityFetchRequest))?.first } - if let index = index, index < (displayedFavoritesFolder.favorites?.count ?? 0) { + if let index = index, index < displayedFavoritesFolder.favoritesArray.count { var currentInsertionIndex = max(index, 0) for bookmarkManagedObject in bookmarkManagedObjects { var adjustedInsertionIndex = currentInsertionIndex - if let currentIndex = displayedFavoritesFolder.favorites?.index(of: bookmarkManagedObject), + if let currentIndex = displayedFavoritesFolder.favoritesArray.firstIndex(of: bookmarkManagedObject), currentInsertionIndex > currentIndex { adjustedInsertionIndex -= 1 } + let nextInsertionIndex = adjustedInsertionIndex + 1 bookmarkManagedObject.removeFromFavorites(with: favoritesDisplayMode) - if adjustedInsertionIndex < (displayedFavoritesFolder.favorites?.count ?? 0) { - bookmarkManagedObject.addToFavorites(insertAt: adjustedInsertionIndex, - favoritesRoot: displayedFavoritesFolder) - bookmarkManagedObject.addToFavorites(folders: favoritesFoldersWithoutDisplayed) + + if adjustedInsertionIndex < displayedFavoritesFolder.favoritesArray.count { + // Handle stubs + let allChildren = (displayedFavoritesFolder.favorites?.array as? [BookmarkEntity]) ?? [] + if displayedFavoritesFolder.favoritesArray.count != allChildren.count { + var correctedIndex = 0 + + while adjustedInsertionIndex > 0 && correctedIndex < allChildren.count { + if allChildren[correctedIndex].isStub == false { + adjustedInsertionIndex -= 1 + } + correctedIndex += 1 + } + bookmarkManagedObject.addToFavorites(insertAt: correctedIndex, + favoritesRoot: displayedFavoritesFolder) + bookmarkManagedObject.addToFavorites(folders: favoritesFoldersWithoutDisplayed) + } else { + bookmarkManagedObject.addToFavorites(insertAt: adjustedInsertionIndex, + favoritesRoot: displayedFavoritesFolder) + bookmarkManagedObject.addToFavorites(folders: favoritesFoldersWithoutDisplayed) + } } else { bookmarkManagedObject.addToFavorites(folders: favoritesFolders) } - currentInsertionIndex = adjustedInsertionIndex + 1 + currentInsertionIndex = nextInsertionIndex } } else { for bookmarkManagedObject in bookmarkManagedObjects { @@ -998,32 +1082,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 +1131,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/NSAlertExtension.swift b/DuckDuckGo/Common/Extensions/NSAlertExtension.swift index a1f71cdeb4..017389de08 100644 --- a/DuckDuckGo/Common/Extensions/NSAlertExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSAlertExtension.swift @@ -215,6 +215,37 @@ extension NSAlert { return alert } + static func autoClearAlert() -> NSAlert { + let alert = NSAlert() + alert.messageText = UserText.warnBeforeQuitDialogHeader + alert.alertStyle = .warning + alert.icon = .burnAlert + alert.addButton(withTitle: UserText.clearAndQuit) + alert.addButton(withTitle: UserText.quitWithoutClearing) + alert.addButton(withTitle: UserText.cancel) + + let checkbox = NSButton(checkboxWithTitle: UserText.warnBeforeQuitDialogCheckboxMessage, + target: DataClearingPreferences.shared, + action: #selector(DataClearingPreferences.toggleWarnBeforeClearing)) + checkbox.state = DataClearingPreferences.shared.isWarnBeforeClearingEnabled ? .on : .off + checkbox.lineBreakMode = .byWordWrapping + checkbox.translatesAutoresizingMaskIntoConstraints = false + + // Create a container view for the checkbox with custom padding + let containerView = NSView(frame: NSRect(x: 0, y: 0, width: 240, height: 25)) + containerView.addSubview(checkbox) + + NSLayoutConstraint.activate([ + checkbox.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), + checkbox.centerYAnchor.constraint(equalTo: containerView.centerYAnchor, constant: -10), // Slightly up for better visual alignment + checkbox.widthAnchor.constraint(lessThanOrEqualTo: containerView.widthAnchor) + ]) + + alert.accessoryView = containerView + + return alert + } + @discardableResult func runModal() async -> NSApplication.ModalResponse { await withCheckedContinuation { continuation in 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/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+NetworkProtection.swift b/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift index 4dd17342ea..99770904df 100644 --- a/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift +++ b/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift @@ -70,160 +70,8 @@ extension UserText { static let networkProtectionPleaseReboot = "VPN update available. Restart your Mac to reconnect." } -// MARK: - VPN Waitlist - -extension UserText { - - // "network-protection.waitlist.notification.title" - Title for VPN waitlist notification - static let networkProtectionWaitlistNotificationTitle = "DuckDuckGo VPN beta is ready!" - // "network-protection.waitlist.notification.text" - Title for VPN waitlist notification - static let networkProtectionWaitlistNotificationText = "Open your invite" - - // "network-protection.waitlist.join.title" - Title for VPN join waitlist screen - static let networkProtectionWaitlistJoinTitle = "DuckDuckGo VPN Beta" - // "network-protection.waitlist.join.subtitle.1" - First subtitle for VPN join waitlist screen - static let networkProtectionWaitlistJoinSubtitle1 = "Secure your connection anytime, anywhere with DuckDuckGo VPN." - // "network-protection.waitlist.join.subtitle.2" - Second subtitle for VPN join waitlist screen - static let networkProtectionWaitlistJoinSubtitle2 = "Join the waitlist, and we’ll notify you when it’s your turn." - - // "network-protection.waitlist.joined.title" - Title for VPN joined waitlist screen - static let networkProtectionWaitlistJoinedTitle = "You’re on the list!" - // "network-protection.waitlist.joined.with-notifications.subtitle.1" - Subtitle 1 for VPN joined waitlist screen when notifications are enabled - static let networkProtectionWaitlistJoinedWithNotificationsSubtitle1 = "New invites are sent every few days, on a first come, first served basis." - // "network-protection.waitlist.joined.with-notifications.subtitle.2" - Subtitle 2 for VPN joined waitlist screen when notifications are enabled - static let networkProtectionWaitlistJoinedWithNotificationsSubtitle2 = "We’ll notify you when your invite is ready." - // "network-protection.waitlist.enable-notifications" - Enable notifications prompt for VPN joined waitlist screen - static let networkProtectionWaitlistEnableNotifications = "Want to get a notification when your VPN invite is ready?" - - // "network-protection.waitlist.invited.title" - Title for VPN invited screen - static let networkProtectionWaitlistInvitedTitle = "You’re invited to try\nDuckDuckGo VPN beta!" - // "network-protection.waitlist.invited.subtitle" - Subtitle for VPN invited screen - static let networkProtectionWaitlistInvitedSubtitle = "Get an extra layer of protection online with the VPN built for speed and simplicity. Encrypt your internet connection across your entire device and hide your location and IP address from sites you visit." - - // "network-protection.waitlist.invited.section-1.title" - Title for section 1 of the VPN invited screen - static let networkProtectionWaitlistInvitedSection1Title = "Full-device coverage" - // "network-protection.waitlist.invited.section-1.subtitle" - Subtitle for section 1 of the VPN invited screen - static let networkProtectionWaitlistInvitedSection1Subtitle = "Encrypt online traffic across your browsers and apps." - - // "network-protection.waitlist.invited.section-2.title" - Title for section 2 of the VPN invited screen - static let networkProtectionWaitlistInvitedSection2Title = "Fast, reliable, and easy to use" - // "network-protection.waitlist.invited.section-2.subtitle" - Subtitle for section 2 of the VPN invited screen - static let networkProtectionWaitlistInvitedSection2Subtitle = "No need for a separate app. Connect in one click and see your connection status at a glance." - - // "network-protection.waitlist.invited.section-3.title" - Title for section 3 of the VPN invited screen - static let networkProtectionWaitlistInvitedSection3Title = "Strict no-logging policy" - // "network-protection.waitlist.invited.section-3.subtitle" - Subtitle for section 3 of the VPN invited screen - static let networkProtectionWaitlistInvitedSection3Subtitle = "We do not log or save any data that can connect you to your online activity." - - // "network-protection.waitlist.enable.title" - Title for VPN enable screen - static let networkProtectionWaitlistEnableTitle = "Ready to enable DuckDuckGo VPN?" - // "network-protection.waitlist.enable.subtitle" - Subtitle for VPN enable screen - static let networkProtectionWaitlistEnableSubtitle = "Look for the globe icon in the browser toolbar or in the Mac menu bar.\n\nYou'll be asked to Allow a VPN connection once when setting up DuckDuckGo VPN the first time." - - // "network-protection.waitlist.availability-disclaimer" - Availability disclaimer for VPN join waitlist screen - static let networkProtectionWaitlistAvailabilityDisclaimer = "DuckDuckGo VPN is free to use during the beta." - - // "network-protection.waitlist.button.close" - Close button for VPN join waitlist screen - static let networkProtectionWaitlistButtonClose = "Close" - // "network-protection.waitlist.button.done" - Close button for VPN joined waitlist screen - static let networkProtectionWaitlistButtonDone = "Done" - // "network-protection.waitlist.button.dismiss" - Dismiss button for VPN join waitlist screen - static let networkProtectionWaitlistButtonDismiss = "Dismiss" - // "network-protection.waitlist.button.cancel" - Cancel button for VPN join waitlist screen - static let networkProtectionWaitlistButtonCancel = "Cancel" - // "network-protection.waitlist.button.no-thanks" - No Thanks button for VPN joined waitlist screen - static let networkProtectionWaitlistButtonNoThanks = "No Thanks" - // "network-protection.waitlist.button.get-started" - Get Started button for VPN joined waitlist screen - static let networkProtectionWaitlistButtonGetStarted = "Get Started" - // "network-protection.waitlist.button.got-it" - Got It button for VPN joined waitlist screen - static let networkProtectionWaitlistButtonGotIt = "Got It" - // "network-protection.waitlist.button.enable-notifications" - Enable Notifications button for VPN joined waitlist screen - static let networkProtectionWaitlistButtonEnableNotifications = "Enable Notifications" - // "network-protection.waitlist.button.join-waitlist" - Join Waitlist button for VPN join waitlist screen - static let networkProtectionWaitlistButtonJoinWaitlist = "Join the Waitlist" - // "network-protection.waitlist.button.agree-and-continue" - Agree and Continue button for VPN join waitlist screen - static let networkProtectionWaitlistButtonAgreeAndContinue = "Agree and Continue" -} - -// MARK: - VPN Terms of Service - extension UserText { - // "network-protection.privacy-policy.title" - Privacy Policy title for VPN - static let networkProtectionPrivacyPolicyTitle = "Privacy Policy" - - // "network-protection.privacy-policy.section.1.title" - Privacy Policy title for VPN - static let networkProtectionPrivacyPolicySection1Title = "We don’t ask for any personal information from you in order to use this beta service." - // "network-protection.privacy-policy.section.1.list" - Privacy Policy list for VPN (Markdown version) - static let networkProtectionPrivacyPolicySection1ListMarkdown = "This Privacy Policy is for our limited waitlist beta VPN product.\n\nOur main [Privacy Policy](https://duckduckgo.com/privacy) also applies here." - // "network-protection.privacy-policy.section.1.list" - Privacy Policy list for VPN (Non-Markdown version) - static let networkProtectionPrivacyPolicySection1ListNonMarkdown = "This Privacy Policy is for our limited waitlist beta VPN product.\n\nOur main Privacy Policy also applies here." - - // "network-protection.privacy-policy.section.2.title" - Privacy Policy title for VPN - static let networkProtectionPrivacyPolicySection2Title = "We don’t keep any logs of your online activity." - // "network-protection.privacy-policy.section.2.list" - Privacy Policy list for VPN - static let networkProtectionPrivacyPolicySection2List = "That means we have no way to tie what you do online to you as an individual and we don’t have any record of things like:\n • Website visits\n • DNS requests\n • Connections made\n • IP addresses used\n • Session lengths" - - // "network-protection.privacy-policy.section.3.title" - Privacy Policy title for VPN - static let networkProtectionPrivacyPolicySection3Title = "We only keep anonymous performance metrics that we cannot connect to your online activity." - // "network-protection.privacy-policy.section.3.list" - Privacy Policy list for VPN - static let networkProtectionPrivacyPolicySection3List = "Our servers store generic usage (for example, CPU load) and diagnostic data (for example, errors), but none of that data is connected to any individual’s activity.\n\nWe use this non-identifying information to monitor and ensure the performance and quality of the service, for example to make sure servers aren’t overloaded." - - // "network-protection.privacy-policy.section.4.title" - Privacy Policy title for VPN - static let networkProtectionPrivacyPolicySection4Title = "We use dedicated servers for all VPN traffic." - // "network-protection.privacy-policy.section.4.list" - Privacy Policy list for VPN - static let networkProtectionPrivacyPolicySection4List = "Dedicated servers means they are not shared with anyone else.\n\nWe rent our servers from providers we carefully selected because they meet our privacy requirements.\n\nWe have strict access controls in place so that only limited DuckDuckGo team members have access to our servers." - - // "network-protection.privacy-policy.section.5.title" - Privacy Policy title for VPN - static let networkProtectionPrivacyPolicySection5Title = "We protect and limit use of your data when you communicate directly with DuckDuckGo." - // "network-protection.privacy-policy.section.5.list" - Privacy Policy list for VPN - static let networkProtectionPrivacyPolicySection5List = "If you reach out to us for support by submitting a bug report or through email and agree to be contacted to troubleshoot the issue, we’ll contact you using the information you provide.\n\nIf you participate in a voluntary product survey or questionnaire and agree to provide further feedback, we may contact you using the information you provide.\n\nWe will permanently delete all personal information you provided to us (email, contact information), within 30 days after closing a support case or, in the case of follow up feedback, within 60 days after ending this beta service." - - // "network-protection.terms-of-service.title" - Terms of Service title for VPN - static let networkProtectionTermsOfServiceTitle = "Terms of Service" - - // "network-protection.terms-of-service.section.1.title" - Terms of Service title for VPN - static let networkProtectionTermsOfServiceSection1Title = "The service is for limited and personal use only." - // "network-protection.terms-of-service.section.1.list" - Terms of Service list for VPN - static let networkProtectionTermsOfServiceSection1List = "This service is provided for your personal use only.\n\nYou are responsible for all activity in the service that occurs on or through your device.\n\nThis service may only be used through the DuckDuckGo app on the device on which you are given access. If you delete the DuckDuckGo app, you will lose access to the service.\n\nYou may not use this service through a third-party client." - - // "network-protection.terms-of-service.section.2.title" - Terms of Service title for VPN - static let networkProtectionTermsOfServiceSection2Title = "You agree to comply with all applicable laws, rules, and regulations." - // "network-protection.terms-of-service.section.2.list" - Terms of Service list for VPN (Markdown version) - static let networkProtectionTermsOfServiceSection2ListMarkdown = "You agree that you will not use the service for any unlawful, illicit, criminal, or fraudulent purpose, or in any manner that could give rise to civil or criminal liability under applicable law.\n\nYou agree to comply with our [DuckDuckGo Terms of Service](https://duckduckgo.com/terms), which are incorporated by reference." - // "network-protection.terms-of-service.section.2.list" - Terms of Service list for VPN (Non-Markdown version) - static let networkProtectionTermsOfServiceSection2ListNonMarkdown = "You agree that you will not use the service for any unlawful, illicit, criminal, or fraudulent purpose, or in any manner that could give rise to civil or criminal liability under applicable law.\n\nYou agree to comply with our DuckDuckGo Terms of Service, which are incorporated by reference." - - // "network-protection.terms-of-service.section.3.title" - Terms of Service title for VPN - static let networkProtectionTermsOfServiceSection3Title = "You must be eligible to use this service." - // "network-protection.terms-of-service.section.3.list" - Terms of Service list for VPN - static let networkProtectionTermsOfServiceSection3List = "Access to this beta is randomly awarded. You are responsible for ensuring eligibility.\n\nYou must be at least 18 years old and live in a location where use of a VPN is legal in order to be eligible to use this service." - - // "network-protection.terms-of-service.section.4.title" - Terms of Service title for VPN - static let networkProtectionTermsOfServiceSection4Title = "We provide this beta service as-is and without warranty." - // "network-protection.terms-of-service.section.4.list" - Terms of Service list for VPN - static let networkProtectionTermsOfServiceSection4List = "This service is provided as-is and without warranties or guarantees of any kind.\n\nTo the extent possible under applicable law, DuckDuckGo will not be liable for any damage or loss arising from your use of the service. In any event, the total aggregate liability of DuckDuckGo shall not exceed $25 or the equivalent in your local currency.\n\nWe may in the future transfer responsibility for the service to a subsidiary of DuckDuckGo. If that happens, you agree that references to “DuckDuckGo” will refer to our subsidiary, which will then become responsible for providing the service and for any liabilities relating to it." - - // "network-protection.terms-of-service.section.5.title" - Terms of Service title for VPN - static let networkProtectionTermsOfServiceSection5Title = "We may terminate access at any time." - // "network-protection.terms-of-service.section.5.list" - Terms of Service list for VPN - static let networkProtectionTermsOfServiceSection5List = "We reserve the right to revoke access to the service at any time in our sole discretion.\n\nWe may also terminate access for violation of these terms, including for repeated infringement of the intellectual property rights of others." - - // "network-protection.terms-of-service.section.6.title" - Terms of Service title for VPN - static let networkProtectionTermsOfServiceSection6Title = "The service is free during the beta period." - // "network-protection.terms-of-service.section.6.list" - Terms of Service list for VPN - static let networkProtectionTermsOfServiceSection6List = "Access to this service is currently free of charge, but that is limited to this beta period.\n\nYou understand and agree that this service is provided on a temporary, testing basis only." - - // "network-protection.terms-of-service.section.7.title" - Terms of Service title for VPN - static let networkProtectionTermsOfServiceSection7Title = "We are continually updating the service." - // "network-protection.terms-of-service.section.7.list" - Terms of Service list for VPN - static let networkProtectionTermsOfServiceSection7List = "The service is in beta, and we are regularly changing it.\n\nService coverage, speed, server locations, and quality may vary without warning." - - // "network-protection.terms-of-service.section.8.title" - Terms of Service title for VPN - static let networkProtectionTermsOfServiceSection8Title = "We need your feedback." - // "network-protection.terms-of-service.section.8.list" - Terms of Service list for VPN - static let networkProtectionTermsOfServiceSection8List = "You may be asked during the beta period to provide feedback about your experience. Doing so is optional and your feedback may be used to improve the service.\n\nIf you have enabled notifications for the DuckDuckGo app, we may use notifications to ask about your experience. You can disable notifications if you do not want to receive them." - // MARK: - Feedback Form // "vpn.feedback-form.title" - Title for each screen of the VPN feedback form static let vpnFeedbackFormTitle = "Help Improve the DuckDuckGo VPN" diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 3724eb0254..13706b7a79 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -53,6 +53,8 @@ struct UserText { static let pasteAndGo = NSLocalizedString("paste.and.go", value: "Paste & Go", comment: "Paste & Go button") static let pasteAndSearch = NSLocalizedString("paste.and.search", value: "Paste & Search", comment: "Paste & Search button") static let clear = NSLocalizedString("clear", value: "Clear", comment: "Clear button") + static let clearAndQuit = NSLocalizedString("clear.and.quit", value: "Clear and Quit", comment: "Button to clear data and quit the application") + static let quitWithoutClearing = NSLocalizedString("quit.without.clearing", value: "Quit Without Clearing", comment: "Button to quit the application without clearing data") static let `continue` = NSLocalizedString("`continue`", value: "Continue", comment: "Continue button") static let bookmarkDialogAdd = NSLocalizedString("bookmark.dialog.add", value: "Add", comment: "Button to confim a bookmark creation") static let newFolderDialogAdd = NSLocalizedString("folder.dialog.add", value: "Add", comment: "Button to confim a bookmark folder creation") @@ -392,9 +394,9 @@ struct UserText { static let restartBitwarden = NSLocalizedString("restart.bitwarden", value: "Restart Bitwarden", comment: "Button to restart Bitwarden application") static let restartBitwardenInfo = NSLocalizedString("restart.bitwarden.info", value: "Bitwarden is not responding. Please restart it to initiate the communication again", comment: "This string represents a message informing the user that Bitwarden is not responding and prompts them to restart the application to initiate communication again.") - static let autofillViewContentButtonPasswords = NSLocalizedString("autofill.view-autofill-content.passwords", value: "View Passwords…", comment: "View Password Content Button title in the autofill Settings") - static let autofillViewContentButtonPaymentMethods = NSLocalizedString("autofill.view-autofill-content.payment-methods", value: "View Payment Methods…", comment: "View Payment Methods Content Button title in the autofill Settings") - static let autofillViewContentButtonIdentities = NSLocalizedString("autofill.view-autofill-content.identities", value: "View Identities…", comment: "View Identities Content Button title in the autofill Settings") + static let autofillViewContentButtonPasswords = NSLocalizedString("autofill.view-autofill-content.passwords", value: "Open Passwords…", comment: "View Password Content Button title in the autofill Settings") + static let autofillViewContentButtonPaymentMethods = NSLocalizedString("autofill.view-autofill-content.payment-methods", value: "Open Payment Methods…", comment: "View Payment Methods Content Button title in the autofill Settings") + static let autofillViewContentButtonIdentities = NSLocalizedString("autofill.view-autofill-content.identities", value: "Open Identities…", comment: "View Identities Content Button title in the autofill Settings") static let autofillAskToSave = NSLocalizedString("autofill.ask-to-save", value: "Ask to Save and Autofill", comment: "Autofill settings section title") static let autofillAskToSaveExplanation = NSLocalizedString("autofill.ask-to-save.explanation", value: "Receive prompts to save new information and autofill online forms.", comment: "Description of Autofill autosaving feature - used in settings") static let autofillPasswords = NSLocalizedString("autofill.passwords", value: "Passwords", comment: "Autofill autosaved data type") @@ -406,9 +408,9 @@ struct UserText { static let autofillExcludedSitesResetActionTitle = NSLocalizedString("autofill.excluded-sites.reset.action.title", value: "Reset Excluded Sites?", comment: "Alert title") static let autofillExcludedSitesResetActionMessage = NSLocalizedString("autofill.excluded-sites.reset.action.message", value: "If you reset excluded sites, you will be prompted to save your password next time you sign in to any of these sites.", comment: "Alert title") static let autofillAutoLock = NSLocalizedString("autofill.auto-lock", value: "Auto-lock", comment: "Autofill settings section title") - static let autofillLockWhenIdle = NSLocalizedString("autofill.lock-when-idle", value: "Lock autofill after computer is idle for", comment: "Autofill auto-lock setting") + static let autofillLockWhenIdle = NSLocalizedString("autofill.lock-when-idle", value: "Lock access to passwords and autofill info after computer is idle for", comment: "Autofill auto-lock setting") static let autofillNeverLock = NSLocalizedString("autofill.never-lock", value: "Never lock autofill", comment: "Autofill auto-lock setting") - static let autofillNeverLockWarning = NSLocalizedString("autofill.never-lock-warning", value: "If not locked, anyone with access to your device will be able to use and modify your autofill data. For security purposes, credit card form fill always requires authentication.", comment: "Autofill disabled auto-lock warning") + static let autofillNeverLockWarning = NSLocalizedString("autofill.never-lock-warning", value: "If not locked, anyone with access to your device will be able to use and modify your autofill info. For security purposes, payment method form fill always requires authentication.", comment: "Autofill disabled auto-lock warning") static let autolockLocksFormFill = NSLocalizedString("autofill.autolock-locks-form-filling", value: "Also lock password form fill", comment: "Lock form filling when auto-lock is active text") static let downloadsLocation = NSLocalizedString("downloads.location", value: "Location", comment: "Downloads directory location") @@ -470,7 +472,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") @@ -909,8 +912,8 @@ struct UserText { static let bitwardenCommunicationInfo = NSLocalizedString("bitwarden.connect.communication-info", value: "All communication between Bitwarden and DuckDuckGo is encrypted and the data never leaves your device.", comment: "Warns users that all communication between the DuckDuckGo browser and the password manager Bitwarden is encrypted and doesn't leave the user device") static let bitwardenHistoryInfo = NSLocalizedString("bitwarden.connect.history-info", value: "Bitwarden will have access to your browsing history.", comment: "Warn users that the password Manager Bitwarden will have access to their browsing history") - static let showAutofillShortcut = NSLocalizedString("pinning.show-autofill-shortcut", value: "Show Autofill Shortcut", comment: "Menu item for showing the autofill shortcut") - static let hideAutofillShortcut = NSLocalizedString("pinning.hide-autofill-shortcut", value: "Hide Autofill Shortcut", comment: "Menu item for hiding the autofill shortcut") + static let showAutofillShortcut = NSLocalizedString("pinning.show-autofill-shortcut", value: "Show Passwords Shortcut", comment: "Menu item for showing the passwords shortcut") + static let hideAutofillShortcut = NSLocalizedString("pinning.hide-autofill-shortcut", value: "Hide Passwords Shortcut", comment: "Menu item for hiding the passwords shortcut") static let showBookmarksShortcut = NSLocalizedString("pinning.show-bookmarks-shortcut", value: "Show Bookmarks Shortcut", comment: "Menu item for showing the bookmarks shortcut") static let hideBookmarksShortcut = NSLocalizedString("pinning.hide-bookmarks-shortcut", value: "Hide Bookmarks Shortcut", comment: "Menu item for hiding the bookmarks shortcut") @@ -1083,6 +1086,17 @@ struct UserText { static let fireproofCheckboxTitle = NSLocalizedString("fireproof.checkbox.title", value: "Ask to Fireproof websites when signing in", comment: "Fireproof settings checkbox title") static let fireproofExplanation = NSLocalizedString("fireproof.explanation", value: "When you Fireproof a site, cookies won't be erased and you'll stay signed in, even after using the Fire Button.", comment: "Fireproofing mechanism explanation") static let manageFireproofSites = NSLocalizedString("fireproof.manage-sites", value: "Manage Fireproof Sites…", comment: "Fireproof settings button caption") + static let autoClear = NSLocalizedString("auto.clear", value: "Auto-Clear", comment: "Header of a section in Settings. The setting configures clearing data automatically after quitting the app.") + static let automaticallyClearData = NSLocalizedString("automatically.clear.data", value: "Automatically clear tabs and browsing data when DuckDuckGo quits", comment: "Label after the checkbox in Settings which configures clearing data automatically after quitting the app.") + static let warnBeforeQuit = NSLocalizedString("warn.before.quit", value: "Warn me that tabs and data will be cleared when quitting", comment: "Label after the checkbox in Settings which configures a warning before clearing data on the application termination.") + static let warnBeforeQuitDialogHeader = NSLocalizedString("warn.before.quit.dialog.header", value: "Clear tabs and browsing data and quit DuckDuckGo?", comment: "A header of warning before clearing data on the application termination.") + static let warnBeforeQuitDialogCheckboxMessage = NSLocalizedString("warn.before.quit.dialog.checkbox.message", value: "Warn me every time", comment: "A label after checkbox to configure the warning before clearing data on the application termination.") + static let disableAutoClearToEnableSessionRestore = NSLocalizedString("disable.auto.clear.to.enable.session.restore", + value: "Disable auto-clear on quit to turn on session restore.", + comment: "Information label in Settings. It tells user that to enable session restoration setting they have to disable burn on quit. Auto-Clear should match the string with 'auto.clear' key") + static let showDataClearingSettings = NSLocalizedString("show.data.clearing.settings", + value: "Open Data Clearing Settings", + comment: "Button in Settings. It navigates user to Data Clearing Settings. The Data Clearing string should match the string with the preferences.data-clearing key") // MARK: Crash Report static let crashReportTitle = NSLocalizedString("crash-report.title", value: "DuckDuckGo Privacy Browser quit unexpectedly.", comment: "Title of the dialog where the user can send a crash report") @@ -1112,15 +1126,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") } } } diff --git a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift index 84c3835b1e..92e2f23e0b 100644 --- a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift +++ b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift @@ -55,6 +55,8 @@ public struct UserDefaultsWrapper { case grammarCheckEnabledOnce = "grammar.check.enabled.once" case loginDetectionEnabled = "fireproofing.login-detection-enabled" + case autoClearEnabled = "preferences.auto-clear-enabled" + case warnBeforeClearingEnabled = "preferences.warn-before-clearing-enabled" case gpcEnabled = "preferences.gpc-enabled" case selectedDownloadLocationKey = "preferences.download-location" case lastUsedCustomDownloadLocation = "preferences.custom-last-used-download-location" @@ -78,6 +80,8 @@ public struct UserDefaultsWrapper { case lastCrashReportCheckDate = "last.crash.report.check.date" case fireInfoPresentedOnce = "fire.info.presented.once" + case appTerminationHandledCorrectly = "app.termination.handled.correctly" + case restoreTabsOnStartup = "restore.tabs.on.startup" case restorePreviousSession = "preferences.startup.restore-previous-session" case launchToCustomHomePage = "preferences.startup.launch-to-custom-home-page" diff --git a/DuckDuckGo/Common/View/AppKit/CircularProgressView.swift b/DuckDuckGo/Common/View/AppKit/CircularProgressView.swift index 204d950546..3e1190a9e9 100644 --- a/DuckDuckGo/Common/View/AppKit/CircularProgressView.swift +++ b/DuckDuckGo/Common/View/AppKit/CircularProgressView.swift @@ -164,6 +164,14 @@ final class CircularProgressView: NSView { guard !isBackgroundAnimating || !animated else { // will call `updateProgressState` on animation completion completion(false) + // if background animation is in progress but 1.0 was received before + // the `progress = nil` update – complete the progress animation + // before hiding + if progress == nil && oldValue == 1.0, animated, + // shouldn‘t be already animating to 100% + progressLayer.strokeStart != 0.0 { + updateProgress(from: 0, to: 1, animated: animated) { _ in } + } return } @@ -177,7 +185,7 @@ final class CircularProgressView: NSView { completion(true) } case (true, true): - updateProgress(oldValue: oldValue, animated: animated, completion: completion) + updateProgress(from: oldValue, to: progress, animated: animated, completion: completion) case (false, false): backgroundLayer.removeAllAnimations() progressLayer.removeAllAnimations() @@ -216,17 +224,16 @@ final class CircularProgressView: NSView { } } - private func updateProgress(oldValue: Double?, animated: Bool, completion: @escaping (Bool) -> Void) { + private func updateProgress(from oldValue: Double?, to progress: Double?, animated: Bool, completion: @escaping (Bool) -> Void) { guard let progress else { assertionFailure("Unexpected flow") completion(false) return } - let currentStrokeStart = (progressLayer.presentation() ?? progressLayer).strokeStart + let currentStrokeStart = progressLayer.currentStrokeStart let newStrokeStart = 1.0 - (progress >= 0.0 ? CGFloat(progress) : max(Constants.indeterminateProgressValue, min(0.9, 1.0 - currentStrokeStart))) - guard animated else { progressLayer.strokeStart = newStrokeStart @@ -274,7 +281,7 @@ final class CircularProgressView: NSView { guard let progress, progress == value else { return } if let oldValue, oldValue < 0, value != progress, animated { - updateProgress(oldValue: value, animated: animated) { _ in } + updateProgress(from: value, to: progress, animated: animated) { _ in } return } @@ -356,7 +363,7 @@ final class CircularProgressView: NSView { progressLayer.add(progressEndAnimation, forKey: #keyPath(CAShapeLayer.strokeEnd)) let progressAnimation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.strokeStart)) - let currentStrokeStart = (progressLayer.presentation() ?? progressLayer).strokeStart + let currentStrokeStart = progressLayer.currentStrokeStart progressLayer.removeAnimation(forKey: #keyPath(CAShapeLayer.strokeStart)) progressLayer.strokeStart = 0.0 @@ -375,6 +382,14 @@ final class CircularProgressView: NSView { private extension CAShapeLayer { + var currentStrokeStart: CGFloat { + if animation(forKey: #keyPath(CAShapeLayer.strokeStart)) != nil, + let presentation = self.presentation() { + return presentation.strokeStart + } + return strokeStart + } + func configureCircle(radius: CGFloat, lineWidth: CGFloat) { self.bounds = CGRect(x: 0, y: 0, width: (radius + lineWidth) * 2, height: (radius + lineWidth) * 2) @@ -530,14 +545,97 @@ struct CircularProgress: NSViewRepresentable { perform { progress = 1 } - perform { - progress = nil + Task { + perform { + progress = nil + } } } } label: { Text(verbatim: "0->1->nil").frame(width: 120) } + Button { + Task { + progress = nil + perform { + progress = 1 + } + Task { + perform { + progress = nil + } + } + } + } label: { + Text(verbatim: "nil->1->nil").frame(width: 120) + } + + Button { + Task { + progress = nil + perform { + progress = 1 + } + Task { + perform { + progress = nil + } + Task { + perform { + progress = 1 + } + Task { + perform { + progress = nil + } + } + } + } + } + } label: { + Text(verbatim: "nil->1->nil->1->nil").frame(width: 120) + } + + Button { + Task { + progress = nil + perform { + progress = 1 + } + Task { + perform { + progress = nil + } + Task { + perform { + progress = nil + } + } + } + } + } label: { + Text(verbatim: "nil->1->nil->nil").frame(width: 120) + } + + Button { + Task { + progress = nil + perform { + progress = 0 + } + try await Task.sleep(interval: 0.2) + for p in [0.26, 0.64, 0.95, 1, nil] { + perform { + progress = p + } + try await Task.sleep(interval: 0.001) + } + } + } label: { + Text(verbatim: "nil->0.2…1->nil").frame(width: 120) + } + Button { Task { perform { @@ -581,7 +679,7 @@ struct CircularProgress: NSViewRepresentable { .background(Color.white) Spacer() } - }.frame(width: 600, height: 400) + }.frame(width: 600, height: 500) } } return ProgressPreview() 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 e4add5a17e..935d103d42 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 = "\"7cf7b71adb62c3cbcbf8b84c61a0004d\"" - public static let embeddedDataSHA = "20e9b59e7e60ccc9ae52853935ebe3d74227234fcf8b46da5a66cff3adc7e6c7" + public static let embeddedDataETag = "\"3471e126687b3688c3512627a8fde0a1\"" + public static let embeddedDataSHA = "a49fcdf77320604568abb7c69704d1fd93069b952cf1e2f6f3ffcf13020172ec" } var embeddedDataEtag: String { diff --git a/DuckDuckGo/ContentBlocker/macos-config.json b/DuckDuckGo/ContentBlocker/macos-config.json index 39163097c1..af01839109 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": 1713542334045, + "version": 1714486769720, "features": { "adClickAttribution": { "readme": "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/#3rd-party-tracker-loading-protection", @@ -135,6 +135,9 @@ { "domain": "ksta.de" }, + { + "domain": "larazon.es" + }, { "domain": "motherdenim.com" }, @@ -288,6 +291,9 @@ { "domain": "sporthoj.com" }, + { + "domain": "www.michelinman.com" + }, { "domain": "marvel.com" }, @@ -299,12 +305,11 @@ "disabledCMPs": [ "generic-cosmetic", "termsfeed3", - "healthline-media", "tarteaucitron.js" ] }, "state": "enabled", - "hash": "f35e24cf85485b441cb9a76146e77e17" + "hash": "bca76e26434e161140776de92c03cb5f" }, "autofill": { "exceptions": [ @@ -1092,6 +1097,10 @@ { "domain": "web.de", "reason": "https://github.com/duckduckgo/privacy-configuration/issues/1931" + }, + { + "domain": "id.seb.se", + "reason": "https://github.com/duckduckgo/privacy-configuration/issues/2025" } ], "webViewDefault": [ @@ -1119,7 +1128,7 @@ }, "exceptions": [], "state": "enabled", - "hash": "76976e1ac417949aae8cb1c7c7ca0a60" + "hash": "c52448ca8da413a93de9822cac039920" }, "dbp": { "state": "enabled", @@ -3519,6 +3528,15 @@ } ] }, + { + "domain": "realtor.com", + "rules": [ + { + "selector": ".ads_container", + "type": "hide" + } + ] + }, { "domain": "reddit.com", "rules": [ @@ -4256,11 +4274,20 @@ "type": "override" } ] + }, + { + "domain": "wideopencountry.com", + "rules": [ + { + "selector": ".entry-ad", + "type": "hide-empty" + } + ] } ] }, "state": "enabled", - "hash": "765e789c939c6e3307f576bc698fbb9e" + "hash": "893bd7422971b3a7b4c6e02cdfc6332d" }, "exceptionHandler": { "exceptions": [ @@ -4274,6 +4301,11 @@ "state": "disabled", "hash": "dc1b4fa301193a03ddcd4bdf7ee3e610" }, + "extendedOnboarding": { + "exceptions": [], + "state": "disabled", + "hash": "728493ef7a1488e4781656d3f9db84aa" + }, "fingerprintingAudio": { "state": "disabled", "exceptions": [ @@ -5981,6 +6013,12 @@ "sbs.com.au" ] }, + { + "rule": "https://googleads.g.doubleclick.net/ads/preferences/naioptout", + "domains": [ + "zojirushi.com" + ] + }, { "rule": "www3.doubleclick.net", "domains": [ @@ -6683,6 +6721,16 @@ } ] }, + "grow.me": { + "rules": [ + { + "rule": "grow.me/main.js", + "domains": [ + "budgetbytes.com" + ] + } + ] + }, "gstatic.com": { "rules": [ { @@ -6909,6 +6957,7 @@ { "rule": "a.klaviyo.com/media/js/onsite/onsite.js", "domains": [ + "bonescoffee.com", "tanglefree.com" ] }, @@ -7121,7 +7170,8 @@ { "rule": "connect.nosto.com/script/shopify/nosto.js", "domains": [ - "oneill.com" + "oneill.com", + "thefryecompany.com" ] } ] @@ -7217,6 +7267,7 @@ "abc.net.au", "emol.com", "oufc.co.uk", + "the-afc.com", "theposh.com" ] } @@ -7235,6 +7286,12 @@ "domains": [ "hgtv.com" ] + }, + { + "rule": "https://cdn.optimizely.com/js/271989291.js", + "domains": [ + "my.zipcar.com" + ] } ] }, @@ -7319,10 +7376,23 @@ "primis.tech": { "rules": [ { - "rule": "live.primis.tech/live/liveView.php", + "rule": "video.primis.tech/", + "domains": [ + "wideopencountry.com" + ] + }, + { + "rule": "live.primis.tech/content/omid/static/", + "domains": [ + "wideopencountry.com" + ] + }, + { + "rule": "live.primis.tech/live/", "domains": [ "belfastlive.co.uk", - "cornwalllive.com" + "cornwalllive.com", + "wideopencountry.com" ] } ] @@ -7698,6 +7768,16 @@ } ] }, + "skimresources.com": { + "rules": [ + { + "rule": "go.skimresources.com/", + "domains": [ + "www.lotustalk.com" + ] + } + ] + }, "slickstream.com": { "rules": [ { @@ -8220,7 +8300,7 @@ "domain": "sundancecatalog.com" } ], - "hash": "2d5ce26ddae089bcb61e4f4a0b1ae487" + "hash": "e21e28c6597b4ac6ab6c03bdf359912e" }, "trackingCookies1p": { "settings": { diff --git a/DuckDuckGo/DBP/DBPHomeViewController.swift b/DuckDuckGo/DBP/DBPHomeViewController.swift index 55d08d7d62..cf9e05e900 100644 --- a/DuckDuckGo/DBP/DBPHomeViewController.swift +++ b/DuckDuckGo/DBP/DBPHomeViewController.swift @@ -112,8 +112,9 @@ final class DBPHomeViewController: NSViewController { override func viewDidLayout() { super.viewDidLayout() - dataBrokerProtectionViewController.view.frame = view.bounds - errorViewController.view.frame = view.bounds + if let currentChildViewController = currentChildViewController { + currentChildViewController.view.frame = view.bounds + } } private func setupUI() { diff --git a/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift b/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift index 05f1584b80..07a579367f 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift @@ -49,8 +49,8 @@ struct DataBrokerProtectionAppEvents { // If we don't have profileQueries it means there's no user profile saved in our DB // In this case, let's disable the agent and delete any left-over data because there's nothing for it to do - if let profileQueries = try? DataBrokerProtectionManager.shared.dataManager.fetchBrokerProfileQueryData(ignoresCache: true), - profileQueries.count > 0 { + if let profileQueriesCount = try? DataBrokerProtectionManager.shared.dataManager.profileQueriesCount(), + profileQueriesCount > 0 { restartBackgroundAgent(loginItemsManager: loginItemsManager) } else { featureVisibility.disableAndDeleteForWaitlistUsers() diff --git a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift index 48a458f3cf..b22ee8eac2 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift @@ -227,7 +227,7 @@ final class DataBrokerProtectionDebugMenu: NSMenu { os_log("Running scan operations...", log: .dataBrokerProtection) let showWebView = sender.representedObject as? Bool ?? false - DataBrokerProtectionManager.shared.scheduler.startManualScan(showWebView: showWebView) { errors in + DataBrokerProtectionManager.shared.scheduler.startManualScan(showWebView: showWebView, startTime: Date()) { errors in if let errors = errors { if let oneTimeError = errors.oneTimeError { os_log("scan operations finished, error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) diff --git a/DuckDuckGo/DBP/DataBrokerProtectionLoginItemScheduler.swift b/DuckDuckGo/DBP/DataBrokerProtectionLoginItemScheduler.swift index cc0d841ee8..50b75e9fac 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionLoginItemScheduler.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionLoginItemScheduler.swift @@ -56,9 +56,10 @@ extension DataBrokerProtectionLoginItemScheduler: DataBrokerProtectionScheduler } func startManualScan(showWebView: Bool, + startTime: Date, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { enableLoginItem() - ipcScheduler.startManualScan(showWebView: showWebView, completion: completion) + ipcScheduler.startManualScan(showWebView: showWebView, startTime: startTime, completion: completion) } func startScheduler(showWebView: Bool) { diff --git a/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift b/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift index 08c5b74f18..2f91fe97a0 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift @@ -99,7 +99,11 @@ public class DataBrokerProtectionPixelsHandler: EventMapping View > Home Button > None item\n Preferences > Home Button > None item", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -24527,7 +25314,7 @@ "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Если у вас есть закладки в %@, попробуйте импортировать их вручную." + "value" : "Если у вас есть %@-файл с закладками, попробуйте импортировать его вручную." } } } @@ -24587,7 +25374,7 @@ "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Если у вас есть пароли в %@, попробуйте импортировать их вручную." + "value" : "Если у вас есть %@-файл с паролями, попробуйте импортировать его вручную." } } } @@ -30473,7 +31260,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Teile deine Gedanken" + "value" : "Sign Up To Participate" } }, "en" : { @@ -30533,7 +31320,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Nimm an unserer kurzen Umfrage teil und hilf uns, den besten Browser zu entwickeln." + "value" : "Join an interview with a member of our research team to help us build the best browser." } }, "en" : { @@ -30593,7 +31380,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Sag uns, was dich hierher gebracht hat" + "value" : "Share Your Thoughts With Us" } }, "en" : { @@ -30833,7 +31620,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Hilf uns, uns zu verbessern" + "value" : "Sign Up To Participate" } }, "en" : { @@ -30965,43 +31752,43 @@ "es" : { "stringUnit" : { "state" : "translated", - "value" : "Ayúdanos a mejorar" + "value" : "Share Your Thoughts With Us" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Aidez-nous à nous améliorer" + "value" : "Share Your Thoughts With Us" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Aiutaci a migliorare" + "value" : "Share Your Thoughts With Us" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Help ons om te verbeteren" + "value" : "Share Your Thoughts With Us" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Pomóż nam we wprowadzaniu ulepszeń" + "value" : "Share Your Thoughts With Us" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Ajuda-nos a melhorar" + "value" : "Share Your Thoughts With Us" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Помогите нам стать лучше" + "value" : "Share Your Thoughts With Us" } } } @@ -37044,60 +37831,60 @@ } }, "pinning.hide-autofill-shortcut" : { - "comment" : "Menu item for hiding the autofill shortcut", + "comment" : "Menu item for hiding the passwords shortcut", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Autovervollständigungs-Verknüpfung ausblenden" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "Hide Autofill Shortcut" + "value" : "Hide Passwords Shortcut" } }, "es" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Ocultar acceso directo a Autocompletar" } }, "fr" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Masquer le raccourci de saisie automatique" } }, "it" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Nascondi scorciatoia compilazione automatica" } }, "nl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Snelkoppeling voor automatisch invullen verbergen" } }, "pl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Ukryj skrót do autouzupełniania" } }, "pt" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Ocultar atalho de preenchimento automático" } }, "ru" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Скрыть ярлык для автозаполнения" } } @@ -37284,60 +38071,60 @@ } }, "pinning.show-autofill-shortcut" : { - "comment" : "Menu item for showing the autofill shortcut", + "comment" : "Menu item for showing the passwords shortcut", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Autofill-Verknüpfung anzeigen" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "Show Autofill Shortcut" + "value" : "Show Passwords Shortcut" } }, "es" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Mostrar acceso directo a Autocompletar" } }, "fr" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Afficher le raccourci de saisie automatique" } }, "it" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Mostra scorciatoia compilazione automatica" } }, "nl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Snelkoppeling voor automatisch invullen weergeven" } }, "pl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Pokaż skrót do autouzupełniania" } }, "pt" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Mostrar atalho de preenchimento automático" } }, "ru" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Показывать ярлык для автозаполнения" } } @@ -39922,55 +40709,55 @@ "localizations" : { "de" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Zugang zu deinen Autovervollständigungs-Infos freischalten" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "unlock access to your autofill info" + "value" : "unlock your passwords and autofill info for you" } }, "es" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "desbloquear el acceso a tu información de autocompletar" } }, "fr" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "déverrouiller l'accès à vos informations de saisie automatique" } }, "it" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "sblocca l'accesso alla compilazione automatica delle informazioni" } }, "nl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "toegang tot automatisch ingevulde gegevens ontgrendelen" } }, "pl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "odblokuj dostęp do informacji autouzupełniania" } }, "pt" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "desbloquear o acesso às tuas informações de preenchimento automático" } }, "ru" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "разблокировать доступ к автозаполняемым данным" } } @@ -41782,55 +42569,55 @@ "localizations" : { "de" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Passwort speichern?" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "Save password?" + "value" : "Save Password to DuckDuckGo?" } }, "es" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "¿Guardar contraseña?" } }, "fr" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Enregistrer le mot de passe ?" } }, "it" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Salvare password?" } }, "nl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Wachtwoord opslaan?" } }, "pl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Zapisać hasło?" } }, "pt" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Guardar palavra-passe?" } }, "ru" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Сохранить пароль?" } } @@ -42376,6 +43163,18 @@ } } }, + "pm.update-credentials.title" : { + "comment" : "Title for the Update Credentials popover", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Update Password?" + } + } + } + }, "pm.username" : { "comment" : "Label for username edit field", "extractionState" : "extracted_with_value", @@ -45982,6 +46781,66 @@ } } }, + "quit.without.clearing" : { + "comment" : "Button to quit the application without clearing data", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Beenden ohne Löschen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Quit Without Clearing" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Salir sin borrar" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quitter sans effacer" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Esci senza cancellare" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stoppen zonder wissen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wyjdź bez czyszczenia" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sair sem limpar" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Выйти без очистки" + } + } + } + }, "Recently Closed" : { "comment" : "Main Menu History item", "localizations" : { @@ -48130,7 +48989,7 @@ }, "Show left of the back button" : { "comment" : "Preferences > Home Button > left position item", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -48184,7 +49043,7 @@ }, "Show Left of the Back Button" : { "comment" : "Main Menu > View > Home Button > left position item", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -48453,7 +49312,7 @@ }, "Show right of the reload button" : { "comment" : "Preferences > Home Button > right position item", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -48507,7 +49366,7 @@ }, "Show Right of the Reload Button" : { "comment" : "Main Menu > View > Home Button > right position item", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -48612,6 +49471,66 @@ } } }, + "show.data.clearing.settings" : { + "comment" : "Button in Settings. It navigates user to Data Clearing Settings. The Data Clearing string should match the string with the preferences.data-clearing key", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Einstellungen zum Löschen von Daten öffnen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Open Data Clearing Settings" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir configuración de borrado de datos" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ouvrir les paramètres d'effacement des données" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apri Impostazioni cancellazione dati" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Instellingen voor het wissen van open gegevens" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otwórz ustawienia czyszczenia danych" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir Definições de limpeza de dados" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Открыть настройки очистки данных" + } + } + } + }, "show.folder.contents" : { "comment" : "Menu item that shows the content of a folder ", "extractionState" : "extracted_with_value", @@ -52702,6 +53621,186 @@ } } }, + "warn.before.quit" : { + "comment" : "Label after the checkbox in Settings which configures a warning before clearing data on the application termination.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Warne mich, dass Tabs und Daten beim Beenden gelöscht werden" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Warn me that tabs and data will be cleared when quitting" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Advertirme de que las pestañas y los datos se borrarán al salir" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "M'avertir que les onglets et les données seront effacés à la fermeture" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avvisa prima di cancellare schede e dati all'uscita" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Waarschuwen dat tabbladen en gegevens worden gewist bij het afsluiten" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ostrzegaj, że karty i dane zostaną wyczyszczone przy wychodzeniu" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avisar-me que os separadores e os dados serão limpos ao sair" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Показывать предупреждение о сбросе вкладок и данных при выходе" + } + } + } + }, + "warn.before.quit.dialog.checkbox.message" : { + "comment" : "A label after checkbox to configure the warning before clearing data on the application termination.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jedes Mal warnen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Warn me every time" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Advertirme cada vez" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toujours me prévenir" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avvisa ogni volta" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Elke keer waarschuwen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ostrzegaj za każdym razem" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avisar-me sempre" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Предупреждать каждый раз" + } + } + } + }, + "warn.before.quit.dialog.header" : { + "comment" : "A header of warning before clearing data on the application termination.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tabs und Browserdaten löschen und DuckDuckGo beenden?" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Clear tabs and browsing data and quit DuckDuckGo?" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Borrar pestañas y datos de navegación y salir de DuckDuckGo?" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Effacer les onglets et les données de navigation et quitter DuckDuckGo ?" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancellare le schede e i dati di navigazione e uscire da DuckDuckGo?" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tabbladen en browsergegevens wissen en DuckDuckGo afsluiten?" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wyczyścić karty i dane przeglądania i wyjść z DuckDuckGo?" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limpar separadores e dados de navegação e sair do DuckDuckGo?" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Очистить вкладки и данные и выйти из DuckDuckGo?" + } + } + } + }, "We couldn‘t find any bookmarks." : { "comment" : "Data import error message: Bookmarks weren‘t found.", "localizations" : { 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 59729584ac..eca7f8004d 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -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) } @@ -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 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..25cc50a039 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift @@ -22,6 +22,7 @@ import Combine import Common import Suggestions import Subscription +import BrowserServicesKit final class AddressBarTextField: NSTextField { @@ -233,7 +234,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 +260,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 +276,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 +419,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 +804,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 +859,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 428ae9bfff..1c6502fe5c 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift @@ -21,13 +21,13 @@ import Combine import Common import BrowserServicesKit import PixelKit - import NetworkProtection 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 +171,10 @@ final class MoreOptionsMenu: NSMenu { actionDelegate?.optionsButtonMenuRequestedBookmarkThisPage(sender) } + @objc func bookmarkAllOpenTabs(_ sender: NSMenuItem) { + actionDelegate?.optionsButtonMenuRequestedBookmarkAllOpenTabs(sender) + } + @objc func openBookmarks(_ sender: NSMenuItem) { actionDelegate?.optionsButtonMenuRequestedBookmarkPopover(self) } @@ -301,30 +305,25 @@ final class MoreOptionsMenu: NSMenu { var items: [NSMenuItem] = [] let subscriptionFeatureAvailability = DefaultSubscriptionFeatureAvailability() + let networkProtectionItem: NSMenuItem - if networkProtectionFeatureVisibility.isNetworkProtectionBetaVisible() { - let networkProtectionItem: NSMenuItem + networkProtectionItem = makeNetworkProtectionItem() - networkProtectionItem = makeNetworkProtectionItem() + items.append(networkProtectionItem) - items.append(networkProtectionItem) + if subscriptionFeatureAvailability.isFeatureAvailable && accountManager.isUserAuthenticated { + Task { + let isMenuItemEnabled: Bool - if subscriptionFeatureAvailability.isFeatureAvailable && accountManager.isUserAuthenticated { - Task { - let isMenuItemEnabled: Bool - - switch await accountManager.hasEntitlement(for: .networkProtection) { - case let .success(result): - isMenuItemEnabled = result - case .failure: - isMenuItemEnabled = false - } - - networkProtectionItem.isEnabled = isMenuItemEnabled + switch await accountManager.hasEntitlement(for: .networkProtection) { + case let .success(result): + isMenuItemEnabled = result + case .failure: + isMenuItemEnabled = false } + + networkProtectionItem.isEnabled = isMenuItemEnabled } - } else { - networkProtectionFeatureVisibility.disableForWaitlistUsers() } #if DBP @@ -402,10 +401,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 +413,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 +633,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 6f3909a3d3..5a4561d1dd 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -289,43 +289,12 @@ final class NavigationBarViewController: NSViewController { } private func toggleNetworkProtectionPopover() { - let featureVisibility = DefaultNetworkProtectionVisibility() - guard featureVisibility.isNetworkProtectionBetaVisible() else { - featureVisibility.disableForWaitlistUsers() - LocalPinningManager.shared.unpin(.networkProtection) + guard DefaultSubscriptionFeatureAvailability().isFeatureAvailable, + NetworkProtectionKeychainTokenStore().isFeatureActivated else { return } - if DefaultSubscriptionFeatureAvailability().isFeatureAvailable { - let accountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) - let networkProtectionTokenStorage = NetworkProtectionKeychainTokenStore() - - if accountManager.accessToken != nil && (try? networkProtectionTokenStorage.fetchToken()) == nil { - print("[NetP Subscription] Got access token but not auth token, meaning token exchange failed") - return - } - } - - // Note: the following code is quite contrived but we're aiming to hotfix issues without mixing subscription and - // waitlist logic. This should be cleaned up once waitlist can safely be removed. - - if DefaultSubscriptionFeatureAvailability().isFeatureAvailable { - if NetworkProtectionKeychainTokenStore().isFeatureActivated { - popovers.toggleNetworkProtectionPopover(usingView: networkProtectionButton, withDelegate: networkProtectionButtonModel) - } - } else { - // 1. If the user is on the waitlist but hasn't been invited or accepted terms and conditions, show the waitlist screen. - // 2. If the user has no waitlist state but has an auth token, show the NetP popover. - // 3. If the user has no state of any kind, show the waitlist screen. - - if NetworkProtectionWaitlist().shouldShowWaitlistViewController { - NetworkProtectionWaitlistViewControllerPresenter.show() - } else if NetworkProtectionKeychainTokenStore().isFeatureActivated { - popovers.toggleNetworkProtectionPopover(usingView: networkProtectionButton, withDelegate: networkProtectionButtonModel) - } else { - NetworkProtectionWaitlistViewControllerPresenter.show() - } - } + popovers.toggleNetworkProtectionPopover(usingView: networkProtectionButton, withDelegate: networkProtectionButtonModel) } @IBAction func downloadsButtonAction(_ sender: NSButton) { @@ -622,7 +591,7 @@ final class NavigationBarViewController: NSViewController { } let heightChange: () -> Void - if animated && view.window != nil { + if animated, let window = view.window, window.isVisible == true { heightChange = { NSAnimationContext.runAnimationGroup { ctx in ctx.duration = 0.1 @@ -644,27 +613,32 @@ final class NavigationBarViewController: NSViewController { performResize() } } - if view.window == nil { - // update synchronously for off-screen view - heightChange() - } else { + if let window = view.window, window.isVisible { let dispatchItem = DispatchWorkItem(block: heightChange) DispatchQueue.main.async(execute: dispatchItem) self.heightChangeAnimation = dispatchItem + } else { + // update synchronously for off-screen view + heightChange() } } 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 { @@ -673,20 +647,26 @@ 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) .map { (total, completed) -> Double? in guard total > 0, completed < total else { return nil } return Double(completed) / Double(total) } + .dropFirst() + .throttle(for: 0.2, scheduler: DispatchQueue.main, latest: true) .sink { [weak downloadsProgressView] progress in - downloadsProgressView?.setProgress(progress, animated: true) + guard let downloadsProgressView else { return } + if progress == nil, downloadsProgressView.progress != 1 { + // show download completed animation before hiding + downloadsProgressView.setProgress(1, animated: true) + } + downloadsProgressView.setProgress(progress, animated: true) } .store(in: &downloadsCancellables) } @@ -705,7 +685,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 @@ -958,14 +938,8 @@ extension NavigationBarViewController: NSMenuDelegate { // MARK: - VPN func showNetworkProtectionStatus() { - let featureVisibility = DefaultNetworkProtectionVisibility() - - if featureVisibility.isNetworkProtectionBetaVisible() { - popovers.showNetworkProtectionPopover(positionedBelow: networkProtectionButton, - withDelegate: networkProtectionButtonModel) - } else { - featureVisibility.disableForWaitlistUsers() - } + popovers.showNetworkProtectionPopover(positionedBelow: networkProtectionButton, + withDelegate: networkProtectionButtonModel) } /// Sets up the VPN button. @@ -1023,6 +997,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..e9b9f4c847 100644 --- a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift +++ b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift @@ -28,16 +28,25 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { case networkProtectionControllerStartAttempt case networkProtectionControllerStartSuccess + case networkProtectionControllerStartCancelled case networkProtectionControllerStartFailure(_ error: Error) case networkProtectionTunnelStartAttempt 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 @@ -89,6 +98,13 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { case networkProtectionUnhandledError(function: String, line: Int, error: Error) + // Temporary pixels added to verify notification delivery rates: + case networkProtectionConnectedNotificationDisplayed + case networkProtectionDisconnectedNotificationDisplayed + case networkProtectionReconnectingNotificationDisplayed + case networkProtectionSupersededNotificationDisplayed + case networkProtectionExpiredEntitlementNotificationDisplayed + /// Name of the pixel event /// - Unique pixels must end with `_u` /// - Daily pixels will automatically have `_d` or `_c` appended to their names @@ -107,6 +123,9 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { case .networkProtectionControllerStartSuccess: return "netp_controller_start_success" + case .networkProtectionControllerStartCancelled: + return "netp_controller_start_cancelled" + case .networkProtectionControllerStartFailure: return "netp_controller_start_failure" @@ -119,6 +138,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 +156,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" @@ -247,6 +284,21 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { case .networkProtectionUnhandledError: return "netp_unhandled_error" + + case .networkProtectionConnectedNotificationDisplayed: + return "netp_connected_notification_displayed" + + case .networkProtectionDisconnectedNotificationDisplayed: + return "netp_disconnected_notification_displayed" + + case .networkProtectionReconnectingNotificationDisplayed: + return "netp_reconnecting_notification_displayed" + + case .networkProtectionSupersededNotificationDisplayed: + return "netp_superseded_notification_displayed" + + case .networkProtectionExpiredEntitlementNotificationDisplayed: + return "netp_expired_entitlement_notification_displayed" } } @@ -296,13 +348,20 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { .networkProtectionNewUser, .networkProtectionControllerStartAttempt, .networkProtectionControllerStartSuccess, + .networkProtectionControllerStartCancelled, .networkProtectionControllerStartFailure, .networkProtectionTunnelStartAttempt, .networkProtectionTunnelStartSuccess, .networkProtectionTunnelStartFailure, + .networkProtectionTunnelStopAttempt, + .networkProtectionTunnelStopSuccess, + .networkProtectionTunnelStopFailure, .networkProtectionTunnelUpdateAttempt, .networkProtectionTunnelUpdateSuccess, .networkProtectionTunnelUpdateFailure, + .networkProtectionTunnelWakeAttempt, + .networkProtectionTunnelWakeSuccess, + .networkProtectionTunnelWakeFailure, .networkProtectionEnableAttemptConnecting, .networkProtectionEnableAttemptSuccess, .networkProtectionEnableAttemptFailure, @@ -328,7 +387,12 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { .networkProtectionRekeyAttempt, .networkProtectionRekeyCompleted, .networkProtectionRekeyFailure, - .networkProtectionSystemExtensionActivationFailure: + .networkProtectionSystemExtensionActivationFailure, + .networkProtectionConnectedNotificationDisplayed, + .networkProtectionDisconnectedNotificationDisplayed, + .networkProtectionReconnectingNotificationDisplayed, + .networkProtectionSupersededNotificationDisplayed, + .networkProtectionExpiredEntitlementNotificationDisplayed: return nil } } @@ -343,7 +407,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), @@ -354,10 +420,15 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { .networkProtectionNewUser, .networkProtectionControllerStartAttempt, .networkProtectionControllerStartSuccess, + .networkProtectionControllerStartCancelled, .networkProtectionTunnelStartAttempt, .networkProtectionTunnelStartSuccess, + .networkProtectionTunnelStopAttempt, + .networkProtectionTunnelStopSuccess, .networkProtectionTunnelUpdateAttempt, .networkProtectionTunnelUpdateSuccess, + .networkProtectionTunnelWakeAttempt, + .networkProtectionTunnelWakeSuccess, .networkProtectionEnableAttemptConnecting, .networkProtectionEnableAttemptSuccess, .networkProtectionEnableAttemptFailure, @@ -387,7 +458,12 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { .networkProtectionWireguardErrorCannotStartWireguardBackend, .networkProtectionNoAuthTokenFoundError, .networkProtectionRekeyAttempt, - .networkProtectionRekeyCompleted: + .networkProtectionRekeyCompleted, + .networkProtectionConnectedNotificationDisplayed, + .networkProtectionDisconnectedNotificationDisplayed, + .networkProtectionReconnectingNotificationDisplayed, + .networkProtectionSupersededNotificationDisplayed, + .networkProtectionExpiredEntitlementNotificationDisplayed: return nil } } 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/Invite/NetworkProtectionInviteCodeViewModel.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInviteCodeViewModel.swift deleted file mode 100644 index 9329924b85..0000000000 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInviteCodeViewModel.swift +++ /dev/null @@ -1,184 +0,0 @@ -// -// NetworkProtectionInviteCodeViewModel.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 Combine -import NetworkProtection -import SwiftUIExtensions - -enum NetworkProtectionInviteDialogKind { - case codeEntry, success -} - -protocol NetworkProtectionInviteViewModelDelegate: AnyObject { - func didCancelInviteFlow() - func didCompleteInviteFlow() -} - -final class NetworkProtectionInviteViewModel: ObservableObject { - - @Published var currentDialog: NetworkProtectionInviteDialogKind? = .codeEntry - let inviteCodeViewModel: NetworkProtectionInviteCodeViewModel - let successCodeViewModel: NetworkProtectionInviteSuccessViewModel - - private weak var delegate: NetworkProtectionInviteViewModelDelegate? - - init(delegate: NetworkProtectionInviteViewModelDelegate, redemptionCoordinator: NetworkProtectionCodeRedeeming) { - self.delegate = delegate - inviteCodeViewModel = NetworkProtectionInviteCodeViewModel(redemptionCoordinator: redemptionCoordinator) - successCodeViewModel = NetworkProtectionInviteSuccessViewModel() - - inviteCodeViewModel.delegate = self - successCodeViewModel.delegate = self - } - - func getStarted() { - delegate?.didCompleteInviteFlow() - } - - func cancel() { - delegate?.didCancelInviteFlow() - currentDialog = nil - } -} - -extension NetworkProtectionInviteViewModel: NetworkProtectionInviteCodeViewModelDelegate { - - func networkProtectionInviteCodeViewModelDidReedemSuccessfully(_ viewModel: NetworkProtectionInviteCodeViewModel) { - currentDialog = .success - } - - func networkProtectionInviteCodeViewModelDidCancel(_ viewModel: NetworkProtectionInviteCodeViewModel) { - delegate?.didCancelInviteFlow() - currentDialog = nil - } -} - -extension NetworkProtectionInviteViewModel: NetworkProtectionInviteSuccessViewModelDelegate { - - func networkProtectionInviteSuccessViewModelDidConfirm(_ viewModel: NetworkProtectionInviteSuccessViewModel) { - delegate?.didCompleteInviteFlow() - } -} - -protocol NetworkProtectionInviteCodeViewModelDelegate: AnyObject { - func networkProtectionInviteCodeViewModelDidReedemSuccessfully(_ viewModel: NetworkProtectionInviteCodeViewModel) - func networkProtectionInviteCodeViewModelDidCancel(_ viewModel: NetworkProtectionInviteCodeViewModel) -} - -final class NetworkProtectionInviteCodeViewModel: InviteCodeViewModel { - - weak var delegate: NetworkProtectionInviteCodeViewModelDelegate? - - var titleText: String { - UserText.networkProtectionInviteDialogTitle - } - - var messageText: String { - UserText.networkProtectionInviteDialogMessage - } - - var textFieldPlaceholder: String { - UserText.networkProtectionInviteFieldPrompt - } - - var cancelButtonText: String { - UserText.cancel - } - - var confirmButtonText: String { - UserText.continue - } - - @Published var textFieldText: String = "" { - didSet { - if oldValue != textFieldText { - textFieldText = textFieldText.uppercased() - } - } - } - - @Published var errorText: String? - - @Published var showProgressView = false - - private let redemptionCoordinator: NetworkProtectionCodeRedeeming - private var textCancellable: AnyCancellable? - - init(redemptionCoordinator: NetworkProtectionCodeRedeeming) { - self.redemptionCoordinator = redemptionCoordinator - textCancellable = $textFieldText.sink { [weak self] _ in - self?.errorText = nil - } - } - - @MainActor - func onConfirm() async { - errorText = nil - showProgressView = true - do { - try await redemptionCoordinator.redeem(textFieldText.trimmingWhitespace()) - - // If the user bypassed the waitlist, then erase any existing waitlist state they already have to avoid confusing the app. - NetworkProtectionWaitlist().waitlistStorage.deleteWaitlistState() - UserDefaults().removeObject(forKey: UserDefaultsWrapper.Key.networkProtectionTermsAndConditionsAccepted.rawValue) - UserDefaults().removeObject(forKey: UserDefaultsWrapper.Key.networkProtectionWaitlistSignUpPromptDismissed.rawValue) - NotificationCenter.default.post(name: .networkProtectionWaitlistAccessChanged, object: nil) - } catch NetworkProtectionClientError.invalidInviteCode { - errorText = UserText.inviteDialogUnrecognizedCodeMessage - showProgressView = false - return - } catch { - errorText = UserText.unknownErrorTryAgainMessage - showProgressView = false - return - } - showProgressView = false - delegate?.networkProtectionInviteCodeViewModelDidReedemSuccessfully(self) - } - - func onCancel() { - delegate?.networkProtectionInviteCodeViewModelDidCancel(self) - } - -} - -protocol NetworkProtectionInviteSuccessViewModelDelegate: AnyObject { - func networkProtectionInviteSuccessViewModelDidConfirm(_ viewModel: NetworkProtectionInviteSuccessViewModel) -} - -final class NetworkProtectionInviteSuccessViewModel: InviteCodeSuccessViewModel { - - weak var delegate: NetworkProtectionInviteSuccessViewModelDelegate? - - var titleText: String { - UserText.networkProtectionInviteSuccessTitle - } - - var messageText: String { - UserText.networkProtectionInviteSuccessMessage - } - - var confirmButtonText: String { - UserText.inviteDialogGetStartedButton - } - - func onConfirm() { - delegate?.networkProtectionInviteSuccessViewModelDidConfirm(self) - } - -} diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInvitePresenter.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInvitePresenter.swift deleted file mode 100644 index d845eedc0c..0000000000 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInvitePresenter.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// NetworkProtectionInvitePresenter.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 SwiftUI -import NetworkProtection - -protocol NetworkProtectionInvitePresenting { - func present() -} - -final class NetworkProtectionInvitePresenter: NetworkProtectionInvitePresenting, NetworkProtectionInviteViewModelDelegate { - - private var presentedViewController: NSViewController? - - // MARK: NetworkProtectionInvitePresenting - - @MainActor func present() { - let viewModel = NetworkProtectionInviteViewModel(delegate: self, redemptionCoordinator: NetworkProtectionCodeRedemptionCoordinator()) - - let view = NetworkProtectionInviteDialog(model: viewModel) - let hostingVC = NSHostingController(rootView: view) - presentedViewController = hostingVC - let newWindowController = hostingVC.wrappedInWindowController() - - guard let newWindow = newWindowController.window, - let parentWindowController = WindowControllersManager.shared.lastKeyMainWindowController - else { - assertionFailure("Failed to present \(hostingVC)") - return - } - parentWindowController.window?.beginSheet(newWindow) - } - - // MARK: NetworkProtectionInviteViewModelDelegate - - func didCancelInviteFlow() { - presentedViewController?.dismiss() - presentedViewController = nil - } - - func didCompleteInviteFlow() { - presentedViewController?.dismiss() - presentedViewController = nil - Task { - await WindowControllersManager.shared.showNetworkProtectionStatus() - } - } -} diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift index 9898138c7f..fe1993cb94 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift @@ -21,6 +21,7 @@ import NetworkProtection import NetworkProtectionIPC import Common import Subscription +import BrowserServicesKit extension NetworkProtectionDeviceManager { diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift index 2833f1a23d..472b1f5275 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift @@ -53,13 +53,6 @@ final class NetworkProtectionDebugMenu: NSMenu { private let excludeLocalNetworksMenuItem = NSMenuItem(title: "excludeLocalNetworks", action: #selector(NetworkProtectionDebugMenu.toggleShouldExcludeLocalRoutes)) - private let enterWaitlistInviteCodeItem = NSMenuItem(title: "Enter Waitlist Invite Code", action: #selector(NetworkProtectionDebugMenu.showNetworkProtectionInviteCodePrompt)) - - private let waitlistTokenItem = NSMenuItem(title: "Waitlist Token:") - private let waitlistTimestampItem = NSMenuItem(title: "Waitlist Timestamp:") - private let waitlistInviteCodeItem = NSMenuItem(title: "Waitlist Invite Code:") - private let waitlistTermsAndConditionsAcceptedItem = NSMenuItem(title: "T&C Accepted:") - // swiftlint:disable:next function_body_length init() { preferredServerMenu = NSMenu { [preferredServerAutomaticItem] in @@ -144,28 +137,6 @@ final class NetworkProtectionDebugMenu: NSMenu { .targetting(self) } - NSMenuItem(title: "NetP Waitlist") { - NSMenuItem(title: "Reset Waitlist State", action: #selector(NetworkProtectionDebugMenu.resetNetworkProtectionWaitlistState)) - .targetting(self) - NSMenuItem(title: "Reset T&C Acceptance", action: #selector(NetworkProtectionDebugMenu.resetNetworkProtectionTermsAndConditionsAcceptance)) - .targetting(self) - - enterWaitlistInviteCodeItem - .targetting(self) - - NSMenuItem(title: "Send Waitlist Notification", action: #selector(NetworkProtectionDebugMenu.sendNetworkProtectionWaitlistAvailableNotification)) - .targetting(self) - NSMenuItem.separator() - - waitlistTokenItem - waitlistTimestampItem - waitlistInviteCodeItem - waitlistTermsAndConditionsAcceptedItem - } - - NSMenuItem(title: "NetP Waitlist Feature Flag Overrides") - .submenu(NetworkProtectionWaitlistFeatureFlagOverridesMenu()) - NSMenuItem.separator() NSMenuItem(title: "Kill Switch (alternative approach)") { @@ -423,10 +394,6 @@ final class NetworkProtectionDebugMenu: NSMenu { excludedRoutesMenu.addItem(menuItem) } - // Only allow testers to enter a custom code if they're on the waitlist, to simulate the correct path through the flow - let waitlist = NetworkProtectionWaitlist() - enterWaitlistInviteCodeItem.isEnabled = waitlist.waitlistStorage.isOnWaitlist || waitlist.waitlistStorage.isInvited - } // MARK: - Menu State Update @@ -437,7 +404,6 @@ final class NetworkProtectionDebugMenu: NSMenu { updatePreferredServerMenu() updateRekeyValidityMenu() updateNetworkProtectionMenuItemsState() - updateNetworkProtectionItems() } private func updateEnvironmentMenu() { @@ -504,27 +470,8 @@ final class NetworkProtectionDebugMenu: NSMenu { disableRekeyingMenuItem.state = settings.disableRekeying ? .on : .off } - private func updateNetworkProtectionItems() { - let waitlistStorage = WaitlistKeychainStore(waitlistIdentifier: NetworkProtectionWaitlist.identifier, keychainAppGroup: NetworkProtectionWaitlist.keychainAppGroup) - waitlistTokenItem.title = "Waitlist Token: \(waitlistStorage.getWaitlistToken() ?? "N/A")" - waitlistInviteCodeItem.title = "Waitlist Invite Code: \(waitlistStorage.getWaitlistInviteCode() ?? "N/A")" - - if let timestamp = waitlistStorage.getWaitlistTimestamp() { - waitlistTimestampItem.title = "Waitlist Timestamp: \(String(describing: timestamp))" - } else { - waitlistTimestampItem.title = "Waitlist Timestamp: N/A" - } - - let accepted = UserDefaults().bool(forKey: UserDefaultsWrapper.Key.networkProtectionTermsAndConditionsAccepted.rawValue) - waitlistTermsAndConditionsAcceptedItem.title = "T&C Accepted: \(accepted ? "Yes" : "No")" - } - // MARK: Waitlist - @objc func sendNetworkProtectionWaitlistAvailableNotification(_ sender: Any?) { - NetworkProtectionWaitlist().sendInviteCodeAvailableNotification(completion: nil) - } - @objc func resetNetworkProtectionActivationDate(_ sender: Any?) { overrideNetworkProtectionActivationDate(to: nil) } @@ -556,52 +503,6 @@ final class NetworkProtectionDebugMenu: NSMenu { } } - @objc func resetNetworkProtectionWaitlistState(_ sender: Any?) { - NetworkProtectionWaitlist().waitlistStorage.deleteWaitlistState() - UserDefaults().removeObject(forKey: UserDefaultsWrapper.Key.networkProtectionTermsAndConditionsAccepted.rawValue) - UserDefaults().removeObject(forKey: UserDefaultsWrapper.Key.networkProtectionWaitlistSignUpPromptDismissed.rawValue) - NotificationCenter.default.post(name: .networkProtectionWaitlistAccessChanged, object: nil) - } - - @objc func resetNetworkProtectionTermsAndConditionsAcceptance(_ sender: Any?) { - UserDefaults().removeObject(forKey: UserDefaultsWrapper.Key.networkProtectionTermsAndConditionsAccepted.rawValue) - NotificationCenter.default.post(name: .networkProtectionWaitlistAccessChanged, object: nil) - } - - @objc func showNetworkProtectionInviteCodePrompt(_ sender: Any?) { - let code = getInviteCode() - - Task { - do { - let redeemer = NetworkProtectionCodeRedemptionCoordinator() - try await redeemer.redeem(code) - NetworkProtectionWaitlist().waitlistStorage.store(inviteCode: code) - NotificationCenter.default.post(name: .networkProtectionWaitlistAccessChanged, object: nil) - } catch { - // Do nothing here, this is just a debug menu - } - } - } - - private func getInviteCode() -> String { - let alert = NSAlert() - alert.addButton(withTitle: "Use Invite Code") - alert.addButton(withTitle: "Cancel") - alert.messageText = "Enter Invite Code" - alert.informativeText = "Please grab a VPN invite code from Asana and enter it here." - - let textField = NSTextField(frame: NSRect(x: 0, y: 0, width: 200, height: 24)) - alert.accessoryView = textField - - let response = alert.runModal() - - if response == .alertFirstButtonReturn { - return textField.stringValue - } else { - return "" - } - } - // MARK: Environment @objc func setSelectedEnvironment(_ menuItem: NSMenuItem) { diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift index 8c141f7061..721f194c0d 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift @@ -55,7 +55,7 @@ final class NetworkProtectionDebugUtilities { // MARK: - Debug commands for the extension func resetAllState(keepAuthToken: Bool) async { - let uninstalledSuccessfully = await networkProtectionFeatureDisabler.disable(keepAuthToken: keepAuthToken, uninstallSystemExtension: true) + let uninstalledSuccessfully = await networkProtectionFeatureDisabler.disable(uninstallSystemExtension: true) guard uninstalledSuccessfully else { return @@ -63,8 +63,6 @@ final class NetworkProtectionDebugUtilities { settings.resetToDefaults() - NetworkProtectionWaitlist().waitlistStorage.deleteWaitlistState() - DefaultWaitlistActivationDateStore(source: .netP).removeDates() DefaultHomePageRemoteMessagingStorage.networkProtection().removeStoredAndDismissedMessages() UserDefaults().removeObject(forKey: UserDefaultsWrapper.Key.networkProtectionTermsAndConditionsAccepted.rawValue) diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift index de9f3ee692..e4742d3521 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift @@ -169,14 +169,6 @@ final class NetworkProtectionNavBarButtonModel: NSObject, ObservableObject { @MainActor func updateVisibility() { - // The button is visible in the case where NetP has not been activated, but the user has been invited and they haven't accepted T&Cs. - if vpnVisibility.isNetworkProtectionBetaVisible() { - if NetworkProtectionWaitlist().readyToAcceptTermsAndConditions { - showButton = true - return - } - } - guard !isPinned, !popoverManager.isShown, !isHavingConnectivityIssues else { diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift index 1cd0015748..14e266a11c 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift @@ -116,7 +116,7 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { userDefaults: .netP, locationFormatter: DefaultVPNLocationFormatter(), uninstallHandler: { [weak self] in - _ = await self?.networkProtectionFeatureDisabler.disable(keepAuthToken: false, uninstallSystemExtension: true) + _ = await self?.networkProtectionFeatureDisabler.disable(uninstallSystemExtension: true) }) popover.delegate = delegate @@ -138,13 +138,7 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { networkProtectionPopover.close() self.networkProtectionPopover = nil } else { - let featureVisibility = DefaultNetworkProtectionVisibility() - - if featureVisibility.isNetworkProtectionBetaVisible() { - show(positionedBelow: view, withDelegate: delegate) - } else { - featureVisibility.disableForWaitlistUsers() - } + show(positionedBelow: view, withDelegate: delegate) } } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift index c2565dc405..ed20ffef1d 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift @@ -448,14 +448,18 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr // MARK: - Starting & Stopping the VPN - enum StartError: LocalizedError { + enum StartError: LocalizedError, CustomNSError { + case cancelled case noAuthToken case connectionStatusInvalid case connectionAlreadyStarted case simulateControllerFailureError + case startTunnelFailure(_ error: Error) var errorDescription: String? { switch self { + case .cancelled: + return nil case .noAuthToken: return "You need a subscription to start the VPN" case .connectionAlreadyStarted: @@ -473,6 +477,34 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr #endif case .simulateControllerFailureError: return "Simulated a controller error as requested" + case .startTunnelFailure(let error): + return error.localizedDescription + } + } + + var errorCode: Int { + switch self { + case .cancelled: return 0 + // MARK: Setup errors + case .noAuthToken: return 1 + case .connectionStatusInvalid: return 2 + case .connectionAlreadyStarted: return 3 + case .simulateControllerFailureError: return 4 + // MARK: Actual connection attempt issues + case .startTunnelFailure: return 100 + } + } + + var errorUserInfo: [String: Any] { + switch self { + case .cancelled, + .noAuthToken, + .connectionStatusInvalid, + .connectionAlreadyStarted, + .simulateControllerFailureError: + return [:] + case .startTunnelFailure(let error): + return [NSUnderlyingErrorKey: error] } } } @@ -480,6 +512,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr /// Starts the VPN connection /// func start() async { + VPNOperationErrorRecorder().beginRecordingControllerStart() PixelKit.fire(NetworkProtectionPixelEvent.networkProtectionControllerStartAttempt, frequency: .dailyAndCount) controllerErrorStore.lastErrorMessage = nil @@ -501,6 +534,8 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr } catch { if case NEVPNError.configurationReadWriteFailed = error { onboardingStatusRawValue = OnboardingStatus.isOnboarding(step: .userNeedsToAllowVPNConfiguration).rawValue + + throw StartError.cancelled } throw error @@ -525,11 +560,21 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr frequency: .dailyAndCount) } } catch { - PixelKit.fire( - NetworkProtectionPixelEvent.networkProtectionControllerStartFailure(error), frequency: .dailyAndCount, includeAppVersionParameter: true - ) + VPNOperationErrorRecorder().recordControllerStartFailure(error) + + if case StartError.cancelled = error { + PixelKit.fire( + NetworkProtectionPixelEvent.networkProtectionControllerStartCancelled, frequency: .dailyAndCount, includeAppVersionParameter: true + ) + } else { + PixelKit.fire( + NetworkProtectionPixelEvent.networkProtectionControllerStartFailure(error), frequency: .dailyAndCount, includeAppVersionParameter: true + ) + } - await stop() + if await isConnected { + await stop() + } // Always keep the first error message shown, as it's the more actionable one. if controllerErrorStore.lastErrorMessage == nil { @@ -574,7 +619,11 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr throw StartError.simulateControllerFailureError } - try tunnelManager.connection.startVPNTunnel(options: options) + do { + try tunnelManager.connection.startVPNTunnel(options: options) + } catch { + throw StartError.startTunnelFailure(error) + } PixelKit.fire( NetworkProtectionPixelEvent.networkProtectionNewUser, diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift deleted file mode 100644 index aa9fce2460..0000000000 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift +++ /dev/null @@ -1,166 +0,0 @@ -// -// NetworkProtectionWaitlistFeatureFlagOverridesMenu.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 AppKit -import Foundation -import NetworkProtection -import NetworkProtectionUI -import SwiftUI - -/// Implements the logic for the VPN's simulate failures menu. -/// -@MainActor -final class NetworkProtectionWaitlistFeatureFlagOverridesMenu: NSMenu { - - // MARK: - Waitlist Active Properties - - private let waitlistActiveUseRemoteValueMenuItem: NSMenuItem - private let waitlistActiveOverrideONMenuItem: NSMenuItem - private let waitlistActiveOverrideOFFMenuItem: NSMenuItem - - @UserDefaultsWrapper(key: .networkProtectionWaitlistActiveOverrideRawValue, - defaultValue: WaitlistOverride.default.rawValue, - defaults: .netP) - private var waitlistActiveOverrideValue: Int - - // MARK: - Waitlist Enabled Properties - - private let waitlistEnabledUseRemoteValueMenuItem: NSMenuItem - private let waitlistEnabledOverrideONMenuItem: NSMenuItem - private let waitlistEnabledOverrideOFFMenuItem: NSMenuItem - - @UserDefaultsWrapper(key: .networkProtectionWaitlistEnabledOverrideRawValue, - defaultValue: WaitlistOverride.default.rawValue, - defaults: .netP) - private var waitlistEnabledOverrideValue: Int - - init() { - waitlistActiveUseRemoteValueMenuItem = NSMenuItem(title: "Remote Value", action: #selector(Self.waitlistEnabledUseRemoteValue)) - waitlistActiveOverrideONMenuItem = NSMenuItem(title: "ON", action: #selector(Self.waitlistEnabledOverrideON)) - waitlistActiveOverrideOFFMenuItem = NSMenuItem(title: "OFF", action: #selector(Self.waitlistEnabledOverrideOFF)) - - waitlistEnabledUseRemoteValueMenuItem = NSMenuItem(title: "Remote Value", action: #selector(Self.waitlistActiveUseRemoteValue)) - waitlistEnabledOverrideONMenuItem = NSMenuItem(title: "ON", action: #selector(Self.waitlistActiveOverrideON)) - waitlistEnabledOverrideOFFMenuItem = NSMenuItem(title: "OFF", action: #selector(Self.waitlistActiveOverrideOFF)) - - super.init(title: "") - buildItems { - NSMenuItem(title: "Reset Waitlist Overrides", action: #selector(Self.waitlistResetFeatureOverrides)).targetting(self) - NSMenuItem.separator() - - NSMenuItem(title: "Waitlist Enabled") { - waitlistActiveUseRemoteValueMenuItem.targetting(self) - waitlistActiveOverrideONMenuItem.targetting(self) - waitlistActiveOverrideOFFMenuItem.targetting(self) - } - - NSMenuItem(title: "Waitlist Active") { - waitlistEnabledUseRemoteValueMenuItem.targetting(self) - waitlistEnabledOverrideONMenuItem.targetting(self) - waitlistEnabledOverrideOFFMenuItem.targetting(self) - } - } - } - - required init(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Misc IBActions - - @objc func waitlistResetFeatureOverrides(sender: NSMenuItem) { - waitlistActiveOverrideValue = WaitlistOverride.default.rawValue - waitlistEnabledOverrideValue = WaitlistOverride.default.rawValue - } - - // MARK: - Waitlist Active IBActions - - @objc func waitlistActiveUseRemoteValue(sender: NSMenuItem) { - waitlistActiveOverrideValue = WaitlistOverride.useRemoteValue.rawValue - } - - @objc func waitlistActiveOverrideON(sender: NSMenuItem) { - waitlistActiveOverrideValue = WaitlistOverride.on.rawValue - } - - @objc func waitlistActiveOverrideOFF(sender: NSMenuItem) { - Task { @MainActor in - guard case .alertFirstButtonReturn = await waitlistOFFAlert().runModal() else { - return - } - - waitlistActiveOverrideValue = WaitlistOverride.off.rawValue - } - } - - // MARK: - Waitlist Enabled IBActions - - @objc func waitlistEnabledUseRemoteValue(sender: NSMenuItem) { - waitlistEnabledOverrideValue = WaitlistOverride.useRemoteValue.rawValue - } - - @objc func waitlistEnabledOverrideON(sender: NSMenuItem) { - waitlistEnabledOverrideValue = WaitlistOverride.on.rawValue - } - - @objc func waitlistEnabledOverrideOFF(sender: NSMenuItem) { - Task { @MainActor in - guard case .alertFirstButtonReturn = await waitlistOFFAlert().runModal() else { - return - } - - waitlistEnabledOverrideValue = WaitlistOverride.off.rawValue - } - } - - // MARK: - Updating the menu state - - override func update() { - waitlistActiveUseRemoteValueMenuItem.state = waitlistActiveOverrideValue == WaitlistOverride.useRemoteValue.rawValue ? .on : .off - waitlistActiveOverrideONMenuItem.state = waitlistActiveOverrideValue == WaitlistOverride.on.rawValue ? .on : .off - waitlistActiveOverrideOFFMenuItem.state = waitlistActiveOverrideValue == WaitlistOverride.off.rawValue ? .on : .off - - waitlistEnabledUseRemoteValueMenuItem.state = waitlistEnabledOverrideValue == WaitlistOverride.useRemoteValue.rawValue ? .on : .off - waitlistEnabledOverrideONMenuItem.state = waitlistEnabledOverrideValue == WaitlistOverride.on.rawValue ? .on : .off - waitlistEnabledOverrideOFFMenuItem.state = waitlistEnabledOverrideValue == WaitlistOverride.off.rawValue ? .on : .off - } - - // MARK: - UI Additions - - private func waitlistOFFAlert() -> NSAlert { - let alert = NSAlert() - alert.messageText = "Override to OFF value?" - alert.informativeText = """ - This will potentially disable DuckDuckGo VPN and erase your invitation. - - You can re-enable DuckDuckGo VPN after reverting this change. - - Please click 'Cancel' if you're unsure. - """ - alert.alertStyle = .warning - alert.addButton(withTitle: "Override") - alert.addButton(withTitle: UserText.cancel) - return alert - } -} - -#if DEBUG -#Preview { - return MenuPreview(menu: NetworkProtectionWaitlistFeatureFlagOverridesMenu()) -} -#endif 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/NetworkProtectionRemoteMessaging/NetworkProtectionRemoteMessaging.swift b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionRemoteMessaging/NetworkProtectionRemoteMessaging.swift index 739c7501ed..6daeb90569 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionRemoteMessaging/NetworkProtectionRemoteMessaging.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionRemoteMessaging/NetworkProtectionRemoteMessaging.swift @@ -132,7 +132,7 @@ final class DefaultNetworkProtectionRemoteMessaging: NetworkProtectionRemoteMess } // Next, check if the message requires access to NetP but it's not visible: - if message.requiresNetworkProtectionAccess, !networkProtectionVisibility.isNetworkProtectionBetaVisible() { + if message.requiresNetworkProtectionAccess, !networkProtectionVisibility.isVPNVisible() { return false } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift index b1e4a36a7d..31795df3a3 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift @@ -17,28 +17,25 @@ // import Combine +import Common import Foundation import Subscription import NetworkProtection import NetworkProtectionUI -import Common final class NetworkProtectionSubscriptionEventHandler { private let accountManager: AccountManager - private let networkProtectionRedemptionCoordinator: NetworkProtectionCodeRedeeming private let networkProtectionTokenStorage: NetworkProtectionTokenStore private let networkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling private let userDefaults: UserDefaults private var cancellables = Set() init(accountManager: AccountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)), - networkProtectionRedemptionCoordinator: NetworkProtectionCodeRedeeming = NetworkProtectionCodeRedemptionCoordinator(), networkProtectionTokenStorage: NetworkProtectionTokenStore = NetworkProtectionKeychainTokenStore(), networkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling = NetworkProtectionFeatureDisabler(), userDefaults: UserDefaults = .netP) { self.accountManager = accountManager - self.networkProtectionRedemptionCoordinator = networkProtectionRedemptionCoordinator self.networkProtectionTokenStorage = networkProtectionTokenStorage self.networkProtectionFeatureDisabler = networkProtectionFeatureDisabler self.userDefaults = userDefaults @@ -109,7 +106,7 @@ final class NetworkProtectionSubscriptionEventHandler { print("[NetP Subscription] Deleted NetP auth token after signing out from Privacy Pro") Task { - await networkProtectionFeatureDisabler.disable(keepAuthToken: false, uninstallSystemExtension: false) + await networkProtectionFeatureDisabler.disable(uninstallSystemExtension: false) } } diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/AppExtensionAndNotificationTargets/NetworkProtectionUNNotificationsPresenter.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/AppExtensionAndNotificationTargets/NetworkProtectionUNNotificationsPresenter.swift index 3977fccef5..80ac5013fc 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/AppExtensionAndNotificationTargets/NetworkProtectionUNNotificationsPresenter.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/AppExtensionAndNotificationTargets/NetworkProtectionUNNotificationsPresenter.swift @@ -20,6 +20,7 @@ import Foundation import UserNotifications import NetworkProtection import NetworkProtectionUI +import PixelKit extension UNNotificationAction { @@ -159,6 +160,20 @@ final class NetworkProtectionUNNotificationsPresenter: NSObject, NetworkProtecti _=self.registerNotificationCategoriesOnce self.userNotificationCenter.removeDeliveredNotifications(withIdentifiers: [identifier.rawValue]) self.userNotificationCenter.add(request) + + switch identifier { + case .disconnected: + PixelKit.fire(NetworkProtectionPixelEvent.networkProtectionDisconnectedNotificationDisplayed, frequency: .dailyAndCount) + case .reconnecting: + PixelKit.fire(NetworkProtectionPixelEvent.networkProtectionReconnectingNotificationDisplayed, frequency: .dailyAndCount) + case .connected: + PixelKit.fire(NetworkProtectionPixelEvent.networkProtectionConnectedNotificationDisplayed, frequency: .dailyAndCount) + case .superseded: + PixelKit.fire(NetworkProtectionPixelEvent.networkProtectionSupersededNotificationDisplayed, frequency: .dailyAndCount) + case .expiredEntitlement: + PixelKit.fire(NetworkProtectionPixelEvent.networkProtectionExpiredEntitlementNotificationDisplayed, frequency: .dailyAndCount) + case .test: break + } } } diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift index 842ff33ad8..5623af1f0c 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,51 @@ 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) + } + case .failureRecoveryAttempt(let step): + switch step { + case .started: + PixelKit.fire( + VPNFailureRecoveryPixel.vpnFailureRecoveryStarted, + frequency: .dailyAndCount, + includeAppVersionParameter: true + ) + case .completed(.healthy): + PixelKit.fire( + VPNFailureRecoveryPixel.vpnFailureRecoveryCompletedHealthy, + frequency: .dailyAndCount, + includeAppVersionParameter: true + ) + case .completed(.unhealthy): + PixelKit.fire( + VPNFailureRecoveryPixel.vpnFailureRecoveryCompletedUnhealthy, + frequency: .dailyAndCount, + includeAppVersionParameter: true + ) + case .failed(let error): + PixelKit.fire( + VPNFailureRecoveryPixel.vpnFailureRecoveryFailed(error), + frequency: .dailyAndCount, + includeAppVersionParameter: true + ) + } } } @@ -264,6 +327,7 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { // MARK: - Initialization + @MainActor @objc public init() { let isSubscriptionEnabled = false @@ -272,6 +336,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) @@ -321,6 +388,7 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { /// Observe server changes to broadcast those changes through distributed notifications. /// + @MainActor private func observeServerChanges() { lastSelectedServerInfoPublisher.sink { [weak self] server in self?.lastStatusChangeDate = Date() @@ -365,6 +433,7 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { /// Broadcasts the current server information. /// + @MainActor private func broadcastLastSelectedServerInfo() { broadcast(lastSelectedServerInfo) } @@ -421,30 +490,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/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/Pixels/VPNFailureRecoveryPixel.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/Pixels/VPNFailureRecoveryPixel.swift new file mode 100644 index 0000000000..3fdae85a54 --- /dev/null +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/Pixels/VPNFailureRecoveryPixel.swift @@ -0,0 +1,67 @@ +// +// VPNFailureRecoveryPixel.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import PixelKit + +/// PrivacyPro pixels. +/// +/// Ref: https://app.asana.com/0/0/1206939413299475/f +/// +public enum VPNFailureRecoveryPixel: PixelKitEventV2 { + + /// This pixel is emitted when the last handshake diff is greater than n minutes and an attempt to recover is made (/register is called with failureRecovery) + /// + case vpnFailureRecoveryStarted + + /// This pixel is emitted when the recovery attempt failed due to any reason. + /// + case vpnFailureRecoveryFailed(Error) + + /// This pixel is emitted when the recovery attempt completed and the server was healthy and no further action needs to be taken. + /// + case vpnFailureRecoveryCompletedHealthy + + /// This pixel is emitted when the recovery attempt completed and the server was unhealthy resulting to reconnecting to a different server. + /// + case vpnFailureRecoveryCompletedUnhealthy + + public var name: String { + switch self { + case .vpnFailureRecoveryStarted: + return "m_mac_netp_ev_failure_recovery_started" + case .vpnFailureRecoveryFailed: + return "m_mac_netp_ev_failure_recovery_failed" + case .vpnFailureRecoveryCompletedHealthy: + return "m_mac_netp_ev_failure_recovery_completed_server_healthy" + case .vpnFailureRecoveryCompletedUnhealthy: + return "m_mac_netp_ev_failure_recovery_completed_server_unhealthy" + } + } + + public var error: Error? { + switch self { + case .vpnFailureRecoveryStarted, .vpnFailureRecoveryCompletedHealthy, .vpnFailureRecoveryCompletedUnhealthy: return nil + case .vpnFailureRecoveryFailed(let error): return error + } + } + + public var parameters: [String: String]? { + nil + } +} 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/AboutModel.swift b/DuckDuckGo/Preferences/Model/AboutModel.swift index 1504240116..ed2e0a914d 100644 --- a/DuckDuckGo/Preferences/Model/AboutModel.swift +++ b/DuckDuckGo/Preferences/Model/AboutModel.swift @@ -22,12 +22,6 @@ import Common final class AboutModel: ObservableObject, PreferencesTabOpening { let appVersion = AppVersion() - private let netPInvitePresenter: NetworkProtectionInvitePresenting - - init(netPInvitePresenter: NetworkProtectionInvitePresenting) { - self.netPInvitePresenter = netPInvitePresenter - } - let displayableAboutURL: String = URL.aboutDuckDuckGo .toString(decodePunycode: false, dropScheme: true, dropTrailingSlash: false) @@ -39,8 +33,4 @@ final class AboutModel: ObservableObject, PreferencesTabOpening { func copy(_ value: String) { NSPasteboard.general.copy(value) } - - func displayNetPInvite() { - netPInvitePresenter.present() - } } diff --git a/DuckDuckGo/Preferences/Model/DataClearingPreferences.swift b/DuckDuckGo/Preferences/Model/DataClearingPreferences.swift index e336ab7cbd..26c2647981 100644 --- a/DuckDuckGo/Preferences/Model/DataClearingPreferences.swift +++ b/DuckDuckGo/Preferences/Model/DataClearingPreferences.swift @@ -29,6 +29,27 @@ final class DataClearingPreferences: ObservableObject, PreferencesTabOpening { } } + @Published + var isAutoClearEnabled: Bool { + didSet { + persistor.autoClearEnabled = isAutoClearEnabled + NotificationCenter.default.post(name: .autoClearDidChange, + object: nil, + userInfo: nil) + } + } + + @Published + var isWarnBeforeClearingEnabled: Bool { + didSet { + persistor.warnBeforeClearingEnabled = isWarnBeforeClearingEnabled + } + } + + @objc func toggleWarnBeforeClearing() { + isWarnBeforeClearingEnabled.toggle() + } + @MainActor func presentManageFireproofSitesDialog() { let fireproofDomainsWindowController = FireproofDomainsViewController.create().wrappedInWindowController() @@ -46,6 +67,8 @@ final class DataClearingPreferences: ObservableObject, PreferencesTabOpening { init(persistor: FireButtonPreferencesPersistor = FireButtonPreferencesUserDefaultsPersistor()) { self.persistor = persistor isLoginDetectionEnabled = persistor.loginDetectionEnabled + isAutoClearEnabled = persistor.autoClearEnabled + isWarnBeforeClearingEnabled = persistor.warnBeforeClearingEnabled } private var persistor: FireButtonPreferencesPersistor @@ -53,6 +76,8 @@ final class DataClearingPreferences: ObservableObject, PreferencesTabOpening { protocol FireButtonPreferencesPersistor { var loginDetectionEnabled: Bool { get set } + var autoClearEnabled: Bool { get set } + var warnBeforeClearingEnabled: Bool { get set } } struct FireButtonPreferencesUserDefaultsPersistor: FireButtonPreferencesPersistor { @@ -60,4 +85,14 @@ struct FireButtonPreferencesUserDefaultsPersistor: FireButtonPreferencesPersisto @UserDefaultsWrapper(key: .loginDetectionEnabled, defaultValue: false) var loginDetectionEnabled: Bool + @UserDefaultsWrapper(key: .autoClearEnabled, defaultValue: false) + var autoClearEnabled: Bool + + @UserDefaultsWrapper(key: .warnBeforeClearingEnabled, defaultValue: false) + var warnBeforeClearingEnabled: Bool + +} + +extension Notification.Name { + static let autoClearDidChange = Notification.Name("autoClearDidChange") } diff --git a/DuckDuckGo/Preferences/Model/PreferencesSection.swift b/DuckDuckGo/Preferences/Model/PreferencesSection.swift index 6276691368..bdb6954120 100644 --- a/DuckDuckGo/Preferences/Model/PreferencesSection.swift +++ b/DuckDuckGo/Preferences/Model/PreferencesSection.swift @@ -19,6 +19,7 @@ import Foundation import SwiftUI import Subscription +import BrowserServicesKit struct PreferencesSection: Hashable, Identifiable { let id: PreferencesSectionIdentifier @@ -98,7 +99,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/Preferences/Model/SearchPreferences.swift b/DuckDuckGo/Preferences/Model/SearchPreferences.swift index bbe5cfb70c..65d385d72b 100644 --- a/DuckDuckGo/Preferences/Model/SearchPreferences.swift +++ b/DuckDuckGo/Preferences/Model/SearchPreferences.swift @@ -61,4 +61,9 @@ extension PreferencesTabOpening { WindowControllersManager.shared.show(url: url, source: .ui, newTab: true) } + @MainActor + func show(url: URL) { + WindowControllersManager.shared.show(url: url, source: .ui, newTab: false) + } + } diff --git a/DuckDuckGo/Preferences/Model/StartupPreferences.swift b/DuckDuckGo/Preferences/Model/StartupPreferences.swift index 6e46f9b729..9bdafbacf3 100644 --- a/DuckDuckGo/Preferences/Model/StartupPreferences.swift +++ b/DuckDuckGo/Preferences/Model/StartupPreferences.swift @@ -40,22 +40,28 @@ struct StartupPreferencesUserDefaultsPersistor: StartupPreferencesPersistor { } -final class StartupPreferences: ObservableObject { +final class StartupPreferences: ObservableObject, PreferencesTabOpening { static let shared = StartupPreferences() private let pinningManager: LocalPinningManager private var persistor: StartupPreferencesPersistor private var pinnedViewsNotificationCancellable: AnyCancellable? + private var dataClearingPreferences: DataClearingPreferences + private var dataClearingPreferencesNotificationCancellable: AnyCancellable? init(pinningManager: LocalPinningManager = LocalPinningManager.shared, - persistor: StartupPreferencesPersistor = StartupPreferencesUserDefaultsPersistor(appearancePrefs: AppearancePreferences.shared)) { + persistor: StartupPreferencesPersistor = StartupPreferencesUserDefaultsPersistor(appearancePrefs: AppearancePreferences.shared), + dataClearingPreferences: DataClearingPreferences = DataClearingPreferences.shared) { self.pinningManager = pinningManager self.persistor = persistor + self.dataClearingPreferences = dataClearingPreferences restorePreviousSession = persistor.restorePreviousSession launchToCustomHomePage = persistor.launchToCustomHomePage customHomePageURL = persistor.customHomePageURL updateHomeButtonState() listenToPinningManagerNotifications() + listenToDataClearingPreferencesNotifications() + checkDataClearingStatus() } @Published var restorePreviousSession: Bool { @@ -129,6 +135,21 @@ final class StartupPreferences: ObservableObject { } } + private func checkDataClearingStatus() { + if dataClearingPreferences.isAutoClearEnabled { + restorePreviousSession = false + } + } + + private func listenToDataClearingPreferencesNotifications() { + dataClearingPreferencesNotificationCancellable = NotificationCenter.default.publisher(for: .autoClearDidChange).sink { [weak self] _ in + guard let self = self else { + return + } + self.checkDataClearingStatus() + } + } + private func urlWithScheme(_ urlString: String) -> String? { guard var urlWithScheme = urlString.url else { return nil diff --git a/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift b/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift index 320d0d2cad..c79ef7a8e2 100644 --- a/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift +++ b/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift @@ -109,7 +109,7 @@ final class VPNPreferencesModel: ObservableObject { switch response { case .OK: - await NetworkProtectionFeatureDisabler().disable(keepAuthToken: true, uninstallSystemExtension: true) + await NetworkProtectionFeatureDisabler().disable(uninstallSystemExtension: true) default: // intentional no-op break diff --git a/DuckDuckGo/Preferences/View/PreferencesAboutView.swift b/DuckDuckGo/Preferences/View/PreferencesAboutView.swift index 63c0601ea8..e4e8900df9 100644 --- a/DuckDuckGo/Preferences/View/PreferencesAboutView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesAboutView.swift @@ -55,9 +55,6 @@ extension Preferences { .multilineTextAlignment(.leading) Text(UserText.versionLabel(version: model.appVersion.versionNumber, build: model.appVersion.buildNumber)) - .onTapGesture(count: 12) { - model.displayNetPInvite() - } .contextMenu(ContextMenu(menuItems: { Button(UserText.copy, action: { model.copy(UserText.versionLabel(version: model.appVersion.versionNumber, build: model.appVersion.buildNumber)) diff --git a/DuckDuckGo/Preferences/View/PreferencesDataClearingView.swift b/DuckDuckGo/Preferences/View/PreferencesDataClearingView.swift index dc3f28607c..1680fb2e69 100644 --- a/DuckDuckGo/Preferences/View/PreferencesDataClearingView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesDataClearingView.swift @@ -28,7 +28,20 @@ extension Preferences { var body: some View { PreferencePane(UserText.dataClearing) { - // SECTION 1: Fireproof Site + // SECTION 1: Automatically Clear Data + PreferencePaneSection(UserText.autoClear) { + + PreferencePaneSubSection { + ToggleMenuItem(UserText.automaticallyClearData, isOn: $model.isAutoClearEnabled) + ToggleMenuItem(UserText.warnBeforeQuit, + isOn: $model.isWarnBeforeClearingEnabled) + .disabled(!model.isAutoClearEnabled) + .padding(.leading, 16) + } + + } + + // SECTION 2: Fireproof Site PreferencePaneSection(UserText.fireproofSites) { PreferencePaneSubSection { diff --git a/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift b/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift index ad54e6e6ac..7d22bc8867 100644 --- a/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift @@ -28,6 +28,7 @@ extension Preferences { @ObservedObject var startupModel: StartupPreferences @ObservedObject var downloadsModel: DownloadsPreferences @ObservedObject var searchModel: SearchPreferences + @ObservedObject var dataClearingModel: DataClearingPreferences @State private var showingCustomHomePageSheet = false var body: some View { @@ -43,9 +44,19 @@ extension Preferences { Text(UserText.reopenAllWindowsFromLastSession).tag(true) .accessibilityIdentifier("PreferencesGeneralView.stateRestorePicker.reopenAllWindowsFromLastSession") }, label: {}) - .pickerStyle(.radioGroup) - .offset(x: PreferencesViews.Const.pickerHorizontalOffset) - .accessibilityIdentifier("PreferencesGeneralView.stateRestorePicker") + .pickerStyle(.radioGroup) + .disabled(dataClearingModel.isAutoClearEnabled) + .offset(x: PreferencesViews.Const.pickerHorizontalOffset) + .accessibilityIdentifier("PreferencesGeneralView.stateRestorePicker") + if dataClearingModel.isAutoClearEnabled { + VStack(alignment: .leading, spacing: 1) { + TextMenuItemCaption(UserText.disableAutoClearToEnableSessionRestore) + TextButton(UserText.showDataClearingSettings) { + startupModel.show(url: .settingsPane(.dataClearing)) + } + } + .padding(.leading, 19) + } } } diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index 4c077395c7..273a5e98ab 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -37,7 +37,7 @@ enum Preferences { return 355 } } - static let paneContentWidth: CGFloat = 524 + static let paneContentWidth: CGFloat = 544 static let panePaddingHorizontal: CGFloat = 40 static let panePaddingVertical: CGFloat = 40 } @@ -87,7 +87,8 @@ enum Preferences { case .general: GeneralView(startupModel: StartupPreferences.shared, downloadsModel: DownloadsPreferences.shared, - searchModel: SearchPreferences.shared) + searchModel: SearchPreferences.shared, + dataClearingModel: DataClearingPreferences.shared) case .sync: SyncView() case .appearance: @@ -108,8 +109,7 @@ enum Preferences { // Opens a new tab Spacer() case .about: - let netPInvitePresenter = NetworkProtectionInvitePresenter() - AboutView(model: AboutModel(netPInvitePresenter: netPInvitePresenter)) + AboutView(model: AboutModel()) } } .frame(maxWidth: Const.paneContentWidth, maxHeight: .infinity, alignment: .topLeading) 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/Extensions/UserText+PasswordManager.swift b/DuckDuckGo/SecureVault/Extensions/UserText+PasswordManager.swift index aefae5c694..e2ba03e0d4 100644 --- a/DuckDuckGo/SecureVault/Extensions/UserText+PasswordManager.swift +++ b/DuckDuckGo/SecureVault/Extensions/UserText+PasswordManager.swift @@ -20,8 +20,9 @@ import Foundation extension UserText { - static let pmSaveCredentialsEditableTitle = NSLocalizedString("pm.save-credentials.editable.title", value: "Save password?", comment: "Title for the editable Save Credentials popover") + static let pmSaveCredentialsEditableTitle = NSLocalizedString("pm.save-credentials.editable.title", value: "Save Password to DuckDuckGo?", comment: "Title for the editable Save Credentials popover") static let pmSaveCredentialsNonEditableTitle = NSLocalizedString("pm.save-credentials.non-editable.title", value: "New Password Saved", comment: "Title for the non-editable Save Credentials popover") + static let pmUpdateCredentialsTitle = NSLocalizedString("pm.update-credentials.title", value: "Update Password?", comment: "Title for the Update Credentials popover") static let pmEmptyStateDefaultTitle = NSLocalizedString("pm.empty.default.title", value: "No passwords or credit cards saved yet", comment: "Label for default empty state title") static let pmEmptyStateDefaultDescription = NSLocalizedString("pm.empty.default.description", @@ -114,7 +115,7 @@ extension UserText { static let pmLockScreenPreferencesLabel = NSLocalizedString("pm.lock-screen.preferences.label", value: "Change in", comment: "Label used for a button that opens preferences") static let pmLockScreenPreferencesLink = NSLocalizedString("pm.lock-screen.preferences.link", value: "Settings", comment: "Label used for a button that opens preferences") - static let pmAutoLockPromptUnlockLogins = NSLocalizedString("pm.lock-screen.prompt.unlock-logins", value: "unlock access to your autofill info", comment: "Label presented when unlocking Autofill") + static let pmAutoLockPromptUnlockLogins = NSLocalizedString("pm.lock-screen.prompt.unlock-logins", value: "unlock your passwords and autofill info for you", comment: "Label presented when unlocking Autofill") static let pmAutoLockPromptExportLogins = NSLocalizedString("pm.lock-screen.prompt.export-logins", value: "export your usernames and passwords", comment: "Label presented when exporting logins") static let pmAutoLockPromptChangeLoginsSettings = NSLocalizedString("pm.lock-screen.prompt.change-settings", value: "change your autofill info access settings", comment: "Label presented when changing Auto-Lock settings") static let pmAutoLockPromptAutofill = NSLocalizedString("pm.lock-screen.prompt.autofill", value: "unlock access to your autofill info", comment: "Label presented when autofilling credit card information") diff --git a/DuckDuckGo/SecureVault/View/PasswordManager.storyboard b/DuckDuckGo/SecureVault/View/PasswordManager.storyboard index 2fd6c58259..b69d374e09 100644 --- a/DuckDuckGo/SecureVault/View/PasswordManager.storyboard +++ b/DuckDuckGo/SecureVault/View/PasswordManager.storyboard @@ -429,14 +429,34 @@ - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -742,18 +762,18 @@ DQ - + - + @@ -782,11 +802,10 @@ DQ + - - @@ -794,6 +813,7 @@ DQ + @@ -1073,6 +1093,7 @@ DQ + diff --git a/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift b/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift index 02a5595240..d9aebeafbe 100644 --- a/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift +++ b/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift @@ -39,6 +39,7 @@ final class SaveCredentialsViewController: NSViewController { return controller } + @IBOutlet var ddgPasswordManagerTitle: NSView! @IBOutlet var titleLabel: NSTextField! @IBOutlet var passwordManagerTitle: NSView! @IBOutlet var passwordManagerAccountLabel: NSTextField! @@ -157,11 +158,11 @@ final class SaveCredentialsViewController: NSViewController { editButton.isHidden = true doneButton.isHidden = true - titleLabel.isHidden = passwordManagerCoordinator.isEnabled + ddgPasswordManagerTitle.isHidden = passwordManagerCoordinator.isEnabled passwordManagerTitle.isHidden = !passwordManagerCoordinator.isEnabled || passwordManagerCoordinator.isLocked passwordManagerAccountLabel.stringValue = UserText.passwordManagementSaveCredentialsAccountLabel(activeVault: passwordManagerCoordinator.activeVaultEmail ?? "") unlockPasswordManagerTitle.isHidden = !passwordManagerCoordinator.isEnabled || !passwordManagerCoordinator.isLocked - titleLabel.stringValue = UserText.pmSaveCredentialsEditableTitle + titleLabel.stringValue = credentials?.account.id == nil ? UserText.pmSaveCredentialsEditableTitle : UserText.pmUpdateCredentialsTitle usernameField.makeMeFirstResponder() } else { notNowSegmentedControl.isHidden = true 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/StateRestoration/StatePersistenceService.swift b/DuckDuckGo/StateRestoration/StatePersistenceService.swift index 6aed78934a..5d65a2c1d0 100644 --- a/DuckDuckGo/StateRestoration/StatePersistenceService.swift +++ b/DuckDuckGo/StateRestoration/StatePersistenceService.swift @@ -65,6 +65,7 @@ final class StatePersistenceService { func removeLastSessionState() { lastSessionStateArchive = nil + fileStore.remove(fileAtURL: URL.persistenceLocation(for: self.fileName)) } @MainActor diff --git a/DuckDuckGo/Statistics/GeneralPixel.swift b/DuckDuckGo/Statistics/GeneralPixel.swift index 708d25fbe4..e2eeb8d192 100644 --- a/DuckDuckGo/Statistics/GeneralPixel.swift +++ b/DuckDuckGo/Statistics/GeneralPixel.swift @@ -727,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" diff --git a/DuckDuckGo/Subscription/DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift b/DuckDuckGo/Subscription/DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift index 7e65e846e5..73e6407c28 100644 --- a/DuckDuckGo/Subscription/DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift +++ b/DuckDuckGo/Subscription/DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift @@ -18,6 +18,7 @@ import Foundation import Subscription +import BrowserServicesKit extension DefaultSubscriptionFeatureAvailability { convenience init() { 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/SyncDebugMenu.swift b/DuckDuckGo/Sync/SyncDebugMenu.swift index da1fe3e0be..c5ff802e84 100644 --- a/DuckDuckGo/Sync/SyncDebugMenu.swift +++ b/DuckDuckGo/Sync/SyncDebugMenu.swift @@ -89,14 +89,34 @@ final class SyncDebugMenu: NSMenu { context.performAndWait { let root = BookmarkUtils.fetchRootFolder(context)! + let favorites = BookmarkUtils.fetchFavoritesFolders(for: .displayNative(.desktop), in: context) + + let nonStub1 = BookmarkEntity.makeBookmark(title: "Non stub", url: "url", parent: root, context: context) + nonStub1.addToFavorites(folders: favorites) + + let stub1 = BookmarkEntity.makeBookmark(title: "Stub", url: "", parent: root, context: context) + stub1.isStub = true + stub1.addToFavorites(folders: favorites) - _ = BookmarkEntity.makeBookmark(title: "Non stub", url: "url", parent: root, context: context) - let stub = BookmarkEntity.makeBookmark(title: "Stub", url: "", parent: root, context: context) - stub.isStub = true let emptyStub = BookmarkEntity.makeBookmark(title: "", url: "", parent: root, context: context) emptyStub.isStub = true emptyStub.title = nil emptyStub.url = nil + emptyStub.addToFavorites(folders: favorites) + + let nonStub2 = BookmarkEntity.makeBookmark(title: "Non stub 2", url: "url", parent: root, context: context) + nonStub2.addToFavorites(folders: favorites) + + let stub2 = BookmarkEntity.makeBookmark(title: "Stub", url: "", parent: root, context: context) + stub2.isStub = true + stub2.addToFavorites(folders: favorites) + + let stub3 = BookmarkEntity.makeBookmark(title: "Stub", url: "", parent: root, context: context) + stub3.isStub = true + stub3.addToFavorites(folders: favorites) + + let nonStub3 = BookmarkEntity.makeBookmark(title: "Non stub 3", url: "url", parent: root, context: context) + nonStub3.addToFavorites(folders: favorites) try? context.save() } diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift index a799ebbb04..031cbbe851 100644 --- a/DuckDuckGo/Tab/Model/Tab.swift +++ b/DuckDuckGo/Tab/Model/Tab.swift @@ -328,15 +328,15 @@ protocol NewWindowPolicyDecisionMaker { } deinit { - cleanUpBeforeClosing(onDeinit: true) + cleanUpBeforeClosing(onDeinit: true, webView: webView, userContentController: userContentController) } func cleanUpBeforeClosing() { - cleanUpBeforeClosing(onDeinit: false) + cleanUpBeforeClosing(onDeinit: false, webView: webView, userContentController: userContentController) } @MainActor(unsafe) - private func cleanUpBeforeClosing(onDeinit: Bool) { + private func cleanUpBeforeClosing(onDeinit: Bool, webView: WebView, userContentController: UserContentController?) { let job = { [webView, userContentController] in webView.stopAllMedia(shouldStopLoading: true) @@ -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/Navigation/RedirectNavigationResponder.swift b/DuckDuckGo/Tab/Navigation/RedirectNavigationResponder.swift index 21c18aafc4..d06cb5f3d3 100644 --- a/DuckDuckGo/Tab/Navigation/RedirectNavigationResponder.swift +++ b/DuckDuckGo/Tab/Navigation/RedirectNavigationResponder.swift @@ -19,6 +19,7 @@ import Navigation import Foundation import Subscription +import BrowserServicesKit struct RedirectNavigationResponder: NavigationResponder { diff --git a/DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift b/DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift index 1aae34f4c6..339aca0a9a 100644 --- a/DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift +++ b/DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift @@ -224,11 +224,12 @@ extension DownloadsTabExtension: NavigationResponder { let task = downloadManager.add(download, fromBurnerWindow: self.isBurner, delegate: self, destination: .auto) var isMainFrameNavigationActionWithNoHistory: Bool { - guard let navigationAction, + // get the first navigation action in the redirect series + guard let navigationAction = navigationAction?.redirectHistory?.first ?? navigationAction, navigationAction.isForMainFrame, navigationAction.isTargetingNewWindow, // webView has no navigation history (downloaded navigationAction has started from an empty state) - (navigationAction.redirectHistory?.first ?? navigationAction).fromHistoryItemIdentity == nil + navigationAction.fromHistoryItemIdentity == nil else { return false } return true } 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 42ac6fcd45..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,59 +248,65 @@ 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) } } @@ -332,41 +353,33 @@ final class TabViewModel { } } + // 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 } } @@ -426,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..700149aad7 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,18 @@ struct VPNMetadata: Encodable { let notificationsAgentIsRunning: Bool } + struct PrivacyProInfo: Encodable { + 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 +119,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 +151,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 +159,8 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { networkInfo: networkInfoMetadata, vpnState: vpnState, vpnSettingsState: vpnSettingsState, - loginItemState: loginItemState + loginItemState: loginItemState, + privacyProInfo: privacyProInfo ) } @@ -153,7 +168,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 +237,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 +249,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 +287,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 +301,15 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { ) } + func collectPrivacyProInfo() async -> VPNMetadata.PrivacyProInfo { + let accountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) + + let hasVPNEntitlement = (try? await accountManager.hasEntitlement(for: .networkProtection).get()) ?? false + + return .init( + hasPrivacyProAccount: accountManager.isUserAuthenticated, + hasVPNEntitlement: hasVPNEntitlement + ) + } + } diff --git a/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift b/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift index e6d085bc91..cddc3b1a9c 100644 --- a/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift +++ b/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift @@ -29,7 +29,7 @@ protocol NetworkProtectionFeatureDisabling { /// - Returns: `true` if the uninstallation was completed. `false` if it was cancelled by the user or an error. /// @discardableResult - func disable(keepAuthToken: Bool, uninstallSystemExtension: Bool) async -> Bool + func disable(uninstallSystemExtension: Bool) async -> Bool func stop() } @@ -68,12 +68,11 @@ final class NetworkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling /// This method disables the VPN and clear all of its state. /// /// - Parameters: - /// - keepAuthToken: If `true`, the auth token will not be removed. /// - includeSystemExtension: Whether this method should uninstall the system extension. /// @MainActor @discardableResult - func disable(keepAuthToken: Bool, uninstallSystemExtension: Bool) async -> Bool { + func disable(uninstallSystemExtension: Bool) async -> Bool { // We can do this optimistically as it has little if any impact. unpinNetworkProtection() @@ -118,10 +117,6 @@ final class NetworkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling try? await Task.sleep(interval: 0.5) disableLoginItems() - if !keepAuthToken { - try? removeAppAuthToken() - } - notifyVPNUninstalled() isDisabling = false return true @@ -151,10 +146,6 @@ final class NetworkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling pinningManager.unpin(.networkProtection) } - private func removeAppAuthToken() throws { - try NetworkProtectionKeychainTokenStore().deleteToken() - } - private func removeVPNConfiguration() async throws { // Remove the agent VPN configuration try await ipcClient.debugCommand(.removeVPNConfiguration) diff --git a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift b/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift index c792a3e788..6956ba22ae 100644 --- a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift +++ b/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift @@ -27,15 +27,12 @@ import PixelKit import Subscription protocol NetworkProtectionFeatureVisibility { - var isEligibleForThankYouMessage: Bool { get } var isInstalled: Bool { get } func canStartVPN() async throws -> Bool func isVPNVisible() -> Bool - func isNetworkProtectionBetaVisible() -> Bool func shouldUninstallAutomatically() -> Bool func disableForAllUsers() async - func disableForWaitlistUsers() @discardableResult func disableIfUserHasNoAccess() async -> Bool @@ -47,16 +44,11 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { private let featureDisabler: NetworkProtectionFeatureDisabling private let featureOverrides: WaitlistBetaOverriding private let networkProtectionFeatureActivation: NetworkProtectionFeatureActivation - private let networkProtectionWaitlist = NetworkProtectionWaitlist() private let privacyConfigurationManager: PrivacyConfigurationManaging private let defaults: UserDefaults let subscriptionAppGroup = Bundle.main.appGroup(bundle: .subs) let accountManager: AccountManager - var waitlistIsOngoing: Bool { - isWaitlistEnabled && isWaitlistBetaActive - } - init(privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager, networkProtectionFeatureActivation: NetworkProtectionFeatureActivation = NetworkProtectionKeychainTokenStore(), featureOverrides: WaitlistBetaOverriding = DefaultWaitlistBetaOverrides(), @@ -72,17 +64,6 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { self.accountManager = AccountManager(subscriptionAppGroup: subscriptionAppGroup) } - /// Calculates whether the VPN is visible. - /// The following criteria are used: - /// - /// 1. If the user has a valid auth token, the feature is visible - /// 2. If no auth token is found, the feature is visible if the waitlist feature flag is enabled - /// - /// Once the waitlist beta has ended, we can trigger a remote change that removes the user's auth token and turn off the waitlist flag, hiding the VPN from the user. - func isNetworkProtectionBetaVisible() -> Bool { - return isEasterEggUser || waitlistIsOngoing - } - var isInstalled: Bool { LoginItem.vpnMenu.status.isInstalled } @@ -94,7 +75,7 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { /// func canStartVPN() async throws -> Bool { guard subscriptionFeatureAvailability.isFeatureAvailable else { - return isNetworkProtectionBetaVisible() + return false } switch await accountManager.hasEntitlement(for: .networkProtection) { @@ -112,7 +93,7 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { /// func isVPNVisible() -> Bool { guard subscriptionFeatureAvailability.isFeatureAvailable else { - return isNetworkProtectionBetaVisible() + return false } return accountManager.isUserAuthenticated @@ -142,111 +123,19 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { defaults.networkProtectionOnboardingStatusPublisher } - /// Easter egg users can be identified by them being internal users and having an auth token (NetP being activated). - /// - private var isEasterEggUser: Bool { - !isWaitlistUser && networkProtectionFeatureActivation.isFeatureActivated - } - - /// Whether it's a user with feature access - private var isEnabledWaitlistUser: Bool { - isWaitlistUser && waitlistIsOngoing - } - - /// Waitlist users are users that have the waitlist enabled and active - /// - private var isWaitlistUser: Bool { - networkProtectionWaitlist.waitlistStorage.isWaitlistUser - } - - /// Waitlist users are users that have the waitlist enabled and active and are invited - /// - private var isInvitedWaitlistUser: Bool { - networkProtectionWaitlist.waitlistStorage.isWaitlistUser && networkProtectionWaitlist.waitlistStorage.isInvited - } - - 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 - } - } - - private var isWaitlistEnabled: Bool { - switch featureOverrides.waitlistEnabled { - case .useRemoteValue: - return privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(NetworkProtectionSubfeature.waitlist) - case .on: - return true - case .off: - return false - } - } - func disableForAllUsers() async { - await featureDisabler.disable(keepAuthToken: true, uninstallSystemExtension: false) - } - - /// Disables the VPN for legacy users, if necessary. - /// - /// This method does not seek to remove tokens or uninstall anything. - /// - private func disableVPNForLegacyUsersIfSubscriptionAvailable() async -> Bool { - guard isEligibleForThankYouMessage && !defaults.vpnLegacyUserAccessDisabledOnce else { - return false - } - - PixelKit.fire(VPNPrivacyProPixel.vpnBetaStoppedWhenPrivacyProEnabled, frequency: .dailyAndCount) - defaults.vpnLegacyUserAccessDisabledOnce = true - await featureDisabler.disable(keepAuthToken: true, uninstallSystemExtension: false) - return true - } - - func disableForWaitlistUsers() { - guard isWaitlistUser else { - return - } - - Task { - await featureDisabler.disable(keepAuthToken: false, uninstallSystemExtension: false) - } + await featureDisabler.disable(uninstallSystemExtension: false) } /// A method meant to be called safely from different places to disable the VPN if the user isn't meant to have access to it. /// @discardableResult func disableIfUserHasNoAccess() async -> Bool { - if shouldUninstallAutomatically() { - await disableForAllUsers() - return true - } - - return await disableVPNForLegacyUsersIfSubscriptionAvailable() - } - - // MARK: - Subscription Start Support - - /// To query whether we're a legacy (waitlist or easter egg) user. - /// - private func isPreSubscriptionUser() -> Bool { - guard let token = try? NetworkProtectionKeychainTokenStore(isSubscriptionEnabled: false).fetchToken() else { + guard shouldUninstallAutomatically() else { return false } - return !token.hasPrefix(Self.subscriptionAuthTokenPrefix) - } - - /// Checks whether the VPN needs to be disabled. - /// - var isEligibleForThankYouMessage: Bool { - isPreSubscriptionUser() && subscriptionFeatureAvailability.isFeatureAvailable + await disableForAllUsers() + return true } } diff --git a/DuckDuckGo/Waitlist/Views/WaitlistRootView.swift b/DuckDuckGo/Waitlist/Views/WaitlistRootView.swift index 776e44acdf..b9a029d4c6 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistRootView.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistRootView.swift @@ -18,31 +18,6 @@ import SwiftUI -struct NetworkProtectionWaitlistRootView: View { - @EnvironmentObject var model: WaitlistViewModel - - var body: some View { - Group { - switch model.viewState { - case .notOnWaitlist, .joiningWaitlist: - JoinWaitlistView(viewData: NetworkProtectionJoinWaitlistViewData()) - case .joinedWaitlist(let state): - JoinedWaitlistView(viewData: NetworkProtectionJoinedWaitlistViewData(), - notificationsAllowed: state == .notificationAllowed) - case .invited: - InvitedToWaitlistView(viewData: NetworkProtectionInvitedToWaitlistViewData()) - case .termsAndConditions: - WaitlistTermsAndConditionsView(viewData: NetworkProtectionWaitlistTermsAndConditionsViewData()) { - NetworkProtectionTermsAndConditionsContentView() - } - case .readyToEnable: - EnableWaitlistFeatureView(viewData: EnableNetworkProtectionViewData()) - } - } - .environmentObject(model) - } -} - #if DBP import SwiftUI diff --git a/DuckDuckGo/Waitlist/Views/WaitlistSteps/EnableWaitlistFeatureView.swift b/DuckDuckGo/Waitlist/Views/WaitlistSteps/EnableWaitlistFeatureView.swift deleted file mode 100644 index 1f4ee5d72c..0000000000 --- a/DuckDuckGo/Waitlist/Views/WaitlistSteps/EnableWaitlistFeatureView.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// EnableWaitlistFeatureView.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 SwiftUI -import SwiftUIExtensions - -protocol EnableWaitlistFeatureViewData { - var headerImageName: String { get } - var title: String { get } - var subtitle: String { get } - var availabilityDisclaimer: String { get } - var buttonConfirmLabel: String { get } -} - -struct EnableWaitlistFeatureView: View { - var viewData: EnableWaitlistFeatureViewData - @EnvironmentObject var model: WaitlistViewModel - - var body: some View { - WaitlistDialogView { - VStack(spacing: 16.0) { - Image(viewData.headerImageName) - - Text(viewData.title) - .font(.system(size: 17, weight: .bold)) - - Text(viewData.subtitle) - .multilineTextAlignment(.center) - .foregroundColor(Color(.blackWhite80)) - - Text(viewData.availabilityDisclaimer) - .multilineTextAlignment(.center) - .font(.system(size: 12)) - .foregroundColor(Color(.blackWhite60)) - } - } buttons: { - Button(viewData.buttonConfirmLabel) { - Task { - await model.perform(action: .closeAndConfirmFeature) - } - } - .buttonStyle(DefaultActionButtonStyle(enabled: true)) - } - .environmentObject(model) - } -} - -struct EnableNetworkProtectionViewData: EnableWaitlistFeatureViewData { - var headerImageName: String = "Network-Protection-256" - var title: String = UserText.networkProtectionWaitlistEnableTitle - var subtitle: String = UserText.networkProtectionWaitlistEnableSubtitle - var availabilityDisclaimer: String = UserText.networkProtectionWaitlistAvailabilityDisclaimer - var buttonConfirmLabel: String = UserText.networkProtectionWaitlistButtonGotIt -} diff --git a/DuckDuckGo/Waitlist/Views/WaitlistSteps/InvitedToWaitlistView.swift b/DuckDuckGo/Waitlist/Views/WaitlistSteps/InvitedToWaitlistView.swift index 52b5ec44cf..ca30a44deb 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistSteps/InvitedToWaitlistView.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistSteps/InvitedToWaitlistView.swift @@ -117,29 +117,6 @@ struct WaitlistEntryViewItemViewData: Identifiable { let subtitle: String } -struct NetworkProtectionInvitedToWaitlistViewData: InvitedToWaitlistViewData { - let headerImageName = "Gift-96" - let title = UserText.networkProtectionWaitlistInvitedTitle - let subtitle = UserText.networkProtectionWaitlistInvitedSubtitle - let buttonDismissLabel = UserText.networkProtectionWaitlistButtonDismiss - let buttonGetStartedLabel = UserText.networkProtectionWaitlistButtonGetStarted - let availabilityDisclaimer = UserText.networkProtectionWaitlistAvailabilityDisclaimer - let entryViewViewDataList: [WaitlistEntryViewItemViewData] = - [ - .init(imageName: "Shield-16", - title: UserText.networkProtectionWaitlistInvitedSection1Title, - subtitle: UserText.networkProtectionWaitlistInvitedSection1Subtitle), - - .init(imageName: "Rocket-16", - title: UserText.networkProtectionWaitlistInvitedSection2Title, - subtitle: UserText.networkProtectionWaitlistInvitedSection2Subtitle), - - .init(imageName: "Card-16", - title: UserText.networkProtectionWaitlistInvitedSection3Title, - subtitle: UserText.networkProtectionWaitlistInvitedSection3Subtitle) - ] -} - #if DBP struct DataBrokerProtectionInvitedToWaitlistViewData: InvitedToWaitlistViewData { diff --git a/DuckDuckGo/Waitlist/Views/WaitlistSteps/JoinWaitlistView.swift b/DuckDuckGo/Waitlist/Views/WaitlistSteps/JoinWaitlistView.swift index b47869f981..6c0e4deb2a 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistSteps/JoinWaitlistView.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistSteps/JoinWaitlistView.swift @@ -73,16 +73,6 @@ struct JoinWaitlistView: View { } } -struct NetworkProtectionJoinWaitlistViewData: JoinWaitlistViewViewData { - let headerImageName = "JoinWaitlistHeader" - let title = UserText.networkProtectionWaitlistJoinTitle - let subtitle1 = UserText.networkProtectionWaitlistJoinSubtitle1 - let subtitle2 = UserText.networkProtectionWaitlistJoinSubtitle2 - let availabilityDisclaimer = UserText.networkProtectionWaitlistAvailabilityDisclaimer - let buttonCloseLabel = UserText.networkProtectionWaitlistButtonClose - let buttonJoinWaitlistLabel = UserText.networkProtectionWaitlistButtonJoinWaitlist -} - #if DBP struct DataBrokerProtectionJoinWaitlistViewData: JoinWaitlistViewViewData { diff --git a/DuckDuckGo/Waitlist/Views/WaitlistSteps/JoinedWaitlistView.swift b/DuckDuckGo/Waitlist/Views/WaitlistSteps/JoinedWaitlistView.swift index bda0183f51..553d3e6562 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistSteps/JoinedWaitlistView.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistSteps/JoinedWaitlistView.swift @@ -85,17 +85,6 @@ struct JoinedWaitlistView: View { } } -struct NetworkProtectionJoinedWaitlistViewData: JoinedWaitlistViewData { - let headerImageName = "JoinedWaitlistHeader" - var title = UserText.networkProtectionWaitlistJoinedTitle - var joinedWithNoNotificationSubtitle1 = UserText.networkProtectionWaitlistJoinedWithNotificationsSubtitle1 - var joinedWithNoNotificationSubtitle2 = UserText.networkProtectionWaitlistJoinedWithNotificationsSubtitle2 - var enableNotificationSubtitle = UserText.networkProtectionWaitlistEnableNotifications - var buttonConfirmLabel = UserText.networkProtectionWaitlistButtonDone - var buttonCancelLabel = UserText.networkProtectionWaitlistButtonNoThanks - var buttonEnableNotificationLabel = UserText.networkProtectionWaitlistButtonEnableNotifications -} - #if DBP struct DataBrokerProtectionJoinedWaitlistViewData: JoinedWaitlistViewData { diff --git a/DuckDuckGo/Waitlist/Views/WaitlistSteps/WaitlistTermsAndConditionsView.swift b/DuckDuckGo/Waitlist/Views/WaitlistSteps/WaitlistTermsAndConditionsView.swift index 0137b172e8..7495ae9e0b 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistSteps/WaitlistTermsAndConditionsView.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistSteps/WaitlistTermsAndConditionsView.swift @@ -82,76 +82,6 @@ private extension Text { } -struct NetworkProtectionTermsAndConditionsContentView: View { - var body: some View { - VStack(alignment: .leading, spacing: 0) { - Text(verbatim: UserText.networkProtectionPrivacyPolicyTitle) - .font(.system(size: 15, weight: .bold)) - .multilineTextAlignment(.leading) - - Group { - Text(verbatim: UserText.networkProtectionPrivacyPolicySection1Title).titleStyle() - - if #available(macOS 12.0, *) { - Text(verbatim: UserText.networkProtectionPrivacyPolicySection1ListMarkdown).bodyStyle() - } else { - Text(verbatim: UserText.networkProtectionPrivacyPolicySection1ListNonMarkdown).bodyStyle() - } - - Text(verbatim: UserText.networkProtectionPrivacyPolicySection2Title).titleStyle() - Text(verbatim: UserText.networkProtectionPrivacyPolicySection2List).bodyStyle() - Text(verbatim: UserText.networkProtectionPrivacyPolicySection3Title).titleStyle() - Text(verbatim: UserText.networkProtectionPrivacyPolicySection3List).bodyStyle() - Text(verbatim: UserText.networkProtectionPrivacyPolicySection4Title).titleStyle() - Text(verbatim: UserText.networkProtectionPrivacyPolicySection4List).bodyStyle() - Text(verbatim: UserText.networkProtectionPrivacyPolicySection5Title).titleStyle() - Text(verbatim: UserText.networkProtectionPrivacyPolicySection5List).bodyStyle() - } - - Text(verbatim: UserText.networkProtectionTermsOfServiceTitle) - .font(.system(size: 15, weight: .bold)) - .multilineTextAlignment(.leading) - .padding(.top, 28) - .padding(.bottom, 14) - - Group { - Text(verbatim: UserText.networkProtectionTermsOfServiceSection1Title).titleStyle(topPadding: 0) - Text(verbatim: UserText.networkProtectionTermsOfServiceSection1List).bodyStyle() - Text(verbatim: UserText.networkProtectionTermsOfServiceSection2Title).titleStyle() - - if #available(macOS 12.0, *) { - Text(verbatim: UserText.networkProtectionTermsOfServiceSection2ListMarkdown).bodyStyle() - } else { - Text(UserText.networkProtectionTermsOfServiceSection2ListNonMarkdown).bodyStyle() - } - - Text(verbatim: UserText.networkProtectionTermsOfServiceSection3Title).titleStyle() - Text(verbatim: UserText.networkProtectionTermsOfServiceSection3List).bodyStyle() - Text(verbatim: UserText.networkProtectionTermsOfServiceSection4Title).titleStyle() - Text(verbatim: UserText.networkProtectionTermsOfServiceSection4List).bodyStyle() - Text(verbatim: UserText.networkProtectionTermsOfServiceSection5Title).titleStyle() - Text(verbatim: UserText.networkProtectionTermsOfServiceSection5List).bodyStyle() - } - - Group { - Text(verbatim: UserText.networkProtectionTermsOfServiceSection6Title).titleStyle() - Text(verbatim: UserText.networkProtectionTermsOfServiceSection6List).bodyStyle() - Text(verbatim: UserText.networkProtectionTermsOfServiceSection7Title).titleStyle() - Text(verbatim: UserText.networkProtectionTermsOfServiceSection7List).bodyStyle() - Text(verbatim: UserText.networkProtectionTermsOfServiceSection8Title).titleStyle() - Text(verbatim: UserText.networkProtectionTermsOfServiceSection8List).bodyStyle() - } - } - .padding(.all, 20) - } -} - -struct NetworkProtectionWaitlistTermsAndConditionsViewData: WaitlistTermsAndConditionsViewData { - let title = "VPN Beta\nService Terms and Privacy Policy" - let buttonCancelLabel = UserText.networkProtectionWaitlistButtonCancel - let buttonAgreeAndContinueLabel = UserText.networkProtectionWaitlistButtonAgreeAndContinue -} - #if DBP struct DataBrokerProtectionTermsAndConditionsContentView: View { diff --git a/DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift b/DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift index 4c895fcf36..11281dcaba 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift @@ -18,38 +18,33 @@ import AppKit import Foundation -import Subscription +import BrowserServicesKit import PixelKit final class WaitlistThankYouPromptPresenter { private enum Constants { static let didShowThankYouPromptKey = "duckduckgo.macos.browser.did-show-thank-you-prompt" - static let didDismissVPNCardKey = "duckduckgo.macos.browser.did-dismiss-vpn-card" static let didDismissPIRCardKey = "duckduckgo.macos.browser.did-dismiss-pir-card" } - private let isVPNBetaTester: () -> Bool private let isPIRBetaTester: () -> Bool private let userDefaults: UserDefaults convenience init() { - self.init(isVPNBetaTester: { - return DefaultNetworkProtectionVisibility().isEligibleForThankYouMessage - }, isPIRBetaTester: { + self.init(isPIRBetaTester: { return DefaultDataBrokerProtectionFeatureVisibility().isEligibleForThankYouMessage() }) } - init(isVPNBetaTester: @escaping () -> Bool, isPIRBetaTester: @escaping () -> Bool, userDefaults: UserDefaults = .standard) { - self.isVPNBetaTester = isVPNBetaTester + init(isPIRBetaTester: @escaping () -> Bool, userDefaults: UserDefaults = .standard) { self.isPIRBetaTester = isPIRBetaTester self.userDefaults = userDefaults } // MARK: - Presentation - // Presents a Thank You prompt to testers of the VPN or PIR. + // Presents a Thank You prompt to testers of PIR. // If the user tested both, the PIR prompt will be displayed. @MainActor func presentThankYouPromptIfNecessary(in window: NSWindow) { @@ -67,19 +62,6 @@ final class WaitlistThankYouPromptPresenter { saveDidShowPromptCheck() PixelKit.fire(PrivacyProPixel.privacyProBetaUserThankYouDBP, frequency: .dailyAndCount) presentPIRThankYouPrompt(in: window) - } else if isVPNBetaTester() { - saveDidShowPromptCheck() - PixelKit.fire(PrivacyProPixel.privacyProBetaUserThankYouVPN, frequency: .dailyAndCount) - presentVPNThankYouPrompt(in: window) - } - } - - @MainActor - func presentVPNThankYouPrompt(in window: NSWindow) { - let thankYouModalView = WaitlistBetaThankYouDialogViewController(copy: .vpn) - let thankYouWindowController = thankYouModalView.wrappedInWindowController() - if let thankYouWindow = thankYouWindowController.window { - window.beginSheet(thankYouWindow) } } @@ -94,14 +76,6 @@ final class WaitlistThankYouPromptPresenter { // MARK: - Eligibility - var canShowVPNCard: Bool { - guard !self.userDefaults.bool(forKey: Constants.didDismissVPNCardKey) else { - return false - } - - return isVPNBetaTester() - } - var canShowPIRCard: Bool { guard !self.userDefaults.bool(forKey: Constants.didDismissPIRCardKey) else { return false @@ -116,10 +90,6 @@ final class WaitlistThankYouPromptPresenter { // MARK: - Dismissal - func didDismissVPNThankYouCard() { - self.userDefaults.setValue(true, forKey: Constants.didDismissVPNCardKey) - } - func didDismissPIRThankYouCard() { self.userDefaults.setValue(true, forKey: Constants.didDismissPIRCardKey) } @@ -132,7 +102,6 @@ final class WaitlistThankYouPromptPresenter { func resetPromptCheck() { self.userDefaults.removeObject(forKey: Constants.didShowThankYouPromptKey) - self.userDefaults.removeObject(forKey: Constants.didDismissVPNCardKey) self.userDefaults.removeObject(forKey: Constants.didDismissPIRCardKey) } diff --git a/DuckDuckGo/Waitlist/Views/WaitlistViewControllerPresenter.swift b/DuckDuckGo/Waitlist/Views/WaitlistViewControllerPresenter.swift index 85c050b790..55ee0e6584 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistViewControllerPresenter.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistViewControllerPresenter.swift @@ -18,7 +18,7 @@ import Foundation import UserNotifications -import Subscription +import BrowserServicesKit protocol WaitlistViewControllerPresenter { static func show(completion: (() -> Void)?) @@ -30,45 +30,6 @@ extension WaitlistViewControllerPresenter { } } -struct NetworkProtectionWaitlistViewControllerPresenter: WaitlistViewControllerPresenter { - - @MainActor - static func show(completion: (() -> Void)? = nil) { - guard let windowController = WindowControllersManager.shared.lastKeyMainWindowController, - windowController.window?.isKeyWindow == true else { - return - } - - // This is a hack to get around an issue with the waitlist notification screen showing the wrong state while it animates in, and then - // jumping to the correct state as soon as the animation is complete. This works around that problem by providing the correct state up front, - // preventing any state changing from occurring. - UNUserNotificationCenter.current().getNotificationSettings { settings in - let status = settings.authorizationStatus - let state = WaitlistViewModel.NotificationPermissionState.from(status) - - DispatchQueue.main.async { - let viewModel = WaitlistViewModel(waitlist: NetworkProtectionWaitlist(), - notificationPermissionState: state, - showNotificationSuccessState: true, - termsAndConditionActionHandler: NetworkProtectionWaitlistTermsAndConditionsActionHandler(), - featureSetupHandler: NetworkProtectionWaitlistFeatureSetupHandler()) - - let viewController = WaitlistModalViewController(viewModel: viewModel, contentView: NetworkProtectionWaitlistRootView()) - windowController.mainViewController.beginSheet(viewController) { _ in - // If the user dismissed the waitlist flow without signing up, hide the button. - let waitlist = NetworkProtectionWaitlist() - if !waitlist.waitlistStorage.isOnWaitlist { - waitlist.waitlistSignUpPromptDismissed = true - NotificationCenter.default.post(name: .networkProtectionWaitlistAccessChanged, object: nil) - } - - completion?() - } - } - } - } -} - #if DBP struct DataBrokerProtectionWaitlistViewControllerPresenter: WaitlistViewControllerPresenter { diff --git a/DuckDuckGo/Waitlist/Waitlist.swift b/DuckDuckGo/Waitlist/Waitlist.swift index 56c9f8cdd7..b8bc8e1ba7 100644 --- a/DuckDuckGo/Waitlist/Waitlist.swift +++ b/DuckDuckGo/Waitlist/Waitlist.swift @@ -154,88 +154,6 @@ extension ProductWaitlistRequest { } } -// MARK: - VPN Waitlist - -struct NetworkProtectionWaitlist: Waitlist { - - static let identifier: String = "networkprotection" - static let apiProductName: String = "networkprotection_macos" - static let keychainAppGroup: String = Bundle.main.appGroup(bundle: .netP) - - static let notificationIdentifier = "com.duckduckgo.macos.browser.network-protection.invite-code-available" - static let inviteAvailableNotificationTitle = UserText.networkProtectionWaitlistNotificationTitle - static let inviteAvailableNotificationBody = UserText.networkProtectionWaitlistNotificationText - - let waitlistStorage: WaitlistStorage - let waitlistRequest: WaitlistRequest - private let networkProtectionCodeRedemption: NetworkProtectionCodeRedeeming - - @UserDefaultsWrapper(key: .networkProtectionWaitlistSignUpPromptDismissed, defaultValue: false) - var waitlistSignUpPromptDismissed: Bool - - var shouldShowWaitlistViewController: Bool { - return isOnWaitlist || readyToAcceptTermsAndConditions - } - - var isOnWaitlist: Bool { - return waitlistStorage.isOnWaitlist - } - - var isInvited: Bool { - return waitlistStorage.isInvited - } - - var readyToAcceptTermsAndConditions: Bool { - let accepted = UserDefaults().bool(forKey: UserDefaultsWrapper.Key.networkProtectionTermsAndConditionsAccepted.rawValue) - return waitlistStorage.isInvited && !accepted - } - - init() { - self.init( - store: WaitlistKeychainStore(waitlistIdentifier: Self.identifier, keychainAppGroup: Self.keychainAppGroup), - request: ProductWaitlistRequest(productName: Self.apiProductName), - networkProtectionCodeRedemption: NetworkProtectionCodeRedemptionCoordinator() - ) - } - - init(store: WaitlistStorage, request: WaitlistRequest, networkProtectionCodeRedemption: NetworkProtectionCodeRedeeming) { - self.waitlistStorage = store - self.waitlistRequest = request - self.networkProtectionCodeRedemption = networkProtectionCodeRedemption - } - - func fetchNetworkProtectionInviteCodeIfAvailable(completion: @escaping (WaitlistInviteCodeFetchError?) -> Void) { - // Never fetch the invite code if the Privacy Pro flag is enabled: - if DefaultSubscriptionFeatureAvailability().isFeatureAvailable { - completion(nil) - return - } - - self.fetchInviteCodeIfAvailable { error in - if let error { - // Do nothing if the app fails to fetch, as the waitlist is being phased out - completion(error) - } else if let inviteCode = waitlistStorage.getWaitlistInviteCode() { - Task { @MainActor in - do { - try await networkProtectionCodeRedemption.redeem(inviteCode) - NotificationCenter.default.post(name: .networkProtectionWaitlistAccessChanged, object: nil) - sendInviteCodeAvailableNotification(completion: nil) - completion(nil) - } catch { - assertionFailure("Failed to redeem invite code") - completion(.failure(error)) - } - } - } else { - completion(nil) - assertionFailure("Didn't get error or invite code") - } - } - } - -} - #if DBP // MARK: - DataBroker Protection Waitlist diff --git a/DuckDuckGo/Waitlist/WaitlistTermsAndConditionsActionHandler.swift b/DuckDuckGo/Waitlist/WaitlistTermsAndConditionsActionHandler.swift index ccde7fd9a7..3f6f456ea0 100644 --- a/DuckDuckGo/Waitlist/WaitlistTermsAndConditionsActionHandler.swift +++ b/DuckDuckGo/Waitlist/WaitlistTermsAndConditionsActionHandler.swift @@ -26,21 +26,6 @@ protocol WaitlistTermsAndConditionsActionHandler { mutating func didAccept() } -struct NetworkProtectionWaitlistTermsAndConditionsActionHandler: WaitlistTermsAndConditionsActionHandler { - @UserDefaultsWrapper(key: .networkProtectionTermsAndConditionsAccepted, defaultValue: false) - var acceptedTermsAndConditions: Bool - - func didShow() { - // Intentional no-op - } - - mutating func didAccept() { - acceptedTermsAndConditions = true - // Remove delivered NetP notifications in case the user didn't click them. - UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [NetworkProtectionWaitlist.notificationIdentifier]) - } -} - #if DBP struct DataBrokerProtectionWaitlistTermsAndConditionsActionHandler: WaitlistTermsAndConditionsActionHandler { 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/IPCServiceManager.swift b/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift index 33a1fc1d77..8c39f28e07 100644 --- a/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift +++ b/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift @@ -85,9 +85,10 @@ extension IPCServiceManager: IPCServerInterface { } func startManualScan(showWebView: Bool, + startTime: Date, completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { pixelHandler.fire(.ipcServerScanAllBrokersReceivedByAgent) - scheduler.startManualScan(showWebView: showWebView) { errors in + scheduler.startManualScan(showWebView: showWebView, startTime: startTime) { errors in if let error = errors?.oneTimeError { switch error { case DataBrokerProtectionSchedulerError.operationsInterrupted: diff --git a/DuckDuckGoNotifications/DuckDuckGoNotificationsAppDelegate.swift b/DuckDuckGoNotifications/DuckDuckGoNotificationsAppDelegate.swift index 41677dc8f1..75b3471644 100644 --- a/DuckDuckGoNotifications/DuckDuckGoNotificationsAppDelegate.swift +++ b/DuckDuckGoNotifications/DuckDuckGoNotificationsAppDelegate.swift @@ -19,6 +19,8 @@ import Cocoa import Combine import Common +import Networking +import PixelKit import NetworkExtension import NetworkProtection @@ -69,6 +71,38 @@ final class DuckDuckGoNotificationsAppDelegate: NSObject, NSApplicationDelegate func applicationDidFinishLaunching(_ aNotification: Notification) { os_log("Login item finished launching", log: .networkProtectionLoginItemLog, type: .info) + let dryRun: Bool + +#if DEBUG + dryRun = true +#else + dryRun = false +#endif + + let pixelSource: String + +#if NETP_SYSTEM_EXTENSION + pixelSource = "vpnNotificationAgent" +#else + pixelSource = "vpnNotificationAgentAppStore" // Should never get used, but just in case +#endif + + PixelKit.setUp(dryRun: dryRun, + appVersion: AppVersion.shared.versionNumber, + source: pixelSource, + defaultHeaders: [:], + defaults: .netP) { (pixelName: String, headers: [String: String], parameters: [String: String], _, _, onComplete: @escaping PixelKit.CompletionBlock) in + + let url = URL.pixelUrl(forPixelNamed: pixelName) + let apiHeaders = APIRequest.Headers(additionalHeaders: headers) // workaround - Pixel class should really handle APIRequest.Headers by itself + let configuration = APIRequest.Configuration(url: url, method: .get, queryParameters: parameters, headers: apiHeaders) + let request = APIRequest(configuration: configuration) + + request.fetch { _, error in + onComplete(error == nil, error) + } + } + startObservingVPNStatusChanges() os_log("Login item listening") } @@ -157,3 +191,17 @@ final class DuckDuckGoNotificationsAppDelegate: NSObject, NSApplicationDelegate } } + +extension NSApplication { + + enum RunType: Int, CustomStringConvertible { + case normal + var description: String { + switch self { + case .normal: return "normal" + } + } + } + static var runType: RunType { .normal } + +} 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/Common/TestsURLExtension.swift b/IntegrationTests/Common/TestsURLExtension.swift index 95da2cd8ca..b1bd1f7a8f 100644 --- a/IntegrationTests/Common/TestsURLExtension.swift +++ b/IntegrationTests/Common/TestsURLExtension.swift @@ -37,7 +37,7 @@ extension URL { let url = URL.testsServer .appendingPathComponent("filename") // "http://localhost:8085/filename" .appendingTestParameters(status: 301, - reason: "Moved" + reason: "Moved", data: Data(), headers: ["Location": "/redirect-location.html"]) Tab.setUrl(url) diff --git a/IntegrationTests/Downloads/DownloadsIntegrationTests.swift b/IntegrationTests/Downloads/DownloadsIntegrationTests.swift index fbe3f5f557..f74e6e42e5 100644 --- a/IntegrationTests/Downloads/DownloadsIntegrationTests.swift +++ b/IntegrationTests/Downloads/DownloadsIntegrationTests.swift @@ -185,6 +185,133 @@ class DownloadsIntegrationTests: XCTestCase { } } + @MainActor + func testWhenDownloadIsStartedInNewTab_tabIsClosed() async throws { + let preferences = DownloadsPreferences.shared + preferences.alwaysRequestDownloadLocation = false + preferences.selectedDownloadLocation = FileManager.default.temporaryDirectory + let dirURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + try FileManager.default.createDirectory(at: dirURL, withIntermediateDirectories: true) + + let downloadUrl = URL.testsServer + .appendingPathComponent("fname.dat") + .appendingTestParameters(data: data.html, + headers: ["Content-Disposition": "attachment; filename=\"fname.dat\"", + "Content-Type": "text/html"]) + + let pageUrl = URL.testsServer + .appendingTestParameters(data: """ + + + + + Clickable Body + + +

Click anywhere on the page to open the link

+ + + """.utf8data) + let tab = tabViewModel.tab + _=await tab.setUrl(pageUrl, source: .link)?.result + + NSApp.activate(ignoringOtherApps: true) + let downloadTaskFuture = FileDownloadManager.shared.downloadsPublisher.timeout(5).first().promise() + + let e1 = expectation(description: "new tab opened") + var e2: XCTestExpectation! + let c = tabCollectionViewModel.$selectedTabViewModel.dropFirst() + .receive(on: DispatchQueue.main) + .sink { [unowned self] tabViewModel in + guard let tabViewModel else { return } + print("tabViewModel", tabViewModel.tab, tab) + if tabViewModel.tab !== tab { + e1.fulfill() + e2 = expectation(description: "new tab closed") + } else { + e2.fulfill() + } + } + + // click to open a new (download) tab and instantly deactivate it + click(tab.webView) + + // download should start in the background tab + _=try await downloadTaskFuture.get() + + // expect for the download tab to close + await fulfillment(of: [e1, e2], timeout: 10) + withExtendedLifetime(c, {}) + } + + @MainActor + func testWhenDownloadIsStartedInNewTabAfterRedirect_tabIsClosed() async throws { + let preferences = DownloadsPreferences.shared + preferences.alwaysRequestDownloadLocation = false + preferences.selectedDownloadLocation = FileManager.default.temporaryDirectory + let dirURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + try FileManager.default.createDirectory(at: dirURL, withIntermediateDirectories: true) + + let downloadUrl = URL.testsServer + .appendingPathComponent("fname.dat") + .appendingTestParameters(data: data.html, + headers: ["Content-Disposition": "attachment; filename=\"fname.dat\"", + "Content-Type": "text/html"]) + + let redirectUrl = URL.testsServer + .appendingTestParameters(data: """ + + + + """.utf8data) + + let pageUrl = URL.testsServer + .appendingTestParameters(data: """ + + + + + Clickable Body + + +

Click anywhere on the page to open the link

+ + + """.utf8data) + let tab = tabViewModel.tab + _=await tab.setUrl(pageUrl, source: .link)?.result + + NSApp.activate(ignoringOtherApps: true) + let downloadTaskFuture = FileDownloadManager.shared.downloadsPublisher.timeout(5).first().promise() + + let e1 = expectation(description: "new tab opened") + var e2: XCTestExpectation! + let c = tabCollectionViewModel.$selectedTabViewModel.dropFirst() + .receive(on: DispatchQueue.main) + .sink { [unowned self] tabViewModel in + guard let tabViewModel else { return } + print("tabViewModel", tabViewModel.tab, tab) + if tabViewModel.tab !== tab { + e1.fulfill() + e2 = expectation(description: "new tab closed") + } else { + e2.fulfill() + } + } + + // click to open a new (download) tab and instantly deactivate it + click(tab.webView) + + // download should start in the background tab + _=try await downloadTaskFuture.get() + + // expect for the download tab to close + await fulfillment(of: [e1, e2], timeout: 10) + withExtendedLifetime(c, {}) + } + @MainActor func testWhenSaveDialogOpenInBackgroundTabAndTabIsClosed_downloadIsCancelled() async throws { let persistor = DownloadsPreferencesUserDefaultsPersistor() 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.. [BrokerProfileQueryData] func prepareBrokerProfileQueryDataCache() throws func hasMatches() throws -> Bool -} - -extension DataBrokerProtectionDataManaging { - func fetchBrokerProfileQueryData() throws -> [BrokerProfileQueryData] { - try fetchBrokerProfileQueryData(ignoresCache: false) - } + func profileQueriesCount() throws -> Int } public protocol DataBrokerProtectionDataManagerDelegate: AnyObject { @@ -74,6 +69,18 @@ public class DataBrokerProtectionDataManager: DataBrokerProtectionDataManaging { return cache.profile } + return try fetchProfileFromDB() + } + + public func profileQueriesCount() throws -> Int { + guard let profile = try fetchProfileFromDB() else { + throw DataBrokerProtectionError.dataNotInDatabase + } + + return profile.profileQueries.count + } + + private func fetchProfileFromDB() throws -> DataBrokerProtectionProfile? { if let profile = try database.fetchProfile() { cache.profile = profile return profile @@ -287,7 +294,7 @@ extension InMemoryDataCache: DBPUICommunicationDelegate { } func startScanAndOptOut() -> Bool { - return scanDelegate?.startScan() ?? false + return scanDelegate?.startScan(startDate: Date()) ?? false } func getInitialScanState() async -> DBPUIInitialScanState { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDatabase.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDatabase.swift index bab6bce44f..c1cbafd4a8 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDatabase.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDatabase.swift @@ -323,7 +323,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func fetchScanHistoryEvents(brokerId: Int64, profileQueryId: Int64) throws -> [HistoryEvent] { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: nil) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) 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(reporter: nil) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) guard let optOut = try vault.fetchOptOut(brokerId: brokerId, profileQueryId: profileQueryId, extractedProfileId: extractedProfileId) else { return [HistoryEvent]() } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift index 9b6a7e00ac..6b5894d5e1 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift @@ -145,6 +145,9 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject { private let runnerProvider: OperationRunnerProvider private let privacyConfigManager: PrivacyConfigurationManaging + private let fakePixelHandler: EventMapping = EventMapping { event, _, _, _ in + print(event) + } private let contentScopeProperties: ContentScopeProperties private let csvColumns = ["name_input", "age_input", "city_input", "state_input", "name_scraped", "age_scraped", "address_scraped", "relatives_scraped", "url", "broker name", "screenshot_id", "error", "matched_fields", "result_match", "expected_match"] @@ -347,7 +350,7 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject { Task { do { - let extractedProfiles = try await runner.scan(query, stageCalculator: FakeStageDurationCalculator(), showWebView: true) { true } + let extractedProfiles = try await runner.scan(query, stageCalculator: FakeStageDurationCalculator(), pixelHandler: fakePixelHandler, showWebView: true) { true } DispatchQueue.main.async { for extractedProfile in extractedProfiles { @@ -383,7 +386,7 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject { ) Task { do { - try await runner.optOut(profileQuery: brokerProfileQueryData, extractedProfile: scanResult.extractedProfile, stageCalculator: FakeStageDurationCalculator(), showWebView: true) { + try await runner.optOut(profileQuery: brokerProfileQueryData, extractedProfile: scanResult.extractedProfile, stageCalculator: FakeStageDurationCalculator(), pixelHandler: fakePixelHandler, showWebView: true) { true } @@ -473,6 +476,7 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject { final class FakeStageDurationCalculator: StageDurationCalculator { var attemptId: UUID = UUID() + var isManualScan: Bool = false func durationSinceLastStage() -> Double { 0.0 diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanOperation.swift index fb632105a3..3d4e4b779c 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanOperation.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanOperation.swift @@ -68,6 +68,8 @@ final class DebugScanOperation: DataBrokerOperation { var scanURL: String? let clickAwaitTime: TimeInterval let cookieHandler: CookieHandler + let pixelHandler: EventMapping + var postLoadingSiteStartTime: Date? private let fileManager = FileManager.default private let debugScanContentPath: String? @@ -96,6 +98,9 @@ final class DebugScanOperation: DataBrokerOperation { } self.cookieHandler = EmptyCookieHandler() stageCalculator = FakeStageDurationCalculator() + pixelHandler = EventMapping(mapping: { _, _, _, _ in + // We do not need the pixel handler for the debug + }) } func run(inputValue: Void, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift index f7b809ac9f..1f1d2451b2 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift @@ -137,6 +137,7 @@ extension DataBrokerProtectionIPCClient: IPCServerInterface { } public func startManualScan(showWebView: Bool, + startTime: Date, completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { self.pixelHandler.fire(.ipcServerScanAllBrokersCalledByApp) @@ -155,7 +156,7 @@ extension DataBrokerProtectionIPCClient: IPCServerInterface { } xpc.execute(call: { server in - server.startManualScan(showWebView: showWebView) { errors in + server.startManualScan(showWebView: showWebView, startTime: startTime) { errors in if let error = errors?.oneTimeError { let nsError = error as NSError let interruptedError = DataBrokerProtectionSchedulerError.operationsInterrupted as NSError diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCScheduler.swift index 0763404514..ea73c3e1a0 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCScheduler.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCScheduler.swift @@ -52,9 +52,10 @@ public final class DataBrokerProtectionIPCScheduler: DataBrokerProtectionSchedul } public func startManualScan(showWebView: Bool, + startTime: Date, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { let completion = completion ?? { _ in } - ipcClient.startManualScan(showWebView: showWebView, completion: completion) + ipcClient.startManualScan(showWebView: showWebView, startTime: startTime, 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 dce168b8fe..7f4ae3e840 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift @@ -95,6 +95,7 @@ public protocol IPCServerInterface: AnyObject { func optOutAllBrokers(showWebView: Bool, completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) func startManualScan(showWebView: Bool, + startTime: Date, completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) func runQueuedOperations(showWebView: Bool, completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) @@ -136,6 +137,7 @@ protocol XPCServerInterface { func optOutAllBrokers(showWebView: Bool, completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) func startManualScan(showWebView: Bool, + startTime: Date, completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) func runQueuedOperations(showWebView: Bool, completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) @@ -214,8 +216,9 @@ extension DataBrokerProtectionIPCServer: XPCServerInterface { } func startManualScan(showWebView: Bool, + startTime: Date, completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { - serverDelegate?.startManualScan(showWebView: showWebView, completion: completion) + serverDelegate?.startManualScan(showWebView: showWebView, startTime: startTime, 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 df3cf6fd82..a43f6d9ae0 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUIViewModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUIViewModel.swift @@ -23,7 +23,7 @@ import BrowserServicesKit import Common protocol DBPUIScanOps: AnyObject { - func startScan() -> Bool + func startScan(startDate: Date) -> Bool func updateCacheWithCurrentScans() async func getBackgroundAgentMetadata() async -> DBPBackgroundAgentMetadata? } @@ -74,8 +74,8 @@ final class DBPUIViewModel { } extension DBPUIViewModel: DBPUIScanOps { - func startScan() -> Bool { - scheduler.startManualScan() + func startScan(startDate: Date) -> Bool { + scheduler.startManualScan(startTime: startDate) return true } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift index fb88d48e61..893ed9e350 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift @@ -33,6 +33,7 @@ protocol DataBrokerOperation: CCFCommunicationDelegate { var captchaService: CaptchaServiceProtocol { get } var cookieHandler: CookieHandler { get } var stageCalculator: StageDurationCalculator { get } + var pixelHandler: EventMapping { get } var webViewHandler: WebViewHandler? { get set } var actionsHandler: ActionsHandler? { get } @@ -41,6 +42,7 @@ protocol DataBrokerOperation: CCFCommunicationDelegate { var shouldRunNextStep: () -> Bool { get } var retriesCountOnError: Int { get set } var clickAwaitTime: TimeInterval { get } + var postLoadingSiteStartTime: Date? { get set } func run(inputValue: InputValue, webViewHandler: WebViewHandler?, @@ -151,11 +153,13 @@ extension DataBrokerOperation { } func complete(_ value: ReturnValue) { + self.firePostLoadingDurationPixel(hasError: false) self.continuation?.resume(returning: value) self.continuation = nil } func failed(with error: Error) { + self.firePostLoadingDurationPixel(hasError: true) self.continuation?.resume(throwing: error) self.continuation = nil } @@ -175,6 +179,8 @@ extension DataBrokerOperation { // MARK: - CSSCommunicationDelegate func loadURL(url: URL) async { + let webSiteStartLoadingTime = Date() + do { // https://app.asana.com/0/1204167627774280/1206912494469284/f if query.dataBroker.url == "spokeo.com" { @@ -183,12 +189,31 @@ extension DataBrokerOperation { } } try await webViewHandler?.load(url: url) + fireSiteLoadingPixel(startTime: webSiteStartLoadingTime, hasError: false) + postLoadingSiteStartTime = Date() await executeNextStep() } catch { + fireSiteLoadingPixel(startTime: webSiteStartLoadingTime, hasError: true) await onError(error: error) } } + private func fireSiteLoadingPixel(startTime: Date, hasError: Bool) { + if stageCalculator.isManualScan { + let dataBrokerURL = self.query.dataBroker.url + let durationInMs = (Date().timeIntervalSince(startTime) * 1000).rounded(.towardZero) + pixelHandler.fire(.initialScanSiteLoadDuration(duration: durationInMs, hasError: hasError, brokerURL: dataBrokerURL)) + } + } + + func firePostLoadingDurationPixel(hasError: Bool) { + if stageCalculator.isManualScan, let postLoadingSiteStartTime = self.postLoadingSiteStartTime { + let dataBrokerURL = self.query.dataBroker.url + let durationInMs = (Date().timeIntervalSince(postLoadingSiteStartTime) * 1000).rounded(.towardZero) + pixelHandler.fire(.initialScanPostLoadingDuration(duration: durationInMs, hasError: hasError, brokerURL: dataBrokerURL)) + } + } + func success(actionId: String, actionType: ActionType) async { switch actionType { case .click: diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationRunner.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationRunner.swift index 15bfb11e9f..52a64c3fac 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationRunner.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationRunner.swift @@ -24,12 +24,14 @@ protocol WebOperationRunner { func scan(_ profileQuery: BrokerProfileQueryData, stageCalculator: StageDurationCalculator, + pixelHandler: EventMapping, showWebView: Bool, shouldRunNextStep: @escaping () -> Bool) async throws -> [ExtractedProfile] func optOut(profileQuery: BrokerProfileQueryData, extractedProfile: ExtractedProfile, stageCalculator: StageDurationCalculator, + pixelHandler: EventMapping, showWebView: Bool, shouldRunNextStep: @escaping () -> Bool) async throws } @@ -38,10 +40,12 @@ extension WebOperationRunner { func scan(_ profileQuery: BrokerProfileQueryData, stageCalculator: StageDurationCalculator, + pixelHandler: EventMapping, shouldRunNextStep: @escaping () -> Bool) async throws -> [ExtractedProfile] { try await scan(profileQuery, stageCalculator: stageCalculator, + pixelHandler: pixelHandler, showWebView: false, shouldRunNextStep: shouldRunNextStep) } @@ -49,11 +53,13 @@ extension WebOperationRunner { func optOut(profileQuery: BrokerProfileQueryData, extractedProfile: ExtractedProfile, stageCalculator: StageDurationCalculator, + pixelHandler: EventMapping, shouldRunNextStep: @escaping () -> Bool) async throws { try await optOut(profileQuery: profileQuery, extractedProfile: extractedProfile, stageCalculator: stageCalculator, + pixelHandler: pixelHandler, showWebView: false, shouldRunNextStep: shouldRunNextStep) } @@ -78,6 +84,7 @@ final class DataBrokerOperationRunner: WebOperationRunner { func scan(_ profileQuery: BrokerProfileQueryData, stageCalculator: StageDurationCalculator, + pixelHandler: EventMapping, showWebView: Bool, shouldRunNextStep: @escaping () -> Bool) async throws -> [ExtractedProfile] { let scan = ScanOperation( @@ -87,6 +94,7 @@ final class DataBrokerOperationRunner: WebOperationRunner { emailService: emailService, captchaService: captchaService, stageDurationCalculator: stageCalculator, + pixelHandler: pixelHandler, shouldRunNextStep: shouldRunNextStep ) return try await scan.run(inputValue: (), showWebView: showWebView) @@ -95,6 +103,7 @@ final class DataBrokerOperationRunner: WebOperationRunner { func optOut(profileQuery: BrokerProfileQueryData, extractedProfile: ExtractedProfile, stageCalculator: StageDurationCalculator, + pixelHandler: EventMapping, showWebView: Bool, shouldRunNextStep: @escaping () -> Bool) async throws { let optOut = OptOutOperation( @@ -104,6 +113,7 @@ final class DataBrokerOperationRunner: WebOperationRunner { emailService: emailService, captchaService: captchaService, stageCalculator: stageCalculator, + pixelHandler: pixelHandler, shouldRunNextStep: shouldRunNextStep ) try await optOut.run(inputValue: extractedProfile, showWebView: showWebView) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift index 78b76af560..cbe22fe8fe 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift @@ -181,6 +181,7 @@ final class DataBrokerOperationsCollection: Operation { runner: runner, pixelHandler: pixelHandler, showWebView: showWebView, + isManualScan: operationType == .scan, userNotificationService: userNotificationService, shouldRunNextStep: { [weak self] in guard let self = self else { return false } @@ -192,8 +193,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 +202,8 @@ final class DataBrokerOperationsCollection: Operation { withDataBrokerName: brokerProfileQueriesData.first?.dataBroker.name) } } + + finish() } private func finish() { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift index f1e9a7b377..f7eeb24d2b 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift @@ -35,6 +35,7 @@ protocol OperationsManager { runner: WebOperationRunner, pixelHandler: EventMapping, showWebView: Bool, + isManualScan: Bool, userNotificationService: DataBrokerProtectionUserNotificationService, shouldRunNextStep: @escaping () -> Bool) async throws } @@ -47,6 +48,7 @@ extension OperationsManager { runner: WebOperationRunner, pixelHandler: EventMapping, userNotificationService: DataBrokerProtectionUserNotificationService, + isManual: Bool, shouldRunNextStep: @escaping () -> Bool) async throws { try await runOperation(operationData: operationData, @@ -56,6 +58,7 @@ extension OperationsManager { runner: runner, pixelHandler: pixelHandler, showWebView: false, + isManualScan: isManual, userNotificationService: userNotificationService, shouldRunNextStep: shouldRunNextStep) } @@ -70,6 +73,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { runner: WebOperationRunner, pixelHandler: EventMapping, showWebView: Bool = false, + isManualScan: Bool = false, userNotificationService: DataBrokerProtectionUserNotificationService, shouldRunNextStep: @escaping () -> Bool) async throws { @@ -80,6 +84,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { notificationCenter: notificationCenter, pixelHandler: pixelHandler, showWebView: showWebView, + isManual: isManualScan, userNotificationService: userNotificationService, shouldRunNextStep: shouldRunNextStep) } else if let optOutOperationData = operationData as? OptOutOperationData { @@ -102,6 +107,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { notificationCenter: NotificationCenter, pixelHandler: EventMapping, showWebView: Bool = false, + isManual: Bool = false, userNotificationService: DataBrokerProtectionUserNotificationService, shouldRunNextStep: @escaping () -> Bool) async throws { os_log("Running scan operation: %{public}@", log: .dataBrokerProtection, String(describing: brokerProfileQueryData.dataBroker.name)) @@ -118,13 +124,15 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { } let eventPixels = DataBrokerProtectionEventPixels(database: database, handler: pixelHandler) - let stageCalculator = DataBrokerProtectionStageDurationCalculator(dataBroker: brokerProfileQueryData.dataBroker.name, handler: pixelHandler) + let stageCalculator = DataBrokerProtectionStageDurationCalculator(dataBroker: brokerProfileQueryData.dataBroker.name, + handler: pixelHandler, + isManualScan: isManual) do { let event = HistoryEvent(brokerId: brokerId, profileQueryId: profileQueryId, type: .scanStarted) try database.add(event) - let extractedProfiles = try await runner.scan(brokerProfileQueryData, stageCalculator: stageCalculator, showWebView: showWebView, shouldRunNextStep: shouldRunNextStep) + let extractedProfiles = try await runner.scan(brokerProfileQueryData, stageCalculator: stageCalculator, pixelHandler: pixelHandler, showWebView: showWebView, shouldRunNextStep: shouldRunNextStep) os_log("Extracted profiles: %@", log: .dataBrokerProtection, extractedProfiles) if !extractedProfiles.isEmpty { @@ -322,6 +330,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { try await runner.optOut(profileQuery: brokerProfileQueryData, extractedProfile: extractedProfile, stageCalculator: stageDurationCalculator, + pixelHandler: pixelHandler, showWebView: showWebView, shouldRunNextStep: shouldRunNextStep) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift index 9128f87178..cc0df841f6 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift @@ -18,6 +18,7 @@ import Foundation import Common +import SecureStorage protocol ResourcesRepository { func fetchBrokerFromResourceFiles() throws -> [DataBroker]? @@ -118,7 +119,7 @@ public struct DataBrokerProtectionBrokerUpdater { } public static func provide() -> DataBrokerProtectionBrokerUpdater? { - if let vault = try? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: nil) { + if let vault = try? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: DataBrokerProtectionSecureVaultErrorReporter.shared) { return DataBrokerProtectionBrokerUpdater(vault: vault) } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutOperation.swift index bf5be40ecb..0a957a62f8 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutOperation.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutOperation.swift @@ -40,6 +40,8 @@ final class OptOutOperation: DataBrokerOperation { private let operationAwaitTime: TimeInterval let shouldRunNextStep: () -> Bool let clickAwaitTime: TimeInterval + let pixelHandler: EventMapping + var postLoadingSiteStartTime: Date? // Captcha is a third-party resource that sometimes takes more time to load // if we are not able to get the captcha information. We will try to run the action again @@ -57,6 +59,7 @@ final class OptOutOperation: DataBrokerOperation { operationAwaitTime: TimeInterval = 3, clickAwaitTime: TimeInterval = 40, stageCalculator: StageDurationCalculator, + pixelHandler: EventMapping, shouldRunNextStep: @escaping () -> Bool ) { self.privacyConfig = privacyConfig @@ -69,6 +72,7 @@ final class OptOutOperation: DataBrokerOperation { self.shouldRunNextStep = shouldRunNextStep self.clickAwaitTime = clickAwaitTime self.cookieHandler = cookieHandler + self.pixelHandler = pixelHandler } func run(inputValue: ExtractedProfile, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanOperation.swift index 10bf5bb9e9..f7bb9de995 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanOperation.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanOperation.swift @@ -41,6 +41,8 @@ final class ScanOperation: DataBrokerOperation { let shouldRunNextStep: () -> Bool var retriesCountOnError: Int = 0 let clickAwaitTime: TimeInterval + let pixelHandler: EventMapping + var postLoadingSiteStartTime: Date? init(privacyConfig: PrivacyConfigurationManaging, prefs: ContentScopeProperties, @@ -51,6 +53,7 @@ final class ScanOperation: DataBrokerOperation { operationAwaitTime: TimeInterval = 3, clickAwaitTime: TimeInterval = 0, stageDurationCalculator: StageDurationCalculator, + pixelHandler: EventMapping, shouldRunNextStep: @escaping () -> Bool ) { self.privacyConfig = privacyConfig @@ -63,6 +66,7 @@ final class ScanOperation: DataBrokerOperation { self.shouldRunNextStep = shouldRunNextStep self.clickAwaitTime = clickAwaitTime self.cookieHandler = cookieHandler + self.pixelHandler = pixelHandler } func run(inputValue: InputValue, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift index d9de1179a7..ad7f3cd61d 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift @@ -64,6 +64,11 @@ public enum DataBrokerProtectionPixels { static let wasOnWaitlist = "was_on_waitlist" static let httpCode = "http_code" static let backendServiceCallSite = "backend_service_callsite" + static let isManualScan = "is_manual_scan" + static let durationInMs = "duration_in_ms" + static let profileQueries = "profile_queries" + static let hasError = "has_error" + static let brokerURL = "broker_url" } case error(error: DataBrokerProtectionError, dataBroker: String) @@ -138,9 +143,9 @@ public enum DataBrokerProtectionPixels { case dataBrokerProtectionNotificationOpenedAllRecordsRemoved // Scan/Search pixels - case scanSuccess(dataBroker: String, matchesFound: Int, duration: Double, tries: Int) - case scanFailed(dataBroker: String, duration: Double, tries: Int) - case scanError(dataBroker: String, duration: Double, category: String, details: String) + case scanSuccess(dataBroker: String, matchesFound: Int, duration: Double, tries: Int, isManualScan: Bool) + case scanFailed(dataBroker: String, duration: Double, tries: Int, isManualScan: Bool) + case scanError(dataBroker: String, duration: Double, category: String, details: String, isManualScan: Bool) // KPIs - engagement case dailyActiveUser @@ -168,6 +173,13 @@ public enum DataBrokerProtectionPixels { case homeViewShowBadPathError case homeViewCTAMoveApplicationClicked case homeViewCTAGrantPermissionClicked + + // Initial scans pixels + // https://app.asana.com/0/1204006570077678/1206981742767458/f + case initialScanTotalDuration(duration: Double, profileQueries: Int) + case initialScanSiteLoadDuration(duration: Double, hasError: Bool, brokerURL: String) + case initialScanPostLoadingDuration(duration: Double, hasError: Bool, brokerURL: String) + case initialScanPreStartDuration(duration: Double) } extension DataBrokerProtectionPixels: PixelKitEvent { @@ -279,6 +291,12 @@ extension DataBrokerProtectionPixels: PixelKitEvent { 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" + + // Initial scans pixels + case .initialScanTotalDuration: return "m_mac_dbp_initial_scan_duration" + case .initialScanSiteLoadDuration: return "m_mac_dbp_scan_broker_site_loaded" + case .initialScanPostLoadingDuration: return "m_mac_dbp_initial_scan_broker_post_loading" + case .initialScanPreStartDuration: return "m_mac_dbp_initial_scan_pre_start_duration" } } @@ -403,12 +421,12 @@ extension DataBrokerProtectionPixels: PixelKitEvent { .ipcServerRunQueuedOperationsCompletion, .ipcServerRunAllOperations: return [Consts.bundleIDParamKey: Bundle.main.bundleIdentifier ?? "nil"] - case .scanSuccess(let dataBroker, let matchesFound, let duration, let tries): - return [Consts.dataBrokerParamKey: dataBroker, Consts.matchesFoundKey: String(matchesFound), Consts.durationParamKey: String(duration), Consts.triesKey: String(tries)] - case .scanFailed(let dataBroker, let duration, let tries): - return [Consts.dataBrokerParamKey: dataBroker, Consts.durationParamKey: String(duration), Consts.triesKey: String(tries)] - case .scanError(let dataBroker, let duration, let category, let details): - return [Consts.dataBrokerParamKey: dataBroker, Consts.durationParamKey: String(duration), Consts.errorCategoryKey: category, Consts.errorDetailsKey: details] + case .scanSuccess(let dataBroker, let matchesFound, let duration, let tries, let isManualScan): + return [Consts.dataBrokerParamKey: dataBroker, Consts.matchesFoundKey: String(matchesFound), Consts.durationParamKey: String(duration), Consts.triesKey: String(tries), Consts.isManualScan: isManualScan.description] + case .scanFailed(let dataBroker, let duration, let tries, let isManualScan): + return [Consts.dataBrokerParamKey: dataBroker, Consts.durationParamKey: String(duration), Consts.triesKey: String(tries), Consts.isManualScan: isManualScan.description] + case .scanError(let dataBroker, let duration, let category, let details, let isManualScan): + return [Consts.dataBrokerParamKey: dataBroker, Consts.durationParamKey: String(duration), Consts.errorCategoryKey: category, Consts.errorDetailsKey: details, Consts.isManualScan: isManualScan.description] case .generateEmailHTTPErrorDaily(let statusCode, let environment, let wasOnWaitlist): return [Consts.environmentKey: environment, Consts.httpCode: String(statusCode), @@ -417,6 +435,14 @@ extension DataBrokerProtectionPixels: PixelKitEvent { return [Consts.environmentKey: environment, Consts.wasOnWaitlist: String(wasOnWaitlist), Consts.backendServiceCallSite: backendServiceCallSite.rawValue] + case .initialScanTotalDuration(let duration, let profileQueries): + return [Consts.durationInMs: String(duration), Consts.profileQueries: String(profileQueries)] + case .initialScanSiteLoadDuration(let duration, let hasError, let brokerURL): + return [Consts.durationInMs: String(duration), Consts.hasError: hasError.description, Consts.brokerURL: brokerURL] + case .initialScanPostLoadingDuration(let duration, let hasError, let brokerURL): + return [Consts.durationInMs: String(duration), Consts.hasError: hasError.description, Consts.brokerURL: brokerURL] + case .initialScanPreStartDuration(let duration): + return [Consts.durationInMs: String(duration)] } } } @@ -502,7 +528,11 @@ public class DataBrokerProtectionPixelsHandler: EventMapping Double func durationSinceStartTime() -> Double @@ -62,6 +63,7 @@ protocol StageDurationCalculator { } final class DataBrokerProtectionStageDurationCalculator: StageDurationCalculator { + let isManualScan: Bool let handler: EventMapping let attemptId: UUID let dataBroker: String @@ -74,12 +76,14 @@ final class DataBrokerProtectionStageDurationCalculator: StageDurationCalculator init(attemptId: UUID = UUID(), startTime: Date = Date(), dataBroker: String, - handler: EventMapping) { + handler: EventMapping, + isManualScan: Bool = false) { self.attemptId = attemptId self.startTime = startTime self.lastStateTime = startTime self.dataBroker = dataBroker self.handler = handler + self.isManualScan = isManualScan } /// Returned in milliseconds @@ -159,11 +163,11 @@ final class DataBrokerProtectionStageDurationCalculator: StageDurationCalculator } func fireScanSuccess(matchesFound: Int) { - handler.fire(.scanSuccess(dataBroker: dataBroker, matchesFound: matchesFound, duration: durationSinceStartTime(), tries: 1)) + handler.fire(.scanSuccess(dataBroker: dataBroker, matchesFound: matchesFound, duration: durationSinceStartTime(), tries: 1, isManualScan: isManualScan)) } func fireScanFailed() { - handler.fire(.scanFailed(dataBroker: dataBroker, duration: durationSinceStartTime(), tries: 1)) + handler.fire(.scanFailed(dataBroker: dataBroker, duration: durationSinceStartTime(), tries: 1, isManualScan: isManualScan)) } func fireScanError(error: Error) { @@ -200,7 +204,8 @@ final class DataBrokerProtectionStageDurationCalculator: StageDurationCalculator dataBroker: dataBroker, duration: durationSinceStartTime(), category: errorCategory.toString, - details: error.localizedDescription + details: error.localizedDescription, + isManualScan: isManualScan ) ) } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionNoOpScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionNoOpScheduler.swift index cb3057b937..41e02edaca 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionNoOpScheduler.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionNoOpScheduler.swift @@ -35,8 +35,9 @@ final class DataBrokerProtectionNoOpScheduler: DataBrokerProtectionScheduler { func startScheduler(showWebView: Bool) { } func stopScheduler() { } func optOutAllBrokers(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { } - func runQueuedOperations(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { } - func startManualScan(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { } + func runQueuedOperations(showWebView: Bool, + completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { } + func startManualScan(showWebView: Bool, startTime: Date, 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 4e0cf5ac7d..74b381eb3e 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift @@ -24,13 +24,6 @@ protocol OperationRunnerProvider { func getOperationRunner() -> WebOperationRunner } -private enum DataBrokerProtectionProcessorFunction { - case startManualScans(pendingCompletion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) - case runAllOptOutOperations(pendingCompletion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) - case runQueuedOperations(pendingCompletion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) - case runAllOperations(pendingCompletion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) -} - final class DataBrokerProtectionProcessor { private let database: DataBrokerProtectionRepository private let config: SchedulerConfig @@ -42,8 +35,6 @@ final class DataBrokerProtectionProcessor { private let engagementPixels: DataBrokerProtectionEngagementPixels private let eventPixels: DataBrokerProtectionEventPixels - private var currentlyRunningOperationsForFunction: DataBrokerProtectionProcessorFunction? - init(database: DataBrokerProtectionRepository, config: SchedulerConfig, operationRunnerProvider: OperationRunnerProvider, @@ -66,15 +57,14 @@ final class DataBrokerProtectionProcessor { // MARK: - Public functions func startManualScans(showWebView: Bool = false, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil) { - interruptCurrentlyRunningFunction() - currentlyRunningOperationsForFunction = .startManualScans(pendingCompletion: completion) + + operationQueue.cancelAllOperations() runOperations(operationType: .scan, priorityDate: nil, - showWebView: showWebView) { [weak self] errors in + showWebView: showWebView) { errors in os_log("Scans done", log: .dataBrokerProtection) - self?.currentlyRunningOperationsForFunction = nil completion?(errors) - self?.calculateMisMatches() + self.calculateMisMatches() } } @@ -85,45 +75,37 @@ final class DataBrokerProtectionProcessor { func runAllOptOutOperations(showWebView: Bool = false, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil) { - interruptCurrentlyRunningFunction() - currentlyRunningOperationsForFunction = .runAllOptOutOperations(pendingCompletion: completion) + operationQueue.cancelAllOperations() runOperations(operationType: .optOut, priorityDate: nil, - showWebView: showWebView) { [weak self] errors in + showWebView: showWebView) { errors in os_log("Optouts done", log: .dataBrokerProtection) - self?.currentlyRunningOperationsForFunction = nil completion?(errors) } } func runQueuedOperations(showWebView: Bool = false, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil ) { - interruptCurrentlyRunningFunction() - currentlyRunningOperationsForFunction = .runQueuedOperations(pendingCompletion: completion) runOperations(operationType: .all, priorityDate: Date(), - showWebView: showWebView) { [weak self] errors in + showWebView: showWebView) { errors in os_log("Queued operations done", log: .dataBrokerProtection) - self?.currentlyRunningOperationsForFunction = nil completion?(errors) } } func runAllOperations(showWebView: Bool = false, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil ) { - interruptCurrentlyRunningFunction() - currentlyRunningOperationsForFunction = .runAllOperations(pendingCompletion: completion) runOperations(operationType: .all, priorityDate: nil, - showWebView: showWebView) { [weak self] errors in + showWebView: showWebView) { errors in os_log("Queued operations done", log: .dataBrokerProtection) - self?.currentlyRunningOperationsForFunction = nil completion?(errors) } } func stopAllOperations() { - interruptCurrentlyRunningFunction() + operationQueue.cancelAllOperations() } // MARK: - Private functions @@ -133,7 +115,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(reporter: nil) { + if let vault = try? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: DataBrokerProtectionSecureVaultErrorReporter.shared) { let brokerUpdater = DataBrokerProtectionBrokerUpdater(vault: vault, pixelHandler: pixelHandler) brokerUpdater.checkForUpdatesInBrokerJSONFiles() } @@ -202,25 +184,6 @@ final class DataBrokerProtectionProcessor { return collections } - private func interruptCurrentlyRunningFunction() { - operationQueue.cancelAllOperations() - - switch currentlyRunningOperationsForFunction { - case .startManualScans(let pendingCompletion), - .runAllOptOutOperations(let pendingCompletion), - .runQueuedOperations(let pendingCompletion), - .runAllOperations(let pendingCompletion): - - if let pendingCompletion = pendingCompletion { - // There's a current limitation that if interrupted, we won't propagate the scan errors - pendingCompletion(DataBrokerProtectionSchedulerErrorCollection(oneTimeError: DataBrokerProtectionSchedulerError.operationsInterrupted)) - } - case nil: - break - } - currentlyRunningOperationsForFunction = nil - } - deinit { os_log("Deinit DataBrokerProtectionProcessor", log: .dataBrokerProtection) } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift index e7a15bd1bb..4e7d9a9846 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift @@ -80,7 +80,7 @@ public protocol DataBrokerProtectionScheduler { func stopScheduler() func optOutAllBrokers(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) - func startManualScan(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) + func startManualScan(showWebView: Bool, startTime: Date, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) func runQueuedOperations(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) func runAllOperations(showWebView: Bool) @@ -98,8 +98,8 @@ extension DataBrokerProtectionScheduler { runAllOperations(showWebView: false) } - public func startManualScan() { - startManualScan(showWebView: false, completion: nil) + public func startManualScan(startTime: Date) { + startManualScan(showWebView: false, startTime: startTime, completion: nil) } } @@ -112,6 +112,14 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch static let tolerance: TimeInterval = 20 * 60 // 20 minutes } + private enum DataBrokerProtectionCurrentOperation { + case idle + case queued + case manualScan + case optOutAll + case all + } + private let privacyConfigManager: PrivacyConfigurationManaging private let contentScopeProperties: ContentScopeProperties private let dataManager: DataBrokerProtectionDataManager @@ -122,6 +130,7 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch private let emailService: EmailServiceProtocol private let captchaService: CaptchaServiceProtocol private let userNotificationService: DataBrokerProtectionUserNotificationService + private var currentOperation: DataBrokerProtectionCurrentOperation = .idle /// Ensures that only one scheduler operation is executed at the same time. /// @@ -186,9 +195,16 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch completion(.finished) return } + + guard self.currentOperation != .manualScan else { + os_log("Manual scan in progress, returning...", log: .dataBrokerProtection) + completion(.finished) + return + } self.lastSchedulerSessionStartTimestamp = Date() self.status = .running os_log("Scheduler running...", log: .dataBrokerProtection) + self.currentOperation = .queued self.dataBrokerProcessor.runQueuedOperations(showWebView: showWebView) { [weak self] errors in if let errors = errors { if let oneTimeError = errors.oneTimeError { @@ -201,6 +217,7 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch } } self?.status = .idle + self?.currentOperation = .idle completion(.finished) } } @@ -214,7 +231,13 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch } public func runAllOperations(showWebView: Bool = false) { + guard self.currentOperation != .manualScan else { + os_log("Manual scan in progress, returning...", log: .dataBrokerProtection) + return + } + os_log("Running all operations...", log: .dataBrokerProtection) + self.currentOperation = .all self.dataBrokerProcessor.runAllOperations(showWebView: showWebView) { [weak self] errors in if let errors = errors { if let oneTimeError = errors.oneTimeError { @@ -226,12 +249,19 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch os_log("Operation error(s) during DefaultDataBrokerProtectionScheduler.runAllOperations in dataBrokerProcessor.runAllOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) } } + self?.currentOperation = .idle } } public func runQueuedOperations(showWebView: Bool = false, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil) { + guard self.currentOperation != .manualScan else { + os_log("Manual scan in progress, returning...", log: .dataBrokerProtection) + return + } + os_log("Running queued operations...", log: .dataBrokerProtection) + self.currentOperation = .queued dataBrokerProcessor.runQueuedOperations(showWebView: showWebView, completion: { [weak self] errors in if let errors = errors { @@ -245,16 +275,20 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch } } completion?(errors) + self?.currentOperation = .idle }) } public func startManualScan(showWebView: Bool = false, + startTime: Date, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil) { + pixelHandler.fire(.initialScanPreStartDuration(duration: (Date().timeIntervalSince(startTime) * 1000).rounded(.towardZero))) + let backgroundAgentManualScanStartTime = Date() stopScheduler() userNotificationService.requestNotificationPermission() - + self.currentOperation = .manualScan os_log("Scanning all brokers...", log: .dataBrokerProtection) dataBrokerProcessor.startManualScans(showWebView: showWebView) { [weak self] errors in guard let self = self else { return } @@ -285,14 +319,33 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch os_log("Operation error(s) during DefaultDataBrokerProtectionScheduler.startManualScan in dataBrokerProcessor.runAllScanOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) } } - + self.currentOperation = .idle + fireManualScanCompletionPixel(startTime: backgroundAgentManualScanStartTime) completion?(errors) } } + private func fireManualScanCompletionPixel(startTime: Date) { + do { + let profileQueries = try dataManager.profileQueriesCount() + let durationSinceStart = Date().timeIntervalSince(startTime) * 1000 + self.pixelHandler.fire(.initialScanTotalDuration(duration: durationSinceStart.rounded(.towardZero), + profileQueries: profileQueries)) + } catch { + os_log("Manual Scan Error when trying to fetch the profile to get the profile queries", log: .dataBrokerProtection) + } + } + public func optOutAllBrokers(showWebView: Bool = false, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { + + guard self.currentOperation != .manualScan else { + os_log("Manual scan in progress, returning...", log: .dataBrokerProtection) + return + } + os_log("Opting out all brokers...", log: .dataBrokerProtection) + self.currentOperation = .optOutAll self.dataBrokerProcessor.runAllOptOutOperations(showWebView: showWebView, completion: { [weak self] errors in if let errors = errors { @@ -305,7 +358,7 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch os_log("Operation error(s) during DefaultDataBrokerProtectionScheduler.optOutAllBrokers in dataBrokerProcessor.runAllOptOutOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) } } - + self?.currentOperation = .idle completion?(errors) }) } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationActionTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationActionTests.swift index fdfc8469bf..cdbb776170 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationActionTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationActionTests.swift @@ -27,6 +27,7 @@ final class DataBrokerOperationActionTests: XCTestCase { let webViewHandler = WebViewHandlerMock() let emailService = EmailServiceMock() let captchaService = CaptchaServiceMock() + let pixelHandler = MockDataBrokerProtectionPixelsHandler() let stageCalulator = DataBrokerProtectionStageDurationCalculator(dataBroker: "broker", handler: MockDataBrokerProtectionPixelsHandler()) override func tearDown() async throws { @@ -47,6 +48,7 @@ final class DataBrokerOperationActionTests: XCTestCase { captchaService: captchaService, operationAwaitTime: 0, stageCalculator: stageCalulator, + pixelHandler: pixelHandler, shouldRunNextStep: { true } ) @@ -71,6 +73,7 @@ final class DataBrokerOperationActionTests: XCTestCase { captchaService: captchaService, operationAwaitTime: 0, stageCalculator: stageCalulator, + pixelHandler: pixelHandler, shouldRunNextStep: { true } ) @@ -102,6 +105,7 @@ final class DataBrokerOperationActionTests: XCTestCase { captchaService: captchaService, operationAwaitTime: 0, stageCalculator: stageCalulator, + pixelHandler: pixelHandler, shouldRunNextStep: { true } ) @@ -131,6 +135,7 @@ final class DataBrokerOperationActionTests: XCTestCase { captchaService: captchaService, operationAwaitTime: 0, stageCalculator: stageCalulator, + pixelHandler: pixelHandler, shouldRunNextStep: { true } ) sut.webViewHandler = webViewHandler @@ -153,6 +158,7 @@ final class DataBrokerOperationActionTests: XCTestCase { captchaService: captchaService, operationAwaitTime: 0, stageCalculator: stageCalulator, + pixelHandler: pixelHandler, shouldRunNextStep: { true } ) emailService.shouldThrow = true @@ -181,6 +187,7 @@ final class DataBrokerOperationActionTests: XCTestCase { operationAwaitTime: 0, clickAwaitTime: 0, stageCalculator: stageCalulator, + pixelHandler: pixelHandler, shouldRunNextStep: { true } ) sut.webViewHandler = webViewHandler @@ -200,6 +207,7 @@ final class DataBrokerOperationActionTests: XCTestCase { captchaService: captchaService, operationAwaitTime: 0, stageCalculator: stageCalulator, + pixelHandler: pixelHandler, shouldRunNextStep: { true } ) sut.webViewHandler = webViewHandler @@ -221,6 +229,7 @@ final class DataBrokerOperationActionTests: XCTestCase { captchaService: captchaService, operationAwaitTime: 0, stageCalculator: stageCalulator, + pixelHandler: pixelHandler, shouldRunNextStep: { true } ) sut.webViewHandler = webViewHandler @@ -243,6 +252,7 @@ final class DataBrokerOperationActionTests: XCTestCase { captchaService: captchaService, operationAwaitTime: 0, stageCalculator: stageCalulator, + pixelHandler: pixelHandler, shouldRunNextStep: { true } ) let actionsHandler = ActionsHandler(step: step) @@ -272,6 +282,7 @@ final class DataBrokerOperationActionTests: XCTestCase { captchaService: captchaService, operationAwaitTime: 0, stageCalculator: stageCalulator, + pixelHandler: pixelHandler, shouldRunNextStep: { true } ) sut.webViewHandler = webViewHandler @@ -294,6 +305,7 @@ final class DataBrokerOperationActionTests: XCTestCase { captchaService: captchaService, operationAwaitTime: 0, stageCalculator: stageCalulator, + pixelHandler: pixelHandler, shouldRunNextStep: { true } ) sut.retriesCountOnError = 0 @@ -317,6 +329,7 @@ final class DataBrokerOperationActionTests: XCTestCase { captchaService: captchaService, operationAwaitTime: 0, stageCalculator: stageCalulator, + pixelHandler: pixelHandler, shouldRunNextStep: { true } ) sut.webViewHandler = webViewHandler @@ -335,6 +348,7 @@ final class DataBrokerOperationActionTests: XCTestCase { captchaService: captchaService, operationAwaitTime: 0, stageCalculator: stageCalulator, + pixelHandler: pixelHandler, shouldRunNextStep: { true } ) sut.webViewHandler = webViewHandler @@ -355,6 +369,7 @@ final class DataBrokerOperationActionTests: XCTestCase { captchaService: captchaService, operationAwaitTime: 0, stageCalculator: mockStageCalculator, + pixelHandler: pixelHandler, shouldRunNextStep: { true } ) @@ -374,6 +389,7 @@ final class DataBrokerOperationActionTests: XCTestCase { captchaService: captchaService, operationAwaitTime: 0, stageCalculator: mockStageCalculator, + pixelHandler: pixelHandler, shouldRunNextStep: { true } ) @@ -393,6 +409,7 @@ final class DataBrokerOperationActionTests: XCTestCase { captchaService: captchaService, operationAwaitTime: 0, stageCalculator: mockStageCalculator, + pixelHandler: pixelHandler, shouldRunNextStep: { true } ) @@ -412,6 +429,7 @@ final class DataBrokerOperationActionTests: XCTestCase { captchaService: captchaService, operationAwaitTime: 0, stageCalculator: mockStageCalculator, + pixelHandler: pixelHandler, shouldRunNextStep: { true } ) @@ -431,6 +449,7 @@ final class DataBrokerOperationActionTests: XCTestCase { cookieHandler: mockCookieHandler, operationAwaitTime: 0, stageCalculator: stageCalulator, + pixelHandler: pixelHandler, shouldRunNextStep: { true } ) @@ -452,6 +471,7 @@ final class DataBrokerOperationActionTests: XCTestCase { cookieHandler: mockCookieHandler, operationAwaitTime: 0, stageCalculator: stageCalulator, + pixelHandler: pixelHandler, shouldRunNextStep: { true } ) diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift index 70619e8736..19d92e74d6 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift @@ -20,6 +20,8 @@ import XCTest import BrowserServicesKit +import Common +import PixelKit @testable import DataBrokerProtection final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { @@ -931,14 +933,13 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { } final class MockWebOperationRunner: WebOperationRunner { - var shouldScanThrow = false var shouldOptOutThrow = false var scanResults = [ExtractedProfile]() var wasScanCalled = false var wasOptOutCalled = false - func scan(_ profileQuery: BrokerProfileQueryData, stageCalculator: StageDurationCalculator, showWebView: Bool, shouldRunNextStep: @escaping () -> Bool) async throws -> [ExtractedProfile] { + func scan(_ profileQuery: BrokerProfileQueryData, stageCalculator: StageDurationCalculator, pixelHandler: EventMapping, showWebView: Bool, shouldRunNextStep: @escaping () -> Bool) async throws -> [ExtractedProfile] { wasScanCalled = true if shouldScanThrow { @@ -948,7 +949,7 @@ final class MockWebOperationRunner: WebOperationRunner { } } - func optOut(profileQuery: BrokerProfileQueryData, extractedProfile: ExtractedProfile, stageCalculator: StageDurationCalculator, showWebView: Bool, shouldRunNextStep: @escaping () -> Bool) async throws { + func optOut(profileQuery: BrokerProfileQueryData, extractedProfile: ExtractedProfile, stageCalculator: StageDurationCalculator, pixelHandler: EventMapping, showWebView: Bool, shouldRunNextStep: @escaping () -> Bool) async throws { wasOptOutCalled = true if shouldOptOutThrow { diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionStageDurationCalculatorTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionStageDurationCalculatorTests.swift index 7ba868976f..c7aeee1db7 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionStageDurationCalculatorTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionStageDurationCalculatorTests.swift @@ -39,7 +39,7 @@ final class DataBrokerProtectionStageDurationCalculatorTests: XCTestCase { if let failurePixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.last{ switch failurePixel { - case .scanFailed(let broker, _, _): + case .scanFailed(let broker, _, _, _): XCTAssertEqual(broker, "broker") default: XCTFail("The scan failed pixel should be fired") } @@ -57,7 +57,7 @@ final class DataBrokerProtectionStageDurationCalculatorTests: XCTestCase { if let failurePixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.last{ switch failurePixel { - case .scanError(_, _, let category, _): + case .scanError(_, _, let category, _, _): XCTAssertEqual(category, ErrorCategory.clientError(httpCode: 403).toString) default: XCTFail("The scan error pixel should be fired") } @@ -75,7 +75,7 @@ final class DataBrokerProtectionStageDurationCalculatorTests: XCTestCase { if let failurePixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.last{ switch failurePixel { - case .scanError(_, _, let category, _): + case .scanError(_, _, let category, _, _): XCTAssertEqual(category, ErrorCategory.serverError(httpCode: 500).toString) default: XCTFail("The scan error pixel should be fired") } @@ -93,7 +93,7 @@ final class DataBrokerProtectionStageDurationCalculatorTests: XCTestCase { if let failurePixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.last{ switch failurePixel { - case .scanError(_, _, let category, _): + case .scanError(_, _, let category, _, _): XCTAssertEqual(category, ErrorCategory.validationError.toString) default: XCTFail("The scan error pixel should be fired") } @@ -112,7 +112,7 @@ final class DataBrokerProtectionStageDurationCalculatorTests: XCTestCase { if let failurePixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.last{ switch failurePixel { - case .scanError(_, _, let category, _): + case .scanError(_, _, let category, _, _): XCTAssertEqual(category, ErrorCategory.networkError.toString) default: XCTFail("The scan error pixel should be fired") } @@ -131,7 +131,7 @@ final class DataBrokerProtectionStageDurationCalculatorTests: XCTestCase { if let failurePixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.last{ switch failurePixel { - case .scanError(_, _, let category, _): + case .scanError(_, _, let category, _, _): XCTAssertEqual(category, "database-error-SecureVaultError-13") default: XCTFail("The scan error pixel should be fired") } @@ -150,7 +150,7 @@ final class DataBrokerProtectionStageDurationCalculatorTests: XCTestCase { if let failurePixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.last{ switch failurePixel { - case .scanError(_, _, let category, _): + case .scanError(_, _, let category, _, _): XCTAssertEqual(category, ErrorCategory.unclassified.toString) default: XCTFail("The scan error pixel should be fired") } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift index 4cdc54e06e..9b60fe4812 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift @@ -868,6 +868,7 @@ final class MockAppVersion: AppVersionNumberProvider { } final class MockStageDurationCalculator: StageDurationCalculator { + var isManualScan: Bool = false var attemptId: UUID = UUID() var stage: Stage? diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 51cd56ed21..c9d81bcd9e 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "140.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "143.0.0"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), 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/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 920a68d365..3062d34484 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: "140.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "143.0.0"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/LocalPackages/SwiftUIExtensions/Sources/PreferencesViews/Assets.xcassets/Colors/Contents.json b/LocalPackages/SwiftUIExtensions/Sources/PreferencesViews/Assets.xcassets/Colors/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/LocalPackages/SwiftUIExtensions/Sources/PreferencesViews/Assets.xcassets/Colors/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LocalPackages/SwiftUIExtensions/Sources/PreferencesViews/Assets.xcassets/Colors/LinkBlueColor.colorset/Contents.json b/LocalPackages/SwiftUIExtensions/Sources/PreferencesViews/Assets.xcassets/Colors/LinkBlueColor.colorset/Contents.json new file mode 100644 index 0000000000..bb413935ee --- /dev/null +++ b/LocalPackages/SwiftUIExtensions/Sources/PreferencesViews/Assets.xcassets/Colors/LinkBlueColor.colorset/Contents.json @@ -0,0 +1,78 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xEE", + "green" : "0x69", + "red" : "0x39" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xF6", + "green" : "0x94", + "red" : "0x71" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "contrast", + "value" : "high" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.933", + "green" : "0.412", + "red" : "0.224" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + }, + { + "appearance" : "contrast", + "value" : "high" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.965", + "green" : "0.580", + "red" : "0.443" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} 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/LocalPackages/SyncUI/Sources/SyncUI/Resources/Localizable.xcstrings b/LocalPackages/SyncUI/Sources/SyncUI/Resources/Localizable.xcstrings index 4abefdc849..dc53430490 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Resources/Localizable.xcstrings +++ b/LocalPackages/SyncUI/Sources/SyncUI/Resources/Localizable.xcstrings @@ -123,11 +123,59 @@ "comment" : "Title for an error alert", "extractionState" : "extracted_with_value", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sync & Backup Error" + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "Sync & Backup Error" } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sync & Backup Error" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sync & Backup Error" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sync & Backup Error" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sync & Backup Error" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sync & Backup Error" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sync & Backup Error" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sync & Backup Error" + } } } }, @@ -135,11 +183,59 @@ "comment" : "Button Title of an error alert", "extractionState" : "extracted_with_value", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Go to Settings" + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "Go to Settings" } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Go to Settings" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Go to Settings" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Go to Settings" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Go to Settings" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Go to Settings" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Go to Settings" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Go to Settings" + } } } }, @@ -207,11 +303,59 @@ "comment" : "Description for unable to authenticate error", "extractionState" : "extracted_with_value", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "A device password is required to use Sync & Backup." + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "A device password is required to use Sync & Backup." } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "A device password is required to use Sync & Backup." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "A device password is required to use Sync & Backup." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "A device password is required to use Sync & Backup." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "A device password is required to use Sync & Backup." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "A device password is required to use Sync & Backup." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "A device password is required to use Sync & Backup." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "A device password is required to use Sync & Backup." + } } } }, @@ -6277,4 +6421,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/UnitTests/AppDelegate/AutoClearHandlerTests.swift b/UnitTests/AppDelegate/AutoClearHandlerTests.swift new file mode 100644 index 0000000000..192de793f3 --- /dev/null +++ b/UnitTests/AppDelegate/AutoClearHandlerTests.swift @@ -0,0 +1,83 @@ +// +// AutoClearHandlerTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import XCTest + +@testable import DuckDuckGo_Privacy_Browser + +@MainActor +class AutoClearHandlerTests: XCTestCase { + + var handler: AutoClearHandler! + var preferences: DataClearingPreferences! + var fireViewModel: FireViewModel! + + override func setUp() { + super.setUp() + let persistor = MockFireButtonPreferencesPersistor() + preferences = DataClearingPreferences(persistor: persistor) + fireViewModel = FireViewModel(fire: Fire(tld: ContentBlocking.shared.tld)) + let fileName = "AutoClearHandlerTests" + let fileStore = FileStoreMock() + let service = StatePersistenceService(fileStore: fileStore, fileName: fileName) + let appStateRestorationManager = AppStateRestorationManager(fileStore: fileStore, + service: service, + shouldRestorePreviousSession: false) + handler = AutoClearHandler(preferences: preferences, fireViewModel: fireViewModel, stateRestorationManager: appStateRestorationManager) + } + + override func tearDown() { + handler = nil + preferences = nil + fireViewModel = nil + super.tearDown() + } + + func testWhenBurningEnabledAndNoWarningRequiredThenTerminateLaterIsReturned() { + preferences.isAutoClearEnabled = true + preferences.isWarnBeforeClearingEnabled = false + + let response = handler.handleAppTermination() + + XCTAssertEqual(response, .terminateLater) + } + + func testWhenBurningDisabledThenNoTerminationResponse() { + preferences.isAutoClearEnabled = false + + let response = handler.handleAppTermination() + + XCTAssertNil(response) + } + + func testWhenBurningEnabledAndFlagFalseThenBurnOnStartTriggered() { + preferences.isAutoClearEnabled = true + handler.resetTheCorrectTerminationFlag() + + XCTAssertTrue(handler.burnOnStartIfNeeded()) + } + + func testWhenBurningDisabledThenBurnOnStartNotTriggered() { + preferences.isAutoClearEnabled = false + handler.resetTheCorrectTerminationFlag() + + XCTAssertFalse(handler.burnOnStartIfNeeded()) + } + +} diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInviteDialog.swift b/UnitTests/Bookmarks/Helpers/WebsiteInfoHelpers.swift similarity index 50% rename from DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInviteDialog.swift rename to UnitTests/Bookmarks/Helpers/WebsiteInfoHelpers.swift index 2d6aa8476e..9a87f91090 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInviteDialog.swift +++ b/UnitTests/Bookmarks/Helpers/WebsiteInfoHelpers.swift @@ -1,7 +1,7 @@ // -// NetworkProtectionInviteDialog.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. @@ -16,21 +16,20 @@ // limitations under the License. // -import SwiftUI -import NetworkProtection -import SwiftUIExtensions +import Foundation +@testable import DuckDuckGo_Privacy_Browser -struct NetworkProtectionInviteDialog: View { - @ObservedObject var model: NetworkProtectionInviteViewModel +extension WebsiteInfo { - var body: some View { - switch model.currentDialog { - case .codeEntry: - InviteCodeView(viewModel: model.inviteCodeViewModel) - case .success: - InviteCodeSuccessView(viewModel: model.successCodeViewModel) - case .none: - EmptyView() - } + @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/DuckDuckGo/Common/Extensions/NSStoryboardExtension.swift b/UnitTests/Bookmarks/Services/BookmarkFolderStoreMock.swift similarity index 68% rename from DuckDuckGo/Common/Extensions/NSStoryboardExtension.swift rename to UnitTests/Bookmarks/Services/BookmarkFolderStoreMock.swift index 9e50fa79ce..014282eff6 100644 --- a/DuckDuckGo/Common/Extensions/NSStoryboardExtension.swift +++ b/UnitTests/Bookmarks/Services/BookmarkFolderStoreMock.swift @@ -1,7 +1,7 @@ // -// NSStoryboardExtension.swift +// BookmarkFolderStoreMock.swift // -// Copyright © 2021 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. @@ -16,10 +16,10 @@ // limitations under the License. // -import AppKit +import Foundation +@testable import DuckDuckGo_Privacy_Browser -extension NSStoryboard { - - static var bookmarks = NSStoryboard(name: "Bookmarks", bundle: .main) +final class BookmarkFolderStoreMock: BookmarkFoldersStore { + var lastBookmarkAllTabsFolderIdUsed: String? } diff --git a/UnitTests/Bookmarks/Services/LocalBookmarkStoreTests.swift b/UnitTests/Bookmarks/Services/LocalBookmarkStoreTests.swift index 2ebff62e7d..1619b751fa 100644 --- a/UnitTests/Bookmarks/Services/LocalBookmarkStoreTests.swift +++ b/UnitTests/Bookmarks/Services/LocalBookmarkStoreTests.swift @@ -30,6 +30,7 @@ extension LocalBookmarkStore { } } +@MainActor final class LocalBookmarkStoreTests: XCTestCase { // MARK: Save/Delete @@ -261,6 +262,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 { @@ -313,6 +442,122 @@ final class LocalBookmarkStoreTests: XCTestCase { XCTAssertEqual(expectedBookmarkUUIDs, updatedFetchedBookmarkUUIDs) } + func testWhenMovingBookmarkWithinParentCollection_AndThereAreStubs_ThenIndexIsCalculatedAndBookmarkIsMoved() async { + let context = container.viewContext + let bookmarkStore = LocalBookmarkStore(context: context) + + guard let rootMO = BookmarkUtils.fetchRootFolder(context) else { + XCTFail("Missing root folder") + return + } + + let folderMO = BookmarkEntity.makeFolder(title: "Parent", parent: rootMO, context: context) + + let bookmarkStub1MO = BookmarkEntity.makeBookmark(title: "Stub 1", url: "", parent: folderMO, + context: context) + bookmarkStub1MO.isStub = true + + let bookmark1MO = BookmarkEntity.makeBookmark(title: "Example 1", url: "https://example1.com", parent: folderMO, + context: context) + + let bookmarkStub2MO = BookmarkEntity.makeBookmark(title: "Stub 2", url: "", parent: folderMO, + context: context) + bookmarkStub2MO.isStub = true + + let bookmark2MO = BookmarkEntity.makeBookmark(title: "Example 2", url: "https://example2.com", parent: folderMO, + context: context) + + let bookmarkStub3MO = BookmarkEntity.makeBookmark(title: "Stub 3", url: "", parent: folderMO, + context: context) + bookmarkStub3MO.isStub = true + let bookmarkStub4MO = BookmarkEntity.makeBookmark(title: "Stub 4", url: "", parent: folderMO, + context: context) + bookmarkStub4MO.isStub = true + + let bookmark3MO = BookmarkEntity.makeBookmark(title: "Example 3", url: "https://example3.com", parent: folderMO, + context: context) + + let bookmarkStub5MO = BookmarkEntity.makeBookmark(title: "Stub 5", url: "", parent: folderMO, + context: context) + bookmarkStub5MO.isStub = true + + let bookmark4MO = BookmarkEntity.makeBookmark(title: "Example 4", url: "https://example3.com", parent: folderMO, + context: context) + + let bookmarkStub6MO = BookmarkEntity.makeBookmark(title: "Stub 6", url: "", parent: folderMO, + context: context) + bookmarkStub6MO.isStub = true + + // Save the initial bookmarks state: + + do { + try context.save() + } catch { + XCTFail("Failed to save context") + } + + // Fetch persisted bookmarks back from the store: + + guard case let .success(initialTopLevelEntities) = await bookmarkStore.loadAll(type: .topLevelEntities), + let initialParentFolder = initialTopLevelEntities.first as? BookmarkFolder else { + XCTFail("Couldn't load top level entities") + return + } + + XCTAssertEqual(initialParentFolder.children.count, 4) + + // Verify initial order of saved bookmarks: + + let initialBookmarkUUIDs = [bookmark1MO.uuid, bookmark2MO.uuid, bookmark3MO.uuid, bookmark4MO.uuid] + let initialFetchedBookmarkUUIDs = initialParentFolder.children.map(\.id) + XCTAssertEqual(initialBookmarkUUIDs, initialFetchedBookmarkUUIDs) + + func testMoving(bookmarkUUIDs: [String], toIndex: Int) async -> [String] { + let moveBookmarksError = await bookmarkStore.move(objectUUIDs: bookmarkUUIDs, toIndex: toIndex, withinParentFolder: .parent(uuid: folderMO.uuid!)) + XCTAssertNil(moveBookmarksError) + + guard case let .success(updatedTopLevelEntities) = await bookmarkStore.loadAll(type: .topLevelEntities), + let updatedParentFolder = updatedTopLevelEntities.first as? BookmarkFolder else { + XCTFail("Couldn't load top level entities") + return [] + } + + return updatedParentFolder.children.map(\.title) + } + + // Update the order of the bookmarks: + // More than one bookmark + // To the end + var result = await testMoving(bookmarkUUIDs: [bookmark1MO.uuid!, bookmark2MO.uuid!], toIndex: 4) + XCTAssertEqual(result, [bookmark3MO.title, bookmark4MO.title, bookmark1MO.title, bookmark2MO.title]) + // To the beginning + result = await testMoving(bookmarkUUIDs: [bookmark1MO.uuid!, bookmark2MO.uuid!], toIndex: 0) + XCTAssertEqual(result, [bookmark1MO.title, bookmark2MO.title, bookmark3MO.title, bookmark4MO.title]) + // To middle + result = await testMoving(bookmarkUUIDs: [bookmark1MO.uuid!, bookmark2MO.uuid!], toIndex: 3) + XCTAssertEqual(result, [bookmark3MO.title, bookmark1MO.title, bookmark2MO.title, bookmark4MO.title]) + // To the beginning + result = await testMoving(bookmarkUUIDs: [bookmark1MO.uuid!, bookmark2MO.uuid!], toIndex: 0) + XCTAssertEqual(result, [bookmark1MO.title, bookmark2MO.title, bookmark3MO.title, bookmark4MO.title]) + + // Single bookmark + // Middle to end + result = await testMoving(bookmarkUUIDs: [bookmark2MO.uuid!], toIndex: 4) + XCTAssertEqual(result, [bookmark1MO.title, bookmark3MO.title, bookmark4MO.title, bookmark2MO.title]) + // First to Beginning + result = await testMoving(bookmarkUUIDs: [bookmark1MO.uuid!], toIndex: 0) + XCTAssertEqual(result, [bookmark1MO.title, bookmark3MO.title, bookmark4MO.title, bookmark2MO.title]) + // First to First + result = await testMoving(bookmarkUUIDs: [bookmark1MO.uuid!], toIndex: 1) + XCTAssertEqual(result, [bookmark1MO.title, bookmark3MO.title, bookmark4MO.title, bookmark2MO.title]) + // First to Second + result = await testMoving(bookmarkUUIDs: [bookmark1MO.uuid!], toIndex: 2) + XCTAssertEqual(result, [bookmark3MO.title, bookmark1MO.title, bookmark4MO.title, bookmark2MO.title]) + // First to End + result = await testMoving(bookmarkUUIDs: [bookmark3MO.uuid!], toIndex: 4) + XCTAssertEqual(result, [bookmark1MO.title, bookmark4MO.title, bookmark2MO.title, bookmark3MO.title]) + } + func testWhenMovingBookmarkWithinParentCollection_AndIndexIsOutOfBounds_ThenBookmarkIsAppended() async { let context = container.viewContext let bookmarkStore = LocalBookmarkStore(context: context) @@ -743,6 +988,99 @@ final class LocalBookmarkStoreTests: XCTestCase { XCTAssertEqual(expectedBookmarkUUIDs, updatedFetchedBookmarkUUIDs) } + func testWhenMovingFavorite_AndThereAreStubs_ThenIndexIsCalculatedAndBookmarkIsMoved() async { + let context = container.viewContext + let bookmarkStore = LocalBookmarkStore(context: context) + bookmarkStore.applyFavoritesDisplayMode(.displayUnified(native: .desktop)) + + guard let rootMO = BookmarkUtils.fetchRootFolder(context) else { + XCTFail("Missing root folder") + return + } + + let folderMO = BookmarkEntity.makeFolder(title: "Parent", parent: rootMO, context: context) + + let bookmark1MO = BookmarkEntity.makeBookmark(title: "Example 1", url: "https://example1.com", parent: folderMO, + context: context) + let bookmark2MO = BookmarkEntity.makeBookmark(title: "Example 2", url: "https://example2.com", parent: folderMO, + context: context) + let bookmarkStub1MO = BookmarkEntity.makeBookmark(title: "Stub 1", url: "", parent: folderMO, + context: context) + bookmarkStub1MO.isStub = true + let bookmarkStub2MO = BookmarkEntity.makeBookmark(title: "Stub 2", url: "", parent: folderMO, + context: context) + bookmarkStub2MO.isStub = true + let bookmark3MO = BookmarkEntity.makeBookmark(title: "Example 3", url: "https://example3.com", parent: folderMO, + context: context) + let bookmarkStub3MO = BookmarkEntity.makeBookmark(title: "Stub 3", url: "", parent: folderMO, + context: context) + bookmarkStub3MO.isStub = true + + let favoriteRoots = BookmarkUtils.fetchFavoritesFolders(for: .displayUnified(native: .desktop), in: context) + guard !favoriteRoots.isEmpty else { + XCTFail("No favorite root") + return + } + bookmark1MO.addToFavorites(folders: favoriteRoots) + bookmark2MO.addToFavorites(folders: favoriteRoots) + bookmarkStub1MO.addToFavorites(folders: favoriteRoots) + bookmarkStub2MO.addToFavorites(folders: favoriteRoots) + bookmark3MO.addToFavorites(folders: favoriteRoots) + bookmarkStub3MO.addToFavorites(folders: favoriteRoots) + + // Save the initial state: + + do { + try context.save() + } catch { + XCTFail("Failed to save context") + } + + // Fetch persisted bookmarks back from the store: + + guard case let .success(favorites) = await bookmarkStore.loadAll(type: .favorites) else { + XCTFail("Couldn't load top level entities") + return + } + + XCTAssertEqual(favorites.count, 3) + + // Verify initial order of saved bookmarks: + + let initialBookmarkUUIDs = [bookmark1MO.uuid, bookmark2MO.uuid, bookmark3MO.uuid] + let initialFetchedBookmarkUUIDs = favorites.map(\.id) + XCTAssertEqual(initialBookmarkUUIDs, initialFetchedBookmarkUUIDs) + + func testMoving(bookmarkUUID: String, toIndex: Int) async -> [String] { + let moveBookmarksError = await bookmarkStore.moveFavorites(with: [bookmarkUUID], toIndex: toIndex) + XCTAssertNil(moveBookmarksError) + + guard case let .success(updatedFavorites) = await bookmarkStore.loadAll(type: .favorites) else { + XCTFail("Couldn't load top level entities") + return [] + } + + return updatedFavorites.map(\.title) + } + + // Update the order of the bookmarks: + // Middle to end + var result = await testMoving(bookmarkUUID: bookmark2MO.uuid!, toIndex: 3) + XCTAssertEqual(result, [bookmark1MO.title, bookmark3MO.title, bookmark2MO.title]) + // First to Beginning + result = await testMoving(bookmarkUUID: bookmark1MO.uuid!, toIndex: 0) + XCTAssertEqual(result, [bookmark1MO.title, bookmark3MO.title, bookmark2MO.title]) + // First to First + result = await testMoving(bookmarkUUID: bookmark1MO.uuid!, toIndex: 1) + XCTAssertEqual(result, [bookmark1MO.title, bookmark3MO.title, bookmark2MO.title]) + // First to Second + result = await testMoving(bookmarkUUID: bookmark1MO.uuid!, toIndex: 2) + XCTAssertEqual(result, [bookmark3MO.title, bookmark1MO.title, bookmark2MO.title]) + // First to End + result = await testMoving(bookmarkUUID: bookmark3MO.uuid!, toIndex: 3) + XCTAssertEqual(result, [bookmark1MO.title, bookmark2MO.title, bookmark3MO.title]) + } + func testWhenMovingFavorite_AndIndexIsOutOfBounds_ThenFavoriteIsAppended() async { let context = container.viewContext let bookmarkStore = LocalBookmarkStore(context: context) @@ -1071,6 +1409,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/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/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 c7c04c2646..1627850eb7 100644 --- a/UnitTests/Menus/MoreOptionsMenuTests.swift +++ b/UnitTests/Menus/MoreOptionsMenuTests.swift @@ -65,7 +65,7 @@ final class MoreOptionsMenuTests: XCTestCase { } @MainActor - func testThatMoreOptionMenuHasTheExpectedItems_WhenNetworkProtectionIsEnabled() { + func testThatMoreOptionMenuHasTheExpectedItems() { moreOptionMenu = MoreOptionsMenu(tabCollectionViewModel: tabCollectionViewModel, passwordManagerCoordinator: passwordManagerCoordinator, networkProtectionFeatureVisibility: NetworkProtectionVisibilityMock(isInstalled: false, visible: true), @@ -100,38 +100,6 @@ final class MoreOptionsMenuTests: XCTestCase { } } - @MainActor - func testThatMoreOptionMenuHasTheExpectedItems_WhenNetworkProtectionIsDisabled() { - moreOptionMenu = MoreOptionsMenu(tabCollectionViewModel: tabCollectionViewModel, - passwordManagerCoordinator: passwordManagerCoordinator, - networkProtectionFeatureVisibility: NetworkProtectionVisibilityMock(isInstalled: false, visible: false), - sharingMenu: NSMenu(), - internalUserDecider: internalUserDecider) - - XCTAssertEqual(moreOptionMenu.items[0].title, UserText.sendFeedback) - XCTAssertTrue(moreOptionMenu.items[1].isSeparatorItem) - XCTAssertEqual(moreOptionMenu.items[2].title, UserText.plusButtonNewTabMenuItem) - XCTAssertEqual(moreOptionMenu.items[3].title, UserText.newWindowMenuItem) - XCTAssertEqual(moreOptionMenu.items[4].title, UserText.newBurnerWindowMenuItem) - XCTAssertTrue(moreOptionMenu.items[5].isSeparatorItem) - XCTAssertEqual(moreOptionMenu.items[6].title, UserText.zoom) - XCTAssertTrue(moreOptionMenu.items[7].isSeparatorItem) - XCTAssertEqual(moreOptionMenu.items[8].title, UserText.bookmarks) - XCTAssertEqual(moreOptionMenu.items[9].title, UserText.downloads) - XCTAssertEqual(moreOptionMenu.items[10].title, UserText.passwordManagementTitle) - XCTAssertTrue(moreOptionMenu.items[11].isSeparatorItem) - XCTAssertEqual(moreOptionMenu.items[12].title, UserText.emailOptionsMenuItem) - XCTAssertTrue(moreOptionMenu.items[13].isSeparatorItem) - - if AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)).isUserAuthenticated { - XCTAssertTrue(moreOptionMenu.items[14].title.hasPrefix(UserText.identityTheftRestorationOptionsMenuItem)) - XCTAssertTrue(moreOptionMenu.items[15].isSeparatorItem) - XCTAssertEqual(moreOptionMenu.items[16].title, UserText.settings) - } else { - XCTAssertEqual(moreOptionMenu.items[14].title, UserText.settings) - } - } - // MARK: Zoom @MainActor @@ -155,6 +123,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 { @@ -178,10 +163,6 @@ final class NetworkProtectionVisibilityMock: NetworkProtectionFeatureVisibility return !visible } - func isNetworkProtectionBetaVisible() -> Bool { - return visible - } - func canStartVPN() async throws -> Bool { return false } @@ -190,10 +171,6 @@ final class NetworkProtectionVisibilityMock: NetworkProtectionFeatureVisibility // intentional no-op } - func disableForWaitlistUsers() { - // intentional no-op - } - var isEligibleForThankYouMessage: Bool { false } diff --git a/UnitTests/Preferences/DataClearingPreferencesTests.swift b/UnitTests/Preferences/DataClearingPreferencesTests.swift index 5563fb6e90..1c0b6675e4 100644 --- a/UnitTests/Preferences/DataClearingPreferencesTests.swift +++ b/UnitTests/Preferences/DataClearingPreferencesTests.swift @@ -20,7 +20,11 @@ import XCTest @testable import DuckDuckGo_Privacy_Browser class MockFireButtonPreferencesPersistor: FireButtonPreferencesPersistor { + + var autoClearEnabled: Bool = false + var warnBeforeClearingEnabled: Bool = false var loginDetectionEnabled: Bool = false + } class DataClearingPreferencesTests: XCTestCase { 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..d9a45fda0c 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,19 @@ private class MockVPNMetadataCollector: VPNMetadataCollector { notificationsAgentIsRunning: true ) + let privacyProInfo = VPNMetadata.PrivacyProInfo( + 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/UnitTests/Waitlist/Mocks/MockNetworkProtectionCodeRedeemer.swift b/UnitTests/Waitlist/Mocks/MockNetworkProtectionCodeRedeemer.swift deleted file mode 100644 index b998d135e0..0000000000 --- a/UnitTests/Waitlist/Mocks/MockNetworkProtectionCodeRedeemer.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// MockNetworkProtectionCodeRedeemer.swift -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import NetworkProtection - -final class MockNetworkProtectionCodeRedeemer: NetworkProtectionCodeRedeeming { - - enum MockNetworkProtectionCodeRedeemerError: Error { - case error - } - - var throwError: Bool = false - - var redeemedCode: String? - func redeem(_ code: String) async throws { - if throwError { - throw MockNetworkProtectionCodeRedeemerError.error - } else { - redeemedCode = code - } - } - - var redeemedAccessToken: String? - func exchange(accessToken: String) async throws { - if throwError { - throw MockNetworkProtectionCodeRedeemerError.error - } else { - redeemedAccessToken = accessToken - } - } - -} 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