From 2abb32f06073b79d8a8b9594f4ba4d8daba1d4c6 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Thu, 25 Apr 2024 19:26:04 -0700 Subject: [PATCH 01/16] Add subscription status to the macOS metadata (#2680) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207144276620677/f Tech Design URL: CC: Description: This PR adds the Privacy Pro status to the VPN metadata. It also reports if the user was a beta user (i.e. they have an old-style auth token) just in case we see anything strange related to that. --- .../VPNMetadataCollector.swift | 28 ++++++++++++++++++- .../VPNFeedbackFormViewModelTests.swift | 9 +++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift b/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift index b4e463df17..925ac292d2 100644 --- a/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift +++ b/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift @@ -24,6 +24,7 @@ import NetworkProtection import NetworkExtension import NetworkProtectionIPC import NetworkProtectionUI +import Subscription struct VPNMetadata: Encodable { @@ -72,12 +73,19 @@ struct VPNMetadata: Encodable { let notificationsAgentIsRunning: Bool } + struct PrivacyProInfo: Encodable { + let betaParticipant: Bool + let hasPrivacyProAccount: Bool + let hasVPNEntitlement: Bool + } + let appInfo: AppInfo let deviceInfo: DeviceInfo let networkInfo: NetworkInfo let vpnState: VPNState let vpnSettingsState: VPNSettingsState let loginItemState: LoginItemState + let privacyProInfo: PrivacyProInfo func toPrettyPrintedJSON() -> String? { let encoder = JSONEncoder() @@ -138,6 +146,7 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { let vpnState = await collectVPNState() let vpnSettingsState = collectVPNSettingsState() let loginItemState = collectLoginItemState() + let privacyProInfo = await collectPrivacyProInfo() return VPNMetadata( appInfo: appInfoMetadata, @@ -145,7 +154,8 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { networkInfo: networkInfoMetadata, vpnState: vpnState, vpnSettingsState: vpnSettingsState, - loginItemState: loginItemState + loginItemState: loginItemState, + privacyProInfo: privacyProInfo ) } @@ -283,4 +293,20 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { ) } + func collectPrivacyProInfo() async -> VPNMetadata.PrivacyProInfo { + let accountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) + let waitlistStore = WaitlistKeychainStore( + waitlistIdentifier: NetworkProtectionWaitlist.identifier, + keychainAppGroup: NetworkProtectionWaitlist.keychainAppGroup + ) + + let hasVPNEntitlement = (try? await accountManager.hasEntitlement(for: .networkProtection).get()) ?? false + + return .init( + betaParticipant: waitlistStore.isInvited, + hasPrivacyProAccount: accountManager.isUserAuthenticated, + hasVPNEntitlement: hasVPNEntitlement + ) + } + } diff --git a/UnitTests/VPNFeedbackForm/VPNFeedbackFormViewModelTests.swift b/UnitTests/VPNFeedbackForm/VPNFeedbackFormViewModelTests.swift index f5f727456e..ee1758f401 100644 --- a/UnitTests/VPNFeedbackForm/VPNFeedbackFormViewModelTests.swift +++ b/UnitTests/VPNFeedbackForm/VPNFeedbackFormViewModelTests.swift @@ -126,13 +126,20 @@ private class MockVPNMetadataCollector: VPNMetadataCollector { notificationsAgentIsRunning: true ) + let privacyProInfo = VPNMetadata.PrivacyProInfo( + betaParticipant: false, + hasPrivacyProAccount: true, + hasVPNEntitlement: true + ) + return VPNMetadata( appInfo: appInfo, deviceInfo: deviceInfo, networkInfo: networkInfo, vpnState: vpnState, vpnSettingsState: vpnSettingsState, - loginItemState: loginItemState + loginItemState: loginItemState, + privacyProInfo: privacyProInfo ) } From 0215160228c0c905044ee649f25e435570661122 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Fri, 26 Apr 2024 11:35:32 +0200 Subject: [PATCH 02/16] Hard-codes the VPN waitlist flags to ON (#2709) Task/Issue URL: https://app.asana.com/0/0/1207169061635762/f ## Description Hardcodes the waitlist remote flags to be ON, since we're relying on those flags for the PPro subscription. This is meant to be a quick fix, and a better cleanup will follow. --- .../NetworkProtectionFeatureVisibility.swift | 22 ++----------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift b/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift index c792a3e788..11bf4a988e 100644 --- a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift +++ b/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift @@ -166,29 +166,11 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { } private var isWaitlistBetaActive: Bool { - switch featureOverrides.waitlistActive { - case .useRemoteValue: - guard privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(NetworkProtectionSubfeature.waitlistBetaActive) else { - return false - } - - return true - case .on: - return true - case .off: - return false - } + true } private var isWaitlistEnabled: Bool { - switch featureOverrides.waitlistEnabled { - case .useRemoteValue: - return privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(NetworkProtectionSubfeature.waitlist) - case .on: - return true - case .off: - return false - } + true } func disableForAllUsers() async { From a072d5906a94e0838f0d714543581674edcc449b Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Fri, 26 Apr 2024 15:23:22 +0200 Subject: [PATCH 03/16] Fix for VPN stop issues. (#2689) Task/Issue URL: https://app.asana.com/0/0/1207063924984126/f iOS PR: Not needed BSK PR: https://github.com/duckduckgo/BrowserServicesKit/pull/794 ## Description: Tentative fix for an issue where the tunnel provider is taking a long time to stop. --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 0adc19521d..92763f1ea5 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -12679,7 +12679,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 138.0.0; + version = "138.0.0-1"; }; }; 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 462a0cc553..0abfe2d2de 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "b8f0e5db431c63943b509d522c157f870ef03ae0", - "version" : "138.0.0" + "revision" : "ad13fb66e5afc2cf27496a56f5b412f83ea507ad", + "version" : "138.0.0-1" } }, { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 51d8ee4755..f0d21708fd 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: "138.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "138.0.0-1"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index ad131b9869..4ae8d01c83 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "138.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "138.0.0-1"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 9092bb8c50..1ba19f05f3 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "138.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "138.0.0-1"), .package(path: "../SwiftUIExtensions") ], targets: [ From d931d5c2002cb0a412880973859f28aee126f1f3 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Fri, 26 Apr 2024 16:27:14 +0200 Subject: [PATCH 04/16] Update release automation to support Privacy Pro section in release notes (#2710) Task/Issue URL: https://app.asana.com/0/1177771139624306/1207157777571909/f Description: This change makes it possible to display a separate section in release notes, dedicated to Privacy Pro: * appcastManager.swift was updated to take pre-formatted HTML document with release notes * extract_release_notes.sh was updated to generate HTML or Asana rich text release notes, in addition to raw output. * update_asana_for_release.sh was updated to rely on extract_release_notes.sh for generating release notes for the Asana release task. * workflows were updated to use extract_release_notes.sh to generate HTML documents * extract_release_notes.sh is now unit-tested using Bats (Bash Automated Testing System). * PR checks workflow is updated with a job dedicated to running shell scripts unit tests. --- .github/workflows/bump_internal_release.yml | 8 +- .github/workflows/code_freeze.yml | 1 + .github/workflows/pr.yml | 34 +- .github/workflows/publish_dmg_release.yml | 18 +- scripts/appcast_manager/appcastManager.swift | 48 ++- scripts/extract_release_notes.sh | 133 ++++++- .../extract_release_notes.bats | 349 ++++++++++++++++++ scripts/update_asana_for_release.sh | 31 +- 8 files changed, 565 insertions(+), 57 deletions(-) create mode 100644 scripts/tests/extract_release_notes/extract_release_notes.bats diff --git a/.github/workflows/bump_internal_release.yml b/.github/workflows/bump_internal_release.yml index 4d0d9f440a..13280743b5 100644 --- a/.github/workflows/bump_internal_release.yml +++ b/.github/workflows/bump_internal_release.yml @@ -118,10 +118,10 @@ jobs: curl -fLSs "https://app.asana.com/api/1.0/tasks/${TASK_ID}?opt_fields=notes" \ -H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}" \ | jq -r .data.notes \ - | ./scripts/extract_release_notes.sh > release_notes.txt - release_notes="$(" ]]; then - echo "::error::Release notes are empty. Please add release notes to the Asana task and restart the workflow." + | ./scripts/extract_release_notes.sh -r > raw_release_notes.txt + raw_release_notes="$("* ]]; then + echo "::error::Release notes are empty or contain a placeholder. Please add release notes to the Asana task and restart the workflow." exit 1 fi diff --git a/.github/workflows/code_freeze.yml b/.github/workflows/code_freeze.yml index 2da53a932a..755282af5e 100644 --- a/.github/workflows/code_freeze.yml +++ b/.github/workflows/code_freeze.yml @@ -172,6 +172,7 @@ jobs: uses: ./.github/workflows/tag_release.yml with: asana-task-url: ${{ needs.create_release_branch.outputs.asana_task_url }} + base-branch: ${{ github.ref_name }} branch: ${{ needs.create_release_branch.outputs.release_branch_name }} prerelease: true secrets: diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 528b219281..fd2341e1b6 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -54,6 +54,36 @@ jobs: env: SHELLCHECK_OPTS: -x -P scripts -P scripts/helpers + bats: + + name: Test Shell Scripts + + runs-on: macos-13 + + steps: + - name: Check out the code + if: github.event_name == 'pull_request' || github.event_name == 'push' + uses: actions/checkout@v4 + + - name: Check out the code + if: github.event_name != 'pull_request' && github.event_name != 'push' + uses: actions/checkout@v4 + with: + ref: ${{ inputs.branch || github.ref_name }} + + - name: Install Bats + run: brew install bats-core + + - name: Run Bats tests + run: bats --formatter junit scripts/tests/* > bats-tests.xml + + - name: Publish unit tests report + uses: mikepenz/action-junit-report@v3 + if: always() # always run even if the previous step fails + with: + check_name: "Test Report: Shell Scripts" + report_paths: 'bats-tests.xml' + tests: name: Test @@ -343,7 +373,7 @@ jobs: create-asana-task: name: Create Asana Task - needs: [swiftlint, tests, release-build, verify-autoconsent-bundle, private-api] + needs: [swiftlint, bats, tests, release-build, verify-autoconsent-bundle, private-api] if: failure() && github.ref_name == 'main' && github.run_attempt == 1 @@ -360,7 +390,7 @@ jobs: close-asana-task: name: Close Asana Task - needs: [swiftlint, tests, release-build, verify-autoconsent-bundle, private-api] + needs: [swiftlint, bats, tests, release-build, verify-autoconsent-bundle, private-api] if: success() && github.ref_name == 'main' && github.run_attempt > 1 diff --git a/.github/workflows/publish_dmg_release.yml b/.github/workflows/publish_dmg_release.yml index 8daecea7be..2dd82d53ed 100644 --- a/.github/workflows/publish_dmg_release.yml +++ b/.github/workflows/publish_dmg_release.yml @@ -145,14 +145,14 @@ jobs: run: | curl -fLSs "https://app.asana.com/api/1.0/tasks/${TASK_ID}?opt_fields=notes" \ -H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}" \ - | jq -r .data.notes \ - | ./scripts/extract_release_notes.sh > release_notes.txt - release_notes="$(" ]]; then - echo "::error::Release notes are empty. Please add release notes to the Asana task and restart the workflow." + | jq -r .data.notes > release_task_content.txt + raw_release_notes="$(./scripts/extract_release_notes.sh -r < release_task_content.txt)" + if [[ ${#raw_release_notes} == 0 || "$raw_release_notes" == *"<-- Add release notes here -->"* ]]; then + echo "::error::Release notes are empty or contain a placeholder. Please add release notes to the Asana task and restart the workflow." exit 1 fi - echo "RELEASE_NOTES_FILE=release_notes.txt" >> $GITHUB_ENV + ./scripts/extract_release_notes.sh < release_task_content.txt > release_notes.html + echo "RELEASE_NOTES_FILE=release_notes.html" >> $GITHUB_ENV - name: Set up Sparkle tools env: @@ -189,21 +189,21 @@ jobs: ./scripts/appcast_manager/appcastManager.swift \ --release-to-internal-channel \ --dmg ${DMG_PATH} \ - --release-notes release_notes.txt \ + --release-notes-html release_notes.html \ --key sparkle_private_key ;; "public") ./scripts/appcast_manager/appcastManager.swift \ --release-to-public-channel \ --version ${VERSION} \ - --release-notes release_notes.txt \ + --release-notes-html release_notes.html \ --key sparkle_private_key ;; "hotfix") ./scripts/appcast_manager/appcastManager.swift \ --release-hotfix-to-public-channel \ --dmg ${DMG_PATH} \ - --release-notes release_notes.txt \ + --release-notes-html release_notes.html \ --key sparkle_private_key ;; *) diff --git a/scripts/appcast_manager/appcastManager.swift b/scripts/appcast_manager/appcastManager.swift index c3906a3cae..e7d28209e5 100755 --- a/scripts/appcast_manager/appcastManager.swift +++ b/scripts/appcast_manager/appcastManager.swift @@ -80,6 +80,9 @@ SYNOPSIS appcastManager --release-to-internal-channel --dmg --release-notes [--key ] appcastManager --release-to-public-channel --version [--release-notes ] [--key ] appcastManager --release-hotfix-to-public-channel --dmg --release-notes [--key ] + appcastManager --release-to-internal-channel --dmg --release-notes-html [--key ] + appcastManager --release-to-public-channel --version [--release-notes-html ] [--key ] + appcastManager --release-hotfix-to-public-channel --dmg --release-notes-html [--key ] appcastManager --help DESCRIPTION @@ -109,7 +112,13 @@ DESCRIPTION exit(0) case .releaseToInternalChannel, .releaseHotfixToPublicChannel: - guard let dmgPath = arguments.parameters["--dmg"], let releaseNotesPath = arguments.parameters["--release-notes"] else { + guard let dmgPath = arguments.parameters["--dmg"] else { + print("Missing required parameters") + exit(1) + } + let releaseNotesPath = arguments.parameters["--release-notes"] + let releaseNotesHTMLPath = arguments.parameters["--release-notes-html"] + guard releaseNotesPath != nil || releaseNotesHTMLPath != nil else { print("Missing required parameters") exit(1) } @@ -117,7 +126,11 @@ case .releaseToInternalChannel, .releaseHotfixToPublicChannel: print("➡️ Action: Add to internal channel") print("➡️ DMG Path: \(dmgPath)") - print("➡️ Release Notes Path: \(releaseNotesPath)") + if let releaseNotesPath { + print("➡️ Release Notes Path: \(releaseNotesPath)") + } else if let releaseNotesHTMLPath { + print("➡️ Release Notes HTML Path: \(releaseNotesHTMLPath)") + } if isCI, let keyFile { print("➡️ Key file: \(keyFile)") } @@ -130,7 +143,11 @@ case .releaseToInternalChannel, .releaseHotfixToPublicChannel: } // Handle release notes file - handleReleaseNotesFile(path: releaseNotesPath, updatesDirectoryURL: specificDir, dmgURL: dmgURL) + if let releaseNotesPath { + handleReleaseNotesFile(path: releaseNotesPath, updatesDirectoryURL: specificDir, dmgURL: dmgURL) + } else if let releaseNotesHTMLPath { + handleReleaseNotesHTML(path: releaseNotesHTMLPath, updatesDirectoryURL: specificDir, dmgURL: dmgURL) + } // Extract version number from DMG file name let versionNumber = getVersionNumberFromDMGFileName(dmgURL: dmgURL) @@ -170,6 +187,10 @@ case .releaseToPublicChannel: print("Release Notes Path: \(releaseNotesPath)") let dmgURLForPublic = specificDir.appendingPathComponent(dmgFileName) handleReleaseNotesFile(path: releaseNotesPath, updatesDirectoryURL: specificDir, dmgURL: dmgURLForPublic) + } else if let releaseNotesHTMLPath = arguments.parameters["--release-notes-html"] { + print("Release Notes Path: \(releaseNotesHTMLPath)") + let dmgURLForPublic = specificDir.appendingPathComponent(dmgFileName) + handleReleaseNotesHTML(path: releaseNotesHTMLPath, updatesDirectoryURL: specificDir, dmgURL: dmgURLForPublic) } else { print("👀 No new release notes provided. Keeping existing release notes.") } @@ -605,6 +626,27 @@ final class AppcastDownloader { // MARK: - Handling of Release Notes +func handleReleaseNotesHTML(path: String, updatesDirectoryURL: URL, dmgURL: URL) { + // Copy release notes file and rename it to match the dmg filename + let releaseNotesURL = URL(fileURLWithPath: path) + let destinationReleaseNotesURL = updatesDirectoryURL.appendingPathComponent(dmgURL.deletingPathExtension().lastPathComponent + ".html") + + do { + if FileManager.default.fileExists(atPath: destinationReleaseNotesURL.path) { + try FileManager.default.removeItem(at: destinationReleaseNotesURL) + print("Old release notes file removed.") + } + + // Save the converted release notes to the destination file + try FileManager.default.copyItem(at: releaseNotesURL, to: destinationReleaseNotesURL) + print("✅ New release notes HTML file copied to the updates directory.") + + } catch { + print("❌ Failed to copy and convert release notes HTML file: \(error).") + exit(1) + } +} + func handleReleaseNotesFile(path: String, updatesDirectoryURL: URL, dmgURL: URL) { // Copy release notes file and rename it to match the dmg filename let releaseNotesURL = URL(fileURLWithPath: path) diff --git a/scripts/extract_release_notes.sh b/scripts/extract_release_notes.sh index 9c7db812a6..f995620dfe 100755 --- a/scripts/extract_release_notes.sh +++ b/scripts/extract_release_notes.sh @@ -7,30 +7,133 @@ # start_marker="release notes" +pp_marker="^for privacy pro subscribers:?$" end_marker="this release includes:" +placeholder="add release notes here" is_capturing=0 +is_capturing_pp=0 has_content=0 +notes= +pp_notes= -if [[ "$1" == "-t" ]]; then - # capture included tasks instead of release notes - start_marker="this release includes:" - end_marker= -fi +output="html" + +case "$1" in + -a) + # Generate Asana rich text output + output="asana" + ;; + -r) + # Generate raw output instead of HTML + output="raw" + ;; + -t) + # Capture raw included tasks' URLs instead of release notes + output="tasks" + start_marker="this release includes:" + pp_marker= + end_marker= + ;; + *) + ;; +esac + +html_escape() { + local input="$1" + sed -e 's/&/\&/g' -e 's//\>/g' <<< "$input" +} + +make_links() { + local input="$1" + sed -E 's|(https://[^ ]*)|\1|' <<< "$input" +} + +lowercase() { + local input="$1" + tr '[:upper:]' '[:lower:]' <<< "$input" +} + +print_and_exit() { + echo -ne "$notes" + exit 0 +} + +add_to_notes() { + notes+="$1" + if [[ "$output" != "asana" ]]; then + notes+="\\n" + fi +} + +add_to_pp_notes() { + pp_notes+="$1" + if [[ "$output" != "asana" ]]; then + pp_notes+="\\n" + fi +} + +add_release_note() { + local release_note="$1" + local processed_release_note= + if [[ "$output" == "raw" || "$output" == "tasks" ]]; then + processed_release_note="$release_note" + else + processed_release_note="
  • $(make_links "$(html_escape "$release_note")")
  • " + fi + if [[ $is_capturing_pp -eq 1 ]]; then + add_to_pp_notes "$processed_release_note" + else + add_to_notes "$processed_release_note" + fi +} while read -r line do - if [[ $(tr '[:upper:]' '[:lower:]' <<< "$line") == "$start_marker" ]]; then - is_capturing=1 - elif [[ -n "$end_marker" && $(tr '[:upper:]' '[:lower:]' <<< "$line") == "$end_marker" ]]; then - exit 0 - elif [[ $is_capturing -eq 1 && -n "$line" ]]; then - has_content=1 - echo "$line" - fi + # Lowercase each line to compare with markers + lowercase_line="$(lowercase "$line")" + + if [[ "$lowercase_line" == "$start_marker" ]]; then + # Only start capturing here + is_capturing=1 + if [[ "$output" == "asana" ]]; then + add_to_notes "
      " + elif [[ "$output" == "html" ]]; then + # Add HTML header and start the list + add_to_notes "

      What's new

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

      For Privacy Pro subscribers

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

      For Privacy Pro subscribers

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

      What's new

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

      What's new

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

      For Privacy Pro subscribers

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

      For Privacy Pro subscribers

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

      What's new

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

        Release notes

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

        This release includes:

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

      Release notes

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

      This release includes:

      ' @@ -282,10 +269,8 @@ handle_internal_release() { done <<< "$(find_task_urls_in_git_log "$last_release_tag")" # 2. Fetch current release notes from Asana release task. - local release_notes=() - while read -r line; do - release_notes+=("$line") - done <<< "$(fetch_current_release_notes "${release_task_id}")" + local release_notes + release_notes="$(fetch_current_release_notes "${release_task_id}")" # 3. Construct new release task description local html_notes @@ -325,10 +310,8 @@ handle_public_release() { complete_tasks "${task_ids[@]}" # 5. Fetch current release notes from Asana release task. - local release_notes=() - while read -r line; do - release_notes+=("$line") - done <<< "$(fetch_current_release_notes "${release_task_id}")" + local release_notes + release_notes="$(fetch_current_release_notes "${release_task_id}")" # 6. Construct release announcement task description local html_notes From ec9aa0268e48e1f29bbe9645e08f619f25a76bf3 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Fri, 26 Apr 2024 14:41:25 +0000 Subject: [PATCH 05/16] Bump version to 1.85.0 (176) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index e5dcb60ef4..fe80d801b7 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 175 +CURRENT_PROJECT_VERSION = 176 From 78f919cd88e6158a168af313d17b1a6ec56c2e99 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Fri, 26 Apr 2024 21:21:30 +0600 Subject: [PATCH 06/16] remove Tab.TabContent.url (#2647) Task/Issue URL: https://app.asana.com/0/0/1207078384259685/f --- .../Autoconsent/AutoconsentUserScript.swift | 2 +- .../Common/Extensions/URLExtension.swift | 5 +++ .../View/FeedbackViewController.swift | 2 +- .../FireproofingURLExtensions.swift | 2 +- .../AddressBarButtonsViewController.swift | 22 +++++------ .../View/AddressBarTextField.swift | 2 +- .../View/AddressBarViewController.swift | 6 +-- .../NavigationBar/View/MoreOptionsMenu.swift | 37 +++++++++++-------- .../View/NavigationBarViewController.swift | 2 +- .../PinnedTabs/View/PinnedTabView.swift | 4 +- .../PrivacyDashboardPermissionHandler.swift | 2 +- .../View/PrivacyDashboardViewController.swift | 4 +- .../Model/RecentlyClosedCacheItem.swift | 2 +- .../View/RecentlyClosedMenu.swift | 2 +- DuckDuckGo/Sharing/SharingMenu.swift | 2 +- DuckDuckGo/Tab/Model/Tab.swift | 25 ++++++++----- DuckDuckGo/Tab/Model/TabContent.swift | 25 +++++++------ .../Tab/TabExtensions/TabExtensions.swift | 2 +- .../TabExtensions/TabSnapshotExtension.swift | 3 +- .../Tab/TabLazyLoader/LazyLoadable.swift | 2 +- .../Tab/View/BrowserTabViewController.swift | 2 +- DuckDuckGo/Tab/ViewModel/TabViewModel.swift | 4 +- .../TabBar/View/TabBarViewController.swift | 2 +- DuckDuckGo/TabBar/View/TabBarViewItem.swift | 2 +- .../TabPreview/TabPreviewViewController.swift | 2 +- .../View/WindowControllersManager.swift | 6 +-- ...NavigationProtectionIntegrationTests.swift | 2 +- IntegrationTests/Tab/AddressBarTests.swift | 2 +- IntegrationTests/Tab/ErrorPageTests.swift | 2 +- UnitTests/Fire/Model/FireTests.swift | 2 +- UnitTests/Tab/Model/TabTests.swift | 2 +- .../TabBar/View/TabBarViewItemTests.swift | 3 -- 32 files changed, 101 insertions(+), 83 deletions(-) diff --git a/DuckDuckGo/Autoconsent/AutoconsentUserScript.swift b/DuckDuckGo/Autoconsent/AutoconsentUserScript.swift index c43f7dc27c..2000073692 100644 --- a/DuckDuckGo/Autoconsent/AutoconsentUserScript.swift +++ b/DuckDuckGo/Autoconsent/AutoconsentUserScript.swift @@ -202,7 +202,7 @@ extension AutoconsentUserScript { return } - guard [.http, .https].contains(url.navigationalScheme) else { + guard url.navigationalScheme?.isHypertextScheme == true else { // ignore special schemes os_log("Ignoring special URL scheme: %s", log: .autoconsent, type: .debug, messageData.url) replyHandler([ "type": "ok" ], nil) // this is just to prevent a Promise rejection diff --git a/DuckDuckGo/Common/Extensions/URLExtension.swift b/DuckDuckGo/Common/Extensions/URLExtension.swift index 62b6c8fc2b..abd290dd0f 100644 --- a/DuckDuckGo/Common/Extensions/URLExtension.swift +++ b/DuckDuckGo/Common/Extensions/URLExtension.swift @@ -30,6 +30,11 @@ extension URL.NavigationalScheme { return [.http, .https, .file] } + /// HTTP or HTTPS + var isHypertextScheme: Bool { + Self.hypertextSchemes.contains(self) + } + } extension URL { diff --git a/DuckDuckGo/Feedback/View/FeedbackViewController.swift b/DuckDuckGo/Feedback/View/FeedbackViewController.swift index ff45c1a7e9..6bec572e75 100644 --- a/DuckDuckGo/Feedback/View/FeedbackViewController.swift +++ b/DuckDuckGo/Feedback/View/FeedbackViewController.swift @@ -75,7 +75,7 @@ final class FeedbackViewController: NSViewController { var currentTab: Tab? var currentTabUrl: URL? { - guard let url = currentTab?.content.url else { + guard let url = currentTab?.content.urlForWebView else { return nil } diff --git a/DuckDuckGo/Fireproofing/Extensions/FireproofingURLExtensions.swift b/DuckDuckGo/Fireproofing/Extensions/FireproofingURLExtensions.swift index d75068166c..da4b9df331 100644 --- a/DuckDuckGo/Fireproofing/Extensions/FireproofingURLExtensions.swift +++ b/DuckDuckGo/Fireproofing/Extensions/FireproofingURLExtensions.swift @@ -52,7 +52,7 @@ extension URL { ] var canFireproof: Bool { - guard let host = self.host else { return false } + guard let host = self.host, self.navigationalScheme?.isHypertextScheme == true else { return false } return (host != Self.cookieDomain) } diff --git a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift index 566a7c413f..878114b49b 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift @@ -279,7 +279,7 @@ final class AddressBarButtonsViewController: NSViewController { guard let tabViewModel, tabViewModel.canBeBookmarked else { return false } var isUrlBookmarked = false - if let url = tabViewModel.tab.content.url, + if let url = tabViewModel.tab.content.userEditableUrl, bookmarkManager.isUrlBookmarked(url: url) { isUrlBookmarked = true } @@ -413,7 +413,7 @@ final class AddressBarButtonsViewController: NSViewController { permissions.microphone = tabViewModel.usedPermissions.microphone } - let url = tabViewModel.tab.content.url ?? .empty + let url = tabViewModel.tab.content.urlForWebView ?? .empty let domain = url.isFileURL ? .localhost : (url.host ?? "") PermissionContextMenu(permissions: permissions.map { ($0, $1) }, domain: domain, delegate: self) @@ -432,7 +432,7 @@ final class AddressBarButtonsViewController: NSViewController { return } - let url = tabViewModel.tab.content.url ?? .empty + let url = tabViewModel.tab.content.urlForWebView ?? .empty let domain = url.isFileURL ? .localhost : (url.host ?? "") PermissionContextMenu(permissions: [(.microphone, state)], domain: domain, delegate: self) @@ -451,7 +451,7 @@ final class AddressBarButtonsViewController: NSViewController { return } - let url = tabViewModel.tab.content.url ?? .empty + let url = tabViewModel.tab.content.urlForWebView ?? .empty let domain = url.isFileURL ? .localhost : (url.host ?? "") PermissionContextMenu(permissions: [(.geolocation, state)], domain: domain, delegate: self) @@ -475,7 +475,7 @@ final class AddressBarButtonsViewController: NSViewController { $0.append( (.popups, .requested($1)) ) } } else { - let url = tabViewModel.tab.content.url ?? .empty + let url = tabViewModel.tab.content.urlForWebView ?? .empty domain = url.isFileURL ? .localhost : (url.host ?? "") permissions = [(.popups, state)] } @@ -499,7 +499,7 @@ final class AddressBarButtonsViewController: NSViewController { } permissions = [(permissionType, state)] - let url = tabViewModel.tab.content.url ?? .empty + let url = tabViewModel.tab.content.urlForWebView ?? .empty let domain = url.isFileURL ? .localhost : (url.host ?? "") PermissionContextMenu(permissions: permissions, domain: domain, delegate: self) @@ -733,7 +733,7 @@ final class AddressBarButtonsViewController: NSViewController { } private func updateBookmarkButtonImage(isUrlBookmarked: Bool = false) { - if let url = tabViewModel?.tab.content.url, + if let url = tabViewModel?.tab.content.userEditableUrl, isUrlBookmarked || bookmarkManager.isUrlBookmarked(url: url) { bookmarkButton.image = .bookmarkFilled @@ -770,11 +770,11 @@ final class AddressBarButtonsViewController: NSViewController { private func updatePrivacyEntryPointButton() { guard let tabViewModel else { return } - let urlScheme = tabViewModel.tab.content.url?.scheme - let isHypertextUrl = urlScheme == "http" || urlScheme == "https" + let url = tabViewModel.tab.content.userEditableUrl + let isHypertextUrl = url?.navigationalScheme?.isHypertextScheme == true && url?.isDuckPlayer == false let isEditingMode = controllerMode?.isEditing ?? false let isTextFieldValueText = textFieldValue?.isText ?? false - let isLocalUrl = tabViewModel.tab.content.url?.isLocalURL ?? false + let isLocalUrl = url?.isLocalURL ?? false // Privacy entry point button privacyEntryPointButton.isHidden = isEditingMode @@ -922,7 +922,7 @@ final class AddressBarButtonsViewController: NSViewController { private func bookmarkForCurrentUrl(setFavorite: Bool, accessPoint: GeneralPixel.AccessPoint) -> (bookmark: Bookmark?, isNew: Bool) { guard let tabViewModel, - let url = tabViewModel.tab.content.url else { + let url = tabViewModel.tab.content.userEditableUrl else { assertionFailure("No URL for bookmarking") return (nil, false) } diff --git a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift index 21bb3b0bfa..c9692f1546 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift @@ -275,7 +275,7 @@ final class AddressBarTextField: NSTextField { guard let selectedTabViewModel = selectedTabViewModel ?? tabCollectionViewModel.selectedTabViewModel else { return } let addressBarString = addressBarString ?? selectedTabViewModel.addressBarString - let isSearch = selectedTabViewModel.tab.content.url?.isDuckDuckGoSearch ?? false + let isSearch = selectedTabViewModel.tab.content.userEditableUrl?.isDuckDuckGoSearch ?? false self.value = Value(stringValue: addressBarString, userTyped: false, isSearch: isSearch) clearUndoManager() } diff --git a/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift index 43ff508e66..cac827debd 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift @@ -239,9 +239,9 @@ final class AddressBarViewController: NSViewController { func shouldShowLoadingIndicator(for tabViewModel: TabViewModel, isLoading: Bool, error: Error?) -> Bool { if isLoading, - let url = tabViewModel.tab.content.url, - [.http, .https].contains(url.navigationalScheme), - url.isDuckDuckGoSearch == false, + let url = tabViewModel.tab.content.urlForWebView, + url.navigationalScheme?.isHypertextScheme == true, + !url.isDuckDuckGoSearch, !url.isDuckPlayer, error == nil { return true } else { diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift index ed7bb58e86..bfc0a1025b 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift @@ -407,10 +407,11 @@ final class MoreOptionsMenu: NSMenu { } private func addPageItems() { - guard let url = tabCollectionViewModel.selectedTabViewModel?.tab.content.url else { return } + guard let tabViewModel = tabCollectionViewModel.selectedTabViewModel, + let url = tabViewModel.tab.content.userEditableUrl else { return } + let oldItemsCount = items.count if url.canFireproof, let host = url.host { - let isFireproof = FireproofDomains.shared.isFireproof(fireproofDomain: host) let title = isFireproof ? UserText.removeFireproofing : UserText.fireproofSite let image: NSImage = isFireproof ? .burn : .fireproof @@ -418,25 +419,29 @@ final class MoreOptionsMenu: NSMenu { addItem(withTitle: title, action: #selector(toggleFireproofing(_:)), keyEquivalent: "") .targetting(self) .withImage(image) - } - addItem(withTitle: UserText.findInPageMenuItem, action: #selector(findInPage(_:)), keyEquivalent: "f") - .targetting(self) - .withImage(.findSearch) - .withAccessibilityIdentifier("MoreOptionsMenu.findInPage") - - addItem(withTitle: UserText.shareMenuItem, action: nil, keyEquivalent: "") - .targetting(self) - .withImage(.share) - .withSubmenu(sharingMenu) + if tabViewModel.canReload { + addItem(withTitle: UserText.findInPageMenuItem, action: #selector(findInPage(_:)), keyEquivalent: "f") + .targetting(self) + .withImage(.findSearch) + .withAccessibilityIdentifier("MoreOptionsMenu.findInPage") - addItem(withTitle: UserText.printMenuItem, action: #selector(doPrint(_:)), keyEquivalent: "") - .targetting(self) - .withImage(.print) + addItem(withTitle: UserText.shareMenuItem, action: nil, keyEquivalent: "") + .targetting(self) + .withImage(.share) + .withSubmenu(sharingMenu) + } - addItem(NSMenuItem.separator()) + if tabViewModel.canPrint { + addItem(withTitle: UserText.printMenuItem, action: #selector(doPrint(_:)), keyEquivalent: "") + .targetting(self) + .withImage(.print) + } + if items.count > oldItemsCount { + addItem(NSMenuItem.separator()) + } } private func makeNetworkProtectionItem() -> NSMenuItem { diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index 582a8b4f28..ee17298593 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -698,7 +698,7 @@ final class NavigationBarViewController: NSViewController { passwordManagementButton.menu = menu passwordManagementButton.toolTip = UserText.autofillShortcutTooltip - let url = tabCollectionViewModel.selectedTabViewModel?.tab.content.url + let url = tabCollectionViewModel.selectedTabViewModel?.tab.content.userEditableUrl passwordManagementButton.image = .passwordManagement diff --git a/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift b/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift index 3e067f1187..c9a0c87f8b 100644 --- a/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift +++ b/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift @@ -224,7 +224,9 @@ struct PinnedTabInnerView: View { .resizable() mutedTabIndicator } - } else if let domain = model.content.url?.host, let eTLDplus1 = ContentBlocking.shared.tld.eTLDplus1(domain), let firstLetter = eTLDplus1.capitalized.first.flatMap(String.init) { + } else if let domain = model.content.userEditableUrl?.host, + let eTLDplus1 = ContentBlocking.shared.tld.eTLDplus1(domain), + let firstLetter = eTLDplus1.capitalized.first.flatMap(String.init) { ZStack { Rectangle() .foregroundColor(.forString(eTLDplus1)) diff --git a/DuckDuckGo/PrivacyDashboard/PrivacyDashboardPermissionHandler.swift b/DuckDuckGo/PrivacyDashboard/PrivacyDashboardPermissionHandler.swift index f1734c4a6f..fe991eade4 100644 --- a/DuckDuckGo/PrivacyDashboard/PrivacyDashboardPermissionHandler.swift +++ b/DuckDuckGo/PrivacyDashboard/PrivacyDashboardPermissionHandler.swift @@ -60,7 +60,7 @@ final class PrivacyDashboardPermissionHandler { assertionFailure("PrivacyDashboardViewController: tabViewModel not set") return } - guard let domain = tabViewModel?.tab.content.url?.host else { + guard let domain = tabViewModel?.tab.content.urlForWebView?.host else { onPermissionChange?([]) return } diff --git a/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardViewController.swift b/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardViewController.swift index 41e1300195..befe61b87a 100644 --- a/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardViewController.swift +++ b/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardViewController.swift @@ -325,7 +325,7 @@ extension PrivacyDashboardViewController { // ⚠️ To limit privacy risk, site URL is trimmed to not include query and fragment guard let currentTab = tabViewModel?.tab, - let currentURL = currentTab.content.url?.trimmingQueryItemsAndFragment() else { + let currentURL = currentTab.content.urlForWebView?.trimmingQueryItemsAndFragment() else { throw BrokenSiteReportError.failedToFetchTheCurrentURL } let blockedTrackerDomains = currentTab.privacyInfo?.trackerInfo.trackersBlocked.compactMap { $0.domain } ?? [] @@ -335,7 +335,7 @@ extension PrivacyDashboardViewController { // current domain's protection status let configuration = ContentBlocking.shared.privacyConfigurationManager.privacyConfig - let protectionsState = configuration.isFeature(.contentBlocking, enabledForDomain: currentTab.content.url?.host) + let protectionsState = configuration.isFeature(.contentBlocking, enabledForDomain: currentTab.content.urlForWebView?.host) let webVitals = await calculateWebVitals(performanceMetrics: currentTab.brokenSiteInfo?.performanceMetrics, privacyConfig: configuration) diff --git a/DuckDuckGo/RecentlyClosed/Model/RecentlyClosedCacheItem.swift b/DuckDuckGo/RecentlyClosed/Model/RecentlyClosedCacheItem.swift index 3314b3da2a..2d313b152c 100644 --- a/DuckDuckGo/RecentlyClosed/Model/RecentlyClosedCacheItem.swift +++ b/DuckDuckGo/RecentlyClosed/Model/RecentlyClosedCacheItem.swift @@ -35,7 +35,7 @@ extension RecentlyClosedTab: RecentlyClosedCacheItemBurning { } private func contentContainsDomains(_ baseDomains: Set, tld: TLD) -> Bool { - if let host = tabContent.url?.host, let baseDomain = tld.eTLDplus1(host), baseDomains.contains(baseDomain) { + if let host = tabContent.urlForWebView?.host, let baseDomain = tld.eTLDplus1(host), baseDomains.contains(baseDomain) { return true } else { return false diff --git a/DuckDuckGo/RecentlyClosed/View/RecentlyClosedMenu.swift b/DuckDuckGo/RecentlyClosed/View/RecentlyClosedMenu.swift index cdbeec8739..3c24609a30 100644 --- a/DuckDuckGo/RecentlyClosed/View/RecentlyClosedMenu.swift +++ b/DuckDuckGo/RecentlyClosed/View/RecentlyClosedMenu.swift @@ -79,7 +79,7 @@ private extension NSMenuItem { case .url, .subscription, .identityTheftRestoration: image = recentlyClosedTab.favicon image?.size = NSSize.faviconSize - title = recentlyClosedTab.title ?? recentlyClosedTab.tabContent.url?.absoluteString ?? "" + title = recentlyClosedTab.title ?? recentlyClosedTab.tabContent.userEditableUrl?.absoluteString ?? "" if title.count > MainMenu.Constants.maxTitleLength { title = String(title.truncated(length: MainMenu.Constants.maxTitleLength)) diff --git a/DuckDuckGo/Sharing/SharingMenu.swift b/DuckDuckGo/Sharing/SharingMenu.swift index 992ddb6103..bdb91d56bf 100644 --- a/DuckDuckGo/Sharing/SharingMenu.swift +++ b/DuckDuckGo/Sharing/SharingMenu.swift @@ -49,7 +49,7 @@ final class SharingMenu: NSMenu { guard let tabViewModel = WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.tabCollectionViewModel.selectedTabViewModel, tabViewModel.canReload, !tabViewModel.isShowingErrorPage, - let url = tabViewModel.tab.content.url else { return nil } + let url = tabViewModel.tab.content.userEditableUrl else { return nil } let sharingData = DuckPlayer.shared.sharingData(for: tabViewModel.title, url: url) ?? (tabViewModel.title, url) diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift index a799ebbb04..99f9fcbf2c 100644 --- a/DuckDuckGo/Tab/Model/Tab.swift +++ b/DuckDuckGo/Tab/Model/Tab.swift @@ -461,7 +461,7 @@ protocol NewWindowPolicyDecisionMaker { if let url = webView.url { let content = TabContent.contentFromURL(url, source: .webViewUpdated) - if self.content.isUrl, self.content.url == url { + if self.content.isUrl, self.content.urlForWebView == url { // ignore content updates when tab.content has userEntered or credential set but equal url as it comes from the WebView url updated event } else if content != self.content { self.content = content @@ -579,7 +579,7 @@ protocol NewWindowPolicyDecisionMaker { @MainActor var currentHistoryItem: BackForwardListItem? { webView.backForwardList.currentItem.map(BackForwardListItem.init) - ?? (content.url ?? navigationDelegate.currentNavigation?.url).map { url in + ?? (content.urlForWebView ?? navigationDelegate.currentNavigation?.url).map { url in BackForwardListItem(kind: .url(url), title: webView.title ?? title, identity: nil) } } @@ -608,7 +608,11 @@ protocol NewWindowPolicyDecisionMaker { let canGoBack = webView.canGoBack let canGoForward = webView.canGoForward - let canReload = self.content.userEditableUrl != nil + let canReload = if case .url(let url, credential: _, source: _) = content, !(url.isDuckPlayer || url.isDuckURLScheme) { + true + } else { + false + } if canGoBack != self.canGoBack { self.canGoBack = canGoBack @@ -721,7 +725,7 @@ protocol NewWindowPolicyDecisionMaker { if startupPreferences.launchToCustomHomePage, let customURL = URL(string: startupPreferences.formattedCustomHomePageURL) { - setContent(.url(customURL, credential: nil, source: .ui)) + setContent(.contentFromURL(customURL, source: .ui)) } else { setContent(.newtab) } @@ -895,9 +899,10 @@ protocol NewWindowPolicyDecisionMaker { } func requestFireproofToggle() { - guard let url = content.userEditableUrl, - let host = url.host - else { return } + guard case .url(let url, _, _) = content, + url.navigationalScheme?.isHypertextScheme == true, + !url.isDuckPlayer, + let host = url.host else { return } _ = FireproofDomains.shared.toggle(domain: host) } @@ -992,7 +997,7 @@ protocol NewWindowPolicyDecisionMaker { if cachedFavicon != favicon { favicon = cachedFavicon } - } else if oldValue?.url?.host != url.host { + } else if oldValue?.urlForWebView?.host != url.host { // If the domain matches the previous value, just keep the same favicon favicon = nil } @@ -1031,7 +1036,7 @@ extension Tab: FaviconUserScriptDelegate { for documentUrl: URL) { guard documentUrl != .error else { return } faviconManagement.handleFaviconLinks(faviconLinks, documentUrl: documentUrl) { favicon in - guard documentUrl == self.content.url, let favicon = favicon else { + guard documentUrl == self.content.urlForWebView, let favicon = favicon else { return } self.favicon = favicon.image @@ -1098,7 +1103,7 @@ extension Tab/*: NavigationResponder*/ { // to be moved to Tab+Navigation.swift // credential is removed from the URL and set to TabContent to be used on next Challenge self.content = .url(navigationAction.url.removingBasicAuthCredential(), credential: credential, source: .webViewUpdated) // reload URL without credentialss - request.url = self.content.url! + request.url = self.content.urlForWebView! navigator.load(request) } } diff --git a/DuckDuckGo/Tab/Model/TabContent.swift b/DuckDuckGo/Tab/Model/TabContent.swift index d052a791b9..369a036ba2 100644 --- a/DuckDuckGo/Tab/Model/TabContent.swift +++ b/DuckDuckGo/Tab/Model/TabContent.swift @@ -129,6 +129,8 @@ extension TabContent { if let settingsPane = url.flatMap(PreferencePaneIdentifier.init(url:)) { return .settings(pane: settingsPane) + } else if url?.isDuckPlayer == true, let (videoId, timestamp) = url?.youtubeVideoParams { + return .url(.duckPlayer(videoId, timestamp: timestamp), credential: nil, source: source) } else if let url, let credential = url.basicAuthCredential { // when navigating to a URL with basic auth username/password, cache it and redirect to a trimmed URL return .url(url.removingBasicAuthCredential(), credential: credential, source: source) @@ -190,19 +192,20 @@ extension TabContent { } } - var url: URL? { - userEditableUrl - } + // !!! don‘t add `url` property to avoid ambiguity with the `.url` enum case + // use `userEditableUrl` or `urlForWebView` instead. + /// user-editable URL displayed in the address bar var userEditableUrl: URL? { - switch self { - case .url(let url, credential: _, source: _) where !(url.isDuckPlayer || url.isDuckURLScheme): - return url - default: - return nil + let url = urlForWebView + if let url, url.isDuckPlayer, + let (videoID, timestamp) = url.youtubeVideoParams { + return .duckPlayer(videoID, timestamp: timestamp) } + return url } + /// `real` URL loaded in the web view var urlForWebView: URL? { switch self { case .url(let url, credential: _, source: _): @@ -290,10 +293,10 @@ extension TabContent { var canBeBookmarked: Bool { switch self { - case .subscription, .identityTheftRestoration, .dataBrokerProtection: + case .newtab, .onboarding, .none: return false - default: - return isUrl + case .url, .settings, .bookmarks, .subscription, .identityTheftRestoration, .dataBrokerProtection: + return true } } diff --git a/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift b/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift index 7624a298b5..139c9c2b97 100644 --- a/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift +++ b/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift @@ -179,7 +179,7 @@ extension TabExtensionsBuilder { HistoryTabExtension(isBurner: args.isTabBurner, historyCoordinating: dependencies.historyCoordinating, trackersPublisher: contentBlocking.trackersPublisher, - urlPublisher: args.contentPublisher.map { content in content.isUrl ? content.url : nil }, + urlPublisher: args.contentPublisher.map { content in content.isUrl ? content.urlForWebView : nil }, titlePublisher: args.titlePublisher) } add { diff --git a/DuckDuckGo/Tab/TabExtensions/TabSnapshotExtension.swift b/DuckDuckGo/Tab/TabExtensions/TabSnapshotExtension.swift index 66f84f5ebd..4aaee77a92 100644 --- a/DuckDuckGo/Tab/TabExtensions/TabSnapshotExtension.swift +++ b/DuckDuckGo/Tab/TabExtensions/TabSnapshotExtension.swift @@ -151,7 +151,8 @@ final class TabSnapshotExtension { @MainActor func renderWebViewSnapshot() async { - guard let webView, let tabContent, let url = tabContent.url else { + guard let webView, let tabContent, + let url = tabContent.userEditableUrl, !url.isDuckURLScheme else { // Previews of native views are rendered in renderNativePreview() return } diff --git a/DuckDuckGo/Tab/TabLazyLoader/LazyLoadable.swift b/DuckDuckGo/Tab/TabLazyLoader/LazyLoadable.swift index 9d2d84628d..7fb5446cb5 100644 --- a/DuckDuckGo/Tab/TabLazyLoader/LazyLoadable.swift +++ b/DuckDuckGo/Tab/TabLazyLoader/LazyLoadable.swift @@ -37,7 +37,7 @@ protocol LazyLoadable: AnyObject, Identifiable { extension Tab: LazyLoadable { var isUrl: Bool { content.isUrl } - var url: URL? { content.url } + var url: URL? { content.urlForWebView } var loadingFinishedPublisher: AnyPublisher { navigationStatePublisher.compactMap { $0 } diff --git a/DuckDuckGo/Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Tab/View/BrowserTabViewController.swift index 327dc8975f..9c2f65619f 100644 --- a/DuckDuckGo/Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Tab/View/BrowserTabViewController.swift @@ -776,7 +776,7 @@ extension BrowserTabViewController: NSDraggingDestination { return true } - selectedTab.setContent(.url(url, source: .appOpenUrl)) + selectedTab.setContent(.contentFromURL(url, source: .appOpenUrl)) return true } diff --git a/DuckDuckGo/Tab/ViewModel/TabViewModel.swift b/DuckDuckGo/Tab/ViewModel/TabViewModel.swift index 42ac6fcd45..ef1365ec9c 100644 --- a/DuckDuckGo/Tab/ViewModel/TabViewModel.swift +++ b/DuckDuckGo/Tab/ViewModel/TabViewModel.swift @@ -233,11 +233,11 @@ final class TabViewModel { } private func updateCanBeBookmarked() { - canBeBookmarked = !isShowingErrorPage && (tab.content.url ?? .blankPage) != .blankPage + canBeBookmarked = !isShowingErrorPage && tab.content.canBeBookmarked } private var tabURL: URL? { - return tab.content.url + return tab.content.userEditableUrl } private var tabHostURL: URL? { diff --git a/DuckDuckGo/TabBar/View/TabBarViewController.swift b/DuckDuckGo/TabBar/View/TabBarViewController.swift index 1fe33c029c..ecce54a428 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewController.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewController.swift @@ -1041,7 +1041,7 @@ extension TabBarViewController: TabBarViewItemDelegate { func tabBarViewItemBookmarkThisPageAction(_ tabBarViewItem: TabBarViewItem) { guard let indexPath = collectionView.indexPath(for: tabBarViewItem), let tabViewModel = tabCollectionViewModel.tabViewModel(at: indexPath.item), - let url = tabViewModel.tab.content.url else { + let url = tabViewModel.tab.content.userEditableUrl else { os_log("TabBarViewController: Failed to get index path of tab bar view item", type: .error) return } diff --git a/DuckDuckGo/TabBar/View/TabBarViewItem.swift b/DuckDuckGo/TabBar/View/TabBarViewItem.swift index 02b3ab4af5..b7e81cacc9 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewItem.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewItem.swift @@ -259,7 +259,7 @@ final class TabBarViewItem: NSCollectionViewItem { }.store(in: &cancellables) tabViewModel.tab.$content.sink { [weak self] content in - self?.currentURL = content.url + self?.currentURL = content.userEditableUrl }.store(in: &cancellables) tabViewModel.$usedPermissions.assign(to: \.usedPermissions, onWeaklyHeld: self).store(in: &cancellables) diff --git a/DuckDuckGo/TabPreview/TabPreviewViewController.swift b/DuckDuckGo/TabPreview/TabPreviewViewController.swift index 9e888b4146..cdf6d3e721 100644 --- a/DuckDuckGo/TabPreview/TabPreviewViewController.swift +++ b/DuckDuckGo/TabPreview/TabPreviewViewController.swift @@ -185,7 +185,7 @@ extension TabPreviewViewController { let title: String var tabContent: Tab.TabContent let shouldShowPreview: Bool - var addressBarString: String { tabContent.url?.absoluteString ?? "Default" } + var addressBarString: String { tabContent.userEditableUrl?.absoluteString ?? "Default" } var snapshot: NSImage? { let image = NSImage(size: size) diff --git a/DuckDuckGo/Windows/View/WindowControllersManager.swift b/DuckDuckGo/Windows/View/WindowControllersManager.swift index 10758ce56d..1bea6538a8 100644 --- a/DuckDuckGo/Windows/View/WindowControllersManager.swift +++ b/DuckDuckGo/Windows/View/WindowControllersManager.swift @@ -174,12 +174,12 @@ extension WindowControllersManager { let firstTab = tabCollection.tabs.first, case .newtab = firstTab.content, !newTab { - firstTab.setContent(url.map { .url($0, source: source) } ?? .newtab) + firstTab.setContent(url.map { .contentFromURL($0, source: source) } ?? .newtab) } else if let tab = tabCollectionViewModel.selectedTabViewModel?.tab, !newTab { - tab.setContent(url.map { .url($0, source: source) } ?? .newtab) + tab.setContent(url.map { .contentFromURL($0, source: source) } ?? .newtab) } else { let newTab = Tab(content: url.map { .url($0, source: source) } ?? .newtab, shouldLoadInBackground: true, burnerMode: tabCollectionViewModel.burnerMode) - newTab.setContent(url.map { .url($0, source: source) } ?? .newtab) + newTab.setContent(url.map { .contentFromURL($0, source: source) } ?? .newtab) tabCollectionViewModel.append(tab: newTab) } } diff --git a/IntegrationTests/NavigationProtection/NavigationProtectionIntegrationTests.swift b/IntegrationTests/NavigationProtection/NavigationProtectionIntegrationTests.swift index f06aa2ade3..9cbc7831bf 100644 --- a/IntegrationTests/NavigationProtection/NavigationProtectionIntegrationTests.swift +++ b/IntegrationTests/NavigationProtection/NavigationProtectionIntegrationTests.swift @@ -77,7 +77,7 @@ class NavigationProtectionIntegrationTests: XCTestCase { for i in 0.. Date: Fri, 26 Apr 2024 21:35:57 +0600 Subject: [PATCH 07/16] Trusted url indicator (#2665) Task/Issue URL: https://app.asana.com/0/1199230911884351/1206381886331933/f PR task: https://app.asana.com/0/0/1207078384259689/f --- DuckDuckGo.xcodeproj/project.pbxproj | 6 - .../Chevron-Right-12-light.svg | 10 + .../Chevron-Right-12.svg | 10 + .../Chevron-Right-12.imageset/Contents.json | 22 ++ .../Contents.json | 15 ++ ...entity-Theft-Restoration-Multicolor-16.pdf | Bin 0 -> 3211 bytes .../Contents.json | 12 + ...rsonalInformationRemoval-Multicolor-16.pdf | Bin 0 -> 5305 bytes .../Contents.json | 12 + .../Settings-Multicolor-16.pdf | Bin 0 -> 7715 bytes .../NSAttributedStringExtension.swift | 35 ++- .../Extensions/NSStoryboardExtension.swift | 25 -- .../Common/Extensions/NSViewExtension.swift | 5 + .../Common/Extensions/URLExtension.swift | 19 +- .../View/AppKit/LoadingProgressView.swift | 2 +- DuckDuckGo/Menus/MainMenuActions.swift | 4 +- .../AddressBarButtonsViewController.swift | 54 ++--- .../View/AddressBarTextField.swift | 5 + .../View/AddressBarViewController.swift | 8 +- .../NavigationBar/View/MoreOptionsMenu.swift | 4 +- .../View/RecentlyClosedMenu.swift | 4 +- DuckDuckGo/Tab/Model/Tab.swift | 5 +- DuckDuckGo/Tab/ViewModel/TabViewModel.swift | 219 ++++++++++++------ IntegrationTests/Tab/AddressBarTests.swift | 2 +- .../Tab/ViewModel/TabViewModelTests.swift | 30 ++- 25 files changed, 347 insertions(+), 161 deletions(-) create mode 100644 DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Chevron-Right-12-light.svg create mode 100644 DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Chevron-Right-12.svg create mode 100644 DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/Identity-Theft-Restoration-Multicolor-16.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/Identity-Theft-Restoration-Multicolor-16.imageset/Identity-Theft-Restoration-Multicolor-16.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/PersonalInformationRemoval-Multicolor-16.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/PersonalInformationRemoval-Multicolor-16.imageset/PersonalInformationRemoval-Multicolor-16.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/Settings-Multicolor-16.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/Settings-Multicolor-16.imageset/Settings-Multicolor-16.pdf delete mode 100644 DuckDuckGo/Common/Extensions/NSStoryboardExtension.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index cf6f987387..54382a2539 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -637,7 +637,6 @@ 3706FC8C293F65D500E42796 /* FirefoxDataImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B5FF67726B602B100D42879 /* FirefoxDataImporter.swift */; }; 3706FC8D293F65D500E42796 /* PreferencesGeneralView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AFCE8A27DB69BC00471A10 /* PreferencesGeneralView.swift */; }; 3706FC8E293F65D500E42796 /* PinnedTabsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BF3F1F286F0A7A00BD9014 /* PinnedTabsView.swift */; }; - 3706FC92293F65D500E42796 /* NSStoryboardExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE0DF0426781961006337B7 /* NSStoryboardExtension.swift */; }; 3706FC93293F65D500E42796 /* PreferencesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AFCE8027DA2CA600471A10 /* PreferencesViewController.swift */; }; 3706FC94293F65D500E42796 /* FireproofDomains.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B02198125E05FAC00ED7DEA /* FireproofDomains.swift */; }; 3706FC95293F65D500E42796 /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B677440255DBEEA00025BD8 /* Database.swift */; }; @@ -1361,7 +1360,6 @@ 4BD57C042AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD57C032AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift */; }; 4BD57C052AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD57C032AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift */; }; 4BDFA4AE27BF19E500648192 /* ToggleableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BDFA4AD27BF19E500648192 /* ToggleableScrollView.swift */; }; - 4BE0DF06267819A1006337B7 /* NSStoryboardExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE0DF0426781961006337B7 /* NSStoryboardExtension.swift */; }; 4BE344EE2B2376DF003FC223 /* VPNFeedbackFormViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE344ED2B2376DF003FC223 /* VPNFeedbackFormViewModelTests.swift */; }; 4BE344EF2B23786F003FC223 /* VPNFeedbackFormViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE344ED2B2376DF003FC223 /* VPNFeedbackFormViewModelTests.swift */; }; 4BE4005327CF3DC3007D3161 /* SavePaymentMethodPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE4005227CF3DC3007D3161 /* SavePaymentMethodPopover.swift */; }; @@ -3216,7 +3214,6 @@ 4BD18F04283F151F00058124 /* BookmarksBar.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = BookmarksBar.storyboard; sourceTree = ""; }; 4BD57C032AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionRemoteMessagingTests.swift; sourceTree = ""; }; 4BDFA4AD27BF19E500648192 /* ToggleableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleableScrollView.swift; sourceTree = ""; }; - 4BE0DF0426781961006337B7 /* NSStoryboardExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSStoryboardExtension.swift; sourceTree = ""; }; 4BE344ED2B2376DF003FC223 /* VPNFeedbackFormViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNFeedbackFormViewModelTests.swift; sourceTree = ""; }; 4BE4005227CF3DC3007D3161 /* SavePaymentMethodPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavePaymentMethodPopover.swift; sourceTree = ""; }; 4BE4005427CF3F19007D3161 /* SavePaymentMethodViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavePaymentMethodViewController.swift; sourceTree = ""; }; @@ -7357,7 +7354,6 @@ B6B3E0DC2657E9CF0040E0A2 /* NSScreenExtension.swift */, AAC5E4E325D6BA9C007F5990 /* NSSizeExtension.swift */, 4B39AAF527D9B2C700A73FD5 /* NSStackViewExtension.swift */, - 4BE0DF0426781961006337B7 /* NSStoryboardExtension.swift */, AA5C8F58258FE21F00748EB7 /* NSTextFieldExtension.swift */, 858A798426A8BB5D00A75A42 /* NSTextViewExtension.swift */, 4B0511E0262CAA8600F6079C /* NSViewControllerExtension.swift */, @@ -10122,7 +10118,6 @@ 3706FC8D293F65D500E42796 /* PreferencesGeneralView.swift in Sources */, 37197EA92942443D00394917 /* WebView.swift in Sources */, 3706FC8E293F65D500E42796 /* PinnedTabsView.swift in Sources */, - 3706FC92293F65D500E42796 /* NSStoryboardExtension.swift in Sources */, B6B5F58A2B03673B008DB58A /* BrowserImportMoreInfoView.swift in Sources */, 3706FC93293F65D500E42796 /* PreferencesViewController.swift in Sources */, 3706FC94293F65D500E42796 /* FireproofDomains.swift in Sources */, @@ -11464,7 +11459,6 @@ AA8EDF2424923E980071C2E8 /* URLExtension.swift in Sources */, B634DBDF293C8F7F00C3C99E /* Tab+UIDelegate.swift in Sources */, B6F1B02E2BCE6B47005E863C /* TunnelControllerProvider.swift in Sources */, - 4BE0DF06267819A1006337B7 /* NSStoryboardExtension.swift in Sources */, 37AFCE8127DA2CA600471A10 /* PreferencesViewController.swift in Sources */, 4B02198A25E05FAC00ED7DEA /* FireproofDomains.swift in Sources */, 4B677442255DBEEA00025BD8 /* Database.swift in Sources */, diff --git a/DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Chevron-Right-12-light.svg b/DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Chevron-Right-12-light.svg new file mode 100644 index 0000000000..2b4a602355 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Chevron-Right-12-light.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Chevron-Right-12.svg b/DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Chevron-Right-12.svg new file mode 100644 index 0000000000..4faab69801 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Chevron-Right-12.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Contents.json new file mode 100644 index 0000000000..7fea6d5282 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "Chevron-Right-12.svg", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "Chevron-Right-12-light.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Identity-Theft-Restoration-Multicolor-16.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Identity-Theft-Restoration-Multicolor-16.imageset/Contents.json new file mode 100644 index 0000000000..c878d4b14f --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Identity-Theft-Restoration-Multicolor-16.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Identity-Theft-Restoration-Multicolor-16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Identity-Theft-Restoration-Multicolor-16.imageset/Identity-Theft-Restoration-Multicolor-16.pdf b/DuckDuckGo/Assets.xcassets/Images/Identity-Theft-Restoration-Multicolor-16.imageset/Identity-Theft-Restoration-Multicolor-16.pdf new file mode 100644 index 0000000000000000000000000000000000000000..30563fa58347e6eeb312cd1b29f83a1b4bcd5cc3 GIT binary patch literal 3211 zcmdT`dpwle8V-})d|OT-B$v-!lDS|g62pv32{XFvsc4LEGBWdxncyr9k7bZI zbt%E#82|_%9)!cA01$L@194PNvSs@$yV};9eCNZ`6LXx`p78U@y<2Mzjc8p^yS1wK_9?rP=aiajKRI@X zyml-S>22fsGtvf)sc&w$vMV$AHhpdd&kY(Hb=P{PuH{ZNjS{zC-T}WXRnf4r7q3%0 z-=UvyCTf4<&pHvdfodGAWUneC*ltznlJ%dCVuy?xI`D;7A+1rzqB_)5J1H>Mhs2<3nd#=*PGo9d@6mVL?%CW%A;kk7sTPCl(cV7h>0wn|u?p8VGL8-GaY0 zoHaDC$Ik6%`JZiy*%{hRI8mhO5uV1cOj~KD)gM9_$6UPFW0jCKvFTu2#hka={#)1? zYoH#@cKv1Ro9bsj##Xv7!IV6OpYG{z9{se-*26m>k`=g{JRiru?{?tNL=^^XfAixj z;~sJB_paQVUoT_kY5yf%BWDQ|?A^0nQ|qspTkbSo^(a{V;N@Zex|*kZ2{-qJHdCzA z?ARws-@ENdsaD&(=2mTeM&P+!y7@)5A^R)R@?L*bP1rTRyqdDe;J|sEaqY_?^?9@H ztIXkxqjN>rFRC}HFgbsZ+d~e#UlHE3XW;8*E3*tON$UtqpTA(^U`9?@0KGV|0z1oH1eH*>v)oXpXyEpl0^V;cjjDZegomiAk z{y{xDx9kUN{Rj)Eq`Oy=+ZH`rMA1vRWX2i@uublBR=HL38Pjz-yhCJbc_S6HV|Elg z#~iwgJ-&rI+;Z4a)2>i#9of{7s`F8!j=1IhjTdR@`PY9DY|HLy(=+qf5fADK^M7mJ zl%Je%_EL3i_SH44RoOl3X|o=f+vWWT`h*LweM&}GM-JAI6N{k9n*H{ zyw`UsZcg#b%12|I!b1KL_54Ey+S=c7QtE8eS7|m!s;s@QI^1S>#^92Ezs`a#_Az2A zfeY?zKG?>8vMhwTy!Bp;%ew3}7xW76LuxVg0qLhBf6a=!i(A@inkgyC)2Jt$PabAu z#n?G@8?m$*lC1JQD?FRJE+sXtZ?JaH_|fjzyL|z=!*^D*Z3g{QWA;mE3s*qzt;Li` zhg<%cK_0il1~`q&pE`#|>vw<~-DRh>fGn_Z|8LDB0jzW}dP_Ph;>%R{Bt_*z9XrMo&ei z#K2gh^~@rvy^J{viaTB$Y~u4Ix6kj7G8aDvQ`cm1;+3tt#g{z3+*_WhQ9eHSQ!XiN z7ejT(xXP|{yLNAuBfo z2WJO#f)sX8z-Sfv54Fbe|G3uRyrDP_3!?duK`%NmrNMPM*?hs2LuD(NIRM~S=xyXL`Bw}6eyZ? zxwepshYbV}6(%%gji0>i`I7F^^M)Mg57M+q$#s|WWQ62zY|4%L z#{I$}G21Exa7bvq;csUNC6r~!k)&AKMFFBo^5pSgkr3R7swm}=f|y$;bz%S%!D0Td zdJJV!N;~vgOzKH5m?wgG$cktLCcz+7Hj2Pya%f7NP)K$HbhE2C5y)0S^aM0;a>rh9 zEaFR;v40_gOkpD6r%|CN6e?C!p4^KmVn)MTr~AeWAU1#p2?RhUNYP)A5z+) zaz&AV$%%#p02*FDZzQ-jLYNCJ25mSIi)A6k(O?*fFXAH07mm)^4xzC?f-4j;1tMv5 Oh|36gz|xWuNc|@|u|MPh literal 0 HcmV?d00001 diff --git a/DuckDuckGo/Assets.xcassets/Images/PersonalInformationRemoval-Multicolor-16.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/PersonalInformationRemoval-Multicolor-16.imageset/Contents.json new file mode 100644 index 0000000000..a30ef3d53e --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/PersonalInformationRemoval-Multicolor-16.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "PersonalInformationRemoval-Multicolor-16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/PersonalInformationRemoval-Multicolor-16.imageset/PersonalInformationRemoval-Multicolor-16.pdf b/DuckDuckGo/Assets.xcassets/Images/PersonalInformationRemoval-Multicolor-16.imageset/PersonalInformationRemoval-Multicolor-16.pdf new file mode 100644 index 0000000000000000000000000000000000000000..fbabea452359ca82df36653248989b67e106aeaf GIT binary patch literal 5305 zcmbVQU2j}95PiSD!Y`FtqDA|SV78QOyXYTbqzR5`rT!78Knzx_Ta_4KKF_H5YhF6H0BE%DV4+ugh4d*uOM z$g0EfcDueARxh@HU2V1(-#%C8ub2OA4#S^=)n@!DGY`%l91Mbpe%mZZq_C1g{sMbvoGM za#4#BMRQA+vKhRN#b)Og`gp!Bgb-zH24WP!0jIc}$Ywaju7UYry~7pHXDvEp{0xxb zL-j!w?V`36zJtE{#bL0uFPM;ixXUYCMva*C4T>`>|>vKep_A;l=>HMi$kA0-i4=p$xf zaIJYvHS4X#HRxz#LR2A4qN#8aEO*8tyF_0JVG6mDNcZ9$6D#+nU;1P!Wn;0E{v z(UWiXDC?(knB9Kum z8I^;4L+d*v0c(9Qh}{mdBbbEL3c)jHQ3lk`SkmBPs1-hvh_Mo+Hri4_pdLmbN06;C zAT?kv2-JbZtweV~ahA!DadHW6@2vwT2c&PdAQNO8@)(RSsfx;kb^<&GDsVnx5Ks(^ z43KmvqT*0vd@X{YdccWN3yU!h`UJ~)>{Y;(z(xeF3*z7kn}l1Ne`Oy^XRaBmQh_YF znV3uhTP4_u6cK#^Tu_BcYbq5=gm{CHl@LzJR?I^Ppa+3@7)Y8FoDV<&Ps3B*mf)}t zjL~|+`t-*9fa0k!F zOB?%?T+tjLqhUE3HDs?%9z+TxSA&kF^(4!z|8laiOz2Ou4c3C%v+y@c8f-%kiN@0C z3TH*It5}G*LL4OLNcZ?ZDNkkS3 ziQx#)lM&V;%GFTRnVVCVH-qZH&?`FSh)j@m))*;#ur)x$SPKLA0crD)ljM53qiN0P zyCz;d3X!QoW*b1weFt=CY$a-h55%0dj)d8z5u#Sv+167h+LB2YMUhg7YzA6JtRIS^ zI<(M;F7&IK%aAs-)L^ajnE{yrg1}dgG~3fx5BIR+mRiV@B6kYd5bc(!){l+p zIETb;Yq~D5Xj@K8__ZP3J+(2V$L@a0Rbpgl)RrkPiBQ1e0|>p@B!Gj{#B5oy0Pu=uOB1V7y$1N{DJ$aQJPyqE2ky74*kQfzH01*mU1R(`*aJrnx7)&Qg z3i`gE9rRPrbo6#pE+L%y^t_XZgwsM^+arlh9eMmPmQa)rV-%7;)s_<(UlAs?#IErH z*+dYdXQX<`XZJGWXSLd=pbwuk+P+s*c%Ko9Doi96r#j@unH%4^sRzFOY`Q{a;kTD+MF#J9B7_g9xN zRmll1)U4-x{{egLiTwv43rYCkSxAxt9;mP%{Cs^}U+>@D-TLu%`*y&K%`lx>{wllx zpvwf@e)zZ@M+))Ue)!zG3x$kcvQmz#Wed w`fyP#eRQOa^TUytJhcmY+?QQ$hf{ej>n0n_~Fr`Z+?3DFQH!&HUIzs literal 0 HcmV?d00001 diff --git a/DuckDuckGo/Assets.xcassets/Images/Settings-Multicolor-16.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Settings-Multicolor-16.imageset/Contents.json new file mode 100644 index 0000000000..d4b2052646 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Settings-Multicolor-16.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Settings-Multicolor-16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Settings-Multicolor-16.imageset/Settings-Multicolor-16.pdf b/DuckDuckGo/Assets.xcassets/Images/Settings-Multicolor-16.imageset/Settings-Multicolor-16.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b3b41002c28e578e1df10814f393e42ce63821bd GIT binary patch literal 7715 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 extension NSAttributedString { /// These values come from Figma. Click on the text in Figma and choose Code > iOS to see the values. @@ -31,6 +32,31 @@ extension NSAttributedString { ]) } + convenience init(image: NSImage, rect: CGRect) { + let attachment = NSTextAttachment() + attachment.image = image + attachment.bounds = rect + self.init(attachment: attachment) + } + + convenience init(@NSAttributedStringBuilder components: () -> [NSAttributedString]) { + let components = components() + guard !components.isEmpty else { + self.init() + return + } + guard components.count > 1 else { + self.init(attributedString: components[0]) + return + } + let result = NSMutableAttributedString(attributedString: components[0]) + for component in components[1...] { + result.append(component) + } + + self.init(attributedString: result) + } + } extension NSMutableAttributedString { @@ -48,12 +74,3 @@ extension NSMutableAttributedString { } } - -extension NSTextAttachment { - func setImageHeight(height: CGFloat, offset: CGPoint = .zero) { - guard let image = image else { return } - let ratio = image.size.width / image.size.height - - bounds = CGRect(x: bounds.origin.x + offset.x, y: bounds.origin.y + offset.y, width: ratio * height, height: height) - } -} diff --git a/DuckDuckGo/Common/Extensions/NSStoryboardExtension.swift b/DuckDuckGo/Common/Extensions/NSStoryboardExtension.swift deleted file mode 100644 index 9e50fa79ce..0000000000 --- a/DuckDuckGo/Common/Extensions/NSStoryboardExtension.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// NSStoryboardExtension.swift -// -// Copyright © 2021 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import AppKit - -extension NSStoryboard { - - static var bookmarks = NSStoryboard(name: "Bookmarks", bundle: .main) - -} diff --git a/DuckDuckGo/Common/Extensions/NSViewExtension.swift b/DuckDuckGo/Common/Extensions/NSViewExtension.swift index b82095c1f0..333dce57cf 100644 --- a/DuckDuckGo/Common/Extensions/NSViewExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSViewExtension.swift @@ -79,6 +79,11 @@ extension NSView { return self } + var isShown: Bool { + get { !isHidden } + set { isHidden = !newValue } + } + func makeMeFirstResponder() { guard let window = window else { os_log("%s: Window not available", type: .error, className) diff --git a/DuckDuckGo/Common/Extensions/URLExtension.swift b/DuckDuckGo/Common/Extensions/URLExtension.swift index abd290dd0f..f55e780cf3 100644 --- a/DuckDuckGo/Common/Extensions/URLExtension.swift +++ b/DuckDuckGo/Common/Extensions/URLExtension.swift @@ -142,7 +142,7 @@ extension URL { // base url for Error Page Alternate HTML loaded into Web View static let error = URL(string: "duck://error")! - static let dataBrokerProtection = URL(string: "duck://dbp")! + static let dataBrokerProtection = URL(string: "duck://personal-information-removal")! #if !SANDBOX_TEST_TOOL static func settingsPane(_ pane: PreferencePaneIdentifier) -> URL { @@ -409,6 +409,10 @@ extension URL { return false } + var isEmailProtection: Bool { + self.isChild(of: .duckDuckGoEmailLogin) || self == .duckDuckGoEmail + } + enum DuckDuckGoParameters: String { case search = "q" case ia @@ -552,7 +556,7 @@ extension URL { return false } - func stripUnsupportedCredentials() -> String { + func strippingUnsupportedCredentials() -> String { if self.absoluteString.firstIndex(of: "@") != nil { let authPattern = "([^:]+):\\/\\/[^\\/]*@" let strippedURL = self.absoluteString.replacingOccurrences(of: authPattern, with: "$1://", options: .regularExpression) @@ -563,7 +567,14 @@ extension URL { } public func isChild(of parentURL: URL) -> Bool { - guard let parentURLHost = parentURL.host, self.isPart(ofDomain: parentURLHost) else { return false } - return pathComponents.starts(with: parentURL.pathComponents) + if scheme == parentURL.scheme, + port == parentURL.port, + let parentURLHost = parentURL.host, + self.isPart(ofDomain: parentURLHost), + pathComponents.starts(with: parentURL.pathComponents) { + return true + } else { + return false + } } } diff --git a/DuckDuckGo/Common/View/AppKit/LoadingProgressView.swift b/DuckDuckGo/Common/View/AppKit/LoadingProgressView.swift index 56f6b2de2b..9c444e511b 100644 --- a/DuckDuckGo/Common/View/AppKit/LoadingProgressView.swift +++ b/DuckDuckGo/Common/View/AppKit/LoadingProgressView.swift @@ -31,7 +31,7 @@ final class LoadingProgressView: NSView, CAAnimationDelegate { private var targetProgress: Double = 0.0 private var targetTime: CFTimeInterval = 0.0 - var isShown: Bool { + var isProgressShown: Bool { progressMask.opacity == 1.0 } diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index ec129e39df..eca7f8004d 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -938,8 +938,8 @@ extension MainViewController: NSMenuItemValidation { case #selector(findInPage), #selector(findInPageNext), #selector(findInPagePrevious): - return activeTabViewModel?.canReload == true // must have content loaded - && view.window?.isKeyWindow == true // disable in full screen + return activeTabViewModel?.canFindInPage == true // must have content loaded + && view.window?.isKeyWindow == true // disable in video full screen case #selector(findInPageDone): return getActiveTabAndIndex()?.tab.findInPage?.isActive == true diff --git a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift index 878114b49b..f277f679b6 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift @@ -275,7 +275,7 @@ final class AddressBarButtonsViewController: NSViewController { guard view.window?.isPopUpWindow == false else { return } bookmarkButton.setAccessibilityIdentifier("AddressBarButtonsViewController.bookmarkButton") let hasEmptyAddressBar = textFieldValue?.isEmpty ?? true - var showBookmarkButton: Bool { + var shouldShowBookmarkButton: Bool { guard let tabViewModel, tabViewModel.canBeBookmarked else { return false } var isUrlBookmarked = false @@ -287,7 +287,7 @@ final class AddressBarButtonsViewController: NSViewController { return clearButton.isHidden && !hasEmptyAddressBar && (isMouseOverNavigationBar || bookmarkPopover?.isShown == true || isUrlBookmarked) } - bookmarkButton.isHidden = !showBookmarkButton + bookmarkButton.isShown = shouldShowBookmarkButton } func openBookmarkPopover(setFavorite: Bool, accessPoint: GeneralPixel.AccessPoint) { @@ -299,7 +299,7 @@ final class AddressBarButtonsViewController: NSViewController { let bookmarkPopover = bookmarkPopoverCreatingIfNeeded() if !bookmarkPopover.isShown { - bookmarkButton.isHidden = false + bookmarkButton.isShown = true bookmarkPopover.isNew = result.isNew bookmarkPopover.bookmark = bookmark bookmarkPopover.show(positionedBelow: bookmarkButton) @@ -319,7 +319,7 @@ final class AddressBarButtonsViewController: NSViewController { }() if query.permissions.contains(.camera) - || (query.permissions.contains(.microphone) && microphoneButton.isHidden && !cameraButton.isHidden) { + || (query.permissions.contains(.microphone) && microphoneButton.isHidden && cameraButton.isShown) { button = cameraButton } else { assert(query.permissions.count == 1) @@ -342,9 +342,7 @@ final class AddressBarButtonsViewController: NSViewController { return } } - guard !button.isHidden, - !permissionButtons.isHidden - else { return } + guard button.isShown, permissionButtons.isShown else { return } (popover.contentViewController as? PermissionAuthorizationViewController)?.query = query popover.show(relativeTo: button.bounds, of: button, preferredEdge: .maxY) @@ -389,7 +387,7 @@ final class AddressBarButtonsViewController: NSViewController { func updateButtons() { stopAnimationsAfterFocus() - clearButton.isHidden = !(isTextFieldEditorFirstResponder && !(textFieldValue?.isEmpty ?? true)) + clearButton.isShown = isTextFieldEditorFirstResponder && !textFieldValue.isEmpty updatePrivacyEntryPointButton() updateImageButton() @@ -690,15 +688,15 @@ final class AddressBarButtonsViewController: NSViewController { } private func updatePermissionButtons() { - permissionButtons.isHidden = isTextFieldEditorFirstResponder - || isAnyTrackerAnimationPlaying - || (tabViewModel?.isShowingErrorPage ?? true) + guard let tabViewModel else { return } + + permissionButtons.isShown = !isTextFieldEditorFirstResponder + && !isAnyTrackerAnimationPlaying + && !tabViewModel.isShowingErrorPage defer { showOrHidePermissionPopoverIfNeeded() } - guard let tabViewModel else { return } - geolocationButton.buttonState = tabViewModel.usedPermissions.geolocation let (camera, microphone) = PermissionState?.combineCamera(tabViewModel.usedPermissions.camera, @@ -771,21 +769,24 @@ final class AddressBarButtonsViewController: NSViewController { guard let tabViewModel else { return } let url = tabViewModel.tab.content.userEditableUrl + let isNewTabOrOnboarding = [.newtab, .onboarding].contains(tabViewModel.tab.content) let isHypertextUrl = url?.navigationalScheme?.isHypertextScheme == true && url?.isDuckPlayer == false let isEditingMode = controllerMode?.isEditing ?? false let isTextFieldValueText = textFieldValue?.isText ?? false let isLocalUrl = url?.isLocalURL ?? false // Privacy entry point button - privacyEntryPointButton.isHidden = isEditingMode - || isTextFieldEditorFirstResponder - || !isHypertextUrl - || tabViewModel.isShowingErrorPage - || isTextFieldValueText - || isLocalUrl - imageButtonWrapper.isHidden = view.window?.isPopUpWindow == true - || !privacyEntryPointButton.isHidden - || isAnyTrackerAnimationPlaying + privacyEntryPointButton.isShown = !isEditingMode + && !isTextFieldEditorFirstResponder + && isHypertextUrl + && !tabViewModel.isShowingErrorPage + && !isTextFieldValueText + && !isLocalUrl + + imageButtonWrapper.isShown = view.window?.isPopUpWindow != true + && (isHypertextUrl || isTextFieldEditorFirstResponder || isEditingMode || isNewTabOrOnboarding) + && privacyEntryPointButton.isHidden + && !isAnyTrackerAnimationPlaying } private func updatePrivacyEntryPointIcon() { @@ -796,7 +797,7 @@ final class AddressBarButtonsViewController: NSViewController { guard !isAnyShieldAnimationPlaying else { return } switch tabViewModel.tab.content { - case .url(let url, _, _): + case .url(let url, _, _), .identityTheftRestoration(let url), .subscription(let url): guard let host = url.host else { break } let isNotSecure = url.scheme == URL.NavigationalScheme.http.rawValue @@ -824,8 +825,7 @@ final class AddressBarButtonsViewController: NSViewController { let trackerAnimationImageProvider = TrackerAnimationImageProvider() private func animateTrackers() { - guard !privacyEntryPointButton.isHidden, - let tabViewModel else { return } + guard privacyEntryPointButton.isShown, let tabViewModel else { return } switch tabViewModel.tab.content { case .url(let url, _, _): @@ -835,7 +835,7 @@ final class AddressBarButtonsViewController: NSViewController { } var animationView: LottieAnimationView - if url.scheme == "http" { + if url.navigationalScheme == .http { animationView = shieldDotAnimationView } else { animationView = shieldAnimationView @@ -878,7 +878,7 @@ final class AddressBarButtonsViewController: NSViewController { shieldAnimations: Bool = true, badgeAnimations: Bool = true) { func stopAnimation(_ animationView: LottieAnimationView) { - if animationView.isAnimationPlaying || !animationView.isHidden { + if animationView.isAnimationPlaying || animationView.isShown { animationView.isHidden = true animationView.stop() } diff --git a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift index c9692f1546..b4807c4cd6 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift @@ -856,6 +856,11 @@ extension AddressBarTextField { } } +extension AddressBarTextField.Value? { + var isEmpty: Bool { + self?.isEmpty ?? true + } +} // MARK: - NSTextFieldDelegate extension AddressBarTextField: NSTextFieldDelegate { diff --git a/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift index cac827debd..f53910b5ec 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift @@ -225,9 +225,9 @@ final class AddressBarViewController: NSViewController { passiveTextField.stringValue = "" return } - tabViewModel.$passiveAddressBarString + tabViewModel.$passiveAddressBarAttributedString .receive(on: DispatchQueue.main) - .assign(to: \.stringValue, onWeaklyHeld: passiveTextField) + .assign(to: \.attributedStringValue, onWeaklyHeld: passiveTextField) .store(in: &tabViewModelCancellables) } @@ -259,7 +259,7 @@ final class AddressBarViewController: NSViewController { .sink { [weak self] value in guard tabViewModel.isLoading, let progressIndicator = self?.progressIndicator, - progressIndicator.isShown + progressIndicator.isProgressShown else { return } progressIndicator.increaseProgress(to: value) @@ -274,7 +274,7 @@ final class AddressBarViewController: NSViewController { if shouldShowLoadingIndicator(for: tabViewModel, isLoading: isLoading, error: error) { progressIndicator.show(progress: tabViewModel.progress, startTime: tabViewModel.loadingStartTime) - } else if progressIndicator.isShown { + } else if progressIndicator.isProgressShown { progressIndicator.finishAndHide() } } diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift index bfc0a1025b..4d2dbb909a 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift @@ -421,12 +421,14 @@ final class MoreOptionsMenu: NSMenu { .withImage(image) } - if tabViewModel.canReload { + if tabViewModel.canFindInPage { addItem(withTitle: UserText.findInPageMenuItem, action: #selector(findInPage(_:)), keyEquivalent: "f") .targetting(self) .withImage(.findSearch) .withAccessibilityIdentifier("MoreOptionsMenu.findInPage") + } + if tabViewModel.canReload { addItem(withTitle: UserText.shareMenuItem, action: nil, keyEquivalent: "") .targetting(self) .withImage(.share) diff --git a/DuckDuckGo/RecentlyClosed/View/RecentlyClosedMenu.swift b/DuckDuckGo/RecentlyClosed/View/RecentlyClosedMenu.swift index 3c24609a30..798e5f153f 100644 --- a/DuckDuckGo/RecentlyClosed/View/RecentlyClosedMenu.swift +++ b/DuckDuckGo/RecentlyClosed/View/RecentlyClosedMenu.swift @@ -71,10 +71,10 @@ private extension NSMenuItem { image = TabViewModel.Favicon.home title = UserText.tabHomeTitle case .settings: - image = TabViewModel.Favicon.preferences + image = TabViewModel.Favicon.settings title = UserText.tabPreferencesTitle case .bookmarks: - image = TabViewModel.Favicon.preferences + image = TabViewModel.Favicon.bookmarks title = UserText.tabPreferencesTitle case .url, .subscription, .identityTheftRestoration: image = recentlyClosedTab.favicon diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift index 99f9fcbf2c..2ec1087956 100644 --- a/DuckDuckGo/Tab/Model/Tab.swift +++ b/DuckDuckGo/Tab/Model/Tab.swift @@ -466,7 +466,10 @@ protocol NewWindowPolicyDecisionMaker { } else if content != self.content { self.content = content } - } else if self.content.isUrl { + } else if self.content.isUrl, + // DuckURLSchemeHandler redirects duck:// address to a simulated request + // ignore webView.url temporarily switching to `nil` + self.content.urlForWebView?.isDuckPlayer != true { // when e.g. opening a download in new tab - web view restores `nil` after the navigation is interrupted // maybe it worths adding another content type like .interruptedLoad(URL) to display a URL in the address bar self.content = .none diff --git a/DuckDuckGo/Tab/ViewModel/TabViewModel.swift b/DuckDuckGo/Tab/ViewModel/TabViewModel.swift index ef1365ec9c..e9726a5dc0 100644 --- a/DuckDuckGo/Tab/ViewModel/TabViewModel.swift +++ b/DuckDuckGo/Tab/ViewModel/TabViewModel.swift @@ -26,12 +26,14 @@ final class TabViewModel { enum Favicon { static let home = NSImage.homeFavicon + static let duckPlayer = NSImage.duckPlayerSettings static let burnerHome = NSImage.burnerTabFavicon - static let preferences = NSImage.preferences - static let bookmarks = NSImage.bookmarks - static let dataBrokerProtection = NSImage.dbpIcon - static let subscription = NSImage.subscriptionIcon - static let identityTheftRestoration = NSImage.itrIcon + static let settings = NSImage.settingsMulticolor16 + static let bookmarks = NSImage.bookmarksFolder + static let emailProtection = NSImage.emailProtectionIcon + static let dataBrokerProtection = NSImage.personalInformationRemovalMulticolor16 + static let subscription = NSImage.privacyPro + static let identityTheftRestoration = NSImage.identityTheftRestorationMulticolor16 } private(set) var tab: Tab @@ -62,7 +64,8 @@ final class TabViewModel { var loadingStartTime: CFTimeInterval? @Published private(set) var addressBarString: String = "" - @Published private(set) var passiveAddressBarString: String = "" + @Published private(set) var passiveAddressBarAttributedString = NSAttributedString() + var lastAddressBarTextFieldValue: AddressBarTextField.Value? @Published private(set) var title: String = UserText.tabHomeTitle @@ -80,6 +83,19 @@ final class TabViewModel { !isShowingErrorPage && canReload && !tab.webView.isInFullScreenMode } + var canFindInPage: Bool { + guard !isShowingErrorPage else { return false } + switch tab.content { + case .url(let url, _, _): + return !(url.isDuckPlayer || url.isDuckURLScheme) + case .subscription, .identityTheftRestoration: + return true + + case .newtab, .settings, .bookmarks, .onboarding, .dataBrokerProtection, .none: + return false + } + } + init(tab: Tab, appearancePreferences: AppearancePreferences = .shared, accessibilityPreferences: AccessibilityPreferences = .shared) { @@ -117,7 +133,7 @@ final class TabViewModel { case .url(let url, _, source: .webViewUpdated), .url(let url, _, source: .link): - guard !url.isEmpty, url != .blankPage else { fallthrough } + guard !url.isEmpty, url != .blankPage, !url.isDuckPlayer else { fallthrough } // Only display the Tab content URL update matching its Security Origin // see https://github.com/mozilla-mobile/firefox-ios/wiki/WKWebView-navigation-and-security-considerations @@ -215,9 +231,8 @@ final class TabViewModel { } private func subscribeToPreferences() { - appearancePreferences.$showFullURL.dropFirst().sink { [weak self] newValue in - guard let self = self, let url = self.tabURL, let host = self.tabHostURL else { return } - self.updatePassiveAddressBarString(showURL: newValue, url: url, hostURL: host) + appearancePreferences.$showFullURL.dropFirst().sink { [weak self] showFullURL in + self?.updatePassiveAddressBarString(showFullURL: showFullURL) }.store(in: &cancellables) accessibilityPreferences.$defaultPageZoom.sink { [weak self] newValue in guard let self = self else { return } @@ -236,56 +251,62 @@ final class TabViewModel { canBeBookmarked = !isShowingErrorPage && tab.content.canBeBookmarked } - private var tabURL: URL? { - return tab.content.userEditableUrl - } - - private var tabHostURL: URL? { - return tabURL?.root + private func updateAddressBarStrings() { + updateAddressBarString() + updatePassiveAddressBarString() } - private func updateAddressBarStrings() { - guard tab.content.isUrl, let url = tabURL else { - addressBarString = "" - passiveAddressBarString = "" - return - } + private func updateAddressBarString() { + addressBarString = { + guard ![.none, .onboarding, .newtab].contains(tab.content), + let url = tab.content.userEditableUrl else { return "" } - if url.isFileURL { - addressBarString = url.absoluteString - passiveAddressBarString = url.absoluteString - return - } + if url.isBlobURL { + return url.strippingUnsupportedCredentials() + } + return url.absoluteString + }() + } - if url.isDataURL { - addressBarString = url.absoluteString - passiveAddressBarString = "data:" - return + private func updatePassiveAddressBarString(showFullURL: Bool? = nil) { + let showFullURL = showFullURL ?? appearancePreferences.showFullURL + passiveAddressBarAttributedString = switch tab.content { + case .newtab, .onboarding, .none: + .init() // empty + case .settings: + .settingsTrustedIndicator + case .bookmarks: + .bookmarksTrustedIndicator + case .dataBrokerProtection: + .dbpTrustedIndicator + case .subscription: + .subscriptionTrustedIndicator + case .identityTheftRestoration: + .identityTheftRestorationTrustedIndicator + case .url(let url, _, _) where url.isDuckPlayer: + .duckPlayerTrustedIndicator + case .url(let url, _, _) where url.isEmailProtection: + .emailProtectionTrustedIndicator + case .url(let url, _, _): + NSAttributedString(string: passiveAddressBarString(with: url, showFullURL: showFullURL)) } + } + private func passiveAddressBarString(with url: URL, showFullURL: Bool) -> String { if url.isBlobURL { - let strippedUrl = url.stripUnsupportedCredentials() - addressBarString = strippedUrl - passiveAddressBarString = strippedUrl - return - } + url.strippingUnsupportedCredentials() - guard let hostURL = tabHostURL else { - // also lands here for about:blank and about:home - addressBarString = "" - passiveAddressBarString = "" - return - } + } else if url.isDataURL { + "data:" - addressBarString = url.absoluteString - updatePassiveAddressBarString(showURL: appearancePreferences.showFullURL, url: url, hostURL: hostURL) - } + } else if !showFullURL && url.isFileURL { + url.lastPathComponent - private func updatePassiveAddressBarString(showURL: Bool, url: URL, hostURL: URL) { - if showURL { - passiveAddressBarString = url.toString(decodePunycode: true, dropScheme: false, dropTrailingSlash: true) - } else { - passiveAddressBarString = hostURL.toString(decodePunycode: true, dropScheme: true, dropTrailingSlash: true).droppingWwwPrefix() + } else if !showFullURL && url.host?.isEmpty == false { + url.root?.toString(decodePunycode: true, dropScheme: true, dropTrailingSlash: true).droppingWwwPrefix() ?? "" + + } else /* display full url */ { + url.toString(decodePunycode: true, dropScheme: false, dropTrailingSlash: true) } } @@ -332,41 +353,33 @@ final class TabViewModel { } } + // swiftlint:disable:next cyclomatic_complexity private func updateFavicon(_ tabFavicon: NSImage?? = .none /* provided from .sink or taken from tab.favicon (optional) if .none */) { guard !isShowingErrorPage else { favicon = errorFaviconToShow(error: tab.error) return } - switch tab.content { + favicon = switch tab.content { case .dataBrokerProtection: - favicon = Favicon.dataBrokerProtection - return + Favicon.dataBrokerProtection + case .newtab where tab.burnerMode.isBurner: + Favicon.burnerHome case .newtab: - if tab.burnerMode.isBurner { - favicon = Favicon.burnerHome - } else { - favicon = Favicon.home - } - return + Favicon.home case .settings: - favicon = Favicon.preferences - return + Favicon.settings case .bookmarks: - favicon = Favicon.bookmarks - return + Favicon.bookmarks case .subscription: - favicon = Favicon.subscription - return + Favicon.subscription case .identityTheftRestoration: - favicon = Favicon.identityTheftRestoration - return - case .url, .onboarding, .none: break - } - - if let favicon: NSImage? = tabFavicon { - self.favicon = favicon - } else { - self.favicon = tab.favicon + Favicon.identityTheftRestoration + case .url(let url, _, _) where url.isDuckPlayer: + Favicon.duckPlayer + case .url(let url, _, _) where url.isEmailProtection: + Favicon.emailProtection + case .url, .onboarding, .none: + tabFavicon ?? tab.favicon } } @@ -426,3 +439,61 @@ extension TabViewModel: TabDataClearing { } } + +private extension NSAttributedString { + + private typealias Component = NSAttributedString + + private static let spacer = NSImage() // empty spacer image attachment for Attributed Strings below + + private static let iconBaselineOffset: CGFloat = -3 + private static let iconSize: CGFloat = 16 + private static let iconSpacing: CGFloat = 6 + private static let chevronSize: CGFloat = 12 + private static let chevronSpacing: CGFloat = 12 + + private static let duckDuckGoWithChevronAttributedString = NSAttributedString { + // logo + Component(image: .homeFavicon, rect: CGRect(x: 0, y: iconBaselineOffset, width: iconSize, height: iconSize)) + // spacing + Component(image: spacer, rect: CGRect(x: 0, y: 0, width: iconSpacing, height: 1)) + // DuckDuckGo + Component(string: UserText.duckDuckGo) + + // spacing (wide) + Component(image: spacer, rect: CGRect(x: 0, y: 0, width: chevronSpacing, height: 1)) + // chevron + Component(image: .chevronRight12, rect: CGRect(x: 0, y: -1, width: chevronSize, height: chevronSize)) + // spacing (wide) + Component(image: spacer, rect: CGRect(x: 0, y: 0, width: chevronSpacing, height: 1)) + } + + private static func trustedIndicatorAttributedString(with icon: NSImage, title: String) -> NSAttributedString { + NSAttributedString { + duckDuckGoWithChevronAttributedString + + // favicon + Component(image: icon, rect: CGRect(x: 0, y: iconBaselineOffset, width: icon.size.width, height: icon.size.height)) + // spacing + Component(image: spacer, rect: CGRect(x: 0, y: 0, width: iconSpacing, height: 1)) + // title + Component(string: title) + } + } + + static let settingsTrustedIndicator = trustedIndicatorAttributedString(with: .settingsMulticolor16, + title: UserText.settings) + static let bookmarksTrustedIndicator = trustedIndicatorAttributedString(with: .bookmarksFolder, + title: UserText.bookmarks) + static let dbpTrustedIndicator = trustedIndicatorAttributedString(with: .personalInformationRemovalMulticolor16, + title: UserText.tabDataBrokerProtectionTitle) + static let subscriptionTrustedIndicator = trustedIndicatorAttributedString(with: .privacyPro, + title: UserText.subscription) + static let identityTheftRestorationTrustedIndicator = trustedIndicatorAttributedString(with: .identityTheftRestorationMulticolor16, + title: UserText.identityTheftRestorationOptionsMenuItem) + static let duckPlayerTrustedIndicator = trustedIndicatorAttributedString(with: .duckPlayerSettings, + title: UserText.duckPlayer) + static let emailProtectionTrustedIndicator = trustedIndicatorAttributedString(with: .emailProtectionIcon, + title: UserText.emailProtectionPreferences) + +} diff --git a/IntegrationTests/Tab/AddressBarTests.swift b/IntegrationTests/Tab/AddressBarTests.swift index 50d74ead32..e5297ed96b 100644 --- a/IntegrationTests/Tab/AddressBarTests.swift +++ b/IntegrationTests/Tab/AddressBarTests.swift @@ -215,7 +215,7 @@ class AddressBarTests: XCTestCase { XCTAssertEqual(addressBarValue, "", "\(idx)") } else { XCTAssertFalse(isAddressBarFirstResponder, "\(idx)") - XCTAssertEqual(addressBarValue, tab.content.isUrl ? tab.content.userEditableUrl!.absoluteString : "", "\(idx)") + XCTAssertEqual(addressBarValue, tab.content == .newtab ? "" : tab.content.userEditableUrl!.absoluteString, "\(idx)") } } } diff --git a/UnitTests/Tab/ViewModel/TabViewModelTests.swift b/UnitTests/Tab/ViewModel/TabViewModelTests.swift index 9dbaa4a0fd..d06378f00f 100644 --- a/UnitTests/Tab/ViewModel/TabViewModelTests.swift +++ b/UnitTests/Tab/ViewModel/TabViewModelTests.swift @@ -74,10 +74,32 @@ final class TabViewModelTests: XCTestCase { } @MainActor - func testWhenURLIsFileURLThenAddressBarIsFilePath() { + func testWhenURLIsFileURLAndShowFullUrlIsDisabledThenAddressBarIsFileName() { let urlString = "file:///Users/Dax/file.txt" let url = URL.makeURL(from: urlString)! - let tabViewModel = TabViewModel.forTabWithURL(url) + let tab = Tab(content: .url(url, source: .link)) + let appearancePreferences = AppearancePreferences(persistor: AppearancePreferencesPersistorMock(showFullURL: false)) + let tabViewModel = TabViewModel(tab: tab, appearancePreferences: appearancePreferences) + + let addressBarStringExpectation = expectation(description: "Address bar string") + + tabViewModel.simulateLoadingCompletion(url, in: tabViewModel.tab.webView) + + tabViewModel.$addressBarString.debounce(for: 0.1, scheduler: RunLoop.main).sink { _ in + XCTAssertEqual(tabViewModel.addressBarString, urlString) + XCTAssertEqual(tabViewModel.passiveAddressBarAttributedString.string, url.lastPathComponent) + addressBarStringExpectation.fulfill() + } .store(in: &cancellables) + waitForExpectations(timeout: 1, handler: nil) + } + + @MainActor + func testWhenURLIsFileURLAndShowFullUrlIsEnabledThenAddressBarIsFilePath() { + let urlString = "file:///Users/Dax/file.txt" + let url = URL.makeURL(from: urlString)! + let tab = Tab(content: .url(url, source: .link)) + let appearancePreferences = AppearancePreferences(persistor: AppearancePreferencesPersistorMock(showFullURL: true)) + let tabViewModel = TabViewModel(tab: tab, appearancePreferences: appearancePreferences) let addressBarStringExpectation = expectation(description: "Address bar string") @@ -85,7 +107,7 @@ final class TabViewModelTests: XCTestCase { tabViewModel.$addressBarString.debounce(for: 0.1, scheduler: RunLoop.main).sink { _ in XCTAssertEqual(tabViewModel.addressBarString, urlString) - XCTAssertEqual(tabViewModel.passiveAddressBarString, urlString) + XCTAssertEqual(tabViewModel.passiveAddressBarAttributedString.string, urlString) addressBarStringExpectation.fulfill() } .store(in: &cancellables) waitForExpectations(timeout: 1, handler: nil) @@ -103,7 +125,7 @@ final class TabViewModelTests: XCTestCase { tabViewModel.$addressBarString.debounce(for: 0.1, scheduler: RunLoop.main).sink { _ in XCTAssertEqual(tabViewModel.addressBarString, urlString) - XCTAssertEqual(tabViewModel.passiveAddressBarString, "data:") + XCTAssertEqual(tabViewModel.passiveAddressBarAttributedString.string, "data:") addressBarStringExpectation.fulfill() } .store(in: &cancellables) waitForExpectations(timeout: 1, handler: nil) From 7e629fabe0d3c8bc0109558bfcc4c68183721a80 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Fri, 26 Apr 2024 17:48:12 +0200 Subject: [PATCH 08/16] macOS: Add pixels to track VPN wake and stop attempts (#2694) Task/Issue URL: https://app.asana.com/0/414235014887631/1207099030609186/f iOS PR:https://github.com/duckduckgo/iOS/pull/2785 BSK PR: https://github.com/duckduckgo/BrowserServicesKit/pull/797 ## Description Adds pixels to track VPN wake and stop attempts. --- DuckDuckGo.xcodeproj/project.pbxproj | 10 ++-- .../xcshareddata/swiftpm/Package.resolved | 9 --- .../NetworkProtectionPixelEvent.swift | 38 ++++++++++++ ...rkProtectionSubscriptionEventHandler.swift | 2 +- .../MacPacketTunnelProvider.swift | 60 +++++++++++-------- .../MacTransparentProxyProvider.swift | 2 +- .../DataBrokerProtection/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 9 files changed, 83 insertions(+), 44 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 54382a2539..34df2d094e 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1089,7 +1089,6 @@ 4B2D06292A11C0C900DE1F49 /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; 4B2D062A2A11C0C900DE1F49 /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; 4B2D062C2A11C0E100DE1F49 /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = 4B2D062B2A11C0E100DE1F49 /* Networking */; }; - 4B2D062D2A11C12300DE1F49 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85799C1725DEBB3F0007EC87 /* Logging.swift */; }; 4B2D06322A11C1D300DE1F49 /* NSApplicationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5C8F622591021700748EB7 /* NSApplicationExtension.swift */; }; 4B2D06332A11C1E300DE1F49 /* OptionalExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B637273C26CCF0C200C8CB02 /* OptionalExtension.swift */; }; 4B2D065B2A11D1FF00DE1F49 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4BEC322A11B509001D9AC5 /* Logging.swift */; }; @@ -1494,7 +1493,6 @@ 7B97CD5D2B7E0BCE004FEF43 /* BundleExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6106B9D26A565DA0013B453 /* BundleExtension.swift */; }; 7B97CD5E2B7E0BEA004FEF43 /* OptionalExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B637273C26CCF0C200C8CB02 /* OptionalExtension.swift */; }; 7B97CD5F2B7E0BF7004FEF43 /* NSApplicationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5C8F622591021700748EB7 /* NSApplicationExtension.swift */; }; - 7B97CD602B7E0C2E004FEF43 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85799C1725DEBB3F0007EC87 /* Logging.swift */; }; 7BA076BB2B65D61400D7FB72 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7BA076BA2B65D61400D7FB72 /* NetworkProtectionProxy */; }; 7BA4727D26F01BC400EAA165 /* CoreDataTestUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292C42667104B00AD2C21 /* CoreDataTestUtilities.swift */; }; 7BA59C9B2AE18B49009A97B1 /* SystemExtensionManager in Frameworks */ = {isa = PBXBuildFile; productRef = 7BA59C9A2AE18B49009A97B1 /* SystemExtensionManager */; }; @@ -3279,6 +3277,7 @@ 7B2DDCF72A93A8BB0039D884 /* NetworkProtectionAppEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionAppEvents.swift; sourceTree = ""; }; 7B2E52242A5FEC09000C6D39 /* NetworkProtectionAgentNotificationsPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionAgentNotificationsPresenter.swift; sourceTree = ""; }; 7B3618C12ADE75C8000D6154 /* NetworkProtectionNavBarPopoverManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionNavBarPopoverManager.swift; sourceTree = ""; }; + 7B427BA82BD81D8C0014AE6C /* BrowserServicesKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = BrowserServicesKit; path = ../../BrowserServicesKit; sourceTree = ""; }; 7B430EA02A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionSimulateFailureMenu.swift; sourceTree = ""; }; 7B4CE8DA26F02108009134B1 /* UI Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "UI Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 7B4CE8E626F02134009134B1 /* TabBarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarTests.swift; sourceTree = ""; }; @@ -4716,6 +4715,7 @@ 378E279C2970217400FCADA2 /* LocalPackages */ = { isa = PBXGroup; children = ( + 7B427BA82BD81D8C0014AE6C /* BrowserServicesKit */, 378E279D2970217400FCADA2 /* BuildToolPlugins */, 3192A2702A4C4E330084EA89 /* DataBrokerProtection */, 9DB6E7222AA0DA7A00A17F3C /* LoginItems */, @@ -10508,7 +10508,6 @@ 4B25377A2A11C01700610219 /* UserText+NetworkProtectionExtensions.swift in Sources */, B65DA5F42A77D3FA00CBEE8D /* BundleExtension.swift in Sources */, EE66418D2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift in Sources */, - 4B2D062D2A11C12300DE1F49 /* Logging.swift in Sources */, 7B2E52252A5FEC09000C6D39 /* NetworkProtectionAgentNotificationsPresenter.swift in Sources */, B602E8232A1E260E006D261F /* Bundle+NetworkProtectionExtensions.swift in Sources */, 4B2D062A2A11C0C900DE1F49 /* NetworkProtectionOptionKeyExtension.swift in Sources */, @@ -10670,7 +10669,6 @@ buildActionMask = 2147483647; files = ( 7B97CD5C2B7E0BBB004FEF43 /* UserDefaultsWrapper.swift in Sources */, - 7B97CD602B7E0C2E004FEF43 /* Logging.swift in Sources */, 7B97CD5E2B7E0BEA004FEF43 /* OptionalExtension.swift in Sources */, 7B97CD5F2B7E0BF7004FEF43 /* NSApplicationExtension.swift in Sources */, 7BDA36F52B7E055800AD5388 /* MacTransparentProxyProvider.swift in Sources */, @@ -12726,7 +12724,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = "138.0.0-1"; + version = 140.0.3; }; }; 4311906792B7676CE9535D76 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */ = { @@ -12742,7 +12740,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 140.0.2; + version = 140.0.3; }; }; 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 d0e88eeb67..c903f1eed8 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -27,15 +27,6 @@ "version" : "3.0.0" } }, - { - "identity" : "browserserviceskit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/BrowserServicesKit", - "state" : { - "revision" : "4340e2840f6ca634e61caffd1ced913603ddf0eb", - "version" : "140.0.2" - } - }, { "identity" : "content-scope-scripts", "kind" : "remoteSourceControl", diff --git a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift index 2446ab0ac5..0a29c23807 100644 --- a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift +++ b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift @@ -34,10 +34,18 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { case networkProtectionTunnelStartSuccess case networkProtectionTunnelStartFailure(_ error: Error) + case networkProtectionTunnelStopAttempt + case networkProtectionTunnelStopSuccess + case networkProtectionTunnelStopFailure(_ error: Error) + case networkProtectionTunnelUpdateAttempt case networkProtectionTunnelUpdateSuccess case networkProtectionTunnelUpdateFailure(_ error: Error) + case networkProtectionTunnelWakeAttempt + case networkProtectionTunnelWakeSuccess + case networkProtectionTunnelWakeFailure(_ error: Error) + case networkProtectionEnableAttemptConnecting case networkProtectionEnableAttemptSuccess case networkProtectionEnableAttemptFailure @@ -119,6 +127,15 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { case .networkProtectionTunnelStartFailure: return "netp_tunnel_start_failure" + case .networkProtectionTunnelStopAttempt: + return "netp_tunnel_stop_attempt" + + case .networkProtectionTunnelStopSuccess: + return "netp_tunnel_stop_success" + + case .networkProtectionTunnelStopFailure: + return "netp_tunnel_stop_failure" + case .networkProtectionTunnelUpdateAttempt: return "netp_tunnel_update_attempt" @@ -128,6 +145,15 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { case .networkProtectionTunnelUpdateFailure: return "netp_tunnel_update_failure" + case .networkProtectionTunnelWakeAttempt: + return "netp_tunnel_wake_attempt" + + case .networkProtectionTunnelWakeSuccess: + return "netp_tunnel_wake_success" + + case .networkProtectionTunnelWakeFailure: + return "netp_tunnel_wake_failure" + case .networkProtectionEnableAttemptConnecting: return "netp_ev_enable_attempt" @@ -300,9 +326,15 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { .networkProtectionTunnelStartAttempt, .networkProtectionTunnelStartSuccess, .networkProtectionTunnelStartFailure, + .networkProtectionTunnelStopAttempt, + .networkProtectionTunnelStopSuccess, + .networkProtectionTunnelStopFailure, .networkProtectionTunnelUpdateAttempt, .networkProtectionTunnelUpdateSuccess, .networkProtectionTunnelUpdateFailure, + .networkProtectionTunnelWakeAttempt, + .networkProtectionTunnelWakeSuccess, + .networkProtectionTunnelWakeFailure, .networkProtectionEnableAttemptConnecting, .networkProtectionEnableAttemptSuccess, .networkProtectionEnableAttemptFailure, @@ -343,7 +375,9 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { return error case .networkProtectionControllerStartFailure(let error), .networkProtectionTunnelStartFailure(let error), + .networkProtectionTunnelStopFailure(let error), .networkProtectionTunnelUpdateFailure(let error), + .networkProtectionTunnelWakeFailure(let error), .networkProtectionClientFailedToParseRedeemResponse(let error), .networkProtectionWireguardErrorCannotSetNetworkSettings(let error), .networkProtectionRekeyFailure(let error), @@ -356,8 +390,12 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { .networkProtectionControllerStartSuccess, .networkProtectionTunnelStartAttempt, .networkProtectionTunnelStartSuccess, + .networkProtectionTunnelStopAttempt, + .networkProtectionTunnelStopSuccess, .networkProtectionTunnelUpdateAttempt, .networkProtectionTunnelUpdateSuccess, + .networkProtectionTunnelWakeAttempt, + .networkProtectionTunnelWakeSuccess, .networkProtectionEnableAttemptConnecting, .networkProtectionEnableAttemptSuccess, .networkProtectionEnableAttemptFailure, diff --git a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift index b1e4a36a7d..ecdc324254 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift @@ -17,11 +17,11 @@ // import Combine +import Common import Foundation import Subscription import NetworkProtection import NetworkProtectionUI -import Common final class NetworkProtectionSubscriptionEventHandler { diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift index 842ff33ad8..2451f4e44c 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift @@ -233,6 +233,24 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { frequency: .dailyAndCount, includeAppVersionParameter: true) } + case .tunnelStopAttempt(let step): + switch step { + case .begin: + PixelKit.fire( + NetworkProtectionPixelEvent.networkProtectionTunnelStopAttempt, + frequency: .standard, + includeAppVersionParameter: true) + case .failure(let error): + PixelKit.fire( + NetworkProtectionPixelEvent.networkProtectionTunnelStopFailure(error), + frequency: .dailyAndCount, + includeAppVersionParameter: true) + case .success: + PixelKit.fire( + NetworkProtectionPixelEvent.networkProtectionTunnelStopSuccess, + frequency: .dailyAndCount, + includeAppVersionParameter: true) + } case .tunnelUpdateAttempt(let step): switch step { case .begin: @@ -251,6 +269,24 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { frequency: .dailyAndCount, includeAppVersionParameter: true) } + case .tunnelWakeAttempt(let step): + switch step { + case .begin: + PixelKit.fire( + NetworkProtectionPixelEvent.networkProtectionTunnelWakeAttempt, + frequency: .dailyAndCount, + includeAppVersionParameter: true) + case .failure(let error): + PixelKit.fire( + NetworkProtectionPixelEvent.networkProtectionTunnelWakeFailure(error), + frequency: .dailyAndCount, + includeAppVersionParameter: true) + case .success: + PixelKit.fire( + NetworkProtectionPixelEvent.networkProtectionTunnelWakeSuccess, + frequency: .dailyAndCount, + includeAppVersionParameter: true) + } } } @@ -421,30 +457,6 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { try? loadDefaultPixelHeaders(from: options) } - // MARK: - Start/Stop Tunnel - - override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { - super.stopTunnel(with: reason) { - Task { - completionHandler() - - // From what I'm seeing in my tests the next call to start the tunnel is MUCH - // less likely to fail if we force this extension to exit when the tunnel is killed. - // - // Ref: https://app.asana.com/0/72649045549333/1204668639086684/f - // - exit(EXIT_SUCCESS) - } - } - } - - override func cancelTunnelWithError(_ error: Error?) { - Task { - super.cancelTunnelWithError(error) - exit(EXIT_SUCCESS) - } - } - // MARK: - Pixels private func setupPixels(defaultHeaders: [String: String] = [:]) { diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacTransparentProxyProvider.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacTransparentProxyProvider.swift index 5d9b0c0fa4..1c4993210e 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacTransparentProxyProvider.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacTransparentProxyProvider.swift @@ -27,7 +27,7 @@ import PixelKit final class MacTransparentProxyProvider: TransparentProxyProvider { - static var vpnProxyLogger = Logger(subsystem: OSLog.subsystem, category: "VPN Proxy") + static var vpnProxyLogger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "DuckDuckGo", category: "VPN Proxy") private var cancellables = Set() diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 8c8bea9c39..f493ccd18a 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: "140.0.2"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "140.0.3"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 81a9dca9a3..71c4ddae58 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "140.0.2"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "140.0.3"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 32884eef9d..dad997f561 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "140.0.2"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "140.0.3"), .package(path: "../SwiftUIExtensions") ], targets: [ From 2723af26aab25580162551bbd546341f09af8cb7 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Fri, 26 Apr 2024 22:03:00 +0600 Subject: [PATCH 09/16] duck page suggestions (#2666) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207078378083698/f BSK PR: https://github.com/duckduckgo/BrowserServicesKit/pull/796 iOS PR: https://github.com/duckduckgo/iOS/pull/2784 --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 9 ++++++++ .../Common/Extensions/URLExtension.swift | 4 ++++ .../View/AddressBarTextField.swift | 10 +++++---- .../View/AddressBarViewController.swift | 2 +- .../Model/PreferencesSection.swift | 2 +- .../Model/SuggestionContainer.swift | 22 +++++++++++++++++-- .../ViewModel/SuggestionViewModel.swift | 15 +++++++++++-- .../Tab/SearchNonexistentDomainTests.swift | 2 +- .../DataBrokerProtection/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- .../Model/SuggestionContainerTests.swift | 2 +- .../SuggestionContainerViewModelTests.swift | 2 +- 14 files changed, 61 insertions(+), 17 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 34df2d094e..84ae54f753 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -12740,7 +12740,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 140.0.3; + version = 141.0.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 c903f1eed8..9e19038261 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -27,6 +27,15 @@ "version" : "3.0.0" } }, + { + "identity" : "browserserviceskit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/BrowserServicesKit", + "state" : { + "revision" : "89680cf5a4d784a1677676562bd96091dc153fc3", + "version" : "141.0.0" + } + }, { "identity" : "content-scope-scripts", "kind" : "remoteSourceControl", diff --git a/DuckDuckGo/Common/Extensions/URLExtension.swift b/DuckDuckGo/Common/Extensions/URLExtension.swift index f55e780cf3..07683d29cc 100644 --- a/DuckDuckGo/Common/Extensions/URLExtension.swift +++ b/DuckDuckGo/Common/Extensions/URLExtension.swift @@ -148,6 +148,10 @@ extension URL { static func settingsPane(_ pane: PreferencePaneIdentifier) -> URL { return settings.appendingPathComponent(pane.rawValue) } + + var isSettingsURL: Bool { + isChild(of: .settings) && (pathComponents.isEmpty || PreferencePaneIdentifier(url: self) != nil) + } #endif enum Invalid { diff --git a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift index b4807c4cd6..c3494d313d 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift @@ -233,7 +233,7 @@ final class AddressBarTextField: NSTextField { case .suggestion(let suggestionViewModel): let suggestion = suggestionViewModel.suggestion switch suggestion { - case .website, .bookmark, .historyEntry: + case .website, .bookmark, .historyEntry, .internalPage: restoreValue(Value(stringValue: suggestionViewModel.autocompletionString, userTyped: true)) case .phrase(phrase: let phase): restoreValue(Value.text(phase, userTyped: false)) @@ -259,7 +259,7 @@ final class AddressBarTextField: NSTextField { switch self.value { case .suggestion(let suggestionViewModel): switch suggestionViewModel.suggestion { - case .phrase, .website, .bookmark, .historyEntry: return false + case .phrase, .website, .bookmark, .historyEntry, .internalPage: return false case .unknown: return true } case .text(_, userTyped: true), .url(_, _, userTyped: true): return false @@ -418,7 +418,8 @@ final class AddressBarTextField: NSTextField { switch suggestion { case .bookmark(title: _, url: let url, isFavorite: _, allowedInTopHits: _), .historyEntry(title: _, url: let url, allowedInTopHits: _), - .website(url: let url): + .website(url: let url), + .internalPage(title: _, url: let url): finalUrl = url userEnteredValue = url.absoluteString case .phrase(phrase: let phrase), @@ -802,7 +803,8 @@ extension AddressBarTextField { self = Suffix.visit(host: host) case .bookmark(title: _, url: let url, isFavorite: _, allowedInTopHits: _), - .historyEntry(title: _, url: let url, allowedInTopHits: _): + .historyEntry(title: _, url: let url, allowedInTopHits: _), + .internalPage(title: _, url: let url): if let title = suggestionViewModel.title, !title.isEmpty, suggestionViewModel.autocompletionString != title { diff --git a/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift index f53910b5ec..7976bb9dc6 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift @@ -364,7 +364,7 @@ final class AddressBarViewController: NSViewController { case .suggestion(let suggestionViewModel): switch suggestionViewModel.suggestion { case .phrase, .unknown: self.mode = .editing(isUrl: false) - case .website, .bookmark, .historyEntry: self.mode = .editing(isUrl: true) + case .website, .bookmark, .historyEntry, .internalPage: self.mode = .editing(isUrl: true) } } } diff --git a/DuckDuckGo/Preferences/Model/PreferencesSection.swift b/DuckDuckGo/Preferences/Model/PreferencesSection.swift index bff6930013..777b1a54dd 100644 --- a/DuckDuckGo/Preferences/Model/PreferencesSection.swift +++ b/DuckDuckGo/Preferences/Model/PreferencesSection.swift @@ -98,7 +98,7 @@ enum PreferencesSectionIdentifier: Hashable, CaseIterable { } -enum PreferencePaneIdentifier: String, Equatable, Hashable, Identifiable { +enum PreferencePaneIdentifier: String, Equatable, Hashable, Identifiable, CaseIterable { case defaultBrowser case privateSearch case webTrackingProtection diff --git a/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift b/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift index 5d4435eb83..94f4a59e46 100644 --- a/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift +++ b/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift @@ -30,15 +30,17 @@ final class SuggestionContainer { private let historyCoordinating: HistoryCoordinating private let bookmarkManager: BookmarkManager + private let startupPreferences: StartupPreferences private let loading: SuggestionLoading private var latestQuery: Query? fileprivate let suggestionsURLSession = URLSession(configuration: .ephemeral) - init(suggestionLoading: SuggestionLoading, historyCoordinating: HistoryCoordinating, bookmarkManager: BookmarkManager) { + init(suggestionLoading: SuggestionLoading, historyCoordinating: HistoryCoordinating, bookmarkManager: BookmarkManager, startupPreferences: StartupPreferences = .shared) { self.bookmarkManager = bookmarkManager self.historyCoordinating = historyCoordinating + self.startupPreferences = startupPreferences self.loading = suggestionLoading self.loading.dataSource = self } @@ -91,7 +93,23 @@ extension SuggestionContainer: SuggestionLoadingDataSource { return historyCoordinating.history ?? [] } - func bookmarks(for suggestionLoading: SuggestionLoading) -> [Suggestions.Bookmark] { + @MainActor func internalPages(for suggestionLoading: Suggestions.SuggestionLoading) -> [Suggestions.InternalPage] { + [ + // suggestions for Bookmarks&Settings + .init(title: UserText.bookmarks, url: .bookmarks), + .init(title: UserText.settings, url: .settings), + ] + PreferencePaneIdentifier.allCases.map { + // preference panes URLs + .init(title: UserText.settings + " → " + $0.displayName, url: .settingsPane($0)) + } + { + guard startupPreferences.launchToCustomHomePage, + let homePage = URL(string: startupPreferences.formattedCustomHomePageURL) else { return [] } + // home page suggestion + return [.init(title: UserText.homePage, url: homePage)] + }() + } + + @MainActor func bookmarks(for suggestionLoading: SuggestionLoading) -> [Suggestions.Bookmark] { bookmarkManager.list?.bookmarks() ?? [] } diff --git a/DuckDuckGo/Suggestions/ViewModel/SuggestionViewModel.swift b/DuckDuckGo/Suggestions/ViewModel/SuggestionViewModel.swift index d7b8c5910c..c3a6d1aad7 100644 --- a/DuckDuckGo/Suggestions/ViewModel/SuggestionViewModel.swift +++ b/DuckDuckGo/Suggestions/ViewModel/SuggestionViewModel.swift @@ -94,7 +94,8 @@ struct SuggestionViewModel: Equatable { } else { return title ?? url.toString(forUserInput: userStringValue) } - case .bookmark(title: let title, url: _, isFavorite: _, allowedInTopHits: _): + case .bookmark(title: let title, url: _, isFavorite: _, allowedInTopHits: _), + .internalPage(title: let title, url: _): return title case .unknown(value: let value): return value @@ -113,7 +114,8 @@ struct SuggestionViewModel: Equatable { } else { return title } - case .bookmark(title: let title, url: _, isFavorite: _, allowedInTopHits: _): + case .bookmark(title: let title, url: _, isFavorite: _, allowedInTopHits: _), + .internalPage(title: let title, url: _): return title } } @@ -155,6 +157,8 @@ struct SuggestionViewModel: Equatable { dropScheme: true, dropTrailingSlash: true) } + case .internalPage: + return " – " + UserText.duckDuckGo } } @@ -174,6 +178,13 @@ struct SuggestionViewModel: Equatable { return .favoritedBookmarkSuggestion case .unknown: return .web + case .internalPage(title: _, url: let url) where url == .bookmarks: + return .bookmarksFolder + case .internalPage(title: _, url: let url) where url.isSettingsURL: + return .settingsMulticolor16 + case .internalPage(title: _, url: let url): + guard url == URL(string: StartupPreferences.shared.formattedCustomHomePageURL) else { return nil } + return .home16 } } diff --git a/IntegrationTests/Tab/SearchNonexistentDomainTests.swift b/IntegrationTests/Tab/SearchNonexistentDomainTests.swift index b715d829e5..6ace162307 100644 --- a/IntegrationTests/Tab/SearchNonexistentDomainTests.swift +++ b/IntegrationTests/Tab/SearchNonexistentDomainTests.swift @@ -241,7 +241,7 @@ final class SearchNonexistentDomainTests: XCTestCase { addressBar.suggestionContainerViewModel = SuggestionContainerViewModel(isHomePage: true, isBurner: false, suggestionContainer: suggestionContainer) suggestionContainer.getSuggestions(for: enteredString) - suggestionLoadingMock.completion!(.init(topHits: [.website(url: url)], duckduckgoSuggestions: [], historyAndBookmarks: []), nil) + suggestionLoadingMock.completion!(.init(topHits: [.website(url: url)], duckduckgoSuggestions: [], localSuggestions: []), nil) addressBar.suggestionViewControllerDidConfirmSelection(addressBar.suggestionViewController) diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index f493ccd18a..743db41a57 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: "140.0.3"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "141.0.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 71c4ddae58..a5da27a4c4 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "140.0.3"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "141.0.0"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index dad997f561..32f9d32616 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "140.0.3"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "141.0.0"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/UnitTests/Suggestions/Model/SuggestionContainerTests.swift b/UnitTests/Suggestions/Model/SuggestionContainerTests.swift index db15131a1c..fbd8316ade 100644 --- a/UnitTests/Suggestions/Model/SuggestionContainerTests.swift +++ b/UnitTests/Suggestions/Model/SuggestionContainerTests.swift @@ -44,7 +44,7 @@ final class SuggestionContainerTests: XCTestCase { withExtendedLifetime(cancellable) { waitForExpectations(timeout: 1) } - XCTAssertEqual(suggestionContainer.result?.all, result.topHits + result.duckduckgoSuggestions + result.historyAndBookmarks) + XCTAssertEqual(suggestionContainer.result?.all, result.topHits + result.duckduckgoSuggestions + result.localSuggestions) } func testWhenStopGettingSuggestionsIsCalled_ThenNoSuggestionsArePublished() { diff --git a/UnitTests/Suggestions/ViewModel/SuggestionContainerViewModelTests.swift b/UnitTests/Suggestions/ViewModel/SuggestionContainerViewModelTests.swift index cee0f7bec3..42beab1350 100644 --- a/UnitTests/Suggestions/ViewModel/SuggestionContainerViewModelTests.swift +++ b/UnitTests/Suggestions/ViewModel/SuggestionContainerViewModelTests.swift @@ -154,7 +154,7 @@ extension SuggestionResult { ] return SuggestionResult(topHits: topHits, duckduckgoSuggestions: [], - historyAndBookmarks: []) + localSuggestions: []) } } From 429d8974c87d896f68427ba7c3836d108478e3bd Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Fri, 26 Apr 2024 18:33:13 +0200 Subject: [PATCH 10/16] macOS: VPN Metadata Improvements (#2704) Task/Issue URL: https://app.asana.com/0/0/1207169059489741/f iOS: https://github.com/duckduckgo/iOS/pull/2791 BSK: https://github.com/duckduckgo/BrowserServicesKit/pull/799 ## Description - Add last IPC, controller, tunnel errors to VPN metadata - Updates VPN feedback metadata to include `ipsec` tunnel count. - Add `lastExtensionVersionRun` to VPN metadata in the App Store build --- DuckDuckGo.xcodeproj/project.pbxproj | 14 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../VPNOperationErrorRecorder.swift | 189 ++++++++++++++++++ .../NetworkProtectionTunnelController.swift | 3 + ...NetworkProtectionIPCTunnelController.swift | 9 +- .../MacPacketTunnelProvider.swift | 3 + .../VPNMetadataCollector.swift | 23 ++- .../TunnelControllerIPCService.swift | 13 ++ .../DataBrokerProtection/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- .../TunnelControllerIPCClient.swift | 6 + .../TunnelControllerIPCServer.swift | 12 ++ LocalPackages/SubscriptionUI/Package.swift | 2 +- .../VPNFeedbackFormViewModelTests.swift | 3 +- 14 files changed, 267 insertions(+), 18 deletions(-) create mode 100644 DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/VPNOperationErrorRecorder.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 84ae54f753..27d34cce98 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1480,6 +1480,10 @@ 7B430EA12A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B430EA02A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift */; }; 7B430EA22A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B430EA02A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift */; }; 7B4CE8E726F02135009134B1 /* TabBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4CE8E626F02134009134B1 /* TabBarTests.swift */; }; + 7B4D8A212BDA857300852966 /* VPNOperationErrorRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4D8A202BDA857300852966 /* VPNOperationErrorRecorder.swift */; }; + 7B4D8A222BDA857300852966 /* VPNOperationErrorRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4D8A202BDA857300852966 /* VPNOperationErrorRecorder.swift */; }; + 7B4D8A232BDA857300852966 /* VPNOperationErrorRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4D8A202BDA857300852966 /* VPNOperationErrorRecorder.swift */; }; + 7B4D8A242BDA857300852966 /* VPNOperationErrorRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4D8A202BDA857300852966 /* VPNOperationErrorRecorder.swift */; }; 7B624F172BA25C1F00A6C544 /* NetworkProtectionUI in Frameworks */ = {isa = PBXBuildFile; productRef = 7B624F162BA25C1F00A6C544 /* NetworkProtectionUI */; }; 7B7DFB202B7E736B009EA1A3 /* MacPacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */; }; 7B7DFB222B7E7473009EA1A3 /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = 7B7DFB212B7E7473009EA1A3 /* Networking */; }; @@ -3277,10 +3281,10 @@ 7B2DDCF72A93A8BB0039D884 /* NetworkProtectionAppEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionAppEvents.swift; sourceTree = ""; }; 7B2E52242A5FEC09000C6D39 /* NetworkProtectionAgentNotificationsPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionAgentNotificationsPresenter.swift; sourceTree = ""; }; 7B3618C12ADE75C8000D6154 /* NetworkProtectionNavBarPopoverManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionNavBarPopoverManager.swift; sourceTree = ""; }; - 7B427BA82BD81D8C0014AE6C /* BrowserServicesKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = BrowserServicesKit; path = ../../BrowserServicesKit; sourceTree = ""; }; 7B430EA02A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionSimulateFailureMenu.swift; sourceTree = ""; }; 7B4CE8DA26F02108009134B1 /* UI Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "UI Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 7B4CE8E626F02134009134B1 /* TabBarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarTests.swift; sourceTree = ""; }; + 7B4D8A202BDA857300852966 /* VPNOperationErrorRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNOperationErrorRecorder.swift; sourceTree = ""; }; 7B5291882A1697680022E406 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 7B5291892A169BC90022E406 /* DeveloperID.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DeveloperID.xcconfig; sourceTree = ""; }; 7B6D98662ADEB4B000CD35FE /* DataBrokerProtectionLoginItemScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionLoginItemScheduler.swift; sourceTree = ""; }; @@ -4715,7 +4719,6 @@ 378E279C2970217400FCADA2 /* LocalPackages */ = { isa = PBXGroup; children = ( - 7B427BA82BD81D8C0014AE6C /* BrowserServicesKit */, 378E279D2970217400FCADA2 /* BuildToolPlugins */, 3192A2702A4C4E330084EA89 /* DataBrokerProtection */, 9DB6E7222AA0DA7A00A17F3C /* LoginItems */, @@ -8106,6 +8109,7 @@ 4BF0E5042AD2551A00FFEC9E /* NetworkProtectionPixelEvent.swift */, 7B934C402A866DD400FC8F9C /* UserDefaults+NetworkProtectionShared.swift */, 7BFE95532A9DF2930081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift */, + 7B4D8A202BDA857300852966 /* VPNOperationErrorRecorder.swift */, ); path = AppAndExtensionAndAgentTargets; sourceTree = ""; @@ -9495,6 +9499,7 @@ 1DDD3EC52B84F96B004CBF2B /* CookiePopupProtectionPreferences.swift in Sources */, 3706FAE0293F65D500E42796 /* BookmarkSidebarTreeController.swift in Sources */, 3706FAE1293F65D500E42796 /* HomePageFavoritesModel.swift in Sources */, + 7B4D8A222BDA857300852966 /* VPNOperationErrorRecorder.swift in Sources */, 3706FAE2293F65D500E42796 /* SequenceExtensions.swift in Sources */, 3706FAE3293F65D500E42796 /* ChromiumDataImporter.swift in Sources */, 3706FAE5293F65D500E42796 /* BackForwardListItemViewModel.swift in Sources */, @@ -10544,6 +10549,7 @@ 7BA7CC5D2AD120C30042E5CE /* EventMapping+NetworkProtectionError.swift in Sources */, 7BA7CC4A2AD11EA00042E5CE /* NetworkProtectionTunnelController.swift in Sources */, EE3424602BA0853900173B1B /* VPNUninstaller.swift in Sources */, + 7B4D8A232BDA857300852966 /* VPNOperationErrorRecorder.swift in Sources */, 7BD1688E2AD4A4C400D24876 /* NetworkExtensionController.swift in Sources */, 7BA7CC3E2AD11E380042E5CE /* TunnelControllerIPCService.swift in Sources */, 7BA7CC402AD11E3D0042E5CE /* AppLauncher+DefaultInitializer.swift in Sources */, @@ -10580,6 +10586,7 @@ 7BA7CC582AD1203A0042E5CE /* UserText+NetworkProtection.swift in Sources */, 7BA7CC4B2AD11EC60042E5CE /* NetworkProtectionControllerErrorStore.swift in Sources */, EE3424612BA0853900173B1B /* VPNUninstaller.swift in Sources */, + 7B4D8A242BDA857300852966 /* VPNOperationErrorRecorder.swift in Sources */, 4BF0E5152AD25A2600FFEC9E /* DuckDuckGoUserAgent.swift in Sources */, 7BFE95592A9DF2AF0081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift in Sources */, 7BA7CC5C2AD120C30042E5CE /* EventMapping+NetworkProtectionError.swift in Sources */, @@ -11300,6 +11307,7 @@ B69B503C2726A12500758A2B /* StatisticsStore.swift in Sources */, 3158B14A2B0BF74300AF130C /* DataBrokerProtectionDebugMenu.swift in Sources */, 4BBDEE9128FC14760092FAA6 /* BWInstallationService.swift in Sources */, + 7B4D8A212BDA857300852966 /* VPNOperationErrorRecorder.swift in Sources */, 859F30642A72A7BB00C20372 /* BookmarksBarPromptPopover.swift in Sources */, 4B4D60CA2A0C849600BCD287 /* NetworkProtectionInvitePresenter.swift in Sources */, B693955426F04BEC0015B914 /* ColorView.swift in Sources */, @@ -12740,7 +12748,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 141.0.0; + version = 141.0.1; }; }; 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 9e19038261..5ddc6f2245 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "89680cf5a4d784a1677676562bd96091dc153fc3", - "version" : "141.0.0" + "revision" : "786272601414243b391c22d370bf807e406a0e71", + "version" : "141.0.1" } }, { diff --git a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/VPNOperationErrorRecorder.swift b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/VPNOperationErrorRecorder.swift new file mode 100644 index 0000000000..7fa58ab46d --- /dev/null +++ b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/VPNOperationErrorRecorder.swift @@ -0,0 +1,189 @@ +// +// VPNOperationErrorRecorder.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import NetworkProtectionIPC + +@objc +final class ErrorInformation: NSObject, Codable { + let domain: String + let code: Int + + init(_ error: Error) { + let nsError = error as NSError + + domain = nsError.domain + code = nsError.code + } +} + +/// This class provides information about VPN operation errors. +/// +/// To be used in combination with ``VPNOperationErrorRecorder`` +/// +final class VPNOperationErrorHistory { + + private let ipcClient: TunnelControllerIPCClient + private let defaults: UserDefaults + + init(ipcClient: TunnelControllerIPCClient, + defaults: UserDefaults = .netP) { + + self.ipcClient = ipcClient + self.defaults = defaults + } + + /// The earliest error is the one that best represents the latest failure + /// + var lastStartError: ErrorInformation? { + lastIPCStartError ?? lastControllerStartError + } + + var lastStartErrorDescription: String { + lastStartError.map { errorInformation in + "Error domain=\(errorInformation.domain) code=\(errorInformation.code)" + } ?? "none" + } + + private var lastIPCStartError: ErrorInformation? { + defaults.vpnIPCStartError + } + + private var lastControllerStartError: ErrorInformation? { + defaults.controllerStartError + } + + var lastTunnelError: ErrorInformation? { + get async { + await withCheckedContinuation { (continuation: CheckedContinuation) in + ipcClient.fetchLastError { error in + if let error { + continuation.resume(returning: ErrorInformation(error)) + } else { + continuation.resume(returning: nil) + } + } + } + } + } + + var lastTunnelErrorDescription: String { + get async { + await lastTunnelError.map { errorInformation in + "Error domain=\(errorInformation.domain) code=\(errorInformation.code)" + } ?? "none" + } + } +} + +/// This class records information about recent errors during VPN operation. +/// +/// To be used in combination with ``VPNOperationErrorHistory`` +/// +final class VPNOperationErrorRecorder { + + private let defaults: UserDefaults + + init(defaults: UserDefaults = .netP) { + self.defaults = defaults + } + + // IPC Errors + + func beginRecordingIPCStart() { + defaults.vpnIPCStartError = nil + } + + func recordIPCStartFailure(_ error: Error) { + defaults.vpnIPCStartError = ErrorInformation(error) + } + + // VPN Controller Errors + + func beginRecordingControllerStart() { + defaults.controllerStartError = nil + + // This needs a special note because it may be non-obvious. The thing is users + // can start the VPN directly from the menu app, and in this case we want IPC + // errors to be cleared because they have priority in the reporting. Additionally + // if the controller is starting the VPN we can safely assume there was no IPC + // error in the current start attempt, so resetting ipc start errors should be fine, + // regardless. + defaults.vpnIPCStartError = nil + } + + func recordControllerStartFailure(_ error: Error) { + defaults.controllerStartError = ErrorInformation(error) + } +} + +fileprivate extension UserDefaults { + private var vpnIPCStartErrorKey: String { + "vpnIPCStartError" + } + + @objc + dynamic var vpnIPCStartError: ErrorInformation? { + get { + guard let payload = data(forKey: vpnIPCStartErrorKey) else { + return nil + } + + return try? JSONDecoder().decode(ErrorInformation.self, from: payload) + } + + set { + guard let newValue, + let payload = try? JSONEncoder().encode(newValue) else { + + removeObject(forKey: vpnIPCStartErrorKey) + return + } + + set(payload, forKey: vpnIPCStartErrorKey) + } + } +} + +fileprivate extension UserDefaults { + private var controllerStartErrorKey: String { + "controllerStartError" + } + + @objc + dynamic var controllerStartError: ErrorInformation? { + get { + guard let payload = data(forKey: controllerStartErrorKey) else { + return nil + } + + return try? JSONDecoder().decode(ErrorInformation.self, from: payload) + } + + set { + guard let newValue, + let payload = try? JSONEncoder().encode(newValue) else { + + removeObject(forKey: controllerStartErrorKey) + return + } + + set(payload, forKey: controllerStartErrorKey) + } + } +} diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift index c2565dc405..b1bebcadbb 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift @@ -480,6 +480,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr /// Starts the VPN connection /// func start() async { + VPNOperationErrorRecorder().beginRecordingControllerStart() PixelKit.fire(NetworkProtectionPixelEvent.networkProtectionControllerStartAttempt, frequency: .dailyAndCount) controllerErrorStore.lastErrorMessage = nil @@ -525,6 +526,8 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr frequency: .dailyAndCount) } } catch { + VPNOperationErrorRecorder().recordControllerStartFailure(error) + PixelKit.fire( NetworkProtectionPixelEvent.networkProtectionControllerStartFailure(error), frequency: .dailyAndCount, includeAppVersionParameter: true ) diff --git a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift index 7e05352066..a39fcb6de4 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift @@ -51,16 +51,19 @@ final class NetworkProtectionIPCTunnelController { private let loginItemsManager: LoginItemsManaging private let ipcClient: NetworkProtectionIPCClient private let pixelKit: PixelFiring? + private let errorRecorder: VPNOperationErrorRecorder init(featureVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(), loginItemsManager: LoginItemsManaging = LoginItemsManager(), ipcClient: NetworkProtectionIPCClient, - pixelKit: PixelFiring? = PixelKit.shared) { + pixelKit: PixelFiring? = PixelKit.shared, + errorRecorder: VPNOperationErrorRecorder = VPNOperationErrorRecorder()) { self.featureVisibility = featureVisibility self.loginItemsManager = loginItemsManager self.ipcClient = ipcClient self.pixelKit = pixelKit + self.errorRecorder = errorRecorder } // MARK: - Login Items Manager @@ -84,9 +87,11 @@ extension NetworkProtectionIPCTunnelController: TunnelController { @MainActor func start() async { + errorRecorder.beginRecordingIPCStart() pixelKit?.fire(StartAttempt.begin) func handleFailure(_ error: Error) { + errorRecorder.recordIPCStartFailure(error) log(error) pixelKit?.fire(StartAttempt.failure(error), frequency: .dailyAndCount) } @@ -156,7 +161,7 @@ extension NetworkProtectionIPCTunnelController: TunnelController { } } -// MARK: - Start Attempts +// MARK: - Start Attempts: Pixels extension NetworkProtectionIPCTunnelController { diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift index 2451f4e44c..3c71dc41a7 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift @@ -308,6 +308,9 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { #else let defaults = UserDefaults.netP #endif + + NetworkProtectionLastVersionRunStore(userDefaults: defaults).lastExtensionVersionRun = AppVersion.shared.versionAndBuildNumber + let settings = VPNSettings(defaults: defaults) let tunnelHealthStore = NetworkProtectionTunnelHealthStore(notificationCenter: notificationCenter) let controllerErrorStore = NetworkProtectionTunnelErrorStore(notificationCenter: notificationCenter) diff --git a/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift b/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift index 925ac292d2..1e9dd5d333 100644 --- a/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift +++ b/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift @@ -50,7 +50,8 @@ struct VPNMetadata: Encodable { struct VPNState: Encodable { let onboardingState: String let connectionState: String - let lastErrorMessage: String + let lastStartErrorDescription: String + let lastTunnelErrorDescription: String let connectedServer: String let connectedServerIP: String } @@ -119,11 +120,16 @@ protocol VPNMetadataCollector { final class DefaultVPNMetadataCollector: VPNMetadataCollector { private let statusReporter: NetworkProtectionStatusReporter + private let ipcClient: TunnelControllerIPCClient + private let defaults: UserDefaults - init() { + init(defaults: UserDefaults = .netP) { let ipcClient = TunnelControllerIPCClient() ipcClient.register() + self.ipcClient = ipcClient + self.defaults = defaults + self.statusReporter = DefaultNetworkProtectionStatusReporter( statusObserver: ipcClient.connectionStatusObserver, serverInfoObserver: ipcClient.serverInfoObserver, @@ -163,7 +169,7 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { private func collectAppInfoMetadata() -> VPNMetadata.AppInfo { let appVersion = AppVersion.shared.versionAndBuildNumber - let versionStore = NetworkProtectionLastVersionRunStore(userDefaults: .netP) + let versionStore = NetworkProtectionLastVersionRunStore(userDefaults: defaults) let isInternalUser = NSApp.delegateTyped.internalUserDecider.isInternalUser let isInApplicationsDirectory = Bundle.main.isInApplicationsDirectory @@ -232,7 +238,7 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { func collectVPNState() async -> VPNMetadata.VPNState { let onboardingState: String - switch UserDefaults.netP.networkProtectionOnboardingStatus { + switch defaults.networkProtectionOnboardingStatus { case .completed: onboardingState = "complete" case .isOnboarding(let step): @@ -244,13 +250,16 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { } } + let errorHistory = VPNOperationErrorHistory(ipcClient: ipcClient, defaults: defaults) + let connectionState = String(describing: statusReporter.statusObserver.recentValue) - let lastErrorMessage = statusReporter.connectionErrorObserver.recentValue ?? "none" + let lastTunnelErrorDescription = await errorHistory.lastTunnelErrorDescription let connectedServer = statusReporter.serverInfoObserver.recentValue.serverLocation?.serverLocation ?? "none" let connectedServerIP = statusReporter.serverInfoObserver.recentValue.serverAddress ?? "none" return .init(onboardingState: onboardingState, connectionState: connectionState, - lastErrorMessage: lastErrorMessage, + lastStartErrorDescription: errorHistory.lastStartErrorDescription, + lastTunnelErrorDescription: lastTunnelErrorDescription, connectedServer: connectedServer, connectedServerIP: connectedServerIP) } @@ -279,7 +288,7 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { } func collectVPNSettingsState() -> VPNMetadata.VPNSettingsState { - let settings = VPNSettings(defaults: .netP) + let settings = VPNSettings(defaults: defaults) return .init( connectOnLoginEnabled: settings.connectOnLogin, diff --git a/DuckDuckGoVPN/TunnelControllerIPCService.swift b/DuckDuckGoVPN/TunnelControllerIPCService.swift index 971dc24522..b88c3f20b8 100644 --- a/DuckDuckGoVPN/TunnelControllerIPCService.swift +++ b/DuckDuckGoVPN/TunnelControllerIPCService.swift @@ -127,6 +127,19 @@ extension TunnelControllerIPCService: IPCServerInterface { completion(nil) } + func fetchLastError(completion: @escaping (Error?) -> Void) { + Task { + guard #available(macOS 13.0, *), + let connection = await tunnelController.connection else { + + completion(nil) + return + } + + connection.fetchLastDisconnectError(completionHandler: completion) + } + } + func resetAll(uninstallSystemExtension: Bool) async { try? await networkExtensionController.deactivateSystemExtension() } diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 743db41a57..6a112190f2 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: "141.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "141.0.1"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index a5da27a4c4..6e9e546e9a 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "141.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "141.0.1"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift index 61996e3945..a3aafbbe11 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift @@ -172,6 +172,12 @@ extension TunnelControllerIPCClient: IPCServerInterface { }, xpcReplyErrorHandler: completion) } + public func fetchLastError(completion: @escaping (Error?) -> Void) { + xpc.execute(call: { server in + server.fetchLastError(completion: completion) + }, xpcReplyErrorHandler: completion) + } + public func debugCommand(_ command: DebugCommand) async throws { guard let payload = try? JSONEncoder().encode(command) else { return diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCServer.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCServer.swift index 0d72a0d7ce..ef5a8d015a 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCServer.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCServer.swift @@ -45,6 +45,10 @@ public protocol IPCServerInterface: AnyObject { /// func stop(completion: @escaping (Error?) -> Void) + /// Fetches the last error directly from the tunnel manager. + /// + func fetchLastError(completion: @escaping (Error?) -> Void) + /// Debug commands /// func debugCommand(_ command: DebugCommand) async throws @@ -71,6 +75,10 @@ protocol XPCServerInterface { /// func stop(completion: @escaping (Error?) -> Void) + /// Fetches the last error directly from the tunnel manager. + /// + func fetchLastError(completion: @escaping (Error?) -> Void) + /// Debug commands /// func debugCommand(_ payload: Data, completion: @escaping (Error?) -> Void) @@ -174,6 +182,10 @@ extension TunnelControllerIPCServer: XPCServerInterface { serverDelegate?.stop(completion: completion) } + func fetchLastError(completion: @escaping (Error?) -> Void) { + serverDelegate?.fetchLastError(completion: completion) + } + func debugCommand(_ payload: Data, completion: @escaping (Error?) -> Void) { guard let command = try? JSONDecoder().decode(DebugCommand.self, from: payload) else { completion(IPCError.cannotDecodeDebugCommand) diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 32f9d32616..b3e2665354 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: "141.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "141.0.1"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/UnitTests/VPNFeedbackForm/VPNFeedbackFormViewModelTests.swift b/UnitTests/VPNFeedbackForm/VPNFeedbackFormViewModelTests.swift index ee1758f401..17b489a705 100644 --- a/UnitTests/VPNFeedbackForm/VPNFeedbackFormViewModelTests.swift +++ b/UnitTests/VPNFeedbackForm/VPNFeedbackFormViewModelTests.swift @@ -103,7 +103,8 @@ private class MockVPNMetadataCollector: VPNMetadataCollector { let vpnState = VPNMetadata.VPNState( onboardingState: "onboarded", connectionState: "connected", - lastErrorMessage: "none", + lastStartErrorDescription: "none", + lastTunnelErrorDescription: "none", connectedServer: "Paoli, PA", connectedServerIP: "123.123.123.123" ) From e965388df49895b3e2671ade11383edf8076bbcd Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 26 Apr 2024 19:57:47 +0200 Subject: [PATCH 11/16] Update BSK to 141.1.1 (#2713) Task/Issue URL: https://app.asana.com/0/414235014887631/1207182733795625/f Description: Update BSK to 141.1.1 No changes to macOS. Contained updates are to prepare for Stripe repurchase flow on macOS. Steps to test this PR: Verify that tests are green. --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 27d34cce98..536b2b14f4 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -12748,7 +12748,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 141.0.1; + version = 141.1.1; }; }; 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 5ddc6f2245..0b63176b7c 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "786272601414243b391c22d370bf807e406a0e71", - "version" : "141.0.1" + "revision" : "9ebcfd17a2dd1422407a24e9e4331a46c3b7733a", + "version" : "141.1.1" } }, { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 6a112190f2..1a2906ce45 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: "141.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "141.1.1"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 6e9e546e9a..761b9691ca 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "141.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "141.1.1"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index b3e2665354..408f9cbfe2 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "141.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "141.1.1"), .package(path: "../SwiftUIExtensions") ], targets: [ From 92592bbfde536a89a5182a2216153d1152d96c5f Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Fri, 26 Apr 2024 12:51:33 -0700 Subject: [PATCH 12/16] Update BSK for iOS on-demand changes (#2668) Task/Issue URL: https://app.asana.com/0/414235014887631/1206396485779556/f Tech Design URL: CC: Description: This PR updates BSK for an iOS on-demand change. Nothing on macOS should change. --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 536b2b14f4..086948af44 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -12748,7 +12748,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 141.1.1; + version = 141.1.2; }; }; 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 0b63176b7c..64aca2b97a 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "9ebcfd17a2dd1422407a24e9e4331a46c3b7733a", - "version" : "141.1.1" + "revision" : "f8c73292d4d6724ec81f98bd29dbb2061f1a8cf6", + "version" : "141.1.2" } }, { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 1a2906ce45..7657755407 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: "141.1.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "141.1.2"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 761b9691ca..45900c0243 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "141.1.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "141.1.2"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 408f9cbfe2..7c57479333 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: "141.1.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "141.1.2"), .package(path: "../SwiftUIExtensions") ], targets: [ From 36aa51f3be75f3705cf9b8bb24d766d031c96e5b Mon Sep 17 00:00:00 2001 From: Tom Strba <57389842+tomasstrba@users.noreply.github.com> Date: Sun, 28 Apr 2024 21:28:02 +0200 Subject: [PATCH 13/16] Automatically clear data upon quitting (#2600) Task/Issue URL: https://app.asana.com/0/1177771139624306/1205062321200340/f **Description**: Adding Burn on Quit feature for macOS. Upon standard exit, data are cleared and the fire animation is presented. If macOS is restarting, the app delays the restart until all data have been cleared. In the event of an unexpected termination, such as a crash or force quit, data clearing is handled at the next startup. --- DuckDuckGo.xcodeproj/project.pbxproj | 6 + DuckDuckGo/Application/AppDelegate.swift | 19 + DuckDuckGo/Application/AutoClearHandler.swift | 125 +++ .../Burn-Original-large.pdf | Bin 43425 -> 0 bytes .../Images/BurnAlert.imageset/Contents.json | 2 +- .../Images/BurnAlert.imageset/Fire-96x96.pdf | Bin 0 -> 17864 bytes .../Common/Extensions/NSAlertExtension.swift | 31 + DuckDuckGo/Common/Localizables/UserText.swift | 13 + .../Utilities/UserDefaultsWrapper.swift | 4 + DuckDuckGo/Localizable.xcstrings | 779 +++++++++++++++++- .../Model/DataClearingPreferences.swift | 35 + .../Preferences/Model/SearchPreferences.swift | 5 + .../Model/StartupPreferences.swift | 25 +- .../View/PreferencesDataClearingView.swift | 15 +- .../View/PreferencesGeneralView.swift | 17 +- .../View/PreferencesRootView.swift | 3 +- .../StatePersistenceService.swift | 1 + .../Assets.xcassets/Colors/Contents.json | 6 + .../LinkBlueColor.colorset/Contents.json | 78 ++ .../SyncUI/Resources/Localizable.xcstrings | 146 +++- .../AppDelegate/AutoClearHandlerTests.swift | 83 ++ .../DataClearingPreferencesTests.swift | 4 + 22 files changed, 1362 insertions(+), 35 deletions(-) create mode 100644 DuckDuckGo/Application/AutoClearHandler.swift delete mode 100644 DuckDuckGo/Assets.xcassets/Images/BurnAlert.imageset/Burn-Original-large.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/BurnAlert.imageset/Fire-96x96.pdf create mode 100644 LocalPackages/SwiftUIExtensions/Sources/PreferencesViews/Assets.xcassets/Colors/Contents.json create mode 100644 LocalPackages/SwiftUIExtensions/Sources/PreferencesViews/Assets.xcassets/Colors/LinkBlueColor.colorset/Contents.json create mode 100644 UnitTests/AppDelegate/AutoClearHandlerTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 086948af44..1caeda173f 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -128,6 +128,8 @@ 1DDF076328F815AD00EDFBE3 /* BWCommunicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDF075D28F815AD00EDFBE3 /* BWCommunicator.swift */; }; 1DDF076428F815AD00EDFBE3 /* BWManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDF075E28F815AD00EDFBE3 /* BWManager.swift */; }; 1DE03425298BC7F000CAB3D7 /* InternalUserDeciderStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D12F2E1298BC660009A65FD /* InternalUserDeciderStoreMock.swift */; }; + 1DEF3BAD2BD145A9004A2FBA /* AutoClearHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DEF3BAC2BD145A9004A2FBA /* AutoClearHandler.swift */; }; + 1DEF3BAE2BD145A9004A2FBA /* AutoClearHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DEF3BAC2BD145A9004A2FBA /* AutoClearHandler.swift */; }; 1DFAB51D2A8982A600A0F7F6 /* SetExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFAB51C2A8982A600A0F7F6 /* SetExtension.swift */; }; 1DFAB51E2A8982A600A0F7F6 /* SetExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFAB51C2A8982A600A0F7F6 /* SetExtension.swift */; }; 1DFAB5222A8983DE00A0F7F6 /* SetExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFAB51F2A89830D00A0F7F6 /* SetExtensionTests.swift */; }; @@ -2821,6 +2823,7 @@ 1DDF075F28F815AD00EDFBE3 /* BWStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BWStatus.swift; sourceTree = ""; }; 1DDF076028F815AD00EDFBE3 /* BWError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BWError.swift; sourceTree = ""; }; 1DDF076128F815AD00EDFBE3 /* BWResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BWResponse.swift; sourceTree = ""; }; + 1DEF3BAC2BD145A9004A2FBA /* AutoClearHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutoClearHandler.swift; sourceTree = ""; }; 1DFAB51C2A8982A600A0F7F6 /* SetExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetExtension.swift; sourceTree = ""; }; 1DFAB51F2A89830D00A0F7F6 /* SetExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetExtensionTests.swift; sourceTree = ""; }; 1E0C72052ABC63BD00802009 /* SubscriptionPagesUserScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionPagesUserScript.swift; sourceTree = ""; }; @@ -6404,6 +6407,7 @@ 858A798226A8B75F00A75A42 /* CopyHandler.swift */, 1D36E65A298ACD2900AA485D /* AppIconChanger.swift */, CB24F70B29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift */, + 1DEF3BAC2BD145A9004A2FBA /* AutoClearHandler.swift */, ); path = Application; sourceTree = ""; @@ -10131,6 +10135,7 @@ 3706FC96293F65D500E42796 /* HorizontallyCenteredLayout.swift in Sources */, 3706FC97293F65D500E42796 /* BookmarksOutlineView.swift in Sources */, 3706FC98293F65D500E42796 /* CountryList.swift in Sources */, + 1DEF3BAE2BD145A9004A2FBA /* AutoClearHandler.swift in Sources */, 4B37EE732B4CFF0800A89A61 /* HomePageRemoteMessagingStorage.swift in Sources */, 3706FC99293F65D500E42796 /* PreferencesSection.swift in Sources */, B6C8CAA82AD010DD0060E1CD /* YandexDataImporter.swift in Sources */, @@ -10952,6 +10957,7 @@ 37054FCE2876472D00033B6F /* WebViewSnapshotView.swift in Sources */, 4BBC16A027C4859400E00A38 /* DeviceAuthenticationService.swift in Sources */, CB24F70C29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift in Sources */, + 1DEF3BAD2BD145A9004A2FBA /* AutoClearHandler.swift in Sources */, 37CBCA9A2A8966E60050218F /* SyncSettingsAdapter.swift in Sources */, EEC4A66D2B2C894F00F7C0AA /* VPNLocationPreferenceItemModel.swift in Sources */, 3776582F27F82E62009A6B35 /* AutofillPreferences.swift in Sources */, diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index 5b6d2a1eb4..fef5473cfe 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -74,6 +74,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { let internalUserDecider: InternalUserDecider let featureFlagger: FeatureFlagger private var appIconChanger: AppIconChanger! + private var autoClearHandler: AutoClearHandler! private(set) var syncDataProviders: SyncDataProviders! private(set) var syncService: DDGSyncing? @@ -315,6 +316,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate { #if DBP DataBrokerProtectionAppEvents().applicationDidFinishLaunching() #endif + + setUpAutoClearHandler() } func applicationDidBecomeActive(_ notification: Notification) { @@ -352,6 +355,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate { } stateRestorationManager?.applicationWillTerminate() + // Handling of "Burn on quit" + if let terminationReply = autoClearHandler.handleAppTermination() { + return terminationReply + } + return .terminateNow } @@ -550,6 +558,17 @@ final class AppDelegate: NSObject, NSApplicationDelegate { PixelKit.fire(GeneralPixel.importDataInitial, frequency: .legacyInitial) } } + + private func setUpAutoClearHandler() { + autoClearHandler = AutoClearHandler(preferences: .shared, + fireViewModel: FireCoordinator.fireViewModel, + stateRestorationManager: stateRestorationManager) + autoClearHandler.handleAppLaunch() + autoClearHandler.onAutoClearCompleted = { + NSApplication.shared.reply(toApplicationShouldTerminate: true) + } + } + } func updateSubscriptionStatus() { diff --git a/DuckDuckGo/Application/AutoClearHandler.swift b/DuckDuckGo/Application/AutoClearHandler.swift new file mode 100644 index 0000000000..f1044dae5f --- /dev/null +++ b/DuckDuckGo/Application/AutoClearHandler.swift @@ -0,0 +1,125 @@ +// +// AutoClearHandler.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Combine + +final class AutoClearHandler { + + private let preferences: DataClearingPreferences + private let fireViewModel: FireViewModel + private let stateRestorationManager: AppStateRestorationManager + + init(preferences: DataClearingPreferences, + fireViewModel: FireViewModel, + stateRestorationManager: AppStateRestorationManager) { + self.preferences = preferences + self.fireViewModel = fireViewModel + self.stateRestorationManager = stateRestorationManager + } + + @MainActor + func handleAppLaunch() { + burnOnStartIfNeeded() + restoreTabsIfNeeded() + resetTheCorrectTerminationFlag() + } + + var onAutoClearCompleted: (() -> Void)? + + @MainActor + func handleAppTermination() -> NSApplication.TerminateReply? { + guard preferences.isAutoClearEnabled else { return nil } + + if preferences.isWarnBeforeClearingEnabled { + switch confirmAutoClear() { + case .alertFirstButtonReturn: + // Clear and Quit + performAutoClear() + return .terminateLater + case .alertSecondButtonReturn: + // Quit without Clearing Data + appTerminationHandledCorrectly = true + restoreTabsOnStartup = true + return .terminateNow + default: + // Cancel + return .terminateCancel + } + } + + performAutoClear() + return .terminateLater + } + + func resetTheCorrectTerminationFlag() { + appTerminationHandledCorrectly = false + } + + // MARK: - Private + + private func confirmAutoClear() -> NSApplication.ModalResponse { + let alert = NSAlert.autoClearAlert() + let response = alert.runModal() + return response + } + + @MainActor + private func performAutoClear() { + fireViewModel.fire.burnAll { [weak self] in + self?.appTerminationHandledCorrectly = true + self?.onAutoClearCompleted?() + } + } + + // MARK: - Burn On Start + // Burning on quit wasn't successful + + @UserDefaultsWrapper(key: .appTerminationHandledCorrectly, defaultValue: false) + private var appTerminationHandledCorrectly: Bool + + @MainActor + @discardableResult + func burnOnStartIfNeeded() -> Bool { + let shouldBurnOnStart = preferences.isAutoClearEnabled && !appTerminationHandledCorrectly + guard shouldBurnOnStart else { return false } + + fireViewModel.fire.burnAll() + return true + } + + // MARK: - Burn without Clearing Data + + @UserDefaultsWrapper(key: .restoreTabsOnStartup, defaultValue: false) + private var restoreTabsOnStartup: Bool + + @MainActor + @discardableResult + func restoreTabsIfNeeded() -> Bool { + let isAutoClearEnabled = preferences.isAutoClearEnabled + let restoreTabsOnStartup = restoreTabsOnStartup + self.restoreTabsOnStartup = false + if isAutoClearEnabled && restoreTabsOnStartup { + stateRestorationManager.restoreLastSessionState(interactive: false) + return true + } + + return false + } + +} diff --git a/DuckDuckGo/Assets.xcassets/Images/BurnAlert.imageset/Burn-Original-large.pdf b/DuckDuckGo/Assets.xcassets/Images/BurnAlert.imageset/Burn-Original-large.pdf deleted file mode 100644 index 2208d560684cf0225cd340b626bb4ff0110e1e21..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43425 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 O2(Me8J`%OG+y4XduDN0W diff --git a/DuckDuckGo/Assets.xcassets/Images/BurnAlert.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/BurnAlert.imageset/Contents.json index 48111f0d3f..3f5be128a3 100644 --- a/DuckDuckGo/Assets.xcassets/Images/BurnAlert.imageset/Contents.json +++ b/DuckDuckGo/Assets.xcassets/Images/BurnAlert.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "Burn-Original-large.pdf", + "filename" : "Fire-96x96.pdf", "idiom" : "universal" } ], diff --git a/DuckDuckGo/Assets.xcassets/Images/BurnAlert.imageset/Fire-96x96.pdf b/DuckDuckGo/Assets.xcassets/Images/BurnAlert.imageset/Fire-96x96.pdf new file mode 100644 index 0000000000000000000000000000000000000000..297359ff71c645b5cf4fc43b5e3171962c063b8a GIT binary patch literal 17864 zcmeI)S#M;=bqDbG{uK8`fE_^1yDxwtAX*W`f@Mt}0t9)WIMj%ahEzySAlvYF&+lKi zySYlk(PG2_Il#>2>8e{*r_S;}OV4{>eEjMA&34*snyRiYzWURqs($yo>ge~U=dZ56 zsanJ@k^CHed3^rrwEB;#-qz)(YW5HRxf}8E#p~m<^B*1Z>6`PD>$8jVfBW-yr&Vv> z2On&%t}joIUvK_edG)fs`tpDXzPnlM)phmPs*u0C`0nmUS8u*Du32Zke)H|k zYmQ&OT<83)I{UgzI_&xG`wrx?LK&T9{D9%X%ahZqasgi)UtgbIp0CeM`RDWF>&vsex9PUy+&67k zz2EiQWf4Zf9$WL7vCJeJUf3?R{rSX;_~Iy zqbE9W?r_GZu5f$xqmM4Wy|{ew-SNq3b@cJ+|D2ties+2M=T)w54}9zM(O$x;2=q>N z-#Y8|fTOpI@$KPnecp4jhrI7>aiKBlg%Tv1S>}G4#z!b8n~pge=BBZ;xgDpevFovBd*-U!*5fqKqmRbUF>=ab z9+#=JUcDX1zH94hWXMNA9U>L^1 z!Fzo2ggc8H#L?ni(cEWbsPAaTMVdCJgSC53W?5Q0b2)Quq0D}SN*uc| zt%y=T&s}EYRX^}iMOfQqC_5+M<1n{&X~kh6AmpQs$SfJzZ|km`2b}K6H3rxt39s=o zYoqq=YpSa`M_4?VJUhYCkejI<#@5`2N_4XMX0x|V6&*eLqN%P<&Np9g&Nq*E_!ykk z!!J{R@bCoxAwC*!*`~h>K60?Q2kPGgAI;mg?FUBd z{efPL?Kn5p%@cXue@xW|>KF6IFx2xvG@t5P_kR5!7a>KYpApg?7VTaC{`VjJcW+7x zM0hHo`!x{~XTHD3|1Sus9kxr;jJyxu1<=406@r3 z`Dx)S7T&O?&Oiqj=b3+)n`PwpM=ppb?KzZw8{O}l3~#3n?rOKz>;`_&JLfw7Jq~;t zrX?<`=f=jkf3O4USh)HCxEV|tE3~d@I=F+QT6%#OQ^=^)UCk>^u!A_QjStPZWmp0o zxVwQ{hK(SD-~r&J1p;R1YP2b~zB9G&05ns9FwhB(fhM?2-qN@k{F(YpZKg#S?1(*F zg*Tl5Fng~}1qPOP1f)$1~^$Z1;h`NST-4DJ)OrVS>H!>EeM|0c6fS&X8T9fjV$Ch$o{kO_47F0$dN-lzTvJtw@Bsxf}7E%n^dl z6PVf&F#-W_gIi?I?8O-gQArAb#F7QM6Ck#+w*()e2MXo1%``$+d5`-7Kh5s(T&e&! zoM)so&VWWNMmH8tcYRG%(QIt73pYYV7rQVU51eel)z&Q!0C7gr#=$MS`9@ISKC_=z z2n2i@*DW(NXLh}vdOy)%D3$dv1W+-SgZh0(uQN56ds#Zq05Et6 zgyK^_3kc05NEQM*KCiDII$^@zbS zH#Mj{+KXc>)`gl~)4-r*<-O3t&Ba6oy|bE5>*}*kTQ8Gf@iCg&q5$y+17_{E7K5xY zPv15PFjgOA0CClC@b5jAw?SZDz8lWf=+67xPDYqij=RGO zeTmi>*Pd~)3uYh|znBD)z zBQtyExNKomiH^Er3d@bLd+y0C3(Exrw-Ks`-|mPY3m-`%S!8VzEa3t-!$C1wM&{P;JI8m7d;%nP*qxjmLZ9V8zLgqI zKz=|SKOFn))U5c?H3E4^*&*&HB;F3$y@I_S`<=bMX z@_Dkqhle|f=r!BCTPM-fVbMc0^ z!n)|;0w23@5RD7iUHH1KCy(E4{4oCA1%&(RByxv?NX%m#(Oz=`n|6t*9~Y3D^x7Q( zxtm{8^7!n<_3`y-$*z3TmVaJ+eth*`x3d5F^?vgI(Ww3CvnulUv|LBZ2fu&w`t1DT z>g@VK@!;Y6e>88f3;Pep`JwGT{*t3l*2>?{Ie0Q}|5yj#^J1T8DL4K_SN{2xYoNb& zPVbRA=a-*-_(MW*N7fF|rwSZ<;*I|7Qs$l^?~v;{`f26L{jJ|Q^!m$Q5ZLsK>!aM7&x9= z{R{a`Y4pFJ*1#a)BB!ZRTqPwU8<>Ln>qm>M@--=xgZi?yOV`Q=YHE;AlyNSd4)Wro zbS%=`DuhcH>NJqxE#+-l%$fqo^GSiOHDpQ4CJ;Tu`qDycl_521P+8u)Nda4uyOJf9#v9UJAk8Cy6+yN0K!ktl_gb5k&4DZZr#lD$p# zOJ?0oss(m2NJm(a3`i-{x*@GU@-?MiqKQOWR{~w7r`OzZ#zbzsQU6r09#T^2HD2H@Sr5%-!%(hNHQKOvEI|)fr*NQ3WiNPUlM&nw$R680a!=vfyFSN3xvsEY5Npll; zA;%MYl$vO4ZmH)jG8Add(V4@&&;rGzw%9dE={Zdn>V3|gj;_)^q)eoZB9f8&kf1Vd z>@jtfHPt8*Qc~LZzmsI@sZ>@*>V8WZ1czGk%NClA&rQ*Js^1hXK_wuJY3@X2DOJbp zhX_Tc*@V;pS=>ix)^;)}o>+$zW*8Z>)%a@Lur~RmIZ3D|7u1ucSP|thMj<DVWAQ+`G5T;?DTMo$)Jj531 za4Tg1pHfN{779lBW7Lq8#<`_fGLQI;M3k9e7s$Y*Sb4IcKa=_&qW8HCIV_uBQpYMv ztmI$@4>Y@98lF>NOA2do65eOSL#c@?0^)fo(H8!>rdW2DOdO1|D(j&3L9y5y;-^AL zCR1T0D{%#6AOf92j3sCl3r%uKvul}C>fcZSMN#I0Go&hU9;^<*O{F;%w&J1jm=97N zETt5lsub2D9s1Br=1|mG@)mo?M19ikLK({4)05~Z+mvFfy=7T-otBx2LXoD0FE8aG z9gWeah)9=$r@FpALHy^>CQHTk?ua&eAm%`h&0*nK{1H8V2!~Rnu`Yowod>c1JsxOm z2uf(U5@jEk>$Y;tbSV+UA=OAVItSuW)xq&6dRDATkPEpeY+(#k+fzBx2}O^>3Z>$- z>!s{ndgdrFH7JZvYlF5-bw@YEX7JLbwLvR_$the*46h9s8V<=hZR2u+h_H{Wv`rkM z!ZhW#1Nxg*$0D=~X^8cV2~zzn4uG*Kt}7Mjat!w0@U#<>WW`0yWwg|;8OH*l;4r&0 ztfMQXCKzDF+62fHxyj9hM&^POi8T;Oz@%4`do`vBDOu%U^EUzuSRB*De6KT z8Sp@knBW+{fx)drh0({jgp{FyB*F!3xvSOua|Jx0qRhzU>XOO#0CJbSRa=eTShL1- zGB-JNGApcsFn@**RM^0C((% zwG}MUV@U=eQFDM@j#P`T#FVQ}a>yKtfE$a?bUXHAJ-sNAAU6E~(0DLBD%8N{x8@H%#8Zol= zq92A$1qbw610ZqZ+H=5SG>uC9UCsbmvNxFxFr`Jw-IMf&O);CEh{STE)&ad46*LUg z1``XP>UNP|v)!(Pm(B_o#4V!n+-b{U|6k4A~uCp03-Y=>(=tZp1KM6Zc=y3KQUS`XZKtf625OKn%-7m1`{8FN~s zHx3Ce(t4r!ouOh9Y$XdPxE%s?6H0++B50n>*Jp~_Ad(-?A=b7K>CtpT0r3P;jH{m~ zB;MB*ATeGExNrt0^=2e%l~^T2?bwbPV1a>TITi^r$lEYqcUwpn(}xfeBhMc@g?mbz znI(jXy7DuQI*Epn(p}GDun5A_*l{ExTw;TEU9P zB{O<5;-Q?n$O;e1G3MM9&|+d7By3BEQ7jNl#p?6lBs?G?E+Kts#iW;N{pl!^XiFT@JPGMBkha(ME2T*-j*ixN4hqiTN
      z2l`X&xS_Z%;2b5s!cQ*#GPBo2!f0r@yU!bN2GLtAreVd2w+q kG~P*9b@bxwFQ>UlvT=2Ne0g2&R!}>&oA=)P NSAlert { + let alert = NSAlert() + alert.messageText = UserText.warnBeforeQuitDialogHeader + alert.alertStyle = .warning + alert.icon = .burnAlert + alert.addButton(withTitle: UserText.clearAndQuit) + alert.addButton(withTitle: UserText.quitWithoutClearing) + alert.addButton(withTitle: UserText.cancel) + + let checkbox = NSButton(checkboxWithTitle: UserText.warnBeforeQuitDialogCheckboxMessage, + target: DataClearingPreferences.shared, + action: #selector(DataClearingPreferences.toggleWarnBeforeClearing)) + checkbox.state = DataClearingPreferences.shared.isWarnBeforeClearingEnabled ? .on : .off + checkbox.lineBreakMode = .byWordWrapping + checkbox.translatesAutoresizingMaskIntoConstraints = false + + // Create a container view for the checkbox with custom padding + let containerView = NSView(frame: NSRect(x: 0, y: 0, width: 240, height: 25)) + containerView.addSubview(checkbox) + + NSLayoutConstraint.activate([ + checkbox.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), + checkbox.centerYAnchor.constraint(equalTo: containerView.centerYAnchor, constant: -10), // Slightly up for better visual alignment + checkbox.widthAnchor.constraint(lessThanOrEqualTo: containerView.widthAnchor) + ]) + + alert.accessoryView = containerView + + return alert + } + @discardableResult func runModal() async -> NSApplication.ModalResponse { await withCheckedContinuation { continuation in diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index a201afff72..bd4d6505d3 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -53,6 +53,8 @@ struct UserText { static let pasteAndGo = NSLocalizedString("paste.and.go", value: "Paste & Go", comment: "Paste & Go button") static let pasteAndSearch = NSLocalizedString("paste.and.search", value: "Paste & Search", comment: "Paste & Search button") static let clear = NSLocalizedString("clear", value: "Clear", comment: "Clear button") + static let clearAndQuit = NSLocalizedString("clear.and.quit", value: "Clear and Quit", comment: "Button to clear data and quit the application") + static let quitWithoutClearing = NSLocalizedString("quit.without.clearing", value: "Quit Without Clearing", comment: "Button to quit the application without clearing data") static let `continue` = NSLocalizedString("`continue`", value: "Continue", comment: "Continue button") static let bookmarkDialogAdd = NSLocalizedString("bookmark.dialog.add", value: "Add", comment: "Button to confim a bookmark creation") static let newFolderDialogAdd = NSLocalizedString("folder.dialog.add", value: "Add", comment: "Button to confim a bookmark folder creation") @@ -1077,6 +1079,17 @@ struct UserText { static let fireproofCheckboxTitle = NSLocalizedString("fireproof.checkbox.title", value: "Ask to Fireproof websites when signing in", comment: "Fireproof settings checkbox title") static let fireproofExplanation = NSLocalizedString("fireproof.explanation", value: "When you Fireproof a site, cookies won't be erased and you'll stay signed in, even after using the Fire Button.", comment: "Fireproofing mechanism explanation") static let manageFireproofSites = NSLocalizedString("fireproof.manage-sites", value: "Manage Fireproof Sites…", comment: "Fireproof settings button caption") + static let autoClear = NSLocalizedString("auto.clear", value: "Auto-Clear", comment: "Header of a section in Settings. The setting configures clearing data automatically after quitting the app.") + static let automaticallyClearData = NSLocalizedString("automatically.clear.data", value: "Automatically clear tabs and browsing data when DuckDuckGo quits", comment: "Label after the checkbox in Settings which configures clearing data automatically after quitting the app.") + static let warnBeforeQuit = NSLocalizedString("warn.before.quit", value: "Warn me that tabs and data will be cleared when quitting", comment: "Label after the checkbox in Settings which configures a warning before clearing data on the application termination.") + static let warnBeforeQuitDialogHeader = NSLocalizedString("warn.before.quit.dialog.header", value: "Clear tabs and browsing data and quit DuckDuckGo?", comment: "A header of warning before clearing data on the application termination.") + static let warnBeforeQuitDialogCheckboxMessage = NSLocalizedString("warn.before.quit.dialog.checkbox.message", value: "Warn me every time", comment: "A label after checkbox to configure the warning before clearing data on the application termination.") + static let disableAutoClearToEnableSessionRestore = NSLocalizedString("disable.auto.clear.to.enable.session.restore", + value: "Disable auto-clear on quit to turn on session restore.", + comment: "Information label in Settings. It tells user that to enable session restoration setting they have to disable burn on quit. Auto-Clear should match the string with 'auto.clear' key") + static let showDataClearingSettings = NSLocalizedString("show.data.clearing.settings", + value: "Open Data Clearing Settings", + comment: "Button in Settings. It navigates user to Data Clearing Settings. The Data Clearing string should match the string with the preferences.data-clearing key") // MARK: Crash Report static let crashReportTitle = NSLocalizedString("crash-report.title", value: "DuckDuckGo Privacy Browser quit unexpectedly.", comment: "Title of the dialog where the user can send a crash report") diff --git a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift index 84c3835b1e..92e2f23e0b 100644 --- a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift +++ b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift @@ -55,6 +55,8 @@ public struct UserDefaultsWrapper { case grammarCheckEnabledOnce = "grammar.check.enabled.once" case loginDetectionEnabled = "fireproofing.login-detection-enabled" + case autoClearEnabled = "preferences.auto-clear-enabled" + case warnBeforeClearingEnabled = "preferences.warn-before-clearing-enabled" case gpcEnabled = "preferences.gpc-enabled" case selectedDownloadLocationKey = "preferences.download-location" case lastUsedCustomDownloadLocation = "preferences.custom-last-used-download-location" @@ -78,6 +80,8 @@ public struct UserDefaultsWrapper { case lastCrashReportCheckDate = "last.crash.report.check.date" case fireInfoPresentedOnce = "fire.info.presented.once" + case appTerminationHandledCorrectly = "app.termination.handled.correctly" + case restoreTabsOnStartup = "restore.tabs.on.startup" case restorePreviousSession = "preferences.startup.restore-previous-session" case launchToCustomHomePage = "preferences.startup.launch-to-custom-home-page" diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 2b07fcd86f..b226cf84c7 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -482,10 +482,56 @@ } }, "1." : { - - }, - "2. Open the downloaded DMG file and drag the Bitwarden application to\nthe /Applications folder." : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "1." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "1." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "1." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "1." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "1." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "1." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "1." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "1." + } + } + } }, "about.app_name" : { "comment" : "Application name to be displayed in the About dialog", @@ -2367,6 +2413,66 @@ } } }, + "auto.clear" : { + "comment" : "Header of a section in Settings. The setting configures clearing data automatically after quitting the app.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatisch löschen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Auto-Clear" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Borrado automático" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Effacement automatique" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancellazione automatica" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatisch wissen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatyczne czyszczenie" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limpeza automática" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Автоочистка" + } + } + } + }, "autoconsent.checkbox.title" : { "comment" : "Autoconsent settings checkbox title", "extractionState" : "extracted_with_value", @@ -5607,6 +5713,66 @@ } } }, + "automatically.clear.data" : { + "comment" : "Label after the checkbox in Settings which configures clearing data automatically after quitting the app.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tabs und Browserdaten automatisch löschen, wenn DuckDuckGo beendet wird" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Automatically clear tabs and browsing data when DuckDuckGo quits" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Borra automáticamente las pestañas y los datos de navegación cuando se cierra DuckDuckGo" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Effacer automatiquement les onglets et les données de navigation lorsque DuckDuckGo se ferme" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancella automaticamente schede e dati di navigazione quando chiudi DuckDuckGo" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tabbladen en browsegegevens automatisch wissen wanneer DuckDuckGo stopt" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatycznie czyść karty i dane przeglądania przy wychodzeniu z DuckDuckGo" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limpar automaticamente os separadores e os dados de navegação quando o DuckDuckGo fecha" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Автоматически закрывает вкладки и стирает данные о посещении сайтов при завершении работы DuckDuckGo." + } + } + } + }, "Birthday" : { "comment" : "Title of the section of the Identities manager where the user can add/modify a date of birth", "localizations" : { @@ -6263,11 +6429,59 @@ "comment" : "Message that warns user that specific Bitwarden app vesions are not compatible with this app", "extractionState" : "extracted_with_value", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Die folgenden Bitwarden-Versionen sind mit DuckDuckGo inkompatibel: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. Bitte kehre zu einer älteren Version zurück, indem du die folgenden Schritte ausführst:" + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "The following Bitwarden versions are incompatible with DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. Please revert to an older version by following these steps:" } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Las siguientes versiones de Bitwarden son incompatibles con DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. Vuelve a una versión anterior siguiendo estos pasos:" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Les versions suivantes de Bitwarden sont incompatibles avec DuckDuckGo : v2024.3.0, v2024.3.2, v2024.4.0 et v2024.4.1. Veuillez revenir à une version antérieure en procédant comme suit :" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Le seguenti versioni di Bitwarden non sono compatibili con DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. È necessario tornare a una versione precedente seguendo questi passaggi:" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "De volgende versies van Bitwarden zijn niet compatibel met DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. Volg deze stappen om terug te gaan naar een oudere versie:" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Następujące wersje Bitwarden są niezgodne z DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. Przywróć starszą wersję, wykonując następujące czynności:" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "As seguintes versões do Bitwarden são incompatíveis com o DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0 e v2024.4.1. Reverte para uma versão mais antiga seguindo estes passos:" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "С DuckDuckGo несовместимы следующие версии Bitwarden: 2024.3.0, 2024.3.2, 2024.4.0, 2024.4.1. Вернитесь к более старой версии, выполнив следующие действия:" + } } } }, @@ -6275,11 +6489,59 @@ "comment" : "First step to downgrade Bitwarden", "extractionState" : "extracted_with_value", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "V2014.2.1 herunterladen" + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "Download v2014.2.1" } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Descargar v2014.2.1" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Télécharger v2014.2.1" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Scarica la versione v2014.2.1" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Download v2014.2.1" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pobierz v2014.2.1" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Transfere a versão v2014.2.1" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Скачайте версию 2014.2.1" + } } } }, @@ -6287,11 +6549,59 @@ "comment" : "Second step to downgrade Bitwarden", "extractionState" : "extracted_with_value", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "2. Öffne die heruntergeladene DMG-Datei und ziehe die Bitwarden-Anwendung auf den Ordner „/Applications“." + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "2. Open the downloaded DMG file and drag the Bitwarden application to\nthe /Applications folder." } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "2. Abre el archivo DMG descargado y arrastra la aplicación Bitwarden a\nla carpeta /Applications." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "2. Ouvrez le fichier DMG téléchargé et faites glisser l'application Bitwarden vers\nle dossier /Applications." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "2. Apri il file DMG scaricato e trascina l'applicazione Bitwarden nella\n/cartella Applications." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "2. Open het gedownloade DMG-bestand en sleep de Bitwarden-app naar\n de map /Applications." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "2. Otwórz pobrany plik DMG i przeciągnij aplikację Bitwarden do\nfolderu /Applications." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "2. Abre o ficheiro DMG transferido e arrasta a aplicação Bitwarden para\na pasta /Applications." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "2. Откройте загруженный файл DMG и перетащите приложение Bitwarden в стандартную папку «Программы»." + } } } }, @@ -11099,6 +11409,66 @@ } } }, + "clear.and.quit" : { + "comment" : "Button to clear data and quit the application", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Löschen und Beenden" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Clear and Quit" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Borrar y salir" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Effacer et quitter" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancella ed esci" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wissen en stoppen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wyczyść i wyjdź" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limpar e sair" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Очистить и выйти" + } + } + } + }, "close.other.tabs" : { "comment" : "Menu item", "extractionState" : "extracted_with_value", @@ -13219,6 +13589,66 @@ } } }, + "disable.auto.clear.to.enable.session.restore" : { + "comment" : "Information label in Settings. It tells user that to enable session restoration setting they have to disable burn on quit. Auto-Clear should match the string with 'auto.clear' key", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deaktiviere das automatische Löschen beim Beenden, um die Sitzungswiederherstellung einzuschalten." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Disable auto-clear on quit to turn on session restore." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desactiva el borrado automático al salir para activar Restaurar sesión." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Désactivez l'effacement automatique à la fermeture pour activer la restauration de session." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Disabilita la cancellazione automatica all'uscita per attivare il ripristino della sessione." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Schakel automatisch wissen uit wanneer u stopt om het herstellen van de sessie in te schakelen." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wyłącz automatyczne czyszczenie przy wychodzeniu, aby włączyć przywracanie sesji." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desativa a limpeza automática ao sair para ativar a restauração da sessão." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Чтобы активировать восстановление сеанса, отключите автоочистку данных при выходе." + } + } + } + }, "disable.email.protection.mesage" : { "comment" : "Message for alert shown when user disables email protection", "extractionState" : "extracted_with_value", @@ -13690,9 +14120,6 @@ } } } - }, - "Download v2014.2.1" : { - }, "download.finishing" : { "comment" : "Download being finished information text", @@ -20420,7 +20847,7 @@ }, "Hide" : { "comment" : "Main Menu > View > Home Button > None item\n Preferences > Home Button > None item", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -24791,7 +25218,7 @@ "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Если у вас есть закладки в %@, попробуйте импортировать их вручную." + "value" : "Если у вас есть %@-файл с закладками, попробуйте импортировать его вручную." } } } @@ -24851,7 +25278,7 @@ "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Если у вас есть пароли в %@, попробуйте импортировать их вручную." + "value" : "Если у вас есть %@-файл с паролями, попробуйте импортировать его вручную." } } } @@ -30734,7 +31161,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Teile deine Gedanken" + "value" : "Sign Up To Participate" } }, "en" : { @@ -30794,7 +31221,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Nimm an unserer kurzen Umfrage teil und hilf uns, den besten Browser zu entwickeln." + "value" : "Join an interview with a member of our research team to help us build the best browser." } }, "en" : { @@ -30854,7 +31281,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Sag uns, was dich hierher gebracht hat" + "value" : "Share Your Thoughts With Us" } }, "en" : { @@ -31094,7 +31521,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Hilf uns, uns zu verbessern" + "value" : "Sign Up To Participate" } }, "en" : { @@ -31226,43 +31653,43 @@ "es" : { "stringUnit" : { "state" : "translated", - "value" : "Ayúdanos a mejorar" + "value" : "Share Your Thoughts With Us" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Aidez-nous à nous améliorer" + "value" : "Share Your Thoughts With Us" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Aiutaci a migliorare" + "value" : "Share Your Thoughts With Us" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Help ons om te verbeteren" + "value" : "Share Your Thoughts With Us" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Pomóż nam we wprowadzaniu ulepszeń" + "value" : "Share Your Thoughts With Us" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Ajuda-nos a melhorar" + "value" : "Share Your Thoughts With Us" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Помогите нам стать лучше" + "value" : "Share Your Thoughts With Us" } } } @@ -46228,6 +46655,66 @@ } } }, + "quit.without.clearing" : { + "comment" : "Button to quit the application without clearing data", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Beenden ohne Löschen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Quit Without Clearing" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Salir sin borrar" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quitter sans effacer" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Esci senza cancellare" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stoppen zonder wissen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wyjdź bez czyszczenia" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sair sem limpar" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Выйти без очистки" + } + } + } + }, "Recently Closed" : { "comment" : "Main Menu History item", "localizations" : { @@ -48373,7 +48860,7 @@ }, "Show left of the back button" : { "comment" : "Preferences > Home Button > left position item", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -48427,7 +48914,7 @@ }, "Show Left of the Back Button" : { "comment" : "Main Menu > View > Home Button > left position item", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -48693,7 +49180,7 @@ }, "Show right of the reload button" : { "comment" : "Preferences > Home Button > right position item", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -48747,7 +49234,7 @@ }, "Show Right of the Reload Button" : { "comment" : "Main Menu > View > Home Button > right position item", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -48852,6 +49339,66 @@ } } }, + "show.data.clearing.settings" : { + "comment" : "Button in Settings. It navigates user to Data Clearing Settings. The Data Clearing string should match the string with the preferences.data-clearing key", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Einstellungen zum Löschen von Daten öffnen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Open Data Clearing Settings" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir configuración de borrado de datos" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ouvrir les paramètres d'effacement des données" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apri Impostazioni cancellazione dati" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Instellingen voor het wissen van open gegevens" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otwórz ustawienia czyszczenia danych" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir Definições de limpeza de dados" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Открыть настройки очистки данных" + } + } + } + }, "show.folder.contents" : { "comment" : "Menu item that shows the content of a folder ", "extractionState" : "extracted_with_value", @@ -52942,6 +53489,186 @@ } } }, + "warn.before.quit" : { + "comment" : "Label after the checkbox in Settings which configures a warning before clearing data on the application termination.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Warne mich, dass Tabs und Daten beim Beenden gelöscht werden" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Warn me that tabs and data will be cleared when quitting" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Advertirme de que las pestañas y los datos se borrarán al salir" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "M'avertir que les onglets et les données seront effacés à la fermeture" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avvisa prima di cancellare schede e dati all'uscita" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Waarschuwen dat tabbladen en gegevens worden gewist bij het afsluiten" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ostrzegaj, że karty i dane zostaną wyczyszczone przy wychodzeniu" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avisar-me que os separadores e os dados serão limpos ao sair" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Показывать предупреждение о сбросе вкладок и данных при выходе" + } + } + } + }, + "warn.before.quit.dialog.checkbox.message" : { + "comment" : "A label after checkbox to configure the warning before clearing data on the application termination.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jedes Mal warnen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Warn me every time" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Advertirme cada vez" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toujours me prévenir" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avvisa ogni volta" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Elke keer waarschuwen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ostrzegaj za każdym razem" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avisar-me sempre" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Предупреждать каждый раз" + } + } + } + }, + "warn.before.quit.dialog.header" : { + "comment" : "A header of warning before clearing data on the application termination.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tabs und Browserdaten löschen und DuckDuckGo beenden?" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Clear tabs and browsing data and quit DuckDuckGo?" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Borrar pestañas y datos de navegación y salir de DuckDuckGo?" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Effacer les onglets et les données de navigation et quitter DuckDuckGo ?" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancellare le schede e i dati di navigazione e uscire da DuckDuckGo?" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tabbladen en browsergegevens wissen en DuckDuckGo afsluiten?" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wyczyścić karty i dane przeglądania i wyjść z DuckDuckGo?" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limpar separadores e dados de navegação e sair do DuckDuckGo?" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Очистить вкладки и данные и выйти из DuckDuckGo?" + } + } + } + }, "We couldn‘t find any bookmarks." : { "comment" : "Data import error message: Bookmarks weren‘t found.", "localizations" : { @@ -53608,4 +54335,4 @@ } }, "version" : "1.0" -} \ No newline at end of file +} diff --git a/DuckDuckGo/Preferences/Model/DataClearingPreferences.swift b/DuckDuckGo/Preferences/Model/DataClearingPreferences.swift index e336ab7cbd..26c2647981 100644 --- a/DuckDuckGo/Preferences/Model/DataClearingPreferences.swift +++ b/DuckDuckGo/Preferences/Model/DataClearingPreferences.swift @@ -29,6 +29,27 @@ final class DataClearingPreferences: ObservableObject, PreferencesTabOpening { } } + @Published + var isAutoClearEnabled: Bool { + didSet { + persistor.autoClearEnabled = isAutoClearEnabled + NotificationCenter.default.post(name: .autoClearDidChange, + object: nil, + userInfo: nil) + } + } + + @Published + var isWarnBeforeClearingEnabled: Bool { + didSet { + persistor.warnBeforeClearingEnabled = isWarnBeforeClearingEnabled + } + } + + @objc func toggleWarnBeforeClearing() { + isWarnBeforeClearingEnabled.toggle() + } + @MainActor func presentManageFireproofSitesDialog() { let fireproofDomainsWindowController = FireproofDomainsViewController.create().wrappedInWindowController() @@ -46,6 +67,8 @@ final class DataClearingPreferences: ObservableObject, PreferencesTabOpening { init(persistor: FireButtonPreferencesPersistor = FireButtonPreferencesUserDefaultsPersistor()) { self.persistor = persistor isLoginDetectionEnabled = persistor.loginDetectionEnabled + isAutoClearEnabled = persistor.autoClearEnabled + isWarnBeforeClearingEnabled = persistor.warnBeforeClearingEnabled } private var persistor: FireButtonPreferencesPersistor @@ -53,6 +76,8 @@ final class DataClearingPreferences: ObservableObject, PreferencesTabOpening { protocol FireButtonPreferencesPersistor { var loginDetectionEnabled: Bool { get set } + var autoClearEnabled: Bool { get set } + var warnBeforeClearingEnabled: Bool { get set } } struct FireButtonPreferencesUserDefaultsPersistor: FireButtonPreferencesPersistor { @@ -60,4 +85,14 @@ struct FireButtonPreferencesUserDefaultsPersistor: FireButtonPreferencesPersisto @UserDefaultsWrapper(key: .loginDetectionEnabled, defaultValue: false) var loginDetectionEnabled: Bool + @UserDefaultsWrapper(key: .autoClearEnabled, defaultValue: false) + var autoClearEnabled: Bool + + @UserDefaultsWrapper(key: .warnBeforeClearingEnabled, defaultValue: false) + var warnBeforeClearingEnabled: Bool + +} + +extension Notification.Name { + static let autoClearDidChange = Notification.Name("autoClearDidChange") } diff --git a/DuckDuckGo/Preferences/Model/SearchPreferences.swift b/DuckDuckGo/Preferences/Model/SearchPreferences.swift index bbe5cfb70c..65d385d72b 100644 --- a/DuckDuckGo/Preferences/Model/SearchPreferences.swift +++ b/DuckDuckGo/Preferences/Model/SearchPreferences.swift @@ -61,4 +61,9 @@ extension PreferencesTabOpening { WindowControllersManager.shared.show(url: url, source: .ui, newTab: true) } + @MainActor + func show(url: URL) { + WindowControllersManager.shared.show(url: url, source: .ui, newTab: false) + } + } diff --git a/DuckDuckGo/Preferences/Model/StartupPreferences.swift b/DuckDuckGo/Preferences/Model/StartupPreferences.swift index 6e46f9b729..9bdafbacf3 100644 --- a/DuckDuckGo/Preferences/Model/StartupPreferences.swift +++ b/DuckDuckGo/Preferences/Model/StartupPreferences.swift @@ -40,22 +40,28 @@ struct StartupPreferencesUserDefaultsPersistor: StartupPreferencesPersistor { } -final class StartupPreferences: ObservableObject { +final class StartupPreferences: ObservableObject, PreferencesTabOpening { static let shared = StartupPreferences() private let pinningManager: LocalPinningManager private var persistor: StartupPreferencesPersistor private var pinnedViewsNotificationCancellable: AnyCancellable? + private var dataClearingPreferences: DataClearingPreferences + private var dataClearingPreferencesNotificationCancellable: AnyCancellable? init(pinningManager: LocalPinningManager = LocalPinningManager.shared, - persistor: StartupPreferencesPersistor = StartupPreferencesUserDefaultsPersistor(appearancePrefs: AppearancePreferences.shared)) { + persistor: StartupPreferencesPersistor = StartupPreferencesUserDefaultsPersistor(appearancePrefs: AppearancePreferences.shared), + dataClearingPreferences: DataClearingPreferences = DataClearingPreferences.shared) { self.pinningManager = pinningManager self.persistor = persistor + self.dataClearingPreferences = dataClearingPreferences restorePreviousSession = persistor.restorePreviousSession launchToCustomHomePage = persistor.launchToCustomHomePage customHomePageURL = persistor.customHomePageURL updateHomeButtonState() listenToPinningManagerNotifications() + listenToDataClearingPreferencesNotifications() + checkDataClearingStatus() } @Published var restorePreviousSession: Bool { @@ -129,6 +135,21 @@ final class StartupPreferences: ObservableObject { } } + private func checkDataClearingStatus() { + if dataClearingPreferences.isAutoClearEnabled { + restorePreviousSession = false + } + } + + private func listenToDataClearingPreferencesNotifications() { + dataClearingPreferencesNotificationCancellable = NotificationCenter.default.publisher(for: .autoClearDidChange).sink { [weak self] _ in + guard let self = self else { + return + } + self.checkDataClearingStatus() + } + } + private func urlWithScheme(_ urlString: String) -> String? { guard var urlWithScheme = urlString.url else { return nil diff --git a/DuckDuckGo/Preferences/View/PreferencesDataClearingView.swift b/DuckDuckGo/Preferences/View/PreferencesDataClearingView.swift index dc3f28607c..1680fb2e69 100644 --- a/DuckDuckGo/Preferences/View/PreferencesDataClearingView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesDataClearingView.swift @@ -28,7 +28,20 @@ extension Preferences { var body: some View { PreferencePane(UserText.dataClearing) { - // SECTION 1: Fireproof Site + // SECTION 1: Automatically Clear Data + PreferencePaneSection(UserText.autoClear) { + + PreferencePaneSubSection { + ToggleMenuItem(UserText.automaticallyClearData, isOn: $model.isAutoClearEnabled) + ToggleMenuItem(UserText.warnBeforeQuit, + isOn: $model.isWarnBeforeClearingEnabled) + .disabled(!model.isAutoClearEnabled) + .padding(.leading, 16) + } + + } + + // SECTION 2: Fireproof Site PreferencePaneSection(UserText.fireproofSites) { PreferencePaneSubSection { diff --git a/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift b/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift index ad54e6e6ac..7d22bc8867 100644 --- a/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift @@ -28,6 +28,7 @@ extension Preferences { @ObservedObject var startupModel: StartupPreferences @ObservedObject var downloadsModel: DownloadsPreferences @ObservedObject var searchModel: SearchPreferences + @ObservedObject var dataClearingModel: DataClearingPreferences @State private var showingCustomHomePageSheet = false var body: some View { @@ -43,9 +44,19 @@ extension Preferences { Text(UserText.reopenAllWindowsFromLastSession).tag(true) .accessibilityIdentifier("PreferencesGeneralView.stateRestorePicker.reopenAllWindowsFromLastSession") }, label: {}) - .pickerStyle(.radioGroup) - .offset(x: PreferencesViews.Const.pickerHorizontalOffset) - .accessibilityIdentifier("PreferencesGeneralView.stateRestorePicker") + .pickerStyle(.radioGroup) + .disabled(dataClearingModel.isAutoClearEnabled) + .offset(x: PreferencesViews.Const.pickerHorizontalOffset) + .accessibilityIdentifier("PreferencesGeneralView.stateRestorePicker") + if dataClearingModel.isAutoClearEnabled { + VStack(alignment: .leading, spacing: 1) { + TextMenuItemCaption(UserText.disableAutoClearToEnableSessionRestore) + TextButton(UserText.showDataClearingSettings) { + startupModel.show(url: .settingsPane(.dataClearing)) + } + } + .padding(.leading, 19) + } } } diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index 4c077395c7..5d2f842652 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -87,7 +87,8 @@ enum Preferences { case .general: GeneralView(startupModel: StartupPreferences.shared, downloadsModel: DownloadsPreferences.shared, - searchModel: SearchPreferences.shared) + searchModel: SearchPreferences.shared, + dataClearingModel: DataClearingPreferences.shared) case .sync: SyncView() case .appearance: diff --git a/DuckDuckGo/StateRestoration/StatePersistenceService.swift b/DuckDuckGo/StateRestoration/StatePersistenceService.swift index 6aed78934a..5d65a2c1d0 100644 --- a/DuckDuckGo/StateRestoration/StatePersistenceService.swift +++ b/DuckDuckGo/StateRestoration/StatePersistenceService.swift @@ -65,6 +65,7 @@ final class StatePersistenceService { func removeLastSessionState() { lastSessionStateArchive = nil + fileStore.remove(fileAtURL: URL.persistenceLocation(for: self.fileName)) } @MainActor diff --git a/LocalPackages/SwiftUIExtensions/Sources/PreferencesViews/Assets.xcassets/Colors/Contents.json b/LocalPackages/SwiftUIExtensions/Sources/PreferencesViews/Assets.xcassets/Colors/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/LocalPackages/SwiftUIExtensions/Sources/PreferencesViews/Assets.xcassets/Colors/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LocalPackages/SwiftUIExtensions/Sources/PreferencesViews/Assets.xcassets/Colors/LinkBlueColor.colorset/Contents.json b/LocalPackages/SwiftUIExtensions/Sources/PreferencesViews/Assets.xcassets/Colors/LinkBlueColor.colorset/Contents.json new file mode 100644 index 0000000000..bb413935ee --- /dev/null +++ b/LocalPackages/SwiftUIExtensions/Sources/PreferencesViews/Assets.xcassets/Colors/LinkBlueColor.colorset/Contents.json @@ -0,0 +1,78 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xEE", + "green" : "0x69", + "red" : "0x39" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xF6", + "green" : "0x94", + "red" : "0x71" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "contrast", + "value" : "high" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.933", + "green" : "0.412", + "red" : "0.224" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + }, + { + "appearance" : "contrast", + "value" : "high" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.965", + "green" : "0.580", + "red" : "0.443" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Resources/Localizable.xcstrings b/LocalPackages/SyncUI/Sources/SyncUI/Resources/Localizable.xcstrings index 4abefdc849..dc53430490 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Resources/Localizable.xcstrings +++ b/LocalPackages/SyncUI/Sources/SyncUI/Resources/Localizable.xcstrings @@ -123,11 +123,59 @@ "comment" : "Title for an error alert", "extractionState" : "extracted_with_value", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sync & Backup Error" + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "Sync & Backup Error" } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sync & Backup Error" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sync & Backup Error" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sync & Backup Error" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sync & Backup Error" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sync & Backup Error" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sync & Backup Error" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sync & Backup Error" + } } } }, @@ -135,11 +183,59 @@ "comment" : "Button Title of an error alert", "extractionState" : "extracted_with_value", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Go to Settings" + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "Go to Settings" } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Go to Settings" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Go to Settings" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Go to Settings" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Go to Settings" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Go to Settings" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Go to Settings" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Go to Settings" + } } } }, @@ -207,11 +303,59 @@ "comment" : "Description for unable to authenticate error", "extractionState" : "extracted_with_value", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "A device password is required to use Sync & Backup." + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "A device password is required to use Sync & Backup." } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "A device password is required to use Sync & Backup." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "A device password is required to use Sync & Backup." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "A device password is required to use Sync & Backup." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "A device password is required to use Sync & Backup." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "A device password is required to use Sync & Backup." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "A device password is required to use Sync & Backup." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "A device password is required to use Sync & Backup." + } } } }, @@ -6277,4 +6421,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/UnitTests/AppDelegate/AutoClearHandlerTests.swift b/UnitTests/AppDelegate/AutoClearHandlerTests.swift new file mode 100644 index 0000000000..192de793f3 --- /dev/null +++ b/UnitTests/AppDelegate/AutoClearHandlerTests.swift @@ -0,0 +1,83 @@ +// +// AutoClearHandlerTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import XCTest + +@testable import DuckDuckGo_Privacy_Browser + +@MainActor +class AutoClearHandlerTests: XCTestCase { + + var handler: AutoClearHandler! + var preferences: DataClearingPreferences! + var fireViewModel: FireViewModel! + + override func setUp() { + super.setUp() + let persistor = MockFireButtonPreferencesPersistor() + preferences = DataClearingPreferences(persistor: persistor) + fireViewModel = FireViewModel(fire: Fire(tld: ContentBlocking.shared.tld)) + let fileName = "AutoClearHandlerTests" + let fileStore = FileStoreMock() + let service = StatePersistenceService(fileStore: fileStore, fileName: fileName) + let appStateRestorationManager = AppStateRestorationManager(fileStore: fileStore, + service: service, + shouldRestorePreviousSession: false) + handler = AutoClearHandler(preferences: preferences, fireViewModel: fireViewModel, stateRestorationManager: appStateRestorationManager) + } + + override func tearDown() { + handler = nil + preferences = nil + fireViewModel = nil + super.tearDown() + } + + func testWhenBurningEnabledAndNoWarningRequiredThenTerminateLaterIsReturned() { + preferences.isAutoClearEnabled = true + preferences.isWarnBeforeClearingEnabled = false + + let response = handler.handleAppTermination() + + XCTAssertEqual(response, .terminateLater) + } + + func testWhenBurningDisabledThenNoTerminationResponse() { + preferences.isAutoClearEnabled = false + + let response = handler.handleAppTermination() + + XCTAssertNil(response) + } + + func testWhenBurningEnabledAndFlagFalseThenBurnOnStartTriggered() { + preferences.isAutoClearEnabled = true + handler.resetTheCorrectTerminationFlag() + + XCTAssertTrue(handler.burnOnStartIfNeeded()) + } + + func testWhenBurningDisabledThenBurnOnStartNotTriggered() { + preferences.isAutoClearEnabled = false + handler.resetTheCorrectTerminationFlag() + + XCTAssertFalse(handler.burnOnStartIfNeeded()) + } + +} diff --git a/UnitTests/Preferences/DataClearingPreferencesTests.swift b/UnitTests/Preferences/DataClearingPreferencesTests.swift index 5563fb6e90..1c0b6675e4 100644 --- a/UnitTests/Preferences/DataClearingPreferencesTests.swift +++ b/UnitTests/Preferences/DataClearingPreferencesTests.swift @@ -20,7 +20,11 @@ import XCTest @testable import DuckDuckGo_Privacy_Browser class MockFireButtonPreferencesPersistor: FireButtonPreferencesPersistor { + + var autoClearEnabled: Bool = false + var warnBeforeClearingEnabled: Bool = false var loginDetectionEnabled: Bool = false + } class DataClearingPreferencesTests: XCTestCase { From da93b48a038ee708123330293ce0606bf8c3c92e Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Mon, 29 Apr 2024 11:34:37 +0200 Subject: [PATCH 14/16] macOS VPN: Remove waitlist code (#2708) Task/Issue URL: https://app.asana.com/0/0/1207169061635760/f ## Description Removes the VPN waitlist code entirely. --- DuckDuckGo.xcodeproj/project.pbxproj | 44 ----- DuckDuckGo/Application/AppDelegate.swift | 10 - .../Card-16.imageset/Card-16.pdf | Bin 3961 -> 0 bytes .../Card-16.imageset/Contents.json | 15 -- .../Rocket-16.imageset/Contents.json | 15 -- .../Rocket-16.imageset/Rocket-16.pdf | Bin 4812 -> 0 bytes .../Shield-16.imageset/Contents.json | 15 -- .../Shield-16.imageset/Shield-16.pdf | Bin 2579 -> 0 bytes .../UserText+NetworkProtection.swift | 152 --------------- .../NavigationBar/View/MoreOptionsMenu.swift | 31 ++- .../View/NavigationBarViewController.swift | 47 +---- ...NetworkProtectionInviteCodeViewModel.swift | 184 ------------------ .../NetworkProtectionInviteDialog.swift | 36 ---- .../NetworkProtectionInvitePresenter.swift | 64 ------ .../NetworkProtectionDebugMenu.swift | 99 ---------- .../NetworkProtectionDebugUtilities.swift | 4 +- .../NetworkProtectionNavBarButtonModel.swift | 8 - ...etworkProtectionNavBarPopoverManager.swift | 10 +- ...tionWaitlistFeatureFlagOverridesMenu.swift | 166 ---------------- .../NetworkProtectionRemoteMessaging.swift | 2 +- ...rkProtectionSubscriptionEventHandler.swift | 5 +- DuckDuckGo/Preferences/Model/AboutModel.swift | 10 - .../Model/VPNPreferencesModel.swift | 2 +- .../View/PreferencesAboutView.swift | 3 - .../View/PreferencesRootView.swift | 3 +- .../VPNMetadataCollector.swift | 6 - .../NetworkProtectionFeatureDisabler.swift | 13 +- .../NetworkProtectionFeatureVisibility.swift | 105 +--------- .../Waitlist/Views/WaitlistRootView.swift | 25 --- .../EnableWaitlistFeatureView.swift | 69 ------- .../WaitlistSteps/InvitedToWaitlistView.swift | 23 --- .../WaitlistSteps/JoinWaitlistView.swift | 10 - .../WaitlistSteps/JoinedWaitlistView.swift | 11 -- .../WaitlistTermsAndConditionsView.swift | 70 ------- .../WaitlistThankYouPromptPresenter.swift | 37 +--- .../WaitlistViewControllerPresenter.swift | 39 ---- DuckDuckGo/Waitlist/Waitlist.swift | 82 -------- ...tlistTermsAndConditionsActionHandler.swift | 15 -- UnitTests/Menus/MoreOptionsMenuTests.swift | 42 +--- .../VPNFeedbackFormViewModelTests.swift | 1 - .../MockNetworkProtectionCodeRedeemer.swift | 48 ----- 41 files changed, 37 insertions(+), 1484 deletions(-) delete mode 100644 DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Card-16.imageset/Card-16.pdf delete mode 100644 DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Card-16.imageset/Contents.json delete mode 100644 DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Rocket-16.imageset/Contents.json delete mode 100644 DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Rocket-16.imageset/Rocket-16.pdf delete mode 100644 DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Shield-16.imageset/Contents.json delete mode 100644 DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Shield-16.imageset/Shield-16.pdf delete mode 100644 DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInviteCodeViewModel.swift delete mode 100644 DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInviteDialog.swift delete mode 100644 DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInvitePresenter.swift delete mode 100644 DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift delete mode 100644 DuckDuckGo/Waitlist/Views/WaitlistSteps/EnableWaitlistFeatureView.swift delete mode 100644 UnitTests/Waitlist/Mocks/MockNetworkProtectionCodeRedeemer.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 1caeda173f..40365ef615 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1162,11 +1162,6 @@ 4B4D60C12A0C848E00BCD287 /* NetworkProtectionControllerErrorStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D606A2A0B29FA00BCD287 /* NetworkProtectionControllerErrorStore.swift */; }; 4B4D60C22A0C849000BCD287 /* EventMapping+NetworkProtectionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60722A0B29FA00BCD287 /* EventMapping+NetworkProtectionError.swift */; }; 4B4D60C32A0C849100BCD287 /* EventMapping+NetworkProtectionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60722A0B29FA00BCD287 /* EventMapping+NetworkProtectionError.swift */; }; - 4B4D60C42A0C849600BCD287 /* NetworkProtectionInvitePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D606F2A0B29FA00BCD287 /* NetworkProtectionInvitePresenter.swift */; }; - 4B4D60C62A0C849600BCD287 /* NetworkProtectionInviteCodeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60702A0B29FA00BCD287 /* NetworkProtectionInviteCodeViewModel.swift */; }; - 4B4D60CA2A0C849600BCD287 /* NetworkProtectionInvitePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D606F2A0B29FA00BCD287 /* NetworkProtectionInvitePresenter.swift */; }; - 4B4D60CC2A0C849600BCD287 /* NetworkProtectionInviteCodeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60702A0B29FA00BCD287 /* NetworkProtectionInviteCodeViewModel.swift */; }; - 4B4D60CF2A0C849600BCD287 /* NetworkProtectionInviteDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D606C2A0B29FA00BCD287 /* NetworkProtectionInviteDialog.swift */; }; 4B4D60D32A0C84F700BCD287 /* UserText+NetworkProtection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60D22A0C84F700BCD287 /* UserText+NetworkProtection.swift */; }; 4B4D60D42A0C84F700BCD287 /* UserText+NetworkProtection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60D22A0C84F700BCD287 /* UserText+NetworkProtection.swift */; }; 4B4D60DD2A0C875E00BCD287 /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; @@ -1209,7 +1204,6 @@ 4B723E1226B0006E00E14D75 /* DataImport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B723DEB26B0002B00E14D75 /* DataImport.swift */; }; 4B723E1326B0007A00E14D75 /* CSVLoginExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B723DFD26B0002B00E14D75 /* CSVLoginExporter.swift */; }; 4B723E1926B000DC00E14D75 /* TemporaryFileCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B723E1726B000DC00E14D75 /* TemporaryFileCreator.swift */; }; - 4B7534CC2A1FD7EA00158A99 /* NetworkProtectionInviteDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D606C2A0B29FA00BCD287 /* NetworkProtectionInviteDialog.swift */; }; 4B7A57CF279A4EF300B1C70E /* ChromiumPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B7A57CE279A4EF300B1C70E /* ChromiumPreferences.swift */; }; 4B7A60A1273E0BE400BBDFEB /* WKWebsiteDataStoreExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B7A60A0273E0BE400BBDFEB /* WKWebsiteDataStoreExtension.swift */; }; 4B85A48028821CC500FC4C39 /* NSPasteboardItemExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B85A47F28821CC500FC4C39 /* NSPasteboardItemExtension.swift */; }; @@ -1272,8 +1266,6 @@ 4B9DB02A2A983B24000927DB /* WaitlistStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB00E2A983B24000927DB /* WaitlistStorage.swift */; }; 4B9DB02C2A983B24000927DB /* WaitlistKeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB00F2A983B24000927DB /* WaitlistKeychainStorage.swift */; }; 4B9DB02D2A983B24000927DB /* WaitlistKeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB00F2A983B24000927DB /* WaitlistKeychainStorage.swift */; }; - 4B9DB0322A983B24000927DB /* EnableWaitlistFeatureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB0132A983B24000927DB /* EnableWaitlistFeatureView.swift */; }; - 4B9DB0332A983B24000927DB /* EnableWaitlistFeatureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB0132A983B24000927DB /* EnableWaitlistFeatureView.swift */; }; 4B9DB0352A983B24000927DB /* WaitlistTermsAndConditionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB0142A983B24000927DB /* WaitlistTermsAndConditionsView.swift */; }; 4B9DB0362A983B24000927DB /* WaitlistTermsAndConditionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB0142A983B24000927DB /* WaitlistTermsAndConditionsView.swift */; }; 4B9DB0382A983B24000927DB /* JoinedWaitlistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB0152A983B24000927DB /* JoinedWaitlistView.swift */; }; @@ -1294,8 +1286,6 @@ 4B9DB0552A983B55000927DB /* MockWaitlistStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB04F2A983B55000927DB /* MockWaitlistStorage.swift */; }; 4B9DB0562A983B55000927DB /* MockNotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB0502A983B55000927DB /* MockNotificationService.swift */; }; 4B9DB0572A983B55000927DB /* MockNotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB0502A983B55000927DB /* MockNotificationService.swift */; }; - 4B9DB0582A983B55000927DB /* MockNetworkProtectionCodeRedeemer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB0512A983B55000927DB /* MockNetworkProtectionCodeRedeemer.swift */; }; - 4B9DB0592A983B55000927DB /* MockNetworkProtectionCodeRedeemer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB0512A983B55000927DB /* MockNetworkProtectionCodeRedeemer.swift */; }; 4B9DB05A2A983B55000927DB /* MockWaitlistRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB0522A983B55000927DB /* MockWaitlistRequest.swift */; }; 4B9DB05B2A983B55000927DB /* MockWaitlistRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB0522A983B55000927DB /* MockWaitlistRequest.swift */; }; 4B9DB05C2A983B55000927DB /* WaitlistViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB0532A983B55000927DB /* WaitlistViewModelTests.swift */; }; @@ -1552,12 +1542,10 @@ 7BEEA5122AD1235B00A9E72B /* NetworkProtectionIPC in Frameworks */ = {isa = PBXBuildFile; productRef = 7BEEA5112AD1235B00A9E72B /* NetworkProtectionIPC */; }; 7BEEA5142AD1236300A9E72B /* NetworkProtectionIPC in Frameworks */ = {isa = PBXBuildFile; productRef = 7BEEA5132AD1236300A9E72B /* NetworkProtectionIPC */; }; 7BEEA5162AD1236E00A9E72B /* NetworkProtectionUI in Frameworks */ = {isa = PBXBuildFile; productRef = 7BEEA5152AD1236E00A9E72B /* NetworkProtectionUI */; }; - 7BFE95522A9DF1CE0081ABE9 /* NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFE95512A9DF1CE0081ABE9 /* NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift */; }; 7BFE95542A9DF2930081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFE95532A9DF2930081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift */; }; 7BFE95552A9DF2990081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFE95532A9DF2930081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift */; }; 7BFE95562A9DF29B0081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFE95532A9DF2930081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift */; }; 7BFE95592A9DF2AF0081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFE95532A9DF2930081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift */; }; - 7BFE955A2A9DF4550081ABE9 /* NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFE95512A9DF1CE0081ABE9 /* NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift */; }; 7BFF850F2B0C09DA00ECACA2 /* DuckDuckGo Personal Information Removal.app in Embed Login Items */ = {isa = PBXBuildFile; fileRef = 9D9AE8D12AAA39A70026E7DC /* DuckDuckGo Personal Information Removal.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 85012B0229133F9F003D0DCC /* NavigationBarPopovers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85012B0129133F9F003D0DCC /* NavigationBarPopovers.swift */; }; 850E8DFB2A6FEC5E00691187 /* BookmarksBarAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850E8DFA2A6FEC5E00691187 /* BookmarksBarAppearance.swift */; }; @@ -3058,9 +3046,6 @@ 4B4D60652A0B29FA00BCD287 /* NetworkProtectionNavBarButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionNavBarButtonModel.swift; sourceTree = ""; }; 4B4D60692A0B29FA00BCD287 /* NetworkProtection+ConvenienceInitializers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NetworkProtection+ConvenienceInitializers.swift"; sourceTree = ""; }; 4B4D606A2A0B29FA00BCD287 /* NetworkProtectionControllerErrorStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionControllerErrorStore.swift; sourceTree = ""; }; - 4B4D606C2A0B29FA00BCD287 /* NetworkProtectionInviteDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionInviteDialog.swift; sourceTree = ""; }; - 4B4D606F2A0B29FA00BCD287 /* NetworkProtectionInvitePresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionInvitePresenter.swift; sourceTree = ""; }; - 4B4D60702A0B29FA00BCD287 /* NetworkProtectionInviteCodeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionInviteCodeViewModel.swift; sourceTree = ""; }; 4B4D60722A0B29FA00BCD287 /* EventMapping+NetworkProtectionError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EventMapping+NetworkProtectionError.swift"; sourceTree = ""; }; 4B4D60762A0B29FA00BCD287 /* NetworkProtectionUNNotificationsPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionUNNotificationsPresenter.swift; sourceTree = ""; }; 4B4D607C2A0B29FA00BCD287 /* UserText+NetworkProtectionExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserText+NetworkProtectionExtensions.swift"; sourceTree = ""; }; @@ -3155,7 +3140,6 @@ 4B9DB00C2A983B24000927DB /* WaitlistViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WaitlistViewModel.swift; sourceTree = ""; }; 4B9DB00E2A983B24000927DB /* WaitlistStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WaitlistStorage.swift; sourceTree = ""; }; 4B9DB00F2A983B24000927DB /* WaitlistKeychainStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WaitlistKeychainStorage.swift; sourceTree = ""; }; - 4B9DB0132A983B24000927DB /* EnableWaitlistFeatureView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnableWaitlistFeatureView.swift; sourceTree = ""; }; 4B9DB0142A983B24000927DB /* WaitlistTermsAndConditionsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WaitlistTermsAndConditionsView.swift; sourceTree = ""; }; 4B9DB0152A983B24000927DB /* JoinedWaitlistView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JoinedWaitlistView.swift; sourceTree = ""; }; 4B9DB0162A983B24000927DB /* InvitedToWaitlistView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InvitedToWaitlistView.swift; sourceTree = ""; }; @@ -3166,7 +3150,6 @@ 4B9DB01C2A983B24000927DB /* NotificationService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; 4B9DB04F2A983B55000927DB /* MockWaitlistStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockWaitlistStorage.swift; sourceTree = ""; }; 4B9DB0502A983B55000927DB /* MockNotificationService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockNotificationService.swift; sourceTree = ""; }; - 4B9DB0512A983B55000927DB /* MockNetworkProtectionCodeRedeemer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockNetworkProtectionCodeRedeemer.swift; sourceTree = ""; }; 4B9DB0522A983B55000927DB /* MockWaitlistRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockWaitlistRequest.swift; sourceTree = ""; }; 4B9DB0532A983B55000927DB /* WaitlistViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WaitlistViewModelTests.swift; sourceTree = ""; }; 4BA1A69A258B076900F6F690 /* FileStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileStore.swift; sourceTree = ""; }; @@ -3329,7 +3312,6 @@ 7BEC20402B0F505F00243D3E /* AddBookmarkPopoverView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddBookmarkPopoverView.swift; sourceTree = ""; }; 7BEC20412B0F505F00243D3E /* AddBookmarkFolderPopoverView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddBookmarkFolderPopoverView.swift; sourceTree = ""; }; 7BF1A9D72AE054D300FCA683 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 7BFE95512A9DF1CE0081ABE9 /* NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift; sourceTree = ""; }; 7BFE95532A9DF2930081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UserDefaults+NetworkProtectionWaitlist.swift"; sourceTree = ""; }; 85012B0129133F9F003D0DCC /* NavigationBarPopovers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarPopovers.swift; sourceTree = ""; }; 850E8DFA2A6FEC5E00691187 /* BookmarksBarAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksBarAppearance.swift; sourceTree = ""; }; @@ -5075,7 +5057,6 @@ children = ( 4B4D60722A0B29FA00BCD287 /* EventMapping+NetworkProtectionError.swift */, BDE981DB2BBD110800645880 /* Assets */, - 4B4D606B2A0B29FA00BCD287 /* Invite */, 7BD3AF5C2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift */, 9D9AE8682AA76CDC0026E7DC /* LoginItem+NetworkProtection.swift */, B602E81C2A1E25B0006D261F /* NEOnDemandRuleExtension.swift */, @@ -5089,7 +5070,6 @@ 7B05829D2A812AC000AC3F7C /* NetworkProtectionOnboardingMenu.swift */, 7B430EA02A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift */, 4B8F52402A18326600BE7131 /* NetworkProtectionTunnelController.swift */, - 7BFE95512A9DF1CE0081ABE9 /* NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift */, EEA3EEAF2B24EB5100E8333A /* VPNLocation */, 7B934C3D2A866CFF00FC8F9C /* NetworkProtectionOnboardingMenu.swift */, B6F1B02D2BCE6B47005E863C /* TunnelControllerProvider.swift */, @@ -5097,16 +5077,6 @@ path = BothAppTargets; sourceTree = ""; }; - 4B4D606B2A0B29FA00BCD287 /* Invite */ = { - isa = PBXGroup; - children = ( - 4B4D606C2A0B29FA00BCD287 /* NetworkProtectionInviteDialog.swift */, - 4B4D606F2A0B29FA00BCD287 /* NetworkProtectionInvitePresenter.swift */, - 4B4D60702A0B29FA00BCD287 /* NetworkProtectionInviteCodeViewModel.swift */, - ); - path = Invite; - sourceTree = ""; - }; 4B4D60742A0B29FA00BCD287 /* NetworkExtensionTargets */ = { isa = PBXGroup; children = ( @@ -5460,7 +5430,6 @@ 4B9DB0122A983B24000927DB /* WaitlistSteps */ = { isa = PBXGroup; children = ( - 4B9DB0132A983B24000927DB /* EnableWaitlistFeatureView.swift */, 4B9DB0142A983B24000927DB /* WaitlistTermsAndConditionsView.swift */, 4B9DB0152A983B24000927DB /* JoinedWaitlistView.swift */, 4B9DB0162A983B24000927DB /* InvitedToWaitlistView.swift */, @@ -5493,7 +5462,6 @@ 317295D02AF058D3002C3206 /* MockWaitlistTermsAndConditionsActionHandler.swift */, 4B9DB04F2A983B55000927DB /* MockWaitlistStorage.swift */, 4B9DB0502A983B55000927DB /* MockNotificationService.swift */, - 4B9DB0512A983B55000927DB /* MockNetworkProtectionCodeRedeemer.swift */, 4B9DB0522A983B55000927DB /* MockWaitlistRequest.swift */, ); path = Mocks; @@ -9451,7 +9419,6 @@ 373D9B4929EEAC1B00381FDD /* SyncMetadataDatabase.swift in Sources */, 3706FAB8293F65D500E42796 /* FaviconImageCache.swift in Sources */, 3706FAB9293F65D500E42796 /* TabBarViewController.swift in Sources */, - 4B9DB0332A983B24000927DB /* EnableWaitlistFeatureView.swift in Sources */, 3706FABA293F65D500E42796 /* BookmarkOutlineViewDataSource.swift in Sources */, 3706FABB293F65D500E42796 /* PasswordManagementBitwardenItemView.swift in Sources */, 1D220BF92B86192200F8BBC6 /* PreferencesEmailProtectionView.swift in Sources */, @@ -9634,7 +9601,6 @@ 1ED910D62B63BFB300936947 /* IdentityTheftRestorationPagesUserScript.swift in Sources */, 3706FB40293F65D500E42796 /* ContextualMenu.swift in Sources */, 3706FB41293F65D500E42796 /* NavigationBarViewController.swift in Sources */, - 4B7534CC2A1FD7EA00158A99 /* NetworkProtectionInviteDialog.swift in Sources */, 3707C71C294B5D1900682A9F /* TabExtensionsBuilder.swift in Sources */, 3706FB42293F65D500E42796 /* MainViewController.swift in Sources */, 3706FB43293F65D500E42796 /* DuckPlayer.swift in Sources */, @@ -9695,7 +9661,6 @@ 3706FB6C293F65D500E42796 /* FaviconStore.swift in Sources */, 3706FB6D293F65D500E42796 /* SuggestionListCharacteristics.swift in Sources */, 377D801F2AB48191002AF251 /* FavoritesDisplayModeSyncHandler.swift in Sources */, - 4B4D60C62A0C849600BCD287 /* NetworkProtectionInviteCodeViewModel.swift in Sources */, 3706FB6F293F65D500E42796 /* BookmarkListViewController.swift in Sources */, 3706FB70293F65D500E42796 /* SecureVaultLoginImporter.swift in Sources */, 3706FB72293F65D500E42796 /* RecentlyClosedCoordinator.swift in Sources */, @@ -9784,7 +9749,6 @@ 9FA173E82B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift in Sources */, 3706FBA1293F65D500E42796 /* FireproofDomainsContainer.swift in Sources */, 3706FBA2293F65D500E42796 /* GeolocationService.swift in Sources */, - 4B4D60C42A0C849600BCD287 /* NetworkProtectionInvitePresenter.swift in Sources */, 3706FBA3293F65D500E42796 /* FireproofingURLExtensions.swift in Sources */, 3169132A2BD2C7570051B46D /* DataBrokerProtectionErrorViewController.swift in Sources */, 1DDD3EC12B84F5D5004CBF2B /* PreferencesCookiePopupProtectionView.swift in Sources */, @@ -10088,7 +10052,6 @@ 3706FC71293F65D500E42796 /* NSColorExtension.swift in Sources */, 1DB9618229F67F6100CF5568 /* FaviconNullStore.swift in Sources */, 3706FC73293F65D500E42796 /* AddressBarButtonsViewController.swift in Sources */, - 7BFE955A2A9DF4550081ABE9 /* NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift in Sources */, 9FDA6C222B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */, C1372EF52BBC5BAD003F8793 /* SecureTextField.swift in Sources */, 316913242BD2B6250051B46D /* DataBrokerProtectionPixelsHandler.swift in Sources */, @@ -10366,7 +10329,6 @@ 3706FE5E293F661700E42796 /* DataImportMocks.swift in Sources */, 3706FE5F293F661700E42796 /* CrashReportTests.swift in Sources */, B60C6F7F29B1B41D007BFAA8 /* TestRunHelperInitializer.m in Sources */, - 4B9DB0592A983B55000927DB /* MockNetworkProtectionCodeRedeemer.swift in Sources */, 9F0FFFBF2BCCAF1F007C87DD /* BookmarkAllTabsDialogViewModelMock.swift in Sources */, 3706FE61293F661700E42796 /* PinnedTabsViewModelTests.swift in Sources */, 3706FE62293F661700E42796 /* PasswordManagementListSectionTests.swift in Sources */, @@ -10834,7 +10796,6 @@ 4B9DB03B2A983B24000927DB /* InvitedToWaitlistView.swift in Sources */, 8589063C267BCDC000D23B0D /* SaveCredentialsViewController.swift in Sources */, 4BBE0AA727B9B027003B37A8 /* PopUpButton.swift in Sources */, - 4B4D60CF2A0C849600BCD287 /* NetworkProtectionInviteDialog.swift in Sources */, AABEE6A524AA0A7F0043105B /* SuggestionViewController.swift in Sources */, 1D6216B229069BBF00386B2C /* BWKeyStorage.swift in Sources */, AA7E919F287872EA00AB6B62 /* VisitViewModel.swift in Sources */, @@ -11004,7 +10965,6 @@ 3184AC6F288F2A1100C35E4B /* CookieNotificationAnimationModel.swift in Sources */, 4B9DB0382A983B24000927DB /* JoinedWaitlistView.swift in Sources */, B63ED0E526BB8FB900A9DAD1 /* SharingMenu.swift in Sources */, - 4B9DB0322A983B24000927DB /* EnableWaitlistFeatureView.swift in Sources */, AA4FF40C2624751A004E2377 /* GrammarFeaturesManager.swift in Sources */, 4B9DB0442A983B24000927DB /* WaitlistModalViewController.swift in Sources */, B6DA06E8291401D700225DE2 /* WKMenuItemIdentifier.swift in Sources */, @@ -11186,7 +11146,6 @@ AAE246F32709EF3B00BEEAEE /* FirePopoverCollectionViewItem.swift in Sources */, 4B41EDA32B1543B9001EEDF4 /* VPNPreferencesModel.swift in Sources */, AA61C0D22727F59B00E6B681 /* ArrayExtension.swift in Sources */, - 4B4D60CC2A0C849600BCD287 /* NetworkProtectionInviteCodeViewModel.swift in Sources */, 373A1AB02842C4EA00586521 /* BookmarkHTMLImporter.swift in Sources */, B6B5F57F2B024105008DB58A /* DataImportSummaryView.swift in Sources */, 31C3CE0228EDC1E70002C24A /* CustomRoundedCornersShape.swift in Sources */, @@ -11315,7 +11274,6 @@ 4BBDEE9128FC14760092FAA6 /* BWInstallationService.swift in Sources */, 7B4D8A212BDA857300852966 /* VPNOperationErrorRecorder.swift in Sources */, 859F30642A72A7BB00C20372 /* BookmarksBarPromptPopover.swift in Sources */, - 4B4D60CA2A0C849600BCD287 /* NetworkProtectionInvitePresenter.swift in Sources */, B693955426F04BEC0015B914 /* ColorView.swift in Sources */, AA5C1DD3285A217F0089850C /* RecentlyClosedCacheItem.swift in Sources */, B6BBF17427475B15004F850E /* PopupBlockedPopover.swift in Sources */, @@ -11394,7 +11352,6 @@ B687B7CA2947A029001DEA6F /* ContentBlockingTabExtension.swift in Sources */, 85B7184C27677C6500B4277F /* OnboardingViewController.swift in Sources */, 4B379C1E27BDB7FF008A968E /* DeviceAuthenticator.swift in Sources */, - 7BFE95522A9DF1CE0081ABE9 /* NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift in Sources */, 1456D6E124EFCBC300775049 /* TabBarCollectionView.swift in Sources */, 4B4D60BF2A0C848A00BCD287 /* NetworkProtection+ConvenienceInitializers.swift in Sources */, 4B6B64842BA930420009FF9F /* WaitlistThankYouPromptPresenter.swift in Sources */, @@ -11619,7 +11576,6 @@ AA0F3DB7261A566C0077F2D9 /* SuggestionLoadingMock.swift in Sources */, B6E6BA162BA2CF5F008AA7E1 /* SandboxTestToolNotifications.swift in Sources */, B60C6F8129B1B4AD007BFAA8 /* TestRunHelper.swift in Sources */, - 4B9DB0582A983B55000927DB /* MockNetworkProtectionCodeRedeemer.swift in Sources */, 9F872DA02B90644800138637 /* ContextualMenuTests.swift in Sources */, 4B9292BE2667103100AD2C21 /* PasteboardFolderTests.swift in Sources */, 4B9292C52667104B00AD2C21 /* CoreDataTestUtilities.swift in Sources */, diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index fef5473cfe..2f93c17ac4 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -326,10 +326,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate { syncService?.initializeIfNeeded() syncService?.scheduler.notifyAppLifecycleEvent() - NetworkProtectionWaitlist().fetchNetworkProtectionInviteCodeIfAvailable { _ in - // Do nothing when code fetching fails, as the app will try again later - } - NetworkProtectionAppEvents().applicationDidBecomeActive() #if DBP @@ -600,12 +596,6 @@ extension AppDelegate: UNUserNotificationCenterDelegate { withCompletionHandler completionHandler: @escaping () -> Void) { if response.actionIdentifier == UNNotificationDefaultActionIdentifier { - if response.notification.request.identifier == NetworkProtectionWaitlist.notificationIdentifier { - if NetworkProtectionWaitlist().readyToAcceptTermsAndConditions { - NetworkProtectionWaitlistViewControllerPresenter.show() - } - } - #if DBP if response.notification.request.identifier == DataBrokerProtectionWaitlist.notificationIdentifier { DataBrokerProtectionAppEvents().handleWaitlistInvitedNotification(source: .localPush) diff --git a/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Card-16.imageset/Card-16.pdf b/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Card-16.imageset/Card-16.pdf deleted file mode 100644 index ead00161852b20d05c5bb2ff0a1871277fdf914d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3961 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 diff --git a/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Shield-16.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Shield-16.imageset/Contents.json deleted file mode 100644 index 510acba745..0000000000 --- a/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Shield-16.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "Shield-16.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" - } -} diff --git a/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Shield-16.imageset/Shield-16.pdf b/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Shield-16.imageset/Shield-16.pdf deleted file mode 100644 index 677edb434b04e64e0c5890edd3905cb8c1120d2d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2579 zcmZveUvFDC48`B?r_f7*c0grO)L)<|u%_!UY{R;AZ^a%w=eEs|*jwxlT|fPP)U}he znI97QSft3qLrRZczkKyne4Qq7!R`L|b8_z4Gxz-YG;c5L>lE+ftDolW`@;vPfakVr zf4G^~*VF3d{LkfPKL7rOJ9~5g+h#xgJ&6zFkq^3@TEJ+f2=8%_ZQ^z|@ks z?nB9?_2I4bk`qu$8fzY>HVz=vNrD%xE#NTuT5C+Kg0ESMV4S@6mI^EJwyld{F{CKT ztRvKrN;Ncu-ZIt^>lgx2tM8?XrKI@oV&Sl_*QVX9@g907T2@lB)?_Q3z{%LDqJr=2 zW5W@V;|Gh_%sTrjghbZrwUtn?&d{UX;*B0Jp$zNZTS)`cgu`(*OfB9aPl%i#DMpnZ zy0hP6u4F*21Pf;@QmrN1!RmuF$<;-#MWYgzcu5ixr}-o#UtybcyP!{>qQ+4tic&RE zyntbfvA8R0*AvwVV6pce^1w7VX=x}=SW@Ys1Cj`+`31Ub3W{F5bq<&T_LBZ_97wTC`nuAuLDlvBSV6~^FqXdYy=QQLJG{7mb zl~y|jLrbY&C-_=H5&ns58XLa!9sj7JB{;^z<(-DFoeTZOt*!=Nd8~1W7Fs zB2DS7n9|83#uk)NJ7tB7(NNL)KN3WWMRasgr8V$IF|0R8oJNn0QDc79^n0|eXuZIF zWKeJj9RUrco9w`H%YyE};IA)H6|_rpEUnCZD%62uhEfn5I~gM3hymq=bnVF+oqb9d znT8gdszVC+3=wNFYI_(S8A~Gy8@r>su%Nl5-KV`)y3bU@lAgHZ znhcf#SY0Y}EnBO|C;=X??_nd^2BFq8Bopf5vb55(Ri9cfa*dRfZpaB|v5)LDSGg%E00@pwiV=%+gFLPt*kZjO2`^F~;DlHuK)uQ{(*T?rgU`%v(;hcg6{Py}ki+`3T*6jTm(MN?HAQc|oYoGB|gq zbIx`jv6Tn$??LW)Vn*KcWC`wX54=nKY<*Z??cRTB`srqVH}Qg)mR#+xE18#r13rEj z%wy%Yw-AE-5{iLo_%(#izk(v-`Wj+9o}k0c`toYNnRvFXes~ETkN3Ol`H6dcdGTZ+ lWp%#W9h@aR2wuIp{5%u=c)qP~4kI0gSDhX``tFxk{{i7b5.Key.networkProtectionTermsAndConditionsAccepted.rawValue) - UserDefaults().removeObject(forKey: UserDefaultsWrapper.Key.networkProtectionWaitlistSignUpPromptDismissed.rawValue) - NotificationCenter.default.post(name: .networkProtectionWaitlistAccessChanged, object: nil) - } catch NetworkProtectionClientError.invalidInviteCode { - errorText = UserText.inviteDialogUnrecognizedCodeMessage - showProgressView = false - return - } catch { - errorText = UserText.unknownErrorTryAgainMessage - showProgressView = false - return - } - showProgressView = false - delegate?.networkProtectionInviteCodeViewModelDidReedemSuccessfully(self) - } - - func onCancel() { - delegate?.networkProtectionInviteCodeViewModelDidCancel(self) - } - -} - -protocol NetworkProtectionInviteSuccessViewModelDelegate: AnyObject { - func networkProtectionInviteSuccessViewModelDidConfirm(_ viewModel: NetworkProtectionInviteSuccessViewModel) -} - -final class NetworkProtectionInviteSuccessViewModel: InviteCodeSuccessViewModel { - - weak var delegate: NetworkProtectionInviteSuccessViewModelDelegate? - - var titleText: String { - UserText.networkProtectionInviteSuccessTitle - } - - var messageText: String { - UserText.networkProtectionInviteSuccessMessage - } - - var confirmButtonText: String { - UserText.inviteDialogGetStartedButton - } - - func onConfirm() { - delegate?.networkProtectionInviteSuccessViewModelDidConfirm(self) - } - -} diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInviteDialog.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInviteDialog.swift deleted file mode 100644 index 2d6aa8476e..0000000000 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInviteDialog.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// NetworkProtectionInviteDialog.swift -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import SwiftUI -import NetworkProtection -import SwiftUIExtensions - -struct NetworkProtectionInviteDialog: View { - @ObservedObject var model: NetworkProtectionInviteViewModel - - var body: some View { - switch model.currentDialog { - case .codeEntry: - InviteCodeView(viewModel: model.inviteCodeViewModel) - case .success: - InviteCodeSuccessView(viewModel: model.successCodeViewModel) - case .none: - EmptyView() - } - } -} diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInvitePresenter.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInvitePresenter.swift deleted file mode 100644 index d845eedc0c..0000000000 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInvitePresenter.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// NetworkProtectionInvitePresenter.swift -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import SwiftUI -import NetworkProtection - -protocol NetworkProtectionInvitePresenting { - func present() -} - -final class NetworkProtectionInvitePresenter: NetworkProtectionInvitePresenting, NetworkProtectionInviteViewModelDelegate { - - private var presentedViewController: NSViewController? - - // MARK: NetworkProtectionInvitePresenting - - @MainActor func present() { - let viewModel = NetworkProtectionInviteViewModel(delegate: self, redemptionCoordinator: NetworkProtectionCodeRedemptionCoordinator()) - - let view = NetworkProtectionInviteDialog(model: viewModel) - let hostingVC = NSHostingController(rootView: view) - presentedViewController = hostingVC - let newWindowController = hostingVC.wrappedInWindowController() - - guard let newWindow = newWindowController.window, - let parentWindowController = WindowControllersManager.shared.lastKeyMainWindowController - else { - assertionFailure("Failed to present \(hostingVC)") - return - } - parentWindowController.window?.beginSheet(newWindow) - } - - // MARK: NetworkProtectionInviteViewModelDelegate - - func didCancelInviteFlow() { - presentedViewController?.dismiss() - presentedViewController = nil - } - - func didCompleteInviteFlow() { - presentedViewController?.dismiss() - presentedViewController = nil - Task { - await WindowControllersManager.shared.showNetworkProtectionStatus() - } - } -} diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift index 2833f1a23d..472b1f5275 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift @@ -53,13 +53,6 @@ final class NetworkProtectionDebugMenu: NSMenu { private let excludeLocalNetworksMenuItem = NSMenuItem(title: "excludeLocalNetworks", action: #selector(NetworkProtectionDebugMenu.toggleShouldExcludeLocalRoutes)) - private let enterWaitlistInviteCodeItem = NSMenuItem(title: "Enter Waitlist Invite Code", action: #selector(NetworkProtectionDebugMenu.showNetworkProtectionInviteCodePrompt)) - - private let waitlistTokenItem = NSMenuItem(title: "Waitlist Token:") - private let waitlistTimestampItem = NSMenuItem(title: "Waitlist Timestamp:") - private let waitlistInviteCodeItem = NSMenuItem(title: "Waitlist Invite Code:") - private let waitlistTermsAndConditionsAcceptedItem = NSMenuItem(title: "T&C Accepted:") - // swiftlint:disable:next function_body_length init() { preferredServerMenu = NSMenu { [preferredServerAutomaticItem] in @@ -144,28 +137,6 @@ final class NetworkProtectionDebugMenu: NSMenu { .targetting(self) } - NSMenuItem(title: "NetP Waitlist") { - NSMenuItem(title: "Reset Waitlist State", action: #selector(NetworkProtectionDebugMenu.resetNetworkProtectionWaitlistState)) - .targetting(self) - NSMenuItem(title: "Reset T&C Acceptance", action: #selector(NetworkProtectionDebugMenu.resetNetworkProtectionTermsAndConditionsAcceptance)) - .targetting(self) - - enterWaitlistInviteCodeItem - .targetting(self) - - NSMenuItem(title: "Send Waitlist Notification", action: #selector(NetworkProtectionDebugMenu.sendNetworkProtectionWaitlistAvailableNotification)) - .targetting(self) - NSMenuItem.separator() - - waitlistTokenItem - waitlistTimestampItem - waitlistInviteCodeItem - waitlistTermsAndConditionsAcceptedItem - } - - NSMenuItem(title: "NetP Waitlist Feature Flag Overrides") - .submenu(NetworkProtectionWaitlistFeatureFlagOverridesMenu()) - NSMenuItem.separator() NSMenuItem(title: "Kill Switch (alternative approach)") { @@ -423,10 +394,6 @@ final class NetworkProtectionDebugMenu: NSMenu { excludedRoutesMenu.addItem(menuItem) } - // Only allow testers to enter a custom code if they're on the waitlist, to simulate the correct path through the flow - let waitlist = NetworkProtectionWaitlist() - enterWaitlistInviteCodeItem.isEnabled = waitlist.waitlistStorage.isOnWaitlist || waitlist.waitlistStorage.isInvited - } // MARK: - Menu State Update @@ -437,7 +404,6 @@ final class NetworkProtectionDebugMenu: NSMenu { updatePreferredServerMenu() updateRekeyValidityMenu() updateNetworkProtectionMenuItemsState() - updateNetworkProtectionItems() } private func updateEnvironmentMenu() { @@ -504,27 +470,8 @@ final class NetworkProtectionDebugMenu: NSMenu { disableRekeyingMenuItem.state = settings.disableRekeying ? .on : .off } - private func updateNetworkProtectionItems() { - let waitlistStorage = WaitlistKeychainStore(waitlistIdentifier: NetworkProtectionWaitlist.identifier, keychainAppGroup: NetworkProtectionWaitlist.keychainAppGroup) - waitlistTokenItem.title = "Waitlist Token: \(waitlistStorage.getWaitlistToken() ?? "N/A")" - waitlistInviteCodeItem.title = "Waitlist Invite Code: \(waitlistStorage.getWaitlistInviteCode() ?? "N/A")" - - if let timestamp = waitlistStorage.getWaitlistTimestamp() { - waitlistTimestampItem.title = "Waitlist Timestamp: \(String(describing: timestamp))" - } else { - waitlistTimestampItem.title = "Waitlist Timestamp: N/A" - } - - let accepted = UserDefaults().bool(forKey: UserDefaultsWrapper.Key.networkProtectionTermsAndConditionsAccepted.rawValue) - waitlistTermsAndConditionsAcceptedItem.title = "T&C Accepted: \(accepted ? "Yes" : "No")" - } - // MARK: Waitlist - @objc func sendNetworkProtectionWaitlistAvailableNotification(_ sender: Any?) { - NetworkProtectionWaitlist().sendInviteCodeAvailableNotification(completion: nil) - } - @objc func resetNetworkProtectionActivationDate(_ sender: Any?) { overrideNetworkProtectionActivationDate(to: nil) } @@ -556,52 +503,6 @@ final class NetworkProtectionDebugMenu: NSMenu { } } - @objc func resetNetworkProtectionWaitlistState(_ sender: Any?) { - NetworkProtectionWaitlist().waitlistStorage.deleteWaitlistState() - UserDefaults().removeObject(forKey: UserDefaultsWrapper.Key.networkProtectionTermsAndConditionsAccepted.rawValue) - UserDefaults().removeObject(forKey: UserDefaultsWrapper.Key.networkProtectionWaitlistSignUpPromptDismissed.rawValue) - NotificationCenter.default.post(name: .networkProtectionWaitlistAccessChanged, object: nil) - } - - @objc func resetNetworkProtectionTermsAndConditionsAcceptance(_ sender: Any?) { - UserDefaults().removeObject(forKey: UserDefaultsWrapper.Key.networkProtectionTermsAndConditionsAccepted.rawValue) - NotificationCenter.default.post(name: .networkProtectionWaitlistAccessChanged, object: nil) - } - - @objc func showNetworkProtectionInviteCodePrompt(_ sender: Any?) { - let code = getInviteCode() - - Task { - do { - let redeemer = NetworkProtectionCodeRedemptionCoordinator() - try await redeemer.redeem(code) - NetworkProtectionWaitlist().waitlistStorage.store(inviteCode: code) - NotificationCenter.default.post(name: .networkProtectionWaitlistAccessChanged, object: nil) - } catch { - // Do nothing here, this is just a debug menu - } - } - } - - private func getInviteCode() -> String { - let alert = NSAlert() - alert.addButton(withTitle: "Use Invite Code") - alert.addButton(withTitle: "Cancel") - alert.messageText = "Enter Invite Code" - alert.informativeText = "Please grab a VPN invite code from Asana and enter it here." - - let textField = NSTextField(frame: NSRect(x: 0, y: 0, width: 200, height: 24)) - alert.accessoryView = textField - - let response = alert.runModal() - - if response == .alertFirstButtonReturn { - return textField.stringValue - } else { - return "" - } - } - // MARK: Environment @objc func setSelectedEnvironment(_ menuItem: NSMenuItem) { diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift index 8c141f7061..721f194c0d 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift @@ -55,7 +55,7 @@ final class NetworkProtectionDebugUtilities { // MARK: - Debug commands for the extension func resetAllState(keepAuthToken: Bool) async { - let uninstalledSuccessfully = await networkProtectionFeatureDisabler.disable(keepAuthToken: keepAuthToken, uninstallSystemExtension: true) + let uninstalledSuccessfully = await networkProtectionFeatureDisabler.disable(uninstallSystemExtension: true) guard uninstalledSuccessfully else { return @@ -63,8 +63,6 @@ final class NetworkProtectionDebugUtilities { settings.resetToDefaults() - NetworkProtectionWaitlist().waitlistStorage.deleteWaitlistState() - DefaultWaitlistActivationDateStore(source: .netP).removeDates() DefaultHomePageRemoteMessagingStorage.networkProtection().removeStoredAndDismissedMessages() UserDefaults().removeObject(forKey: UserDefaultsWrapper.Key.networkProtectionTermsAndConditionsAccepted.rawValue) diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift index de9f3ee692..e4742d3521 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift @@ -169,14 +169,6 @@ final class NetworkProtectionNavBarButtonModel: NSObject, ObservableObject { @MainActor func updateVisibility() { - // The button is visible in the case where NetP has not been activated, but the user has been invited and they haven't accepted T&Cs. - if vpnVisibility.isNetworkProtectionBetaVisible() { - if NetworkProtectionWaitlist().readyToAcceptTermsAndConditions { - showButton = true - return - } - } - guard !isPinned, !popoverManager.isShown, !isHavingConnectivityIssues else { diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift index 1cd0015748..14e266a11c 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift @@ -116,7 +116,7 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { userDefaults: .netP, locationFormatter: DefaultVPNLocationFormatter(), uninstallHandler: { [weak self] in - _ = await self?.networkProtectionFeatureDisabler.disable(keepAuthToken: false, uninstallSystemExtension: true) + _ = await self?.networkProtectionFeatureDisabler.disable(uninstallSystemExtension: true) }) popover.delegate = delegate @@ -138,13 +138,7 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { networkProtectionPopover.close() self.networkProtectionPopover = nil } else { - let featureVisibility = DefaultNetworkProtectionVisibility() - - if featureVisibility.isNetworkProtectionBetaVisible() { - show(positionedBelow: view, withDelegate: delegate) - } else { - featureVisibility.disableForWaitlistUsers() - } + show(positionedBelow: view, withDelegate: delegate) } } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift deleted file mode 100644 index aa9fce2460..0000000000 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift +++ /dev/null @@ -1,166 +0,0 @@ -// -// NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import AppKit -import Foundation -import NetworkProtection -import NetworkProtectionUI -import SwiftUI - -/// Implements the logic for the VPN's simulate failures menu. -/// -@MainActor -final class NetworkProtectionWaitlistFeatureFlagOverridesMenu: NSMenu { - - // MARK: - Waitlist Active Properties - - private let waitlistActiveUseRemoteValueMenuItem: NSMenuItem - private let waitlistActiveOverrideONMenuItem: NSMenuItem - private let waitlistActiveOverrideOFFMenuItem: NSMenuItem - - @UserDefaultsWrapper(key: .networkProtectionWaitlistActiveOverrideRawValue, - defaultValue: WaitlistOverride.default.rawValue, - defaults: .netP) - private var waitlistActiveOverrideValue: Int - - // MARK: - Waitlist Enabled Properties - - private let waitlistEnabledUseRemoteValueMenuItem: NSMenuItem - private let waitlistEnabledOverrideONMenuItem: NSMenuItem - private let waitlistEnabledOverrideOFFMenuItem: NSMenuItem - - @UserDefaultsWrapper(key: .networkProtectionWaitlistEnabledOverrideRawValue, - defaultValue: WaitlistOverride.default.rawValue, - defaults: .netP) - private var waitlistEnabledOverrideValue: Int - - init() { - waitlistActiveUseRemoteValueMenuItem = NSMenuItem(title: "Remote Value", action: #selector(Self.waitlistEnabledUseRemoteValue)) - waitlistActiveOverrideONMenuItem = NSMenuItem(title: "ON", action: #selector(Self.waitlistEnabledOverrideON)) - waitlistActiveOverrideOFFMenuItem = NSMenuItem(title: "OFF", action: #selector(Self.waitlistEnabledOverrideOFF)) - - waitlistEnabledUseRemoteValueMenuItem = NSMenuItem(title: "Remote Value", action: #selector(Self.waitlistActiveUseRemoteValue)) - waitlistEnabledOverrideONMenuItem = NSMenuItem(title: "ON", action: #selector(Self.waitlistActiveOverrideON)) - waitlistEnabledOverrideOFFMenuItem = NSMenuItem(title: "OFF", action: #selector(Self.waitlistActiveOverrideOFF)) - - super.init(title: "") - buildItems { - NSMenuItem(title: "Reset Waitlist Overrides", action: #selector(Self.waitlistResetFeatureOverrides)).targetting(self) - NSMenuItem.separator() - - NSMenuItem(title: "Waitlist Enabled") { - waitlistActiveUseRemoteValueMenuItem.targetting(self) - waitlistActiveOverrideONMenuItem.targetting(self) - waitlistActiveOverrideOFFMenuItem.targetting(self) - } - - NSMenuItem(title: "Waitlist Active") { - waitlistEnabledUseRemoteValueMenuItem.targetting(self) - waitlistEnabledOverrideONMenuItem.targetting(self) - waitlistEnabledOverrideOFFMenuItem.targetting(self) - } - } - } - - required init(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Misc IBActions - - @objc func waitlistResetFeatureOverrides(sender: NSMenuItem) { - waitlistActiveOverrideValue = WaitlistOverride.default.rawValue - waitlistEnabledOverrideValue = WaitlistOverride.default.rawValue - } - - // MARK: - Waitlist Active IBActions - - @objc func waitlistActiveUseRemoteValue(sender: NSMenuItem) { - waitlistActiveOverrideValue = WaitlistOverride.useRemoteValue.rawValue - } - - @objc func waitlistActiveOverrideON(sender: NSMenuItem) { - waitlistActiveOverrideValue = WaitlistOverride.on.rawValue - } - - @objc func waitlistActiveOverrideOFF(sender: NSMenuItem) { - Task { @MainActor in - guard case .alertFirstButtonReturn = await waitlistOFFAlert().runModal() else { - return - } - - waitlistActiveOverrideValue = WaitlistOverride.off.rawValue - } - } - - // MARK: - Waitlist Enabled IBActions - - @objc func waitlistEnabledUseRemoteValue(sender: NSMenuItem) { - waitlistEnabledOverrideValue = WaitlistOverride.useRemoteValue.rawValue - } - - @objc func waitlistEnabledOverrideON(sender: NSMenuItem) { - waitlistEnabledOverrideValue = WaitlistOverride.on.rawValue - } - - @objc func waitlistEnabledOverrideOFF(sender: NSMenuItem) { - Task { @MainActor in - guard case .alertFirstButtonReturn = await waitlistOFFAlert().runModal() else { - return - } - - waitlistEnabledOverrideValue = WaitlistOverride.off.rawValue - } - } - - // MARK: - Updating the menu state - - override func update() { - waitlistActiveUseRemoteValueMenuItem.state = waitlistActiveOverrideValue == WaitlistOverride.useRemoteValue.rawValue ? .on : .off - waitlistActiveOverrideONMenuItem.state = waitlistActiveOverrideValue == WaitlistOverride.on.rawValue ? .on : .off - waitlistActiveOverrideOFFMenuItem.state = waitlistActiveOverrideValue == WaitlistOverride.off.rawValue ? .on : .off - - waitlistEnabledUseRemoteValueMenuItem.state = waitlistEnabledOverrideValue == WaitlistOverride.useRemoteValue.rawValue ? .on : .off - waitlistEnabledOverrideONMenuItem.state = waitlistEnabledOverrideValue == WaitlistOverride.on.rawValue ? .on : .off - waitlistEnabledOverrideOFFMenuItem.state = waitlistEnabledOverrideValue == WaitlistOverride.off.rawValue ? .on : .off - } - - // MARK: - UI Additions - - private func waitlistOFFAlert() -> NSAlert { - let alert = NSAlert() - alert.messageText = "Override to OFF value?" - alert.informativeText = """ - This will potentially disable DuckDuckGo VPN and erase your invitation. - - You can re-enable DuckDuckGo VPN after reverting this change. - - Please click 'Cancel' if you're unsure. - """ - alert.alertStyle = .warning - alert.addButton(withTitle: "Override") - alert.addButton(withTitle: UserText.cancel) - return alert - } -} - -#if DEBUG -#Preview { - return MenuPreview(menu: NetworkProtectionWaitlistFeatureFlagOverridesMenu()) -} -#endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionRemoteMessaging/NetworkProtectionRemoteMessaging.swift b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionRemoteMessaging/NetworkProtectionRemoteMessaging.swift index 739c7501ed..6daeb90569 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionRemoteMessaging/NetworkProtectionRemoteMessaging.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionRemoteMessaging/NetworkProtectionRemoteMessaging.swift @@ -132,7 +132,7 @@ final class DefaultNetworkProtectionRemoteMessaging: NetworkProtectionRemoteMess } // Next, check if the message requires access to NetP but it's not visible: - if message.requiresNetworkProtectionAccess, !networkProtectionVisibility.isNetworkProtectionBetaVisible() { + if message.requiresNetworkProtectionAccess, !networkProtectionVisibility.isVPNVisible() { return false } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift index ecdc324254..31795df3a3 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift @@ -26,19 +26,16 @@ import NetworkProtectionUI final class NetworkProtectionSubscriptionEventHandler { private let accountManager: AccountManager - private let networkProtectionRedemptionCoordinator: NetworkProtectionCodeRedeeming private let networkProtectionTokenStorage: NetworkProtectionTokenStore private let networkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling private let userDefaults: UserDefaults private var cancellables = Set() init(accountManager: AccountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)), - networkProtectionRedemptionCoordinator: NetworkProtectionCodeRedeeming = NetworkProtectionCodeRedemptionCoordinator(), networkProtectionTokenStorage: NetworkProtectionTokenStore = NetworkProtectionKeychainTokenStore(), networkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling = NetworkProtectionFeatureDisabler(), userDefaults: UserDefaults = .netP) { self.accountManager = accountManager - self.networkProtectionRedemptionCoordinator = networkProtectionRedemptionCoordinator self.networkProtectionTokenStorage = networkProtectionTokenStorage self.networkProtectionFeatureDisabler = networkProtectionFeatureDisabler self.userDefaults = userDefaults @@ -109,7 +106,7 @@ final class NetworkProtectionSubscriptionEventHandler { print("[NetP Subscription] Deleted NetP auth token after signing out from Privacy Pro") Task { - await networkProtectionFeatureDisabler.disable(keepAuthToken: false, uninstallSystemExtension: false) + await networkProtectionFeatureDisabler.disable(uninstallSystemExtension: false) } } diff --git a/DuckDuckGo/Preferences/Model/AboutModel.swift b/DuckDuckGo/Preferences/Model/AboutModel.swift index 1504240116..ed2e0a914d 100644 --- a/DuckDuckGo/Preferences/Model/AboutModel.swift +++ b/DuckDuckGo/Preferences/Model/AboutModel.swift @@ -22,12 +22,6 @@ import Common final class AboutModel: ObservableObject, PreferencesTabOpening { let appVersion = AppVersion() - private let netPInvitePresenter: NetworkProtectionInvitePresenting - - init(netPInvitePresenter: NetworkProtectionInvitePresenting) { - self.netPInvitePresenter = netPInvitePresenter - } - let displayableAboutURL: String = URL.aboutDuckDuckGo .toString(decodePunycode: false, dropScheme: true, dropTrailingSlash: false) @@ -39,8 +33,4 @@ final class AboutModel: ObservableObject, PreferencesTabOpening { func copy(_ value: String) { NSPasteboard.general.copy(value) } - - func displayNetPInvite() { - netPInvitePresenter.present() - } } diff --git a/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift b/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift index 320d0d2cad..c79ef7a8e2 100644 --- a/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift +++ b/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift @@ -109,7 +109,7 @@ final class VPNPreferencesModel: ObservableObject { switch response { case .OK: - await NetworkProtectionFeatureDisabler().disable(keepAuthToken: true, uninstallSystemExtension: true) + await NetworkProtectionFeatureDisabler().disable(uninstallSystemExtension: true) default: // intentional no-op break diff --git a/DuckDuckGo/Preferences/View/PreferencesAboutView.swift b/DuckDuckGo/Preferences/View/PreferencesAboutView.swift index 63c0601ea8..e4e8900df9 100644 --- a/DuckDuckGo/Preferences/View/PreferencesAboutView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesAboutView.swift @@ -55,9 +55,6 @@ extension Preferences { .multilineTextAlignment(.leading) Text(UserText.versionLabel(version: model.appVersion.versionNumber, build: model.appVersion.buildNumber)) - .onTapGesture(count: 12) { - model.displayNetPInvite() - } .contextMenu(ContextMenu(menuItems: { Button(UserText.copy, action: { model.copy(UserText.versionLabel(version: model.appVersion.versionNumber, build: model.appVersion.buildNumber)) diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index 5d2f842652..8cecf62db6 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -109,8 +109,7 @@ enum Preferences { // Opens a new tab Spacer() case .about: - let netPInvitePresenter = NetworkProtectionInvitePresenter() - AboutView(model: AboutModel(netPInvitePresenter: netPInvitePresenter)) + AboutView(model: AboutModel()) } } .frame(maxWidth: Const.paneContentWidth, maxHeight: .infinity, alignment: .topLeading) diff --git a/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift b/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift index 1e9dd5d333..700149aad7 100644 --- a/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift +++ b/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift @@ -75,7 +75,6 @@ struct VPNMetadata: Encodable { } struct PrivacyProInfo: Encodable { - let betaParticipant: Bool let hasPrivacyProAccount: Bool let hasVPNEntitlement: Bool } @@ -304,15 +303,10 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { func collectPrivacyProInfo() async -> VPNMetadata.PrivacyProInfo { let accountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) - let waitlistStore = WaitlistKeychainStore( - waitlistIdentifier: NetworkProtectionWaitlist.identifier, - keychainAppGroup: NetworkProtectionWaitlist.keychainAppGroup - ) let hasVPNEntitlement = (try? await accountManager.hasEntitlement(for: .networkProtection).get()) ?? false return .init( - betaParticipant: waitlistStore.isInvited, hasPrivacyProAccount: accountManager.isUserAuthenticated, hasVPNEntitlement: hasVPNEntitlement ) diff --git a/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift b/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift index e6d085bc91..cddc3b1a9c 100644 --- a/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift +++ b/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift @@ -29,7 +29,7 @@ protocol NetworkProtectionFeatureDisabling { /// - Returns: `true` if the uninstallation was completed. `false` if it was cancelled by the user or an error. /// @discardableResult - func disable(keepAuthToken: Bool, uninstallSystemExtension: Bool) async -> Bool + func disable(uninstallSystemExtension: Bool) async -> Bool func stop() } @@ -68,12 +68,11 @@ final class NetworkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling /// This method disables the VPN and clear all of its state. /// /// - Parameters: - /// - keepAuthToken: If `true`, the auth token will not be removed. /// - includeSystemExtension: Whether this method should uninstall the system extension. /// @MainActor @discardableResult - func disable(keepAuthToken: Bool, uninstallSystemExtension: Bool) async -> Bool { + func disable(uninstallSystemExtension: Bool) async -> Bool { // We can do this optimistically as it has little if any impact. unpinNetworkProtection() @@ -118,10 +117,6 @@ final class NetworkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling try? await Task.sleep(interval: 0.5) disableLoginItems() - if !keepAuthToken { - try? removeAppAuthToken() - } - notifyVPNUninstalled() isDisabling = false return true @@ -151,10 +146,6 @@ final class NetworkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling pinningManager.unpin(.networkProtection) } - private func removeAppAuthToken() throws { - try NetworkProtectionKeychainTokenStore().deleteToken() - } - private func removeVPNConfiguration() async throws { // Remove the agent VPN configuration try await ipcClient.debugCommand(.removeVPNConfiguration) diff --git a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift b/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift index 11bf4a988e..6956ba22ae 100644 --- a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift +++ b/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift @@ -27,15 +27,12 @@ import PixelKit import Subscription protocol NetworkProtectionFeatureVisibility { - var isEligibleForThankYouMessage: Bool { get } var isInstalled: Bool { get } func canStartVPN() async throws -> Bool func isVPNVisible() -> Bool - func isNetworkProtectionBetaVisible() -> Bool func shouldUninstallAutomatically() -> Bool func disableForAllUsers() async - func disableForWaitlistUsers() @discardableResult func disableIfUserHasNoAccess() async -> Bool @@ -47,16 +44,11 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { private let featureDisabler: NetworkProtectionFeatureDisabling private let featureOverrides: WaitlistBetaOverriding private let networkProtectionFeatureActivation: NetworkProtectionFeatureActivation - private let networkProtectionWaitlist = NetworkProtectionWaitlist() private let privacyConfigurationManager: PrivacyConfigurationManaging private let defaults: UserDefaults let subscriptionAppGroup = Bundle.main.appGroup(bundle: .subs) let accountManager: AccountManager - var waitlistIsOngoing: Bool { - isWaitlistEnabled && isWaitlistBetaActive - } - init(privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager, networkProtectionFeatureActivation: NetworkProtectionFeatureActivation = NetworkProtectionKeychainTokenStore(), featureOverrides: WaitlistBetaOverriding = DefaultWaitlistBetaOverrides(), @@ -72,17 +64,6 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { self.accountManager = AccountManager(subscriptionAppGroup: subscriptionAppGroup) } - /// Calculates whether the VPN is visible. - /// The following criteria are used: - /// - /// 1. If the user has a valid auth token, the feature is visible - /// 2. If no auth token is found, the feature is visible if the waitlist feature flag is enabled - /// - /// Once the waitlist beta has ended, we can trigger a remote change that removes the user's auth token and turn off the waitlist flag, hiding the VPN from the user. - func isNetworkProtectionBetaVisible() -> Bool { - return isEasterEggUser || waitlistIsOngoing - } - var isInstalled: Bool { LoginItem.vpnMenu.status.isInstalled } @@ -94,7 +75,7 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { /// func canStartVPN() async throws -> Bool { guard subscriptionFeatureAvailability.isFeatureAvailable else { - return isNetworkProtectionBetaVisible() + return false } switch await accountManager.hasEntitlement(for: .networkProtection) { @@ -112,7 +93,7 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { /// func isVPNVisible() -> Bool { guard subscriptionFeatureAvailability.isFeatureAvailable else { - return isNetworkProtectionBetaVisible() + return false } return accountManager.isUserAuthenticated @@ -142,93 +123,19 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { defaults.networkProtectionOnboardingStatusPublisher } - /// Easter egg users can be identified by them being internal users and having an auth token (NetP being activated). - /// - private var isEasterEggUser: Bool { - !isWaitlistUser && networkProtectionFeatureActivation.isFeatureActivated - } - - /// Whether it's a user with feature access - private var isEnabledWaitlistUser: Bool { - isWaitlistUser && waitlistIsOngoing - } - - /// Waitlist users are users that have the waitlist enabled and active - /// - private var isWaitlistUser: Bool { - networkProtectionWaitlist.waitlistStorage.isWaitlistUser - } - - /// Waitlist users are users that have the waitlist enabled and active and are invited - /// - private var isInvitedWaitlistUser: Bool { - networkProtectionWaitlist.waitlistStorage.isWaitlistUser && networkProtectionWaitlist.waitlistStorage.isInvited - } - - private var isWaitlistBetaActive: Bool { - true - } - - private var isWaitlistEnabled: Bool { - true - } - func disableForAllUsers() async { - await featureDisabler.disable(keepAuthToken: true, uninstallSystemExtension: false) - } - - /// Disables the VPN for legacy users, if necessary. - /// - /// This method does not seek to remove tokens or uninstall anything. - /// - private func disableVPNForLegacyUsersIfSubscriptionAvailable() async -> Bool { - guard isEligibleForThankYouMessage && !defaults.vpnLegacyUserAccessDisabledOnce else { - return false - } - - PixelKit.fire(VPNPrivacyProPixel.vpnBetaStoppedWhenPrivacyProEnabled, frequency: .dailyAndCount) - defaults.vpnLegacyUserAccessDisabledOnce = true - await featureDisabler.disable(keepAuthToken: true, uninstallSystemExtension: false) - return true - } - - func disableForWaitlistUsers() { - guard isWaitlistUser else { - return - } - - Task { - await featureDisabler.disable(keepAuthToken: false, uninstallSystemExtension: false) - } + await featureDisabler.disable(uninstallSystemExtension: false) } /// A method meant to be called safely from different places to disable the VPN if the user isn't meant to have access to it. /// @discardableResult func disableIfUserHasNoAccess() async -> Bool { - if shouldUninstallAutomatically() { - await disableForAllUsers() - return true - } - - return await disableVPNForLegacyUsersIfSubscriptionAvailable() - } - - // MARK: - Subscription Start Support - - /// To query whether we're a legacy (waitlist or easter egg) user. - /// - private func isPreSubscriptionUser() -> Bool { - guard let token = try? NetworkProtectionKeychainTokenStore(isSubscriptionEnabled: false).fetchToken() else { + guard shouldUninstallAutomatically() else { return false } - return !token.hasPrefix(Self.subscriptionAuthTokenPrefix) - } - - /// Checks whether the VPN needs to be disabled. - /// - var isEligibleForThankYouMessage: Bool { - isPreSubscriptionUser() && subscriptionFeatureAvailability.isFeatureAvailable + await disableForAllUsers() + return true } } diff --git a/DuckDuckGo/Waitlist/Views/WaitlistRootView.swift b/DuckDuckGo/Waitlist/Views/WaitlistRootView.swift index 776e44acdf..b9a029d4c6 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistRootView.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistRootView.swift @@ -18,31 +18,6 @@ import SwiftUI -struct NetworkProtectionWaitlistRootView: View { - @EnvironmentObject var model: WaitlistViewModel - - var body: some View { - Group { - switch model.viewState { - case .notOnWaitlist, .joiningWaitlist: - JoinWaitlistView(viewData: NetworkProtectionJoinWaitlistViewData()) - case .joinedWaitlist(let state): - JoinedWaitlistView(viewData: NetworkProtectionJoinedWaitlistViewData(), - notificationsAllowed: state == .notificationAllowed) - case .invited: - InvitedToWaitlistView(viewData: NetworkProtectionInvitedToWaitlistViewData()) - case .termsAndConditions: - WaitlistTermsAndConditionsView(viewData: NetworkProtectionWaitlistTermsAndConditionsViewData()) { - NetworkProtectionTermsAndConditionsContentView() - } - case .readyToEnable: - EnableWaitlistFeatureView(viewData: EnableNetworkProtectionViewData()) - } - } - .environmentObject(model) - } -} - #if DBP import SwiftUI diff --git a/DuckDuckGo/Waitlist/Views/WaitlistSteps/EnableWaitlistFeatureView.swift b/DuckDuckGo/Waitlist/Views/WaitlistSteps/EnableWaitlistFeatureView.swift deleted file mode 100644 index 1f4ee5d72c..0000000000 --- a/DuckDuckGo/Waitlist/Views/WaitlistSteps/EnableWaitlistFeatureView.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// EnableWaitlistFeatureView.swift -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import SwiftUI -import SwiftUIExtensions - -protocol EnableWaitlistFeatureViewData { - var headerImageName: String { get } - var title: String { get } - var subtitle: String { get } - var availabilityDisclaimer: String { get } - var buttonConfirmLabel: String { get } -} - -struct EnableWaitlistFeatureView: View { - var viewData: EnableWaitlistFeatureViewData - @EnvironmentObject var model: WaitlistViewModel - - var body: some View { - WaitlistDialogView { - VStack(spacing: 16.0) { - Image(viewData.headerImageName) - - Text(viewData.title) - .font(.system(size: 17, weight: .bold)) - - Text(viewData.subtitle) - .multilineTextAlignment(.center) - .foregroundColor(Color(.blackWhite80)) - - Text(viewData.availabilityDisclaimer) - .multilineTextAlignment(.center) - .font(.system(size: 12)) - .foregroundColor(Color(.blackWhite60)) - } - } buttons: { - Button(viewData.buttonConfirmLabel) { - Task { - await model.perform(action: .closeAndConfirmFeature) - } - } - .buttonStyle(DefaultActionButtonStyle(enabled: true)) - } - .environmentObject(model) - } -} - -struct EnableNetworkProtectionViewData: EnableWaitlistFeatureViewData { - var headerImageName: String = "Network-Protection-256" - var title: String = UserText.networkProtectionWaitlistEnableTitle - var subtitle: String = UserText.networkProtectionWaitlistEnableSubtitle - var availabilityDisclaimer: String = UserText.networkProtectionWaitlistAvailabilityDisclaimer - var buttonConfirmLabel: String = UserText.networkProtectionWaitlistButtonGotIt -} diff --git a/DuckDuckGo/Waitlist/Views/WaitlistSteps/InvitedToWaitlistView.swift b/DuckDuckGo/Waitlist/Views/WaitlistSteps/InvitedToWaitlistView.swift index 52b5ec44cf..ca30a44deb 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistSteps/InvitedToWaitlistView.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistSteps/InvitedToWaitlistView.swift @@ -117,29 +117,6 @@ struct WaitlistEntryViewItemViewData: Identifiable { let subtitle: String } -struct NetworkProtectionInvitedToWaitlistViewData: InvitedToWaitlistViewData { - let headerImageName = "Gift-96" - let title = UserText.networkProtectionWaitlistInvitedTitle - let subtitle = UserText.networkProtectionWaitlistInvitedSubtitle - let buttonDismissLabel = UserText.networkProtectionWaitlistButtonDismiss - let buttonGetStartedLabel = UserText.networkProtectionWaitlistButtonGetStarted - let availabilityDisclaimer = UserText.networkProtectionWaitlistAvailabilityDisclaimer - let entryViewViewDataList: [WaitlistEntryViewItemViewData] = - [ - .init(imageName: "Shield-16", - title: UserText.networkProtectionWaitlistInvitedSection1Title, - subtitle: UserText.networkProtectionWaitlistInvitedSection1Subtitle), - - .init(imageName: "Rocket-16", - title: UserText.networkProtectionWaitlistInvitedSection2Title, - subtitle: UserText.networkProtectionWaitlistInvitedSection2Subtitle), - - .init(imageName: "Card-16", - title: UserText.networkProtectionWaitlistInvitedSection3Title, - subtitle: UserText.networkProtectionWaitlistInvitedSection3Subtitle) - ] -} - #if DBP struct DataBrokerProtectionInvitedToWaitlistViewData: InvitedToWaitlistViewData { diff --git a/DuckDuckGo/Waitlist/Views/WaitlistSteps/JoinWaitlistView.swift b/DuckDuckGo/Waitlist/Views/WaitlistSteps/JoinWaitlistView.swift index b47869f981..6c0e4deb2a 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistSteps/JoinWaitlistView.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistSteps/JoinWaitlistView.swift @@ -73,16 +73,6 @@ struct JoinWaitlistView: View { } } -struct NetworkProtectionJoinWaitlistViewData: JoinWaitlistViewViewData { - let headerImageName = "JoinWaitlistHeader" - let title = UserText.networkProtectionWaitlistJoinTitle - let subtitle1 = UserText.networkProtectionWaitlistJoinSubtitle1 - let subtitle2 = UserText.networkProtectionWaitlistJoinSubtitle2 - let availabilityDisclaimer = UserText.networkProtectionWaitlistAvailabilityDisclaimer - let buttonCloseLabel = UserText.networkProtectionWaitlistButtonClose - let buttonJoinWaitlistLabel = UserText.networkProtectionWaitlistButtonJoinWaitlist -} - #if DBP struct DataBrokerProtectionJoinWaitlistViewData: JoinWaitlistViewViewData { diff --git a/DuckDuckGo/Waitlist/Views/WaitlistSteps/JoinedWaitlistView.swift b/DuckDuckGo/Waitlist/Views/WaitlistSteps/JoinedWaitlistView.swift index bda0183f51..553d3e6562 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistSteps/JoinedWaitlistView.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistSteps/JoinedWaitlistView.swift @@ -85,17 +85,6 @@ struct JoinedWaitlistView: View { } } -struct NetworkProtectionJoinedWaitlistViewData: JoinedWaitlistViewData { - let headerImageName = "JoinedWaitlistHeader" - var title = UserText.networkProtectionWaitlistJoinedTitle - var joinedWithNoNotificationSubtitle1 = UserText.networkProtectionWaitlistJoinedWithNotificationsSubtitle1 - var joinedWithNoNotificationSubtitle2 = UserText.networkProtectionWaitlistJoinedWithNotificationsSubtitle2 - var enableNotificationSubtitle = UserText.networkProtectionWaitlistEnableNotifications - var buttonConfirmLabel = UserText.networkProtectionWaitlistButtonDone - var buttonCancelLabel = UserText.networkProtectionWaitlistButtonNoThanks - var buttonEnableNotificationLabel = UserText.networkProtectionWaitlistButtonEnableNotifications -} - #if DBP struct DataBrokerProtectionJoinedWaitlistViewData: JoinedWaitlistViewData { diff --git a/DuckDuckGo/Waitlist/Views/WaitlistSteps/WaitlistTermsAndConditionsView.swift b/DuckDuckGo/Waitlist/Views/WaitlistSteps/WaitlistTermsAndConditionsView.swift index 0137b172e8..7495ae9e0b 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistSteps/WaitlistTermsAndConditionsView.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistSteps/WaitlistTermsAndConditionsView.swift @@ -82,76 +82,6 @@ private extension Text { } -struct NetworkProtectionTermsAndConditionsContentView: View { - var body: some View { - VStack(alignment: .leading, spacing: 0) { - Text(verbatim: UserText.networkProtectionPrivacyPolicyTitle) - .font(.system(size: 15, weight: .bold)) - .multilineTextAlignment(.leading) - - Group { - Text(verbatim: UserText.networkProtectionPrivacyPolicySection1Title).titleStyle() - - if #available(macOS 12.0, *) { - Text(verbatim: UserText.networkProtectionPrivacyPolicySection1ListMarkdown).bodyStyle() - } else { - Text(verbatim: UserText.networkProtectionPrivacyPolicySection1ListNonMarkdown).bodyStyle() - } - - Text(verbatim: UserText.networkProtectionPrivacyPolicySection2Title).titleStyle() - Text(verbatim: UserText.networkProtectionPrivacyPolicySection2List).bodyStyle() - Text(verbatim: UserText.networkProtectionPrivacyPolicySection3Title).titleStyle() - Text(verbatim: UserText.networkProtectionPrivacyPolicySection3List).bodyStyle() - Text(verbatim: UserText.networkProtectionPrivacyPolicySection4Title).titleStyle() - Text(verbatim: UserText.networkProtectionPrivacyPolicySection4List).bodyStyle() - Text(verbatim: UserText.networkProtectionPrivacyPolicySection5Title).titleStyle() - Text(verbatim: UserText.networkProtectionPrivacyPolicySection5List).bodyStyle() - } - - Text(verbatim: UserText.networkProtectionTermsOfServiceTitle) - .font(.system(size: 15, weight: .bold)) - .multilineTextAlignment(.leading) - .padding(.top, 28) - .padding(.bottom, 14) - - Group { - Text(verbatim: UserText.networkProtectionTermsOfServiceSection1Title).titleStyle(topPadding: 0) - Text(verbatim: UserText.networkProtectionTermsOfServiceSection1List).bodyStyle() - Text(verbatim: UserText.networkProtectionTermsOfServiceSection2Title).titleStyle() - - if #available(macOS 12.0, *) { - Text(verbatim: UserText.networkProtectionTermsOfServiceSection2ListMarkdown).bodyStyle() - } else { - Text(UserText.networkProtectionTermsOfServiceSection2ListNonMarkdown).bodyStyle() - } - - Text(verbatim: UserText.networkProtectionTermsOfServiceSection3Title).titleStyle() - Text(verbatim: UserText.networkProtectionTermsOfServiceSection3List).bodyStyle() - Text(verbatim: UserText.networkProtectionTermsOfServiceSection4Title).titleStyle() - Text(verbatim: UserText.networkProtectionTermsOfServiceSection4List).bodyStyle() - Text(verbatim: UserText.networkProtectionTermsOfServiceSection5Title).titleStyle() - Text(verbatim: UserText.networkProtectionTermsOfServiceSection5List).bodyStyle() - } - - Group { - Text(verbatim: UserText.networkProtectionTermsOfServiceSection6Title).titleStyle() - Text(verbatim: UserText.networkProtectionTermsOfServiceSection6List).bodyStyle() - Text(verbatim: UserText.networkProtectionTermsOfServiceSection7Title).titleStyle() - Text(verbatim: UserText.networkProtectionTermsOfServiceSection7List).bodyStyle() - Text(verbatim: UserText.networkProtectionTermsOfServiceSection8Title).titleStyle() - Text(verbatim: UserText.networkProtectionTermsOfServiceSection8List).bodyStyle() - } - } - .padding(.all, 20) - } -} - -struct NetworkProtectionWaitlistTermsAndConditionsViewData: WaitlistTermsAndConditionsViewData { - let title = "VPN Beta\nService Terms and Privacy Policy" - let buttonCancelLabel = UserText.networkProtectionWaitlistButtonCancel - let buttonAgreeAndContinueLabel = UserText.networkProtectionWaitlistButtonAgreeAndContinue -} - #if DBP struct DataBrokerProtectionTermsAndConditionsContentView: View { diff --git a/DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift b/DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift index 4c895fcf36..4db6e135b5 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift @@ -25,31 +25,26 @@ final class WaitlistThankYouPromptPresenter { private enum Constants { static let didShowThankYouPromptKey = "duckduckgo.macos.browser.did-show-thank-you-prompt" - static let didDismissVPNCardKey = "duckduckgo.macos.browser.did-dismiss-vpn-card" static let didDismissPIRCardKey = "duckduckgo.macos.browser.did-dismiss-pir-card" } - private let isVPNBetaTester: () -> Bool private let isPIRBetaTester: () -> Bool private let userDefaults: UserDefaults convenience init() { - self.init(isVPNBetaTester: { - return DefaultNetworkProtectionVisibility().isEligibleForThankYouMessage - }, isPIRBetaTester: { + self.init(isPIRBetaTester: { return DefaultDataBrokerProtectionFeatureVisibility().isEligibleForThankYouMessage() }) } - init(isVPNBetaTester: @escaping () -> Bool, isPIRBetaTester: @escaping () -> Bool, userDefaults: UserDefaults = .standard) { - self.isVPNBetaTester = isVPNBetaTester + init(isPIRBetaTester: @escaping () -> Bool, userDefaults: UserDefaults = .standard) { self.isPIRBetaTester = isPIRBetaTester self.userDefaults = userDefaults } // MARK: - Presentation - // Presents a Thank You prompt to testers of the VPN or PIR. + // Presents a Thank You prompt to testers of PIR. // If the user tested both, the PIR prompt will be displayed. @MainActor func presentThankYouPromptIfNecessary(in window: NSWindow) { @@ -67,19 +62,6 @@ final class WaitlistThankYouPromptPresenter { saveDidShowPromptCheck() PixelKit.fire(PrivacyProPixel.privacyProBetaUserThankYouDBP, frequency: .dailyAndCount) presentPIRThankYouPrompt(in: window) - } else if isVPNBetaTester() { - saveDidShowPromptCheck() - PixelKit.fire(PrivacyProPixel.privacyProBetaUserThankYouVPN, frequency: .dailyAndCount) - presentVPNThankYouPrompt(in: window) - } - } - - @MainActor - func presentVPNThankYouPrompt(in window: NSWindow) { - let thankYouModalView = WaitlistBetaThankYouDialogViewController(copy: .vpn) - let thankYouWindowController = thankYouModalView.wrappedInWindowController() - if let thankYouWindow = thankYouWindowController.window { - window.beginSheet(thankYouWindow) } } @@ -94,14 +76,6 @@ final class WaitlistThankYouPromptPresenter { // MARK: - Eligibility - var canShowVPNCard: Bool { - guard !self.userDefaults.bool(forKey: Constants.didDismissVPNCardKey) else { - return false - } - - return isVPNBetaTester() - } - var canShowPIRCard: Bool { guard !self.userDefaults.bool(forKey: Constants.didDismissPIRCardKey) else { return false @@ -116,10 +90,6 @@ final class WaitlistThankYouPromptPresenter { // MARK: - Dismissal - func didDismissVPNThankYouCard() { - self.userDefaults.setValue(true, forKey: Constants.didDismissVPNCardKey) - } - func didDismissPIRThankYouCard() { self.userDefaults.setValue(true, forKey: Constants.didDismissPIRCardKey) } @@ -132,7 +102,6 @@ final class WaitlistThankYouPromptPresenter { func resetPromptCheck() { self.userDefaults.removeObject(forKey: Constants.didShowThankYouPromptKey) - self.userDefaults.removeObject(forKey: Constants.didDismissVPNCardKey) self.userDefaults.removeObject(forKey: Constants.didDismissPIRCardKey) } diff --git a/DuckDuckGo/Waitlist/Views/WaitlistViewControllerPresenter.swift b/DuckDuckGo/Waitlist/Views/WaitlistViewControllerPresenter.swift index 85c050b790..78e0d51794 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistViewControllerPresenter.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistViewControllerPresenter.swift @@ -30,45 +30,6 @@ extension WaitlistViewControllerPresenter { } } -struct NetworkProtectionWaitlistViewControllerPresenter: WaitlistViewControllerPresenter { - - @MainActor - static func show(completion: (() -> Void)? = nil) { - guard let windowController = WindowControllersManager.shared.lastKeyMainWindowController, - windowController.window?.isKeyWindow == true else { - return - } - - // This is a hack to get around an issue with the waitlist notification screen showing the wrong state while it animates in, and then - // jumping to the correct state as soon as the animation is complete. This works around that problem by providing the correct state up front, - // preventing any state changing from occurring. - UNUserNotificationCenter.current().getNotificationSettings { settings in - let status = settings.authorizationStatus - let state = WaitlistViewModel.NotificationPermissionState.from(status) - - DispatchQueue.main.async { - let viewModel = WaitlistViewModel(waitlist: NetworkProtectionWaitlist(), - notificationPermissionState: state, - showNotificationSuccessState: true, - termsAndConditionActionHandler: NetworkProtectionWaitlistTermsAndConditionsActionHandler(), - featureSetupHandler: NetworkProtectionWaitlistFeatureSetupHandler()) - - let viewController = WaitlistModalViewController(viewModel: viewModel, contentView: NetworkProtectionWaitlistRootView()) - windowController.mainViewController.beginSheet(viewController) { _ in - // If the user dismissed the waitlist flow without signing up, hide the button. - let waitlist = NetworkProtectionWaitlist() - if !waitlist.waitlistStorage.isOnWaitlist { - waitlist.waitlistSignUpPromptDismissed = true - NotificationCenter.default.post(name: .networkProtectionWaitlistAccessChanged, object: nil) - } - - completion?() - } - } - } - } -} - #if DBP struct DataBrokerProtectionWaitlistViewControllerPresenter: WaitlistViewControllerPresenter { diff --git a/DuckDuckGo/Waitlist/Waitlist.swift b/DuckDuckGo/Waitlist/Waitlist.swift index 56c9f8cdd7..b8bc8e1ba7 100644 --- a/DuckDuckGo/Waitlist/Waitlist.swift +++ b/DuckDuckGo/Waitlist/Waitlist.swift @@ -154,88 +154,6 @@ extension ProductWaitlistRequest { } } -// MARK: - VPN Waitlist - -struct NetworkProtectionWaitlist: Waitlist { - - static let identifier: String = "networkprotection" - static let apiProductName: String = "networkprotection_macos" - static let keychainAppGroup: String = Bundle.main.appGroup(bundle: .netP) - - static let notificationIdentifier = "com.duckduckgo.macos.browser.network-protection.invite-code-available" - static let inviteAvailableNotificationTitle = UserText.networkProtectionWaitlistNotificationTitle - static let inviteAvailableNotificationBody = UserText.networkProtectionWaitlistNotificationText - - let waitlistStorage: WaitlistStorage - let waitlistRequest: WaitlistRequest - private let networkProtectionCodeRedemption: NetworkProtectionCodeRedeeming - - @UserDefaultsWrapper(key: .networkProtectionWaitlistSignUpPromptDismissed, defaultValue: false) - var waitlistSignUpPromptDismissed: Bool - - var shouldShowWaitlistViewController: Bool { - return isOnWaitlist || readyToAcceptTermsAndConditions - } - - var isOnWaitlist: Bool { - return waitlistStorage.isOnWaitlist - } - - var isInvited: Bool { - return waitlistStorage.isInvited - } - - var readyToAcceptTermsAndConditions: Bool { - let accepted = UserDefaults().bool(forKey: UserDefaultsWrapper.Key.networkProtectionTermsAndConditionsAccepted.rawValue) - return waitlistStorage.isInvited && !accepted - } - - init() { - self.init( - store: WaitlistKeychainStore(waitlistIdentifier: Self.identifier, keychainAppGroup: Self.keychainAppGroup), - request: ProductWaitlistRequest(productName: Self.apiProductName), - networkProtectionCodeRedemption: NetworkProtectionCodeRedemptionCoordinator() - ) - } - - init(store: WaitlistStorage, request: WaitlistRequest, networkProtectionCodeRedemption: NetworkProtectionCodeRedeeming) { - self.waitlistStorage = store - self.waitlistRequest = request - self.networkProtectionCodeRedemption = networkProtectionCodeRedemption - } - - func fetchNetworkProtectionInviteCodeIfAvailable(completion: @escaping (WaitlistInviteCodeFetchError?) -> Void) { - // Never fetch the invite code if the Privacy Pro flag is enabled: - if DefaultSubscriptionFeatureAvailability().isFeatureAvailable { - completion(nil) - return - } - - self.fetchInviteCodeIfAvailable { error in - if let error { - // Do nothing if the app fails to fetch, as the waitlist is being phased out - completion(error) - } else if let inviteCode = waitlistStorage.getWaitlistInviteCode() { - Task { @MainActor in - do { - try await networkProtectionCodeRedemption.redeem(inviteCode) - NotificationCenter.default.post(name: .networkProtectionWaitlistAccessChanged, object: nil) - sendInviteCodeAvailableNotification(completion: nil) - completion(nil) - } catch { - assertionFailure("Failed to redeem invite code") - completion(.failure(error)) - } - } - } else { - completion(nil) - assertionFailure("Didn't get error or invite code") - } - } - } - -} - #if DBP // MARK: - DataBroker Protection Waitlist diff --git a/DuckDuckGo/Waitlist/WaitlistTermsAndConditionsActionHandler.swift b/DuckDuckGo/Waitlist/WaitlistTermsAndConditionsActionHandler.swift index ccde7fd9a7..3f6f456ea0 100644 --- a/DuckDuckGo/Waitlist/WaitlistTermsAndConditionsActionHandler.swift +++ b/DuckDuckGo/Waitlist/WaitlistTermsAndConditionsActionHandler.swift @@ -26,21 +26,6 @@ protocol WaitlistTermsAndConditionsActionHandler { mutating func didAccept() } -struct NetworkProtectionWaitlistTermsAndConditionsActionHandler: WaitlistTermsAndConditionsActionHandler { - @UserDefaultsWrapper(key: .networkProtectionTermsAndConditionsAccepted, defaultValue: false) - var acceptedTermsAndConditions: Bool - - func didShow() { - // Intentional no-op - } - - mutating func didAccept() { - acceptedTermsAndConditions = true - // Remove delivered NetP notifications in case the user didn't click them. - UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [NetworkProtectionWaitlist.notificationIdentifier]) - } -} - #if DBP struct DataBrokerProtectionWaitlistTermsAndConditionsActionHandler: WaitlistTermsAndConditionsActionHandler { diff --git a/UnitTests/Menus/MoreOptionsMenuTests.swift b/UnitTests/Menus/MoreOptionsMenuTests.swift index 209701e87a..6bda1607b7 100644 --- a/UnitTests/Menus/MoreOptionsMenuTests.swift +++ b/UnitTests/Menus/MoreOptionsMenuTests.swift @@ -65,7 +65,7 @@ final class MoreOptionsMenuTests: XCTestCase { } @MainActor - func testThatMoreOptionMenuHasTheExpectedItems_WhenNetworkProtectionIsEnabled() { + func testThatMoreOptionMenuHasTheExpectedItems() { moreOptionMenu = MoreOptionsMenu(tabCollectionViewModel: tabCollectionViewModel, passwordManagerCoordinator: passwordManagerCoordinator, networkProtectionFeatureVisibility: NetworkProtectionVisibilityMock(isInstalled: false, visible: true), @@ -100,38 +100,6 @@ final class MoreOptionsMenuTests: XCTestCase { } } - @MainActor - func testThatMoreOptionMenuHasTheExpectedItems_WhenNetworkProtectionIsDisabled() { - moreOptionMenu = MoreOptionsMenu(tabCollectionViewModel: tabCollectionViewModel, - passwordManagerCoordinator: passwordManagerCoordinator, - networkProtectionFeatureVisibility: NetworkProtectionVisibilityMock(isInstalled: false, visible: false), - sharingMenu: NSMenu(), - internalUserDecider: internalUserDecider) - - XCTAssertEqual(moreOptionMenu.items[0].title, UserText.sendFeedback) - XCTAssertTrue(moreOptionMenu.items[1].isSeparatorItem) - XCTAssertEqual(moreOptionMenu.items[2].title, UserText.plusButtonNewTabMenuItem) - XCTAssertEqual(moreOptionMenu.items[3].title, UserText.newWindowMenuItem) - XCTAssertEqual(moreOptionMenu.items[4].title, UserText.newBurnerWindowMenuItem) - XCTAssertTrue(moreOptionMenu.items[5].isSeparatorItem) - XCTAssertEqual(moreOptionMenu.items[6].title, UserText.zoom) - XCTAssertTrue(moreOptionMenu.items[7].isSeparatorItem) - XCTAssertEqual(moreOptionMenu.items[8].title, UserText.bookmarks) - XCTAssertEqual(moreOptionMenu.items[9].title, UserText.downloads) - XCTAssertEqual(moreOptionMenu.items[10].title, UserText.passwordManagement) - XCTAssertTrue(moreOptionMenu.items[11].isSeparatorItem) - XCTAssertEqual(moreOptionMenu.items[12].title, UserText.emailOptionsMenuItem) - XCTAssertTrue(moreOptionMenu.items[13].isSeparatorItem) - - if AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)).isUserAuthenticated { - XCTAssertTrue(moreOptionMenu.items[14].title.hasPrefix(UserText.identityTheftRestorationOptionsMenuItem)) - XCTAssertTrue(moreOptionMenu.items[15].isSeparatorItem) - XCTAssertEqual(moreOptionMenu.items[16].title, UserText.settings) - } else { - XCTAssertEqual(moreOptionMenu.items[14].title, UserText.settings) - } - } - // MARK: Zoom @MainActor @@ -195,10 +163,6 @@ final class NetworkProtectionVisibilityMock: NetworkProtectionFeatureVisibility return !visible } - func isNetworkProtectionBetaVisible() -> Bool { - return visible - } - func canStartVPN() async throws -> Bool { return false } @@ -207,10 +171,6 @@ final class NetworkProtectionVisibilityMock: NetworkProtectionFeatureVisibility // intentional no-op } - func disableForWaitlistUsers() { - // intentional no-op - } - var isEligibleForThankYouMessage: Bool { false } diff --git a/UnitTests/VPNFeedbackForm/VPNFeedbackFormViewModelTests.swift b/UnitTests/VPNFeedbackForm/VPNFeedbackFormViewModelTests.swift index 17b489a705..d9a45fda0c 100644 --- a/UnitTests/VPNFeedbackForm/VPNFeedbackFormViewModelTests.swift +++ b/UnitTests/VPNFeedbackForm/VPNFeedbackFormViewModelTests.swift @@ -128,7 +128,6 @@ private class MockVPNMetadataCollector: VPNMetadataCollector { ) let privacyProInfo = VPNMetadata.PrivacyProInfo( - betaParticipant: false, hasPrivacyProAccount: true, hasVPNEntitlement: true ) diff --git a/UnitTests/Waitlist/Mocks/MockNetworkProtectionCodeRedeemer.swift b/UnitTests/Waitlist/Mocks/MockNetworkProtectionCodeRedeemer.swift deleted file mode 100644 index b998d135e0..0000000000 --- a/UnitTests/Waitlist/Mocks/MockNetworkProtectionCodeRedeemer.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// MockNetworkProtectionCodeRedeemer.swift -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import NetworkProtection - -final class MockNetworkProtectionCodeRedeemer: NetworkProtectionCodeRedeeming { - - enum MockNetworkProtectionCodeRedeemerError: Error { - case error - } - - var throwError: Bool = false - - var redeemedCode: String? - func redeem(_ code: String) async throws { - if throwError { - throw MockNetworkProtectionCodeRedeemerError.error - } else { - redeemedCode = code - } - } - - var redeemedAccessToken: String? - func exchange(accessToken: String) async throws { - if throwError { - throw MockNetworkProtectionCodeRedeemerError.error - } else { - redeemedAccessToken = accessToken - } - } - -} From 956e5265f1337a55301fb22725c866d8cce17258 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Mon, 29 Apr 2024 13:13:26 +0200 Subject: [PATCH 15/16] Fix reporting success in create_variants.yml (#2714) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207192089896666/f Description: Always fetch Mattermost message template from main branch. --- .github/workflows/create_variants.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/create_variants.yml b/.github/workflows/create_variants.yml index c48424a947..eb3acf62ff 100644 --- a/.github/workflows/create_variants.yml +++ b/.github/workflows/create_variants.yml @@ -143,7 +143,7 @@ jobs: GH_TOKEN: ${{ github.token }} WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} run: | - curl -fLSs $(gh api https://api.github.com/repos/${{ github.repository }}/contents/scripts/assets/variants-release-mm-template.json?ref=${{ github.ref }} --jq .download_url) \ + curl -fLSs $(gh api https://api.github.com/repos/${{ github.repository }}/contents/scripts/assets/variants-release-mm-template.json --jq .download_url) \ --output message-template.json export MM_USER_HANDLE=$(base64 -d <<< ${{ secrets.MM_HANDLES_BASE64 }} | jq ".${{ github.actor }}" | tr -d '"') From 8762c1392093339f263d2d124970532ac0be8403 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Mon, 29 Apr 2024 18:52:31 +0600 Subject: [PATCH 16/16] Fix quick download button animation (#2711) Task/Issue URL: https://app.asana.com/0/1177771139624306/1207169957308547/f --- .../View/AppKit/CircularProgressView.swift | 116 ++++++++++++++++-- .../View/NavigationBarViewController.swift | 20 +-- 2 files changed, 120 insertions(+), 16 deletions(-) diff --git a/DuckDuckGo/Common/View/AppKit/CircularProgressView.swift b/DuckDuckGo/Common/View/AppKit/CircularProgressView.swift index 204d950546..3e1190a9e9 100644 --- a/DuckDuckGo/Common/View/AppKit/CircularProgressView.swift +++ b/DuckDuckGo/Common/View/AppKit/CircularProgressView.swift @@ -164,6 +164,14 @@ final class CircularProgressView: NSView { guard !isBackgroundAnimating || !animated else { // will call `updateProgressState` on animation completion completion(false) + // if background animation is in progress but 1.0 was received before + // the `progress = nil` update – complete the progress animation + // before hiding + if progress == nil && oldValue == 1.0, animated, + // shouldn‘t be already animating to 100% + progressLayer.strokeStart != 0.0 { + updateProgress(from: 0, to: 1, animated: animated) { _ in } + } return } @@ -177,7 +185,7 @@ final class CircularProgressView: NSView { completion(true) } case (true, true): - updateProgress(oldValue: oldValue, animated: animated, completion: completion) + updateProgress(from: oldValue, to: progress, animated: animated, completion: completion) case (false, false): backgroundLayer.removeAllAnimations() progressLayer.removeAllAnimations() @@ -216,17 +224,16 @@ final class CircularProgressView: NSView { } } - private func updateProgress(oldValue: Double?, animated: Bool, completion: @escaping (Bool) -> Void) { + private func updateProgress(from oldValue: Double?, to progress: Double?, animated: Bool, completion: @escaping (Bool) -> Void) { guard let progress else { assertionFailure("Unexpected flow") completion(false) return } - let currentStrokeStart = (progressLayer.presentation() ?? progressLayer).strokeStart + let currentStrokeStart = progressLayer.currentStrokeStart let newStrokeStart = 1.0 - (progress >= 0.0 ? CGFloat(progress) : max(Constants.indeterminateProgressValue, min(0.9, 1.0 - currentStrokeStart))) - guard animated else { progressLayer.strokeStart = newStrokeStart @@ -274,7 +281,7 @@ final class CircularProgressView: NSView { guard let progress, progress == value else { return } if let oldValue, oldValue < 0, value != progress, animated { - updateProgress(oldValue: value, animated: animated) { _ in } + updateProgress(from: value, to: progress, animated: animated) { _ in } return } @@ -356,7 +363,7 @@ final class CircularProgressView: NSView { progressLayer.add(progressEndAnimation, forKey: #keyPath(CAShapeLayer.strokeEnd)) let progressAnimation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.strokeStart)) - let currentStrokeStart = (progressLayer.presentation() ?? progressLayer).strokeStart + let currentStrokeStart = progressLayer.currentStrokeStart progressLayer.removeAnimation(forKey: #keyPath(CAShapeLayer.strokeStart)) progressLayer.strokeStart = 0.0 @@ -375,6 +382,14 @@ final class CircularProgressView: NSView { private extension CAShapeLayer { + var currentStrokeStart: CGFloat { + if animation(forKey: #keyPath(CAShapeLayer.strokeStart)) != nil, + let presentation = self.presentation() { + return presentation.strokeStart + } + return strokeStart + } + func configureCircle(radius: CGFloat, lineWidth: CGFloat) { self.bounds = CGRect(x: 0, y: 0, width: (radius + lineWidth) * 2, height: (radius + lineWidth) * 2) @@ -530,14 +545,97 @@ struct CircularProgress: NSViewRepresentable { perform { progress = 1 } - perform { - progress = nil + Task { + perform { + progress = nil + } } } } label: { Text(verbatim: "0->1->nil").frame(width: 120) } + Button { + Task { + progress = nil + perform { + progress = 1 + } + Task { + perform { + progress = nil + } + } + } + } label: { + Text(verbatim: "nil->1->nil").frame(width: 120) + } + + Button { + Task { + progress = nil + perform { + progress = 1 + } + Task { + perform { + progress = nil + } + Task { + perform { + progress = 1 + } + Task { + perform { + progress = nil + } + } + } + } + } + } label: { + Text(verbatim: "nil->1->nil->1->nil").frame(width: 120) + } + + Button { + Task { + progress = nil + perform { + progress = 1 + } + Task { + perform { + progress = nil + } + Task { + perform { + progress = nil + } + } + } + } + } label: { + Text(verbatim: "nil->1->nil->nil").frame(width: 120) + } + + Button { + Task { + progress = nil + perform { + progress = 0 + } + try await Task.sleep(interval: 0.2) + for p in [0.26, 0.64, 0.95, 1, nil] { + perform { + progress = p + } + try await Task.sleep(interval: 0.001) + } + } + } label: { + Text(verbatim: "nil->0.2…1->nil").frame(width: 120) + } + Button { Task { perform { @@ -581,7 +679,7 @@ struct CircularProgress: NSViewRepresentable { .background(Color.white) Spacer() } - }.frame(width: 600, height: 400) + }.frame(width: 600, height: 500) } } return ProgressPreview() diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index 7f916c8cd2..6b81061e5e 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -579,7 +579,7 @@ final class NavigationBarViewController: NSViewController { } let heightChange: () -> Void - if animated && view.window != nil { + if animated, let window = view.window, window.isVisible == true { heightChange = { NSAnimationContext.runAnimationGroup { ctx in ctx.duration = 0.1 @@ -601,13 +601,13 @@ final class NavigationBarViewController: NSViewController { performResize() } } - if view.window == nil { - // update synchronously for off-screen view - heightChange() - } else { + if let window = view.window, window.isVisible { let dispatchItem = DispatchWorkItem(block: heightChange) DispatchQueue.main.async(execute: dispatchItem) self.heightChangeAnimation = dispatchItem + } else { + // update synchronously for off-screen view + heightChange() } } @@ -642,13 +642,19 @@ final class NavigationBarViewController: NSViewController { downloadListCoordinator.progress.publisher(for: \.totalUnitCount) .combineLatest(downloadListCoordinator.progress.publisher(for: \.completedUnitCount)) - .throttle(for: 0.2, scheduler: DispatchQueue.main, latest: true) .map { (total, completed) -> Double? in guard total > 0, completed < total else { return nil } return Double(completed) / Double(total) } + .dropFirst() + .throttle(for: 0.2, scheduler: DispatchQueue.main, latest: true) .sink { [weak downloadsProgressView] progress in - downloadsProgressView?.setProgress(progress, animated: true) + guard let downloadsProgressView else { return } + if progress == nil, downloadsProgressView.progress != 1 { + // show download completed animation before hiding + downloadsProgressView.setProgress(1, animated: true) + } + downloadsProgressView.setProgress(progress, animated: true) } .store(in: &downloadsCancellables) }