diff --git a/.github/workflows/adhoc.yml b/.github/workflows/adhoc.yml index 5a2e33c100..0b78830657 100644 --- a/.github/workflows/adhoc.yml +++ b/.github/workflows/adhoc.yml @@ -58,13 +58,13 @@ jobs: echo "dsyms_path=${{ github.workspace }}/${{ env.dsyms_filename }}" >> $GITHUB_ENV - name: Upload IPA artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ${{ env.ipa_filename }} path: ${{ env.ipa_path }} - name: Upload dSYMs artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ${{ env.dsyms_filename }} path: ${{ env.dsyms_path }} diff --git a/.github/workflows/alpha.yml b/.github/workflows/alpha.yml index 5d39e3648f..bb7668cf09 100644 --- a/.github/workflows/alpha.yml +++ b/.github/workflows/alpha.yml @@ -33,6 +33,7 @@ jobs: make-alpha: runs-on: macos-13 name: Make TestFlight Alpha Build + timeout-minutes: 30 env: destination: ${{ github.event.inputs.destination || inputs.destination }} @@ -58,7 +59,7 @@ jobs: - name: Set cache key hash run: | - has_only_tags=$(jq '[ .object.pins[].state | has("version") ] | all' DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved) + has_only_tags=$(jq '[ .pins[].state | has("version") ] | all' DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved) if [[ "$has_only_tags" == "true" ]]; then echo "cache_key_hash=${{ hashFiles('DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}" >> $GITHUB_ENV else @@ -97,7 +98,7 @@ jobs: echo "build_version=${build_version}" >> $GITHUB_ENV - name: Upload dSYMs artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: DuckDuckGo-Alpha-dSYM-${{ env.app_version }} path: ${{ env.dsyms_path }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 0ce6cf2ed4..8a6d1ba63b 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -43,6 +43,7 @@ jobs: -target "DuckDuckGo" \ -scheme "DuckDuckGo" \ -destination "platform=iOS Simulator,name=iPhone 14,OS=16.4" + -skipPackagePluginValidation \ - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/end-to-end.yml b/.github/workflows/end-to-end.yml index de34f5fd98..2fcfe9b64e 100644 --- a/.github/workflows/end-to-end.yml +++ b/.github/workflows/end-to-end.yml @@ -17,7 +17,7 @@ jobs: - name: Set cache key hash run: | - has_only_tags=$(jq '[ .object.pins[].state | has("version") ] | all' DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved) + has_only_tags=$(jq '[ .pins[].state | has("version") ] | all' DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved) if [[ "$has_only_tags" == "true" ]]; then echo "cache_key_hash=${{ hashFiles('DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}" >> $GITHUB_ENV else @@ -42,6 +42,7 @@ jobs: -scheme "DuckDuckGo" \ -destination "platform=iOS Simulator,name=iPhone 14,OS=16.4" \ -derivedDataPath "DerivedData" \ + -skipPackagePluginValidation \ | tee xcodebuild.log - name: Release tests @@ -78,7 +79,7 @@ jobs: --data ' { "data": { "name": "GH Workflow Failure - End to end tests", "workspace": "${{ vars.GH_ASANA_WORKSPACE_ID }}", "projects": [ "${{ vars.GH_ASANA_IOS_APP_PROJECT_ID }}" ], "notes" : "The end to end workflow has failed. See https://github.com/duckduckgo/iOS/actions/runs/${{ github.run_id }}" } }' - name: Upload logs when workflow failed - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: failure() with: name: BuildLogs diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 5a67fdb265..e565805327 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -18,7 +18,7 @@ jobs: - name: Set cache key hash run: | - has_only_tags=$(jq '[ .object.pins[].state | has("version") ] | all' DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved) + has_only_tags=$(jq '[ .pins[].state | has("version") ] | all' DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved) if [[ "$has_only_tags" == "true" ]]; then echo "cache_key_hash=${{ hashFiles('DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}" >> $GITHUB_ENV else @@ -46,11 +46,12 @@ jobs: -scheme "AtbUITests" \ -destination "platform=iOS Simulator,name=iPhone 14,OS=16.4" \ -derivedDataPath "DerivedData" \ + -skipPackagePluginValidation \ | tee xcodebuild.log \ | xcbeautify --report junit --report-path . --junit-report-filename unittests.xml - name: Upload logs if workflow failed - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: failure() with: name: BuildLogs @@ -87,6 +88,7 @@ jobs: -scheme "FingerprintingUITests" \ -destination "platform=iOS Simulator,name=iPhone 14,OS=16.4" \ -derivedDataPath "DerivedData" \ + -skipPackagePluginValidation \ | xcbeautify --report junit --report-path . --junit-report-filename unittests.xml - name: Publish unit tests report diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 1b2e4bba26..79d4b459c5 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: SwiftLint - uses: docker://norionomura/swiftlint:0.53.0 + uses: docker://norionomura/swiftlint:0.54.0_swift-5.9.0 with: args: swiftlint --reporter github-actions-logging --strict @@ -53,7 +53,7 @@ jobs: - name: Set cache key hash run: | - has_only_tags=$(jq '[ .object.pins[].state | has("version") ] | all' DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved) + has_only_tags=$(jq '[ .pins[].state | has("version") ] | all' DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved) if [[ "$has_only_tags" == "true" ]]; then echo "cache_key_hash=${{ hashFiles('DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}" >> $GITHUB_ENV else @@ -81,12 +81,13 @@ jobs: -scheme "DuckDuckGo" \ -destination "platform=iOS Simulator,name=iPhone 14,OS=16.4" \ -derivedDataPath "DerivedData" \ + -skipPackagePluginValidation \ DDG_SLOW_COMPILE_CHECK_THRESHOLD=250 \ | tee xcodebuild.log \ | xcbeautify --report junit --report-path . --junit-report-filename unittests.xml - name: Upload logs if workflow failed - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: failure() with: name: BuildLogs @@ -144,7 +145,7 @@ jobs: - name: Set cache key hash run: | - has_only_tags=$(jq '[ .object.pins[].state | has("version") ] | all' DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved) + has_only_tags=$(jq '[ .pins[].state | has("version") ] | all' DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved) if [[ "$has_only_tags" == "true" ]]; then echo "cache_key_hash=${{ hashFiles('DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}" >> $GITHUB_ENV else @@ -183,6 +184,7 @@ jobs: -destination "platform=iOS Simulator,name=iPhone 14" \ -derivedDataPath "DerivedData" \ -configuration "Release" \ + -skipPackagePluginValidation \ | xcbeautify create-asana-task: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 58f13d0b12..eed601f957 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -80,7 +80,7 @@ jobs: - name: Upload dSYMs artifact if: always() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: DuckDuckGo-${{ steps.destination.outputs.destination }}-dSYM-${{ env.app_version }} path: ${{ env.dsyms_path }} diff --git a/.github/workflows/sync-end-to-end.yml b/.github/workflows/sync-end-to-end.yml index 0125ab669a..025aaae511 100644 --- a/.github/workflows/sync-end-to-end.yml +++ b/.github/workflows/sync-end-to-end.yml @@ -5,9 +5,10 @@ on: - cron: '0 5 * * *' # run at 5 AM UTC jobs: - sync-end-to-end-tests: - name: Sync End to end Tests + build-for-sync-end-to-end-tests: + name: Build for Sync End To End Tests runs-on: macos-13 + timeout-minutes: 30 steps: - name: Check out the code @@ -17,7 +18,7 @@ jobs: - name: Set cache key hash run: | - has_only_tags=$(jq '[ .object.pins[].state | has("version") ] | all' DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved) + has_only_tags=$(jq '[ .pins[].state | has("version") ] | all' DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved) if [[ "$has_only_tags" == "true" ]]; then echo "cache_key_hash=${{ hashFiles('DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}" >> $GITHUB_ENV else @@ -42,63 +43,78 @@ jobs: -scheme "DuckDuckGo" \ -destination "platform=iOS Simulator,name=iPhone 14" \ -derivedDataPath "DerivedData" \ + -skipPackagePluginValidation \ | tee xcodebuild.log + + - name: Store Binary + uses: actions/upload-artifact@v4 + with: + name: duckduckgo-ios-app + path: DerivedData/Build/Products/Debug-iphonesimulator/DuckDuckGo.app + + - name: Upload logs when workflow failed + uses: actions/upload-artifact@v4 + if: failure() + with: + name: BuildLogs + path: | + xcodebuild.log + DerivedData/Logs/Test/*.xcresult + retention-days: 7 + sync-end-to-end-tests: + name: Sync End To End Tests + needs: build-for-sync-end-to-end-tests + runs-on: macos-13 + timeout-minutes: 60 + strategy: + matrix: + os-version: [15, 16, 17] + max-parallel: 1 # Uncomment this line to run tests sequentially. + fail-fast: false + + steps: + - name: Check out the code + uses: actions/checkout@v3 # Don't need submodules here as this is only for the tests folder + - name: Create test account for Sync and return the recovery code uses: duckduckgo/sync_crypto/action@main id: sync-recovery-code with: debug: true - - name: Sync e2e tests - iOS 15 - uses: mobile-dev-inc/action-maestro-cloud@v1.8.0 - with: - api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }} - app-file: DerivedData/Build/Products/Debug-iphonesimulator/DuckDuckGo.app - workspace: .maestro - include-tags: sync - env: | - CODE=${{ steps.sync-recovery-code.outputs.recovery-code }} - - - name: Sync e2e tests - iOS 16 - uses: mobile-dev-inc/action-maestro-cloud@v1.8.0 + - name: Retrieve Binary + uses: actions/download-artifact@v4 with: - api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }} - app-file: DerivedData/Build/Products/Debug-iphonesimulator/DuckDuckGo.app - ios-version: 16 - workspace: .maestro - include-tags: sync - env: | - CODE=${{ steps.sync-recovery-code.outputs.recovery-code }} + name: duckduckgo-ios-app + path: DerivedData/Build/Products/Debug-iphonesimulator/DuckDuckGo.app - - name: Sync e2e tests - iOS 17 + - name: Sync e2e tests uses: mobile-dev-inc/action-maestro-cloud@v1.8.0 with: api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }} app-file: DerivedData/Build/Products/Debug-iphonesimulator/DuckDuckGo.app - ios-version: 17 + ios-version: ${{ matrix.os-version }} workspace: .maestro include-tags: sync env: | CODE=${{ steps.sync-recovery-code.outputs.recovery-code }} - - name: Create Asana task when workflow failed - if: ${{ failure() }} - run: | - curl -s "https://app.asana.com/api/1.0/tasks" \ - --header "Accept: application/json" \ - --header "Authorization: Bearer ${{ secrets.ASANA_ACCESS_TOKEN }}" \ - --header "Content-Type: application/json" \ - --data ' { "data": { "name": "GH Workflow Failure - Sync End to end tests", "workspace": "${{ vars.GH_ASANA_WORKSPACE_ID }}", "projects": [ "${{ vars.GH_ASANA_IOS_APP_PROJECT_ID }}" ], "notes" : "The end to end workflow has failed. See https://github.com/duckduckgo/iOS/actions/runs/${{ github.run_id }}" } }' + notify-failure: + name: Notify on failure + if: ${{ always() && contains(join(needs.*.result, ','), 'failure') }} + needs: [build-for-sync-end-to-end-tests, sync-end-to-end-tests] + runs-on: ubuntu-latest - - name: Upload logs when workflow failed - uses: actions/upload-artifact@v3 - if: failure() - with: - name: BuildLogs - path: | - xcodebuild.log - DerivedData/Logs/Test/*.xcresult - retention-days: 7 + steps: + - name: Create Asana task when workflow failed + run: | + curl -s "https://app.asana.com/api/1.0/tasks" \ + --header "Accept: application/json" \ + --header "Authorization: Bearer ${{ secrets.ASANA_ACCESS_TOKEN }}" \ + --header "Content-Type: application/json" \ + --data ' { "data": { "name": "GH Workflow Failure - Sync End to end tests", "workspace": "${{ vars.GH_ASANA_WORKSPACE_ID }}", "projects": [ "${{ vars.GH_ASANA_IOS_APP_PROJECT_ID }}" ], "notes" : "The end to end workflow has failed. See https://github.com/duckduckgo/iOS/actions/runs/${{ github.run_id }}" } }' + + diff --git a/.maestro/shared/add_login_from_settings.yaml b/.maestro/shared/add_login_from_settings.yaml index 199dc4f83f..1f34c6df2b 100644 --- a/.maestro/shared/add_login_from_settings.yaml +++ b/.maestro/shared/add_login_from_settings.yaml @@ -1,7 +1,7 @@ appId: com.duckduckgo.mobile.ios --- -- tapOn: Logins +- tapOn: Passwords - tapOn: Add 24 - tapOn: Title - inputText: My Personal Website @@ -10,6 +10,6 @@ appId: com.duckduckgo.mobile.ios - tapOn: example.com - inputText: mypersonalwebsite.com - tapOn: Save -- tapOn: Logins +- tapOn: Passwords - tapOn: Settings - tapOn: Done \ No newline at end of file diff --git a/.maestro/shared/remove_local_logins.yaml b/.maestro/shared/remove_local_logins.yaml index fc62c1e26d..e830ee3ec3 100644 --- a/.maestro/shared/remove_local_logins.yaml +++ b/.maestro/shared/remove_local_logins.yaml @@ -2,13 +2,12 @@ appId: com.duckduckgo.mobile.ios --- - tapOn: Settings -- tapOn: Logins -- assertVisible: Unlock device to access saved Logins +- tapOn: Passwords - tapOn: Passcode field - inputText: "0000" - pressKey: Enter - tapOn: My Personal Website -- tapOn: Delete Login -- tapOn: Delete Login +- tapOn: Delete Password +- tapOn: Delete Password - tapOn: Settings - tapOn: Done \ No newline at end of file diff --git a/.maestro/shared/sync_verify_logins.yaml b/.maestro/shared/sync_verify_logins.yaml index aed87359c8..1fb1c8b334 100644 --- a/.maestro/shared/sync_verify_logins.yaml +++ b/.maestro/shared/sync_verify_logins.yaml @@ -1,8 +1,7 @@ appId: com.duckduckgo.mobile.ios --- -- tapOn: Logins -- assertVisible: Unlock device to access saved Logins +- tapOn: Passwords - tapOn: Passcode field - inputText: "0000" - pressKey: Enter @@ -10,20 +9,20 @@ appId: com.duckduckgo.mobile.ios - tapOn: Dax Login - assertVisible: daxthetest - assertVisible: duckduckgo.com -- tapOn: Logins +- tapOn: Passwords - assertVisible: Github - tapOn: Github - assertVisible: githubusername - assertVisible: github.com -- tapOn: Logins +- tapOn: Passwords - assertVisible: StackOverflow - tapOn: StackOverflow - assertVisible: stacker - assertVisible: stackoverflow.com -- tapOn: Logins +- tapOn: Passwords - assertVisible: My Personal Website - tapOn: My Personal Website - assertVisible: me@mypersonalwebsite.com - assertVisible: mypersonalwebsite.com -- tapOn: Logins -- tapOn: Settings \ No newline at end of file +- tapOn: Passwords +- tapOn: Settings diff --git a/.maestro/sync_tests/05_delete_account.yaml b/.maestro/sync_tests/05_delete_account.yaml new file mode 100644 index 0000000000..1a40d0916a --- /dev/null +++ b/.maestro/sync_tests/05_delete_account.yaml @@ -0,0 +1,39 @@ +appId: com.duckduckgo.mobile.ios +tags: + - sync + +--- + +# Clear and launch +- clearState +- launchApp + +# Run onboarding Flow +- runFlow: + when: + visible: + text: "Let’s Do It!" + index: 0 + file: ../shared/onboarding.yaml + +# Set Internal User +- tapOn: Settings +- runFlow: + file: ../shared/set_internal_user_from_settings.yaml + +# Create account +- runFlow: + file: ../shared/sync_create.yaml + +# Remove account +- runFlow: + file: ../shared/sync_delete.yaml + +# Try to login and check for error +- assertVisible: Begin Syncing +- tapOn: Sync with Another Device +- assertVisible: Scan QR Code +- tapOn: Manually Enter Code +- tapOn: Paste +- assertVisible: Sync & Backup Error +- tapOn: OK diff --git a/.maestro/sync_tests/add_login_from_settings.yaml b/.maestro/sync_tests/add_login_from_settings.yaml deleted file mode 100644 index 199dc4f83f..0000000000 --- a/.maestro/sync_tests/add_login_from_settings.yaml +++ /dev/null @@ -1,15 +0,0 @@ -appId: com.duckduckgo.mobile.ios ---- - -- tapOn: Logins -- tapOn: Add 24 -- tapOn: Title -- inputText: My Personal Website -- tapOn: username@example.com -- inputText: me@mypersonalwebsite.com -- tapOn: example.com -- inputText: mypersonalwebsite.com -- tapOn: Save -- tapOn: Logins -- tapOn: Settings -- tapOn: Done \ No newline at end of file diff --git a/.swiftlint.yml b/.swiftlint.yml index 9b5e726984..5808536458 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,3 +1,5 @@ +allow_zero_lintable_files: true + disabled_rules: - discarded_notification_center_observer - notification_center_detachment @@ -93,3 +95,4 @@ excluded: - vendor - LocalPackages/*/Package.swift - PacketTunnelProvider/ProxyServer + - .ruby-lsp diff --git a/Configuration/Version.xcconfig b/Configuration/Version.xcconfig index 3cfb3a57fe..8d65bcd73a 100644 --- a/Configuration/Version.xcconfig +++ b/Configuration/Version.xcconfig @@ -1 +1 @@ -MARKETING_VERSION = 7.101.0 +MARKETING_VERSION = 7.102.0 diff --git a/Core/AppPrivacyConfigurationDataProvider.swift b/Core/AppPrivacyConfigurationDataProvider.swift index b4d249dd65..88beeca5eb 100644 --- a/Core/AppPrivacyConfigurationDataProvider.swift +++ b/Core/AppPrivacyConfigurationDataProvider.swift @@ -23,8 +23,8 @@ import BrowserServicesKit final public class AppPrivacyConfigurationDataProvider: EmbeddedDataProvider { public struct Constants { - public static let embeddedDataETag = "\"9f95a5896c461768903058315bf6b931\"" - public static let embeddedDataSHA = "33bbcc8e715e93f6a6052b92d990860758774d7cf4828b6aedcc958b9f46f50a" + public static let embeddedDataETag = "\"388dd0526e94f80473728c0bfbb48b39\"" + public static let embeddedDataSHA = "f7b9ae8860ff84f33e602b40d0938776d2d9327115b4ddfe09fc0fa09b5e1ff1" } public var embeddedDataEtag: String { diff --git a/Core/BookmarksModelsErrorHandling.swift b/Core/BookmarksModelsErrorHandling.swift index 5d25ef7c80..b60ecdd4d8 100644 --- a/Core/BookmarksModelsErrorHandling.swift +++ b/Core/BookmarksModelsErrorHandling.swift @@ -57,10 +57,6 @@ public class BookmarksModelsErrorHandling: EventMapping { case .missingParent(let object): domainEvent = .missingParent(object) - case .orphanedBookmarksPresent: - if let syncService, syncService.authState == .inactive { - domainEvent = .orphanedBookmarksPresent - } } if let domainEvent { diff --git a/Core/DailyPixel.swift b/Core/DailyPixel.swift index eb583ee6af..af326479de 100644 --- a/Core/DailyPixel.swift +++ b/Core/DailyPixel.swift @@ -51,10 +51,14 @@ public final class DailyPixel { /// Does not append any suffix unlike the alternative function below public static func fire(pixel: Pixel.Event, withAdditionalParameters params: [String: String] = [:], + includedParameters: [Pixel.QueryParameters] = [.atb, .appVersion], onComplete: @escaping (Swift.Error?) -> Void = { _ in }) { if !pixel.hasBeenFiredToday(dailyPixelStorage: storage) { - Pixel.fire(pixel: pixel, withAdditionalParameters: params, onComplete: onComplete) + Pixel.fire(pixel: pixel, + withAdditionalParameters: params, + includedParameters: includedParameters, + onComplete: onComplete) updatePixelLastFireDate(pixel: pixel) } else { onComplete(Error.alreadyFired) diff --git a/Core/FeatureFlag.swift b/Core/FeatureFlag.swift index dfe79f7d48..06e6589641 100644 --- a/Core/FeatureFlag.swift +++ b/Core/FeatureFlag.swift @@ -39,8 +39,10 @@ public enum FeatureFlag: String { extension FeatureFlag: FeatureFlagSourceProviding { public var source: FeatureFlagSource { switch self { - case .debugMenu, .sync, .appTrackingProtection: + case .debugMenu, .appTrackingProtection: return .internalOnly + case .sync: + return .remoteReleasable(.subfeature(SyncSubfeature.level0ShowSync)) case .networkProtection: return .remoteReleasable(.feature(.networkProtection)) case .networkProtectionWaitlistAccess: diff --git a/Core/Pixel.swift b/Core/Pixel.swift index 229598fd36..b6d16d54cf 100644 --- a/Core/Pixel.swift +++ b/Core/Pixel.swift @@ -203,7 +203,7 @@ public class Pixel { headers: headers) let request = APIRequest(configuration: configuration, urlSession: .session(useMainThreadCallbackQueue: true)) request.fetch { _, error in - os_log("Pixel fired %s %s", log: .generalLog, type: .debug, pixelName, "\(params)") + os_log("Pixel fired %{public}s %{public}s", log: .generalLog, type: .debug, pixelName, "\(params)") onComplete(error) } } diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index d35c102063..8d1ac89d99 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -22,6 +22,7 @@ import BrowserServicesKit import Bookmarks import Configuration import DDGSync +import NetworkProtection // swiftlint:disable file_length extension Pixel { @@ -316,8 +317,19 @@ extension Pixel { case networkProtectionActiveUser case networkProtectionNewUser + case networkProtectionEnableAttemptConnecting + case networkProtectionEnableAttemptSuccess + case networkProtectionEnableAttemptFailure + + case networkProtectionTunnelFailureDetected + case networkProtectionTunnelFailureRecovered + + case networkProtectionLatency(quality: NetworkProtectionLatencyMonitor.ConnectionQuality) + case networkProtectionLatencyError + + case networkProtectionEnabledOnSearch + case networkProtectionRekeyCompleted - case networkProtectionLatency case networkProtectionTunnelConfigurationNoServerRegistrationInfo case networkProtectionTunnelConfigurationCouldNotSelectClosestServer @@ -493,7 +505,6 @@ extension Pixel { case indexOutOfRange(BookmarksModelError.ModelType) case saveFailed(BookmarksModelError.ModelType) case missingParent(BookmarksModelError.ObjectType) - case orphanedBookmarksPresent case bookmarksCouldNotLoadDatabase case bookmarksCouldNotPrepareDatabase @@ -851,8 +862,15 @@ extension Pixel.Event { case .networkProtectionActiveUser: return "m_netp_daily_active_d" case .networkProtectionNewUser: return "m_netp_daily_active_u" + case .networkProtectionEnableAttemptConnecting: return "m_netp_ev_enable_attempt" + case .networkProtectionEnableAttemptSuccess: return "m_netp_ev_enable_attempt_success" + case .networkProtectionEnableAttemptFailure: return "m_netp_ev_enable_attempt_failure" + case .networkProtectionTunnelFailureDetected: return "m_netp_ev_tunnel_failure" + case .networkProtectionTunnelFailureRecovered: return "m_netp_ev_tunnel_failure_recovered" + case .networkProtectionLatency(let quality): return "m_netp_ev_\(quality.rawValue)_latency" + case .networkProtectionLatencyError: return "m_netp_ev_latency_error_d" case .networkProtectionRekeyCompleted: return "m_netp_rekey_completed" - case .networkProtectionLatency: return "m_netp_latency" + case .networkProtectionEnabledOnSearch: return "m_netp_enabled_on_search" case .networkProtectionTunnelConfigurationNoServerRegistrationInfo: return "m_netp_tunnel_config_error_no_server_registration_info" case .networkProtectionTunnelConfigurationCouldNotSelectClosestServer: return "m_netp_tunnel_config_error_could_not_select_closest_server" case .networkProtectionTunnelConfigurationCouldNotGetPeerPublicKey: return "m_netp_tunnel_config_error_could_not_get_peer_public_key" @@ -1017,7 +1035,6 @@ extension Pixel.Event { case .indexOutOfRange(let modelType): return "m_d_bookmarks_index_out_of_range_\(modelType.rawValue)" case .saveFailed(let modelType): return "m_d_bookmarks_view_model_save_failed_\(modelType.rawValue)" case .missingParent(let objectType): return "m_d_bookmark_model_missing_parent_\(objectType.rawValue)" - case .orphanedBookmarksPresent: return "m_d_bookmarks_orphans_present" case .bookmarksCouldNotLoadDatabase: return "m_d_bookmarks_could_not_load_database" case .bookmarksCouldNotPrepareDatabase: return "m_d_bookmarks_could_not_prepare_database" diff --git a/Core/SyncMetricsEventsHandler.swift b/Core/SyncMetricsEventsHandler.swift index 4b288c4478..a9fb6f2a10 100644 --- a/Core/SyncMetricsEventsHandler.swift +++ b/Core/SyncMetricsEventsHandler.swift @@ -27,9 +27,9 @@ public class SyncMetricsEventsHandler: EventMapping { super.init { event, _, _, _ in switch event { case .overrideEmailProtectionSettings: - Pixel.fire(pixel: .syncDuckAddressOverride) + Pixel.fire(pixel: .syncDuckAddressOverride, includedParameters: [.appVersion]) case .localTimestampResolutionTriggered(let feature): - Pixel.fire(pixel: .syncLocalTimestampResolutionTriggered(feature)) + Pixel.fire(pixel: .syncLocalTimestampResolutionTriggered(feature), includedParameters: [.appVersion]) } } } diff --git a/Core/URLExtension.swift b/Core/URLExtension.swift index 75c2cf99fe..1d65b8c0b5 100644 --- a/Core/URLExtension.swift +++ b/Core/URLExtension.swift @@ -20,6 +20,7 @@ import Foundation import JavaScriptCore import BrowserServicesKit +import Network extension URL { @@ -48,10 +49,19 @@ extension URL { break case .none: // assume http by default - guard let urlWithScheme = URL(string: URLProtocol.http.scheme + text), - // only allow 2nd+ level domains or "localhost" without scheme - urlWithScheme.host?.contains(".") == true || urlWithScheme.host == .localhost - else { return nil } + guard let urlWithScheme = URL(string: URLProtocol.http.scheme + text), let host = urlWithScheme.host else { + return nil + } + // only allow 2nd+ level domains or "localhost" without scheme + guard host.contains(".") == true || host == .localhost else { + return nil + } + if IPv4Address(host) != nil { + // Require 4 octets specified explicitly for an IPv4 address (avoid 1.4 -> 1.0.0.4 expansion) + guard host.split(separator: ".").count == 4 else { + return nil + } + } url = urlWithScheme default: diff --git a/Core/UniquePixel.swift b/Core/UniquePixel.swift new file mode 100644 index 0000000000..4be27a5e61 --- /dev/null +++ b/Core/UniquePixel.swift @@ -0,0 +1,84 @@ +// +// UniquePixel.swift +// DuckDuckGo +// +// 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 + +/// A variant of pixel that is fired just once. Ever. +/// +/// The 'fire' method mimics standard Pixel API. +/// The 'onComplete' closure is always called - even when no pixel is fired. +/// In those scenarios a 'UniquePixelError' is returned denoting the reason. +/// +public final class UniquePixel { + + public enum Error: Swift.Error { + + case alreadyFired + + } + + private enum Constant { + + static let uniquePixelStorageIdentifier = "com.duckduckgo.unique.pixel.storage" + + } + + public static let storage = UserDefaults(suiteName: Constant.uniquePixelStorageIdentifier)! + + /// Sends a unique Pixel + /// This requires the pixel name to end with `_u` + public static func fire(pixel: Pixel.Event, + withAdditionalParameters params: [String: String] = [:], + onComplete: @escaping (Swift.Error?) -> Void = { _ in }) { + guard pixel.name.hasSuffix("_u") else { + assertionFailure("Unique pixel: must end with _u") + return + } + + if !pixel.hasBeenFiredEver(uniquePixelStorage: storage) { + Pixel.fire(pixel: pixel, withAdditionalParameters: params, onComplete: onComplete) + storage.set(Date(), forKey: pixel.name) + } else { + onComplete(Error.alreadyFired) + } + } + + public static func dateString(for date: Date?) -> String { + guard let date else { return "" } + + let dateFormatter = DateFormatter() + dateFormatter.calendar = Calendar.current + dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) + dateFormatter.dateFormat = "yyyy-MM-dd" + + return dateFormatter.string(from: date) + } +} + +extension Pixel.Event { + + public func lastFireDate(uniquePixelStorage: UserDefaults) -> Date? { + uniquePixelStorage.object(forKey: name) as? Date + } + + func hasBeenFiredEver(uniquePixelStorage: UserDefaults) -> Bool { + lastFireDate(uniquePixelStorage: uniquePixelStorage) != nil + } + +} diff --git a/Core/UserDefaultsPropertyWrapper.swift b/Core/UserDefaultsPropertyWrapper.swift index e5316c4e73..2cd47169db 100644 --- a/Core/UserDefaultsPropertyWrapper.swift +++ b/Core/UserDefaultsPropertyWrapper.swift @@ -57,6 +57,7 @@ public struct UserDefaultsWrapper { case legacyCovidInfo = "com.duckduckgo.ios.home.covidInfo" case lastConfigurationRefreshDate = "com.duckduckgo.ios.lastConfigurationRefreshDate" + case lastConfigurationUpdateDate = "com.duckduckgo.ios.lastConfigurationUpdateDate" case lastRemoteMessagingRefreshDate = "com.duckduckgo.ios.lastRemoteMessagingRefreshDate" case doNotSell = "com.duckduckgo.ios.sendDoNotSell" @@ -106,12 +107,16 @@ public struct UserDefaultsWrapper { case syncIsEligibleForFaviconsFetcherOnboarding = "com.duckduckgo.ios.sync-is-eligible-for-favicons-fetcher-onboarding" case syncDidPresentFaviconsFetcherOnboarding = "com.duckduckgo.ios.sync-did-present-favicons-fetcher-onboarding" case syncDidMigrateToImprovedListsHandling = "com.duckduckgo.ios.sync-did-migrate-to-improved-lists-handling" + case syncDidShowSyncPausedByFeatureFlagAlert = "com.duckduckgo.ios.sync-did-show-sync-paused-by-feature-flag-alert" case networkProtectionDebugOptionAlwaysOnDisabled = "com.duckduckgo.network-protection.always-on.disabled" case networkProtectionWaitlistTermsAndConditionsAccepted = "com.duckduckgo.ios.vpn.terms-and-conditions-accepted" case addressBarPosition = "com.duckduckgo.ios.addressbarposition" case showFullSiteAddress = "com.duckduckgo.ios.showfullsiteaddress" + + case privacyConfigCustomURL = "com.duckduckgo.ios.privacyConfigCustomURL" + } private let key: Key diff --git a/Core/ios-config.json b/Core/ios-config.json index a7e3c08b58..2afc6d62c5 100644 --- a/Core/ios-config.json +++ b/Core/ios-config.json @@ -1,6 +1,6 @@ { "readme": "https://github.com/duckduckgo/privacy-configuration", - "version": 1702059545091, + "version": 1703026028516, "features": { "adClickAttribution": { "readme": "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/#3rd-party-tracker-loading-protection", @@ -245,6 +245,9 @@ { "domain": "metro.co.uk" }, + { + "domain": "youtube.com" + }, { "domain": "earth.google.com" }, @@ -264,7 +267,7 @@ ] }, "state": "enabled", - "hash": "9ab9e1acdb6a8617c77109acc1e3943c" + "hash": "a1060783f1bf56f5cc661b994c9c9e56" }, "autofill": { "exceptions": [ @@ -1306,6 +1309,10 @@ "selector": ".ad-unit", "type": "hide-empty" }, + { + "selector": ".ad-unit-wrapper", + "type": "hide-empty" + }, { "selector": ".column-ad", "type": "hide-empty" @@ -1394,6 +1401,10 @@ "selector": ".ad-banner-container", "type": "hide-empty" }, + { + "selector": "#banner_ad", + "type": "hide-empty" + }, { "selector": "[class*='bannerAd']", "type": "hide-empty" @@ -1482,6 +1493,10 @@ "selector": "[id*='advert-']", "type": "hide-empty" }, + { + "selector": "[aria-label='advertisement']", + "type": "hide-empty" + }, { "selector": ".ads__inline", "type": "closest-empty" @@ -1730,6 +1745,19 @@ "upgrade to flickr pro to hide these ads" ], "domains": [ + { + "domain": "10minutemail.com", + "rules": [ + { + "selector": "#secondary_ads", + "type": "hide-empty" + }, + { + "selector": "#vi-smartbanner", + "type": "hide" + } + ] + }, { "domain": "3bmeteo.com", "rules": [ @@ -1746,6 +1774,27 @@ } ] }, + { + "domain": "9gag.com", + "rules": [ + { + "selector": ".billboard", + "type": "hide-empty" + }, + { + "selector": ".inline-ad-container", + "type": "hide-empty" + }, + { + "selector": ".salt-section", + "type": "hide-empty" + }, + { + "selector": "#top-adhesion", + "type": "hide-empty" + } + ] + }, { "domain": "abc.es", "rules": [ @@ -1915,6 +1964,19 @@ } ] }, + { + "domain": "businessinsider.com", + "rules": [ + { + "selector": ".in-post-sticky", + "type": "hide-empty" + }, + { + "selector": ".subnav-ad-layout", + "type": "hide-empty" + } + ] + }, { "domain": "carandclassic.com", "rules": [ @@ -2045,6 +2107,23 @@ } ] }, + { + "domain": "dexerto.com", + "rules": [ + { + "selector": "#leaderboard-top-adhesion", + "type": "closest-empty" + }, + { + "selector": "[data-cy='Ad']", + "type": "closest-empty" + }, + { + "selector": "[data-cy='VidazooPlayer']", + "type": "closest-empty" + } + ] + }, { "domain": "dpreview.com", "rules": [ @@ -2054,6 +2133,23 @@ } ] }, + { + "domain": "drugs.com", + "rules": [ + { + "selector": ".topbanner-wrap", + "type": "hide" + }, + { + "selector": ".display-ad-wrapper", + "type": "hide-empty" + }, + { + "selector": "[id*='ddc-sidebox-ad-stacked-wrap']", + "type": "hide-empty" + } + ] + }, { "domain": "ebay.com", "rules": [ @@ -2300,6 +2396,35 @@ } ] }, + { + "domain": "gbnews.com", + "rules": [ + { + "selector": ".video-inbody", + "type": "hide-empty" + }, + { + "selector": ".ad--billboard", + "type": "hide" + }, + { + "selector": ".ad--placeholder", + "type": "hide" + }, + { + "selector": ".stiky_sky", + "type": "hide" + }, + { + "selector": "[position='sticky_banner']", + "type": "hide" + }, + { + "selector": ".ad-inbody", + "type": "hide" + } + ] + }, { "domain": "getpocket.com", "rules": [ @@ -2516,6 +2641,10 @@ { "selector": ".in-post-sticky", "type": "hide-empty" + }, + { + "selector": ".subnav-ad-layout", + "type": "hide-empty" } ] }, @@ -2827,11 +2956,14 @@ ] }, { - "domain": "orange.fr", + "domain": "oceanofcompressed.xyz", "rules": [ { - "selector": ".tag-rm", - "type": "hide-empty" + "type": "disable-default" + }, + { + "selector": "#sticky-ads", + "type": "hide" } ] }, @@ -2848,6 +2980,15 @@ } ] }, + { + "domain": "orange.fr", + "rules": [ + { + "selector": ".tag-rm", + "type": "hide-empty" + } + ] + }, { "domain": "ouest-france.fr", "rules": [ @@ -2870,6 +3011,35 @@ } ] }, + { + "domain": "pcgamesn.com", + "rules": [ + { + "selector": ".static_mpu_wrap", + "type": "hide-empty" + }, + { + "selector": "#nn_astro_wrapper", + "type": "hide-empty" + }, + { + "selector": ".ad-nextpage", + "type": "hide" + }, + { + "selector": ".legion_primiswrapper", + "type": "hide-empty" + }, + { + "selector": ".nn_mobile_mpu_wrapper", + "type": "hide-empty" + }, + { + "selector": ".nn-sticky", + "type": "hide-empty" + } + ] + }, { "domain": "petapixel.com", "rules": [ @@ -3509,6 +3679,10 @@ { "selector": "[data-content='Advertisement']", "type": "hide-empty" + }, + { + "selector": "#YDC-Lead-Stack", + "type": "hide-empty" } ] }, @@ -3626,7 +3800,7 @@ ] }, "state": "enabled", - "hash": "5a0bfa6ba53c5d1f14ef839a136f2f59" + "hash": "182ef21a9dcfd3a160468f851c4b1789" }, "exceptionHandler": { "exceptions": [ @@ -4340,6 +4514,25 @@ "state": "disabled", "hash": "5e792dd491428702bc0104240fbce0ce" }, + "sync": { + "exceptions": [], + "state": "internal", + "features": { + "level0ShowSync": { + "state": "enabled" + }, + "level1AllowDataSyncing": { + "state": "enabled" + }, + "level2AllowSetupFlows": { + "state": "enabled" + }, + "level3AllowCreateAccount": { + "state": "enabled" + } + }, + "hash": "92673fe625ae2b888a4b0bfa9a974ce4" + }, "trackerAllowlist": { "state": "enabled", "settings": { @@ -4374,6 +4567,16 @@ } ] }, + "a2z.com": { + "rules": [ + { + "rule": "assets.brightspot.abebooks.a2z.com/", + "domains": [ + "" + ] + } + ] + }, "acsbapp.com": { "rules": [ { @@ -4592,6 +4795,12 @@ "wxii12.com", "wyff4.com" ] + }, + { + "rule": "z-na.amazon-adsystem.com/widgets/onejs", + "domains": [ + "oceanofcompressed.xyz" + ] } ] }, @@ -4620,8 +4829,17 @@ { "rule": "analytics.analytics-egain.com/onetag/", "domains": [ - "landsend.com", - "support.norton.com" + "" + ] + } + ] + }, + "appboycdn.com": { + "rules": [ + { + "rule": "js.appboycdn.com/web-sdk/3.1/appboy.min.js", + "domains": [ + "edx.org" ] } ] @@ -5167,6 +5385,7 @@ "asics.com", "brooklinen.com", "carters.com", + "otterbox.com", "seatosummit.com" ] } @@ -5226,6 +5445,16 @@ } ] }, + "egain.cloud": { + "rules": [ + { + "rule": "egain.cloud/", + "domains": [ + "" + ] + } + ] + }, "ensighten.com": { "rules": [ { @@ -5330,8 +5559,7 @@ { "rule": "app.five9.com", "domains": [ - "gmsdnv.com", - "machiassavings.bank" + "" ] } ] @@ -5458,6 +5686,7 @@ "domains": [ "doterra.com", "easyjet.com", + "edx.org", "worlddutyfree.com" ] }, @@ -5564,6 +5793,12 @@ "rawstory.com", "usatoday.com" ] + }, + { + "rule": "storage.googleapis.com/code.snapengage.com/", + "domains": [ + "" + ] } ] }, @@ -5582,6 +5817,8 @@ { "rule": "pagead2.googlesyndication.com/pagead/js/adsbygoogle.js", "domains": [ + "air-journal.fr", + "arcadepunks.com", "daotranslate.com", "drakescans.com", "duden.de", @@ -5783,15 +6020,9 @@ "gorgias.chat": { "rules": [ { - "rule": "config.gorgias.chat", + "rule": "gorgias.chat", "domains": [ - "help.athleticbrewing.com" - ] - }, - { - "rule": "assets.gorgias.chat", - "domains": [ - "help.athleticbrewing.com" + "" ] } ] @@ -5863,7 +6094,13 @@ { "rule": "api.hubspot.com/livechat-public/v1/message/public", "domains": [ - "pippintitle.com" + "" + ] + }, + { + "rule": "js.hubspot.com/web-interactives-embed.js", + "domains": [ + "" ] }, { @@ -6040,6 +6277,7 @@ "rule": "www.klaviyo.com/media/js/public/klaviyo_subscribe.js", "domains": [ "fearofgod.com", + "restrap.com", "shopyalehome.com", "silhouetteu.com" ] @@ -6171,6 +6409,16 @@ } ] }, + "media.net": { + "rules": [ + { + "rule": "contextual.media.net/dmedianet.js", + "domains": [ + "oceanofcompressed.xyz" + ] + } + ] + }, "mediavine.com": { "rules": [ { @@ -6629,7 +6877,8 @@ { "rule": "secure.quantserve.com/quant.js", "domains": [ - "aternos.org" + "aternos.org", + "oceanofcompressed.xyz" ] } ] @@ -7314,7 +7563,7 @@ "domain": "sundancecatalog.com" } ], - "hash": "d9e22bdb82a33762f16ce921ef991054" + "hash": "c1968268cb8a82bf532443edd17d9499" }, "trackingCookies1p": { "settings": { diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 5dbab49ad4..2fbdc7b2f6 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -537,6 +537,7 @@ 9830A06325ED0DB900DB64DE /* BrowsingMenu.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9830A06225ED0DB900DB64DE /* BrowsingMenu.xcassets */; }; 9833913727AC400800DAF119 /* AppTrackerDataSetProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9833913627AC400800DAF119 /* AppTrackerDataSetProvider.swift */; }; 9838059F2228208E00385F1A /* PositiveFeedbackViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9838059E2228208E00385F1A /* PositiveFeedbackViewController.swift */; }; + 983BD6B52B34760600AAC78E /* MockPrivacyConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1B0F6412AB08BE9001EAF05 /* MockPrivacyConfiguration.swift */; }; 983D71B12A286E810072E26D /* SyncDebugViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 983D71B02A286E810072E26D /* SyncDebugViewController.swift */; }; 983EABB8236198F6003948D1 /* DatabaseMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 983EABB7236198F6003948D1 /* DatabaseMigration.swift */; }; 984147A824F0259000362052 /* Onboarding.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 984147AA24F0259000362052 /* Onboarding.storyboard */; }; @@ -692,6 +693,7 @@ B6BA95C528894A28004ABA20 /* BrowsingMenuViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B6BA95C428894A28004ABA20 /* BrowsingMenuViewController.storyboard */; }; B6BA95E828924730004ABA20 /* JSAlertController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B6BA95E728924730004ABA20 /* JSAlertController.storyboard */; }; B6CB93E5286445AB0090FEB4 /* Base64DownloadSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6CB93E4286445AB0090FEB4 /* Base64DownloadSession.swift */; }; + BDC234F72B27F51100D3C798 /* UniquePixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC234F62B27F51100D3C798 /* UniquePixel.swift */; }; C10CB5F32A1A5BDF0048E503 /* AutofillViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = C10CB5F22A1A5BDF0048E503 /* AutofillViews.swift */; }; C111B26927F579EF006558B1 /* BookmarkOrFolderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C111B26827F579EF006558B1 /* BookmarkOrFolderTests.swift */; }; C12726EE2A5FF88C00215B02 /* EmailSignupPromptView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C12726ED2A5FF88C00215B02 /* EmailSignupPromptView.swift */; }; @@ -716,7 +718,6 @@ C18ED43A2AB6F77600BF3805 /* AutofillSettingsEnableFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C18ED4392AB6F77600BF3805 /* AutofillSettingsEnableFooterView.swift */; }; C18ED43C2AB8364400BF3805 /* FileTextPreviewDebugViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C18ED43B2AB8364400BF3805 /* FileTextPreviewDebugViewController.swift */; }; C1963863283794A000298D4D /* BookmarksCachingSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1963862283794A000298D4D /* BookmarksCachingSearch.swift */; }; - C1B0F6422AB08BE9001EAF05 /* MockPrivacyConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1B0F6412AB08BE9001EAF05 /* MockPrivacyConfiguration.swift */; }; C1B7B51C28941E980098FD6A /* HomeMessageViewModelBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1B7B51B28941E980098FD6A /* HomeMessageViewModelBuilder.swift */; }; C1B7B52328941F2A0098FD6A /* RemoteMessagingStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1B7B51F28941F2A0098FD6A /* RemoteMessagingStore.swift */; }; C1B7B52428941F2A0098FD6A /* RemoteMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1B7B52028941F2A0098FD6A /* RemoteMessageRequest.swift */; }; @@ -763,6 +764,7 @@ CBDD5DDF29A6736A00832877 /* APIHeadersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBDD5DDE29A6736A00832877 /* APIHeadersTests.swift */; }; CBDD5DE129A6741300832877 /* MockBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBDD5DE029A6741300832877 /* MockBundle.swift */; }; CBEFB9142AE0844700DEDE7B /* CriticalAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBEFB9102ADFFE7900DEDE7B /* CriticalAlerts.swift */; }; + CBFCB30E2B2CD47800253E9E /* ConfigurationURLDebugViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBFCB30D2B2CD47800253E9E /* ConfigurationURLDebugViewController.swift */; }; D63657192A7BAE7C001AF19D /* EmailManagerRequestDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63657182A7BAE7C001AF19D /* EmailManagerRequestDelegate.swift */; }; D6E83C122B1E6AB3006C8AFB /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E83C112B1E6AB3006C8AFB /* SettingsView.swift */; }; D6E83C2E2B1EA06E006C8AFB /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E83C2D2B1EA06E006C8AFB /* SettingsViewModel.swift */; }; @@ -2286,6 +2288,7 @@ B6BA95C428894A28004ABA20 /* BrowsingMenuViewController.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = BrowsingMenuViewController.storyboard; sourceTree = ""; }; B6BA95E728924730004ABA20 /* JSAlertController.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = JSAlertController.storyboard; sourceTree = ""; }; B6CB93E4286445AB0090FEB4 /* Base64DownloadSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Base64DownloadSession.swift; sourceTree = ""; }; + BDC234F62B27F51100D3C798 /* UniquePixel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UniquePixel.swift; sourceTree = ""; }; C10CB5F22A1A5BDF0048E503 /* AutofillViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillViews.swift; sourceTree = ""; }; C111B26827F579EF006558B1 /* BookmarkOrFolderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkOrFolderTests.swift; sourceTree = ""; }; C12726ED2A5FF88C00215B02 /* EmailSignupPromptView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailSignupPromptView.swift; sourceTree = ""; }; @@ -2373,6 +2376,7 @@ CBF14FC227970072001D94D0 /* HomeMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeMessageView.swift; sourceTree = ""; }; CBF14FC427970AB0001D94D0 /* HomeMessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeMessageViewModel.swift; sourceTree = ""; }; CBF14FC627970C8A001D94D0 /* HomeMessageCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeMessageCollectionViewCell.swift; sourceTree = ""; }; + CBFCB30D2B2CD47800253E9E /* ConfigurationURLDebugViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationURLDebugViewController.swift; sourceTree = ""; }; D63657182A7BAE7C001AF19D /* EmailManagerRequestDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmailManagerRequestDelegate.swift; sourceTree = ""; }; D6E83C112B1E6AB3006C8AFB /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; D6E83C2D2B1EA06E006C8AFB /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; @@ -3849,6 +3853,7 @@ F46FEC5627987A5F0061D9DF /* KeychainItemsDebugViewController.swift */, 983D71B02A286E810072E26D /* SyncDebugViewController.swift */, EE72CA842A862D000043B5B3 /* NetworkProtectionDebugViewController.swift */, + CBFCB30D2B2CD47800253E9E /* ConfigurationURLDebugViewController.swift */, ); name = Debug; sourceTree = ""; @@ -4644,6 +4649,7 @@ F1134EAE1F40AB2300B73467 /* Parser */, F1134EA91F3E2BA700B73467 /* Store */, CB2A7EF028410DF700885F67 /* PixelEvent.swift */, + BDC234F62B27F51100D3C798 /* UniquePixel.swift */, 853A717520F62FE800FE60BC /* Pixel.swift */, 1E05D1D729C46EDA00BF9A1F /* TimedPixel.swift */, 1E05D1D529C46EBB00BF9A1F /* DailyPixel.swift */, @@ -5423,6 +5429,7 @@ buildRules = ( ); dependencies = ( + B6080BB52B20B03800B418EF /* PBXTargetDependency */, 4B470EE7299C6DFB0086EBDC /* PBXTargetDependency */, ); name = PacketTunnelProvider; @@ -5444,6 +5451,7 @@ buildRules = ( ); dependencies = ( + B6080BBD2B20B05000B418EF /* PBXTargetDependency */, 025CCFE82582601C001CD5BB /* PBXTargetDependency */, ); name = FingerprintingUITests; @@ -5463,6 +5471,7 @@ buildRules = ( ); dependencies = ( + B6080BAF2B20B02800B418EF /* PBXTargetDependency */, ); name = ShareExtension; productName = ShareExtension; @@ -5489,6 +5498,7 @@ buildRules = ( ); dependencies = ( + B6080BAD2B20B02400B418EF /* PBXTargetDependency */, F143C2EA1E4A4CD400CFDE3A /* PBXTargetDependency */, 8390447520BDCE10006461CD /* PBXTargetDependency */, 85482D932462DCD100EDEDD1 /* PBXTargetDependency */, @@ -5521,6 +5531,7 @@ buildRules = ( ); dependencies = ( + B6080BBB2B20B04D00B418EF /* PBXTargetDependency */, 84E341A81E2F7EFB00BDBA6F /* PBXTargetDependency */, ); name = UnitTests; @@ -5544,6 +5555,7 @@ buildRules = ( ); dependencies = ( + B6080BB32B20B03400B418EF /* PBXTargetDependency */, 85DF714924F7FE6100C89288 /* PBXTargetDependency */, ); name = WidgetsExtension; @@ -5565,6 +5577,7 @@ buildRules = ( ); dependencies = ( + B6080BB12B20B02B00B418EF /* PBXTargetDependency */, ); name = OpenAction; productName = OpenAction; @@ -5582,6 +5595,7 @@ buildRules = ( ); dependencies = ( + B6080BBF2B20B05300B418EF /* PBXTargetDependency */, 85D33FD125C97B6E002B91A6 /* PBXTargetDependency */, ); name = IntegrationTests; @@ -5604,6 +5618,7 @@ buildRules = ( ); dependencies = ( + B6080BB92B20B04A00B418EF /* PBXTargetDependency */, 85F21DB3210F5E32002631A6 /* PBXTargetDependency */, ); name = AtbUITests; @@ -5625,6 +5640,7 @@ buildRules = ( ); dependencies = ( + B6080BC12B20B05600B418EF /* PBXTargetDependency */, 9825F9CC293F2DE900F220F2 /* PBXTargetDependency */, ); name = PerformanceTests; @@ -5661,6 +5677,7 @@ buildRules = ( ); dependencies = ( + B6080BB72B20B03B00B418EF /* PBXTargetDependency */, ); name = Core; packageProductDependencies = ( @@ -6474,6 +6491,7 @@ EE0153EF2A70021E002A8B26 /* NetworkProtectionInviteView.swift in Sources */, 9888F77B2224980500C46159 /* FeedbackViewController.swift in Sources */, D6E83C662B23936F006C8AFB /* SettingsDebugView.swift in Sources */, + CBFCB30E2B2CD47800253E9E /* ConfigurationURLDebugViewController.swift in Sources */, 982686AD2600C0850011A8D6 /* ActionMessageView.swift in Sources */, F446B9B5251150AC00324016 /* HomeMessageViewSectionRenderer.swift in Sources */, 98D98A8225ED88E300D8E3DF /* BrowsingMenuSeparatorViewCell.swift in Sources */, @@ -6673,7 +6691,6 @@ C1B7B52428941F2A0098FD6A /* RemoteMessageRequest.swift in Sources */, EE9D68DA2AE1659F00B55EF4 /* NetworkProtectionVPNNotificationsViewModel.swift in Sources */, 1E8AD1D727C2E24E00ABA377 /* DownloadsListRowViewModel.swift in Sources */, - C1B0F6422AB08BE9001EAF05 /* MockPrivacyConfiguration.swift in Sources */, 1E865AF0272042DB001C74F3 /* TextSizeSettingsViewController.swift in Sources */, 8524CC9A246DA81700E59D45 /* FullscreenDaxDialogViewController.swift in Sources */, F17669D71E43401C003D3222 /* MainViewController.swift in Sources */, @@ -6755,6 +6772,7 @@ 8341D807212D5E8D000514C2 /* HashExtensionTest.swift in Sources */, C1D21E2F293A599C006E5A05 /* AutofillLoginSessionTests.swift in Sources */, 85D2187924BF6B8B004373D2 /* FaviconSourcesProviderTests.swift in Sources */, + 983BD6B52B34760600AAC78E /* MockPrivacyConfiguration.swift in Sources */, 1E8146AD28C8ABF000D1AF63 /* TrackerAnimationLogicTests.swift in Sources */, C1CDA31E2AFBF811006D1476 /* AutofillNeverPromptWebsitesManagerTests.swift in Sources */, B6AD9E3A28D456820019CDE9 /* PrivacyConfigurationManagerMock.swift in Sources */, @@ -6939,6 +6957,7 @@ 37445F972A155F7C0029F789 /* SyncDataProviders.swift in Sources */, EE9D68DE2AE2A65600B55EF4 /* UserDefaults+NetworkProtection.swift in Sources */, CB258D1F29A52B2500DEBA24 /* Configuration.swift in Sources */, + BDC234F72B27F51100D3C798 /* UniquePixel.swift in Sources */, 9847C00027A2DDBB00DB07AA /* AppPrivacyConfigurationDataProvider.swift in Sources */, F143C3281E4A9A0E00CFDE3A /* StringExtension.swift in Sources */, 85449EFB23FDA0BC00512AAF /* UserDefaultsPropertyWrapper.swift in Sources */, @@ -7056,6 +7075,50 @@ target = 84E341911E2F7EFB00BDBA6F /* DuckDuckGo */; targetProxy = 9825F9CD293F2DE900F220F2 /* PBXContainerItemProxy */; }; + B6080BAD2B20B02400B418EF /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = B6080BAC2B20B02400B418EF /* SwiftLintPlugin */; + }; + B6080BAF2B20B02800B418EF /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = B6080BAE2B20B02800B418EF /* SwiftLintPlugin */; + }; + B6080BB12B20B02B00B418EF /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = B6080BB02B20B02B00B418EF /* SwiftLintPlugin */; + }; + B6080BB32B20B03400B418EF /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = B6080BB22B20B03400B418EF /* SwiftLintPlugin */; + }; + B6080BB52B20B03800B418EF /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = B6080BB42B20B03800B418EF /* SwiftLintPlugin */; + }; + B6080BB72B20B03B00B418EF /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = B6080BB62B20B03B00B418EF /* SwiftLintPlugin */; + }; + B6080BB92B20B04A00B418EF /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = B6080BB82B20B04A00B418EF /* SwiftLintPlugin */; + }; + B6080BBB2B20B04D00B418EF /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = B6080BBA2B20B04D00B418EF /* SwiftLintPlugin */; + }; + B6080BBD2B20B05000B418EF /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = B6080BBC2B20B05000B418EF /* SwiftLintPlugin */; + }; + B6080BBF2B20B05300B418EF /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = B6080BBE2B20B05300B418EF /* SwiftLintPlugin */; + }; + B6080BC12B20B05600B418EF /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = B6080BC02B20B05600B418EF /* SwiftLintPlugin */; + }; F143C2EA1E4A4CD400CFDE3A /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = F143C2E31E4A4CD400CFDE3A /* Core */; @@ -9226,7 +9289,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 94.0.0; + version = 100.0.2; }; }; C14882EB27F211A000D59F0C /* XCRemoteSwiftPackageReference "SwiftSoup" */ = { @@ -9377,6 +9440,61 @@ package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = Bookmarks; }; + B6080BAC2B20B02400B418EF /* SwiftLintPlugin */ = { + isa = XCSwiftPackageProductDependency; + package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = "plugin:SwiftLintPlugin"; + }; + B6080BAE2B20B02800B418EF /* SwiftLintPlugin */ = { + isa = XCSwiftPackageProductDependency; + package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = "plugin:SwiftLintPlugin"; + }; + B6080BB02B20B02B00B418EF /* SwiftLintPlugin */ = { + isa = XCSwiftPackageProductDependency; + package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = "plugin:SwiftLintPlugin"; + }; + B6080BB22B20B03400B418EF /* SwiftLintPlugin */ = { + isa = XCSwiftPackageProductDependency; + package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = "plugin:SwiftLintPlugin"; + }; + B6080BB42B20B03800B418EF /* SwiftLintPlugin */ = { + isa = XCSwiftPackageProductDependency; + package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = "plugin:SwiftLintPlugin"; + }; + B6080BB62B20B03B00B418EF /* SwiftLintPlugin */ = { + isa = XCSwiftPackageProductDependency; + package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = "plugin:SwiftLintPlugin"; + }; + B6080BB82B20B04A00B418EF /* SwiftLintPlugin */ = { + isa = XCSwiftPackageProductDependency; + package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = "plugin:SwiftLintPlugin"; + }; + B6080BBA2B20B04D00B418EF /* SwiftLintPlugin */ = { + isa = XCSwiftPackageProductDependency; + package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = "plugin:SwiftLintPlugin"; + }; + B6080BBC2B20B05000B418EF /* SwiftLintPlugin */ = { + isa = XCSwiftPackageProductDependency; + package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = "plugin:SwiftLintPlugin"; + }; + B6080BBE2B20B05300B418EF /* SwiftLintPlugin */ = { + isa = XCSwiftPackageProductDependency; + package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = "plugin:SwiftLintPlugin"; + }; + B6080BC02B20B05600B418EF /* SwiftLintPlugin */ = { + isa = XCSwiftPackageProductDependency; + package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = "plugin:SwiftLintPlugin"; + }; C14882EC27F211A000D59F0C /* SwiftSoup */ = { isa = XCSwiftPackageProductDependency; package = C14882EB27F211A000D59F0C /* XCRemoteSwiftPackageReference "SwiftSoup" */; diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 1f616e1038..963d9334c6 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,178 +1,176 @@ { - "object": { - "pins": [ - { - "package": "BloomFilter", - "repositoryURL": "https://github.com/duckduckgo/bloom_cpp.git", - "state": { - "branch": null, - "revision": "8076199456290b61b4544bf2f4caf296759906a0", - "version": "3.0.0" - } - }, - { - "package": "BrowserServicesKit", - "repositoryURL": "https://github.com/DuckDuckGo/BrowserServicesKit", - "state": { - "branch": null, - "revision": "e4f4ae624174c1398d345cfc387db38f8f69986d", - "version": "94.0.0" - } - }, - { - "package": "CocoaAsyncSocket", - "repositoryURL": "https://github.com/robbiehanson/CocoaAsyncSocket", - "state": { - "branch": null, - "revision": "dbdc00669c1ced63b27c3c5f052ee4d28f10150c", - "version": "7.6.5" - } - }, - { - "package": "ContentScopeScripts", - "repositoryURL": "https://github.com/duckduckgo/content-scope-scripts", - "state": { - "branch": null, - "revision": "b7ad9843e70cede0c2ca9c4260d970f62cb28156", - "version": "4.52.0" - } - }, - { - "package": "DesignResourcesKit", - "repositoryURL": "https://github.com/duckduckgo/DesignResourcesKit", - "state": { - "branch": null, - "revision": "d7ea2561ec7624c224f52e1c9b349075ddf1c782", - "version": "2.0.0" - } - }, - { - "package": "Autofill", - "repositoryURL": "https://github.com/duckduckgo/duckduckgo-autofill.git", - "state": { - "branch": null, - "revision": "dbecae0df07650a21b5632a92fa2e498c96af7b5", - "version": "10.0.1" - } - }, - { - "package": "GRDB", - "repositoryURL": "https://github.com/duckduckgo/GRDB.swift.git", - "state": { - "branch": null, - "revision": "77d9a83191a74e319a5cfa27b0e3145d15607573", - "version": "2.2.0" - } - }, - { - "package": "FindInPageIOSJSSupport", - "repositoryURL": "https://github.com/duckduckgo/ios-js-support", - "state": { - "branch": null, - "revision": "6a6789ac8104a587316c58af75539753853b50d9", - "version": "2.0.0" - } - }, - { - "package": "Kingfisher", - "repositoryURL": "https://github.com/onevcat/Kingfisher.git", - "state": { - "branch": null, - "revision": "af4be924ad984cf4d16f4ae4df424e79a443d435", - "version": "7.6.2" - } - }, - { - "package": "Lottie", - "repositoryURL": "https://github.com/duckduckgo/lottie-ios.git", - "state": { - "branch": null, - "revision": "abf5510e261c85ffddd29de0bca9b72592ea2bdd", - "version": "3.3.0" - } - }, - { - "package": "OHHTTPStubs", - "repositoryURL": "https://github.com/AliSoftware/OHHTTPStubs.git", - "state": { - "branch": null, - "revision": "12f19662426d0434d6c330c6974d53e2eb10ecd9", - "version": "9.1.0" - } - }, - { - "package": "PrivacyDashboardResources", - "repositoryURL": "https://github.com/duckduckgo/privacy-dashboard", - "state": { - "branch": null, - "revision": "38336a574e13090764ba09a6b877d15ee514e371", - "version": "3.1.1" - } - }, - { - "package": "Punycode", - "repositoryURL": "https://github.com/gumob/PunycodeSwift.git", - "state": { - "branch": null, - "revision": "4356ec54e073741449640d3d50a1fd24fd1e1b8b", - "version": "2.1.0" - } - }, - { - "package": "swift-argument-parser", - "repositoryURL": "https://github.com/apple/swift-argument-parser", - "state": { - "branch": null, - "revision": "c8ed701b513cf5177118a175d85fbbbcd707ab41", - "version": "1.3.0" - } - }, - { - "package": "Swifter", - "repositoryURL": "https://github.com/httpswift/swifter.git", - "state": { - "branch": null, - "revision": "9483a5d459b45c3ffd059f7b55f9638e268632fd", - "version": "1.5.0" - } - }, - { - "package": "SwiftSoup", - "repositoryURL": "https://github.com/scinfu/SwiftSoup", - "state": { - "branch": null, - "revision": "41e7c263fb8c277e980ebcb9b0b5f6031d3d4886", - "version": "2.4.2" - } - }, - { - "package": "DDGSyncCrypto", - "repositoryURL": "https://github.com/duckduckgo/sync_crypto", - "state": { - "branch": null, - "revision": "2ab6ab6f0f96b259c14c2de3fc948935fc16ac78", - "version": "0.2.0" - } - }, - { - "package": "TrackerRadarKit", - "repositoryURL": "https://github.com/duckduckgo/TrackerRadarKit", - "state": { - "branch": null, - "revision": "a6b7ba151d9dc6684484f3785293875ec01cc1ff", - "version": "1.2.2" - } - }, - { - "package": "WireGuardKit", - "repositoryURL": "https://github.com/duckduckgo/wireguard-apple", - "state": { - "branch": null, - "revision": "2d8172c11478ab11b0f5ad49bdb4f93f4b3d5e0d", - "version": "1.1.1" - } - } - ] - }, - "version": 1 + "pins" : [ + { + "identity" : "bloom_cpp", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/bloom_cpp.git", + "state" : { + "revision" : "8076199456290b61b4544bf2f4caf296759906a0", + "version" : "3.0.0" + } + }, + { + "identity" : "browserserviceskit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", + "state" : { + "revision" : "39a5daf268f9fcb57d95e6193eca20f7ea222de6", + "version" : "100.0.2" + } + }, + { + "identity" : "cocoaasyncsocket", + "kind" : "remoteSourceControl", + "location" : "https://github.com/robbiehanson/CocoaAsyncSocket", + "state" : { + "revision" : "dbdc00669c1ced63b27c3c5f052ee4d28f10150c", + "version" : "7.6.5" + } + }, + { + "identity" : "content-scope-scripts", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/content-scope-scripts", + "state" : { + "revision" : "b7ad9843e70cede0c2ca9c4260d970f62cb28156", + "version" : "4.52.0" + } + }, + { + "identity" : "designresourceskit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/DesignResourcesKit", + "state" : { + "revision" : "d7ea2561ec7624c224f52e1c9b349075ddf1c782", + "version" : "2.0.0" + } + }, + { + "identity" : "duckduckgo-autofill", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/duckduckgo-autofill.git", + "state" : { + "revision" : "5597bc17709c8acf454ecaad4f4082007986242a", + "version" : "10.0.2" + } + }, + { + "identity" : "grdb.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/GRDB.swift.git", + "state" : { + "revision" : "77d9a83191a74e319a5cfa27b0e3145d15607573", + "version" : "2.2.0" + } + }, + { + "identity" : "ios-js-support", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/ios-js-support", + "state" : { + "revision" : "6a6789ac8104a587316c58af75539753853b50d9", + "version" : "2.0.0" + } + }, + { + "identity" : "kingfisher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/onevcat/Kingfisher.git", + "state" : { + "revision" : "af4be924ad984cf4d16f4ae4df424e79a443d435", + "version" : "7.6.2" + } + }, + { + "identity" : "lottie-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/lottie-ios.git", + "state" : { + "revision" : "abf5510e261c85ffddd29de0bca9b72592ea2bdd", + "version" : "3.3.0" + } + }, + { + "identity" : "ohhttpstubs", + "kind" : "remoteSourceControl", + "location" : "https://github.com/AliSoftware/OHHTTPStubs.git", + "state" : { + "revision" : "12f19662426d0434d6c330c6974d53e2eb10ecd9", + "version" : "9.1.0" + } + }, + { + "identity" : "privacy-dashboard", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/privacy-dashboard", + "state" : { + "revision" : "38336a574e13090764ba09a6b877d15ee514e371", + "version" : "3.1.1" + } + }, + { + "identity" : "punycodeswift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/gumob/PunycodeSwift.git", + "state" : { + "revision" : "4356ec54e073741449640d3d50a1fd24fd1e1b8b", + "version" : "2.1.0" + } + }, + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser", + "state" : { + "revision" : "c8ed701b513cf5177118a175d85fbbbcd707ab41", + "version" : "1.3.0" + } + }, + { + "identity" : "swifter", + "kind" : "remoteSourceControl", + "location" : "https://github.com/httpswift/swifter.git", + "state" : { + "revision" : "9483a5d459b45c3ffd059f7b55f9638e268632fd", + "version" : "1.5.0" + } + }, + { + "identity" : "swiftsoup", + "kind" : "remoteSourceControl", + "location" : "https://github.com/scinfu/SwiftSoup", + "state" : { + "revision" : "41e7c263fb8c277e980ebcb9b0b5f6031d3d4886", + "version" : "2.4.2" + } + }, + { + "identity" : "sync_crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/sync_crypto", + "state" : { + "revision" : "2ab6ab6f0f96b259c14c2de3fc948935fc16ac78", + "version" : "0.2.0" + } + }, + { + "identity" : "trackerradarkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/TrackerRadarKit", + "state" : { + "revision" : "a6b7ba151d9dc6684484f3785293875ec01cc1ff", + "version" : "1.2.2" + } + }, + { + "identity" : "wireguard-apple", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/wireguard-apple", + "state" : { + "revision" : "2d8172c11478ab11b0f5ad49bdb4f93f4b3d5e0d", + "version" : "1.1.1" + } + } + ], + "version" : 2 } diff --git a/DuckDuckGo/AppConfigurationFetch.swift b/DuckDuckGo/AppConfigurationFetch.swift index b40b56d92c..fcf1664d94 100644 --- a/DuckDuckGo/AppConfigurationFetch.swift +++ b/DuckDuckGo/AppConfigurationFetch.swift @@ -98,6 +98,7 @@ class AppConfigurationFetch { } func start(isBackgroundFetch: Bool = false, + isDebug: Bool = false, completion: AppConfigurationFetchCompletion?) { guard Self.shouldRefresh else { // Statistics are not sent after a successful background refresh in order to reduce the time spent in the background, so they are checked @@ -113,7 +114,7 @@ class AppConfigurationFetch { type(of: self).fetchQueue.async { let taskID = UIApplication.shared.beginBackgroundTask(withName: Constants.backgroundTaskName) - self.fetchConfigurationFiles(isBackground: isBackgroundFetch) { result in + self.fetchConfigurationFiles(isBackground: isBackgroundFetch, isDebug: isDebug) { result in if !isBackgroundFetch { type(of: self).fetchQueue.async { self.sendStatistics { @@ -165,10 +166,10 @@ class AppConfigurationFetch { #endif } - private func fetchConfigurationFiles(isBackground: Bool, onDidComplete: @escaping AppConfigurationFetchCompletion) { + private func fetchConfigurationFiles(isBackground: Bool, isDebug: Bool = false, onDidComplete: @escaping AppConfigurationFetchCompletion) { Task { self.markFetchStarted(isBackground: isBackground) - let result = await AppDependencyProvider.shared.configurationManager.update() + let result = await AppDependencyProvider.shared.configurationManager.update(isDebug: isDebug) switch result { case .noData: diff --git a/DuckDuckGo/AppDelegate+Waitlists.swift b/DuckDuckGo/AppDelegate+Waitlists.swift index d228ec680b..598131d836 100644 --- a/DuckDuckGo/AppDelegate+Waitlists.swift +++ b/DuckDuckGo/AppDelegate+Waitlists.swift @@ -43,20 +43,21 @@ extension AppDelegate { #if NETWORK_PROTECTION private func checkNetworkProtectionWaitlist() { - if AppDependencyProvider.shared.featureFlagger.isFeatureOn(.networkProtectionWaitlistAccess) { + let accessController = NetworkProtectionAccessController() + if accessController.isPotentialOrCurrentWaitlistUser { DailyPixel.fire(pixel: .networkProtectionWaitlistUserActive) } VPNWaitlist.shared.fetchInviteCodeIfAvailable { [weak self] error in guard error == nil else { #if !DEBUG - // If the user already has an invite code but their auth token has gone missing, attempt to redeem it again. - let tokenStore = NetworkProtectionKeychainTokenStore() - let waitlistStorage = VPNWaitlist.shared.waitlistStorage - if error == .alreadyHasInviteCode, - let inviteCode = waitlistStorage.getWaitlistInviteCode(), - !tokenStore.isFeatureActivated { - self?.fetchVPNWaitlistAuthToken(inviteCode: inviteCode) + if error == .alreadyHasInviteCode { + // If the user already has an invite code but their auth token has gone missing, attempt to redeem it again. + let tokenStore = NetworkProtectionKeychainTokenStore() + let waitlistStorage = VPNWaitlist.shared.waitlistStorage + if let inviteCode = waitlistStorage.getWaitlistInviteCode(), !tokenStore.isFeatureActivated { + self?.fetchVPNWaitlistAuthToken(inviteCode: inviteCode) + } } #endif return diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index a7b87702f1..036f91edd9 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -84,6 +84,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // MARK: lifecycle + @UserDefaultsWrapper(key: .privacyConfigCustomURL, defaultValue: nil) + private var privacyConfigCustomURL: String? + // swiftlint:disable:next function_body_length cyclomatic_complexity func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { @@ -105,7 +108,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate { cleanUpIncrementalRolloutPixelTest() APIRequest.Headers.setUserAgent(DefaultUserAgentManager.duckDuckGoUserAgent) - Configuration.setURLProvider(AppConfigurationURLProvider()) + + if isDebugBuild, let privacyConfigCustomURL, let url = URL(string: privacyConfigCustomURL) { + Configuration.setURLProvider(CustomConfigurationURLProvider(customPrivacyConfigurationURL: url)) + } else { + Configuration.setURLProvider(AppConfigurationURLProvider()) + } CrashCollection.start { Pixel.fire(pixel: .dbCrashDetected, withAdditionalParameters: $0, includedParameters: []) @@ -283,15 +291,25 @@ class AppDelegate: UIResponder, UIApplicationDelegate { favoritesDisplayModeStorage: FavoritesDisplayModeStorage() ) - let syncService = DDGSync(dataProvidersSource: syncDataProviders, errorEvents: SyncErrorHandler(), log: .syncLog, environment: environment) + let syncService = DDGSync( + dataProvidersSource: syncDataProviders, + errorEvents: SyncErrorHandler(), + privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager, + log: .syncLog, + environment: environment + ) syncService.initializeIfNeeded() self.syncService = syncService isSyncInProgressCancellable = syncService.isSyncInProgressPublisher .filter { $0 } - .prefix(1) - .sink { _ in - DailyPixel.fire(pixel: .syncDaily) + .sink { [weak syncService] _ in + DailyPixel.fire(pixel: .syncDaily, includedParameters: [.appVersion]) + syncService?.syncDailyStats.sendStatsIfNeeded(handler: { params in + Pixel.fire(pixel: .syncSuccessRateDaily, + withAdditionalParameters: params, + includedParameters: [.appVersion]) + }) } #if APP_TRACKING_PROTECTION diff --git a/DuckDuckGo/AppDependencyProvider.swift b/DuckDuckGo/AppDependencyProvider.swift index 5f7854281c..b23caebde0 100644 --- a/DuckDuckGo/AppDependencyProvider.swift +++ b/DuckDuckGo/AppDependencyProvider.swift @@ -46,7 +46,7 @@ class AppDependencyProvider: DependencyProvider { let appSettings: AppSettings = AppUserDefaults() let variantManager: VariantManager = DefaultVariantManager() - let internalUserDecider: InternalUserDecider = DefaultInternalUserDecider(store: InternalUserStore()) + let internalUserDecider: InternalUserDecider = ContentBlocking.shared.privacyConfigurationManager.internalUserDecider private lazy var privacyConfig: PrivacyConfiguration = ContentBlocking.shared.privacyConfigurationManager.privacyConfig lazy var featureFlagger: FeatureFlagger = DefaultFeatureFlagger(internalUserDecider: internalUserDecider, privacyConfig: privacyConfig) diff --git a/DuckDuckGo/Assets.xcassets/Card-24.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Card-24.imageset/Contents.json index 1c287f2d03..2c6b1d8921 100644 --- a/DuckDuckGo/Assets.xcassets/Card-24.imageset/Contents.json +++ b/DuckDuckGo/Assets.xcassets/Card-24.imageset/Contents.json @@ -8,5 +8,9 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" } } diff --git a/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/Rocket-24.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/Rocket-24.imageset/Contents.json index 2f63ce75ba..ab90683755 100644 --- a/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/Rocket-24.imageset/Contents.json +++ b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/Rocket-24.imageset/Contents.json @@ -8,5 +8,9 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" } } diff --git a/DuckDuckGo/AutocompleteRequest.swift b/DuckDuckGo/AutocompleteRequest.swift index 9b5770db39..ab17ef62f6 100644 --- a/DuckDuckGo/AutocompleteRequest.swift +++ b/DuckDuckGo/AutocompleteRequest.swift @@ -44,7 +44,7 @@ class AutocompleteRequest { var request = URLRequest.developerInitiated(url) request.allHTTPHeaderFields = APIRequest.Headers().httpHeaders - task = AutocompleteRequest.session.dataTask(with: request) { [weak self] (data, _, error) -> Void in + task = AutocompleteRequest.session.dataTask(with: request) { [weak self] (data, _, error) in guard let weakSelf = self else { return } do { let suggestions = try weakSelf.processResult(data: data, error: error) diff --git a/DuckDuckGo/Base.lproj/Settings.storyboard b/DuckDuckGo/Base.lproj/Settings.storyboard index 31f784d3cc..69c60efd61 100644 --- a/DuckDuckGo/Base.lproj/Settings.storyboard +++ b/DuckDuckGo/Base.lproj/Settings.storyboard @@ -1,5 +1,5 @@ - + @@ -10,12 +10,1106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + @@ -155,18 +1249,18 @@ - + - + - + @@ -174,14 +1268,32 @@ - + + + + + + + + + + + + + + + + + + + - + @@ -410,9 +1522,9 @@ - + - + @@ -475,9 +1587,9 @@ - + - + @@ -611,12 +1723,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + @@ -768,14 +1936,14 @@ - + - + - + @@ -868,14 +2036,14 @@ - + - + @@ -961,14 +2129,14 @@ - + - + - + @@ -1061,10 +2229,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1073,7 +2346,7 @@ - + diff --git a/DuckDuckGo/BlankSnapshotViewController.swift b/DuckDuckGo/BlankSnapshotViewController.swift index dd70e0ac01..5537036a85 100644 --- a/DuckDuckGo/BlankSnapshotViewController.swift +++ b/DuckDuckGo/BlankSnapshotViewController.swift @@ -127,6 +127,7 @@ class BlankSnapshotViewController: UIViewController { } extension BlankSnapshotViewController: OmniBarDelegate { + func onVoiceSearchPressed() { // No-op } @@ -142,7 +143,7 @@ extension BlankSnapshotViewController: OmniBarDelegate { func onSettingsPressed() { userInteractionDetected() } - + func onTextFieldDidBeginEditing(_ omniBar: OmniBar) -> Bool { DispatchQueue.main.async { self.viewCoordinator.omniBar.resignFirstResponder() diff --git a/DuckDuckGo/BookmarksViewController.swift b/DuckDuckGo/BookmarksViewController.swift index 20a03d1efa..b98a54e0e2 100644 --- a/DuckDuckGo/BookmarksViewController.swift +++ b/DuckDuckGo/BookmarksViewController.swift @@ -935,7 +935,6 @@ extension BookmarksViewController: AddOrEditBookmarkViewControllerDelegate { func showBookmarkDeletedMessage(_ bookmark: BookmarkEntity) { guard let parent = bookmark.parent, let index = parent.childrenArray.firstIndex(of: bookmark), - let domain = bookmark.urlObject?.host, let title = bookmark.title, let url = bookmark.url else { assertionFailure() @@ -962,9 +961,11 @@ extension BookmarksViewController: AddOrEditBookmarkViewControllerDelegate { self?.tableView.reloadData() self?.refreshAll() } onDidDismiss: { - NotificationCenter.default.post(name: FireproofFaviconUpdater.deleteFireproofFaviconNotification, - object: nil, - userInfo: [FireproofFaviconUpdater.UserInfoKeys.faviconDomain: domain]) + if let domain = bookmark.urlObject?.host { + NotificationCenter.default.post(name: FireproofFaviconUpdater.deleteFireproofFaviconNotification, + object: nil, + userInfo: [FireproofFaviconUpdater.UserInfoKeys.faviconDomain: domain]) + } } } diff --git a/DuckDuckGo/Configuration/ConfigurationManager.swift b/DuckDuckGo/Configuration/ConfigurationManager.swift index db9ab83618..f0a148adcd 100644 --- a/DuckDuckGo/Configuration/ConfigurationManager.swift +++ b/DuckDuckGo/Configuration/ConfigurationManager.swift @@ -66,8 +66,8 @@ struct ConfigurationManager { } } - func update() async -> UpdateResult { - async let didFetchAnyTrackerBlockingDependencies = fetchAndUpdateTrackerBlockingDependencies() + func update(isDebug: Bool = false) async -> UpdateResult { + async let didFetchAnyTrackerBlockingDependencies = fetchAndUpdateTrackerBlockingDependencies(isDebug: isDebug) async let didFetchExcludedDomains = fetchAndUpdateBloomFilterExcludedDomains() async let didFetchBloomFilter = fetchAndUpdateBloomFilter() @@ -80,21 +80,21 @@ struct ConfigurationManager { } @discardableResult - func fetchAndUpdateTrackerBlockingDependencies() async -> Bool { - let didFetchAnyTrackerBlockingDependencies = await fetchTrackerBlockingDependencies() + func fetchAndUpdateTrackerBlockingDependencies(isDebug: Bool = false) async -> Bool { + let didFetchAnyTrackerBlockingDependencies = await fetchTrackerBlockingDependencies(isDebug: isDebug) if didFetchAnyTrackerBlockingDependencies { updateTrackerBlockingDependencies() } return didFetchAnyTrackerBlockingDependencies } - private func fetchTrackerBlockingDependencies() async -> Bool { + private func fetchTrackerBlockingDependencies(isDebug: Bool = false) async -> Bool { var didFetchAnyTrackerBlockingDependencies = false var tasks = [Configuration: Task<(), Swift.Error>]() tasks[.trackerDataSet] = Task { try await fetcher.fetch(.trackerDataSet) } tasks[.surrogates] = Task { try await fetcher.fetch(.surrogates) } - tasks[.privacyConfiguration] = Task { try await fetcher.fetch(.privacyConfiguration) } + tasks[.privacyConfiguration] = Task { try await fetcher.fetch(.privacyConfiguration, isDebug: isDebug) } for (configuration, task) in tasks { do { diff --git a/DuckDuckGo/ConfigurationDebugViewController.swift b/DuckDuckGo/ConfigurationDebugViewController.swift index ef8aac9aad..15d7c25392 100644 --- a/DuckDuckGo/ConfigurationDebugViewController.swift +++ b/DuckDuckGo/ConfigurationDebugViewController.swift @@ -62,6 +62,22 @@ class ConfigurationDebugViewController: UITableViewController { } + private func fetchAssets() { + AppConfigurationFetch().start(isDebug: true) { [weak tableView] result in + switch result { + case .assetsUpdated(let protectionsUpdated): + if protectionsUpdated { + ContentBlocking.shared.contentBlockingManager.scheduleCompilation() + } + DispatchQueue.main.async { + tableView?.reloadData() + } + case .noData: + break + } + } + } + @UserDefaultsWrapper(key: .lastConfigurationRefreshDate, defaultValue: .distantPast) private var lastConfigurationRefreshDate: Date private var queuedTasks: [BGTaskRequest] = [] @@ -100,7 +116,6 @@ class ConfigurationDebugViewController: UITableViewController { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) switch Sections(rawValue: indexPath.section) { - case .refreshInformation: switch RefreshInformationRows(rawValue: indexPath.row) { case .lastRefreshDate: @@ -154,7 +169,6 @@ class ConfigurationDebugViewController: UITableViewController { } } - // swiftlint:disable:next cyclomatic_complexity override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { switch Sections(rawValue: indexPath.section) { case .refreshInformation: @@ -166,19 +180,7 @@ class ConfigurationDebugViewController: UITableViewController { let location = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: ContentBlockerStoreConstants.groupName) UIPasteboard.general.string = location?.path ?? "" case .forceRefresh: - AppConfigurationFetch().start { [weak tableView] result in - switch result { - case .assetsUpdated(let protectionsUpdated): - if protectionsUpdated { - ContentBlocking.shared.contentBlockingManager.scheduleCompilation() - } - DispatchQueue.main.async { - tableView?.reloadData() - } - case .noData: - break - } - } + fetchAssets() default: break } case .etags: diff --git a/DuckDuckGo/ConfigurationURLDebugViewController.swift b/DuckDuckGo/ConfigurationURLDebugViewController.swift new file mode 100644 index 0000000000..f508a40393 --- /dev/null +++ b/DuckDuckGo/ConfigurationURLDebugViewController.swift @@ -0,0 +1,214 @@ +// +// ConfigurationURLDebugViewController.swift +// DuckDuckGo +// +// 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 UIKit +import WebKit +import Core +import Configuration +import DesignResourcesKit + +final class ConfigurationURLDebugViewController: UITableViewController { + + enum Sections: Int, CaseIterable { + + case customURLs + + } + + enum CustomURLsRows: Int, CaseIterable { + + case privacyConfigURL + + var title: String { + switch self { + case .privacyConfigURL: return "Privacy Config" + } + } + + } + + private let dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeStyle = .long + return formatter + }() + + private var customURLProvider = CustomConfigurationURLProvider() + + @UserDefaultsWrapper(key: .lastConfigurationRefreshDate, defaultValue: .distantPast) + private var lastConfigurationRefreshDate: Date + + @UserDefaultsWrapper(key: .lastConfigurationUpdateDate, defaultValue: nil) + private var lastConfigurationUpdateDate: Date? + + @UserDefaultsWrapper(key: .privacyConfigCustomURL, defaultValue: nil) + private var privacyConfigCustomURL: String? { + didSet { + customURLProvider.customPrivacyConfigurationURL = privacyConfigCustomURL.flatMap { URL(string: $0) } + Configuration.setURLProvider(customURLProvider) + lastConfigurationRefreshDate = Date.distantPast + fetchAssets() + } + } + + private func customURL(for row: CustomURLsRows) -> String? { + switch row { + case .privacyConfigURL: return privacyConfigCustomURL + } + } + + private func url(for row: CustomURLsRows) -> String { + switch row { + case .privacyConfigURL: return customURL(for: row) ?? customURLProvider.url(for: .privacyConfiguration).absoluteString + } + } + + private func setCustomURL(_ urlString: String?, for row: CustomURLsRows) { + switch row { + case .privacyConfigURL: privacyConfigCustomURL = urlString + } + } + + private func fetchAssets() { + AppConfigurationFetch().start(isDebug: true) { [weak tableView] result in + switch result { + case .assetsUpdated(let protectionsUpdated): + if protectionsUpdated { + ContentBlocking.shared.contentBlockingManager.scheduleCompilation() + DispatchQueue.main.async { + self.lastConfigurationUpdateDate = Date() + } + } + DispatchQueue.main.async { + tableView?.reloadData() + } + + case .noData: + break + } + } + } + + override func numberOfSections(in tableView: UITableView) -> Int { + Sections.allCases.count + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + switch Sections(rawValue: section) { + case .customURLs: return CustomURLsRows.allCases.count + case nil: return 0 + } + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let row = CustomURLsRows(rawValue: indexPath.row)! + guard let cell = + tableView.dequeueReusableCell(withIdentifier: ConfigurationURLTableViewCell.reuseIdentifier) as? ConfigurationURLTableViewCell else { + fatalError("Failed to dequeue cell") + } + cell.title.text = row.title + cell.subtitle.text = url(for: row) + cell.subtitle.textColor = customURL(for: row) != nil ? UIColor(designSystemColor: .accent) : .black + cell.ternary.text = lastConfigurationUpdateDate != nil ? dateFormatter.string(from: lastConfigurationUpdateDate!) : "-" + cell.refresh.addAction(makeAction(for: row), for: .allEvents) + return cell + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let row = CustomURLsRows(rawValue: indexPath.row)! + presentCustomURLAlert(for: row) + } + + private func makeAction(for row: CustomURLsRows) -> UIAction { + UIAction { [weak self] _ in + self?.lastConfigurationRefreshDate = Date.distantPast + self?.fetchAssets() + self?.tableView.reloadData() + } + } + + private func presentCustomURLAlert(for row: CustomURLsRows) { + let alert = UIAlertController(title: row.title, message: "Provide custom URL", preferredStyle: .alert) + alert.addTextField { textField in + textField.tag = row.rawValue + } + let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { _ in + self.tableView.reloadData() + } + alert.addAction(cancelAction) + + if customURL(for: row) != nil { + let resetToDefaultAction = UIAlertAction(title: "Reset to default URL", style: .default) { _ in + self.privacyConfigCustomURL = nil + } + alert.addAction(resetToDefaultAction) + } + + let submitAction = UIAlertAction(title: "Override", style: .default) { _ in + self.setCustomURL(alert.textFields?.first?.text, for: row) + self.tableView.reloadData() + } + alert.addAction(submitAction) + let cell = self.tableView.cellForRow(at: IndexPath(row: row.rawValue, + section: Sections.customURLs.rawValue))! + present(controller: alert, fromView: cell) + } + +} + +struct CustomConfigurationURLProvider: ConfigurationURLProviding { + + var customBloomFilterSpecURL: URL? + var customBloomFilterBinaryURL: URL? + var customBloomFilterExcludedDomainsURL: URL? + var customPrivacyConfigurationURL: URL? + var customTrackerDataSetURL: URL? + var customSurrogatesURL: URL? + var customFBConfigURL: URL? + + let defaultProvider = AppConfigurationURLProvider() + + func url(for configuration: Configuration) -> URL { + let defaultURL = defaultProvider.url(for: configuration) + let customURL: URL? + switch configuration { + case .bloomFilterSpec: customURL = customBloomFilterSpecURL + case .bloomFilterBinary: customURL = customBloomFilterBinaryURL + case .bloomFilterExcludedDomains: customURL = customBloomFilterExcludedDomainsURL + case .privacyConfiguration: customURL = customPrivacyConfigurationURL + case .trackerDataSet: customURL = customTrackerDataSetURL + case .surrogates: customURL = customSurrogatesURL + case .FBConfig: customURL = nil + } + return customURL ?? defaultURL + } + +} + +final class ConfigurationURLTableViewCell: UITableViewCell { + + static let reuseIdentifier = "ConfigurationURLTableViewCell" + + @IBOutlet weak var title: UILabel! + @IBOutlet weak var subtitle: UILabel! + @IBOutlet weak var refresh: UIButton! + @IBOutlet weak var ternary: UILabel! + +} diff --git a/DuckDuckGo/Debug.storyboard b/DuckDuckGo/Debug.storyboard index 8e770932b8..2a19b573f7 100644 --- a/DuckDuckGo/Debug.storyboard +++ b/DuckDuckGo/Debug.storyboard @@ -1,9 +1,10 @@ - + - + + @@ -60,9 +61,29 @@ - + + + + + + + + + + + + + + + @@ -81,7 +102,7 @@ - + @@ -101,7 +122,7 @@ - + @@ -121,7 +142,7 @@ - + @@ -141,7 +162,7 @@ - + @@ -161,7 +182,7 @@ - + @@ -181,7 +202,7 @@ - + @@ -201,7 +222,7 @@ - + @@ -210,7 +231,7 @@ - + @@ -219,7 +240,7 @@ - + @@ -228,7 +249,7 @@ - + @@ -237,7 +258,7 @@ - + @@ -246,7 +267,7 @@ - + @@ -799,9 +820,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DuckDuckGo/EventMapping+NetworkProtectionError.swift b/DuckDuckGo/EventMapping+NetworkProtectionError.swift index cefbd105ec..a139b4c9a2 100644 --- a/DuckDuckGo/EventMapping+NetworkProtectionError.swift +++ b/DuckDuckGo/EventMapping+NetworkProtectionError.swift @@ -88,7 +88,8 @@ extension EventMapping where Event == NetworkProtectionError { .wireGuardInvalidState, .wireGuardDnsResolution, .wireGuardSetNetworkSettings, - .startWireGuardBackend: + .startWireGuardBackend, + .failedToRetrieveAuthToken: pixelEvent = .networkProtectionUnhandledError params[PixelParameters.function] = #function params[PixelParameters.line] = String(#line) diff --git a/DuckDuckGo/FaviconsFetcherOnboarding.swift b/DuckDuckGo/FaviconsFetcherOnboarding.swift index ec1418f2a7..1f87dd7c82 100644 --- a/DuckDuckGo/FaviconsFetcherOnboarding.swift +++ b/DuckDuckGo/FaviconsFetcherOnboarding.swift @@ -60,7 +60,8 @@ final class FaviconsFetcherOnboarding { } private var shouldPresentOnboarding: Bool { - !didPresentFaviconsFetchingOnboarding + syncService.featureFlags.contains(.userInterface) + && !didPresentFaviconsFetchingOnboarding && !syncBookmarksAdapter.isFaviconsFetchingEnabled && syncBookmarksAdapter.isEligibleForFaviconsFetcherOnboarding } diff --git a/DuckDuckGo/ForgetDataAlert.swift b/DuckDuckGo/ForgetDataAlert.swift index 0e7d4cf07f..52704c3922 100644 --- a/DuckDuckGo/ForgetDataAlert.swift +++ b/DuckDuckGo/ForgetDataAlert.swift @@ -31,13 +31,18 @@ class ForgetDataAlert { let forgetTabsAndDataAction = UIAlertAction(title: UserText.actionForgetAll, style: .destructive) { _ in forgetTabsAndDataHandler() } - + + forgetTabsAndDataAction.accessibilityIdentifier = "alert.forget-data.confirm" + let cancelAction = UIAlertAction(title: UserText.actionCancel, style: .cancel) { _ in cancelHandler?() } + cancelAction.accessibilityIdentifier = "alert.forget-data.cancel" + alert.addAction(forgetTabsAndDataAction) alert.addAction(cancelAction) + return alert } diff --git a/DuckDuckGo/MainViewController+Segues.swift b/DuckDuckGo/MainViewController+Segues.swift index a6e773d0e8..3dd12b6bc7 100644 --- a/DuckDuckGo/MainViewController+Segues.swift +++ b/DuckDuckGo/MainViewController+Segues.swift @@ -201,6 +201,12 @@ extension MainViewController { launchSettings() } + func segueToDebugSettings() { + os_log(#function, log: .generalLog, type: .debug) + hideAllHighlightsIfNeeded() + launchDebugSettings() + } + func segueToSettingsCookiePopupManagement() { os_log(#function, log: .generalLog, type: .debug) hideAllHighlightsIfNeeded() @@ -253,6 +259,24 @@ extension MainViewController { } } + private func launchDebugSettings(completion: ((RootDebugViewController) -> Void)? = nil) { + os_log(#function, log: .generalLog, type: .debug) + + let storyboard = UIStoryboard(name: "Debug", bundle: nil) + let settings = storyboard.instantiateViewController(identifier: "DebugMenu") { coder in + RootDebugViewController(coder: coder, + sync: self.syncService, + bookmarksDatabase: self.bookmarksDatabase, + internalUserDecider: AppDependencyProvider.shared.internalUserDecider) + } + + let controller = ThemableNavigationController(rootViewController: settings) + controller.modalPresentationStyle = .automatic + present(controller, animated: true) { + completion?(settings) + } + } + private func hideAllHighlightsIfNeeded() { os_log(#function, log: .generalLog, type: .debug) if !DaxDialogs.shared.shouldShowFireButtonPulse { diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index 48a7385548..d3c1e96aa6 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -89,11 +89,18 @@ class MainViewController: UIViewController { private var favoritesViewModel: FavoritesListInteracting let syncService: DDGSyncing let syncDataProviders: SyncDataProviders + + @UserDefaultsWrapper(key: .syncDidShowSyncPausedByFeatureFlagAlert, defaultValue: false) + private var syncDidShowSyncPausedByFeatureFlagAlert: Bool + private var localUpdatesCancellable: AnyCancellable? private var syncUpdatesCancellable: AnyCancellable? + private var syncFeatureFlagsCancellable: AnyCancellable? private var favoritesDisplayModeCancellable: AnyCancellable? private var emailCancellables = Set() + private lazy var featureFlagger = AppDependencyProvider.shared.featureFlagger + lazy var menuBookmarksViewModel: MenuBookmarksInteracting = { let viewModel = MenuBookmarksViewModel(bookmarksDatabase: bookmarksDatabase, syncService: syncService) viewModel.favoritesDisplayMode = appSettings.favoritesDisplayMode @@ -349,6 +356,21 @@ class MainViewController: UIViewController { selector: #selector(showSyncPausedError), name: SyncCredentialsAdapter.credentialsSyncLimitReached, object: nil) + syncFeatureFlagsCancellable = syncService.featureFlagsPublisher + .dropFirst() + .map { $0.contains(.dataSyncing) } + .receive(on: DispatchQueue.main) + .sink { [weak self] isDataSyncingAvailable in + guard let self else { + return + } + if isDataSyncingAvailable { + self.syncDidShowSyncPausedByFeatureFlagAlert = false + } else if self.syncService.authState == .active, !self.syncDidShowSyncPausedByFeatureFlagAlert { + self.showSyncPausedByFeatureFlagAlert() + self.syncDidShowSyncPausedByFeatureFlagAlert = true + } + } } @objc private func showSyncPausedError(_ notification: Notification) { @@ -378,6 +400,26 @@ class MainViewController: UIViewController { } } + private func showSyncPausedByFeatureFlagAlert(upgradeRequired: Bool = false) { + let title = UserText.syncPausedTitle + let description = upgradeRequired ? UserText.syncUnavailableMessageUpgradeRequired : UserText.syncUnavailableMessage + if self.presentedViewController is SyncSettingsViewController { + return + } + self.presentedViewController?.dismiss(animated: true) + let alert = UIAlertController(title: title, + message: description, + preferredStyle: .alert) + if syncService.featureFlags.contains(.userInterface) { + let learnMoreAction = UIAlertAction(title: UserText.syncPausedAlertLearnMoreButton, style: .default) { _ in + self.segueToSettingsSync() + } + alert.addAction(learnMoreAction) + } + alert.addAction(UIAlertAction(title: UserText.syncPausedAlertOkButton, style: .cancel)) + self.present(alert, animated: true) + } + func registerForSettingsChangeNotifications() { NotificationCenter.default.addObserver(self, selector: #selector(onAddressBarPositionChanged), @@ -1419,7 +1461,15 @@ extension MainViewController: OmniBarDelegate { } segueToSettings() } - + + func onSettingsLongPressed() { + if featureFlagger.isFeatureOn(.debugMenu) || isDebugBuild { + segueToDebugSettings() + } else { + segueToSettings() + } + } + func onCancelPressed() { dismissOmniBar() hideSuggestionTray() @@ -1466,7 +1516,15 @@ extension MainViewController: OmniBarDelegate { guard let link = currentTab?.link else { return } currentTab?.onShareAction(forLink: link, fromView: viewCoordinator.omniBar.shareButton) } - + + func onShareLongPressed() { + if featureFlagger.isFeatureOn(.debugMenu) || isDebugBuild { + segueToDebugSettings() + } else { + onSharePressed() + } + } + func onVoiceSearchPressed() { SpeechRecognizer.requestMicAccess { permission in if permission { diff --git a/DuckDuckGo/NetworkProtectionAccessController.swift b/DuckDuckGo/NetworkProtectionAccessController.swift index 16da156180..289addbfa4 100644 --- a/DuckDuckGo/NetworkProtectionAccessController.swift +++ b/DuckDuckGo/NetworkProtectionAccessController.swift @@ -70,6 +70,14 @@ struct NetworkProtectionAccessController: NetworkProtectionAccess { return (regionCode ?? "US") == "US" } + var isPotentialOrCurrentWaitlistUser: Bool { + switch self.networkProtectionAccessType() { + case .none, .inviteCodeInvited: + return false + case .waitlistAvailable, .waitlistJoined, .waitlistInvitedPendingTermsAcceptance, .waitlistInvited: + return true + } + } init( networkProtectionActivation: NetworkProtectionFeatureActivation = NetworkProtectionKeychainTokenStore(), @@ -130,14 +138,18 @@ struct NetworkProtectionAccessController: NetworkProtectionAccess { } if !featureFlagger.isFeatureOn(.networkProtectionWaitlistActive) { - networkProtectionWaitlistStorage.deleteWaitlistState() - try? NetworkProtectionKeychainTokenStore().deleteToken() + revokeNetworkProtectionAccess() + } + } - Task { - let controller = NetworkProtectionTunnelController() - await controller.stop() - await controller.removeVPN() - } + func revokeNetworkProtectionAccess() { + networkProtectionWaitlistStorage.deleteWaitlistState() + try? NetworkProtectionKeychainTokenStore().deleteToken() + + Task { + let controller = NetworkProtectionTunnelController() + await controller.stop() + await controller.removeVPN() } } diff --git a/DuckDuckGo/NetworkProtectionDebugViewController.swift b/DuckDuckGo/NetworkProtectionDebugViewController.swift index eb6e272c9f..e50c45e17f 100644 --- a/DuckDuckGo/NetworkProtectionDebugViewController.swift +++ b/DuckDuckGo/NetworkProtectionDebugViewController.swift @@ -37,7 +37,7 @@ import NetworkProtection // swiftlint:disable:next type_body_length final class NetworkProtectionDebugViewController: UITableViewController { private let titles = [ - Sections.keychain: "Keychain", + Sections.clearData: "Clear Data", Sections.debugFeature: "Debug Features", Sections.simulateFailure: "Simulate Failure", Sections.registrationKey: "Registration Key", @@ -49,7 +49,7 @@ final class NetworkProtectionDebugViewController: UITableViewController { ] enum Sections: Int, CaseIterable { - case keychain + case clearData case debugFeature case simulateFailure case registrationKey @@ -59,9 +59,10 @@ final class NetworkProtectionDebugViewController: UITableViewController { case vpnConfiguration } - enum KeychainRows: Int, CaseIterable { + enum ClearDataRows: Int, CaseIterable { case clearAuthToken + case clearAllVPNData } @@ -162,10 +163,12 @@ final class NetworkProtectionDebugViewController: UITableViewController { switch Sections(rawValue: indexPath.section) { - case .keychain: - switch KeychainRows(rawValue: indexPath.row) { + case .clearData: + switch ClearDataRows(rawValue: indexPath.row) { case .clearAuthToken: - cell.textLabel?.text = "Clear auth token" + cell.textLabel?.text = "Clear Auth Token" + case .clearAllVPNData: + cell.textLabel?.text = "Reset VPN State Completely" case .none: break } @@ -200,7 +203,7 @@ final class NetworkProtectionDebugViewController: UITableViewController { override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { switch Sections(rawValue: section) { - case .keychain: return KeychainRows.allCases.count + case .clearData: return ClearDataRows.allCases.count case .debugFeature: return DebugFeatureRows.allCases.count case .simulateFailure: return SimulateFailureRows.allCases.count case .registrationKey: return RegistrationKeyRows.allCases.count @@ -216,9 +219,10 @@ final class NetworkProtectionDebugViewController: UITableViewController { // swiftlint:disable:next cyclomatic_complexity override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { switch Sections(rawValue: indexPath.section) { - case .keychain: - switch KeychainRows(rawValue: indexPath.row) { + case .clearData: + switch ClearDataRows(rawValue: indexPath.row) { case .clearAuthToken: clearAuthToken() + case .clearAllVPNData: clearAllVPNData() default: break } case .debugFeature: @@ -542,6 +546,12 @@ final class NetworkProtectionDebugViewController: UITableViewController { private func clearAuthToken() { try? tokenStore.deleteToken() } + + private func clearAllVPNData() { + let accessController = NetworkProtectionAccessController() + accessController.revokeNetworkProtectionAccess() + } + } diff --git a/DuckDuckGo/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtectionTunnelController.swift index 2a1d1b6f8c..595b0afa13 100644 --- a/DuckDuckGo/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtectionTunnelController.swift @@ -132,6 +132,12 @@ final class NetworkProtectionTunnelController: TunnelController { do { try tunnelManager.connection.startVPNTunnel(options: options) + UniquePixel.fire(pixel: .networkProtectionNewUser) { error in + guard error != nil else { return } + VPNSettings(defaults: .networkProtectionGroupDefaults).vpnFirstEnabled = Pixel.Event.networkProtectionNewUser.lastFireDate( + uniquePixelStorage: UniquePixel.storage + ) + } } catch { Pixel.fire(pixel: .networkProtectionActivationRequestFailed, error: error) throw error diff --git a/DuckDuckGo/OmniBar.swift b/DuckDuckGo/OmniBar.swift index 9498074044..1322736fa8 100644 --- a/DuckDuckGo/OmniBar.swift +++ b/DuckDuckGo/OmniBar.swift @@ -92,6 +92,8 @@ class OmniBar: UIView { super.awakeFromNib() configureMenuButton() configureTextField() + configureSettingsLongPressButton() + configureShareLongPressButton() registerNotifications() configureSeparator() @@ -102,7 +104,31 @@ class OmniBar: UIView { privacyInfoContainer.isHidden = true } - + + private func configureSettingsLongPressButton() { + let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleSettingsLongPress(_:))) + longPressGesture.minimumPressDuration = 0.7 + settingsButton.addGestureRecognizer(longPressGesture) + } + + private func configureShareLongPressButton() { + let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleShareLongPress(_:))) + longPressGesture.minimumPressDuration = 0.7 + shareButton.addGestureRecognizer(longPressGesture) + } + + @objc private func handleSettingsLongPress(_ gesture: UILongPressGestureRecognizer) { + if gesture.state == .began { + omniDelegate?.onSettingsLongPressed() + } + } + + @objc private func handleShareLongPress(_ gesture: UILongPressGestureRecognizer) { + if gesture.state == .began { + omniDelegate?.onShareLongPressed() + } + } + private func registerNotifications() { NotificationCenter.default.addObserver(self, selector: #selector(textDidChange), @@ -449,7 +475,7 @@ class OmniBar: UIView { Pixel.fire(pixel: .addressBarSettings) omniDelegate?.onSettingsPressed() } - + @IBAction func onCancelPressed(_ sender: Any) { omniDelegate?.onCancelPressed() } diff --git a/DuckDuckGo/OmniBarDelegate.swift b/DuckDuckGo/OmniBarDelegate.swift index 293031d026..de8a977cef 100644 --- a/DuckDuckGo/OmniBarDelegate.swift +++ b/DuckDuckGo/OmniBarDelegate.swift @@ -32,11 +32,13 @@ protocol OmniBarDelegate: AnyObject { func onPrivacyIconPressed() func onMenuPressed() - + func onBookmarksPressed() func onSettingsPressed() - + + func onSettingsLongPressed() + func onCancelPressed() func onEnterPressed() @@ -48,7 +50,9 @@ protocol OmniBarDelegate: AnyObject { func onForwardPressed() func onSharePressed() - + + func onShareLongPressed() + func onTextFieldWillBeginEditing(_ omniBar: OmniBar) // Returns whether field should select the text or not @@ -81,7 +85,11 @@ extension OmniBarDelegate { func onMenuPressed() { } - + + func onShareLongPressed() { + + } + func onBookmarksPressed() { } @@ -89,7 +97,11 @@ extension OmniBarDelegate { func onSettingsPressed() { } - + + func onSettingsLongPressed() { + + } + func onCancelPressed() { } diff --git a/DuckDuckGo/Settings.bundle/Root.plist b/DuckDuckGo/Settings.bundle/Root.plist index 8180e96bfe..e87115da14 100644 --- a/DuckDuckGo/Settings.bundle/Root.plist +++ b/DuckDuckGo/Settings.bundle/Root.plist @@ -6,7 +6,7 @@ DefaultValue - 7.101.0 + 7.102.0 Key version Title diff --git a/DuckDuckGo/SettingsViewController.swift b/DuckDuckGo/SettingsViewController.swift new file mode 100644 index 0000000000..be726a9d4b --- /dev/null +++ b/DuckDuckGo/SettingsViewController.swift @@ -0,0 +1,709 @@ +// +// SettingsViewController.swift +// DuckDuckGo +// +// Copyright © 2017 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 UIKit +import Core +import BrowserServicesKit +import Persistence +import SwiftUI +import Common +import DDGSync +import Combine + +#if APP_TRACKING_PROTECTION +import NetworkExtension +#endif + +#if NETWORK_PROTECTION +import NetworkProtection +#endif + +// swiftlint:disable file_length type_body_length +class SettingsViewController: UITableViewController { + + @IBOutlet weak var defaultBrowserCell: UITableViewCell! + @IBOutlet weak var themeAccessoryText: UILabel! + @IBOutlet weak var fireButtonAnimationAccessoryText: UILabel! + @IBOutlet weak var addressBarPositionCell: UITableViewCell! + @IBOutlet weak var addressBarPositionAccessoryText: UILabel! + @IBOutlet weak var appIconCell: UITableViewCell! + @IBOutlet weak var appIconImageView: UIImageView! + @IBOutlet weak var autocompleteToggle: UISwitch! + @IBOutlet weak var authenticationToggle: UISwitch! + @IBOutlet weak var autoClearAccessoryText: UILabel! + @IBOutlet weak var versionText: UILabel! + @IBOutlet weak var openUniversalLinksToggle: UISwitch! + @IBOutlet weak var longPressPreviewsToggle: UISwitch! + @IBOutlet weak var rememberLoginsCell: UITableViewCell! + @IBOutlet weak var rememberLoginsAccessoryText: UILabel! + @IBOutlet weak var doNotSellCell: UITableViewCell! + @IBOutlet weak var doNotSellAccessoryText: UILabel! + @IBOutlet weak var autoconsentCell: UITableViewCell! + @IBOutlet weak var autoconsentAccessoryText: UILabel! + @IBOutlet weak var emailProtectionCell: UITableViewCell! + @IBOutlet weak var emailProtectionAccessoryText: UILabel! + @IBOutlet weak var macBrowserWaitlistCell: UITableViewCell! + @IBOutlet weak var macBrowserWaitlistAccessoryText: UILabel! + @IBOutlet weak var windowsBrowserWaitlistCell: UITableViewCell! + @IBOutlet weak var windowsBrowserWaitlistAccessoryText: UILabel! + @IBOutlet weak var netPCell: UITableViewCell! + @IBOutlet weak var longPressCell: UITableViewCell! + @IBOutlet weak var versionCell: UITableViewCell! + @IBOutlet weak var textSizeCell: UITableViewCell! + @IBOutlet weak var textSizeAccessoryText: UILabel! + @IBOutlet weak var widgetEducationCell: UITableViewCell! + @IBOutlet weak var syncCell: UITableViewCell! + @IBOutlet weak var autofillCell: UITableViewCell! + @IBOutlet weak var debugCell: UITableViewCell! + @IBOutlet weak var voiceSearchCell: UITableViewCell! + @IBOutlet weak var voiceSearchToggle: UISwitch! + + @IBOutlet var labels: [UILabel]! + @IBOutlet var accessoryLabels: [UILabel]! + + private let syncSectionIndex = 1 + private let autofillSectionIndex = 2 + private let appearanceSectionIndex = 3 + private let moreFromDDGSectionIndex = 6 + private let debugSectionIndex = 8 + + private let bookmarksDatabase: CoreDataDatabase + + private lazy var emailManager = EmailManager() + + private lazy var versionProvider: AppVersion = AppVersion.shared + fileprivate lazy var privacyStore = PrivacyUserDefaults() + fileprivate lazy var appSettings = AppDependencyProvider.shared.appSettings + fileprivate lazy var variantManager = AppDependencyProvider.shared.variantManager + fileprivate lazy var featureFlagger = AppDependencyProvider.shared.featureFlagger + fileprivate let syncService: DDGSyncing + fileprivate let syncDataProviders: SyncDataProviders + fileprivate let internalUserDecider: InternalUserDecider +#if NETWORK_PROTECTION + private let connectionObserver = ConnectionStatusObserverThroughSession() +#endif + private var cancellables: Set = [] + + private var shouldShowDebugCell: Bool { + return featureFlagger.isFeatureOn(.debugMenu) || isDebugBuild + } + + private var shouldShowVoiceSearchCell: Bool { + AppDependencyProvider.shared.voiceSearchHelper.isSpeechRecognizerAvailable + } + + private var shouldShowAutofillCell: Bool { + return featureFlagger.isFeatureOn(.autofillAccessCredentialManagement) + } + + private var shouldShowSyncCell: Bool { + return syncService.featureFlags.contains(.userInterface) + } + + private var shouldShowTextSizeCell: Bool { + return UIDevice.current.userInterfaceIdiom != .pad + } + + private var shouldShowAddressBarPositionCell: Bool { + return UIDevice.current.userInterfaceIdiom != .pad + } + + private lazy var shouldShowNetPCell: Bool = { +#if NETWORK_PROTECTION + if #available(iOS 15, *) { + let accessController = NetworkProtectionAccessController() + return accessController.networkProtectionAccessType() != .none + } else { + return false + } +#else + return false +#endif + }() + + override func viewDidLoad() { + super.viewDidLoad() + + configureAutofillCell() + configureSyncCell() + configureThemeCellAccessory() + configureFireButtonAnimationCellAccessory() + configureAddressBarPositionCell() + configureTextSizeCell() + configureDisableAutocompleteToggle() + configureSecurityToggles() + configureVersionText() + configureUniversalLinksToggle() + configureLinkPreviewsToggle() + configureRememberLogins() + configureDebugCell() + configureVoiceSearchCell() + configureNetPCell() + applyTheme(ThemeManager.shared.currentTheme) + + internalUserDecider.isInternalUserPublisher.dropFirst().sink(receiveValue: { [weak self] _ in + self?.configureAutofillCell() + self?.configureSyncCell() + self?.configureDebugCell() + self?.tableView.reloadData() + + // Scroll to force-redraw section headers and footers + self?.tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: false) + }) + .store(in: &cancellables) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + configureFireButtonAnimationCellAccessory() + configureAddressBarPositionCell() + configureTextSizeCell() + configureAutoClearCellAccessory() + configureRememberLogins() + configureDoNotSell() + configureAutoconsent() + configureIconViews() + configureEmailProtectionAccessoryText() + configureMacBrowserWaitlistCell() + configureWindowsBrowserWaitlistCell() + configureSyncCell() + +#if NETWORK_PROTECTION + updateNetPCellSubtitle(connectionStatus: connectionObserver.recentValue) +#endif + + // Make sure multiline labels are correctly presented + tableView.setNeedsLayout() + tableView.layoutIfNeeded() + } + + init?(coder: NSCoder, + bookmarksDatabase: CoreDataDatabase, + syncService: DDGSyncing, + syncDataProviders: SyncDataProviders, + internalUserDecider: InternalUserDecider) { + + self.bookmarksDatabase = bookmarksDatabase + self.syncService = syncService + self.syncDataProviders = syncDataProviders + self.internalUserDecider = internalUserDecider + super.init(coder: coder) + } + + required init?(coder: NSCoder) { + fatalError("Not implemented") + } + + func openLogins() { + showAutofill() + } + + func openLogins(accountDetails: SecureVaultModels.WebsiteAccount) { + showAutofillAccountDetails(accountDetails) + } + + func openCookiePopupManagement() { + showCookiePopupManagement(animated: true) + } + + @IBSegueAction func onCreateRootDebugScreen(_ coder: NSCoder, sender: Any?, segueIdentifier: String?) -> RootDebugViewController { + guard let controller = RootDebugViewController(coder: coder, + sync: syncService, + bookmarksDatabase: bookmarksDatabase, + internalUserDecider: AppDependencyProvider.shared.internalUserDecider) else { + fatalError("Failed to create controller") + } + + return controller + } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + if segue.destination is DoNotSellSettingsViewController { + Pixel.fire(pixel: .settingsDoNotSellShown) + return + } else if segue.destination is AutoconsentSettingsViewController { + Pixel.fire(pixel: .settingsAutoconsentShown) + return + } else if let textSizeSettings = segue.destination as? TextSizeSettingsViewController { + Pixel.fire(pixel: .textSizeSettingsShown) + presentationController?.delegate = textSizeSettings + return + } + + if let navController = segue.destination as? UINavigationController, navController.topViewController is FeedbackViewController { + if UIDevice.current.userInterfaceIdiom == .pad { + segue.destination.modalPresentationStyle = .formSheet + } + } + } + + private func configureAutofillCell() { + autofillCell.isHidden = !shouldShowAutofillCell + } + + private func configureSyncCell() { + syncCell.textLabel?.text = "Sync & Backup" + let isDataSyncingDisabled = !syncService.featureFlags.contains(.dataSyncing) && syncService.authState == .active + if SyncBookmarksAdapter.isSyncBookmarksPaused || SyncCredentialsAdapter.isSyncCredentialsPaused || isDataSyncingDisabled { + syncCell.textLabel?.text = "⚠️ " + "Sync & Backup" + } + syncCell.isHidden = !shouldShowSyncCell + } + + private func configureVoiceSearchCell() { + voiceSearchCell.isHidden = !shouldShowVoiceSearchCell + voiceSearchToggle.isOn = appSettings.voiceSearchEnabled + } + + private func configureThemeCellAccessory() { + switch appSettings.currentThemeName { + case .systemDefault: + themeAccessoryText.text = UserText.themeAccessoryDefault + case .light: + themeAccessoryText.text = UserText.themeAccessoryLight + case .dark: + themeAccessoryText.text = UserText.themeAccessoryDark + } + } + + private func configureFireButtonAnimationCellAccessory() { + fireButtonAnimationAccessoryText.text = appSettings.currentFireButtonAnimation.descriptionText + } + + private func configureAddressBarPositionCell() { + addressBarPositionCell.isHidden = !shouldShowAddressBarPositionCell + addressBarPositionAccessoryText.text = appSettings.currentAddressBarPosition.descriptionText + } + + private func configureTextSizeCell() { + textSizeCell.isHidden = !shouldShowTextSizeCell + textSizeAccessoryText.text = "\(appSettings.textSize)%" + } + + private func configureIconViews() { + if AppIconManager.shared.isAppIconChangeSupported { + appIconImageView.image = AppIconManager.shared.appIcon.smallImage + } else { + appIconCell.isHidden = true + } + } + + private func configureDisableAutocompleteToggle() { + autocompleteToggle.isOn = appSettings.autocomplete + } + + private func configureSecurityToggles() { + authenticationToggle.isOn = privacyStore.authenticationEnabled + } + + private func configureAutoClearCellAccessory() { + if AutoClearSettingsModel(settings: appSettings) != nil { + autoClearAccessoryText.text = UserText.autoClearAccessoryOn + } else { + autoClearAccessoryText.text = UserText.autoClearAccessoryOff + } + } + + private func configureDoNotSell() { + doNotSellAccessoryText.text = appSettings.sendDoNotSell ? UserText.doNotSellEnabled : UserText.doNotSellDisabled + } + + private func configureAutoconsent() { + autoconsentAccessoryText.text = appSettings.autoconsentEnabled ? UserText.autoconsentEnabled : UserText.autoconsentDisabled + } + + private func configureRememberLogins() { + rememberLoginsAccessoryText.text = PreserveLogins.shared.allowedDomains.isEmpty ? "" : "\(PreserveLogins.shared.allowedDomains.count)" + } + + private func configureVersionText() { + versionText.text = versionProvider.versionAndBuildNumber + } + + private func configureUniversalLinksToggle() { + openUniversalLinksToggle.isOn = appSettings.allowUniversalLinks + } + + private func configureLinkPreviewsToggle() { + longPressCell.isHidden = false + longPressPreviewsToggle.isOn = appSettings.longPressPreviews + } + + private func configureMacBrowserWaitlistCell() { + macBrowserWaitlistCell.detailTextLabel?.text = MacBrowserWaitlist.shared.settingsSubtitle + } + + private func configureWindowsBrowserWaitlistCell() { + windowsBrowserWaitlistCell.isHidden = !WindowsBrowserWaitlist.shared.isAvailable + + if WindowsBrowserWaitlist.shared.isAvailable { + windowsBrowserWaitlistCell.detailTextLabel?.text = WindowsBrowserWaitlist.shared.settingsSubtitle + } + } + + private func configureNetPCell() { + netPCell.isHidden = !shouldShowNetPCell +#if NETWORK_PROTECTION + updateNetPCellSubtitle(connectionStatus: connectionObserver.recentValue) + connectionObserver.publisher + .receive(on: DispatchQueue.main) + .sink { [weak self] status in + self?.updateNetPCellSubtitle(connectionStatus: status) + } + .store(in: &cancellables) +#endif + } + +#if NETWORK_PROTECTION + private func updateNetPCellSubtitle(connectionStatus: ConnectionStatus) { + switch NetworkProtectionAccessController().networkProtectionAccessType() { + case .none, .waitlistAvailable, .waitlistJoined, .waitlistInvitedPendingTermsAcceptance: + netPCell.detailTextLabel?.text = VPNWaitlist.shared.settingsSubtitle + case .waitlistInvited, .inviteCodeInvited: + switch connectionStatus { + case .connected: netPCell.detailTextLabel?.text = UserText.netPCellConnected + default: netPCell.detailTextLabel?.text = UserText.netPCellDisconnected + } + } + } +#endif + + private func configureDebugCell() { + debugCell.isHidden = !shouldShowDebugCell + } + + func showSync(animated: Bool = true) { + let controller = SyncSettingsViewController(syncService: syncService, syncBookmarksAdapter: syncDataProviders.bookmarksAdapter) + navigationController?.pushViewController(controller, animated: animated) + } + + private func showAutofill(animated: Bool = true) { + let autofillController = AutofillLoginSettingsListViewController( + appSettings: appSettings, + syncService: syncService, + syncDataProviders: syncDataProviders + ) + autofillController.delegate = self + Pixel.fire(pixel: .autofillSettingsOpened) + navigationController?.pushViewController(autofillController, animated: animated) + } + + func showAutofillAccountDetails(_ account: SecureVaultModels.WebsiteAccount) { + let autofillController = AutofillLoginSettingsListViewController( + appSettings: appSettings, + syncService: syncService, + syncDataProviders: syncDataProviders + ) + autofillController.delegate = self + let detailsController = autofillController.makeAccountDetailsScreen(account) + + var controllers = navigationController?.viewControllers ?? [] + controllers.append(autofillController) + controllers.append(detailsController) + navigationController?.viewControllers = controllers + } + + private func configureEmailProtectionAccessoryText() { + if let userEmail = emailManager.userEmail { + emailProtectionAccessoryText.text = userEmail + } else { + emailProtectionAccessoryText.text = UserText.emailSettingsSubtitle + } + } + + private func showEmailWebDashboard() { + UIApplication.shared.open(URL.emailProtectionQuickLink, options: [:], completionHandler: nil) + } + + private func showMacBrowserWaitlistViewController() { + navigationController?.pushViewController(MacWaitlistViewController(nibName: nil, bundle: nil), animated: true) + } + +#if NETWORK_PROTECTION + @available(iOS 15, *) + private func showNetP() { + switch NetworkProtectionAccessController().networkProtectionAccessType() { + case .inviteCodeInvited, .waitlistInvited: + // This will be tidied up as part of https://app.asana.com/0/0/1205084446087078/f + let rootViewController = NetworkProtectionRootViewController { [weak self] in + self?.navigationController?.popViewController(animated: true) + let newRootViewController = NetworkProtectionRootViewController() + self?.pushNetP(newRootViewController) + } + + pushNetP(rootViewController) + default: + navigationController?.pushViewController(VPNWaitlistViewController(nibName: nil, bundle: nil), animated: true) + } + } + + @available(iOS 15, *) + private func pushNetP(_ rootViewController: NetworkProtectionRootViewController) { + navigationController?.pushViewController( + rootViewController, + animated: true + ) + } +#endif + + private func showWindowsBrowserWaitlistViewController() { + navigationController?.pushViewController(WindowsWaitlistViewController(nibName: nil, bundle: nil), animated: true) + } + + func showCookiePopupManagement(animated: Bool = true) { + navigationController?.pushViewController(AutoconsentSettingsViewController.loadFromStoryboard(), animated: animated) + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + + let cell = tableView.cellForRow(at: indexPath) + + switch cell { + + case defaultBrowserCell: + Pixel.fire(pixel: .defaultBrowserButtonPressedSettings) + guard let url = URL(string: UIApplication.openSettingsURLString) else { return } + UIApplication.shared.open(url) + + case emailProtectionCell: + showEmailWebDashboard() + + case macBrowserWaitlistCell: + showMacBrowserWaitlistViewController() + + case windowsBrowserWaitlistCell: + showWindowsBrowserWaitlistViewController() + + case autofillCell: + showAutofill() + + case syncCell: + showSync() + + case netPCell: + if #available(iOS 15, *) { +#if NETWORK_PROTECTION + showNetP() +#else + break +#endif + } + default: break + } + + } + + override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { + let theme = ThemeManager.shared.currentTheme + cell.backgroundColor = theme.tableCellBackgroundColor + + if cell == netPCell { + DailyPixel.fire(pixel: .networkProtectionSettingsRowDisplayed) + } + } + + override func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection: Int) { + if let view = view as? UITableViewHeaderFooterView { + let theme = ThemeManager.shared.currentTheme + view.textLabel?.textColor = theme.tableHeaderTextColor + } + } + + override func tableView(_ tableView: UITableView, willDisplayFooterView view: UIView, forSection: Int) { + if let view = view as? UITableViewHeaderFooterView { + let theme = ThemeManager.shared.currentTheme + view.textLabel?.textColor = theme.tableHeaderTextColor + } + } + + override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + let cell = super.tableView(tableView, cellForRowAt: indexPath) + return cell.isHidden ? 0 : UITableView.automaticDimension + } + + override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { + return UITableView.automaticDimension + } + + /// Only use this to hide the header if the entire section can be conditionally hidden. + override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + if syncSectionIndex == section && !shouldShowSyncCell { + return CGFloat.leastNonzeroMagnitude + } else if autofillSectionIndex == section && !shouldShowAutofillCell { + return CGFloat.leastNonzeroMagnitude + } else if debugSectionIndex == section && !shouldShowDebugCell { + return CGFloat.leastNonzeroMagnitude + } else { + return super.tableView(tableView, heightForHeaderInSection: section) + } + } + + /// Only use this to hide the footer if the entire section can be conditionally hidden. + override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { + if syncSectionIndex == section && !shouldShowSyncCell { + return CGFloat.leastNonzeroMagnitude + } else if autofillSectionIndex == section && !shouldShowAutofillCell { + return CGFloat.leastNonzeroMagnitude + } else if debugSectionIndex == section && !shouldShowDebugCell { + return CGFloat.leastNonzeroMagnitude + } else { + return super.tableView(tableView, heightForFooterInSection: section) + } + } + + /// Only use this if the *last cell* in the section is to be conditionally hidden in order to retain the section rounding. + /// If your cell is not the last you don't need to modify the number of rows. + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + let rows = super.tableView(tableView, numberOfRowsInSection: section) + if section == moreFromDDGSectionIndex && !shouldShowNetPCell { + return rows - 1 + } else if section == appearanceSectionIndex && UIDevice.current.userInterfaceIdiom == .pad { + // Both the text size and bottom bar settings are at the end of the section so need to reduce the section size appropriately + return rows - 2 + } else { + return rows + } + } + + @IBAction func onVoiceSearchToggled(_ sender: UISwitch) { + var enableVoiceSearch = sender.isOn + let isFirstTimeAskingForPermission = SpeechRecognizer.recordPermission == .undetermined + + SpeechRecognizer.requestMicAccess { permission in + if !permission { + enableVoiceSearch = false + sender.setOn(false, animated: true) + if !isFirstTimeAskingForPermission { + self.showNoMicrophonePermissionAlert() + } + } + + AppDependencyProvider.shared.voiceSearchHelper.enableVoiceSearch(enableVoiceSearch) + } + } + + @IBAction func onAboutTapped() { + navigationController?.pushViewController(AboutViewController(), animated: true) + } + + private func showNoMicrophonePermissionAlert() { + let alertController = NoMicPermissionAlert.buildAlert() + present(alertController, animated: true, completion: nil) + } + + @IBAction func onAuthenticationToggled(_ sender: UISwitch) { + privacyStore.authenticationEnabled = sender.isOn + } + + @IBAction func onDonePressed(_ sender: Any) { + dismiss(animated: true, completion: nil) + } + + @IBAction func onAutocompleteToggled(_ sender: UISwitch) { + appSettings.autocomplete = sender.isOn + } + + @IBAction func onAllowUniversalLinksToggled(_ sender: UISwitch) { + appSettings.allowUniversalLinks = sender.isOn + } + + @IBAction func onLinkPreviewsToggle(_ sender: UISwitch) { + appSettings.longPressPreviews = sender.isOn + } +} + +extension SettingsViewController: Themable { + + func decorate(with theme: Theme) { + view.backgroundColor = theme.backgroundColor + + decorateNavigationBar(with: theme) + configureThemeCellAccessory() + + for label in labels { + label.textColor = theme.tableCellTextColor + } + + for label in accessoryLabels { + label.textColor = theme.tableCellAccessoryTextColor + } + + versionText.textColor = theme.tableCellTextColor + + autocompleteToggle.onTintColor = theme.buttonTintColor + authenticationToggle.onTintColor = theme.buttonTintColor + openUniversalLinksToggle.onTintColor = theme.buttonTintColor + longPressPreviewsToggle.onTintColor = theme.buttonTintColor + voiceSearchToggle.onTintColor = theme.buttonTintColor + + tableView.backgroundColor = theme.backgroundColor + tableView.separatorColor = theme.tableCellSeparatorColor + + UIView.transition(with: view, + duration: 0.2, + options: .transitionCrossDissolve, animations: { + self.tableView.reloadData() + }, completion: nil) + } +} + +extension SettingsViewController { + static var fontSizeForHeaderView: CGFloat { + let contentSize = UIApplication.shared.preferredContentSizeCategory + switch contentSize { + case .extraSmall: + return 12 + case .small: + return 12 + case .medium: + return 12 + case .large: + return 13 + case .extraLarge: + return 15 + case .extraExtraLarge: + return 17 + case .extraExtraExtraLarge: + return 19 + case .accessibilityMedium: + return 23 + case .accessibilityLarge: + return 27 + case .accessibilityExtraLarge: + return 33 + case .accessibilityExtraExtraLarge: + return 38 + case .accessibilityExtraExtraExtraLarge: + return 44 + default: + return 13 + } + } +} + +// MARK: - AutofillLoginSettingsListViewControllerDelegate + +extension SettingsViewController: AutofillLoginSettingsListViewControllerDelegate { + func autofillLoginSettingsListViewControllerDidFinish(_ controller: AutofillLoginSettingsListViewController) { + navigationController?.popViewController(animated: true) + } +} +// swiftlint:enable file_length type_body_length diff --git a/DuckDuckGo/SyncSettingsViewController+SyncDelegate.swift b/DuckDuckGo/SyncSettingsViewController+SyncDelegate.swift index 981cfe0a9e..fca50e7b02 100644 --- a/DuckDuckGo/SyncSettingsViewController+SyncDelegate.swift +++ b/DuckDuckGo/SyncSettingsViewController+SyncDelegate.swift @@ -45,7 +45,7 @@ extension SyncSettingsViewController: SyncManagementViewModelDelegate { let devices = try await syncService.updateDeviceName(name) mapDevices(devices) } catch { - handleError(error) + handleError(SyncError.unableToUpdateDeviceName, error: error) } } } @@ -56,20 +56,40 @@ extension SyncSettingsViewController: SyncManagementViewModelDelegate { self.dismissPresentedViewController() self.showPreparingSync() try await syncService.createAccount(deviceName: deviceName, deviceType: deviceType) - Pixel.fire(pixel: .syncSignupDirect) + Pixel.fire(pixel: .syncSignupDirect, includedParameters: [.appVersion]) self.rootView.model.syncEnabled(recoveryCode: recoveryCode) self.refreshDevices() navigationController?.topViewController?.dismiss(animated: true, completion: showRecoveryPDF) } catch { - handleError(error) + handleError(SyncError.unableToSyncToServer, error: error) } } } @MainActor - func handleError(_ error: Error) { - // Work out how to handle this properly later - assertionFailure(error.localizedDescription) + func handleError(_ type: SyncError, error: Error?) { + let alertController = UIAlertController( + title: type.title, + message: [type.description, error?.localizedDescription].compactMap({ $0 }).joined(separator: "\n"), + preferredStyle: .alert) + + let okAction = UIAlertAction(title: UserText.syncPausedAlertOkButton, style: .default, handler: nil) + alertController.addAction(okAction) + + if type == .unableToSyncToServer || + type == .unableToSyncWithDevice || + type == .unableToMergeTwoAccounts { + // Gives time to the is syncing view to appear + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + self.dismissPresentedViewController { [weak self] in + self?.present(alertController, animated: true, completion: nil) + } + } + } else { + self.dismissPresentedViewController { [weak self] in + self?.present(alertController, animated: true, completion: nil) + } + } } func showSyncWithAnotherDevice() { @@ -158,14 +178,15 @@ extension SyncSettingsViewController: SyncManagementViewModelDelegate { alert.addAction(title: UserText.syncTurnOffConfirmAction, style: .destructive) { Task { @MainActor in do { - self.rootView.model.isSyncEnabled = false try await self.syncService.disconnect() + self.rootView.model.isSyncEnabled = false AppUserDefaults().isSyncBookmarksPaused = false AppUserDefaults().isSyncCredentialsPaused = false + continuation.resume(returning: true) } catch { - self.handleError(error) + self.handleError(SyncError.unableToTurnSyncOff, error: error) + continuation.resume(returning: false) } - continuation.resume(returning: true) } } self.present(alert, animated: true) @@ -183,14 +204,15 @@ extension SyncSettingsViewController: SyncManagementViewModelDelegate { alert.addAction(title: UserText.syncDeleteAllConfirmAction, style: .destructive) { Task { @MainActor in do { - self.rootView.model.isSyncEnabled = false try await self.syncService.deleteAccount() + self.rootView.model.isSyncEnabled = false AppUserDefaults().isSyncBookmarksPaused = false AppUserDefaults().isSyncCredentialsPaused = false + continuation.resume(returning: true) } catch { - self.handleError(error) + self.handleError(SyncError.unableToDeleteData, error: error) + continuation.resume(returning: false) } - continuation.resume(returning: true) } } self.present(alert, animated: true) @@ -224,7 +246,7 @@ extension SyncSettingsViewController: SyncManagementViewModelDelegate { try await syncService.disconnect(deviceId: device.id) refreshDevices() } catch { - handleError(error) + handleError(SyncError.unableToRemoveDevice, error: error) } } } @@ -261,3 +283,39 @@ private class PortraitNavigationController: UINavigationController { } } + +enum SyncError { + case unableToSyncToServer + case unableToSyncWithDevice + case unableToMergeTwoAccounts + case unableToUpdateDeviceName + case unableToTurnSyncOff + case unableToDeleteData + case unableToRemoveDevice + case unableToCreateRecoveryPdf + + var title: String { + return UserText.syncErrorAlertTitle + } + + var description: String { + switch self { + case .unableToSyncToServer: + return UserText.unableToSyncToServerDescription + case .unableToSyncWithDevice: + return UserText.unableToSyncWithOtherDeviceDescription + case .unableToMergeTwoAccounts: + return UserText.unableToMergeTwoAccountsErrorDescription + case .unableToUpdateDeviceName: + return UserText.unableToUpdateDeviceNameDescription + case .unableToTurnSyncOff: + return UserText.unableToTurnSyncOffDescription + case .unableToDeleteData: + return UserText.unableToDeleteDataDescription + case .unableToRemoveDevice: + return UserText.unableToRemoveDeviceDescription + case .unableToCreateRecoveryPdf: + return UserText.unableToCreateRecoveryPDF + } + } +} diff --git a/DuckDuckGo/SyncSettingsViewController.swift b/DuckDuckGo/SyncSettingsViewController.swift index 2fec903a79..86cb9f8b95 100644 --- a/DuckDuckGo/SyncSettingsViewController.swift +++ b/DuckDuckGo/SyncSettingsViewController.swift @@ -22,6 +22,7 @@ import Core import Combine import SyncUI import DDGSync +import Common @MainActor class SyncSettingsViewController: UIHostingController { @@ -55,13 +56,20 @@ class SyncSettingsViewController: UIHostingController { self.syncService = syncService self.syncBookmarksAdapter = syncBookmarksAdapter - let viewModel = SyncSettingsViewModel() + let viewModel = SyncSettingsViewModel( + isOnDevEnvironment: { syncService.serverEnvironment == .development }, + switchToProdEnvironment: { + syncService.updateServerEnvironment(.production) + UserDefaults.standard.set(ServerEnvironment.production.description, forKey: UserDefaultsWrapper.Key.syncEnvironment.rawValue) + } + ) super.init(rootView: SyncSettingsView(model: viewModel)) setUpFaviconsFetcherSwitch(viewModel) setUpFavoritesDisplayModeSwitch(viewModel, appSettings) setUpSyncPaused(viewModel, appSettings) + setUpSyncFeatureFlags(viewModel) refreshForState(syncService.authState) syncService.authStatePublisher @@ -80,6 +88,19 @@ class SyncSettingsViewController: UIHostingController { fatalError("init(coder:) has not been implemented") } + private func setUpSyncFeatureFlags(_ viewModel: SyncSettingsViewModel) { + syncService.featureFlagsPublisher.prepend(syncService.featureFlags) + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { featureFlags in + viewModel.isDataSyncingAvailable = featureFlags.contains(.dataSyncing) + viewModel.isConnectingDevicesAvailable = featureFlags.contains(.connectFlows) + viewModel.isAccountCreationAvailable = featureFlags.contains(.accountCreation) + viewModel.isAccountRecoveryAvailable = featureFlags.contains(.accountRecovery) + } + .store(in: &cancellables) + } + private func setUpFaviconsFetcherSwitch(_ viewModel: SyncSettingsViewModel) { viewModel.isFaviconsFetchingEnabled = syncBookmarksAdapter.isFaviconsFetchingEnabled @@ -171,10 +192,13 @@ class SyncSettingsViewController: UIHostingController { } } - func dismissPresentedViewController() { + func dismissPresentedViewController(completion: (() -> Void)? = nil) { guard let presentedViewController = navigationController?.presentedViewController, - !(presentedViewController is UIHostingController) else { return } - presentedViewController.dismiss(animated: true, completion: nil) + !(presentedViewController is UIHostingController) else { + completion?() + return + } + presentedViewController.dismiss(animated: true, completion: completion) endConnectMode() } @@ -190,7 +214,8 @@ class SyncSettingsViewController: UIHostingController { let devices = try await syncService.fetchDevices() mapDevices(devices) } catch { - handleError(error) + // Not displaying error since there is the spinner and it is called every few seconds + os_log(error.localizedDescription, log: .syncLog, type: .error) } } } @@ -223,7 +248,7 @@ extension SyncSettingsViewController: ScanOrPasteCodeViewModelDelegate { self.startPolling() return self.connector?.code } catch { - self.handleError(error) + self.handleError(SyncError.unableToSyncToServer, error: error) return nil } } @@ -231,7 +256,7 @@ extension SyncSettingsViewController: ScanOrPasteCodeViewModelDelegate { func loginAndShowDeviceConnected(recoveryKey: SyncCode.RecoveryKey) async throws { let registeredDevices = try await syncService.login(recoveryKey, deviceName: deviceName, deviceType: deviceType) mapDevices(registeredDevices) - Pixel.fire(pixel: .syncLogin) + Pixel.fire(pixel: .syncLogin, includedParameters: [.appVersion]) DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { self.dismissVCAndShowRecoveryPDF() } @@ -249,34 +274,45 @@ extension SyncSettingsViewController: ScanOrPasteCodeViewModelDelegate { return } } catch { - handleError(error) + handleError(SyncError.unableToSyncWithDevice, error: error) } } } func syncCodeEntered(code: String) async -> Bool { var shouldShowSyncEnabled = true - do { - guard let syncCode = try? SyncCode.decodeBase64String(code) else { - return false - } - if let recoveryKey = syncCode.recovery { - dismissPresentedViewController() - showPreparingSync() + guard let syncCode = try? SyncCode.decodeBase64String(code) else { + return false + } + if let recoveryKey = syncCode.recovery { + dismissPresentedViewController() + showPreparingSync() + do { try await loginAndShowDeviceConnected(recoveryKey: recoveryKey) return true - } else if let connectKey = syncCode.connect { - dismissPresentedViewController() - showPreparingSync() - if syncService.account == nil { + } catch { + if self.rootView.model.isSyncEnabled { + handleError(.unableToMergeTwoAccounts, error: nil) + } else { + handleError(.unableToSyncToServer, error: error) + } + } + } else if let connectKey = syncCode.connect { + dismissPresentedViewController() + showPreparingSync() + if syncService.account == nil { + do { try await syncService.createAccount(deviceName: deviceName, deviceType: deviceType) - Pixel.fire(pixel: .syncSignupConnect) + Pixel.fire(pixel: .syncSignupConnect, includedParameters: [.appVersion]) self.dismissVCAndShowRecoveryPDF() shouldShowSyncEnabled = false rootView.model.syncEnabled(recoveryCode: recoveryCode) + } catch { + handleError(.unableToSyncToServer, error: error) } + } + do { try await syncService.transmitRecoveryKey(connectKey) - self.rootView.model.$devices .removeDuplicates() .dropFirst() @@ -287,12 +323,11 @@ extension SyncSettingsViewController: ScanOrPasteCodeViewModelDelegate { self.dismissVCAndShowRecoveryPDF() } }.store(in: &cancellables) - - return true + } catch { + handleError(.unableToSyncWithDevice, error: error) } - } catch { - handleError(error) + return true } return false } diff --git a/DuckDuckGo/TabViewController.swift b/DuckDuckGo/TabViewController.swift index 9d1348546e..05f59ce06b 100644 --- a/DuckDuckGo/TabViewController.swift +++ b/DuckDuckGo/TabViewController.swift @@ -35,6 +35,10 @@ import TrackerRadarKit import Networking import SecureStorage +#if NETWORK_PROTECTION +import NetworkProtection +#endif + // swiftlint:disable file_length // swiftlint:disable type_body_length class TabViewController: UIViewController { @@ -116,6 +120,12 @@ class TabViewController: UIViewController { private var trackersInfoWorkItem: DispatchWorkItem? +#if NETWORK_PROTECTION + private let netPConnectionObserver = ConnectionStatusObserverThroughSession() + private var netPConnectionObserverCancellable: AnyCancellable? + private var netPConnectionStatus: ConnectionStatus = .default +#endif + // Required to know when to disable autofill, see SaveLoginViewModel for details // Stored in memory on TabViewController for privacy reasons private var domainSaveLoginPromptLastShownOn: String? @@ -306,6 +316,10 @@ class TabViewController: UIViewController { if #available(iOS 16.4, *) { registerForInspectableWebViewNotifications() } + +#if NETWORK_PROTECTION + observeNetPConnectionStatusChanges() +#endif } @available(iOS 16.4, *) @@ -325,6 +339,12 @@ class TabViewController: UIViewController { #endif } + private func observeNetPConnectionStatusChanges() { + netPConnectionObserverCancellable = netPConnectionObserver.publisher + .receive(on: DispatchQueue.main) + .assign(to: \.netPConnectionStatus, onWeaklyHeld: self) + } + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) // The email manager is pulled from the main view controller, so reconnect it now, otherwise, it's nil @@ -406,16 +426,16 @@ class TabViewController: UIViewController { if consumeCookies { consumeCookiesThenLoadRequest(request) - } else if let url = request?.url { + } else if let urlRequest = request { var loadingStopped = false - linkProtection.getCleanURL(from: url, onStartExtracting: { [weak self] in + linkProtection.getCleanURLRequest(from: urlRequest, onStartExtracting: { [weak self] in if loadingInitiatedByParentTab { // stop parent-initiated URL loading only if canonical URL extraction process has started loadingStopped = true self?.webView.stopLoading() } self?.showProgressIndicator() - }, onFinishExtracting: {}, completion: { [weak self] cleanURL in + }, onFinishExtracting: {}, completion: { [weak self] cleanURLRequest in // restart the cleaned-up URL loading here if: // link protection provided an updated URL // OR if loading was stopped for a popup loaded by its parent @@ -423,8 +443,8 @@ class TabViewController: UIViewController { // the check is here to let an (about:blank) popup which has its loading // initiated by its parent to keep its active request, otherwise we would // break a js-initiated popup request such as printing from a popup - guard url != cleanURL || loadingStopped || !loadingInitiatedByParentTab else { return } - self?.load(urlRequest: .userInitiated(cleanURL)) + guard self?.url != cleanURLRequest.url || loadingStopped || !loadingInitiatedByParentTab else { return } + self?.load(urlRequest: cleanURLRequest) }) } @@ -468,7 +488,7 @@ class TabViewController: UIViewController { webView.evaluateJavaScript(js) } } - + public func load(url: URL) { webView.stopLoading() dismissJSAlertIfNeeded() @@ -531,6 +551,7 @@ class TabViewController: UIViewController { } webView.stopLoading() + dismissJSAlertIfNeeded() webView.load(urlRequest) } @@ -1104,6 +1125,12 @@ extension TabViewController: WKNavigationDelegate { linkProtection.setMainFrameUrl(nil) referrerTrimming.onFinishNavigation() urlProvidedBasicAuthCredential = nil + +#if NETWORK_PROTECTION + if webView.url?.isDuckDuckGoSearch == true, case .connected = netPConnectionStatus { + DailyPixel.fireDailyAndCount(pixel: .networkProtectionEnabledOnSearch) + } +#endif } func preparePreview(completion: @escaping (UIImage?) -> Void) { @@ -1325,16 +1352,9 @@ extension TabViewController: WKNavigationDelegate { navigationAction: navigationAction, onStartExtracting: { showProgressIndicator() }, onFinishExtracting: { }, - onLinkRewrite: { [weak self] newURL, navigationAction in + onLinkRewrite: { [weak self] newRequest, _ in guard let self = self else { return } - if self.isNewTargetBlankRequest(navigationAction: navigationAction) { - self.delegate?.tab(self, - didRequestNewTabForUrl: newURL, - openedByPage: true, - inheritingAttribution: self.adClickAttributionLogic.state) - } else { - self.load(url: newURL) - } + self.load(urlRequest: newRequest) }, policyDecisionHandler: decisionHandler) diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index fbcf8e7daa..3420aacaa3 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -385,10 +385,10 @@ public struct UserText { public static let emptyDownloads = NSLocalizedString("downloads.downloads-list.empty", value: "No files downloaded yet", comment: "Empty downloads list placholder") public static let autofillSaveLoginTitleNewUser = NSLocalizedString("autofill.save-login.new-user.title", value: "Do you want DuckDuckGo to save your password?", comment: "Title displayed on modal asking for the user to save the login for the first time") - public static let autofillSaveLoginTitle = NSLocalizedString("autofill.save-login.title", value: "Save Login?", comment: "Title displayed on modal asking for the user to save the login") + public static let autofillSaveLoginTitle = NSLocalizedString("autofill.save-login.title", value: "Save Password?", comment: "Title displayed on modal asking for the user to save the login") public static let autofillUpdateUsernameTitle = NSLocalizedString("autofill.update-usernamr.title", value: "Update username?", comment: "Title displayed on modal asking for the user to update the username") - - public static let autofillSaveLoginMessageNewUser = NSLocalizedString("autofill.save-login.new-user.message", value: "Passwords are stored securely on your device in the Logins menu.", comment: "Message displayed on modal asking for the user to save the login for the first time") + + public static let autofillSaveLoginMessageNewUser = NSLocalizedString("autofill.save-login.new-user.message", value: "Passwords are stored securely on your device.", comment: "Message displayed on modal asking for the user to save the login for the first time") public static let autofillSaveLoginNotNowCTA = NSLocalizedString("autofill.save-login.not-now.CTA", value: "Don’t Save", comment: "Cancel CTA displayed on modal asking for the user to save the login") public static let autofillSaveLoginNeverPromptCTA = NSLocalizedString("autofill.save-login.never-prompt.CTA", value:"Never Ask for This Site", comment: "CTA displayed on modal asking if the user never wants to be prompted to save a login for this website agin") @@ -403,17 +403,17 @@ public struct UserText { public static let autofillShowPassword = NSLocalizedString("autofill.show-password", value: "Show Password", comment: "Accessibility title for a Show Password button displaying actial password instead of *****") public static let autofillHidePassword = NSLocalizedString("autofill.hide-password", value: "Hide Password", comment: "Accessibility title for a Hide Password button replacing displayed password with *****") public static let autofillUpdateUsernameSaveCTA = NSLocalizedString("autofill.update-username.save.CTA", value: "Update Username", comment: "Confirm CTA displayed on modal asking for the user to update the login") - public static let autofillLoginSavedToastMessage = NSLocalizedString("autofill.login-saved.toast", value: "Login saved", comment: "Message displayed after saving an autofill login") - public static let autofillLoginUpdatedToastMessage = NSLocalizedString("autofill.login-updated.toast", value: "Login updated", comment: "Message displayed after updating an autofill login") + public static let autofillLoginSavedToastMessage = NSLocalizedString("autofill.login-saved.toast", value: "Password saved", comment: "Message displayed after saving an autofill login") + public static let autofillLoginUpdatedToastMessage = NSLocalizedString("autofill.login-updated.toast", value: "Password updated", comment: "Message displayed after updating an autofill login") public static let autofillLoginSaveToastActionButton = NSLocalizedString("autofill.login-save-action-button.toast", value: "View", comment: "Button displayed after saving/updating an autofill login that takes the user to the saved login") - - public static let autofillKeepEnabledAlertTitle = NSLocalizedString("autofill.keep-enabled.alert.title", value: "Do you want to keep saving Logins?", comment: "Title for alert when asking the user if they want to keep using autofill") + + public static let autofillKeepEnabledAlertTitle = NSLocalizedString("autofill.keep-enabled.alert.title", value: "Do you want to keep saving passwords?", comment: "Title for alert when asking the user if they want to keep using autofill") public static let autofillKeepEnabledAlertMessage = NSLocalizedString("autofill.keep-enabled.alert.message", value: "You can disable this at any time in Settings.", comment: "Message for alert when asking the user if they want to keep using autofill") public static let autofillKeepEnabledAlertKeepUsingAction = NSLocalizedString("autofill.keep-enabled.alert.keep-using", value: "Keep Saving", comment: "Confirm action for alert when asking the user if they want to keep using autofill") public static let autofillKeepEnabledAlertDisableAction = NSLocalizedString("autofill.keep-enabled.alert.disable", value: "Disable", comment: "Disable action for alert when asking the user if they want to keep using autofill") - - public static let actionAutofillLogins = NSLocalizedString("action.title.autofill.logins", value: "Logins", comment: "Autofill Logins menu item opening the login list") - + + public static let actionAutofillLogins = NSLocalizedString("action.title.autofill.logins", value: "Passwords", comment: "Autofill Logins menu item opening the login list") + // MARK: - Waitlist public static let waitlistPrivacyDisclaimer = NSLocalizedString("waitlist.privacy-disclaimer", @@ -692,15 +692,15 @@ In addition to the details entered into this form, your app issue report will co """, comment: "Message used when sharing to iMessage") // MARK: Autofill - - public static let autofillLoginDetailsLoginName = NSLocalizedString("autofill.logins.details.login-name", value:"Login Title", comment: "Login name label for login details on autofill") + + public static let autofillLoginDetailsLoginName = NSLocalizedString("autofill.logins.details.login-name", value:"Title", comment: "Login name label for login details on autofill") public static let autofillLoginDetailsUsername = NSLocalizedString("autofill.logins.details.username", value:"Username", comment: "Username label for login details on autofill") public static let autofillLoginDetailsPassword = NSLocalizedString("autofill.logins.details.password", value:"Password", comment: "Password label for login details on autofill") public static let autofillLoginDetailsAddress = NSLocalizedString("autofill.logins.details.address", value:"Website URL", comment: "Address label for login details on autofill") public static let autofillLoginDetailsNotes = NSLocalizedString("autofill.logins.details.notes", value:"Notes", comment: "Notes label for login details on autofill") - public static let autofillEmptyViewTitle = NSLocalizedString("autofill.logins.empty-view.title", value:"No Logins saved yet", comment: "Title for view displayed when autofill has no items") - public static let autofillEmptyViewSubtitle = NSLocalizedString("autofill.logins.empty-view.subtitle", value:"Logins are stored securely on your device.", comment: "Subtitle for view displayed when autofill has no items") + public static let autofillEmptyViewTitle = NSLocalizedString("autofill.logins.empty-view.title", value:"No passwords saved yet", comment: "Title for view displayed when autofill has no items") + public static let autofillEmptyViewSubtitle = NSLocalizedString("autofill.logins.empty-view.subtitle", value:"Passwords are stored securely on your device.", comment: "Subtitle for view displayed when autofill has no items") public static let autofillSearchNoResultTitle = NSLocalizedString("autofill.logins.search.no-results.title", value:"No Results", comment: "Title displayed when there are no results on Autofill search") public static func autofillSearchNoResultSubtitle(for query: String) -> String { let message = NSLocalizedString("autofill.logins.search.no-results.subtitle", value: "for '%@'", comment: "Subtitle displayed when there are no results on Autofill search, example : No Result (Title) for Duck (Subtitle)") @@ -716,19 +716,19 @@ Our privacy protections work without having to know anything about the technical But if you *do* want a peek under the hood, you can find more information about how DuckDuckGo privacy protections work on our [help pages](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/). """, comment: "about page") - - public static let autofillEnableSettings = NSLocalizedString("autofill.logins.list.enable", value:"Save and Autofill Logins", comment: "Title for a toggle that enables autofill") + + public static let autofillEnableSettings = NSLocalizedString("autofill.logins.list.enable", value:"Save and autofill passwords", comment: "Title for a toggle that enables autofill") public static let autofillNeverSavedSettings = NSLocalizedString("autofill.logins.list.never.saved", value:"Reset Excluded Sites", comment: "Title for a button that allows a user to reset their list of never saved sites") - public static let autofillLoginListTitle = NSLocalizedString("autofill.logins.list.title", value:"Logins", comment: "Title for screen listing autofill logins") - public static let autofillLoginListSearchPlaceholder = NSLocalizedString("autofill.logins.list.search-placeholder", value:"Search Logins", comment: "Placeholder for search field on autofill login listing") + public static let autofillLoginListTitle = NSLocalizedString("autofill.logins.list.title", value:"Passwords", comment: "Title for screen listing autofill logins") + public static let autofillLoginListSearchPlaceholder = NSLocalizedString("autofill.logins.list.search-placeholder", value:"Search passwords", comment: "Placeholder for search field on autofill login listing") public static let autofillLoginListSuggested = NSLocalizedString("autofill.logins.list.suggested", value:"Suggested", comment: "Section title for group of suggested saved logins") - - public static let autofillResetNeverSavedActionTitle = NSLocalizedString("autofill.logins.list.never.saved.reset.action.title", value:"If you reset excluded sites, you will be prompted to save your Login next time you sign in to any of these sites.", comment: "Alert title") + + public static let autofillResetNeverSavedActionTitle = NSLocalizedString("autofill.logins.list.never.saved.reset.action.title", value:"If you reset excluded sites, you will be prompted to save your password next time you sign in to any of these sites.", comment: "Alert title") public static let autofillResetNeverSavedActionConfirmButton = NSLocalizedString("autofill.logins.list.never.saved.reset.action.confirm", value: "Reset Excluded Sites", comment: "Confirm button to reset list of never saved sites") public static let autofillResetNeverSavedActionCancelButton = NSLocalizedString("autofill.logins.list.never.saved.reset.action.cancel", value: "Cancel", comment: "Cancel button for resetting list of never saved sites") public static let autofillLoginPromptAuthenticationCancelButton = NSLocalizedString("autofill.logins.prompt.auth.cancel", value:"Cancel", comment: "Cancel button for auth during login prompt") - public static let autofillLoginPromptAuthenticationReason = NSLocalizedString("autofill.logins.prompt.auth.reason", value:"Unlock To Use Saved Login", comment: "Reason for auth during login prompt") + public static let autofillLoginPromptAuthenticationReason = NSLocalizedString("autofill.logins.prompt.auth.reason", value:"Unlock to use saved password", comment: "Reason for auth during login prompt") public static let autofillLoginPromptTitle = NSLocalizedString("autofill.logins.prompt.title", value:"Use a saved password?", comment: "Title for autofill login prompt") public static let autofillLoginPromptExactMatchTitle = NSLocalizedString("autofill.logins.prompt.exact.match.title", value:"From this website", comment: "Title for section of autofill logins that are an exact match to the current website") public static func autofillLoginPromptPartialMatchTitle(for type: String) -> String { @@ -741,10 +741,10 @@ But if you *do* want a peek under the hood, you can find more information about } public static let autofillLoginPromptMoreOptions = NSLocalizedString("autofill.logins.prompt.more-options", value:"More Options", comment: "Button title for autofill login prompt if more options are available") - - public static let autofillNoAuthViewTitle = NSLocalizedString("autofill.logins.no-auth.title", value:"Secure your device to save Logins", comment: "Title for view displayed when autofill is locked on devices where a passcode has not been set") - public static let autofillNoAuthViewSubtitle = NSLocalizedString("autofill.logins.no-auth.subtitle", value:"A passcode is required to protect your Logins.", comment: "Title for view displayed when autofill is locked on devices where a passcode has not been set") - + + public static let autofillNoAuthViewTitle = NSLocalizedString("autofill.logins.no-auth.title", value:"Secure your device to save passwords", comment: "Title for view displayed when autofill is locked on devices where a passcode has not been set") + public static let autofillNoAuthViewSubtitle = NSLocalizedString("autofill.logins.no-auth.subtitle", value:"A passcode is required to protect your passwords.", comment: "Title for view displayed when autofill is locked on devices where a passcode has not been set") + public static let autofillOpenWebsitePrompt = NSLocalizedString("autofill.logins.details.open-website-prompt.title", value:"Open Website", comment: "Menu item title for option to open website from selected url") public static func autofillCopyPrompt(for type: String) -> String { let message = NSLocalizedString("autofill.logins.copy-prompt", value: "Copy %@", comment: "Menu item text for copying autofill login details") @@ -756,38 +756,38 @@ But if you *do* want a peek under the hood, you can find more information about public static let autofillCopyToastNotesCopied = NSLocalizedString("autofill.logins.copy-toast.notes-copied", value:"Notes copied", comment: "Title for toast when copying notes") public static func autofillLoginDetailsLastUpdated(for date: String) -> String { - let message = NSLocalizedString("autofill.logins.details.last-updated", value: "Login last updated %@", comment: "Message displaying when the login was last updated by") + let message = NSLocalizedString("autofill.logins.details.last-updated", value: "Last updated %@", comment: "Message displaying when the login was last updated") return message.format(arguments: date) } public static let autofillLoginListAuthenticationCancelButton = NSLocalizedString("autofill.logins.list.auth.cancel", value:"Cancel", comment: "Cancel button for auth when opening login list") - public static let autofillLoginListAuthenticationReason = NSLocalizedString("autofill.logins.list.auth.reason", value:"Unlock device to access saved Logins", comment: "Reason for auth when opening login list") - public static let autofillLoginDetailsDefaultTitle = NSLocalizedString("autofill.logins.details.default-title", value:"Login", comment: "Title for autofill login details") - public static let autofillLoginDetailsEditTitle = NSLocalizedString("autofill.logins.details.edit-title", value:"Edit Login", comment: "Title when editing autofill login details") - public static let autofillLoginDetailsNewTitle = NSLocalizedString("autofill.logins.details.new-title", value:"Add Login", comment: "Title when adding new autofill login") - public static let autofillLoginDetailsDeleteButton = NSLocalizedString("autofill.logins.details.delete", value:"Delete Login", comment: "Delete button when deleting an autofill login") - public static let autofillLoginDetailsDeleteConfirmationTitle = NSLocalizedString("autofill.logins.details.delete-confirmation.title", value:"Are you sure you want to delete this Login?", comment: "Title of confirmation alert when deleting an autofill login") - public static let autofillLoginDetailsDeleteConfirmationButtonTitle = NSLocalizedString("autofill.logins.details.delete-confirmation.button", value:"Delete Login", comment: "Autofill alert button confirming delete autofill login") - + public static let autofillLoginListAuthenticationReason = NSLocalizedString("autofill.logins.list.auth.reason", value:"Unlock device to access passwords", comment: "Reason for auth when opening login list") + public static let autofillLoginDetailsDefaultTitle = NSLocalizedString("autofill.logins.details.default-title", value:"Password", comment: "Title for autofill login details") + public static let autofillLoginDetailsEditTitle = NSLocalizedString("autofill.logins.details.edit-title", value:"Edit Password", comment: "Title when editing autofill login details") + public static let autofillLoginDetailsNewTitle = NSLocalizedString("autofill.logins.details.new-title", value:"Add password", comment: "Title when adding new autofill login") + public static let autofillLoginDetailsDeleteButton = NSLocalizedString("autofill.logins.details.delete", value:"Delete Password", comment: "Delete button when deleting an autofill login") + public static let autofillLoginDetailsDeleteConfirmationTitle = NSLocalizedString("autofill.logins.details.delete-confirmation.title", value:"Are you sure you want to delete this password?", comment: "Title of confirmation alert when deleting an autofill login") + public static let autofillLoginDetailsDeleteConfirmationButtonTitle = NSLocalizedString("autofill.logins.details.delete-confirmation.button", value:"Delete Password", comment: "Autofill alert button confirming delete autofill login") + public static func autofillLoginListLoginDeletedToastMessage(for title: String) -> String { - let message = NSLocalizedString("autofill.logins.list.login-deleted-message", value: "Login for %@ deleted", comment: "Toast message when a login item is deleted") + let message = NSLocalizedString("autofill.logins.list.login-deleted-message", value: "Password for %@ deleted", comment: "Toast message when a login item is deleted") return message.format(arguments: title) } - public static let autofillLoginListLoginDeletedToastMessageNoTitle = NSLocalizedString("autofill.logins.list.login-deleted-message-no-title", value: "Login deleted", comment: "Toast message when a login item without a title is deleted") - + public static let autofillLoginListLoginDeletedToastMessageNoTitle = NSLocalizedString("autofill.logins.list.login-deleted-message-no-title", value: "Password deleted", comment: "Toast message when a login item without a title is deleted") + public static let autofillLoginDetailsEditTitlePlaceholder = NSLocalizedString("autofill.logins.details.edit.title-placeholder", value:"Title", comment: "Placeholder for title field on autofill login details") public static let autofillLoginDetailsEditUsernamePlaceholder = NSLocalizedString("autofill.logins.details.edit.username-placeholder", value:"username@example.com", comment: "Placeholder for userbane field on autofill login details") public static let autofillLoginDetailsEditPasswordPlaceholder = NSLocalizedString("autofill.logins.details.edit.password-placeholder", value:"Password", comment: "Placeholder for password field on autofill login details") public static let autofillLoginDetailsEditURLPlaceholder = NSLocalizedString("autofill.logins.details.edit.url-placeholder", value:"example.com", comment: "Placeholder for url field on autofill login details") - - public static let autofillLoginDetailsSaveDuplicateLoginAlertTitle = NSLocalizedString("autofill.logins.details.save-duplicate-alert.title", value:"Duplicated Login", comment: "Title for alert when attempting to save a duplicate login") - public static let autofillLoginDetailsSaveDuplicateLoginAlertMessage = NSLocalizedString("autofill.logins.details.save-duplicate-alert.message", value:"You already have a login for this username and website.", comment: "Message for alert when attempting to save a duplicate login") + + public static let autofillLoginDetailsSaveDuplicateLoginAlertTitle = NSLocalizedString("autofill.logins.details.save-duplicate-alert.title", value:"Duplicate Password", comment: "Title for alert when attempting to save a duplicate login") + public static let autofillLoginDetailsSaveDuplicateLoginAlertMessage = NSLocalizedString("autofill.logins.details.save-duplicate-alert.message", value:"You already have a password saved for this username and website.", comment: "Message for alert when attempting to save a duplicate login") public static let autofillLoginDetailsSaveDuplicateLoginAlertAction = NSLocalizedString("autofill.logins.details.save-duplicate-alert.action", value:"OK", comment: "Action text for alert when attempting to save a duplicate login") public static let autofillNavigationButtonItemTitleClose = NSLocalizedString("autofill.logins.list.close-title", value:"Close", comment: "Title for close navigation button") // Autofill Password Generation Prompt public static let autofillPasswordGenerationPromptTitle = NSLocalizedString("autofill.password-generation-prompt.title", value:"Use a strong password from DuckDuckGo?", comment: "Title for prompt to use suggested strong password for creating a login") - public static let autofillPasswordGenerationPromptSubtitle = NSLocalizedString("autofill.password-generation-prompt.subtitle", value:"Passwords are stored securely on your device in the Logins menu.", comment: "Subtitle for prompt to use suggested strong password for creating a login") + public static let autofillPasswordGenerationPromptSubtitle = NSLocalizedString("autofill.password-generation-prompt.subtitle", value:"Passwords are stored securely on your device.", comment: "Subtitle for prompt to use suggested strong password for creating a login") public static let autofillPasswordGenerationPromptUseGeneratedPasswordCTA = NSLocalizedString("autofill.password-generation-prompt.use-generated-password.cta", value:"Use Strong Password", comment: "Button title choosing to use the suggested generated password for creating a login") public static let autofillPasswordGenerationPromptUseOwnPasswordCTA = NSLocalizedString("autofill.password-generation-prompt.use-own-password.cta", value:"Create My Own", comment: "Button title choosing to use own password for creating a login") @@ -861,7 +861,19 @@ But if you *do* want a peek under the hood, you can find more information about static let syncCredentialsPausedAlertDescription = NSLocalizedString("alert.sync-credentials-paused-description", value: "You have exceeded the passwords sync limit. Try deleting some passwords. Until this is resolved your passwords will not be backed up.", comment: "Description for alert shown when sync credentials paused for too many items") public static let syncPausedAlertOkButton = NSLocalizedString("alert.sync-paused-alert-ok-button", value: "OK", comment: "Confirmation button in alert") public static let syncPausedAlertLearnMoreButton = NSLocalizedString("alert.sync-paused-alert-learn-more-button", value: "Learn More", comment: "Learn more button in alert") - + public static let syncErrorAlertTitle = NSLocalizedString("alert.sync-error", value: "Sync & Backup Error", comment: "Title for sync error alert") + public static let unableToSyncToServerDescription = NSLocalizedString("alert.unable-to-sync-to-server-description", value: "Unable to connect to the server.", comment: "Description for unable to sync to server error") + public static let unableToSyncWithOtherDeviceDescription = NSLocalizedString("alert.unable-to-sync-with-other-device-description", value: "Unable to Sync with another device.", comment: "Description for unable to sync with another device error") + public static let unableToMergeTwoAccountsErrorDescription = NSLocalizedString("alert.unable-to-merge-two-accounts-description", value: "To pair these devices, turn off Sync & Backup on one device then tap \"Sync with Another Device\" on the other device.", comment: "Description for unable to merge two accounts error") + public static let unableToUpdateDeviceNameDescription = NSLocalizedString("alert.unable-to-update-device-name-description", value: "Unable to update the device name.", comment: "Description for unable to update device name error") + public static let unableToTurnSyncOffDescription = NSLocalizedString("alert.unable-to-turn-sync-off-description", value: "Unable to turn Sync & Backup off.", comment: "Description for unable to turn sync off error") + public static let unableToDeleteDataDescription = NSLocalizedString("alert.unable-to-delete-data-description", value: "Unable to delete data on the server.", comment: "Description for unable to delete data error") + public static let unableToRemoveDeviceDescription = NSLocalizedString("alert.unable-to-remove-device-description", value: "Unable to remove this device from Sync & Backup.", comment: "Description for unable to remove device error") + public static let unableToCreateRecoveryPDF = NSLocalizedString("alert.unable-to-create-recovery-pdf-description", value: "Unable to create the recovery PDF.", comment: "Description for unable to create recovery pdf error") + static let syncPausedTitle = NSLocalizedString("sync.warning.sync.paused", value: "Sync & Backup is Paused", comment: "Title of the warning message") + static let syncUnavailableMessage = NSLocalizedString("sync.warning.data.syncing.disabled", value: "Sorry, but Sync & Backup is currently unavailable. Please try again later.", comment: "Data syncing unavailable warning message") + static let syncUnavailableMessageUpgradeRequired = NSLocalizedString("sync.warning.data.syncing.disabled.upgrade.required", value: "Sorry, but Sync & Backup is no longer available in this app version. Please update DuckDuckGo to the latest version to continue.", comment: "Data syncing unavailable warning message") + static let preemptiveCrashTitle = NSLocalizedString("error.preemptive-crash.title", value: "App issue detected", comment: "Alert title") static let preemptiveCrashBody = NSLocalizedString("error.preemptive-crash.body", value: "Looks like there's an issue with the app and it needs to close. Please reopen to continue.", comment: "Alert message") static let preemptiveCrashAction = NSLocalizedString("error.preemptive-crash.action", value: "Close App", comment: "Button title that is shutting down the app") diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index dcb69ae89f..df7d58ccf2 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -14,7 +14,7 @@ "action.title.add" = "Add"; /* Autofill Logins menu item opening the login list */ -"action.title.autofill.logins" = "Logins"; +"action.title.autofill.logins" = "Passwords"; /* Confirmation of Add to Bookmarks action in Add All Open Tabs to Bookmarks alert */ "action.title.bookmark" = "Bookmark"; @@ -139,6 +139,9 @@ /* Title for alert shown when sync credentials paused for too many items */ "alert.sync-credentials-paused-title" = "Passwords Sync is Paused"; +/* Title for sync error alert */ +"alert.sync-error" = "Sync & Backup Error"; + /* Learn more button in alert */ "alert.sync-paused-alert-learn-more-button" = "Learn More"; @@ -160,6 +163,30 @@ /* Save Favorite action */ "alert.title.save.favorite" = "Save Favorite"; +/* Description for unable to create recovery pdf error */ +"alert.unable-to-create-recovery-pdf-description" = "Unable to create the recovery PDF."; + +/* Description for unable to delete data error */ +"alert.unable-to-delete-data-description" = "Unable to delete data on the server."; + +/* Description for unable to merge two accounts error */ +"alert.unable-to-merge-two-accounts-description" = "To pair these devices, turn off Sync & Backup on one device then tap \"Sync with Another Device\" on the other device."; + +/* Description for unable to remove device error */ +"alert.unable-to-remove-device-description" = "Unable to remove this device from Sync & Backup."; + +/* Description for unable to sync to server error */ +"alert.unable-to-sync-to-server-description" = "Unable to connect to the server."; + +/* Description for unable to sync with another device error */ +"alert.unable-to-sync-with-other-device-description" = "Unable to Sync with another device."; + +/* Description for unable to turn sync off error */ +"alert.unable-to-turn-sync-off-description" = "Unable to turn Sync & Backup off."; + +/* Description for unable to update device name error */ +"alert.unable-to-update-device-name-description" = "Unable to update the device name."; + /* Shown on authentication screen */ "app.authentication.unlock" = "Unlock DuckDuckGo."; @@ -434,16 +461,16 @@ "autofill.keep-enabled.alert.message" = "You can disable this at any time in Settings."; /* Title for alert when asking the user if they want to keep using autofill */ -"autofill.keep-enabled.alert.title" = "Do you want to keep saving Logins?"; +"autofill.keep-enabled.alert.title" = "Do you want to keep saving passwords?"; /* Button displayed after saving/updating an autofill login that takes the user to the saved login */ "autofill.login-save-action-button.toast" = "View"; /* Message displayed after saving an autofill login */ -"autofill.login-saved.toast" = "Login saved"; +"autofill.login-saved.toast" = "Password saved"; /* Message displayed after updating an autofill login */ -"autofill.login-updated.toast" = "Login updated"; +"autofill.login-updated.toast" = "Password updated"; /* Menu item text for copying autofill login details */ "autofill.logins.copy-prompt" = "Copy %@"; @@ -464,19 +491,19 @@ "autofill.logins.details.address" = "Website URL"; /* Title for autofill login details */ -"autofill.logins.details.default-title" = "Login"; +"autofill.logins.details.default-title" = "Password"; /* Delete button when deleting an autofill login */ -"autofill.logins.details.delete" = "Delete Login"; +"autofill.logins.details.delete" = "Delete Password"; /* Autofill alert button confirming delete autofill login */ -"autofill.logins.details.delete-confirmation.button" = "Delete Login"; +"autofill.logins.details.delete-confirmation.button" = "Delete Password"; /* Title of confirmation alert when deleting an autofill login */ -"autofill.logins.details.delete-confirmation.title" = "Are you sure you want to delete this Login?"; +"autofill.logins.details.delete-confirmation.title" = "Are you sure you want to delete this password?"; /* Title when editing autofill login details */ -"autofill.logins.details.edit-title" = "Edit Login"; +"autofill.logins.details.edit-title" = "Edit Password"; /* Placeholder for password field on autofill login details */ "autofill.logins.details.edit.password-placeholder" = "Password"; @@ -490,14 +517,14 @@ /* Placeholder for userbane field on autofill login details */ "autofill.logins.details.edit.username-placeholder" = "username@example.com"; -/* Message displaying when the login was last updated by */ -"autofill.logins.details.last-updated" = "Login last updated %@"; +/* Message displaying when the login was last updated */ +"autofill.logins.details.last-updated" = "Last updated %@"; /* Login name label for login details on autofill */ -"autofill.logins.details.login-name" = "Login Title"; +"autofill.logins.details.login-name" = "Title"; /* Title when adding new autofill login */ -"autofill.logins.details.new-title" = "Add Login"; +"autofill.logins.details.new-title" = "Add password"; /* Notes label for login details on autofill */ "autofill.logins.details.notes" = "Notes"; @@ -512,37 +539,37 @@ "autofill.logins.details.save-duplicate-alert.action" = "OK"; /* Message for alert when attempting to save a duplicate login */ -"autofill.logins.details.save-duplicate-alert.message" = "You already have a login for this username and website."; +"autofill.logins.details.save-duplicate-alert.message" = "You already have a password saved for this username and website."; /* Title for alert when attempting to save a duplicate login */ -"autofill.logins.details.save-duplicate-alert.title" = "Duplicated Login"; +"autofill.logins.details.save-duplicate-alert.title" = "Duplicate Password"; /* Username label for login details on autofill */ "autofill.logins.details.username" = "Username"; /* Subtitle for view displayed when autofill has no items */ -"autofill.logins.empty-view.subtitle" = "Logins are stored securely on your device."; +"autofill.logins.empty-view.subtitle" = "Passwords are stored securely on your device."; /* Title for view displayed when autofill has no items */ -"autofill.logins.empty-view.title" = "No Logins saved yet"; +"autofill.logins.empty-view.title" = "No passwords saved yet"; /* Cancel button for auth when opening login list */ "autofill.logins.list.auth.cancel" = "Cancel"; /* Reason for auth when opening login list */ -"autofill.logins.list.auth.reason" = "Unlock device to access saved Logins"; +"autofill.logins.list.auth.reason" = "Unlock device to access passwords"; /* Title for close navigation button */ "autofill.logins.list.close-title" = "Close"; /* Title for a toggle that enables autofill */ -"autofill.logins.list.enable" = "Save and Autofill Logins"; +"autofill.logins.list.enable" = "Save and autofill passwords"; /* Toast message when a login item is deleted */ -"autofill.logins.list.login-deleted-message" = "Login for %@ deleted"; +"autofill.logins.list.login-deleted-message" = "Password for %@ deleted"; /* Toast message when a login item without a title is deleted */ -"autofill.logins.list.login-deleted-message-no-title" = "Login deleted"; +"autofill.logins.list.login-deleted-message-no-title" = "Password deleted"; /* Title for a button that allows a user to reset their list of never saved sites */ "autofill.logins.list.never.saved" = "Reset Excluded Sites"; @@ -554,28 +581,28 @@ "autofill.logins.list.never.saved.reset.action.confirm" = "Reset Excluded Sites"; /* Alert title */ -"autofill.logins.list.never.saved.reset.action.title" = "If you reset excluded sites, you will be prompted to save your Login next time you sign in to any of these sites."; +"autofill.logins.list.never.saved.reset.action.title" = "If you reset excluded sites, you will be prompted to save your password next time you sign in to any of these sites."; /* Placeholder for search field on autofill login listing */ -"autofill.logins.list.search-placeholder" = "Search Logins"; +"autofill.logins.list.search-placeholder" = "Search passwords"; /* Section title for group of suggested saved logins */ "autofill.logins.list.suggested" = "Suggested"; /* Title for screen listing autofill logins */ -"autofill.logins.list.title" = "Logins"; +"autofill.logins.list.title" = "Passwords"; /* Title for view displayed when autofill is locked on devices where a passcode has not been set */ -"autofill.logins.no-auth.subtitle" = "A passcode is required to protect your Logins."; +"autofill.logins.no-auth.subtitle" = "A passcode is required to protect your passwords."; /* Title for view displayed when autofill is locked on devices where a passcode has not been set */ -"autofill.logins.no-auth.title" = "Secure your device to save Logins"; +"autofill.logins.no-auth.title" = "Secure your device to save passwords"; /* Cancel button for auth during login prompt */ "autofill.logins.prompt.auth.cancel" = "Cancel"; /* Reason for auth during login prompt */ -"autofill.logins.prompt.auth.reason" = "Unlock To Use Saved Login"; +"autofill.logins.prompt.auth.reason" = "Unlock to use saved password"; /* Title for section of autofill logins that are an exact match to the current website */ "autofill.logins.prompt.exact.match.title" = "From this website"; @@ -599,7 +626,7 @@ "autofill.logins.search.no-results.title" = "No Results"; /* Subtitle for prompt to use suggested strong password for creating a login */ -"autofill.password-generation-prompt.subtitle" = "Passwords are stored securely on your device in the Logins menu."; +"autofill.password-generation-prompt.subtitle" = "Passwords are stored securely on your device."; /* Title for prompt to use suggested strong password for creating a login */ "autofill.password-generation-prompt.title" = "Use a strong password from DuckDuckGo?"; @@ -644,7 +671,7 @@ "autofill.save-login.never-prompt.CTA" = "Never Ask for This Site"; /* Message displayed on modal asking for the user to save the login for the first time */ -"autofill.save-login.new-user.message" = "Passwords are stored securely on your device in the Logins menu."; +"autofill.save-login.new-user.message" = "Passwords are stored securely on your device."; /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Do you want DuckDuckGo to save your password?"; @@ -653,7 +680,7 @@ "autofill.save-login.not-now.CTA" = "Don’t Save"; /* Title displayed on modal asking for the user to save the login */ -"autofill.save-login.title" = "Save Login?"; +"autofill.save-login.title" = "Save Password?"; /* Confirm CTA displayed on modal asking for the user to save the password */ "autofill.save-password.save.CTA" = "Save Password"; @@ -1855,6 +1882,15 @@ But if you *do* want a peek under the hood, you can find more information about /* No comment provided by engineer. */ "sync.remove-device.message" = "\"%@\" will no longer be able to access your synced data."; +/* Data syncing unavailable warning message */ +"sync.warning.data.syncing.disabled" = "Sorry, but Sync & Backup is currently unavailable. Please try again later."; + +/* Data syncing unavailable warning message */ +"sync.warning.data.syncing.disabled.upgrade.required" = "Sorry, but Sync & Backup is no longer available in this app version. Please update DuckDuckGo to the latest version to continue."; + +/* Title of the warning message */ +"sync.warning.sync.paused" = "Sync & Backup is Paused"; + /* Accessibility label on remove button */ "tab.close.home" = "Close home tab"; diff --git a/DuckDuckGoTests/AppURLsTests.swift b/DuckDuckGoTests/AppURLsTests.swift index 0d2adfdbe1..280e855dcc 100644 --- a/DuckDuckGoTests/AppURLsTests.swift +++ b/DuckDuckGoTests/AppURLsTests.swift @@ -204,6 +204,24 @@ final class AppURLsTests: XCTestCase { XCTAssertEqual(url.getParameter(named: "q"), "query") } + func testSearchUrlCreatesSearchUrlWhenFloatingPointNumberIsPassed() { + let url = URL.makeSearchURL(query: "1.4") + XCTAssertEqual(url?.getParameter(named: "q"), "1.4") + } + + func testSearchUrlCreatesSearchUrlWhenFloatingPointNumbersDivisionIsPassed() { + let url = URL.makeSearchURL(query: "1.4/3.4") + XCTAssertEqual(url?.getParameter(named: "q"), "1.4/3.4") + + let url2 = URL.makeSearchURL(query: "4/3.4") + XCTAssertEqual(url2?.getParameter(named: "q"), "4/3.4") + } + + func testSearchUrlCreatesWebUrlWhenIPv4WithFourOctetsIsPassed() { + let url = URL.makeSearchURL(query: "1.0.0.4/3.4") + XCTAssertEqual(url?.absoluteString, "http://1.0.0.4/3.4") + } + func testExtiUrlCreatesUrlWithAtbParam() throws { let url = URL.makeExtiURL(atb: "x") XCTAssertEqual(url.getParameter(named: "atb"), "x") @@ -272,7 +290,7 @@ final class AppURLsTests: XCTestCase { let result = url.searchQuery XCTAssertNil(result) } - + func testExternalDependencyURLsNotChanged() { XCTAssertEqual(URL.surrogates.absoluteString, "https://staticcdn.duckduckgo.com/surrogates.txt") XCTAssertEqual(URL.privacyConfig.absoluteString, "https://staticcdn.duckduckgo.com/trackerblocking/config/v4/ios-config.json") diff --git a/DuckDuckGoTests/MockPrivacyConfiguration.swift b/DuckDuckGoTests/MockPrivacyConfiguration.swift index 2ce15df62f..d52cb55e35 100644 --- a/DuckDuckGoTests/MockPrivacyConfiguration.swift +++ b/DuckDuckGoTests/MockPrivacyConfiguration.swift @@ -53,6 +53,7 @@ class MockPrivacyConfiguration: PrivacyConfiguration { @objc(MockPrivacyConfigurationManager) class MockPrivacyConfigurationManager: NSObject, PrivacyConfigurationManaging { + var embeddedConfigData: BrowserServicesKit.PrivacyConfigurationManager.ConfigurationData { fatalError("not implemented") } @@ -71,4 +72,5 @@ class MockPrivacyConfigurationManager: NSObject, PrivacyConfigurationManaging { var updatesPublisher: AnyPublisher = Just(()).eraseToAnyPublisher() var privacyConfig: PrivacyConfiguration = MockPrivacyConfiguration() + var internalUserDecider: InternalUserDecider = DefaultInternalUserDecider() } diff --git a/DuckDuckGoTests/NetworkProtectionAccessControllerTests.swift b/DuckDuckGoTests/NetworkProtectionAccessControllerTests.swift index 0e8d5cae7e..7817e1cc1d 100644 --- a/DuckDuckGoTests/NetworkProtectionAccessControllerTests.swift +++ b/DuckDuckGoTests/NetworkProtectionAccessControllerTests.swift @@ -117,6 +117,43 @@ final class NetworkProtectionAccessControllerTests: XCTestCase { XCTAssertEqual(controller.networkProtectionAccessType(), .waitlistInvited) } + func testWhenUserHasWaitlistAccess_ThenWaitlistUserCheckIsTrue() { + let controller = createMockAccessController( + isInternalUser: true, + featureActivated: true, + termsAccepted: true, + featureFlagsEnabled: true, + hasJoinedWaitlist: true, + hasBeenInvited: true + ) + + XCTAssertTrue(controller.isPotentialOrCurrentWaitlistUser) + } + + func testWhenUserDoesNotHaveWaitlistAccess_ThenWaitlistUserCheckIsFalse() { + let controller = createMockAccessController( + featureActivated: false, + termsAccepted: false, + featureFlagsEnabled: false, + hasJoinedWaitlist: false, + hasBeenInvited: false + ) + + XCTAssertFalse(controller.isPotentialOrCurrentWaitlistUser) + } + + func testWhenUserIsInternal_ThenWaitlistUserCheckIsFalse() { + let controller = createMockAccessController( + featureActivated: true, + termsAccepted: false, + featureFlagsEnabled: false, + hasJoinedWaitlist: false, + hasBeenInvited: false + ) + + XCTAssertFalse(controller.isPotentialOrCurrentWaitlistUser) + } + // MARK: - Mock Creation private func createMockAccessController( @@ -146,12 +183,14 @@ final class NetworkProtectionAccessControllerTests: XCTestCase { let mockTermsAndConditionsStore = MockNetworkProtectionTermsAndConditionsStore() mockTermsAndConditionsStore.networkProtectionWaitlistTermsAndConditionsAccepted = termsAccepted let mockFeatureFlagger = createFeatureFlagger(withSubfeatureEnabled: featureFlagsEnabled) + let internalUserDecider = DefaultInternalUserDecider(store: internalUserDeciderStore) return NetworkProtectionAccessController( networkProtectionActivation: mockActivation, networkProtectionWaitlistStorage: mockWaitlistStorage, networkProtectionTermsAndConditionsStore: mockTermsAndConditionsStore, - featureFlagger: mockFeatureFlagger + featureFlagger: mockFeatureFlagger, + internalUserDecider: internalUserDecider ) } diff --git a/DuckDuckGoTests/NetworkProtectionInviteViewModelTests.swift b/DuckDuckGoTests/NetworkProtectionInviteViewModelTests.swift index 1d63a15faf..08c396103b 100644 --- a/DuckDuckGoTests/NetworkProtectionInviteViewModelTests.swift +++ b/DuckDuckGoTests/NetworkProtectionInviteViewModelTests.swift @@ -133,8 +133,15 @@ final class NetworkProtectionInviteViewModelTests: XCTestCase { } private class MockRedemptionCoordinator: NetworkProtectionCodeRedeeming { + var callCount = 0 func redeem(_ code: String) async throws { callCount += 1 } + + var exchangeCallCount = 0 + func exchange(accessToken: String) async throws { + exchangeCallCount += 1 + } + } diff --git a/DuckDuckGoTests/PrivacyConfigurationManagerMock.swift b/DuckDuckGoTests/PrivacyConfigurationManagerMock.swift index 5062f25e74..dd7e49f723 100644 --- a/DuckDuckGoTests/PrivacyConfigurationManagerMock.swift +++ b/DuckDuckGoTests/PrivacyConfigurationManagerMock.swift @@ -105,6 +105,7 @@ class PrivacyConfigurationManagerMock: PrivacyConfigurationManaging { } var privacyConfig: PrivacyConfiguration = PrivacyConfigurationMock() + var internalUserDecider: InternalUserDecider = DefaultInternalUserDecider() var reloadFired = [(etag: String?, data: Data?)]() var reloadResult: PrivacyConfigurationManager.ReloadResult = .embedded diff --git a/DuckDuckGoTests/SyncManagementViewModelTests.swift b/DuckDuckGoTests/SyncManagementViewModelTests.swift index 5edc9b8ba4..3fbf7ba3fa 100644 --- a/DuckDuckGoTests/SyncManagementViewModelTests.swift +++ b/DuckDuckGoTests/SyncManagementViewModelTests.swift @@ -26,7 +26,7 @@ class SyncManagementViewModelTests: XCTestCase, SyncManagementViewModelDelegate fileprivate var monitor = Monitor() lazy var model: SyncSettingsViewModel = { - let model = SyncSettingsViewModel() + let model = SyncSettingsViewModel(isOnDevEnvironment: { false }, switchToProdEnvironment: {}) model.delegate = self return model }() diff --git a/FingerprintingUITests/FingerprintUITest.swift b/FingerprintingUITests/FingerprintUITest.swift index 1005ec06cd..2982f4ce45 100644 --- a/FingerprintingUITests/FingerprintUITest.swift +++ b/FingerprintingUITests/FingerprintUITest.swift @@ -126,8 +126,8 @@ class FingerprintUITest: XCTestCase { app.navigationBars.buttons["Done"].tap() // Clear all tabs and data - app.toolbars["Toolbar"].buttons["Fire"].tap() - app.buttons["Close Tabs and Clear Data"].tap() + app.toolbars["Toolbar"].buttons["Close Tabs and Clear Data"].tap() + app.buttons["alert.forget-data.confirm"].tap() sleep(2) @@ -148,7 +148,11 @@ class FingerprintUITest: XCTestCase { } else { XCTFail("Bookmarks button missing") } - app.tables.staticTexts["DuckDuckGo — Privacy, simplified."].tap() + if app.tables.staticTexts["DuckDuckGo — Privacy, simplified."].waitForExistence(timeout: 25) { + app.staticTexts["DuckDuckGo — Privacy, simplified."].tap() + } else { + XCTFail("Could not find bookmark") + } // Verify the test passed XCTAssertTrue(webview.staticTexts["TEST PASSED"].waitForExistence(timeout: 25), "Test not run") diff --git a/LocalPackages/DuckUI/Package.swift b/LocalPackages/DuckUI/Package.swift index 0ec099aeed..80e0a08d0f 100644 --- a/LocalPackages/DuckUI/Package.swift +++ b/LocalPackages/DuckUI/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.3 +// swift-tools-version:5.7 // Package.swift // DuckDuckGo // @@ -30,10 +30,17 @@ let package = Package( name: "DuckUI", targets: ["DuckUI"]) ], - dependencies: [], + dependencies: [ + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "100.0.2"), + ], targets: [ .target( name: "DuckUI", - dependencies: []) + dependencies: [], + swiftSettings: [ + .define("DEBUG", .when(configuration: .debug)) + ], + plugins: [.plugin(name: "SwiftLintPlugin", package: "BrowserServicesKit")] + ) ] ) diff --git a/LocalPackages/SyncUI/Package.swift b/LocalPackages/SyncUI/Package.swift index f95f0f7cc2..09fdb7dc37 100644 --- a/LocalPackages/SyncUI/Package.swift +++ b/LocalPackages/SyncUI/Package.swift @@ -32,6 +32,7 @@ let package = Package( ], dependencies: [ .package(path: "../DuckUI"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "100.0.2"), .package(url: "https://github.com/duckduckgo/DesignResourcesKit", exact: "2.0.0") ], targets: [ @@ -40,6 +41,11 @@ let package = Package( dependencies: [ .product(name: "DuckUI", package: "DuckUI"), "DesignResourcesKit" - ]) + ], + swiftSettings: [ + .define("DEBUG", .when(configuration: .debug)) + ], + plugins: [.plugin(name: "SwiftLintPlugin", package: "BrowserServicesKit")] + ) ] ) diff --git a/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/SyncSettingsViewModel.swift b/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/SyncSettingsViewModel.swift index 6923a9322d..6934aaae01 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/SyncSettingsViewModel.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/SyncSettingsViewModel.swift @@ -84,9 +84,22 @@ public class SyncSettingsViewModel: ObservableObject { @Published var isBusy = false @Published var recoveryCode = "" - public weak var delegate: SyncManagementViewModelDelegate? + @Published public var isDataSyncingAvailable: Bool = true + @Published public var isConnectingDevicesAvailable: Bool = true + @Published public var isAccountCreationAvailable: Bool = true + @Published public var isAccountRecoveryAvailable: Bool = true - public init() { } + public weak var delegate: SyncManagementViewModelDelegate? + private(set) var isOnDevEnvironment: Bool + private(set) var switchToProdEnvironment: () -> Void = {} + + public init(isOnDevEnvironment: @escaping () -> Bool, switchToProdEnvironment: @escaping () -> Void) { + self.isOnDevEnvironment = isOnDevEnvironment() + self.switchToProdEnvironment = { [weak self] in + switchToProdEnvironment() + self?.isOnDevEnvironment = isOnDevEnvironment() + } + } func disableSync() { isBusy = true diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/Internal/UserText.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/Internal/UserText.swift index f9691a5ff9..9c6ffcd586 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Views/Internal/UserText.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/Internal/UserText.swift @@ -43,7 +43,7 @@ struct UserText { static let turnSyncOffSectionHeader = NSLocalizedString("turn.sync.off.section.header", value: "Sync Enabled", comment: "Turn Sync Off - Section Header") static let turnSyncOffSectionFooter = NSLocalizedString("turn.sync.off.section.footer", value: "Bookmarks and passwords are currently synced across your devices.", comment: "Turn Sync Off - Section Footer") // Sync Paused Errors - static let syncLimitExceededTitle = NSLocalizedString("sync.limit.exceeded.title", value: "⚠️ Sync Paused", comment: "Sync Paused Errors - Title") + static let syncLimitExceededTitle = NSLocalizedString("sync.limit.exceeded.title", value: "Sync Paused", comment: "Sync Paused Errors - Title") static let bookmarksLimitExceededDescription = NSLocalizedString("bookmarks.limit.exceeded.description", value: "Bookmark limit exceeded. Delete some to resume syncing.", comment: "Sync Paused Errors - Bookmarks Limit Exceeded Description") static let credentialsLimitExceededDescription = NSLocalizedString("credentials.limit.exceeded.description", value: "Logins limit exceeded. Delete some to resume syncing.", comment: "Sync Paused Errors - Credentials Limit Exceeded Description") static let bookmarksLimitExceededAction = NSLocalizedString("bookmarks.limit.exceeded.action", value: "Manage Bookmarks", comment: "Sync Paused Errors - Bookmarks Limit Exceeded Action") @@ -159,6 +159,11 @@ struct UserText { static let fetchFaviconsOnboardingMessage = NSLocalizedString("fetch.favicons.onboarding.message", value: "Do you want this device to automatically download icons for any new bookmarks synced from your other devices? This will expose the download to your network any time a bookmark is synced.", comment: "Fetch Favicons Onboarding - Message") static let fetchFaviconsOnboardingButtonTitle = NSLocalizedString("fetch.favicons.onboarding.button.title", value: "Keep Bookmarks Icons Updated", comment: "Fetch Favicons Onboarding - Button Title") + // Sync Feature Flags + static let syncUnavailableTitle = NSLocalizedString("sync.warning.sync.unavailable", value: "Sync & Backup is Unavailable", comment: "Title of the warning message") + static let syncPausedTitle = NSLocalizedString("sync.warning.sync.paused", value: "Sync & Backup is Paused", comment: "Title of the warning message") + static let syncUnavailableMessage = NSLocalizedString("sync.warning.data.syncing.disabled", value: "Sorry, but Sync & Backup is currently unavailable. Please try again later.", comment: "Data syncing unavailable warning message") + static let syncUnavailableMessageUpgradeRequired = NSLocalizedString("sync.warning.data.syncing.disabled.upgrade.required", value: "Sorry, but Sync & Backup is no longer available in this app version. Please update DuckDuckGo to the latest version to continue.", comment: "Data syncing unavailable warning message") // swiftlint:enable line_length } diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/SaveRecoveryKeyView.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/SaveRecoveryKeyView.swift index 3fa628d10e..bfd4027131 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Views/SaveRecoveryKeyView.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/SaveRecoveryKeyView.swift @@ -39,22 +39,18 @@ public struct SaveRecoveryKeyView: View { @ViewBuilder func recoveryInfo() -> some View { - VStack(spacing: 26) { - HStack(spacing: 16) { - QRCodeView(string: model.key, size: 64) - - Text(model.key) - .fontWeight(.light) - .lineSpacing(1.6) - .lineLimit(3) - .applyKerning(2) - .truncationMode(.tail) - .monospaceSystemFont(ofSize: 16) - .frame(maxWidth: .infinity) - } - codeButtons() - } - .padding(.top, 20) + VStack(spacing: 26) { + Text(model.key) + .fontWeight(.light) + .lineSpacing(1.6) + .lineLimit(3) + .applyKerning(2) + .truncationMode(.tail) + .monospaceSystemFont(ofSize: 16) + .frame(maxWidth: .infinity) + codeButtons() + } + .padding(.top, 20) .padding(.horizontal, 20) .padding(.bottom, 12) .background(RoundedRectangle(cornerRadius: 10).foregroundColor(.black.opacity(0.03))) diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/SyncSettingsView.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/SyncSettingsView.swift index b6c38f5de1..c941ca8b0f 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Views/SyncSettingsView.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/SyncSettingsView.swift @@ -17,8 +17,9 @@ // limitations under the License. // -import SwiftUI import DesignResourcesKit +import DuckUI +import SwiftUI public struct SyncSettingsView: View { @@ -27,6 +28,7 @@ public struct SyncSettingsView: View { let timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect() @State var isSyncWithSetUpSheetVisible = false @State var isRecoverSyncedDataSheetVisible = false + @State var isEnvironmentSwitcherInstructionsVisible = false public init(model: SyncSettingsViewModel) { self.model = model @@ -45,6 +47,8 @@ public struct SyncSettingsView: View { if model.isSyncEnabled { + syncUnavailableViewWhileLoggedIn() + turnOffSync() // Sync Paused Errors @@ -65,6 +69,8 @@ public struct SyncSettingsView: View { } else { + syncUnavailableViewWhileLoggedOut() + syncWithAnotherDeviceView() otherOptions() @@ -88,9 +94,19 @@ public struct SyncSettingsView: View { @State var selectedDevice: SyncSettingsViewModel.Device? } -// Sync Set up Views +// MARK: - Sync Set up Views + extension SyncSettingsView { + @ViewBuilder + fileprivate func syncUnavailableViewWhileLoggedOut() -> some View { + if !model.isDataSyncingAvailable || !model.isConnectingDevicesAvailable || !model.isAccountCreationAvailable { + SyncWarningMessageView(title: UserText.syncUnavailableTitle, message: UserText.syncUnavailableMessage) + } else { + EmptyView() + } + } + @ViewBuilder func syncWithAnotherDeviceView() -> some View { Section { @@ -104,23 +120,16 @@ extension SyncSettingsView { .daxBodyRegular() .multilineTextAlignment(.center) .foregroundColor(Color(designSystemColor: .textPrimary)) - Button(action: { - model.scanQRCode() - }, label: { - Text(UserText.syncWithAnotherDeviceButton) - .daxButton() - .foregroundColor(.white) - .frame(maxWidth: 310) - .frame(height: 50) - .background( - RoundedRectangle(cornerRadius: 8) - .fill(Color(designSystemColor: .accent)) - ) - }) - .padding(.vertical, 16) + Button(UserText.syncWithAnotherDeviceButton, action: model.scanQRCode) + .buttonStyle(PrimaryButtonStyle(disabled: !model.isAccountCreationAvailable)) + .frame(maxWidth: 310) + .disabled(!model.isAccountCreationAvailable) + .padding(.vertical, 16) } Spacer() } + } header: { + devEnvironmentIndicator() } footer: { HStack { Spacer() @@ -135,37 +144,46 @@ extension SyncSettingsView { @ViewBuilder func otherOptions() -> some View { Section { - Text(UserText.syncAndBackUpThisDeviceLink) - .daxBodyRegular() - .foregroundColor(Color(designSystemColor: .accent)) - .onTapGesture { - isSyncWithSetUpSheetVisible = true - } - .sheet(isPresented: $isSyncWithSetUpSheetVisible, content: { - SyncWithServerView(model: model, onCancel: { - isSyncWithSetUpSheetVisible = false - }) + + Button(UserText.syncAndBackUpThisDeviceLink) { + isSyncWithSetUpSheetVisible = true + } + .sheet(isPresented: $isSyncWithSetUpSheetVisible, content: { + SyncWithServerView(model: model, onCancel: { + isSyncWithSetUpSheetVisible = false }) - Text(UserText.recoverSyncedDataLink) - .daxBodyRegular() - .foregroundColor(Color(designSystemColor: .accent)) - .onTapGesture { - isRecoverSyncedDataSheetVisible = true - } - .sheet(isPresented: $isRecoverSyncedDataSheetVisible, content: { - RecoverSyncedDataView(model: model, onCancel: { - isRecoverSyncedDataSheetVisible = false - }) + }) + .disabled(!model.isAccountCreationAvailable) + + Button(UserText.recoverSyncedDataLink) { + isRecoverSyncedDataSheetVisible = true + } + .sheet(isPresented: $isRecoverSyncedDataSheetVisible, content: { + RecoverSyncedDataView(model: model, onCancel: { + isRecoverSyncedDataSheetVisible = false }) + }) + .disabled(!model.isAccountRecoveryAvailable) + } header: { Text(UserText.otherOptionsSectionHeader) } } } +// MARK: - Sync Enabled Views -// Sync Enabled Views extension SyncSettingsView { + + @ViewBuilder + fileprivate func syncUnavailableViewWhileLoggedIn() -> some View { + if model.isDataSyncingAvailable { + EmptyView() + } else { + SyncWarningMessageView(title: UserText.syncPausedTitle, message: UserText.syncUnavailableMessage) + } + } + @ViewBuilder func deleteAllData() -> some View { Section { @@ -216,12 +234,9 @@ extension SyncSettingsView { .padding() } devicesList() - Button(action: { - model.scanQRCode() - }, label: { - Text(UserText.syncedDevicesSyncWithAnotherDeviceLabel) - .padding(.leading, 32) - }) + Button(UserText.syncedDevicesSyncWithAnotherDeviceLabel, action: model.scanQRCode) + .padding(.leading, 32) + .disabled(!model.isConnectingDevicesAvailable) } header: { Text(UserText.syncedDevicesSectionHeader) } @@ -265,6 +280,7 @@ extension SyncSettingsView { .fill(.green) .frame(width: 8) .padding(.bottom, 1) + devEnvironmentIndicator() } } footer: { Text(UserText.turnSyncOffSectionFooter) @@ -326,21 +342,43 @@ extension SyncSettingsView { } } - Section { - VStack(alignment: .leading, spacing: 4) { - Text(UserText.syncLimitExceededTitle) - .daxBodyBold() - Text(explanation) - .daxBodyRegular() + SyncWarningMessageView(title: UserText.syncLimitExceededTitle, message: explanation, buttonTitle: buttonTitle) { + switch itemType { + case .bookmarks: + model.manageBookmarks() + case .credentials: + model.manageLogins() } - Button(buttonTitle) { - switch itemType { - case .bookmarks: - model.manageBookmarks() - case .credentials: - model.manageLogins() + } + } + + @ViewBuilder + func devEnvironmentIndicator() -> some View { + if model.isOnDevEnvironment { + Button(action: { + isEnvironmentSwitcherInstructionsVisible.toggle() + }, label: { + if #available(iOS 15.0, *) { + Text("Dev environment") + .daxFootnoteRegular() + .padding(.horizontal, 10) + .padding(.vertical, 2) + .foregroundColor(.white) + .background(Color.red40) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } else { + Text("Dev environment") } + }) + .alert(isPresented: $isEnvironmentSwitcherInstructionsVisible) { + Alert( + title: Text("You're using Sync Development environment"), + primaryButton: .default(Text("Keep Development")), + secondaryButton: .destructive(Text("Switch to Production"), action: model.switchToProdEnvironment) + ) } + } else { + EmptyView() } } diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/SyncWarningMessageView.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/SyncWarningMessageView.swift new file mode 100644 index 0000000000..3d0ecd4d02 --- /dev/null +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/SyncWarningMessageView.swift @@ -0,0 +1,46 @@ +// +// SyncWarningMessageView.swift +// DuckDuckGo +// +// 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 + +struct SyncWarningMessageView: View { + let title: String + let message: String + let buttonTitle: String? + let buttonAction: (() -> Void)? + + init(title: String, message: String, buttonTitle: String? = nil, buttonAction: (() -> Void)? = nil) { + self.title = title + self.message = message + self.buttonTitle = buttonTitle + self.buttonAction = buttonAction + } + + var body: some View { + Section { + VStack(alignment: .leading, spacing: 4) { + Text("⚠️ " + title).daxBodyBold() + Text(message).daxBodyRegular() + } + if let buttonTitle, let buttonAction { + Button(buttonTitle, action: buttonAction) + } + } + } +} diff --git a/LocalPackages/Waitlist/Package.swift b/LocalPackages/Waitlist/Package.swift index 394fad7869..a666f90ef7 100644 --- a/LocalPackages/Waitlist/Package.swift +++ b/LocalPackages/Waitlist/Package.swift @@ -15,17 +15,30 @@ let package = Package( targets: ["Waitlist", "WaitlistMocks"]) ], dependencies: [ + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "100.0.2"), .package(url: "https://github.com/duckduckgo/DesignResourcesKit", exact: "2.0.0") ], targets: [ .target( name: "Waitlist", - dependencies: ["DesignResourcesKit"]), + dependencies: ["DesignResourcesKit"], + swiftSettings: [ + .define("DEBUG", .when(configuration: .debug)) + ], + plugins: [.plugin(name: "SwiftLintPlugin", package: "BrowserServicesKit")] + ), .target( name: "WaitlistMocks", - dependencies: ["Waitlist"]), + dependencies: ["Waitlist"], + swiftSettings: [ + .define("DEBUG", .when(configuration: .debug)) + ], + plugins: [.plugin(name: "SwiftLintPlugin", package: "BrowserServicesKit")] + ), .testTarget( name: "WaitlistTests", - dependencies: ["Waitlist", "WaitlistMocks"]) + dependencies: ["Waitlist", "WaitlistMocks"], + plugins: [.plugin(name: "SwiftLintPlugin", package: "BrowserServicesKit")] + ) ] ) diff --git a/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift b/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift index 73ad4b3c74..8bb0ed7ea8 100644 --- a/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift +++ b/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift @@ -37,10 +37,33 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { private static var packetTunnelProviderEvents: EventMapping = .init { event, _, _, _ in switch event { case .userBecameActive: - DailyPixel.fire(pixel: .networkProtectionActiveUser) - case .reportLatency, .reportTunnelFailure, .reportConnectionAttempt: - // TODO: Fire these pixels - break + let settings = VPNSettings(defaults: .networkProtectionGroupDefaults) + DailyPixel.fire(pixel: .networkProtectionActiveUser, + withAdditionalParameters: ["cohort": UniquePixel.dateString(for: settings.vpnFirstEnabled)]) + case .reportConnectionAttempt(attempt: let attempt): + switch attempt { + case .connecting: + DailyPixel.fireDailyAndCount(pixel: .networkProtectionEnableAttemptConnecting) + case .success: + DailyPixel.fireDailyAndCount(pixel: .networkProtectionEnableAttemptSuccess) + case .failure: + DailyPixel.fireDailyAndCount(pixel: .networkProtectionEnableAttemptFailure) + } + case .reportTunnelFailure(result: let result): + switch result { + case .failureDetected: + DailyPixel.fireDailyAndCount(pixel: .networkProtectionTunnelFailureDetected) + case .failureRecovered: + DailyPixel.fireDailyAndCount(pixel: .networkProtectionTunnelFailureRecovered) + } + case .reportLatency(result: let result): + switch result { + case .error: + DailyPixel.fire(pixel: .networkProtectionLatencyError) + case .quality(let quality): + guard quality != .unknown else { return } + DailyPixel.fireDailyAndCount(pixel: .networkProtectionLatency(quality: quality)) + } case .rekeyCompleted: Pixel.fire(pixel: .networkProtectionRekeyCompleted) } @@ -119,8 +142,9 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { params[PixelParameters.keychainErrorCode] = String(status) case .wireGuardCannotLocateTunnelFileDescriptor: pixelEvent = .networkProtectionWireguardErrorCannotLocateTunnelFileDescriptor - case .wireGuardInvalidState: + case .wireGuardInvalidState(reason: let reason): pixelEvent = .networkProtectionWireguardErrorInvalidState + params[PixelParameters.reason] = reason case .wireGuardDnsResolution: pixelEvent = .networkProtectionWireguardErrorFailedDNSResolution case .wireGuardSetNetworkSettings(let error): @@ -136,6 +160,8 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { params[PixelParameters.function] = function params[PixelParameters.line] = String(line) pixelError = error + case .failedToRetrieveAuthToken: + return case .failedToFetchLocationList: return case .failedToParseLocationListResponse: diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 40b796aeb0..5713dc41c2 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -168,7 +168,8 @@ private_lane :build_release do |options| export_method: "app-store", scheme: "DuckDuckGo", export_options: "appStoreExportOptions.plist", - derived_data_path: "DerivedData" + derived_data_path: "DerivedData", + xcargs: "-skipPackagePluginValidation" ) end @@ -180,7 +181,8 @@ private_lane :build_alpha do |options| configuration: "Alpha", scheme: "DuckDuckGo-Alpha", export_options: "alphaExportOptions.plist", - derived_data_path: "DerivedData" + derived_data_path: "DerivedData", + xcargs: "-skipPackagePluginValidation" ) end diff --git a/fastlane/metadata/default/release_notes.txt b/fastlane/metadata/default/release_notes.txt index 6cd7a9e939..ea2040b588 100644 --- a/fastlane/metadata/default/release_notes.txt +++ b/fastlane/metadata/default/release_notes.txt @@ -1,3 +1,2 @@ -- Bug fixes and other improvements. - +Bug fixes and other improvements. Join our fully distributed team and help raise the standard of trust online! https://duckduckgo.com/hiring diff --git a/fastlane/metadata/review_information/notes.txt b/fastlane/metadata/review_information/notes.txt deleted file mode 100644 index f4258bee0d..0000000000 --- a/fastlane/metadata/review_information/notes.txt +++ /dev/null @@ -1,3 +0,0 @@ -See "What's New in This Version" - -This app does not include Siri Shortcuts.