diff --git a/.github/actions/asana-create-action-item/action.yml b/.github/actions/asana-create-action-item/action.yml index b83f2dd1ad..a5adf09d9d 100644 --- a/.github/actions/asana-create-action-item/action.yml +++ b/.github/actions/asana-create-action-item/action.yml @@ -38,22 +38,20 @@ runs: using: "composite" steps: - id: get-automation-subtask - uses: ./.github/actions/asana-get-release-automation-subtask-id - with: - access-token: ${{ inputs.access-token }} - task-url: ${{ inputs.release-task-url }} + shell: bash + env: + ASANA_ACCESS_TOKEN: ${{ inputs.access-token }} + run: bundle exec fastlane run asana_get_release_automation_subtask_id task_url:"${{ inputs.release-task-url }}" - id: get-asana-user-id + shell: bash if: github.event_name != 'schedule' - uses: duckduckgo/apple-toolbox/actions/asana-get-user-id-for-github-handle@main - with: - access-token: ${{ inputs.access-token }} - github-handle: ${{ github.actor }} + run: bundle exec fastlane run asana_get_user_id_for_github_handle github_handle:"${{ github.actor }}" - id: set-assignee-id shell: bash env: - USER_ID: ${{ steps.get-asana-user-id.outputs.user-id || steps.get-automation-subtask.outputs.assignee-id }} + USER_ID: ${{ steps.get-asana-user-id.outputs.asana_user_id || steps.get-automation-subtask.outputs.asana_assignee_id }} run: | echo "assignee-id=${USER_ID}" >> $GITHUB_OUTPUT @@ -62,7 +60,7 @@ runs: shell: bash env: ASSIGNEE_ID: ${{ steps.set-assignee-id.outputs.assignee-id }} - AUTOMATION_TASK_ID: ${{ steps.get-automation-subtask.outputs.automation-task-id }} + AUTOMATION_TASK_ID: ${{ steps.get-automation-subtask.outputs.asana_automation_task_id }} TEMPLATE_PATH: ${{ github.action_path }}/templates/${{ inputs.template-name }}.yml WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} run: | @@ -80,7 +78,7 @@ runs: shell: bash env: ASSIGNEE_ID: ${{ steps.set-assignee-id.outputs.assignee-id }} - AUTOMATION_TASK_ID: ${{ steps.get-automation-subtask.outputs.automation-task-id }} + AUTOMATION_TASK_ID: ${{ steps.get-automation-subtask.outputs.asana_automation_task_id }} NOTES: ${{ inputs.notes }} TASK_NAME: ${{ inputs.task-name }} WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} @@ -93,7 +91,7 @@ runs: shell: bash env: ASSIGNEE_ID: ${{ steps.set-assignee-id.outputs.assignee-id }} - AUTOMATION_TASK_ID: ${{ steps.get-automation-subtask.outputs.automation-task-id }} + AUTOMATION_TASK_ID: ${{ steps.get-automation-subtask.outputs.asana_automation_task_id }} HTML_NOTES: ${{ inputs.html-notes }} TASK_NAME: ${{ inputs.task-name }} WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} @@ -106,7 +104,7 @@ runs: env: ASANA_ACCESS_TOKEN: ${{ inputs.access-token }} ASSIGNEE_ID: ${{ steps.set-assignee-id.outputs.assignee-id }} - TASK_ID: ${{ steps.get-automation-subtask.outputs.automation-task-id }} + TASK_ID: ${{ steps.get-automation-subtask.outputs.asana_automation_task_id }} PAYLOAD_BASE64: ${{ steps.process-template-payload.outputs.payload-base64 || steps.process-notes-payload.outputs.payload-base64 || steps.process-html-notes-payload.outputs.payload-base64 }} run: | # Create a subtask and retrieve its ID from the response (.data.gid) to return as an output diff --git a/.github/actions/asana-get-release-automation-subtask-id/action.yml b/.github/actions/asana-get-release-automation-subtask-id/action.yml deleted file mode 100644 index 0b50aa37bc..0000000000 --- a/.github/actions/asana-get-release-automation-subtask-id/action.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Get Release Automation subtask ID -description: Finds 'Automation' subtask in the release task and returns its ID -inputs: - access-token: - description: "Asana access token" - required: true - type: string - task-url: - description: "Asana release task URL" - required: true - type: string -outputs: - automation-task-id: - description: "Automation task ID" - value: ${{ steps.extract-automation-task-id.outputs.automation-task-id }} - assignee-id: - description: "Release task assignee ID" - value: ${{ steps.extract-assignee-id.outputs.asana_assignee_id }} -runs: - using: "composite" - steps: - - id: extract-task-id - shell: bash - run: bundle exec fastlane run asana_extract_task_id task_url:"${{ inputs.task-url }}" - - - id: extract-assignee-id - shell: bash - env: - ASANA_ACCESS_TOKEN: ${{ inputs.access-token }} - run: bundle exec fastlane run asana_extract_task_assignee task_id:"${{ steps.extract-task-id.outputs.asana_task_id }}" - - - id: extract-automation-task-id - run: | - task_id="${{ steps.extract-task-id.outputs.asana_task_id }}" - automation_task_id=$(curl -fLSs "https://app.asana.com/api/1.0/tasks/${task_id}/subtasks?opt_fields=name" \ - -H "Authorization: Bearer ${{ inputs.access-token }}" \ - | jq -r '.data[] | select(.name=="Automation").gid') - echo "automation-task-id=${automation_task_id}" >> $GITHUB_OUTPUT - shell: bash diff --git a/.github/actions/asana-log-message/action.yml b/.github/actions/asana-log-message/action.yml index e00cbd2a6d..fac2c2b2d3 100644 --- a/.github/actions/asana-log-message/action.yml +++ b/.github/actions/asana-log-message/action.yml @@ -24,24 +24,22 @@ runs: using: "composite" steps: - id: get-automation-subtask - uses: ./.github/actions/asana-get-release-automation-subtask-id - with: - access-token: ${{ inputs.access-token }} - task-url: ${{ inputs.task-url }} + shell: bash + env: + ASANA_ACCESS_TOKEN: ${{ inputs.access-token }} + run: bundle exec fastlane run asana_get_release_automation_subtask_id task_url:"${{ inputs.task-url }}" - id: get-asana-user-id + shell: bash if: github.event_name != 'schedule' - uses: duckduckgo/apple-toolbox/actions/asana-get-user-id-for-github-handle@main - with: - access-token: ${{ inputs.access-token }} - github-handle: ${{ github.actor }} + run: bundle exec fastlane run asana_get_user_id_for_github_handle github_handle:"${{ github.actor }}" - id: add-colaborator shell: bash env: ASANA_ACCESS_TOKEN: ${{ inputs.access-token }} - ASSIGNEE_ID: ${{ steps.get-asana-user-id.outputs.user-id || steps.get-automation-subtask.outputs.assignee-id }} - TASK_ID: ${{ steps.get-automation-subtask.outputs.automation-task-id }} + ASSIGNEE_ID: ${{ steps.get-asana-user-id.outputs.asana_user_id || steps.get-automation-subtask.outputs.asana_assignee_id }} + TASK_ID: ${{ steps.get-automation-subtask.outputs.asana_automation_task_id }} run: | return_code=$(curl -fLSs "https://app.asana.com/api/1.0/tasks/${TASK_ID}/addFollowers" \ -H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}" \ @@ -59,6 +57,6 @@ runs: uses: ./.github/actions/asana-add-comment with: access-token: ${{ inputs.access-token }} - task-id: ${{ steps.get-automation-subtask.outputs.automation-task-id }} + task-id: ${{ steps.get-automation-subtask.outputs.asana_automation_task_id }} comment: ${{ inputs.comment }} template-name: ${{ inputs.template-name }} diff --git a/.github/workflows/build_notarized.yml b/.github/workflows/build_notarized.yml index 1db6405485..fc24455ba4 100644 --- a/.github/workflows/build_notarized.yml +++ b/.github/workflows/build_notarized.yml @@ -352,11 +352,12 @@ jobs: - name: Upload DMG to Asana if: ${{ env.upload-to == 'asana' }} - uses: ./.github/actions/asana-upload - with: - access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} - file-name: ${{ github.workspace }}/duckduckgo-${{ env.app-version }}.dmg - task-id: ${{ steps.task-id.outputs.asana_task_id }} + env: + ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }} + run: | + bundle exec fastlane run asana_upload \ + file_name:"${{ github.workspace }}/duckduckgo-${{ env.app-version }}.dmg" \ + task_id:"${{ steps.task-id.outputs.asana_task_id }}" mattermost: diff --git a/.github/workflows/code_freeze.yml b/.github/workflows/code_freeze.yml index f80bd09e71..9d66ce6cc0 100644 --- a/.github/workflows/code_freeze.yml +++ b/.github/workflows/code_freeze.yml @@ -47,16 +47,14 @@ jobs: - name: Get Asana user ID id: get-asana-user-id - uses: duckduckgo/apple-toolbox/actions/asana-get-user-id-for-github-handle@main - with: - access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} - github-handle: ${{ github.actor }} + shell: bash + run: bundle exec fastlane run asana_get_user_id_for_github_handle github_handle:"${{ github.actor }}" - name: Create release task id: create_release_task env: ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }} - ASSIGNEE_ID: ${{ steps.get-asana-user-id.outputs.user-id }} + ASSIGNEE_ID: ${{ steps.get-asana-user-id.outputs.asana_user_id }} run: | version="$(echo ${{ steps.make_release_branch.outputs.release_branch_name }} | cut -d '/' -f 2)" task_name="macOS App Release $version" diff --git a/.github/workflows/publish_dmg_release.yml b/.github/workflows/publish_dmg_release.yml index fa476c8ee5..5fdc98c79e 100644 --- a/.github/workflows/publish_dmg_release.yml +++ b/.github/workflows/publish_dmg_release.yml @@ -345,29 +345,32 @@ jobs: - name: Upload patch to the Asana task id: upload-patch if: success() - uses: ./.github/actions/asana-upload - with: - access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} - file-name: ${{ env.SPARKLE_DIR }}/${{ steps.appcast.outputs.appcast-patch-name }} - task-id: ${{ steps.create-task.outputs.new-task-id }} + env: + ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }} + run: | + bundle exec fastlane run asana_upload \ + file_name:"${{ env.SPARKLE_DIR }}/${{ steps.appcast.outputs.appcast-patch-name }}" \ + task_id:"${{ steps.create-task.outputs.new-task-id }}" - name: Upload old appcast file to the Asana task id: upload-old-appcast if: success() - uses: ./.github/actions/asana-upload - with: - access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} - file-name: ${{ env.OLD_APPCAST_NAME }} - task-id: ${{ steps.create-task.outputs.new-task-id }} + env: + ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }} + run: | + bundle exec fastlane run asana_upload \ + file_name:"${{ env.OLD_APPCAST_NAME }}" \ + task_id:"${{ steps.create-task.outputs.new-task-id }}" - name: Upload release notes to the Asana task id: upload-release-notes if: success() - uses: ./.github/actions/asana-upload - with: - access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} - file-name: ${{ env.RELEASE_NOTES_FILE }} - task-id: ${{ steps.create-task.outputs.new-task-id }} + env: + ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }} + run: | + bundle exec fastlane run asana_upload \ + file_name:"${{ env.RELEASE_NOTES_FILE }}" \ + task_id:"${{ steps.create-task.outputs.new-task-id }}" - name: Report status if: always() diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 527880d7b3..a2a5610bdd 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 251 +CURRENT_PROJECT_VERSION = 253 diff --git a/Configuration/Version.xcconfig b/Configuration/Version.xcconfig index 5f34e807bc..7510b879b0 100644 --- a/Configuration/Version.xcconfig +++ b/Configuration/Version.xcconfig @@ -1 +1 @@ -MARKETING_VERSION = 1.104.0 +MARKETING_VERSION = 1.105.0 diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 7ff66dbce8..fcad0f7799 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -233,6 +233,28 @@ 317295D52AF058D3002C3206 /* MockWaitlistFeatureSetupHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317295D12AF058D3002C3206 /* MockWaitlistFeatureSetupHandler.swift */; }; 3184AC6D288F29D800C35E4B /* BadgeNotificationAnimationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3184AC6C288F29D800C35E4B /* BadgeNotificationAnimationModel.swift */; }; 3184AC6F288F2A1100C35E4B /* CookieNotificationAnimationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3184AC6E288F2A1100C35E4B /* CookieNotificationAnimationModel.swift */; }; + 3199AF6F2C80734A003AEBDC /* DuckPlayerOnboardingDecider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF632C80734A003AEBDC /* DuckPlayerOnboardingDecider.swift */; }; + 3199AF702C80734A003AEBDC /* DuckPlayerOnboardingDecider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF632C80734A003AEBDC /* DuckPlayerOnboardingDecider.swift */; }; + 3199AF732C80734A003AEBDC /* DuckPlayerOnboardingModalManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF652C80734A003AEBDC /* DuckPlayerOnboardingModalManager.swift */; }; + 3199AF742C80734A003AEBDC /* DuckPlayerOnboardingModalManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF652C80734A003AEBDC /* DuckPlayerOnboardingModalManager.swift */; }; + 3199AF752C80734A003AEBDC /* DuckPlayerOnboardingModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF662C80734A003AEBDC /* DuckPlayerOnboardingModalView.swift */; }; + 3199AF762C80734A003AEBDC /* DuckPlayerOnboardingModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF662C80734A003AEBDC /* DuckPlayerOnboardingModalView.swift */; }; + 3199AF772C80734A003AEBDC /* DuckPlayerOnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF672C80734A003AEBDC /* DuckPlayerOnboardingViewController.swift */; }; + 3199AF782C80734A003AEBDC /* DuckPlayerOnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF672C80734A003AEBDC /* DuckPlayerOnboardingViewController.swift */; }; + 3199AF792C80734A003AEBDC /* DuckPlayerOnboardingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF682C80734A003AEBDC /* DuckPlayerOnboardingViewModel.swift */; }; + 3199AF7A2C80734A003AEBDC /* DuckPlayerOnboardingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF682C80734A003AEBDC /* DuckPlayerOnboardingViewModel.swift */; }; + 3199AF7B2C80734A003AEBDC /* DuckPlayerOnboardingLocationValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF6A2C80734A003AEBDC /* DuckPlayerOnboardingLocationValidator.swift */; }; + 3199AF7C2C80734A003AEBDC /* DuckPlayerOnboardingLocationValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF6A2C80734A003AEBDC /* DuckPlayerOnboardingLocationValidator.swift */; }; + 3199AF7D2C80734A003AEBDC /* TabModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF6C2C80734A003AEBDC /* TabModal.swift */; }; + 3199AF7E2C80734A003AEBDC /* TabModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF6C2C80734A003AEBDC /* TabModal.swift */; }; + 3199AF7F2C80734A003AEBDC /* TabModalManageable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF6D2C80734A003AEBDC /* TabModalManageable.swift */; }; + 3199AF802C80734A003AEBDC /* TabModalManageable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF6D2C80734A003AEBDC /* TabModalManageable.swift */; }; + 3199AF832C80736C003AEBDC /* DuckPlayerOnboardingLocationValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF812C80736B003AEBDC /* DuckPlayerOnboardingLocationValidatorTests.swift */; }; + 3199AF842C80736C003AEBDC /* DuckPlayerOnboardingLocationValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF812C80736B003AEBDC /* DuckPlayerOnboardingLocationValidatorTests.swift */; }; + 3199AF852C80736C003AEBDC /* DefaultDuckPlayerOnboardingDeciderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF822C80736B003AEBDC /* DefaultDuckPlayerOnboardingDeciderTests.swift */; }; + 3199AF862C80736C003AEBDC /* DefaultDuckPlayerOnboardingDeciderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF822C80736B003AEBDC /* DefaultDuckPlayerOnboardingDeciderTests.swift */; }; + 3199AF882C8073CB003AEBDC /* DuckPlayerOnboardingTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF872C8073CA003AEBDC /* DuckPlayerOnboardingTabExtension.swift */; }; + 3199AF892C8073CB003AEBDC /* DuckPlayerOnboardingTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF872C8073CA003AEBDC /* DuckPlayerOnboardingTabExtension.swift */; }; 31A2FD172BAB41C500D0E741 /* DataBrokerProtectionFeatureGatekeeperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A2FD162BAB41C500D0E741 /* DataBrokerProtectionFeatureGatekeeperTests.swift */; }; 31A2FD182BAB43BA00D0E741 /* DataBrokerProtectionFeatureGatekeeperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A2FD162BAB41C500D0E741 /* DataBrokerProtectionFeatureGatekeeperTests.swift */; }; 31A3A4E32B0C115F0021063C /* DataBrokerProtection in Frameworks */ = {isa = PBXBuildFile; productRef = 31A3A4E22B0C115F0021063C /* DataBrokerProtection */; }; @@ -2575,10 +2597,12 @@ BB7B5F992C4ED73800BA4AF8 /* BookmarksSearchAndSortMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB7B5F972C4ED73800BA4AF8 /* BookmarksSearchAndSortMetrics.swift */; }; BBB881882C4029BA001247C6 /* BookmarkListTreeControllerSearchDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB881872C4029BA001247C6 /* BookmarkListTreeControllerSearchDataSource.swift */; }; BBB881892C4029BA001247C6 /* BookmarkListTreeControllerSearchDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB881872C4029BA001247C6 /* BookmarkListTreeControllerSearchDataSource.swift */; }; + BBBB65402C77BB9400E69AC6 /* BookmarkSearchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBBB653F2C77BB9400E69AC6 /* BookmarkSearchTests.swift */; }; BBBEE1BF2C4FF63600035ABA /* SortBookmarksViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBBEE1BE2C4FF63600035ABA /* SortBookmarksViewModelTests.swift */; }; BBBEE1C02C4FF63600035ABA /* SortBookmarksViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBBEE1BE2C4FF63600035ABA /* SortBookmarksViewModelTests.swift */; }; BBC063E82C5A9E4B007BDC18 /* BookmarkManagementDetailViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBC063E72C5A9E4B007BDC18 /* BookmarkManagementDetailViewModelTests.swift */; }; BBC063E92C5A9E4B007BDC18 /* BookmarkManagementDetailViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBC063E72C5A9E4B007BDC18 /* BookmarkManagementDetailViewModelTests.swift */; }; + BBCD467A2C8643EC004DB483 /* XCUIApplicationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBCD46792C8643EC004DB483 /* XCUIApplicationExtension.swift */; }; BBE013EA2C5BFD660025F2C6 /* BookmarksEmptyStateContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBE013E92C5BFD660025F2C6 /* BookmarksEmptyStateContent.swift */; }; BBE013EB2C5BFD660025F2C6 /* BookmarksEmptyStateContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBE013E92C5BFD660025F2C6 /* BookmarksEmptyStateContent.swift */; }; BBFB727F2C48047C0088884C /* SortBookmarksViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBFB727E2C48047C0088884C /* SortBookmarksViewModel.swift */; }; @@ -3148,6 +3172,17 @@ 3184AC6E288F2A1100C35E4B /* CookieNotificationAnimationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieNotificationAnimationModel.swift; sourceTree = ""; }; 3192A2702A4C4E330084EA89 /* DataBrokerProtection */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = DataBrokerProtection; sourceTree = ""; }; 3192EC872A4DCF21001E97A5 /* DBPHomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBPHomeViewController.swift; sourceTree = ""; }; + 3199AF632C80734A003AEBDC /* DuckPlayerOnboardingDecider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DuckPlayerOnboardingDecider.swift; sourceTree = ""; }; + 3199AF652C80734A003AEBDC /* DuckPlayerOnboardingModalManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DuckPlayerOnboardingModalManager.swift; sourceTree = ""; }; + 3199AF662C80734A003AEBDC /* DuckPlayerOnboardingModalView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DuckPlayerOnboardingModalView.swift; sourceTree = ""; }; + 3199AF672C80734A003AEBDC /* DuckPlayerOnboardingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DuckPlayerOnboardingViewController.swift; sourceTree = ""; }; + 3199AF682C80734A003AEBDC /* DuckPlayerOnboardingViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DuckPlayerOnboardingViewModel.swift; sourceTree = ""; }; + 3199AF6A2C80734A003AEBDC /* DuckPlayerOnboardingLocationValidator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DuckPlayerOnboardingLocationValidator.swift; sourceTree = ""; }; + 3199AF6C2C80734A003AEBDC /* TabModal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabModal.swift; sourceTree = ""; }; + 3199AF6D2C80734A003AEBDC /* TabModalManageable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabModalManageable.swift; sourceTree = ""; }; + 3199AF812C80736B003AEBDC /* DuckPlayerOnboardingLocationValidatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DuckPlayerOnboardingLocationValidatorTests.swift; sourceTree = ""; }; + 3199AF822C80736B003AEBDC /* DefaultDuckPlayerOnboardingDeciderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultDuckPlayerOnboardingDeciderTests.swift; sourceTree = ""; }; + 3199AF872C8073CA003AEBDC /* DuckPlayerOnboardingTabExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DuckPlayerOnboardingTabExtension.swift; sourceTree = ""; }; 3199C6F82AF94F5B002A7BA1 /* DataBrokerProtectionFeatureDisabler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionFeatureDisabler.swift; sourceTree = ""; }; 3199C6FC2AF97367002A7BA1 /* DataBrokerProtectionAppEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionAppEvents.swift; sourceTree = ""; }; 31A2FD162BAB41C500D0E741 /* DataBrokerProtectionFeatureGatekeeperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionFeatureGatekeeperTests.swift; sourceTree = ""; }; @@ -4316,8 +4351,10 @@ BB5789712B2CA70F0009DFE2 /* DataBrokerProtectionSubscriptionEventHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionSubscriptionEventHandler.swift; sourceTree = ""; }; BB7B5F972C4ED73800BA4AF8 /* BookmarksSearchAndSortMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksSearchAndSortMetrics.swift; sourceTree = ""; }; BBB881872C4029BA001247C6 /* BookmarkListTreeControllerSearchDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkListTreeControllerSearchDataSource.swift; sourceTree = ""; }; + BBBB653F2C77BB9400E69AC6 /* BookmarkSearchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkSearchTests.swift; sourceTree = ""; }; BBBEE1BE2C4FF63600035ABA /* SortBookmarksViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortBookmarksViewModelTests.swift; sourceTree = ""; }; BBC063E72C5A9E4B007BDC18 /* BookmarkManagementDetailViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkManagementDetailViewModelTests.swift; sourceTree = ""; }; + BBCD46792C8643EC004DB483 /* XCUIApplicationExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCUIApplicationExtension.swift; sourceTree = ""; }; BBE013E92C5BFD660025F2C6 /* BookmarksEmptyStateContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksEmptyStateContent.swift; sourceTree = ""; }; BBFB727E2C48047C0088884C /* SortBookmarksViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortBookmarksViewModel.swift; sourceTree = ""; }; BBFF355C2C4AF26200DA3289 /* BookmarksSortModeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksSortModeTests.swift; sourceTree = ""; }; @@ -4967,6 +5004,36 @@ path = DBP; sourceTree = ""; }; + 3199AF692C80734A003AEBDC /* DuckPlayerOnboardingModal */ = { + isa = PBXGroup; + children = ( + 3199AF632C80734A003AEBDC /* DuckPlayerOnboardingDecider.swift */, + 3199AF652C80734A003AEBDC /* DuckPlayerOnboardingModalManager.swift */, + 3199AF662C80734A003AEBDC /* DuckPlayerOnboardingModalView.swift */, + 3199AF672C80734A003AEBDC /* DuckPlayerOnboardingViewController.swift */, + 3199AF682C80734A003AEBDC /* DuckPlayerOnboardingViewModel.swift */, + ); + path = DuckPlayerOnboardingModal; + sourceTree = ""; + }; + 3199AF6B2C80734A003AEBDC /* Onboarding */ = { + isa = PBXGroup; + children = ( + 3199AF692C80734A003AEBDC /* DuckPlayerOnboardingModal */, + 3199AF6A2C80734A003AEBDC /* DuckPlayerOnboardingLocationValidator.swift */, + ); + path = Onboarding; + sourceTree = ""; + }; + 3199AF6E2C80734A003AEBDC /* TabModal */ = { + isa = PBXGroup; + children = ( + 3199AF6C2C80734A003AEBDC /* TabModal.swift */, + 3199AF6D2C80734A003AEBDC /* TabModalManageable.swift */, + ); + path = TabModal; + sourceTree = ""; + }; 31A2FD152BAB419400D0E741 /* DBP */ = { isa = PBXGroup; children = ( @@ -4998,6 +5065,8 @@ 31F28C4B28C8EE9000119F70 /* YoutubePlayer */ = { isa = PBXGroup; children = ( + 3199AF6B2C80734A003AEBDC /* Onboarding */, + 3199AF6E2C80734A003AEBDC /* TabModal */, 37F19A6928E2F2D000740DC6 /* DuckPlayer.swift */, 31F28C5228C8EECA00119F70 /* DuckURLSchemeHandler.swift */, 315AA06F28CA5CC800200030 /* YoutubePlayerNavigationHandler.swift */, @@ -5065,6 +5134,8 @@ 376718FE28E58504003A2A15 /* YoutubePlayer */ = { isa = PBXGroup; children = ( + 3199AF822C80736B003AEBDC /* DefaultDuckPlayerOnboardingDeciderTests.swift */, + 3199AF812C80736B003AEBDC /* DuckPlayerOnboardingLocationValidatorTests.swift */, 3714B1E828EDBAAB0056C57A /* DuckPlayerTests.swift */, 567DA94429E95C3F008AC5EE /* YoutubeOverlayUserScriptTests.swift */, ); @@ -6265,6 +6336,7 @@ EE9D81C22BC57A3700338BE3 /* StateRestorationTests.swift */, 7B4CE8E626F02134009134B1 /* TabBarTests.swift */, 56A054522C2592CE007D8FAB /* OnboardingUITests.swift */, + BBBB653F2C77BB9400E69AC6 /* BookmarkSearchTests.swift */, ); path = UITests; sourceTree = ""; @@ -8196,6 +8268,7 @@ B6D574B12947224C008ED1B6 /* ContentBlockingTabExtension.swift */, B6DA06E32913ECEE00225DE2 /* ContextMenuManager.swift */, B6685E4129A61C460043D2EE /* DownloadsTabExtension.swift */, + 3199AF872C8073CA003AEBDC /* DuckPlayerOnboardingTabExtension.swift */, B6C416A6294A4AE500C4F2E7 /* DuckPlayerTabExtension.swift */, B6D574B329472253008ED1B6 /* FBProtectionTabExtension.swift */, B6C00ECC292F89D9009C73A6 /* FindInPageTabExtension.swift */, @@ -8701,6 +8774,7 @@ children = ( EE02D4192BB4609900DBE6B3 /* UITests.swift */, EEBCE6812BA444FA00B9DF00 /* XCUIElementExtension.swift */, + BBCD46792C8643EC004DB483 /* XCUIApplicationExtension.swift */, ); path = Common; sourceTree = ""; @@ -10070,6 +10144,7 @@ 7BCB90C32C1863BA008E3543 /* VPNControllerXPCClient+ConvenienceInitializers.swift in Sources */, EEC4A66E2B2C894F00F7C0AA /* VPNLocationPreferenceItemModel.swift in Sources */, 3706FA83293F65D500E42796 /* LazyLoadable.swift in Sources */, + 3199AF7C2C80734A003AEBDC /* DuckPlayerOnboardingLocationValidator.swift in Sources */, 3706FA85293F65D500E42796 /* KeyedCodingExtension.swift in Sources */, 3706FA87293F65D500E42796 /* DownloadListStore.swift in Sources */, 37197EAB2942443D00394917 /* WebViewContainerView.swift in Sources */, @@ -10278,6 +10353,7 @@ 3707C71F294B5D2900682A9F /* WKUserContentControllerExtension.swift in Sources */, 3706FB1A293F65D500E42796 /* OutlineSeparatorViewCell.swift in Sources */, 3706FB1B293F65D500E42796 /* SafariDataImporter.swift in Sources */, + 3199AF702C80734A003AEBDC /* DuckPlayerOnboardingDecider.swift in Sources */, 3706FB1D293F65D500E42796 /* StatisticsLoader.swift in Sources */, 56406D4C2C636A8900BF8FA2 /* SpecialPagesUserScriptExtension.swift in Sources */, 3793FDD829535EBA00A2E28F /* Assertions.swift in Sources */, @@ -10310,6 +10386,7 @@ 3706FB31293F65D500E42796 /* PinnedTabsHostingView.swift in Sources */, B6AFE6BC29A5D3F8002FF962 /* PrivacyDashboardTabExtension.swift in Sources */, 3706FB32293F65D500E42796 /* FirefoxBookmarksReader.swift in Sources */, + 3199AF7E2C80734A003AEBDC /* TabModal.swift in Sources */, 1D39E57B2C2C0F3700757339 /* ReleaseNotesUserScript.swift in Sources */, 3706FB33293F65D500E42796 /* DeviceIdleStateDetector.swift in Sources */, 1DB67F2A2B6FEB17003DF243 /* WebViewSnapshotRenderer.swift in Sources */, @@ -10327,6 +10404,7 @@ 3706FB3E293F65D500E42796 /* BookmarksBarViewModel.swift in Sources */, 3706FB3F293F65D500E42796 /* NSPopUpButtonView.swift in Sources */, F1FDC9392BF51F41006B1435 /* VPNSettings+Environment.swift in Sources */, + 3199AF762C80734A003AEBDC /* DuckPlayerOnboardingModalView.swift in Sources */, 1ED910D62B63BFB300936947 /* IdentityTheftRestorationPagesUserScript.swift in Sources */, 3706FB40293F65D500E42796 /* BookmarksContextMenu.swift in Sources */, 3706FB41293F65D500E42796 /* NavigationBarViewController.swift in Sources */, @@ -10359,6 +10437,7 @@ B6B5F5802B024105008DB58A /* DataImportSummaryView.swift in Sources */, 4B9DB0422A983B24000927DB /* WaitlistDialogView.swift in Sources */, 3706FB57293F65D500E42796 /* AppPrivacyConfigurationDataProvider.swift in Sources */, + 3199AF7A2C80734A003AEBDC /* DuckPlayerOnboardingViewModel.swift in Sources */, C1B1CBE22BE1915100B6049C /* DataImportShortcutsViewModel.swift in Sources */, 857E5AF62A790B7000FC0FB4 /* PixelExperiment.swift in Sources */, 9F33445F2BBFA77F0040CBEB /* BookmarksBarVisibilityManager.swift in Sources */, @@ -10599,11 +10678,13 @@ 31267C6B2B640C5200FEF811 /* DataBrokerProtectionAppEvents.swift in Sources */, 3706FBE4293F65D500E42796 /* FireAnimationView.swift in Sources */, 3706FBE5293F65D500E42796 /* FaviconUrlReference.swift in Sources */, + 3199AF802C80734A003AEBDC /* TabModalManageable.swift in Sources */, 3706FBE7293F65D500E42796 /* PasswordManagementItemListModel.swift in Sources */, 3706FBE8293F65D500E42796 /* SuggestionTableCellView.swift in Sources */, 3706FBE9293F65D500E42796 /* FireViewModel.swift in Sources */, B68D21D02ACBC9FD002DA3C2 /* ContentBlockerRulesManagerMock.swift in Sources */, BD7090D02C5182FB009EED82 /* UnifiedFeedbackFormView.swift in Sources */, + 3199AF742C80734A003AEBDC /* DuckPlayerOnboardingModalManager.swift in Sources */, 3706FEC6293F6F0600E42796 /* BWKeyStorage.swift in Sources */, 4B6785482AA8DE69008A5004 /* VPNUninstaller.swift in Sources */, 3706FBEC293F65D500E42796 /* EditableTextView.swift in Sources */, @@ -10798,6 +10879,7 @@ 3706FC61293F65D500E42796 /* NSAlertExtension.swift in Sources */, 3706FC62293F65D500E42796 /* ThirdPartyBrowser.swift in Sources */, 3706FC63293F65D500E42796 /* CircularProgressView.swift in Sources */, + 3199AF782C80734A003AEBDC /* DuckPlayerOnboardingViewController.swift in Sources */, 3706FC64293F65D500E42796 /* SuggestionContainer.swift in Sources */, C16127EF2BDFB46400966BB9 /* DataImportShortcutsView.swift in Sources */, 3706FC65293F65D500E42796 /* HomePageViewController.swift in Sources */, @@ -10843,6 +10925,7 @@ 3706FC82293F65D500E42796 /* PermissionAuthorizationPopover.swift in Sources */, 4BCBE4552BA7E16600FC75A1 /* NetworkProtectionSubscriptionEventHandler.swift in Sources */, 371209312C233D69003ADF3D /* RemoteMessagingStoreErrorHandling.swift in Sources */, + 3199AF892C8073CB003AEBDC /* DuckPlayerOnboardingTabExtension.swift in Sources */, 3706FC83293F65D500E42796 /* PopoverMessageViewController.swift in Sources */, 7B5A23702C46A116007213AC /* ExcludedDomainsModel.swift in Sources */, 9D9AE86E2AA76D1F0026E7DC /* LoginItem+NetworkProtection.swift in Sources */, @@ -11101,6 +11184,7 @@ 1DE03425298BC7F000CAB3D7 /* InternalUserDeciderStoreMock.swift in Sources */, 3706FE4A293F661700E42796 /* BookmarkManagedObjectTests.swift in Sources */, BBFF355E2C4AF26200DA3289 /* BookmarksSortModeTests.swift in Sources */, + 3199AF842C80736C003AEBDC /* DuckPlayerOnboardingLocationValidatorTests.swift in Sources */, EEC8EB402982CD550065AA39 /* JSAlertViewModelTests.swift in Sources */, BDA764922BC4E57200D0400C /* MockVPNLocationFormatter.swift in Sources */, 3706FE4B293F661700E42796 /* BookmarksHTMLImporterTests.swift in Sources */, @@ -11115,6 +11199,7 @@ 3706FE51293F661700E42796 /* SafariBookmarksReaderTests.swift in Sources */, 3706FE52293F661700E42796 /* FileSystemDSLTests.swift in Sources */, 3706FE53293F661700E42796 /* CoreDataEncryptionTests.swift in Sources */, + 3199AF862C80736C003AEBDC /* DefaultDuckPlayerOnboardingDeciderTests.swift in Sources */, 3706FE54293F661700E42796 /* PasteboardBookmarkTests.swift in Sources */, 3706FE55293F661700E42796 /* CBRCompileTimeReporterTests.swift in Sources */, B6E6BA172BA2CF60008AA7E1 /* SandboxTestToolNotifications.swift in Sources */, @@ -11467,7 +11552,9 @@ EE02D41C2BB460A600DBE6B3 /* BrowsingHistoryTests.swift in Sources */, EE02D41A2BB4609900DBE6B3 /* UITests.swift in Sources */, EE0429E02BA31D2F009EB20F /* FindInPageTests.swift in Sources */, + BBCD467A2C8643EC004DB483 /* XCUIApplicationExtension.swift in Sources */, EE02D4212BB460FE00DBE6B3 /* StringExtension.swift in Sources */, + BBBB65402C77BB9400E69AC6 /* BookmarkSearchTests.swift in Sources */, 56A054532C2592CE007D8FAB /* OnboardingUITests.swift in Sources */, EE9D81C32BC57A3700338BE3 /* StateRestorationTests.swift in Sources */, EEC7BE2E2BC6C09500F86835 /* AddressBarKeyboardShortcutsTests.swift in Sources */, @@ -11678,6 +11765,7 @@ F18826912BC0105800D9AC4F /* PixelDataStore.swift in Sources */, B69B503E2726A12500758A2B /* AtbParser.swift in Sources */, 37F19A6528E1B3FB00740DC6 /* PreferencesDuckPlayerView.swift in Sources */, + 3199AF7D2C80734A003AEBDC /* TabModal.swift in Sources */, 84F1C8DE2C774D4200716446 /* NSTableViewExtension.swift in Sources */, 7B7F5D212C526CE600826256 /* AddExcludedDomainView.swift in Sources */, 4B92929E26670D2A00AD2C21 /* BookmarkSidebarTreeController.swift in Sources */, @@ -11695,6 +11783,7 @@ AAC6881928626BF800D54247 /* RecentlyClosedTab.swift in Sources */, B688B4DF27420D290087BEAF /* PDFSearchTextMenuItemHandler.swift in Sources */, AA7E919728746BCC00AB6B62 /* HistoryMenu.swift in Sources */, + 3199AF752C80734A003AEBDC /* DuckPlayerOnboardingModalView.swift in Sources */, BB470EBB2C5A66D6002EE91D /* BookmarkManagementDetailViewModel.swift in Sources */, F4A6198C283CFFBB007F2080 /* ContentScopeFeatureFlagging.swift in Sources */, 85707F24276A332A00DC0649 /* OnboardingButtonStyles.swift in Sources */, @@ -11847,9 +11936,11 @@ B60293E62BA19ECD0033186B /* NetPPopoverManagerMock.swift in Sources */, B6B3E0E12657EA7A0040E0A2 /* NSScreenExtension.swift in Sources */, B65E6BA026D9F10600095F96 /* NSBezierPathExtension.swift in Sources */, + 3199AF882C8073CB003AEBDC /* DuckPlayerOnboardingTabExtension.swift in Sources */, 31ECDA112BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */, 4B4D60E02A0C875F00BCD287 /* Bundle+VPN.swift in Sources */, AA6820E425502F19005ED0D5 /* WebsiteDataStore.swift in Sources */, + 3199AF732C80734A003AEBDC /* DuckPlayerOnboardingModalManager.swift in Sources */, B6F9BDDC2B45B7EE00677B33 /* WebsiteInfo.swift in Sources */, 4B67854A2AA8DE75008A5004 /* VPNFeatureGatekeeper.swift in Sources */, 37A089FB2C510FE0003BB417 /* RemoteMessagingDebugMenu.swift in Sources */, @@ -11875,6 +11966,7 @@ 4B723E0F26B0006500E14D75 /* CSVParser.swift in Sources */, B63BDF7E27FDAA640072D75B /* PrivacyDashboardWebView.swift in Sources */, 37CD54CF27F2FDD100F1F7B9 /* AppearancePreferences.swift in Sources */, + 3199AF7B2C80734A003AEBDC /* DuckPlayerOnboardingLocationValidator.swift in Sources */, B6B1E87B26D381710062C350 /* DownloadListCoordinator.swift in Sources */, B68D21C82ACBC96D002DA3C2 /* MockPrivacyConfiguration.swift in Sources */, B647EFBB2922584B00BA628D /* AdClickAttributionTabExtension.swift in Sources */, @@ -11900,6 +11992,7 @@ AA5C1DD5285C780C0089850C /* RecentlyClosedCoordinator.swift in Sources */, AA88D14B252A557100980B4E /* URLRequestExtension.swift in Sources */, AA6197C6276B3168008396F0 /* FaviconHostReference.swift in Sources */, + 3199AF792C80734A003AEBDC /* DuckPlayerOnboardingViewModel.swift in Sources */, B6685E4229A61C470043D2EE /* DownloadsTabExtension.swift in Sources */, 4B8AC93B26B48ADF00879451 /* ASN1Parser.swift in Sources */, 856C98DF257014BD00A22F1F /* FileDownloadManager.swift in Sources */, @@ -11991,6 +12084,7 @@ 56A0541F2C1CA1F5007D8FAB /* OnboardingTabExtension.swift in Sources */, B63D466925BEB6C200874977 /* WKWebView+SessionState.swift in Sources */, B6F1B0222BCE5658005E863C /* BrokenSiteInfoTabExtension.swift in Sources */, + 3199AF7F2C80734A003AEBDC /* TabModalManageable.swift in Sources */, 4B4D60C02A0C848D00BCD287 /* NetworkProtectionControllerErrorStore.swift in Sources */, 4B723E1226B0006E00E14D75 /* DataImport.swift in Sources */, 7BE146072A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift in Sources */, @@ -12132,6 +12226,7 @@ 7B60AFFF2C51426A008E32A3 /* VPNURLEventHandler.swift in Sources */, D64A5FF82AEA5C2B00B6D6E7 /* HomeButtonMenuFactory.swift in Sources */, 37A6A8F62AFCCA59008580A3 /* FaviconsFetcherOnboardingViewController.swift in Sources */, + 3199AF6F2C80734A003AEBDC /* DuckPlayerOnboardingDecider.swift in Sources */, AA3F895324C18AD500628DDE /* SuggestionViewModel.swift in Sources */, 4B9292A326670D2A00AD2C21 /* BookmarkManagedObject.swift in Sources */, 4B723E1326B0007A00E14D75 /* CSVLoginExporter.swift in Sources */, @@ -12146,6 +12241,7 @@ 31F28C5128C8EEC500119F70 /* YoutubeOverlayUserScript.swift in Sources */, B6ABD0CA2BC03F610000EB69 /* SecurityScopedFileURLController.swift in Sources */, B6040856274B830F00680351 /* DictionaryExtension.swift in Sources */, + 3199AF772C80734A003AEBDC /* DuckPlayerOnboardingViewController.swift in Sources */, B684592725C93C0500DC17B6 /* Publishers.NestedObjectChanges.swift in Sources */, B6DA06E62913F39400225DE2 /* MenuItemSelectors.swift in Sources */, 3712092C2C23383C003ADF3D /* RemoteMessagingStoreErrorHandling.swift in Sources */, @@ -12442,6 +12538,7 @@ AAC9C01C24CB594C00AD1325 /* TabViewModelTests.swift in Sources */, 84DC715B2C1C1E9000033B8C /* UserDefaultsWrapperTests.swift in Sources */, 561D29CA2BDA752F007B91D0 /* MockAppearancePreferencesPersistor.swift in Sources */, + 3199AF832C80736C003AEBDC /* DuckPlayerOnboardingLocationValidatorTests.swift in Sources */, 56A0540D2C1C375D007D8FAB /* MockWindow.swift in Sources */, 567DA93F29E8045D008AC5EE /* MockEmailStorage.swift in Sources */, 37CD54B727F1B28A00F1F7B9 /* DefaultBrowserPreferencesTests.swift in Sources */, @@ -12677,6 +12774,7 @@ 376C4DB928A1A48A00CC0F5B /* FirePopoverViewModelTests.swift in Sources */, AAEC74B62642CC6A00C2EFBC /* HistoryStoringMock.swift in Sources */, AA652CB125DD825B009059CC /* LocalBookmarkStoreTests.swift in Sources */, + 3199AF852C80736C003AEBDC /* DefaultDuckPlayerOnboardingDeciderTests.swift in Sources */, B630794226731F5400DCEE41 /* WKDownloadMock.swift in Sources */, B6C0B24626E9CB190031CB7F /* RunLoopExtensionTests.swift in Sources */, 56D145F129E6F06D00E3488A /* MockBookmarkManager.swift in Sources */, @@ -13690,7 +13788,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 190.0.0; + version = 191.1.0; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index b9bc44fa1d..c77166b9c5 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -167,8 +167,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/TrackerRadarKit", "state" : { - "revision" : "1403e17eeeb8493b92fb9d11eb8c846bb9776581", - "version" : "2.1.2" + "revision" : "5de0a610a7927b638a5fd463a53032c9934a2c3b", + "version" : "3.0.0" } }, { diff --git a/DuckDuckGo/Application/AppConfigurationURLProvider.swift b/DuckDuckGo/Application/AppConfigurationURLProvider.swift index a0aa950a9b..188a95d698 100644 --- a/DuckDuckGo/Application/AppConfigurationURLProvider.swift +++ b/DuckDuckGo/Application/AppConfigurationURLProvider.swift @@ -58,7 +58,6 @@ struct AppConfigurationURLProvider: ConfigurationURLProviding { case .surrogates: return URL(string: "https://staticcdn.duckduckgo.com/surrogates.txt")! case .trackerDataSet: return URL(string: "https://staticcdn.duckduckgo.com/trackerblocking/v6/current/macos-tds.json")! // In archived repo, to be refactored shortly (https://staticcdn.duckduckgo.com/useragents/social_ctp_configuration.json) - case .FBConfig: return URL(string: "https://staticcdn.duckduckgo.com/useragents/")! case .remoteMessagingConfig: return RemoteMessagingClient.Constants.endpoint } } diff --git a/DuckDuckGo/Assets.xcassets/Colors/CookieConsentPrimaryButton.colorset/Contents.json b/DuckDuckGo/Assets.xcassets/Colors/DuckPlayerOnboardingPrimaryButton.colorset/Contents.json similarity index 100% rename from DuckDuckGo/Assets.xcassets/Colors/CookieConsentPrimaryButton.colorset/Contents.json rename to DuckDuckGo/Assets.xcassets/Colors/DuckPlayerOnboardingPrimaryButton.colorset/Contents.json diff --git a/DuckDuckGo/Assets.xcassets/Colors/CookieConsentPrimaryButtonPressed.colorset/Contents.json b/DuckDuckGo/Assets.xcassets/Colors/DuckPlayerOnboardingPrimaryButtonPressed.colorset/Contents.json similarity index 100% rename from DuckDuckGo/Assets.xcassets/Colors/CookieConsentPrimaryButtonPressed.colorset/Contents.json rename to DuckDuckGo/Assets.xcassets/Colors/DuckPlayerOnboardingPrimaryButtonPressed.colorset/Contents.json diff --git a/DuckDuckGo/Assets.xcassets/Colors/CookieConsentSecondaryButton.colorset/Contents.json b/DuckDuckGo/Assets.xcassets/Colors/DuckPlayerOnboardingSecondaryButton.colorset/Contents.json similarity index 100% rename from DuckDuckGo/Assets.xcassets/Colors/CookieConsentSecondaryButton.colorset/Contents.json rename to DuckDuckGo/Assets.xcassets/Colors/DuckPlayerOnboardingSecondaryButton.colorset/Contents.json diff --git a/DuckDuckGo/Assets.xcassets/Colors/CookieConsentSecondaryButtonPressed.colorset/Contents.json b/DuckDuckGo/Assets.xcassets/Colors/DuckPlayerOnboardingSecondaryButtonPressed.colorset/Contents.json similarity index 100% rename from DuckDuckGo/Assets.xcassets/Colors/CookieConsentSecondaryButtonPressed.colorset/Contents.json rename to DuckDuckGo/Assets.xcassets/Colors/DuckPlayerOnboardingSecondaryButtonPressed.colorset/Contents.json diff --git a/DuckDuckGo/Assets.xcassets/Images/DuckPlayer/Contents.json b/DuckDuckGo/Assets.xcassets/Images/DuckPlayer/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/DuckPlayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/DuckPlayer/DuckPlayerOnboardingModal.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/DuckPlayer/DuckPlayerOnboardingModal.imageset/Contents.json new file mode 100644 index 0000000000..35fadfe35d --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/DuckPlayer/DuckPlayerOnboardingModal.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "DuckPlayerConsentModal.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/DuckPlayer/DuckPlayerOnboardingModal.imageset/DuckPlayerConsentModal.pdf b/DuckDuckGo/Assets.xcassets/Images/DuckPlayer/DuckPlayerOnboardingModal.imageset/DuckPlayerConsentModal.pdf new file mode 100644 index 0000000000..6c039fc856 Binary files /dev/null and b/DuckDuckGo/Assets.xcassets/Images/DuckPlayer/DuckPlayerOnboardingModal.imageset/DuckPlayerConsentModal.pdf differ diff --git a/DuckDuckGo/Assets.xcassets/Images/DuckPlayer/DuckPlayerOnboardingModalDax.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/DuckPlayer/DuckPlayerOnboardingModalDax.imageset/Contents.json new file mode 100644 index 0000000000..b2115618cd --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/DuckPlayer/DuckPlayerOnboardingModalDax.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "DuckPlayerConsentModalDax.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/DuckPlayer/DuckPlayerOnboardingModalDax.imageset/DuckPlayerConsentModalDax.pdf b/DuckDuckGo/Assets.xcassets/Images/DuckPlayer/DuckPlayerOnboardingModalDax.imageset/DuckPlayerConsentModalDax.pdf new file mode 100644 index 0000000000..f35514d214 Binary files /dev/null and b/DuckDuckGo/Assets.xcassets/Images/DuckPlayer/DuckPlayerOnboardingModalDax.imageset/DuckPlayerConsentModalDax.pdf differ diff --git a/DuckDuckGo/Bookmarks/Model/BookmarkOutlineViewDataSource.swift b/DuckDuckGo/Bookmarks/Model/BookmarkOutlineViewDataSource.swift index c80c02f163..9a6d7b840c 100644 --- a/DuckDuckGo/Bookmarks/Model/BookmarkOutlineViewDataSource.swift +++ b/DuckDuckGo/Bookmarks/Model/BookmarkOutlineViewDataSource.swift @@ -264,6 +264,13 @@ final class BookmarkOutlineViewDataSource: NSObject, BookmarksOutlineViewDataSou } let destination = destinationNode.isRoot ? PseudoFolder.bookmarks : destinationNode.representedObject + + guard !isSearching || destination is BookmarkFolder else { return .none } + + if let destinationFolder = destination as? BookmarkFolder { + self.dragDestinationFolder = destinationFolder + } + let operation = dragDropManager.validateDrop(info, to: destination) self.dragDestinationFolder = (operation == .none || item == nil) ? nil : destinationNode.representedObject as? BookmarkFolder diff --git a/DuckDuckGo/Bookmarks/Services/BookmarksContextMenu.swift b/DuckDuckGo/Bookmarks/Services/BookmarksContextMenu.swift index 24e6b3ef28..18902d8122 100644 --- a/DuckDuckGo/Bookmarks/Services/BookmarksContextMenu.swift +++ b/DuckDuckGo/Bookmarks/Services/BookmarksContextMenu.swift @@ -143,7 +143,7 @@ extension BookmarksContextMenu { static func menuItems(for folder: BookmarkFolder, target: AnyObject?, forSearch: Bool, includeManageBookmarksItem: Bool) -> [NSMenuItem] { // disable "Open All" if no Bookmarks in folder - var hasBookmarks = folder.children.contains(where: { $0 is Bookmark }) + let hasBookmarks = folder.children.contains(where: { $0 is Bookmark }) var items = [ openInNewTabsMenuItem(folder: folder, target: target, enabled: hasBookmarks), openAllInNewWindowMenuItem(folder: folder, target: target, enabled: hasBookmarks), diff --git a/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift b/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift index bcef29a4c5..2783fe14b4 100644 --- a/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift +++ b/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift @@ -51,6 +51,7 @@ struct AddBookmarkPopoverView: View { otherActionTitle: UserText.delete, isOtherActionDisabled: false, otherAction: model.removeButtonAction, + isOtherActionTriggeredByEscKey: false, defaultActionTitle: UserText.done, isDefaultActionDisabled: model.isDefaultActionButtonDisabled, defaultAction: model.doneButtonAction diff --git a/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift b/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift index 208e47b3ec..fb670ee326 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift @@ -41,7 +41,9 @@ final class BookmarkListViewController: NSViewController { private lazy var stackView = NSStackView() private lazy var newBookmarkButton = MouseOverButton(image: .addBookmark, target: self, action: #selector(newBookmarkButtonClicked)) private lazy var newFolderButton = MouseOverButton(image: .addFolder, target: outlineView.menu, action: #selector(FolderMenuItemSelectors.newFolder)) + .withAccessibilityIdentifier("BookmarkListViewController.newFolderButton") private lazy var searchBookmarksButton = MouseOverButton(image: .searchBookmarks, target: self, action: #selector(searchBookmarkButtonClicked)) + .withAccessibilityIdentifier("BookmarkListViewController.searchBookmarksButton") private lazy var sortBookmarksButton = MouseOverButton(image: .bookmarkSortAsc, target: self, action: #selector(sortBookmarksButtonClicked)) private lazy var buttonsDivider = NSBox() @@ -53,10 +55,14 @@ final class BookmarkListViewController: NSViewController { private lazy var emptyState = NSView() private lazy var emptyStateTitle = NSTextField() + .withAccessibilityIdentifier(BookmarksEmptyStateContent.titleAccessibilityIdentifier) private lazy var emptyStateMessage = NSTextField() + .withAccessibilityIdentifier(BookmarksEmptyStateContent.descriptionAccessibilityIdentifier) private lazy var emptyStateImageView = NSImageView(image: .bookmarksEmpty) + .withAccessibilityIdentifier(BookmarksEmptyStateContent.imageAccessibilityIdentifier) private lazy var importButton = NSButton(title: UserText.importBookmarksButtonTitle, target: self, action: #selector(onImportClicked)) private lazy var searchBar = NSSearchField() + .withAccessibilityIdentifier("BookmarkListViewController.searchBar") private var boxDividerTopConstraint = NSLayoutConstraint() private let bookmarkManager: BookmarkManager @@ -158,6 +164,9 @@ final class BookmarkListViewController: NSViewController { boxDivider.setContentHuggingPriority(.defaultHigh, for: .vertical) boxDivider.translatesAutoresizingMaskIntoConstraints = false + // keep OutlineView menu declaration before buttons as it‘s used as target + outlineView.menu = BookmarksContextMenu(bookmarkManager: bookmarkManager, delegate: self) + stackView.orientation = .horizontal stackView.spacing = 4 stackView.setHuggingPriority(.defaultHigh, for: .horizontal) @@ -170,9 +179,6 @@ final class BookmarkListViewController: NSViewController { stackView.addArrangedSubview(buttonsDivider) stackView.addArrangedSubview(manageBookmarksButton) - // keep OutlineView menu declaration before the buttons as it‘s their target - outlineView.menu = BookmarksContextMenu(bookmarkManager: bookmarkManager, delegate: self) - newBookmarkButton.bezelStyle = .shadowlessSquare newBookmarkButton.cornerRadius = 4 newBookmarkButton.normalTintColor = .button @@ -254,7 +260,6 @@ final class BookmarkListViewController: NSViewController { outlineView.usesAutomaticRowHeights = true outlineView.target = self outlineView.action = #selector(handleClick) - outlineView.menu = BookmarksContextMenu(bookmarkManager: bookmarkManager, delegate: self) outlineView.dataSource = dataSource outlineView.delegate = dataSource @@ -452,7 +457,7 @@ final class BookmarkListViewController: NSViewController { expandFoldersAndScrollUntil(folder) outlineView.scrollToAdjustedPositionInOutlineView(folder) - guard let node = treeController.node(representing: folder) else { return } + guard let node = treeController.findNodeWithId(representing: folder) else { return } outlineView.highlight(node) } diff --git a/DuckDuckGo/Bookmarks/View/BookmarkManagementDetailViewController.swift b/DuckDuckGo/Bookmarks/View/BookmarkManagementDetailViewController.swift index f857da6728..8bcfe5d45a 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkManagementDetailViewController.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkManagementDetailViewController.swift @@ -45,14 +45,18 @@ final class BookmarkManagementDetailViewController: NSViewController, NSMenuItem .withAccessibilityIdentifier("BookmarkManagementDetailViewController.sortItemsButton") lazy var searchBar = NSSearchField() + .withAccessibilityIdentifier("BookmarkManagementDetailViewController.searchBar") private lazy var separator = NSBox() private lazy var scrollView = NSScrollView() private lazy var tableView = NSTableView() private lazy var emptyState = NSView() private lazy var emptyStateImageView = NSImageView(image: .bookmarksEmpty) + .withAccessibilityIdentifier(BookmarksEmptyStateContent.imageAccessibilityIdentifier) private lazy var emptyStateTitle = NSTextField() + .withAccessibilityIdentifier(BookmarksEmptyStateContent.titleAccessibilityIdentifier) private lazy var emptyStateMessage = NSTextField() + .withAccessibilityIdentifier(BookmarksEmptyStateContent.descriptionAccessibilityIdentifier) private lazy var importButton = NSButton(title: UserText.importBookmarksButtonTitle, target: self, action: #selector(onImportClicked)) weak var delegate: BookmarkManagementDetailViewControllerDelegate? @@ -473,6 +477,9 @@ extension BookmarkManagementDetailViewController: NSTableViewDelegate, NSTableVi proposedRow row: Int, proposedDropOperation dropOperation: NSTableView.DropOperation) -> NSDragOperation { let destination = destination(for: dropOperation, at: row) + + guard !isSearching || destination is BookmarkFolder else { return .none } + return dragDropManager.validateDrop(info, to: destination) } diff --git a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift index 78cdc6efcd..114ecb6858 100644 --- a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift +++ b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift @@ -51,6 +51,7 @@ struct AddEditBookmarkDialogView: ModalView { otherActionTitle: viewModel.bookmarkModel.cancelActionTitle, isOtherActionDisabled: viewModel.bookmarkModel.isOtherActionDisabled, otherAction: viewModel.bookmarkModel.cancel, + isOtherActionTriggeredByEscKey: true, defaultActionTitle: viewModel.bookmarkModel.defaultActionTitle, isDefaultActionDisabled: viewModel.bookmarkModel.isDefaultActionDisabled, defaultAction: viewModel.bookmarkModel.addOrSave diff --git a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkView.swift b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkView.swift index 8d34889432..7831248391 100644 --- a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkView.swift +++ b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkView.swift @@ -36,6 +36,7 @@ struct AddEditBookmarkView: View { let otherActionTitle: String let isOtherActionDisabled: Bool let otherAction: @MainActor (_ dismiss: () -> Void) -> Void + let isOtherActionTriggeredByEscKey: Bool let defaultActionTitle: String let isDefaultActionDisabled: Bool @@ -78,6 +79,7 @@ struct AddEditBookmarkView: View { viewState: .init(buttonsState), otherButtonAction: .init( title: otherActionTitle, + keyboardShortCut: isOtherActionTriggeredByEscKey ? .cancelAction : nil, isDisabled: isOtherActionDisabled, action: otherAction ), diff --git a/DuckDuckGo/Bookmarks/ViewModel/BookmarksEmptyStateContent.swift b/DuckDuckGo/Bookmarks/ViewModel/BookmarksEmptyStateContent.swift index 3a32c9c2ce..acbb8ee4ac 100644 --- a/DuckDuckGo/Bookmarks/ViewModel/BookmarksEmptyStateContent.swift +++ b/DuckDuckGo/Bookmarks/ViewModel/BookmarksEmptyStateContent.swift @@ -20,6 +20,18 @@ enum BookmarksEmptyStateContent { case noBookmarks case noSearchResults + static var titleAccessibilityIdentifier: String { + "BookmarksEmptyStateContent.emptyStateTitle" + } + + static var descriptionAccessibilityIdentifier: String { + "BookmarksEmptyStateContent.emptyStateMessage" + } + + static var imageAccessibilityIdentifier: String { + "BookmarksEmptyStateContent.emptyStateImageView" + } + var title: String { switch self { case .noBookmarks: return UserText.bookmarksEmptyStateTitle diff --git a/DuckDuckGo/BookmarksBar/View/BookmarksBarMenuPopover.swift b/DuckDuckGo/BookmarksBar/View/BookmarksBarMenuPopover.swift index 2de6a55def..5da88072c1 100644 --- a/DuckDuckGo/BookmarksBar/View/BookmarksBarMenuPopover.swift +++ b/DuckDuckGo/BookmarksBar/View/BookmarksBarMenuPopover.swift @@ -113,12 +113,12 @@ final class BookmarksBarMenuPopover: NSPopover { return frame } - /// close other BookmarkListPopover-s shown from the main window when opening a new one + /// close other `BookmarksBarMenuPopover`-s and `BookmarkListPopover`-s shown from the main window when opening a new one static func closeBookmarkListPopovers(shownIn window: NSWindow?, except popoverToKeep: BookmarksBarMenuPopover? = nil) { guard let window, // ignore when opening a submenu from another BookmarkListPopover !(window.contentViewController?.nextResponder is Self) else { return } - for case let .some(popover as Self) in (window.childWindows ?? []).map(\.contentViewController?.nextResponder) where popover !== popoverToKeep && popover.isShown { + for case let .some(popover as NSPopover) in (window.childWindows ?? []).map(\.contentViewController?.nextResponder) where popover !== popoverToKeep && popover.isShown { popover.close() } } diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 21443b6a1a..c397dcfa68 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -362,6 +362,15 @@ struct UserText { static let duckPlayerContingencyMessageBody = NSLocalizedString("duck-player.video-contingency-message", value: "Duck Player's functionality has been affected by recent changes to YouTube. We’re working to fix these issues and appreciate your understanding.", comment: "Message explaining to the user that Duck Player is not available") static let duckPlayerContingencyMessageCTA = NSLocalizedString("duck-player.video-contingency-cta", value: "Learn More", comment: "Button for the message explaining to the user that Duck Player is not available so the user can learn more") + static let duckPlayerOnboardingChoiceModalTitle = NSLocalizedString("duck-player.onboarding-choice-modal-title", value: "Drowning in ads on YouTube?", comment: "Title for a Duck Player onboarding modal screen") + static let duckPlayerOnboardingChoiceModalMessage = NSLocalizedString("duck-player.onboarding-choice-modal-message", value: "Duck Player lets you watch without targeted ads and comes free to use in DuckDuckGo.", comment: "Message for a Duck Player onboarding modal screen") + static let duckPlayerOnboardingChoiceModalCTAConfirm = NSLocalizedString("duck-player.onboarding-choice-modal-CTA-confirm", value: "Turn on Duck Player", comment: "Confirm Button to enable Duck Player. -Duck Player- should not be translated") + static let duckPlayerOnboardingChoiceModalCTADeny = NSLocalizedString("duck-player.onboarding-choice-modal-CTA-deny", value: "Not Now", comment: "Deny Button to enable Duck Player") + + static let duckPlayerOnboardingConfirmationModalTitle = NSLocalizedString("duck-player.onboarding-confirmation-modal-title", value: "All set!", comment: "Title for a Duck Player onboarding modal confirmation screen") + static let duckPlayerOnboardingConfirmationModalMessage = NSLocalizedString("duck-player.onboarding-confirmation-modal-message", value: "Pick a video to see Duck Player work its magic.", comment: "Message for a Duck Player onboarding modal confirmation screen") + static let duckPlayerOnboardingConfirmationModalCTAConfirm = NSLocalizedString("duck-player.onboarding-confirmation-modal-CTA-confirm", value: "Got it", comment: "Button to confirm on Duck Player onboarding modal confirmation screen") + static let gpcCheckboxTitle = NSLocalizedString("gpc.checkbox.title", value: "Enable Global Privacy Control", comment: "GPC settings checkbox title") static let gpcExplanation = NSLocalizedString("gpc.explanation", value: "Tells participating websites not to sell or share your data.", comment: "GPC explanation in settings") diff --git a/DuckDuckGo/Configuration/ConfigurationStore.swift b/DuckDuckGo/Configuration/ConfigurationStore.swift index 1d0c6bfe3b..af9f800413 100644 --- a/DuckDuckGo/Configuration/ConfigurationStore.swift +++ b/DuckDuckGo/Configuration/ConfigurationStore.swift @@ -33,7 +33,6 @@ final class ConfigurationStore: ConfigurationStoring { .surrogates: "surrogates.txt", .privacyConfiguration: "macos-config.json", .trackerDataSet: "tracker-radar.json", - .FBConfig: "social_ctp_configuration.json", .remoteMessagingConfig: "remote-messaging-config.json" ] @@ -134,7 +133,6 @@ final class ConfigurationStore: ConfigurationStoring { case .surrogates: return surrogatesEtag case .trackerDataSet: return trackerRadarEtag case .privacyConfiguration: return privacyConfigurationEtag - case .FBConfig: return FBConfigEtag case .remoteMessagingConfig: return remoteMessagingConfigEtag } } @@ -155,7 +153,6 @@ final class ConfigurationStore: ConfigurationStoring { case .surrogates: surrogatesEtag = etag case .trackerDataSet: trackerRadarEtag = etag case .privacyConfiguration: privacyConfigurationEtag = etag - case .FBConfig: FBConfigEtag = etag case .remoteMessagingConfig: remoteMessagingConfigEtag = etag } } @@ -211,7 +208,6 @@ final class ConfigurationStore: ConfigurationStoring { Logger.config.info("surrogatesEtag \(self.surrogatesEtag ?? "", privacy: .public)") Logger.config.info("trackerRadarEtag \(self.trackerRadarEtag ?? "", privacy: .public)") Logger.config.info("privacyConfigurationEtag \(self.privacyConfigurationEtag ?? "", privacy: .public)") - Logger.config.info("FBConfigEtag \(self.FBConfigEtag ?? "", privacy: .public)") Logger.config.info("remoteMessagingConfig \(self.remoteMessagingConfigEtag ?? "", privacy: .public)") } diff --git a/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift b/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift index 9e52626554..7988b7b423 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 = "\"e2e01f769d64b248bff516166ddb19f5\"" - public static let embeddedDataSHA = "aea5eeea59fba229fdf46a41b82ebc917593a5f2b86478a1c504c24df3632195" + public static let embeddedDataETag = "\"c44d102eca183ab66c46945a42d2d08c\"" + public static let embeddedDataSHA = "6a608de7ef93edbd8c700aac9a879cf6dd9f4cef9868c06433fe8fe28106e9af" } var embeddedDataEtag: String { diff --git a/DuckDuckGo/ContentBlocker/macos-config.json b/DuckDuckGo/ContentBlocker/macos-config.json index b013b6528e..72b0aa504d 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": 1724668148700, + "version": 1725043256685, "features": { "adClickAttribution": { "readme": "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/#3rd-party-tracker-loading-protection", @@ -86,9 +86,6 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } @@ -117,7 +114,7 @@ ] }, "state": "enabled", - "hash": "f480c22abb392f288fbf2caf186ce082" + "hash": "fa5f86bac5946c528cd6bc7449a2718a" }, "androidBrowserConfig": { "exceptions": [], @@ -293,9 +290,6 @@ { "domain": "bitsofwar.com" }, - { - "domain": "mitglieder.franzspitzer.de" - }, { "domain": "disneyplus.com" }, @@ -323,12 +317,6 @@ { "domain": "hertz.com" }, - { - "domain": "www.thrifty.com" - }, - { - "domain": "sports.tipico.de" - }, { "domain": "lotusbakeries.com" }, @@ -356,9 +344,6 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } @@ -374,10 +359,10 @@ "state": "enabled", "features": { "filterlistExperiment": { - "state": "disabled" + "state": "enabled" } }, - "hash": "47b8af08b78f08d4ac808469f1a2bd98" + "hash": "0f117021faa53765286cc70d041a5bc2" }, "autofillBreakageReporter": { "state": "disabled", @@ -397,9 +382,12 @@ "features": { "deduplicateLoginsOnImport": { "state": "enabled" + }, + "unknownUsernameCategorization": { + "state": "enabled" } }, - "hash": "1832dd4088cba6eaf3a9776a6fc9a1f5" + "hash": "4fb2ab2ceed78c92aac780bc7ae8d4a2" }, "breakageReporting": { "state": "disabled", @@ -416,14 +404,11 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], - "hash": "1cc5be3534e326e74083566308e726f4" + "hash": "37e0cf88badfc8b01b6394f0884502f6" }, "brokenSitePrompt": { "state": "disabled", @@ -1120,9 +1105,6 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "tinder.com" }, @@ -1146,7 +1128,7 @@ }, "state": "enabled", "minSupportedVersion": "1.93.0", - "hash": "ca0e8ea2cfbd1c60b0cbd08bc9545ff1" + "hash": "81933e08c573f41c7a7e1620de3e2dbd" }, "clickToPlay": { "exceptions": [ @@ -1162,9 +1144,6 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } @@ -1179,7 +1158,7 @@ } }, "state": "enabled", - "hash": "aa1e279f495037feb71c38538ab25479" + "hash": "f48fcbcedd692bef6e3279e323727b54" }, "clientBrandHint": { "exceptions": [], @@ -1222,14 +1201,11 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], - "hash": "51628b06bd28538540ce9e5262e42070" + "hash": "96b2f778bab196aa424e9c859ddea778" }, "cookie": { "settings": { @@ -1292,15 +1268,12 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], "state": "disabled", - "hash": "518de3f4cdd6f78e664c00ea2ec349cb" + "hash": "6b4d2cef180104c5c84f5687479b8492" }, "customUserAgent": { "settings": { @@ -1522,9 +1495,6 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "wunderground.com" }, @@ -4797,7 +4767,7 @@ ] }, "state": "enabled", - "hash": "10a59471aab6744f176b20577b7caacb" + "hash": "85f1dc0d2a2bcd309d8683c058b7aa52" }, "exceptionHandler": { "exceptions": [ @@ -4813,15 +4783,12 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], "state": "disabled", - "hash": "4818e6983b16ddfdd078a2565bc69859" + "hash": "be6751fe0307a7e1b9476f4d8b8d0aaf" }, "extendedOnboarding": { "exceptions": [], @@ -4846,14 +4813,11 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], - "hash": "429a0ad38c859268f48d96c4c46864c6" + "hash": "7f042650922da2636492e77ed1101bce" }, "fingerprintingBattery": { "exceptions": [ @@ -4872,15 +4836,12 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], "state": "disabled", - "hash": "f3d4db6f69a4da53cfb1de3ac601b3a1" + "hash": "b9993a70bb5d6bfba23b4ec797f8a684" }, "fingerprintingCanvas": { "settings": { @@ -4983,15 +4944,12 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], "state": "disabled", - "hash": "69e97b215250895325a279ae6203e6fe" + "hash": "49a3d497835bf5715aaaa73f87dd974f" }, "fingerprintingHardware": { "settings": { @@ -5061,9 +5019,6 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "airbnb.com" }, @@ -5102,7 +5057,7 @@ } ], "state": "enabled", - "hash": "e387aaf98e1ce90af9678f5a6c80020d" + "hash": "97e86f6982f80cebfd56915be1457595" }, "fingerprintingScreenSize": { "settings": { @@ -5154,15 +5109,12 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], "state": "disabled", - "hash": "b6db688f3bd599c6906efa98f70254ae" + "hash": "9c9c85ea83b8e7c6d07c20d1539a000f" }, "fingerprintingTemporaryStorage": { "exceptions": [ @@ -5187,15 +5139,12 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], "state": "disabled", - "hash": "92bfb7484b6c52115159ef47d5c82b44" + "hash": "2dc4c0ade2e385d208695d770002c725" }, "googleRejected": { "exceptions": [ @@ -5211,15 +5160,12 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], "state": "disabled", - "hash": "4818e6983b16ddfdd078a2565bc69859" + "hash": "be6751fe0307a7e1b9476f4d8b8d0aaf" }, "gpc": { "state": "enabled", @@ -5284,9 +5230,6 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } @@ -5300,7 +5243,7 @@ "privacy-test-pages.site" ] }, - "hash": "63d2e3b00421ee3c04e28fcca1b03245" + "hash": "1f76ccef190bbe46af6766d2efcb4da3" }, "harmfulApis": { "settings": { @@ -5414,15 +5357,12 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], "state": "disabled", - "hash": "8cb27af67989404f1651e164f90b7940" + "hash": "fb598c4167ff166d85dd49c701cc5579" }, "history": { "state": "disabled", @@ -5453,14 +5393,11 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], - "hash": "8e60b4c39599560b0bea1030299fb3a7" + "hash": "b47d255c6f836ecb7ae0b3e61cc2c025" }, "incontextSignup": { "exceptions": [], @@ -5509,9 +5446,6 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } @@ -5524,7 +5458,7 @@ ] }, "state": "enabled", - "hash": "4877b3fce635ba78a673e87943f53091" + "hash": "d14f6e3a9aa4139ee1d517016b59691e" }, "networkProtection": { "state": "enabled", @@ -5600,15 +5534,12 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], "state": "disabled", - "hash": "596714c5b23bcdd1e0181ca2af22001d" + "hash": "82088db85ca7f64418fbfd57db25ade1" }, "performanceMetrics": { "state": "enabled", @@ -5625,14 +5556,11 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], - "hash": "a81619b928b99f1d21c9d513faf69b30" + "hash": "6792064606a5a72c5cd44addb4d40bda" }, "phishingDetection": { "state": "internal", @@ -5649,9 +5577,6 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } @@ -5664,7 +5589,7 @@ "state": "internal" } }, - "hash": "a2ee20806fbb71157619760407ef7623" + "hash": "9a9143022e6cc8976461b337abfa81a1" }, "pluginPointFocusedViewPlugin": { "state": "disabled", @@ -5801,15 +5726,12 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], "state": "disabled", - "hash": "9e37b1b1111a12703ba85b825a5ee6cf" + "hash": "138c3b2409f6b3bf967b804ab9bf2ce2" }, "remoteMessaging": { "state": "enabled", @@ -5831,9 +5753,6 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } @@ -5841,7 +5760,7 @@ "settings": { "windowInMs": 0 }, - "hash": "f37136d479956daf56bcd58e02eee96a" + "hash": "baf19d9e0f506ed09f46c95b1849adee" }, "runtimeChecks": { "state": "disabled", @@ -5858,15 +5777,12 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], "settings": {}, - "hash": "8b903028eea1ff475f6a25035e428558" + "hash": "dfede9f06b9e322e198736703d013d15" }, "serviceworkerInitiatedRequests": { "exceptions": [ @@ -5882,15 +5798,12 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], "state": "disabled", - "hash": "4818e6983b16ddfdd078a2565bc69859" + "hash": "be6751fe0307a7e1b9476f4d8b8d0aaf" }, "sslCertificates": { "state": "enabled", @@ -6899,6 +6812,7 @@ { "rule": "eccmp.com/sts/scripts/conversen-SDK.js", "domains": [ + "citi.com", "pch.com" ] } @@ -9324,14 +9238,11 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], - "hash": "2014f5e979fe2af4295fc99f78e8b252" + "hash": "896d7fc02c7f3f523fdf48e775ce30ef" }, "trackingCookies1p": { "settings": { @@ -9353,15 +9264,12 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], "state": "disabled", - "hash": "af648fa280cea7a9d0d10c90cd704b60" + "hash": "763f56424b0827b5731927a043219912" }, "trackingCookies3p": { "settings": { @@ -9380,15 +9288,12 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], "state": "disabled", - "hash": "596714c5b23bcdd1e0181ca2af22001d" + "hash": "82088db85ca7f64418fbfd57db25ade1" }, "trackingParameters": { "exceptions": [ @@ -9407,9 +9312,6 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } @@ -9445,7 +9347,7 @@ }, "state": "enabled", "minSupportedVersion": "0.22.3", - "hash": "a2e7e95c0788ff99d03d0b9887416fbf" + "hash": "d4fd05626079512387bd1fc0de585cb9" }, "userAgentRotation": { "settings": { @@ -9464,15 +9366,12 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], "state": "disabled", - "hash": "66f96a6be1339c70b110cef3abd09a98" + "hash": "9225b8785d6973db37abde99d81d219c" }, "voiceSearch": { "exceptions": [], @@ -9498,9 +9397,6 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "lastpass.com" }, @@ -9766,7 +9662,7 @@ } ] }, - "hash": "fcfba000dbe8a50227bfcfdc0c0ff90c" + "hash": "d1b128e9ff49486fb7122c70fef0ff51" }, "webViewBlobDownload": { "exceptions": [], @@ -9803,6 +9699,11 @@ "state": "disabled", "hash": "728493ef7a1488e4781656d3f9db84aa" }, + "windowsWebviewFailures": { + "exceptions": [], + "state": "disabled", + "hash": "728493ef7a1488e4781656d3f9db84aa" + }, "windowsDownloadLink": { "exceptions": [], "state": "disabled", diff --git a/DuckDuckGo/DBP/DBPHomeViewController.swift b/DuckDuckGo/DBP/DBPHomeViewController.swift index 6da4daef5e..9638418afd 100644 --- a/DuckDuckGo/DBP/DBPHomeViewController.swift +++ b/DuckDuckGo/DBP/DBPHomeViewController.swift @@ -50,7 +50,8 @@ final class DBPHomeViewController: NSViewController { credentialsSaving: false, passwordGeneration: false, inlineIconCredentials: false, - thirdPartyCredentialsProvider: false) + thirdPartyCredentialsProvider: false, + unknownUsernameCategorization: false) let isGPCEnabled = WebTrackingProtectionPreferences.shared.isGPCEnabled let sessionKey = UUID().uuidString diff --git a/DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift b/DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift index 11919d7bd0..fd9e360536 100644 --- a/DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift +++ b/DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift @@ -32,6 +32,9 @@ public enum FeatureFlag: String { // https://app.asana.com/0/1206488453854252/1207136666798700/f case freemiumPIR + + // https://app.asana.com/0/1201462886803403/1208030658792310/f + case unknownUsernameCategorization } extension FeatureFlag: FeatureFlagSourceProviding { @@ -45,6 +48,8 @@ extension FeatureFlag: FeatureFlagSourceProviding { return .remoteReleasable(.subfeature(sslCertificatesSubfeature.allowBypass)) case .deduplicateLoginsOnImport: return .remoteReleasable(.subfeature(AutofillSubfeature.deduplicateLoginsOnImport)) + case .unknownUsernameCategorization: + return .remoteReleasable(.subfeature(AutofillSubfeature.unknownUsernameCategorization)) case .freemiumPIR: return .remoteDevelopment(.subfeature(DBPSubfeature.freemium)) } diff --git a/DuckDuckGo/Menus/MainMenu.swift b/DuckDuckGo/Menus/MainMenu.swift index fb8cd22668..ca6441e4c1 100644 --- a/DuckDuckGo/Menus/MainMenu.swift +++ b/DuckDuckGo/Menus/MainMenu.swift @@ -290,28 +290,30 @@ final class MainMenu: NSMenu { } 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() + NSMenuItem(title: UserText.bookmarks) + .withAccessibilityIdentifier("MainMenu.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() - importBookmarksMenuItem - NSMenuItem(title: UserText.exportBookmarks, action: #selector(AppDelegate.openExportBookmarks)) - NSMenuItem.separator() + importBookmarksMenuItem + NSMenuItem(title: UserText.exportBookmarks, action: #selector(AppDelegate.openExportBookmarks)) + NSMenuItem.separator() - NSMenuItem(title: UserText.favorites) - .submenu(favoritesMenu.buildItems { - NSMenuItem(title: UserText.mainMenuHistoryFavoriteThisPage, action: #selector(MainViewController.favoriteThisPage)) - .withImage(.favorite) - .withAccessibilityIdentifier("MainMenu.favoriteThisPage") - NSMenuItem.separator() - }) - .withImage(.favorite) + NSMenuItem(title: UserText.favorites) + .submenu(favoritesMenu.buildItems { + NSMenuItem(title: UserText.mainMenuHistoryFavoriteThisPage, action: #selector(MainViewController.favoriteThisPage)) + .withImage(.favorite) + .withAccessibilityIdentifier("MainMenu.favoriteThisPage") + NSMenuItem.separator() + }) + .withImage(.favorite) - NSMenuItem.separator() - }) + NSMenuItem.separator() + }) } func buildWindowMenu() -> NSMenuItem { @@ -604,6 +606,9 @@ final class MainMenu: NSMenu { NSMenuItem(title: "Reset Pixels Storage", action: #selector(MainViewController.resetDailyPixels)) NSMenuItem(title: "Reset Remote Messages", action: #selector(AppDelegate.resetRemoteMessages)) NSMenuItem(title: "Reset CPM Experiment Cohort (needs restart)", action: #selector(AppDelegate.resetCpmCohort)) + NSMenuItem(title: "Reset Duck Player Onboarding", action: #selector(MainViewController.resetDuckPlayerOnboarding)) + NSMenuItem(title: "Reset Duck Player Preferences", action: #selector(MainViewController.resetDuckPlayerPreferences)) + }.withAccessibilityIdentifier("MainMenu.resetData") NSMenuItem(title: "UI Triggers") { NSMenuItem(title: "Show Save Credentials Popover", action: #selector(MainViewController.showSaveCredentialsPopover)) diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index 279aaca5d0..6bbcf6e262 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -803,6 +803,7 @@ extension MainViewController { @objc func resetBookmarks(_ sender: Any?) { LocalBookmarkManager.shared.resetBookmarks() UserDefaults.standard.set(false, forKey: UserDefaultsWrapper.Key.homePageContinueSetUpImport.rawValue) + UserDefaults.standard.set(false, forKey: UserDefaultsWrapper.Key.bookmarksBarPromptShown.rawValue) } @objc func resetPinnedTabs(_ sender: Any?) { @@ -826,6 +827,14 @@ extension MainViewController { UserDefaults.standard.set(true, forKey: UserDefaultsWrapper.Key.homePageShowPermanentSurvey.rawValue) } + @objc func resetDuckPlayerOnboarding(_ sender: Any?) { + DefaultDuckPlayerOnboardingDecider().reset() + } + + @objc func resetDuckPlayerPreferences(_ sender: Any?) { + DuckPlayerPreferences.shared.reset() + } + @objc func internalUserState(_ sender: Any?) { guard let internalUserDecider = NSApp.delegateTyped.internalUserDecider as? DefaultInternalUserDecider else { return } let state = internalUserDecider.isInternalUser diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index 2a16c707e8..4aac241e6f 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -169,6 +169,7 @@ final class NavigationBarViewController: NSViewController { bookmarkListButton.sendAction(on: .leftMouseDown) bookmarkListButton.registerForDraggedTypes(BookmarkDragDropManager.draggedTypes) bookmarkListButton.delegate = self + bookmarkListButton.setAccessibilityIdentifier("NavigationBarViewController.bookmarkListButton") downloadsButton.sendAction(on: .leftMouseDown) networkProtectionButton.sendAction(on: .leftMouseDown) passwordManagementButton.sendAction(on: .leftMouseDown) diff --git a/DuckDuckGo/Preferences/Model/DuckPlayerPreferences.swift b/DuckDuckGo/Preferences/Model/DuckPlayerPreferences.swift index 653e7cc4c2..ca523a9875 100644 --- a/DuckDuckGo/Preferences/Model/DuckPlayerPreferences.swift +++ b/DuckDuckGo/Preferences/Model/DuckPlayerPreferences.swift @@ -117,6 +117,14 @@ final class DuckPlayerPreferences: ObservableObject { duckPlayerContingencyHandler.shouldDisplayContingencyMessage } + func reset() { + youtubeOverlayAnyButtonPressed = false + youtubeOverlayInteracted = false + duckPlayerMode = .alwaysAsk + duckPlayerOpenInNewTab = true + duckPlayerAutoplay = true + } + @MainActor func openLearnMoreContingencyURL() { guard let url = duckPlayerContingencyHandler.learnMoreURL else { return } diff --git a/DuckDuckGo/Tab/Model/ContentScopeFeatureFlagging.swift b/DuckDuckGo/Tab/Model/ContentScopeFeatureFlagging.swift index 41272ed324..09010c106e 100644 --- a/DuckDuckGo/Tab/Model/ContentScopeFeatureFlagging.swift +++ b/DuckDuckGo/Tab/Model/ContentScopeFeatureFlagging.swift @@ -31,6 +31,7 @@ extension ContentScopeFeatureToggles { credentialsSaving: autofillPrefs.askToSaveUsernamesAndPasswords, passwordGeneration: autofillPrefs.askToSaveUsernamesAndPasswords, inlineIconCredentials: autofillPrefs.askToSaveUsernamesAndPasswords, - thirdPartyCredentialsProvider: true) + thirdPartyCredentialsProvider: true, + unknownUsernameCategorization: privacyConfig.isSubfeatureEnabled(AutofillSubfeature.unknownUsernameCategorization)) } } diff --git a/DuckDuckGo/Tab/Model/Tab+Navigation.swift b/DuckDuckGo/Tab/Model/Tab+Navigation.swift index ab3fc72e87..1a4bce2d59 100644 --- a/DuckDuckGo/Tab/Model/Tab+Navigation.swift +++ b/DuckDuckGo/Tab/Model/Tab+Navigation.swift @@ -54,6 +54,9 @@ extension Tab: NavigationResponder { // Duck Player overlay navigations handling .weak(nullable: self.duckPlayer), + // Duck Player onboarding banner + .weak(nullable: self.duckPlayerOnboarding), + // open external scheme link in another app .weak(nullable: self.externalAppSchemeHandler), diff --git a/DuckDuckGo/Tab/TabExtensions/DuckPlayerOnboardingTabExtension.swift b/DuckDuckGo/Tab/TabExtensions/DuckPlayerOnboardingTabExtension.swift new file mode 100644 index 0000000000..9c428dec5e --- /dev/null +++ b/DuckDuckGo/Tab/TabExtensions/DuckPlayerOnboardingTabExtension.swift @@ -0,0 +1,76 @@ +// +// DuckPlayerOnboardingTabExtension.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 Navigation +import Combine + +typealias DuckPlayerOnboardingPublisher = AnyPublisher + +final class DuckPlayerOnboardingTabExtension: TabExtension { + @Published private(set) var onboardingState: OnboardingState? + private let onboardingDecider: DuckPlayerOnboardingDecider + + init(onboardingDecider: DuckPlayerOnboardingDecider) { + self.onboardingDecider = onboardingDecider + } +} + +extension DuckPlayerOnboardingTabExtension: NavigationResponder { + + func navigationDidFinish(_ navigation: Navigation) { + guard onboardingDecider.canDisplayOnboarding else { return } + + let locationValidator = DuckPlayerOnboardingLocationValidator() + + Task { @MainActor in + if let webView = navigation.navigationAction.targetFrame?.webView, + await locationValidator.isValidLocation(webView) { + onboardingState = .init(onboardingDecider: onboardingDecider) + } + } + } +} + +struct OnboardingState { + let onboardingDecider: DuckPlayerOnboardingDecider +} + +protocol DuckPlayerOnboardingProtocol: AnyObject, NavigationResponder { + var duckPlayerOnboardingPublisher: DuckPlayerOnboardingPublisher { get } +} + +extension DuckPlayerOnboardingTabExtension: DuckPlayerOnboardingProtocol { + func getPublicProtocol() -> DuckPlayerOnboardingProtocol { self } + + var duckPlayerOnboardingPublisher: DuckPlayerOnboardingPublisher { + self.$onboardingState.eraseToAnyPublisher() + } +} + +extension TabExtensions { + var duckPlayerOnboarding: DuckPlayerOnboardingProtocol? { + resolve(DuckPlayerOnboardingTabExtension.self) + } +} + +extension Tab { + var duckPlayerOnboardingPublisher: DuckPlayerOnboardingPublisher { + self.duckPlayerOnboarding?.duckPlayerOnboardingPublisher ?? Just(nil).eraseToAnyPublisher() + } +} diff --git a/DuckDuckGo/Tab/TabExtensions/DuckPlayerTabExtension.swift b/DuckDuckGo/Tab/TabExtensions/DuckPlayerTabExtension.swift index 92d5a63573..65a9134400 100644 --- a/DuckDuckGo/Tab/TabExtensions/DuckPlayerTabExtension.swift +++ b/DuckDuckGo/Tab/TabExtensions/DuckPlayerTabExtension.swift @@ -53,17 +53,19 @@ final class DuckPlayerTabExtension { } private weak var youtubeOverlayScript: YoutubeOverlayUserScript? private weak var youtubePlayerScript: YoutubePlayerUserScript? - + private let onboardingDecider: DuckPlayerOnboardingDecider private var shouldSelectNextNewTab: Bool? init(duckPlayer: DuckPlayer, isBurner: Bool, scriptsPublisher: some Publisher, webViewPublisher: some Publisher, - preferences: DuckPlayerPreferences = .shared) { + preferences: DuckPlayerPreferences = .shared, + onboardingDecider: DuckPlayerOnboardingDecider) { self.duckPlayer = duckPlayer self.isBurner = isBurner self.preferences = preferences + self.onboardingDecider = onboardingDecider webViewPublisher.sink { [weak self] webView in self?.webView = webView @@ -87,6 +89,12 @@ final class DuckPlayerTabExtension { youtubePlayerCancellables.removeAll() guard duckPlayer.isAvailable else { return } + onboardingDecider.valueChangedPublisher.sink {[weak self] _ in + guard let self = self else { return } + + self.youtubeOverlayScript?.userUISettingsUpdated(uiValues: UIUserValues(onboardingDecider: self.onboardingDecider)) + }.store(in: &youtubePlayerCancellables) + if let hostname = url?.host, let script = youtubeOverlayScript { if script.messageOriginPolicy.isAllowed(hostname) { duckPlayer.$mode @@ -176,6 +184,7 @@ extension DuckPlayerTabExtension: NavigationResponder { @MainActor func decidePolicy(for navigationAction: NavigationAction, preferences: inout NavigationPreferences) async -> NavigationActionPolicy? { // only proceed when Private Player is enabled + guard duckPlayer.isAvailable, duckPlayer.mode != .disabled else { return decidePolicyWithDisabledDuckPlayer(for: navigationAction) } @@ -254,7 +263,7 @@ extension DuckPlayerTabExtension: NavigationResponder { func navigation(_ navigation: Navigation, didSameDocumentNavigationOf navigationType: WKSameDocumentNavigationType) { // Navigating to a Youtube URL without page reload - if duckPlayer.mode == .enabled, + if shouldOpenDuckPlayerDirectly, case .sessionStatePush = navigationType, let webView, let url = webView.url, url.isYoutubeVideo, @@ -295,7 +304,7 @@ extension DuckPlayerTabExtension: NavigationResponder { // SERP+Video <<<< YT (redirected to DP) <- Duck Player // if case .backForward(distance: let distance) = navigationAction.navigationType, distance < 0, - duckPlayer.mode == .enabled, + shouldOpenDuckPlayerDirectly, navigationAction.sourceFrame.url.isDuckPlayer, navigationAction.url.youtubeVideoID == navigationAction.sourceFrame.url.youtubeVideoID, let mainFrame = navigationAction.mainFrameTarget { @@ -319,7 +328,7 @@ extension DuckPlayerTabExtension: NavigationResponder { } // Redirect youtube urls to Duck Player when [Always enable] preference is set - if duckPlayer.mode == .enabled + if shouldOpenDuckPlayerDirectly // - or - recommendations must always be opened in the Duck Player || (navigationAction.sourceFrame.url.isDuckPlayer && navigationAction.url.isYoutubeVideoRecommendation), let mainFrame = navigationAction.mainFrameTarget { @@ -353,7 +362,7 @@ extension DuckPlayerTabExtension: NavigationResponder { return } if navigation.url.isDuckPlayer { - let setting = duckPlayer.mode == .enabled ? "always" : "default" + let setting = preferences.duckPlayerMode == .enabled ? "always" : "default" let newTabSettings = preferences.duckPlayerOpenInNewTab ? "true" : "false" let autoplay = preferences.duckPlayerAutoplay ? "true" : "false" @@ -380,5 +389,7 @@ extension DuckPlayerTabExtension: DuckPlayerExtensionProtocol, TabExtension { } extension TabExtensions { - var duckPlayer: DuckPlayerExtensionProtocol? { resolve(DuckPlayerTabExtension.self) } + var duckPlayer: DuckPlayerExtensionProtocol? { + resolve(DuckPlayerTabExtension.self) + } } diff --git a/DuckDuckGo/Tab/TabExtensions/PrivacyDashboardTabExtension.swift b/DuckDuckGo/Tab/TabExtensions/PrivacyDashboardTabExtension.swift index 7d2900d390..486b20ead8 100644 --- a/DuckDuckGo/Tab/TabExtensions/PrivacyDashboardTabExtension.swift +++ b/DuckDuckGo/Tab/TabExtensions/PrivacyDashboardTabExtension.swift @@ -114,7 +114,7 @@ extension PrivacyDashboardTabExtension { private func makePrivacyInfo(url: URL) -> PrivacyInfo? { guard let host = url.host else { return nil } - let entity = contentBlocking.trackerDataManager.trackerData.findEntity(forHost: host) + let entity = contentBlocking.trackerDataManager.trackerData.findParentEntityOrFallback(forHost: host) privacyInfo = PrivacyInfo(url: url, parentEntity: entity, diff --git a/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift b/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift index e998c25902..42eb48da2c 100644 --- a/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift +++ b/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift @@ -188,11 +188,17 @@ extension TabExtensionsBuilder { NavigationHotkeyHandler(isTabPinned: args.isTabPinned, isBurner: args.isTabBurner) } + let duckPlayerOnboardingDecider = DefaultDuckPlayerOnboardingDecider() add { DuckPlayerTabExtension(duckPlayer: dependencies.duckPlayer, isBurner: args.isTabBurner, scriptsPublisher: userScripts.compactMap { $0 }, - webViewPublisher: args.webViewFuture) + webViewPublisher: args.webViewFuture, + onboardingDecider: duckPlayerOnboardingDecider) + } + + add { + DuckPlayerOnboardingTabExtension(onboardingDecider: duckPlayerOnboardingDecider) } add { diff --git a/DuckDuckGo/Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Tab/View/BrowserTabViewController.swift index 051665ebde..d1e20e9248 100644 --- a/DuckDuckGo/Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Tab/View/BrowserTabViewController.swift @@ -44,6 +44,7 @@ final class BrowserTabViewController: NSViewController { private var tabViewModelCancellables = Set() private var activeUserDialogCancellable: Cancellable? + private var duckPlayerConsentCancellable: AnyCancellable? private var pinnedTabsDelegatesCancellable: AnyCancellable? private var keyWindowSelectedTabCancellable: AnyCancellable? private var cancellables = Set() @@ -54,6 +55,10 @@ final class BrowserTabViewController: NSViewController { private var hoverLabelWorkItem: DispatchWorkItem? private(set) var transientTabContentViewController: NSViewController? + private lazy var duckPlayerOnboardingModalManager: DuckPlayerOnboardingModalManager = { + let modal = DuckPlayerOnboardingModalManager() + return modal + }() required init?(coder: NSCoder) { fatalError("BrowserTabViewController: Bad initializer") @@ -255,6 +260,7 @@ final class BrowserTabViewController: NSViewController { self.subscribeToTabContent(of: selectedTabViewModel) self.subscribeToHoveredLink(of: selectedTabViewModel) self.subscribeToUserDialogs(of: selectedTabViewModel) + self.subscribeToDuckPlayerOnboardingPrompt(of: selectedTabViewModel) self.adjustFirstResponder(force: true) } @@ -430,6 +436,18 @@ final class BrowserTabViewController: NSViewController { #endif } + private func subscribeToDuckPlayerOnboardingPrompt(of tabViewModel: TabViewModel?) { + tabViewModel?.tab.duckPlayerOnboardingPublisher.sink { [weak self, weak tab = tabViewModel?.tab] onboardingState in + + guard let self, tab != nil, let onboardingState = onboardingState, onboardingState.onboardingDecider.canDisplayOnboarding else { + self?.duckPlayerOnboardingModalManager.close(animated: false, completion: nil) + return + } + + self.duckPlayerOnboardingModalManager.show(on: self.view, animated: true) + }.store(in: &tabViewModelCancellables) + } + private func shouldMakeContentViewFirstResponder(for tabContent: Tab.TabContent) -> Bool { // always steal focus when first responder is not a text field guard view.window?.firstResponder is NSText else { diff --git a/DuckDuckGo/YoutubePlayer/DuckPlayer.swift b/DuckDuckGo/YoutubePlayer/DuckPlayer.swift index 1890a232fe..bc26df30dd 100644 --- a/DuckDuckGo/YoutubePlayer/DuckPlayer.swift +++ b/DuckDuckGo/YoutubePlayer/DuckPlayer.swift @@ -57,6 +57,7 @@ struct InitialPlayerSettings: Codable { struct PlayerSettings: Codable { let pip: PIP let autoplay: Autoplay + let focusMode: FocusMode } struct PIP: Codable { @@ -75,6 +76,17 @@ struct InitialPlayerSettings: Codable { let state: State } + /// Represents the current focus mode of the player. + /// + /// Focus mode determines whether the bottom toolbar should be visible or hidden. + /// When focus mode is enabled, the toolbar will auto-hide after a few seconds. + /// When focus mode is disabled, the toolbar will always be visible and the background wallpaper will be slightly brighter. + /// + /// Default should be enabled. + struct FocusMode: Codable { + let state: State + } + enum State: String, Codable { case enabled case disabled @@ -90,10 +102,12 @@ struct InitialPlayerSettings: Codable { let platform: Platform let environment: Environment let locale: Locale + } struct InitialOverlaySettings: Codable { let userValues: UserValues + let ui: UIUserValues } // Values that the YouTube Overlays can use to determine the current state @@ -111,6 +125,15 @@ public struct UserValues: Codable { let overlayInteracted: Bool } +public struct UIUserValues: Codable { + /// If this value is true, we force the FE layer to play in duck player even if the settings is off + let playInDuckPlayer: Bool + + init(onboardingDecider: DuckPlayerOnboardingDecider) { + self.playInDuckPlayer = onboardingDecider.shouldOpenFirstVideoOnDuckPlayer + } +} + final class DuckPlayer { static let usesSimulatedRequests: Bool = { if #available(macOS 12.0, *) { @@ -145,12 +168,14 @@ final class DuckPlayer { init( preferences: DuckPlayerPreferences = .shared, - privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager + privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager, + onboardingDecider: DuckPlayerOnboardingDecider = DefaultDuckPlayerOnboardingDecider() ) { self.preferences = preferences isFeatureEnabled = privacyConfigurationManager.privacyConfig.isEnabled(featureKey: .duckPlayer) isPiPFeatureEnabled = privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(DuckPlayerSubfeature.pip) isAutoplayFeatureEnabled = privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(DuckPlayerSubfeature.autoplay) + self.onboardingDecider = onboardingDecider mode = preferences.duckPlayerMode bindDuckPlayerModeIfNeeded() @@ -265,9 +290,13 @@ final class DuckPlayer { let platform = InitialPlayerSettings.Platform(name: "macos") let environment = InitialPlayerSettings.Environment.development let locale = InitialPlayerSettings.Locale.en - let playerSettings = InitialPlayerSettings.PlayerSettings(pip: pip, autoplay: autoplay) + let focusMode = InitialPlayerSettings.FocusMode(state: onboardingDecider.shouldOpenFirstVideoOnDuckPlayer ? .disabled : .enabled) + let playerSettings = InitialPlayerSettings.PlayerSettings(pip: pip, autoplay: autoplay, focusMode: focusMode) let userValues = encodeUserValues() + /// Since the FE is requesting player-encoded values, we can assume that the first player video setup is complete from the onboarding point of view. + onboardingDecider.setFirstVideoInDuckPlayerAsDone() + return InitialPlayerSettings(userValues: userValues, settings: playerSettings, platform: platform, @@ -279,7 +308,7 @@ final class DuckPlayer { private func encodedOverlaySettings(with webView: WKWebView?) async -> InitialOverlaySettings { let userValues = encodeUserValues() - return InitialOverlaySettings(userValues: userValues) + return InitialOverlaySettings(userValues: userValues, ui: UIUserValues(onboardingDecider: onboardingDecider)) } // MARK: - Private @@ -296,6 +325,7 @@ final class DuckPlayer { private var isFeatureEnabledCancellable: AnyCancellable? private var isPiPFeatureEnabled: Bool private var isAutoplayFeatureEnabled: Bool + private let onboardingDecider: DuckPlayerOnboardingDecider private func bindDuckPlayerModeIfNeeded() { if isFeatureEnabled { diff --git a/DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingLocationValidator.swift b/DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingLocationValidator.swift new file mode 100644 index 0000000000..1b467c9f04 --- /dev/null +++ b/DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingLocationValidator.swift @@ -0,0 +1,52 @@ +// +// DuckPlayerOnboardingLocationValidator.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 Navigation + +struct DuckPlayerOnboardingLocationValidator { + private static let youtubeChannelCheckScript = """ + (function() { + var canonicalLink = document.querySelector('link[rel="canonical"]'); + return canonicalLink && canonicalLink.href.includes('channel'); + })(); + """ + + func isValidLocation(_ webView: WKWebView) async -> Bool { + guard let url = await webView.url else { return false } + + let isRootURL = isYoutubeRootURL(url) + let isInChannel = await isCurrentWebViewInAYoutubeChannel(webView) + return isRootURL || isInChannel + } + + private func isYoutubeRootURL(_ url: URL) -> Bool { + guard let urlComponents = URLComponents(string: url.absoluteString) else { return false } + return urlComponents.scheme == "https" && + urlComponents.host == "www.youtube.com" && + urlComponents.path == "/" + } + + private func isCurrentWebViewInAYoutubeChannel(_ webView: WKWebView) async -> Bool { + do { + return try await webView.evaluateJavaScript(DuckPlayerOnboardingLocationValidator.youtubeChannelCheckScript) as Bool? ?? false + } catch { + return false + } + } +} diff --git a/DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingModal/DuckPlayerOnboardingDecider.swift b/DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingModal/DuckPlayerOnboardingDecider.swift new file mode 100644 index 0000000000..d76955b560 --- /dev/null +++ b/DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingModal/DuckPlayerOnboardingDecider.swift @@ -0,0 +1,143 @@ +// +// DuckPlayerOnboardingDecider.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 protocol for deciding whether to display onboarding and open the first video in the Duck Player. +protocol DuckPlayerOnboardingDecider { + /// A boolean indicating whether the onboarding should be displayed. + var canDisplayOnboarding: Bool { get } + + /// A boolean indicating whether the first video should be opened in the Duck Player. + var shouldOpenFirstVideoOnDuckPlayer: Bool { get } + + /// Sets the onboarding as done. + /// + /// This method should be called when the onboarding has been completed. + func setOnboardingAsDone() + + /// Sets the flag to open the first video in the Duck Player. + /// + /// This method should be called when user selects to use Duck Player during the onboarding + func setOpenFirstVideoOnDuckPlayer() + + /// Sets the first video in the Duck Player as done. + /// + /// This method should be called when the first video has been opened in the Duck Player. + func setFirstVideoInDuckPlayerAsDone() + + /// A publisher that emits a notification whenever any onboarding or video flags change. + /// + /// Subscribe to receive updates when `canDisplayOnboarding`, `shouldOpenFirstVideoOnDuckPlayer`, or related values change. + /// + /// Note that this publisher will emit a notification whenever any of the underlying values change, even if the change is made on a different instance of `DefaultDuckPlayerOnboardingDecider`. + var valueChangedPublisher: PassthroughSubject { get set } + + /// Resets the onboarding and video flags to their initial state. + /// + /// This method should be called when the onboarding and video flags need to be reset. + func reset() +} + +import Combine + +struct DefaultDuckPlayerOnboardingDecider: DuckPlayerOnboardingDecider { + private let defaults: UserDefaults + private var observer: NSObjectProtocol? + private let preferences: DuckPlayerPreferences + var valueChangedPublisher: PassthroughSubject = .init() + + init(defaults: UserDefaults = .standard, preferences: DuckPlayerPreferences = .shared) { + self.defaults = defaults + self.preferences = preferences + observer = NotificationCenter.default.addObserver(forName: .valuesDidChange, object: nil, queue: nil) { [valuesDidChange] _ in + valuesDidChange() + } + } + + /// We only want to display the onboarding if it was never displayed, the settings is set to alwaysAsk and haven't interacted with the overlay. + var canDisplayOnboarding: Bool { +#if DEBUG + return !defaults.onboardingWasDisplayed && preferences.duckPlayerMode == .alwaysAsk +#else + // returning false until we turn on the experiment + return false +#endif + + } + + private var isUserInExperiment: Bool { + return false + } + + var shouldOpenFirstVideoOnDuckPlayer: Bool { + return defaults.shouldOpenFirstVideoInDuckPlayer && !defaults.firstVideoWasOpenedInDuckPlayer + } + + func setOnboardingAsDone() { + NotificationCenter.default.post(name: .valuesDidChange, object: nil) + defaults.onboardingWasDisplayed = true + } + + func setOpenFirstVideoOnDuckPlayer() { + defaults.shouldOpenFirstVideoInDuckPlayer = true + NotificationCenter.default.post(name: .valuesDidChange, object: nil) + } + + func setFirstVideoInDuckPlayerAsDone() { + defaults.firstVideoWasOpenedInDuckPlayer = true + NotificationCenter.default.post(name: .valuesDidChange, object: nil) + } + + func reset() { + defaults.onboardingWasDisplayed = false + defaults.shouldOpenFirstVideoInDuckPlayer = false + defaults.firstVideoWasOpenedInDuckPlayer = false + } + + private func valuesDidChange() { + valueChangedPublisher.send() + } +} + +private extension UserDefaults { + enum Keys { + static let onboardingWasDisplayed = "duckplayer.onboarding-displayed" + static let firstVideoWasOpenedInDuckPlayer = "duckplayer.onboarding.first-video-opened" + static let shouldOpenFirstVideoInDuckPlayer = "duckplayer.onboarding.should-open-in-duckplayer" + } + + var onboardingWasDisplayed: Bool { + get { return bool(forKey: Keys.onboardingWasDisplayed) } + set { set(newValue, forKey: Keys.onboardingWasDisplayed) } + } + + var firstVideoWasOpenedInDuckPlayer: Bool { + get { return bool(forKey: Keys.firstVideoWasOpenedInDuckPlayer) } + set { set(newValue, forKey: Keys.firstVideoWasOpenedInDuckPlayer) } + } + + var shouldOpenFirstVideoInDuckPlayer: Bool { + get { return bool(forKey: Keys.shouldOpenFirstVideoInDuckPlayer) } + set { set(newValue, forKey: Keys.shouldOpenFirstVideoInDuckPlayer) } + } +} + +private extension Notification.Name { + static let valuesDidChange = Notification.Name("duckplayer.onboarding.should-open-first-video") +} diff --git a/DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingModal/DuckPlayerOnboardingModalManager.swift b/DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingModal/DuckPlayerOnboardingModalManager.swift new file mode 100644 index 0000000000..63e56e7c06 --- /dev/null +++ b/DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingModal/DuckPlayerOnboardingModalManager.swift @@ -0,0 +1,29 @@ +// +// DuckPlayerOnboardingModalManager.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 + +final class DuckPlayerOnboardingModalManager: TabModalManageable { + var modal: TabModal? + + var viewController: NSViewController { + DuckPlayerOnboardingViewController { [weak self] in + self?.close(animated: true, completion: nil) + } + } +} diff --git a/DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingModal/DuckPlayerOnboardingModalView.swift b/DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingModal/DuckPlayerOnboardingModalView.swift new file mode 100644 index 0000000000..8f7ecbae99 --- /dev/null +++ b/DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingModal/DuckPlayerOnboardingModalView.swift @@ -0,0 +1,293 @@ +// +// DuckPlayerOnboardingModalView.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 + +struct DuckPlayerOnboardingModalView: View { + private enum Constants { + static let outerContainerWidth: CGFloat = 504 + static let smallContainerHeight: CGFloat = 182 + static let bigContainerHeight: CGFloat = 286 + static let containerCornerRadius: CGFloat = 12 + static let darkModeBorderColor: Color = .white.opacity(0.2) + static let whiteModeBorderColor: Color = .black.opacity(0.1) + } + + @ObservedObject var viewModel: DuckPlayerOnboardingViewModel + @Environment(\.colorScheme) var colorScheme + + var body: some View { + currentView + .padding() + .frame(width: Constants.outerContainerWidth, height: containerHeight) + .padding(.horizontal) + .background(Color("DialogPanelBackground")) + .cornerRadius(Constants.containerCornerRadius) + .overlay( + RoundedRectangle(cornerRadius: Constants.containerCornerRadius) + .stroke(colorScheme == .dark ? Constants.darkModeBorderColor : Constants.whiteModeBorderColor, lineWidth: 1) + ) + } + + private var containerHeight: CGFloat { + switch viewModel.currentView { + case .confirmation: + return Constants.smallContainerHeight + + case .onboardingOptions: + return Constants.bigContainerHeight + } + } + + @ViewBuilder + var currentView: some View { + switch viewModel.currentView { + case .confirmation: + DuckPlayerOnboardingConfirmationView { + viewModel.handleGotItCTA() + } + + case .onboardingOptions: + DuckPlayerOnboardingChoiceView(turnOnButtonPressed: { + viewModel.currentView = .confirmation + viewModel.handleTurnOnCTA() + }, notNowPressed: viewModel.handleNotNowCTA) + } + } +} + +private struct DuckPlayerOnboardingChoiceView: View { + let turnOnButtonPressed: () -> Void + let notNowPressed: () -> Void + + var body: some View { + VStack(spacing: 20) { + DaxSpeechBubble { + VStack (alignment: .leading, spacing: 8) { + Text(UserText.duckPlayerOnboardingChoiceModalTitle) + .font(.title) + .padding(.horizontal) + + Text(UserText.duckPlayerOnboardingChoiceModalMessage) + .font(.body) + .multilineText() + .padding(.horizontal) + + HStack { + Spacer() + Image("DuckPlayerOnboardingModal") + Spacer() + } + }.frame(maxWidth: .infinity) + .padding() + + } + + HStack { + Button { + notNowPressed() + } label: { + Text(UserText.duckPlayerOnboardingChoiceModalCTADeny) + } + .buttonStyle(SecondaryCTAStyle()) + + Spacer() + Button { + turnOnButtonPressed() + } label: { + Text(UserText.duckPlayerOnboardingChoiceModalCTAConfirm) + } + .buttonStyle(PrimaryCTAStyle()) + } + } + } +} + +private struct DuckPlayerOnboardingConfirmationView: View { + let voidButtonPressed: () -> Void + var body: some View { + VStack(spacing: 20) { + DaxSpeechBubble { + VStack(alignment: .leading, spacing: 8) { + Text(UserText.duckPlayerOnboardingConfirmationModalTitle) + .font(.title) + .padding(.horizontal) + + Text(UserText.duckPlayerOnboardingConfirmationModalMessage) + .font(.body) + .padding(.horizontal) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal) + + } + + Button { + voidButtonPressed() + } label: { + Text(UserText.duckPlayerOnboardingConfirmationModalCTAConfirm) + } + .buttonStyle(PrimaryCTAStyle()) + } + } +} + +private struct DaxSpeechBubble: View { + let content: Content + + init(@ViewBuilder content: () -> Content) { + self.content = content() + } + + var body: some View { + VStack { + HStack(alignment: .top, spacing: 12) { + Image("DuckPlayerOnboardingModalDax") + .padding(.leading, -10) + + ZStack { + SpeechBubble() + content + } + } + } + } +} + +private struct SpeechBubble: View { + let radius: CGFloat = 20 + let tailSize: CGFloat = 12 + let tailPosition: CGFloat = 38 + let tailHeight: CGFloat = 28 + + var body: some View { + ZStack { + GeometryReader { g in + let rect = CGRect(x: 0, y: 0, width: g.size.width, height: g.size.height) + + Path { path in + + path.move(to: CGPoint(x: rect.minX, y: rect.maxY - radius)) + + path.addLine(to: CGPoint(x: rect.minX, y: tailPosition + tailHeight / 2)) + path.addLine(to: CGPoint(x: rect.minX - tailSize, y: tailPosition)) + path.addLine(to: CGPoint(x: rect.minX, y: tailPosition - tailHeight / 2)) + + path.addArc( + center: CGPoint(x: rect.minX + radius, y: rect.minY + radius), + radius: radius, + startAngle: .degrees(180), + endAngle: .degrees(270), + clockwise: false + ) + path.addArc( + center: CGPoint(x: rect.maxX - radius, y: rect.minY + radius), + radius: radius, + startAngle: .degrees(270), + endAngle: .degrees(0), + clockwise: false + ) + path.addArc( + center: CGPoint(x: rect.maxX - radius, y: rect.maxY - radius), + radius: radius, + startAngle: .degrees(0), + endAngle: .degrees(90), + clockwise: false + ) + path.addArc( + center: CGPoint(x: rect.minX + radius, y: rect.maxY - radius), + radius: radius, + startAngle: .degrees(90), + endAngle: .degrees(180), + clockwise: false + ) + + } + .fill(Color(.interfaceBackground)) + .shadow(color: Color(.onboardingDaxSpeechShadow), radius: 2, x: 0, y: 0) + } + + } + } +} + +private enum CTAConstants { + static let CTACornerRadius: CGFloat = 8 +} + +private struct PrimaryCTAStyle: ButtonStyle { + + func makeBody(configuration: Self.Configuration) -> some View { + + let color = configuration.isPressed ? Color("DuckPlayerOnboardingPrimaryButtonPressed") : Color("DuckPlayerOnboardingPrimaryButton") + + configuration.label + .padding(.vertical, 10) + .frame(maxWidth: .infinity) + .truncationMode(.tail) + .background(RoundedRectangle(cornerRadius: CTAConstants.CTACornerRadius, style: .continuous).fill(color)) + .foregroundColor(.white) + .font(.system(size: 13, weight: .light, design: .default)) + } +} + +private struct SecondaryCTAStyle: ButtonStyle { + @Environment(\.colorScheme) var colorScheme + + func makeBody(configuration: Self.Configuration) -> some View { + + let color = configuration.isPressed ? Color("DuckPlayerOnboardingSecondaryButtonPressed") : Color("DuckPlayerOnboardingSecondaryButton") + + let outterShadowOpacity = colorScheme == .dark ? 0.8 : 0.0 + + configuration.label + .font(.system(size: 13, weight: .light, design: .default)) + .foregroundColor(.primary) + .padding(.vertical, 10) + .frame(maxWidth: .infinity) + .background( + RoundedRectangle(cornerRadius: CTAConstants.CTACornerRadius, style: .continuous) + .fill(color) + .shadow(color: .black.opacity(0.1), radius: 0.1, x: 0, y: 1) + .shadow(color: .primary.opacity(outterShadowOpacity), radius: 0.1, x: 0, y: -0.6)) + + .overlay( + RoundedRectangle(cornerRadius: CTAConstants.CTACornerRadius) + .stroke(Color.black.opacity(0.1), lineWidth: 1)) + } +} + +#Preview { + VStack { + DuckPlayerOnboardingChoiceView(turnOnButtonPressed: { + + }, notNowPressed: { + + }) + .frame(width: 504, height: 286) + + Divider() + .padding() + + DuckPlayerOnboardingConfirmationView(voidButtonPressed: { + + }) + .frame(width: 504, height: 152) + } + .padding() +} diff --git a/DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingModal/DuckPlayerOnboardingViewController.swift b/DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingModal/DuckPlayerOnboardingViewController.swift new file mode 100644 index 0000000000..ff9e00ba07 --- /dev/null +++ b/DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingModal/DuckPlayerOnboardingViewController.swift @@ -0,0 +1,72 @@ +// +// DuckPlayerOnboardingViewController.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 AppKit + +final class DuckPlayerOnboardingViewController: NSViewController { + var didFinish: () -> Void + + internal init(didFinish: @escaping () -> Void) { + self.didFinish = didFinish + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private var hostingView: NSHostingView! + private lazy var viewModel: DuckPlayerOnboardingViewModel = { + let viewModel = DuckPlayerOnboardingViewModel() + viewModel.delegate = self + return viewModel + }() + + override func loadView() { + let consentView = DuckPlayerOnboardingModalView(viewModel: viewModel) + hostingView = NSHostingView(rootView: consentView) + self.view = hostingView + } + + private func handleEnableDuckPlayerActionButton() { + print("Enabled") + } + + private func handleNotNowActionButton() { + didFinish() + } + + private func handleGotItActionButton() { + didFinish() + } +} + +extension DuckPlayerOnboardingViewController: DuckPlayerOnboardingViewModelDelegate{ + func duckPlayerOnboardingViewModelDidSelectTurnOn(_ viewModel: DuckPlayerOnboardingViewModel) { + handleEnableDuckPlayerActionButton() + } + + func duckPlayerOnboardingViewModelDidSelectNotNow(_ viewModel: DuckPlayerOnboardingViewModel) { + handleNotNowActionButton() + } + + func duckPlayerOnboardingViewModelDidSelectGotIt(_ viewModel: DuckPlayerOnboardingViewModel) { + handleGotItActionButton() + } +} diff --git a/DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingModal/DuckPlayerOnboardingViewModel.swift b/DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingModal/DuckPlayerOnboardingViewModel.swift new file mode 100644 index 0000000000..d5996ae3ca --- /dev/null +++ b/DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingModal/DuckPlayerOnboardingViewModel.swift @@ -0,0 +1,57 @@ +// +// DuckPlayerOnboardingViewModel.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 + +protocol DuckPlayerOnboardingViewModelDelegate: AnyObject { + func duckPlayerOnboardingViewModelDidSelectTurnOn(_ viewModel: DuckPlayerOnboardingViewModel) + func duckPlayerOnboardingViewModelDidSelectNotNow(_ viewModel: DuckPlayerOnboardingViewModel) + func duckPlayerOnboardingViewModelDidSelectGotIt(_ viewModel: DuckPlayerOnboardingViewModel) +} + +final class DuckPlayerOnboardingViewModel: ObservableObject { + private let onboardingDecider: DuckPlayerOnboardingDecider + + enum DuckPlayerModalCurrentView { + case onboardingOptions + case confirmation + } + + init(onboardingDecider: DuckPlayerOnboardingDecider = DefaultDuckPlayerOnboardingDecider()) { + self.onboardingDecider = onboardingDecider + } + + @Published var currentView: DuckPlayerModalCurrentView = .onboardingOptions + weak var delegate: DuckPlayerOnboardingViewModelDelegate? + + func handleTurnOnCTA() { + delegate?.duckPlayerOnboardingViewModelDidSelectTurnOn(self) + + onboardingDecider.setOpenFirstVideoOnDuckPlayer() + onboardingDecider.setOnboardingAsDone() + } + + func handleNotNowCTA() { + delegate?.duckPlayerOnboardingViewModelDidSelectNotNow(self) + onboardingDecider.setOnboardingAsDone() + } + + func handleGotItCTA() { + delegate?.duckPlayerOnboardingViewModelDidSelectGotIt(self) + } +} diff --git a/DuckDuckGo/YoutubePlayer/TabModal/TabModal.swift b/DuckDuckGo/YoutubePlayer/TabModal/TabModal.swift new file mode 100644 index 0000000000..1c6422ba8e --- /dev/null +++ b/DuckDuckGo/YoutubePlayer/TabModal/TabModal.swift @@ -0,0 +1,140 @@ +// +// TabModal.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 Cocoa +import Combine +private enum AnimationConsts { + static let yAnimationOffset: CGFloat = 65 + static let duration: CGFloat = 0.6 +} + +public final class TabModal { + private let modalViewController: NSViewController + private lazy var windowController: NSWindowController = { + let windowController = NSWindowController(window: NSWindow(contentViewController: modalViewController)) + + if let window = windowController.window { + window.styleMask = [.borderless] + window.acceptsMouseMovedEvents = true + window.ignoresMouseEvents = false + window.backgroundColor = .clear + window.isOpaque = false + window.hasShadow = true + window.level = .floating + } + modalViewController.view.wantsLayer = true + return windowController + }() + + private var resizeObserver: Any? + private var cancellables = Set() + + public init(modalViewController: NSViewController) { + self.modalViewController = modalViewController + } + + public required init?(coder: NSCoder) { + fatalError("OnboardingModal: Bad initializer") + } + + // MARK: - Private methods + + private func windowDidResize(_ parent: NSWindow) { + guard let overlayWindow = windowController.window else { + return + } + + let xPosition = (parent.frame.width / 2) - (overlayWindow.frame.width / 2) + parent.frame.origin.x + let yPosition = parent.frame.origin.y + parent.frame.height - overlayWindow.frame.height - AnimationConsts.yAnimationOffset + + let size = overlayWindow.frame.size + let newOrigin = NSPoint(x: xPosition, y: yPosition) + overlayWindow.setFrame(NSRect(origin: newOrigin, size: size), display: true) + } + + private func addObserverForWindowResize(_ window: NSWindow) { + NotificationCenter.default.publisher(for: NSWindow.didResizeNotification, object: window) + .receive(on: DispatchQueue.main) + .sink { [weak self] notification in + guard let parent = notification.object as? NSWindow else { return } + self?.windowDidResize(parent) + } + .store(in: &cancellables) + } +} + +// MARK: - Public methods +extension TabModal: TabModalPresentable { + public func close(animated: Bool, completion: (() -> Void)? = nil) { + guard let overlayWindow = windowController.window else { + return + } + if !overlayWindow.isVisible { return } + + let removeWindow = { + overlayWindow.parent?.removeChildWindow(overlayWindow) + overlayWindow.orderOut(nil) + completion?() + } + + if animated { + NSAnimationContext.runAnimationGroup { context in + context.duration = AnimationConsts.duration + + let newOrigin = NSPoint(x: overlayWindow.frame.origin.x, y: overlayWindow.frame.origin.y + AnimationConsts.yAnimationOffset) + let size = overlayWindow.frame.size + overlayWindow.animator().alphaValue = 0 + overlayWindow.animator().setFrame(NSRect(origin: newOrigin, size: size), display: true) + } completionHandler: { + removeWindow() + } + } else { + removeWindow() + } + } + + public func show(on currentTabView: NSView, animated: Bool) { + guard let currentTabViewWindow = currentTabView.window, + let overlayWindow = windowController.window else { + return + } + + addObserverForWindowResize(currentTabViewWindow) + + currentTabViewWindow.addChildWindow(overlayWindow, ordered: .above) + + let xPosition = (currentTabViewWindow.frame.width / 2) - (overlayWindow.frame.width / 2) + currentTabViewWindow.frame.origin.x + let yPosition = currentTabViewWindow.frame.origin.y + currentTabViewWindow.frame.height - overlayWindow.frame.height + + if animated { + overlayWindow.setFrameOrigin(NSPoint(x: xPosition, y: yPosition)) + overlayWindow.alphaValue = 0 + + NSAnimationContext.runAnimationGroup { context in + context.duration = AnimationConsts.duration + let newOrigin = NSPoint(x: xPosition, y: yPosition - AnimationConsts.yAnimationOffset) + let size = overlayWindow.frame.size + overlayWindow.animator().alphaValue = 1 + overlayWindow.animator().setFrame(NSRect(origin: newOrigin, size: size), display: true) + + } + } else { + overlayWindow.setFrameOrigin(NSPoint(x: xPosition, y: yPosition - AnimationConsts.yAnimationOffset)) + } + } +} diff --git a/DuckDuckGo/YoutubePlayer/TabModal/TabModalManageable.swift b/DuckDuckGo/YoutubePlayer/TabModal/TabModalManageable.swift new file mode 100644 index 0000000000..7da40e235f --- /dev/null +++ b/DuckDuckGo/YoutubePlayer/TabModal/TabModalManageable.swift @@ -0,0 +1,82 @@ +// +// TabModalManageable.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 protocol for handling the view controller to be displayed in the modal. +protocol TabModalPresentable: AnyObject { + /// Initializes a new instance of the modal presenter with the given view controller. + /// + /// - Parameter modalViewController: The view controller to be presented in the modal. + init(modalViewController: NSViewController) + + /// Closes the modal view controller. + /// + /// - Parameters: + /// - animated: A boolean indicating whether the closure of the modal should be animated. + /// - completion: An optional closure to be executed after the modal has been closed. + func close(animated: Bool, completion: (() -> Void)?) + + /// Shows the modal view controller on the given view. + /// + /// - Parameters: + /// - currentTabView: The view on which the modal should be presented. + /// - animated: A boolean indicating whether the presentation of the modal should be animated. + func show(on currentTabView: NSView, animated: Bool) +} + +/// A protocol for managing the modal to be presented. +protocol TabModalManageable: AnyObject { + associatedtype ModalType: TabModalPresentable + + var modal: ModalType? { get set } + var viewController: NSViewController { get } + + /// Closes the modal view controller. + /// + /// - Parameters: + /// - animated: A boolean indicating whether the closure of the modal should be animated. + /// - completion: An optional closure to be executed after the modal has been closed. + func close(animated: Bool, completion: (() -> Void)?) + + /// Shows the modal view controller on the given view. + /// + /// - Parameters: + /// - currentTabView: The view on which the modal should be presented. + /// - animated: A boolean indicating whether the presentation of the modal should be animated. + func show(on currentTabView: NSView, animated: Bool) +} + +extension TabModalManageable { + + func close(animated: Bool, completion: (() -> Void)?) { + modal?.close(animated: animated) { [weak self] in + self?.modal = nil + } + } + + func show(on currentTabView: NSView, animated: Bool) { + prepareModal() + modal?.show(on: currentTabView, animated: animated) + } + + private func prepareModal() { + guard modal == nil else { return } + modal = ModalType(modalViewController: viewController) + } +} diff --git a/DuckDuckGo/YoutubePlayer/YoutubeOverlayUserScript.swift b/DuckDuckGo/YoutubePlayer/YoutubeOverlayUserScript.swift index 79ba756c2d..4d20a6ccd4 100644 --- a/DuckDuckGo/YoutubePlayer/YoutubeOverlayUserScript.swift +++ b/DuckDuckGo/YoutubePlayer/YoutubeOverlayUserScript.swift @@ -21,6 +21,7 @@ import WebKit import Common import UserScript import PixelKit +import Combine protocol YoutubeOverlayUserScriptDelegate: AnyObject { func youtubeOverlayUserScriptDidRequestDuckPlayer(with url: URL, in webView: WKWebView) @@ -95,6 +96,7 @@ final class YoutubeOverlayUserScript: NSObject, Subfeature { } } + // User values are user controled values public func userValuesUpdated(userValues: UserValues) { guard let webView = webView else { return assertionFailure("Could not access webView") @@ -102,6 +104,14 @@ final class YoutubeOverlayUserScript: NSObject, Subfeature { broker?.push(method: "onUserValuesChanged", params: userValues, for: self, into: webView) } + // Temporary changes to user settings + public func userUISettingsUpdated(uiValues: UIUserValues) { + guard let webView = webView else { + return assertionFailure("Could not access webView") + } + broker?.push(method: "onUIValuesChanged", params: uiValues, for: self, into: webView) + } + // MARK: - Private Methods @MainActor diff --git a/Gemfile.lock b/Gemfile.lock index 1277000bcc..9be15d591e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,9 +1,9 @@ GIT remote: https://github.com/duckduckgo/fastlane-plugin-ddg_apple_automation - revision: 5d267b66e9c63f879eecbb4ae1f40cb42547026e - tag: 0.2.0 + revision: 857c55c75a1153746a2db89710b6d69c241d534f + tag: 0.5.0 specs: - fastlane-plugin-ddg_apple_automation (0.2.0) + fastlane-plugin-ddg_apple_automation (0.5.0) GEM remote: https://rubygems.org/ @@ -17,8 +17,8 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.968.0) - aws-sdk-core (3.201.5) + aws-partitions (1.970.0) + aws-sdk-core (3.202.2) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.9) @@ -231,4 +231,4 @@ DEPENDENCIES httparty BUNDLED WITH - 2.4.19 + 2.4.4 diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 30afef8b97..a9266c048f 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -29,7 +29,7 @@ let package = Package( targets: ["DataBrokerProtection"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "190.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "191.1.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift index 7454795761..523857756c 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift @@ -164,7 +164,8 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject { credentialsSaving: false, passwordGeneration: false, inlineIconCredentials: false, - thirdPartyCredentialsProvider: false) + thirdPartyCredentialsProvider: false, + unknownUsernameCategorization: false) let sessionKey = UUID().uuidString self.authenticationManager = authenticationManager diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionAgentManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionAgentManager.swift index 0e33c1728c..071ae1be4e 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionAgentManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionAgentManager.swift @@ -50,7 +50,8 @@ public class DataBrokerProtectionAgentManagerProvider { credentialsSaving: false, passwordGeneration: false, inlineIconCredentials: false, - thirdPartyCredentialsProvider: false) + thirdPartyCredentialsProvider: false, + unknownUsernameCategorization: false) let contentScopeProperties = ContentScopeProperties(gpcEnabled: false, sessionKey: UUID().uuidString, featureToggles: features) diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift index 8cb1b5a407..5e78fc5f17 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift @@ -253,7 +253,8 @@ extension ContentScopeFeatureToggles { credentialsSaving: false, passwordGeneration: false, inlineIconCredentials: false, - thirdPartyCredentialsProvider: false + thirdPartyCredentialsProvider: false, + unknownUsernameCategorization: false ) } } diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index b1da627366..1a4855c3bd 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -32,7 +32,7 @@ let package = Package( .library(name: "VPNAppLauncher", targets: ["VPNAppLauncher"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "190.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "191.1.0"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.3"), .package(path: "../AppLauncher"), .package(path: "../UDSHelper"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index dc054cbeb6..3484bafa20 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: "190.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "191.1.0"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/UITests/BookmarkSearchTests.swift b/UITests/BookmarkSearchTests.swift new file mode 100644 index 0000000000..79f2395ddf --- /dev/null +++ b/UITests/BookmarkSearchTests.swift @@ -0,0 +1,334 @@ +// +// BookmarkSearchTests.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 + +class BookmarkSearchTests: XCTestCase { + private var app: XCUIApplication! + + private enum AccessibilityIdentifiers { + static let bookmarkButton = "AddressBarButtonsViewController.bookmarkButton" + static let addressBarTextField = "AddressBarViewController.addressBarTextField" + static let manageBookmarksMenuItem = "MainMenu.manageBookmarksMenuItem" + static let bookmarksMenu = "MainMenu.bookmarks" + static let bookmarksPanelShortcutButton = "NavigationBarViewController.bookmarkListButton" + static let optionsButton = "NavigationBarViewController.optionsButton" + static let resetBookmarksMenuItem = "MainMenu.resetBookmarks" + static let searchBookmarksButton = "BookmarkListViewController.searchBookmarksButton" + static let bookmarksPanelSearchBar = "BookmarkListViewController.searchBar" + static let bookmarksManagerSearchBar = "BookmarkManagementDetailViewController.searchBar" + static let emptyStateTitle = "BookmarksEmptyStateContent.emptyStateTitle" + static let emptyStateMessage = "BookmarksEmptyStateContent.emptyStateMessage" + static let emptyStateImageView = "BookmarksEmptyStateContent.emptyStateImageView" + static let newFolderButton = "BookmarkListViewController.newFolderButton" + } + + override class func setUp() { + UITests.firstRun() + } + + override func setUpWithError() throws { + continueAfterFailure = false + app = XCUIApplication() + app.launchEnvironment["UITEST_MODE"] = "1" + app.launch() + app.resetBookmarks() + enforceSingleWindow() + } + + // MARK: - Tests + + func testEmptyStateWhenSearchingInPanel() { + addBookmarkAndOpenBookmarksPanel(bookmarkPageTitle: "Bookmark #1") + verifyEmptyState(in: app.popovers.firstMatch, with: AccessibilityIdentifiers.bookmarksPanelSearchBar, mode: .panel) + } + + func testEmptyStateWhenSearchingInManager() { + addBookmarkAndOpenBookmarksManager(bookmarkPageTitle: "Bookmark #1") + verifyEmptyState(in: app, with: AccessibilityIdentifiers.bookmarksManagerSearchBar, mode: .manager) + } + + func testFilteredResultsInPanel() { + addThreeBookmarks() + closeShowBookmarksBarAlert() + openBookmarksPanel() + searchInBookmarksPanel(for: "Bookmark #2") + assertOnlyBookmarkExists(on: app.outlines.firstMatch, bookmarkTitle: "Bookmark #2") + } + + func testFilteredResultsInManager() { + addThreeBookmarks() + openBookmarksManager() + searchInBookmarksManager(for: "Bookmark #2") + assertOnlyBookmarkExists(on: app.tables.firstMatch, bookmarkTitle: "Bookmark #2") + } + + func testShowInFolderFunctionalityOnBookmarksPanel() { + testShowInFolderFunctionality(in: .panel) + } + + func testShowInFolderFunctionalityOnBookmarksManager() { + testShowInFolderFunctionality(in: .manager) + } + + func testDragAndDropToReorderIsNotPossibleWhenInSearchOnBookmarksPanel() { + testDragAndDropToReorder(in: .panel) + } + + func testDragAndDropToReorderIsNotPossibleWhenInSearchOnBookmarksManager() { + testDragAndDropToReorder(in: .manager) + } + + func testSearchActionIsHiddenOnBookmarksPanelWhenUserHasNoBookmarks() { + openBookmarksPanel() + let bookmarksPanelPopover = app.popovers.firstMatch + XCTAssertFalse(bookmarksPanelPopover.buttons[AccessibilityIdentifiers.searchBookmarksButton].exists) + } + + // MARK: - Utilities + + private func enforceSingleWindow() { + app.typeKey("w", modifierFlags: [.command, .option, .shift]) + app.typeKey("n", modifierFlags: .command) + } + + private func addBookmarkAndOpenBookmarksPanel(bookmarkPageTitle: String, in folder: String? = nil) { + addBookmark(pageTitle: bookmarkPageTitle, in: folder) + closeShowBookmarksBarAlert() + openBookmarksPanel() + } + + private func addBookmarkAndOpenBookmarksManager(bookmarkPageTitle: String, in folder: String? = nil) { + addBookmark(pageTitle: bookmarkPageTitle, in: folder) + openBookmarksManager() + } + + private func addThreeBookmarks() { + ["Bookmark #1", "Bookmark #2", "Bookmark #3"].forEach { + addBookmark(pageTitle: $0) + openNewTab() + } + } + + private func addBookmark(pageTitle: String, in folder: String? = nil) { + let urlForBookmarksBar = UITests.simpleServedPage(titled: pageTitle) + app.openSiteToBookmark(app: app, + url: urlForBookmarksBar, + pageTitle: pageTitle, + bookmarkingViaDialog: true, + escapingDialog: true, + folderName: folder) + } + + private func searchInBookmarksPanel(for title: String) { + bringFocusToBookmarksPanelSearchBar() + app.popovers.firstMatch.searchFields[AccessibilityIdentifiers.bookmarksPanelSearchBar].typeText(title) + } + + private func searchInBookmarksManager(for title: String) { + let searchField = app.searchFields[AccessibilityIdentifiers.bookmarksManagerSearchBar] + searchField.tap() + searchField.typeText(title) + } + + private func assertOnlyBookmarkExists(on element: XCUIElement, bookmarkTitle: String) { + XCTAssertTrue(element.staticTexts[bookmarkTitle].exists) + // Assert that other bookmarks do not exist + ["Bookmark #1", "Bookmark #2", "Bookmark #3"].filter { $0 != bookmarkTitle }.forEach { + XCTAssertFalse(element.staticTexts[$0].exists) + } + } + + private func verifyEmptyState(in element: XCUIElement, with accessibilityIdentifier: String, mode: BookmarkMode) { + if mode == .panel { + searchInBookmarksPanel(for: "No results") + } else { + searchInBookmarksManager(for: "No results") + } + assertEmptyState(in: element) + } + + private func assertEmptyState(in element: XCUIElement) { + let emptyStateTitle = element.staticTexts[AccessibilityIdentifiers.emptyStateTitle] + let emptyStateDescription = element.staticTexts[AccessibilityIdentifiers.emptyStateMessage] + let emptyStateImage = element.images[AccessibilityIdentifiers.emptyStateImageView] + + XCTAssertTrue(emptyStateImage.exists, "The empty state image does not exist.") + XCTAssertTrue(emptyStateTitle.exists, "The empty state title does not exist.") + XCTAssertTrue(emptyStateDescription.exists, "The empty state description does not exist.") + + XCTAssertEqual(emptyStateTitle.value as? String, "No bookmarks found") + XCTAssertEqual(emptyStateDescription.value as? String, "Try different search terms.") + } + + private func bringFocusToBookmarksPanelSearchBar() { + let popover = app.popovers.firstMatch + popover.buttons[AccessibilityIdentifiers.searchBookmarksButton].tap() + } + + private func openBookmarksPanel() { + app.showAndTapBookmarksPanelShortcut() + } + + private func openBookmarksManager() { + app.openBookmarksManager() + } + + private func openNewTab() { + app.typeKey("t", modifierFlags: .command) + } + + private func testShowInFolderFunctionality(in mode: BookmarkMode) { + createFolderWithSubFolder() + openNewTab() + addBookmark(pageTitle: "Bookmark #1", in: "Folder #2") + closeShowBookmarksBarAlert() + + if mode == .panel { + openBookmarksPanel() + searchInBookmarksPanel(for: "Bookmark #1") + } else { + openBookmarksManager() + searchInBookmarksManager(for: "Bookmark #1") + } + + let result = app.staticTexts["Bookmark #1"] + result.rightClick() + let showInFolderMenuItem = app.menuItems["Show in Folder"] + XCTAssertTrue(showInFolderMenuItem.exists) + showInFolderMenuItem.tap() + + assertSearchBarVisibilityAfterShowInFolder(mode: mode) + assertFolderStructure(mode: mode) + } + + private func assertSearchBarVisibilityAfterShowInFolder(mode: BookmarkMode) { + if mode == .panel { + XCTAssertFalse(app.popovers.firstMatch.searchFields[AccessibilityIdentifiers.bookmarksPanelSearchBar].exists) + } else { + XCTAssertEqual(app.searchFields[AccessibilityIdentifiers.bookmarksManagerSearchBar].value as? String, "") + } + } + + private func assertFolderStructure(mode: BookmarkMode) { + let treeBookmarks: XCUIElement = mode == .panel ? app.popovers.firstMatch.outlines.firstMatch : app.outlines.firstMatch + + XCTAssertTrue(treeBookmarks.staticTexts["Folder #1"].exists) + if mode == .panel { + XCTAssertTrue(treeBookmarks.staticTexts["Bookmark #1"].exists) + XCTAssertTrue(treeBookmarks.staticTexts["Folder #2"].exists) + } else { + /// On the bookmarks manager the sidebar tree structure only has folders while the list has what's inside the selected folder in the tree. + XCTAssertTrue(treeBookmarks.staticTexts["Folder #2"].exists) + let bookmarksList = app.tables.firstMatch + XCTAssertTrue(bookmarksList.staticTexts["Bookmark #1"].exists) + } + } + + private func testDragAndDropToReorder(in mode: BookmarkMode) { + addThreeBookmarks() + if mode == .panel { + closeShowBookmarksBarAlert() + openBookmarksPanel() + } else { + openBookmarksManager() + } + searchInBookmarks(mode: mode) + + let thirdBookmarkCell = getThirdBookmarkCell(mode: mode) + dragAndDropBookmark(thirdBookmarkCell, mode: mode) + + if mode == .panel { + bringFocusToBookmarksPanelSearchBar() + } else { + clearSearchInBookmarksManager() + } + + verifyBookmarkOrder(expectedOrder: ["Bookmark #1", "Bookmark #2", "Bookmark #3"], mode: mode) + } + + private func searchInBookmarks(mode: BookmarkMode) { + if mode == .panel { + searchInBookmarksPanel(for: "Bookmark") + } else { + searchInBookmarksManager(for: "Bookmark") + } + } + + private func getThirdBookmarkCell(mode: BookmarkMode) -> XCUIElement { + if mode == .panel { + let treeBookmarks = app.popovers.firstMatch.outlines.firstMatch + return treeBookmarks.staticTexts["Bookmark #3"] + } else { + let bookmarksSearchResultsList = app.tables.firstMatch + return bookmarksSearchResultsList.staticTexts["Bookmark #3"] + } + } + + private func dragAndDropBookmark(_ thirdBookmarkCell: XCUIElement, mode: BookmarkMode) { + let startCoordinate = thirdBookmarkCell.coordinate(withNormalizedOffset: .zero) + let targetCoordinate = (mode == .panel ? app.popovers.firstMatch.outlines.firstMatch : app.tables.firstMatch).coordinate(withNormalizedOffset: .zero) + startCoordinate.press(forDuration: 0.1, thenDragTo: targetCoordinate) + } + + private func clearSearchInBookmarksManager() { + let searchField = app.searchFields[AccessibilityIdentifiers.bookmarksManagerSearchBar] + searchField.doubleTap() + app.typeKey(.delete, modifierFlags: []) + } + + private func verifyBookmarkOrder(expectedOrder: [String], mode: BookmarkMode) { + let rowCount = (mode == .panel ? app.popovers.firstMatch.outlines.firstMatch : app.tables.firstMatch).cells.count + XCTAssertEqual(rowCount, expectedOrder.count, "Row count does not match expected count.") + + for index in 0.. Void)? = nil) { + completionHandler?(evaluateJavaScriptResult, nil) + } +} diff --git a/fastlane/Pluginfile b/fastlane/Pluginfile index cd249a9c24..745ff05f20 100644 --- a/fastlane/Pluginfile +++ b/fastlane/Pluginfile @@ -2,4 +2,4 @@ # # Ensure this file is checked in to source control! -gem 'fastlane-plugin-ddg_apple_automation', git: 'https://github.com/duckduckgo/fastlane-plugin-ddg_apple_automation', tag: '0.2.0' +gem 'fastlane-plugin-ddg_apple_automation', git: 'https://github.com/duckduckgo/fastlane-plugin-ddg_apple_automation', tag: '0.5.0'