diff --git a/.github/workflows/sync-end-to-end.yml b/.github/workflows/sync-end-to-end.yml index b57d4d4a69..0808ae0570 100644 --- a/.github/workflows/sync-end-to-end.yml +++ b/.github/workflows/sync-end-to-end.yml @@ -69,7 +69,7 @@ jobs: name: Sync End To End Tests needs: build-for-sync-end-to-end-tests runs-on: macos-13-xlarge - timeout-minutes: 60 + timeout-minutes: 90 strategy: matrix: os-version: [15, 16, 17] @@ -92,20 +92,25 @@ jobs: name: duckduckgo-ios-app path: DerivedData/Build/Products/Debug-iphonesimulator/DuckDuckGo.app + - name: Install Maestro + run: | + export MAESTRO_VERSION=1.36.0; curl -Ls "https://get.maestro.mobile.dev" | bash + + - name: Overwrite default config with sync one + run: | + cp .maestro/config-sync .maestro/config.yaml + - 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: ${{ matrix.os-version }} - workspace: .maestro - include-tags: sync - env: | - CODE=${{ steps.sync-recovery-code.outputs.recovery-code }} + run: | + export PATH="$PATH":"$HOME/.maestro/bin"; maestro cloud --apiKey ${{ secrets.MAESTRO_CLOUD_API_KEY }} --env=CODE=${{ steps.sync-recovery-code.outputs.recovery-code }} --fail-on-timeout=true --timeout=150 --ios-version=${{ matrix.os-version }} --include-tags=sync DerivedData/Build/Products/Debug-iphonesimulator/DuckDuckGo.app .maestro/ + + - name: Reset config + run: | + git checkout .maestro/config.yaml notify-failure: name: Notify on failure - if: ${{ always() && contains(join(needs.*.result, ','), 'failure') }} + if: ${{ always() && contains(join(needs.*.result, ','), 'failure') && github.ref_name == 'main' }} needs: [build-for-sync-end-to-end-tests, sync-end-to-end-tests] runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 8540891709..6c91b16da7 100644 --- a/.gitignore +++ b/.gitignore @@ -77,3 +77,4 @@ Configuration/ExternalDeveloper.xcconfig scripts/assets DuckDuckGoTests/NetworkProtectionVPNLocationViewModelTests.swift*.plist +*.profraw diff --git a/.maestro/browser_features/swipe_tabs.yaml b/.maestro/browser_features/swipe_tabs.yaml new file mode 100644 index 0000000000..4f79623a01 --- /dev/null +++ b/.maestro/browser_features/swipe_tabs.yaml @@ -0,0 +1,68 @@ +# swipe_tabs.yaml +appId: com.duckduckgo.mobile.ios +tags: + - release + +--- + +# Set up +- clearState +- launchApp +- runFlow: + when: + visible: + text: "Let’s Do It!" + index: 0 + file: ../shared/onboarding.yaml + +# Load Site +- assertVisible: + id: "searchEntry" +- tapOn: + id: "searchEntry" +- inputText: "https://www.search-company.site" +- pressKey: Enter + +# Manage onboarding +- runFlow: + when: + visible: + text: "Got It" + index: 0 + file: ../shared/onboarding_browsing.yaml + +- assertVisible: "Search engine" + +# Open New Tab +- tapOn: "Tab Switcher" +- tapOn: + id: Add + +# Perform a search +- assertVisible: + id: "searchEntry" +- tapOn: + id: "searchEntry" +- inputText: "https://privacy-test-pages.site/features/favicon/" +- pressKey: Enter + +- assertVisible: "Favicon Tests" + +# Swipe to first tab +- swipe: + start: 10%, 10% + end: 90%, 10% + +- assertVisible: "Search engine" + +# Ensure address bar still works +- assertVisible: + id: "searchEntry" +- tapOn: + id: "searchEntry" +- inputText: "https://privacy-test-pages.site/features/download/" +- pressKey: Enter + +- assertVisible: "Download PDF" + +# TODO when settings experiment finishes update this test to open settings and move bar to bottom then test swipe again diff --git a/.maestro/config-sync b/.maestro/config-sync new file mode 100644 index 0000000000..13ee9dbe32 --- /dev/null +++ b/.maestro/config-sync @@ -0,0 +1,15 @@ +appStartup: + enabled: false +appSize: + enabled: false +flows: + - "**" +executionOrder: + flowsOrder: + - 01_create_account + - 02_login_account + - 03_recover_account + - 04_sync_data_setup + - 05_sync_data_check + - 06_delete_account + diff --git a/.maestro/config.yaml b/.maestro/config.yaml index b74550e22c..fd2b9297ea 100644 --- a/.maestro/config.yaml +++ b/.maestro/config.yaml @@ -3,4 +3,5 @@ appStartup: appSize: enabled: false flows: - - "**" \ No newline at end of file + - "**" + diff --git a/.maestro/sync_tests/01_create_account.yaml b/.maestro/sync_tests/01_create_account.yaml index 4cfb810980..225b8a7ec5 100644 --- a/.maestro/sync_tests/01_create_account.yaml +++ b/.maestro/sync_tests/01_create_account.yaml @@ -1,6 +1,7 @@ appId: com.duckduckgo.mobile.ios tags: - sync +name: 01_create_account --- diff --git a/.maestro/sync_tests/02_login_account.yaml b/.maestro/sync_tests/02_login_account.yaml index b2214cb476..ab524933e8 100644 --- a/.maestro/sync_tests/02_login_account.yaml +++ b/.maestro/sync_tests/02_login_account.yaml @@ -1,6 +1,7 @@ appId: com.duckduckgo.mobile.ios tags: - sync +name: 02_login_account --- diff --git a/.maestro/sync_tests/03_recover_account.yaml b/.maestro/sync_tests/03_recover_account.yaml index 99a542d74a..5ce0eaa697 100644 --- a/.maestro/sync_tests/03_recover_account.yaml +++ b/.maestro/sync_tests/03_recover_account.yaml @@ -1,6 +1,7 @@ appId: com.duckduckgo.mobile.ios tags: - sync +name: 03_recover_account --- diff --git a/.maestro/sync_tests/04_sync_data.yaml b/.maestro/sync_tests/04_sync_data_setup.yaml similarity index 74% rename from .maestro/sync_tests/04_sync_data.yaml rename to .maestro/sync_tests/04_sync_data_setup.yaml index 48f6a617ad..0bdb945862 100644 --- a/.maestro/sync_tests/04_sync_data.yaml +++ b/.maestro/sync_tests/04_sync_data_setup.yaml @@ -1,6 +1,7 @@ appId: com.duckduckgo.mobile.ios tags: - sync +name: 04_sync_data_setup --- @@ -65,25 +66,6 @@ tags: - runFlow: file: ../shared/remove_local_logins.yaml -# Login -- tapOn: Settings -- tapOn: Sync & Backup -- runFlow: - file: ../shared/sync_login.yaml - -# Verify bookmarks have been merged -- tapOn: Settings -- runFlow: - file: ../shared/sync_verify_bookmarks.yaml - -# Verify favorites are unified -- tapOn: Done -- tapOn: Settings -- runFlow: - file: ../shared/sync_verify_unified_favorites.yaml - -# Verify logins -- tapOn: Settings -- runFlow: - file: ../shared/sync_verify_logins.yaml - +# Maestro seems to get stuck without an additional step here +- assertVisible: + id: searchEntry diff --git a/.maestro/sync_tests/05_sync_data_check.yaml b/.maestro/sync_tests/05_sync_data_check.yaml new file mode 100644 index 0000000000..1716e3637f --- /dev/null +++ b/.maestro/sync_tests/05_sync_data_check.yaml @@ -0,0 +1,56 @@ +appId: com.duckduckgo.mobile.ios +tags: + - sync +name: 05_sync_data_check + +--- +# IMPORTANT: This test is strictly related to 04_sync_data_setup +# and it will fail if 04 is not executed before. +# The test is split in two different flow to accomodate +# for Maestro CI max execution time. +# Clear and launch +- clearState +- launchApp + +# Run onboarding Flow +- runFlow: + when: + visible: + text: "Let’s Do It!" + index: 0 + file: ../shared/onboarding.yaml + +# Copy Recovery Code +- tapOn: Settings +- runFlow: + file: ../shared/copy_recovery_code_from_settings.yaml + env: + CODE: ${CODE} + +# Set Internal User +- runFlow: + file: ../shared/set_internal_user_from_settings.yaml + +# Login +- assertVisible: Sync & Backup +- tapOn: Sync & Backup +- runFlow: + file: ../shared/sync_login.yaml +- assertVisible: Sync & Backup + +# Verify bookmarks have been merged +- tapOn: Settings +- runFlow: + file: ../shared/sync_verify_bookmarks.yaml + +# Verify favorites are unified +- tapOn: Done +- tapOn: Settings +- runFlow: + file: ../shared/sync_verify_unified_favorites.yaml + +# Verify logins +- tapOn: Settings +- runFlow: + file: ../shared/sync_verify_logins.yaml + diff --git a/.maestro/sync_tests/05_delete_account.yaml b/.maestro/sync_tests/06_delete_account.yaml similarity index 96% rename from .maestro/sync_tests/05_delete_account.yaml rename to .maestro/sync_tests/06_delete_account.yaml index 541a8f9174..8321465592 100644 --- a/.maestro/sync_tests/05_delete_account.yaml +++ b/.maestro/sync_tests/06_delete_account.yaml @@ -1,7 +1,7 @@ appId: com.duckduckgo.mobile.ios tags: - sync - +name: 06_delete_account --- # Clear and launch diff --git a/Configuration/Version.xcconfig b/Configuration/Version.xcconfig index 9e149d131c..a9f2860809 100644 --- a/Configuration/Version.xcconfig +++ b/Configuration/Version.xcconfig @@ -1 +1 @@ -MARKETING_VERSION = 7.121.0 +MARKETING_VERSION = 7.122.0 diff --git a/Core/AppPrivacyConfigurationDataProvider.swift b/Core/AppPrivacyConfigurationDataProvider.swift index 5cc3fc28f9..b79da7cbf7 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 = "\"e9c1178b9f9d725dd3f182c7847e848b\"" - public static let embeddedDataSHA = "c64ce5c56a9cd93b5479f4997b34835992d06f85b5a00e2df5b8b3fde1796369" + public static let embeddedDataETag = "\"81e5722b1fef45e0e8f0332b500d12d8\"" + public static let embeddedDataSHA = "e0f7b8e519cb818fdf512e0ee9251f176f1ed6ed283d2b6c865f264e23eae377" } public var embeddedDataEtag: String { diff --git a/Core/AppTrackerDataSetProvider.swift b/Core/AppTrackerDataSetProvider.swift index f9dea133e2..28ec784feb 100644 --- a/Core/AppTrackerDataSetProvider.swift +++ b/Core/AppTrackerDataSetProvider.swift @@ -23,8 +23,8 @@ import BrowserServicesKit final public class AppTrackerDataSetProvider: EmbeddedDataProvider { public struct Constants { - public static let embeddedDataETag = "\"004872ea25514c61490f047cd5f088b8\"" - public static let embeddedDataSHA = "4a06a3df999fad7829baecc9ccfcbc54c20526ba304f6c5f2846899d29b281cc" + public static let embeddedDataETag = "\"ea184137cdaa19ca5de76352215a9e0e\"" + public static let embeddedDataSHA = "faa4dfbef4903710374b153c9a87e09b713fc19d64fa0bcfd1fd392fff93af21" } public var embeddedDataEtag: String { diff --git a/Core/DataStoreWarmup.swift b/Core/DataStoreWarmup.swift index 0ef67ac0b6..298a8b929f 100644 --- a/Core/DataStoreWarmup.swift +++ b/Core/DataStoreWarmup.swift @@ -23,11 +23,21 @@ import WebKit /// WKWebsiteDataStore is basically non-functional until a web view has been instanciated and a page is successfully loaded. public class DataStoreWarmup { + public enum ApplicationState: String { + case active + case inactive + case background + case handlingShortcut + case unknown + } + public init() { } @MainActor - public func ensureReady() async { + public func ensureReady(applicationState: ApplicationState) async { + Pixel.fire(pixel: .webkitWarmupStart(appState: applicationState.rawValue)) await BlockingNavigationDelegate().loadInBackgroundWebView(url: URL(string: "about:blank")!) + Pixel.fire(pixel: .webkitWarmupFinished(appState: applicationState.rawValue)) } } diff --git a/Core/DefaultVariantManager.swift b/Core/DefaultVariantManager.swift index f4fc7ccac0..1be1051020 100644 --- a/Core/DefaultVariantManager.swift +++ b/Core/DefaultVariantManager.swift @@ -27,7 +27,6 @@ extension FeatureName { // public static let experimentalFeature = FeatureName(rawValue: "experimentalFeature") public static let history = FeatureName(rawValue: "history") - public static let newSuggestionLogic = FeatureName(rawValue: "newSuggestionLogic") } public struct VariantIOS: Variant { @@ -62,7 +61,9 @@ public struct VariantIOS: Variant { VariantIOS(name: "sc", weight: doNotAllocate, isIncluded: When.always, features: []), VariantIOS(name: "sd", weight: doNotAllocate, isIncluded: When.always, features: []), VariantIOS(name: "se", weight: doNotAllocate, isIncluded: When.always, features: []), - VariantIOS(name: "mc", weight: doNotAllocate, isIncluded: When.inEnglish, features: [.newSuggestionLogic]), + + // This needs to stay until we finish rolling out history to all users... + // This ensures that users who previously had do not lose it. VariantIOS(name: "md", weight: doNotAllocate, isIncluded: When.inEnglish, features: [.history]), returningUser diff --git a/Core/FeatureFlag.swift b/Core/FeatureFlag.swift index 6792271ce9..de0518b0c7 100644 --- a/Core/FeatureFlag.swift +++ b/Core/FeatureFlag.swift @@ -35,6 +35,7 @@ public enum FeatureFlag: String { case networkProtectionWaitlistActive case autoconsentOnByDefault case history + case historyRollout } extension FeatureFlag: FeatureFlagSourceProviding { @@ -68,7 +69,9 @@ extension FeatureFlag: FeatureFlagSourceProviding { return .remoteReleasable(.subfeature(AutoconsentSubfeature.onByDefault)) case .history: return .remoteReleasable(.feature(.history)) - + case .historyRollout: + return .remoteReleasable(.subfeature(HistorySubFeature.onByDefault)) + } } } diff --git a/Core/HistoryCapture.swift b/Core/HistoryCapture.swift index 68e90a8953..ffd8b2834e 100644 --- a/Core/HistoryCapture.swift +++ b/Core/HistoryCapture.swift @@ -39,12 +39,13 @@ public class HistoryCapture { } public func webViewDidCommit(url: URL) { + let url = url.urlOrDuckDuckGoCleanQuery self.url = url coordinator.addVisit(of: url) } public func titleDidChange(_ title: String?, forURL url: URL?) { - guard let url, self.url == url else { + guard let url = url?.urlOrDuckDuckGoCleanQuery, self.url == url else { return } diff --git a/Core/HistoryManager.swift b/Core/HistoryManager.swift index b0b833224e..ab2e7785da 100644 --- a/Core/HistoryManager.swift +++ b/Core/HistoryManager.swift @@ -27,21 +27,32 @@ import Persistence public protocol HistoryManaging { var historyCoordinator: HistoryCoordinating { get } - func loadStore() + func loadStore(onCleanFinished: @escaping () -> Void) throws } +// Used for controlling incremental rollout +public enum HistorySubFeature: String, PrivacySubfeature { + public var parent: PrivacyFeature { + .history + } + + case onByDefault +} + public class HistoryManager: HistoryManaging { let privacyConfigManager: PrivacyConfigurationManaging let variantManager: VariantManager let database: CoreDataDatabase - let onStoreLoadFailed: (Error) -> Void + let internalUserDecider: InternalUserDecider + let isEnabledByUser: () -> Bool private var currentHistoryCoordinator: HistoryCoordinating? public var historyCoordinator: HistoryCoordinating { - guard isHistoryFeatureEnabled() else { + guard isHistoryFeatureEnabled(), + isEnabledByUser() else { currentHistoryCoordinator = nil return NullHistoryCoordinator() } @@ -50,31 +61,47 @@ public class HistoryManager: HistoryManaging { return currentHistoryCoordinator } - var loadError: Error? - database.loadStore { _, error in - loadError = error - } - - if let loadError { - onStoreLoadFailed(loadError) - return NullHistoryCoordinator() + let coordinator = makeDatabaseHistoryCoordinator() + coordinator.loadHistory { + // no-op - only done here in case it was flipped in settings } - let context = database.makeContext(concurrencyType: .privateQueueConcurrencyType) - let historyCoordinator = HistoryCoordinator(historyStoring: HistoryStore(context: context, eventMapper: HistoryStoreEventMapper())) - currentHistoryCoordinator = historyCoordinator - return historyCoordinator + currentHistoryCoordinator = coordinator + return coordinator } - public init(privacyConfigManager: PrivacyConfigurationManaging, variantManager: VariantManager, database: CoreDataDatabase, onStoreLoadFailed: @escaping (Error) -> Void) { + public init(privacyConfigManager: PrivacyConfigurationManaging, + variantManager: VariantManager, + database: CoreDataDatabase, + internalUserDecider: InternalUserDecider, + isEnabledByUser: @autoclosure @escaping () -> Bool) { + self.privacyConfigManager = privacyConfigManager self.variantManager = variantManager self.database = database - self.onStoreLoadFailed = onStoreLoadFailed + self.internalUserDecider = internalUserDecider + self.isEnabledByUser = isEnabledByUser } - func isHistoryFeatureEnabled() -> Bool { - return privacyConfigManager.privacyConfig.isEnabled(featureKey: .history) && variantManager.isSupported(feature: .history) + /// Determines if the history feature is enabled. This code will need to be cleaned up once the roll out is at 100% + public func isHistoryFeatureEnabled() -> Bool { + guard privacyConfigManager.privacyConfig.isEnabled(featureKey: .history) else { + // Whatever happens if this is disabled then disable the feature + return false + } + + if internalUserDecider.isInternalUser { + // Internal users get the feature + return true + } + + if variantManager.isSupported(feature: .history) { + // Users in the experiment get the feature + return true + } + + // Handles incremental roll out to everyone else + return privacyConfigManager.privacyConfig.isSubfeatureEnabled(HistorySubFeature.onByDefault) } public func removeAllHistory() async { @@ -85,12 +112,19 @@ public class HistoryManager: HistoryManaging { } } - public func loadStore() { - historyCoordinator.loadHistory { - // Do migrations here if needed + public func loadStore(onCleanFinished: @escaping () -> Void) { + let coordinator = makeDatabaseHistoryCoordinator() + coordinator.loadHistory { + onCleanFinished() } } + private func makeDatabaseHistoryCoordinator() -> HistoryCoordinator { + let context = database.makeContext(concurrencyType: .privateQueueConcurrencyType) + let historyCoordinator = HistoryCoordinator(historyStoring: HistoryStore(context: context, eventMapper: HistoryStoreEventMapper())) + return historyCoordinator + } + } class NullHistoryCoordinator: HistoryCoordinating { diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index 5978ca8d7e..34413a1328 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -123,11 +123,17 @@ extension Pixel { case favoriteLaunchedWebsite case favoriteLaunchedWidget + case autocompleteMessageShown + case autocompleteMessageDismissed case autocompleteClickPhrase case autocompleteClickWebsite case autocompleteClickBookmark case autocompleteClickFavorite - case autocompleteClickHistory + case autocompleteClickSearchHistory + case autocompleteClickSiteHistory + case autocompleteDisplayedLocalBookmark + case autocompleteDisplayedLocalFavorite + case autocompleteDisplayedLocalHistory case feedbackPositive case feedbackNegativePrefix(category: String) @@ -447,16 +453,20 @@ extension Pixel { case blankOverlayNotDismissed - case cookieDeletionTimedOut + case cookieDeletionTime(_ time: BucketAggregation) case cookieDeletionLeftovers - + case legacyDataClearingTime(_ time: BucketAggregation) + + case webkitWarmupStart(appState: String) + case webkitWarmupFinished(appState: String) + case cachedTabPreviewsExceedsTabCount case cachedTabPreviewRemovalError case missingDownloadedFile case unhandledDownload - case compilationResult(result: CompileRulesResult, waitTime: CompileRulesWaitTime, appState: AppState) + case compilationResult(result: CompileRulesResult, waitTime: BucketAggregation, appState: AppState) case emailAutofillKeychainError @@ -489,7 +499,6 @@ extension Pixel { case debugCannotClearObservationsDatabase case debugWebsiteDataStoresNotClearedMultiple case debugWebsiteDataStoresNotClearedOne - case debugCookieCleanupError case debugBookmarksMigratedMoreThanOnce @@ -553,6 +562,7 @@ extension Pixel { case syncWrongEnvironment case swipeTabsUsed + case swipeTabsIncorrectScrollState case swipeTabsUsedDaily case swipeToOpenNewTab @@ -817,11 +827,17 @@ extension Pixel.Event { case .favoriteLaunchedWebsite: return "m_favorite_launched_website" case .favoriteLaunchedWidget: return "m_favorite_launched_widget" + case .autocompleteMessageShown: return "m_autocomplete_message_shown" + case .autocompleteMessageDismissed: return "m_autocomplete_message_dismissed" case .autocompleteClickPhrase: return "m_autocomplete_click_phrase" case .autocompleteClickWebsite: return "m_autocomplete_click_website" case .autocompleteClickBookmark: return "m_autocomplete_click_bookmark" case .autocompleteClickFavorite: return "m_autocomplete_click_favorite" - case .autocompleteClickHistory: return "m_autocomplete_click_history" + case .autocompleteClickSearchHistory: return "m_autocomplete_click_history_search" + case .autocompleteClickSiteHistory: return "m_autocomplete_click_history_site" + case .autocompleteDisplayedLocalBookmark: return "m_autocomplete_display_local_bookmark" + case .autocompleteDisplayedLocalFavorite: return "m_autocomplete_display_local_favorite" + case .autocompleteDisplayedLocalHistory: return "m_autocomplete_display_local_history" case .feedbackPositive: return "mfbs_positive_submit" case .feedbackNegativePrefix(category: let category): return "mfbs_negative_\(category)" @@ -1114,9 +1130,17 @@ extension Pixel.Event { case .blankOverlayNotDismissed: return "m_d_ovs" - case .cookieDeletionTimedOut: return "m_debug_cookie-clearing-timeout" + case .cookieDeletionTime(let aggregation): + return "m_debug_cookie-clearing-time-\(aggregation)" + case .legacyDataClearingTime(let aggregation): + return "m_debug_legacy-data-clearing-time-\(aggregation)" case .cookieDeletionLeftovers: return "m_cookie_deletion_leftovers" - + + case .webkitWarmupStart(let appState): + return "m_webkit-warmup-start-\(appState)" + case .webkitWarmupFinished(let appState): + return "m_webkit-warmup-finished-\(appState)" + case .cachedTabPreviewsExceedsTabCount: return "m_d_tpetc" case .cachedTabPreviewRemovalError: return "m_d_tpre" @@ -1143,7 +1167,6 @@ extension Pixel.Event { case .debugCannotClearObservationsDatabase: return "m_d_cannot_clear_observations_database" case .debugWebsiteDataStoresNotClearedMultiple: return "m_d_wkwebsitedatastoresnotcleared_multiple" case .debugWebsiteDataStoresNotClearedOne: return "m_d_wkwebsitedatastoresnotcleared_one" - case .debugCookieCleanupError: return "m_d_cookie-cleanup-error" // MARK: Ad Attribution @@ -1215,6 +1238,7 @@ extension Pixel.Event { case .syncWrongEnvironment: return "m_d_sync_wrong_environment_u" case .swipeTabsUsed: return "m_swipe-tabs-used" + case .swipeTabsIncorrectScrollState: return "m_swipe-tabs.incorrect-scrollview-state" case .swipeTabsUsedDaily: return "m_swipe-tabs-used-daily" case .swipeToOpenNewTab: return "m_addressbar_swipe_new_tab" @@ -1392,32 +1416,38 @@ extension Pixel.Event { // swiftlint:disable file_length extension Pixel.Event { - public enum CompileRulesWaitTime: String, CustomStringConvertible { - + public enum BucketAggregation: String, CustomStringConvertible { + public var description: String { rawValue } - case noWait = "0" - case lessThan1s = "1" - case lessThan5s = "5" - case lessThan10s = "10" - case lessThan20s = "20" - case lessThan40s = "40" + case zero = "0" + case lessThan01 = "0.1" + case lessThan05 = "0.5" + case lessThan1 = "1" + case lessThan5 = "5" + case lessThan10 = "10" + case lessThan20 = "20" + case lessThan40 = "40" case more - public init(waitTime: TimeInterval) { - switch waitTime { + public init(number: Double) { + switch number { case 0: - self = .noWait + self = .zero + case ...0.1: + self = .lessThan01 + case ...0.5: + self = .lessThan05 case ...1: - self = .lessThan1s + self = .lessThan1 case ...5: - self = .lessThan5s + self = .lessThan5 case ...10: - self = .lessThan10s + self = .lessThan10 case ...20: - self = .lessThan20s + self = .lessThan20 case ...40: - self = .lessThan40s + self = .lessThan40 default: self = .more } diff --git a/Core/PixelExperiment.swift b/Core/PixelExperiment.swift index e5b839ce14..2824c32b19 100644 --- a/Core/PixelExperiment.swift +++ b/Core/PixelExperiment.swift @@ -54,7 +54,7 @@ public enum PixelExperiment: String, CaseIterable { logic.install() } - static func cleanup() { + public static func cleanup() { logic.cleanup() } diff --git a/Core/StatisticsLoader.swift b/Core/StatisticsLoader.swift index 6d4bae6c06..60b5fe14dc 100644 --- a/Core/StatisticsLoader.swift +++ b/Core/StatisticsLoader.swift @@ -141,7 +141,3 @@ public class StatisticsLoader { } } } - -extension NSNotification.Name { - public static let searchDAU: NSNotification.Name = Notification.Name(rawValue: "com.duckduckgo.notification.searchDAU") -} diff --git a/Core/UserAgentManager.swift b/Core/UserAgentManager.swift index 9c9225e6eb..fecbbbf503 100644 --- a/Core/UserAgentManager.swift +++ b/Core/UserAgentManager.swift @@ -32,6 +32,8 @@ public protocol UserAgentManager { func userAgent(isDesktop: Bool) -> String + func userAgent(isDesktop: Bool, url: URL?) -> String + } public class DefaultUserAgentManager: UserAgentManager { @@ -60,6 +62,10 @@ public class DefaultUserAgentManager: UserAgentManager { return userAgent.agent(forUrl: nil, isDesktop: isDesktop) } + public func userAgent(isDesktop: Bool, url: URL?) -> String { + return userAgent.agent(forUrl: url, isDesktop: isDesktop) + } + public func update(request: inout URLRequest, isDesktop: Bool) { request.addValue(userAgent.agent(forUrl: nil, isDesktop: isDesktop), forHTTPHeaderField: "User-Agent") } diff --git a/Core/UserDefaultsPropertyWrapper.swift b/Core/UserDefaultsPropertyWrapper.swift index bf92b8a80b..9accccda03 100644 --- a/Core/UserDefaultsPropertyWrapper.swift +++ b/Core/UserDefaultsPropertyWrapper.swift @@ -145,9 +145,10 @@ public struct UserDefaultsWrapper { case pixelExperimentCohort = "com.duckduckgo.ios.pixel.experiment.cohort" case pixelExperimentEnrollmentDate = "com.duckduckgo.ios.pixel.experiment.enrollment.date" + case historyMessageDisplayCount = "com.duckduckgo.ios.historyMessage.displayCount" + case historyMessageDismissed = "com.duckduckgo.ios.historyMessage.dismissed" case pixelExperimentForBrokenSitesInstalled = "com.duckduckgo.ios.pixel.experiment.for.broken.sites.installed" case pixelExperimentForBrokenSitesCohort = "com.duckduckgo.ios.pixel.experiment.for.broken.sites.cohort" - } private let key: Key diff --git a/Core/WebCacheManager.swift b/Core/WebCacheManager.swift index faefc24b3b..682afae536 100644 --- a/Core/WebCacheManager.swift +++ b/Core/WebCacheManager.swift @@ -66,22 +66,14 @@ public class WebCacheManager { public func removeCookies(forDomains domains: [String], dataStore: WKWebsiteDataStore) async { - - let timeoutTask = Task.detached { - try? await Task.sleep(interval: 5.0) - if !Task.isCancelled { - Pixel.fire(pixel: .cookieDeletionTimedOut, withAdditionalParameters: [ - PixelParameters.removeCookiesTimedOut: "1" - ]) - } - } - + let startTime = CACurrentMediaTime() let cookieStore = dataStore.httpCookieStore let cookies = await cookieStore.allCookies() for cookie in cookies where domains.contains(where: { cookie.matchesDomain($0) }) { await cookieStore.deleteCookie(cookie) } - timeoutTask.cancel() + let totalTime = CACurrentMediaTime() - startTime + Pixel.fire(pixel: .cookieDeletionTime(.init(number: totalTime))) } public func clear(cookieStorage: CookieStorage = CookieStorage(), @@ -131,15 +123,10 @@ extension WebCacheManager { } private func legacyDataClearing() async -> [HTTPCookie]? { - let timeoutTask = Task.detached { - try? await Task.sleep(interval: 5.0) - if !Task.isCancelled { - Pixel.fire(pixel: .cookieDeletionTimedOut, withAdditionalParameters: [ - PixelParameters.clearWebDataTimedOut: "1" - ]) - } - } + let dataStore = WKWebsiteDataStore.default() + let startTime = CACurrentMediaTime() + let cookies = await dataStore.httpCookieStore.allCookies() var types = WKWebsiteDataStore.allWebsiteDataTypes() types.insert("_WKWebsiteDataTypeMediaKeys") @@ -152,8 +139,11 @@ extension WebCacheManager { types.insert("_WKWebsiteDataTypeAlternativeServices") await dataStore.removeData(ofTypes: types, modifiedSince: .distantPast) + self.removeObservationsData() - timeoutTask.cancel() + let totalTime = CACurrentMediaTime() - startTime + Pixel.fire(pixel: .legacyDataClearingTime(.init(number: totalTime))) + return cookies } diff --git a/Core/ios-config.json b/Core/ios-config.json index 87f7cb549c..988980e7f4 100644 --- a/Core/ios-config.json +++ b/Core/ios-config.json @@ -1,6 +1,6 @@ { "readme": "https://github.com/duckduckgo/privacy-configuration", - "version": 1716212212537, + "version": 1716814163860, "features": { "adClickAttribution": { "readme": "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/#3rd-party-tracker-loading-protection", @@ -303,6 +303,9 @@ { "domain": "www.thrifty.com" }, + { + "domain": "sports.tipico.de" + }, { "domain": "marvel.com" }, @@ -317,7 +320,8 @@ "disabledCMPs": [ "generic-cosmetic", "termsfeed3", - "healthline-media" + "healthline-media", + "ketch" ] }, "state": "enabled", @@ -340,7 +344,7 @@ } } }, - "hash": "8ba3daa77d86d04b7dc15bb7ac36d768" + "hash": "e327901635a0f00ab376e026317ab975" }, "autofill": { "exceptions": [ @@ -1242,6 +1246,11 @@ "state": "disabled", "hash": "59ef4ce748dc657aa3d42756131dcb16" }, + "dummyWebMessageListener": { + "exceptions": [], + "state": "disabled", + "hash": "728493ef7a1488e4781656d3f9db84aa" + }, "elementHiding": { "exceptions": [ { @@ -3487,6 +3496,15 @@ } ] }, + { + "domain": "post-gazette.com", + "rules": [ + { + "selector": "[data-dfpads-position]", + "type": "hide-empty" + } + ] + }, { "domain": "prajwaldesai.com", "rules": [ @@ -3932,6 +3950,15 @@ } ] }, + { + "domain": "toledoblade.com", + "rules": [ + { + "selector": "[data-dfpads-position]", + "type": "hide-empty" + } + ] + }, { "domain": "tripadvisor.ca", "rules": [ @@ -4204,6 +4231,10 @@ { "selector": ".gam-placeholder", "type": "closest-empty" + }, + { + "selector": "[class*='sdaContainer']", + "type": "hide-empty" } ] }, @@ -4365,7 +4396,7 @@ ] }, "state": "enabled", - "hash": "659ee3a53847979bf626ff368c20fc57" + "hash": "4403e14c942cbd375d40b9eddbdc7115" }, "exceptionHandler": { "exceptions": [ @@ -4681,6 +4712,12 @@ { "domain": "tirerack.com" }, + { + "domain": "milesplit.live" + }, + { + "domain": "dollargeneral.com" + }, { "domain": "marvel.com" }, @@ -4700,7 +4737,7 @@ "privacy-test-pages.site" ] }, - "hash": "d4f8f4fd4016f14fad7bca4d72bb48c3" + "hash": "8d421aba941721a80861bdc2aec73ed8" }, "harmfulApis": { "settings": { @@ -6504,6 +6541,7 @@ "domains": [ "abril.com.br", "algomalegalclinic.com", + "bodyelectricvitality.com.au", "cosmicbook.news", "eatroyo.com", "thesimsresource.com", @@ -6626,6 +6664,7 @@ "newschannel20.com", "newschannel9.com", "okcfox.com", + "post-gazette.com", "raleighcw.com", "siouxlandnews.com", "southernoregoncw.com", @@ -6634,6 +6673,7 @@ "thecw46.com", "thecwtc.com", "thenationaldesk.com", + "toledoblade.com", "triblive.com", "turnto10.com", "univisionseattle.com", @@ -6688,7 +6728,8 @@ "rule": "grow.me/main.js", "domains": [ "budgetbytes.com", - "foodfornet.com" + "foodfornet.com", + "homesteadingfamily.com" ] }, { @@ -6931,11 +6972,7 @@ { "rule": "static.klaviyo.com/onsite/js/klaviyo.js", "domains": [ - "essentialpraxis.com", - "kidsguide.com", - "muc-off.com", - "paria.cc", - "urbanebikes.com" + "" ] }, { @@ -7786,6 +7823,12 @@ "domains": [ "" ] + }, + { + "rule": "c.slickstream.com/app/", + "domains": [ + "" + ] } ] }, @@ -8312,7 +8355,7 @@ "domain": "sundancecatalog.com" } ], - "hash": "d667a9b7a3d72a97c26bb0811ea71283" + "hash": "37e67043d8e92bcf5c31e05a7ccf065d" }, "trackingCookies1p": { "settings": { diff --git a/Core/trackerData.json b/Core/trackerData.json index 2c632f15c2..ef11c78f3c 100644 --- a/Core/trackerData.json +++ b/Core/trackerData.json @@ -1,6 +1,6 @@ { "_builtWith": { - "tracker-radar": "a8f276714a31d43fb185a1b233ed92f12680627ca54c6f98da44de9fe27f097e-4013b4e91930c643394cb31c6c745356f133b04f", + "tracker-radar": "9c02278e8bfb43db5e6b8756cdedc469924d600b8c4d6c9d7177de01405c8f5c-4013b4e91930c643394cb31c6c745356f133b04f", "tracker-surrogates": "0528e3226df15b1a3e319ad68ef76612a8f26623" }, "readme": "https://github.com/duckduckgo/tracker-blocklists", @@ -41441,7 +41441,8 @@ "twittercommunity.com", "twttr.com", "twttr.net", - "vine.co" + "vine.co", + "x.com" ], "prevalence": 8.79, "displayName": "Twitter" @@ -52872,6 +52873,7 @@ "twttr.com": "Twitter, Inc.", "twttr.net": "Twitter, Inc.", "vine.co": "Twitter, Inc.", + "x.com": "Twitter, Inc.", "ads1-adnow.com": "Mas Capital Group Ltd", "ads2-adnow.com": "Mas Capital Group Ltd", "ads3-adnow.com": "Mas Capital Group Ltd", diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 3fd6ddfb18..f405e7df35 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -213,24 +213,16 @@ 4B6484F327FD1E350050A7A1 /* MenuControllerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B6484E927FD1E340050A7A1 /* MenuControllerView.swift */; }; 4B6ED9452B992FE4007F5CAA /* vpn-dark-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = 4B6ED9442B992FE4007F5CAA /* vpn-dark-mode.json */; }; 4B75EA9226A266CB00018634 /* PrintingUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B75EA9126A266CB00018634 /* PrintingUserScript.swift */; }; - 4B78074E2B183A1F009DB2CF /* SurveyURLBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B78074D2B183A1F009DB2CF /* SurveyURLBuilder.swift */; }; + 4B78074E2B183A1F009DB2CF /* RemoteMessagingSurveyURLBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B78074D2B183A1F009DB2CF /* RemoteMessagingSurveyURLBuilder.swift */; }; 4B948E2629DCCDB9002531FA /* Persistence in Frameworks */ = {isa = PBXBuildFile; productRef = 4B948E2529DCCDB9002531FA /* Persistence */; }; - 4BB697A42B1D99C4003699B5 /* VPNWaitlistActivationDateStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B78074B2B1823C5009DB2CF /* VPNWaitlistActivationDateStore.swift */; }; - 4BB697A52B1D99C5003699B5 /* VPNWaitlistActivationDateStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B78074B2B1823C5009DB2CF /* VPNWaitlistActivationDateStore.swift */; }; 4BB7CBB02AF59C310014A35F /* VPNWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB7CBAF2AF59C310014A35F /* VPNWidget.swift */; }; 4BBBBA872B02E85400D965DA /* DesignResourcesKit in Frameworks */ = {isa = PBXBuildFile; productRef = 4BBBBA862B02E85400D965DA /* DesignResourcesKit */; }; - 4BBBBA8D2B031B4200D965DA /* VPNWaitlistDebugViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBBBA892B031B4200D965DA /* VPNWaitlistDebugViewController.swift */; }; - 4BBBBA8E2B031B4200D965DA /* VPNWaitlistViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBBBA8A2B031B4200D965DA /* VPNWaitlistViewController.swift */; }; - 4BBBBA8F2B031B4200D965DA /* VPNWaitlistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBBBA8B2B031B4200D965DA /* VPNWaitlistView.swift */; }; - 4BBBBA902B031B4200D965DA /* VPNWaitlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBBBA8C2B031B4200D965DA /* VPNWaitlist.swift */; }; 4BBBBA922B03291700D965DA /* VPNWaitlistUserText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBBBA912B03291700D965DA /* VPNWaitlistUserText.swift */; }; 4BC21A2F27238B7500229F0E /* RunLoopExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BC21A2C272388BD00229F0E /* RunLoopExtensionTests.swift */; }; 4BCBE45E2BA7E81F00FC75A1 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 4BCBE45D2BA7E81F00FC75A1 /* PrivacyInfo.xcprivacy */; }; 4BCBE4602BA7E87100FC75A1 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 4BCBE45F2BA7E87100FC75A1 /* PrivacyInfo.xcprivacy */; }; 4BCD14632B05AF2B000B1E4C /* NetworkProtectionAccessController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCD14622B05AF2B000B1E4C /* NetworkProtectionAccessController.swift */; }; 4BCD14672B05B682000B1E4C /* NetworkProtectionTermsAndConditionsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCD14662B05B682000B1E4C /* NetworkProtectionTermsAndConditionsStore.swift */; }; - 4BCD14692B05BDD5000B1E4C /* AppDelegate+Waitlists.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCD14682B05BDD5000B1E4C /* AppDelegate+Waitlists.swift */; }; - 4BCD146B2B05C4B5000B1E4C /* VPNWaitlistTermsAndConditionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCD146A2B05C4B5000B1E4C /* VPNWaitlistTermsAndConditionsViewController.swift */; }; 4BCD146D2B05DB09000B1E4C /* NetworkProtectionAccessControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCD146C2B05DB09000B1E4C /* NetworkProtectionAccessControllerTests.swift */; }; 4BE2756827304F57006B20B0 /* URLRequestExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE27566272F878F006B20B0 /* URLRequestExtension.swift */; }; 4BE67B012B96B741007335F7 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 4BE67B002B96B741007335F7 /* Common */; }; @@ -256,12 +248,17 @@ 6AC98419288055C1005FA9CA /* BarsAnimatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AC98418288055C1005FA9CA /* BarsAnimatorTests.swift */; }; 6F655BE22BAB289E00AC3597 /* DefaultTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F655BE12BAB289E00AC3597 /* DefaultTheme.swift */; }; 6F8496412BC3D8EE00ADA54E /* OnboardingButtonsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F8496402BC3D8EE00ADA54E /* OnboardingButtonsView.swift */; }; + 6FBF0F8B2BD7C0A900136CF0 /* AllProtectedCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FBF0F8A2BD7C0A900136CF0 /* AllProtectedCell.swift */; }; 6FD1BAE42B87A107000C475C /* AdAttributionPixelReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD1BAE12B87A107000C475C /* AdAttributionPixelReporter.swift */; }; 6FD1BAE52B87A107000C475C /* AdAttributionReporterStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD1BAE22B87A107000C475C /* AdAttributionReporterStorage.swift */; }; 6FD1BAE62B87A107000C475C /* AdAttributionFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD1BAE32B87A107000C475C /* AdAttributionFetcher.swift */; }; 6FD3AEE32B8F4EEB0060FCCC /* AdAttributionPixelReporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD3AEE12B8DFBB80060FCCC /* AdAttributionPixelReporterTests.swift */; }; 6FDA1FB32B59584400AC962A /* AddressDisplayHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDA1FB22B59584400AC962A /* AddressDisplayHelper.swift */; }; + 6FDB3F192BD11A4400F7A307 /* AutocompleteSuggestionsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDB3F182BD11A4400F7A307 /* AutocompleteSuggestionsModel.swift */; }; + 6FE095D82BD90AFB00490FF8 /* UniversalOmniBarState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE095D72BD90AFB00490FF8 /* UniversalOmniBarState.swift */; }; 6FF915822B88E07A0042AC87 /* AdAttributionFetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF915802B88E0750042AC87 /* AdAttributionFetcherTests.swift */; }; + 7BC571202BDBB877003B0CCE /* VPNActivationDateStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC5711F2BDBB877003B0CCE /* VPNActivationDateStore.swift */; }; + 7BC571212BDBB977003B0CCE /* VPNActivationDateStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC5711F2BDBB877003B0CCE /* VPNActivationDateStore.swift */; }; 83004E802193BB8200DA013C /* WKNavigationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83004E7F2193BB8200DA013C /* WKNavigationExtension.swift */; }; 83004E862193E5ED00DA013C /* TabViewControllerBrowsingMenuExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83004E852193E5ED00DA013C /* TabViewControllerBrowsingMenuExtension.swift */; }; 83004E882193E8C700DA013C /* TabViewControllerLongPressMenuExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83004E872193E8C700DA013C /* TabViewControllerLongPressMenuExtension.swift */; }; @@ -314,6 +311,9 @@ 851624C22B95F8BD002D5CD7 /* HistoryCapture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851624C12B95F8BD002D5CD7 /* HistoryCapture.swift */; }; 851624C52B9602A4002D5CD7 /* HistoryCaptureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851624C32B96029B002D5CD7 /* HistoryCaptureTests.swift */; }; 851624C72B96389D002D5CD7 /* HistoryDebugViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851624C62B96389D002D5CD7 /* HistoryDebugViewController.swift */; }; + 851672CF2BED1F9500592F24 /* HistoryMessageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851672CE2BED1F9500592F24 /* HistoryMessageManager.swift */; }; + 851672D12BED1FC900592F24 /* AutocompleteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851672D02BED1FC900592F24 /* AutocompleteView.swift */; }; + 851672D32BED23FE00592F24 /* AutocompleteViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851672D22BED23FE00592F24 /* AutocompleteViewModel.swift */; }; 8517D98B221783A0006A8DD0 /* FindInPage.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8517D98A221783A0006A8DD0 /* FindInPage.xcassets */; }; 851B1283221FE65E004781BC /* ImproveOnboardingExperiment1Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851B1281221FE64E004781BC /* ImproveOnboardingExperiment1Tests.swift */; }; 851B128822200575004781BC /* Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851B128722200575004781BC /* Onboarding.swift */; }; @@ -496,7 +496,6 @@ 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 */; }; - 984147AB24F025F700362052 /* Autocomplete.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 984147AD24F025F700362052 /* Autocomplete.storyboard */; }; 984147AE24F0261A00362052 /* Feedback.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 984147B024F0261A00362052 /* Feedback.storyboard */; }; 984147B124F0264300362052 /* Home.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 984147B324F0264300362052 /* Home.storyboard */; }; 984147B424F0264B00362052 /* Authentication.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 984147B624F0264B00362052 /* Authentication.storyboard */; }; @@ -666,8 +665,6 @@ C12726EE2A5FF88C00215B02 /* EmailSignupPromptView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C12726ED2A5FF88C00215B02 /* EmailSignupPromptView.swift */; }; C12726F02A5FF89900215B02 /* EmailSignupPromptViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C12726EF2A5FF89900215B02 /* EmailSignupPromptViewModel.swift */; }; C12726F22A5FF8CB00215B02 /* EmailSignupPromptViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C12726F12A5FF8CB00215B02 /* EmailSignupPromptViewController.swift */; }; - C12B6E7A2BED639500050D93 /* AutofillPixelReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C12B6E792BED639500050D93 /* AutofillPixelReporter.swift */; }; - C12B6E7C2BED69C100050D93 /* AutofillPixelReporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C12B6E7B2BED69C100050D93 /* AutofillPixelReporterTests.swift */; }; C136C7542BEB8CFC00ACC3B0 /* PasswordsSurveyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C136C7532BEB8CFC00ACC3B0 /* PasswordsSurveyView.swift */; }; C13B32D22A0E750700A59236 /* AutofillSettingStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13B32D12A0E750700A59236 /* AutofillSettingStatus.swift */; }; C13F3F682B7F88100083BE40 /* AuthConfirmationPromptView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13F3F672B7F88100083BE40 /* AuthConfirmationPromptView.swift */; }; @@ -785,7 +782,6 @@ D68DF81C2B58302E0023DBEA /* SubscriptionRestoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68DF81B2B58302E0023DBEA /* SubscriptionRestoreView.swift */; }; D68DF81E2B5830380023DBEA /* SubscriptionRestoreViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68DF81D2B5830380023DBEA /* SubscriptionRestoreViewModel.swift */; }; D69FBF762B28BE3600B505F1 /* SettingsSubscriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69FBF752B28BE3600B505F1 /* SettingsSubscriptionView.swift */; }; - D6A4645C2BD6D6DA00F80DC2 /* UserDefaultsCacheKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A4645B2BD6D6DA00F80DC2 /* UserDefaultsCacheKey.swift */; }; D6ACEA322BBD55BF008FADDF /* TabURLInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6ACEA312BBD55BF008FADDF /* TabURLInterceptor.swift */; }; D6BFCB5F2B7524AA0051FF81 /* SubscriptionPIRView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BFCB5E2B7524AA0051FF81 /* SubscriptionPIRView.swift */; }; D6BFCB612B7525160051FF81 /* SubscriptionPIRViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BFCB602B7525160051FF81 /* SubscriptionPIRViewModel.swift */; }; @@ -814,14 +810,10 @@ D6FEB8B12B7498A300C3615F /* HeadlessWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6FEB8B02B7498A300C3615F /* HeadlessWebView.swift */; }; D6FEB8B32B74990D00C3615F /* HeadlessWebViewNavCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6FEB8B22B74990D00C3615F /* HeadlessWebViewNavCoordinator.swift */; }; D6FEB8B52B74994000C3615F /* HeadlessWebViewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6FEB8B42B74994000C3615F /* HeadlessWebViewCoordinator.swift */; }; - D6FF22482BC95F0B008E7BCC /* AccountManager+AppGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6FF22472BC95F0B008E7BCC /* AccountManager+AppGroup.swift */; }; EA39B7E2268A1A35000C62CD /* privacy-reference-tests in Resources */ = {isa = PBXBuildFile; fileRef = EA39B7E1268A1A35000C62CD /* privacy-reference-tests */; }; EAB19EDA268963510015D3EA /* DomainMatchingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB19ED9268963510015D3EA /* DomainMatchingTests.swift */; }; EE0153E12A6EABE0002A8B26 /* NetworkProtectionConvenienceInitialisers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0153E02A6EABE0002A8B26 /* NetworkProtectionConvenienceInitialisers.swift */; }; - EE0153E62A6FE106002A8B26 /* NetworkProtectionRootViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0153E52A6FE106002A8B26 /* NetworkProtectionRootViewModel.swift */; }; - EE0153EB2A6FF970002A8B26 /* NetworkProtectionRootViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0153EA2A6FF970002A8B26 /* NetworkProtectionRootViewModelTests.swift */; }; EE0153ED2A6FF9E6002A8B26 /* NetworkProtectionRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0153EC2A6FF9E6002A8B26 /* NetworkProtectionRootView.swift */; }; - EE0153EF2A70021E002A8B26 /* NetworkProtectionInviteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0153EE2A70021E002A8B26 /* NetworkProtectionInviteView.swift */; }; EE01EB402AFBD0000096AAC9 /* NetworkProtectionVPNSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE01EB3F2AFBD0000096AAC9 /* NetworkProtectionVPNSettingsViewModel.swift */; }; EE01EB432AFC1E0A0096AAC9 /* NetworkProtectionVPNLocationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE01EB422AFC1E0A0096AAC9 /* NetworkProtectionVPNLocationView.swift */; }; EE0798C52B179936000A4F64 /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0798C42B179936000A4F64 /* NetworkProtectionVPNCountryLabelsModel.swift */; }; @@ -829,7 +821,6 @@ EE3766DE2AC5945500AAB575 /* NetworkProtectionUNNotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE3766DD2AC5945500AAB575 /* NetworkProtectionUNNotificationPresenter.swift */; }; EE3B226B29DE0F110082298A /* MockInternalUserStoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE3B226A29DE0F110082298A /* MockInternalUserStoring.swift */; }; EE3B226C29DE0FD30082298A /* MockInternalUserStoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE3B226A29DE0F110082298A /* MockInternalUserStoring.swift */; }; - EE41BD192A729E9C00546C57 /* NetworkProtectionInviteViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE41BD182A729E9C00546C57 /* NetworkProtectionInviteViewModelTests.swift */; }; EE458D0D2AB1DA4600FC651A /* EventMapping+NetworkProtectionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE458D0C2AB1DA4600FC651A /* EventMapping+NetworkProtectionError.swift */; }; EE458D142ABB652900FC651A /* NetworkProtectionDebugUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE458D132ABB652900FC651A /* NetworkProtectionDebugUtilities.swift */; }; EE4BE0092A740BED00CD6AA8 /* ClearTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE4BE0082A740BED00CD6AA8 /* ClearTextField.swift */; }; @@ -853,7 +844,6 @@ EEF0F8CC2ABC832300630031 /* NetworkProtectionDebugFeatures.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF0F8CB2ABC832200630031 /* NetworkProtectionDebugFeatures.swift */; }; EEFAB4672A73C230008A38E4 /* NetworkProtectionTestUtils in Frameworks */ = {isa = PBXBuildFile; productRef = EEFAB4662A73C230008A38E4 /* NetworkProtectionTestUtils */; }; EEFC6A602AC0F2F80065027D /* UserText.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEFC6A5F2AC0F2F80065027D /* UserText.swift */; }; - EEFD562F2A65B6CA00DAEC48 /* NetworkProtectionInviteViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEFD562E2A65B6CA00DAEC48 /* NetworkProtectionInviteViewModel.swift */; }; EEFE9C732A603CE9005B0A26 /* NetworkProtectionStatusViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEFE9C722A603CE9005B0A26 /* NetworkProtectionStatusViewModelTests.swift */; }; F103073B1E7C91330059FEC7 /* BookmarksDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = F103073A1E7C91330059FEC7 /* BookmarksDataSource.swift */; }; F1075C921E9EF827006BE8A8 /* UserDefaultsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1075C911E9EF827006BE8A8 /* UserDefaultsExtension.swift */; }; @@ -886,9 +876,14 @@ F143C3281E4A9A0E00CFDE3A /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F143C3241E4A9A0E00CFDE3A /* StringExtension.swift */; }; F143C3291E4A9A0E00CFDE3A /* URLExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F143C3251E4A9A0E00CFDE3A /* URLExtension.swift */; }; F14E491F1E391CE900DC037C /* URLExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F14E491E1E391CE900DC037C /* URLExtensionTests.swift */; }; + F15531902BF215ED0029ED04 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = F155318F2BF215ED0029ED04 /* Subscription */; }; + F15531922BF215ED0029ED04 /* SubscriptionTestingUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = F15531912BF215ED0029ED04 /* SubscriptionTestingUtilities */; }; + F15531942BF215F60029ED04 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = F15531932BF215F60029ED04 /* Subscription */; }; + F15531962BF215F60029ED04 /* SubscriptionTestingUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = F15531952BF215F60029ED04 /* SubscriptionTestingUtilities */; }; F1564F032B7B915F00D454A6 /* AppDelegate+SKAD4.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1564F022B7B915F00D454A6 /* AppDelegate+SKAD4.swift */; }; F159BDA41F0BDB5A00B4A01D /* TabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F159BDA31F0BDB5A00B4A01D /* TabViewController.swift */; }; F15D43201E706CC500BF2CDC /* AutocompleteViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F15D431F1E706CC500BF2CDC /* AutocompleteViewController.swift */; }; + F15E9F3E2BEE128200DEFDDE /* SubscriptionManageriOS14.swift in Sources */ = {isa = PBXBuildFile; fileRef = F15E9F3D2BEE128200DEFDDE /* SubscriptionManageriOS14.swift */; }; F1617C131E572E0300DEDCAF /* TabSwitcherViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1617C121E572E0300DEDCAF /* TabSwitcherViewController.swift */; }; F1617C151E57336D00DEDCAF /* TabManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1617C141E57336D00DEDCAF /* TabManager.swift */; }; F1617C191E573EA800DEDCAF /* TabSwitcherDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1617C181E573EA800DEDCAF /* TabSwitcherDelegate.swift */; }; @@ -898,9 +893,7 @@ F176699F1E40BC86003D3222 /* Settings.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F176699D1E40BC86003D3222 /* Settings.storyboard */; }; F17669D71E43401C003D3222 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17669D61E43401C003D3222 /* MainViewController.swift */; }; F17843E91F36226700390DCD /* MockFiles in Resources */ = {isa = PBXBuildFile; fileRef = F17843E81F36226700390DCD /* MockFiles */; }; - F17922DE1E7192E6006E3D97 /* SuggestionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17922DD1E7192E6006E3D97 /* SuggestionTableViewCell.swift */; }; F17922E01E71BB59006E3D97 /* AutocompleteViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17922DF1E71BB59006E3D97 /* AutocompleteViewControllerDelegate.swift */; }; - F17922E21E71CD67006E3D97 /* NoSuggestionsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17922E11E71CD67006E3D97 /* NoSuggestionsTableViewCell.swift */; }; F17D72391E8B35C6003E8B0E /* AppURLsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17D72381E8B35C6003E8B0E /* AppURLsTests.swift */; }; F17D723C1E8BB374003E8B0E /* AppDeepLinkSchemes.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17D723B1E8BB374003E8B0E /* AppDeepLinkSchemes.swift */; }; F189AED71F18F6DE001EBAE1 /* TabTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F189AED61F18F6DE001EBAE1 /* TabTests.swift */; }; @@ -934,6 +927,10 @@ F1ED309D1EDC2EA400651986 /* TabSwitcher.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F1ED309B1EDC2EA400651986 /* TabSwitcher.storyboard */; }; F1F5337C1F26A9EF00D80D4F /* UserText.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1F5337B1F26A9EF00D80D4F /* UserText.swift */; }; F1F533841F26ABAC00D80D4F /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F1F533861F26ABAC00D80D4F /* Localizable.strings */; }; + F1FDC9302BF4E0B3006B1435 /* SubscriptionEnvironment+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC92F2BF4E0B3006B1435 /* SubscriptionEnvironment+Default.swift */; }; + F1FDC9312BF4E0B3006B1435 /* SubscriptionEnvironment+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC92F2BF4E0B3006B1435 /* SubscriptionEnvironment+Default.swift */; }; + F1FDC9352BF51E41006B1435 /* VPNSettings+Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9342BF51E41006B1435 /* VPNSettings+Environment.swift */; }; + F1FDC9362BF51E41006B1435 /* VPNSettings+Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9342BF51E41006B1435 /* VPNSettings+Environment.swift */; }; F40F843728C939760081AE75 /* AutofillLoginListViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F40F843528C938370081AE75 /* AutofillLoginListViewModelTests.swift */; }; F4147354283BF834004AA7A5 /* AutofillContentScopeFeatureToggles.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4147353283BF834004AA7A5 /* AutofillContentScopeFeatureToggles.swift */; }; F41610BC29E5DF66001F709D /* DeprecatedColors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F41610BB29E5DF65001F709D /* DeprecatedColors.xcassets */; }; @@ -1303,21 +1300,14 @@ 4B6484E927FD1E340050A7A1 /* MenuControllerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MenuControllerView.swift; sourceTree = ""; }; 4B6ED9442B992FE4007F5CAA /* vpn-dark-mode.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "vpn-dark-mode.json"; sourceTree = ""; }; 4B75EA9126A266CB00018634 /* PrintingUserScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrintingUserScript.swift; sourceTree = ""; }; - 4B78074B2B1823C5009DB2CF /* VPNWaitlistActivationDateStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNWaitlistActivationDateStore.swift; sourceTree = ""; }; - 4B78074D2B183A1F009DB2CF /* SurveyURLBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurveyURLBuilder.swift; sourceTree = ""; }; + 4B78074D2B183A1F009DB2CF /* RemoteMessagingSurveyURLBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteMessagingSurveyURLBuilder.swift; sourceTree = ""; }; 4BB7CBAF2AF59C310014A35F /* VPNWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNWidget.swift; sourceTree = ""; }; - 4BBBBA892B031B4200D965DA /* VPNWaitlistDebugViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VPNWaitlistDebugViewController.swift; sourceTree = ""; }; - 4BBBBA8A2B031B4200D965DA /* VPNWaitlistViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VPNWaitlistViewController.swift; sourceTree = ""; }; - 4BBBBA8B2B031B4200D965DA /* VPNWaitlistView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VPNWaitlistView.swift; sourceTree = ""; }; - 4BBBBA8C2B031B4200D965DA /* VPNWaitlist.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VPNWaitlist.swift; sourceTree = ""; }; 4BBBBA912B03291700D965DA /* VPNWaitlistUserText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNWaitlistUserText.swift; sourceTree = ""; }; 4BC21A2C272388BD00229F0E /* RunLoopExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunLoopExtensionTests.swift; sourceTree = ""; }; 4BCBE45D2BA7E81F00FC75A1 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 4BCBE45F2BA7E87100FC75A1 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 4BCD14622B05AF2B000B1E4C /* NetworkProtectionAccessController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionAccessController.swift; sourceTree = ""; }; 4BCD14662B05B682000B1E4C /* NetworkProtectionTermsAndConditionsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionTermsAndConditionsStore.swift; sourceTree = ""; }; - 4BCD14682B05BDD5000B1E4C /* AppDelegate+Waitlists.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+Waitlists.swift"; sourceTree = ""; }; - 4BCD146A2B05C4B5000B1E4C /* VPNWaitlistTermsAndConditionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNWaitlistTermsAndConditionsViewController.swift; sourceTree = ""; }; 4BCD146C2B05DB09000B1E4C /* NetworkProtectionAccessControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionAccessControllerTests.swift; sourceTree = ""; }; 4BE27566272F878F006B20B0 /* URLRequestExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = URLRequestExtension.swift; path = ../DuckDuckGo/URLRequestExtension.swift; sourceTree = ""; }; 560E990E2BEE2CB800507CE0 /* SyncErrorMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncErrorMessage.swift; sourceTree = ""; }; @@ -1340,12 +1330,16 @@ 6F655BE12BAB289E00AC3597 /* DefaultTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultTheme.swift; sourceTree = ""; }; 6F8496402BC3D8EE00ADA54E /* OnboardingButtonsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingButtonsView.swift; sourceTree = ""; }; 6FB030C7234331B400A10DB9 /* Configuration.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Configuration.xcconfig; path = Configuration/Configuration.xcconfig; sourceTree = ""; }; + 6FBF0F8A2BD7C0A900136CF0 /* AllProtectedCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllProtectedCell.swift; sourceTree = ""; }; 6FD1BAE12B87A107000C475C /* AdAttributionPixelReporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AdAttributionPixelReporter.swift; path = AdAttribution/AdAttributionPixelReporter.swift; sourceTree = ""; }; 6FD1BAE22B87A107000C475C /* AdAttributionReporterStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AdAttributionReporterStorage.swift; path = AdAttribution/AdAttributionReporterStorage.swift; sourceTree = ""; }; 6FD1BAE32B87A107000C475C /* AdAttributionFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AdAttributionFetcher.swift; path = AdAttribution/AdAttributionFetcher.swift; sourceTree = ""; }; 6FD3AEE12B8DFBB80060FCCC /* AdAttributionPixelReporterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdAttributionPixelReporterTests.swift; sourceTree = ""; }; 6FDA1FB22B59584400AC962A /* AddressDisplayHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressDisplayHelper.swift; sourceTree = ""; }; + 6FDB3F182BD11A4400F7A307 /* AutocompleteSuggestionsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutocompleteSuggestionsModel.swift; sourceTree = ""; }; + 6FE095D72BD90AFB00490FF8 /* UniversalOmniBarState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UniversalOmniBarState.swift; sourceTree = ""; }; 6FF915802B88E0750042AC87 /* AdAttributionFetcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdAttributionFetcherTests.swift; sourceTree = ""; }; + 7BC5711F2BDBB877003B0CCE /* VPNActivationDateStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VPNActivationDateStore.swift; sourceTree = ""; }; 83004E7F2193BB8200DA013C /* WKNavigationExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKNavigationExtension.swift; sourceTree = ""; }; 83004E832193E14C00DA013C /* UIAlertControllerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = UIAlertControllerExtension.swift; path = ../Core/UIAlertControllerExtension.swift; sourceTree = ""; }; 83004E852193E5ED00DA013C /* TabViewControllerBrowsingMenuExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabViewControllerBrowsingMenuExtension.swift; sourceTree = ""; }; @@ -1406,6 +1400,9 @@ 851624C12B95F8BD002D5CD7 /* HistoryCapture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryCapture.swift; sourceTree = ""; }; 851624C32B96029B002D5CD7 /* HistoryCaptureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryCaptureTests.swift; sourceTree = ""; }; 851624C62B96389D002D5CD7 /* HistoryDebugViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryDebugViewController.swift; sourceTree = ""; }; + 851672CE2BED1F9500592F24 /* HistoryMessageManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryMessageManager.swift; sourceTree = ""; }; + 851672D02BED1FC900592F24 /* AutocompleteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutocompleteView.swift; sourceTree = ""; }; + 851672D22BED23FE00592F24 /* AutocompleteViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutocompleteViewModel.swift; sourceTree = ""; }; 8517D98A221783A0006A8DD0 /* FindInPage.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = FindInPage.xcassets; sourceTree = ""; }; 851B1281221FE64E004781BC /* ImproveOnboardingExperiment1Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImproveOnboardingExperiment1Tests.swift; sourceTree = ""; }; 851B128722200575004781BC /* Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Onboarding.swift; sourceTree = ""; }; @@ -1580,7 +1577,6 @@ 980891A62237D5D800313A70 /* FeedbackPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackPresenter.swift; sourceTree = ""; }; 980891A82238504B00313A70 /* UILabelExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UILabelExtension.swift; sourceTree = ""; }; 9813F79722BA71AA00A80EDB /* StorageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageCache.swift; sourceTree = ""; }; - 981685432521EEEF00FA91A1 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/Autocomplete.strings; sourceTree = ""; }; 981685442521EEEF00FA91A1 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/Bookmarks.strings; sourceTree = ""; }; 981685452521EEF000FA91A1 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/Feedback.strings; sourceTree = ""; }; 981685462521EEF000FA91A1 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/Home.strings; sourceTree = ""; }; @@ -1647,7 +1643,6 @@ 983E134F251EABF200149BD9 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/InfoPlist.strings; sourceTree = ""; }; 983EABB7236198F6003948D1 /* DatabaseMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseMigration.swift; sourceTree = ""; }; 984147A924F0259000362052 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Onboarding.storyboard; sourceTree = ""; }; - 984147AC24F025F700362052 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Autocomplete.storyboard; sourceTree = ""; }; 984147AF24F0261A00362052 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Feedback.storyboard; sourceTree = ""; }; 984147B224F0264300362052 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Home.storyboard; sourceTree = ""; }; 984147B524F0264B00362052 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Authentication.storyboard; sourceTree = ""; }; @@ -1684,7 +1679,6 @@ 985AAE4424899369007A43EC /* HomeScreenTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenTransition.swift; sourceTree = ""; }; 9865DFF822A8220D00D27829 /* FavoritesOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesOverlay.swift; sourceTree = ""; }; 9865DFFC22A84CF300D27829 /* FavoriteHomeCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FavoriteHomeCell.xib; sourceTree = ""; }; - 9866DB8B251CA8F300612E3A /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Autocomplete.strings; sourceTree = ""; }; 9866DB8C251CA8F300612E3A /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Bookmarks.strings; sourceTree = ""; }; 9866DB8D251CA8F300612E3A /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Feedback.strings; sourceTree = ""; }; 9866DB8E251CA8F400612E3A /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Home.strings; sourceTree = ""; }; @@ -1701,7 +1695,6 @@ 9866DB9F251CA8F700612E3A /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/MainInterface.strings; sourceTree = ""; }; 9866DBA0251CA8F700612E3A /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = ""; }; 9866DBA1251CA8F700612E3A /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/InfoPlist.strings; sourceTree = ""; }; - 9866DBA2251CA91700612E3A /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/Autocomplete.strings; sourceTree = ""; }; 9866DBA3251CA91700612E3A /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/Bookmarks.strings; sourceTree = ""; }; 9866DBA4251CA91700612E3A /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/Feedback.strings; sourceTree = ""; }; 9866DBA5251CA91800612E3A /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/Home.strings; sourceTree = ""; }; @@ -1718,7 +1711,6 @@ 9866DBB6251CA91900612E3A /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/MainInterface.strings; sourceTree = ""; }; 9866DBB7251CA91900612E3A /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/Localizable.strings; sourceTree = ""; }; 9866DBB8251CA91900612E3A /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/InfoPlist.strings; sourceTree = ""; }; - 9866DBB9251CA92A00612E3A /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Autocomplete.strings; sourceTree = ""; }; 9866DBBA251CA92A00612E3A /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Bookmarks.strings; sourceTree = ""; }; 9866DBBB251CA92A00612E3A /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Feedback.strings; sourceTree = ""; }; 9866DBBC251CA92A00612E3A /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Home.strings; sourceTree = ""; }; @@ -1735,7 +1727,6 @@ 9866DBCD251CA92D00612E3A /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/MainInterface.strings; sourceTree = ""; }; 9866DBCE251CA92E00612E3A /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; 9866DBCF251CA92E00612E3A /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/InfoPlist.strings; sourceTree = ""; }; - 9866DBD0251CA93800612E3A /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Autocomplete.strings; sourceTree = ""; }; 9866DBD1251CA93800612E3A /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Bookmarks.strings; sourceTree = ""; }; 9866DBD2251CA93900612E3A /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Feedback.strings; sourceTree = ""; }; 9866DBD3251CA93900612E3A /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Home.strings; sourceTree = ""; }; @@ -1752,7 +1743,6 @@ 9866DBE4251CA93B00612E3A /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/MainInterface.strings; sourceTree = ""; }; 9866DBE5251CA93B00612E3A /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Localizable.strings; sourceTree = ""; }; 9866DBE6251CA93B00612E3A /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/InfoPlist.strings; sourceTree = ""; }; - 9866DBE7251CA94E00612E3A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Autocomplete.strings; sourceTree = ""; }; 9866DBE8251CA94E00612E3A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Bookmarks.strings; sourceTree = ""; }; 9866DBE9251CA94E00612E3A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Feedback.strings; sourceTree = ""; }; 9866DBEA251CA94F00612E3A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Home.strings; sourceTree = ""; }; @@ -1769,7 +1759,6 @@ 9866DBFB251CA95200612E3A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/MainInterface.strings; sourceTree = ""; }; 9866DBFC251CA95200612E3A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; 9866DBFD251CA95200612E3A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = ""; }; - 9866DBFE251CA96200612E3A /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = et.lproj/Autocomplete.strings; sourceTree = ""; }; 9866DBFF251CA96200612E3A /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = et.lproj/Bookmarks.strings; sourceTree = ""; }; 9866DC00251CA96200612E3A /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = et.lproj/Feedback.strings; sourceTree = ""; }; 9866DC01251CA96200612E3A /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = et.lproj/Home.strings; sourceTree = ""; }; @@ -1786,7 +1775,6 @@ 9866DC12251CA96400612E3A /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = et.lproj/MainInterface.strings; sourceTree = ""; }; 9866DC13251CA96500612E3A /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = et.lproj/Localizable.strings; sourceTree = ""; }; 9866DC14251CA96500612E3A /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = et.lproj/InfoPlist.strings; sourceTree = ""; }; - 9866DC15251CA99A00612E3A /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Autocomplete.strings; sourceTree = ""; }; 9866DC16251CA99A00612E3A /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Bookmarks.strings; sourceTree = ""; }; 9866DC17251CA99B00612E3A /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Feedback.strings; sourceTree = ""; }; 9866DC18251CA99B00612E3A /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Home.strings; sourceTree = ""; }; @@ -1803,7 +1791,6 @@ 9866DC29251CA99E00612E3A /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/MainInterface.strings; sourceTree = ""; }; 9866DC2A251CA99E00612E3A /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; 9866DC2B251CA99E00612E3A /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = ""; }; - 9866DC2C251CA9AF00612E3A /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Autocomplete.strings; sourceTree = ""; }; 9866DC2D251CA9B000612E3A /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Bookmarks.strings; sourceTree = ""; }; 9866DC2E251CA9B000612E3A /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Feedback.strings; sourceTree = ""; }; 9866DC2F251CA9B000612E3A /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Home.strings; sourceTree = ""; }; @@ -1820,7 +1807,6 @@ 9866DC40251CA9B200612E3A /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/MainInterface.strings; sourceTree = ""; }; 9866DC41251CA9B200612E3A /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Localizable.strings; sourceTree = ""; }; 9866DC42251CA9B200612E3A /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/InfoPlist.strings; sourceTree = ""; }; - 9866DC43251CA9BF00612E3A /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Autocomplete.strings; sourceTree = ""; }; 9866DC44251CA9BF00612E3A /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Bookmarks.strings; sourceTree = ""; }; 9866DC45251CA9BF00612E3A /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Feedback.strings; sourceTree = ""; }; 9866DC46251CA9C000612E3A /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Home.strings; sourceTree = ""; }; @@ -1837,7 +1823,6 @@ 9866DC57251CA9C300612E3A /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/MainInterface.strings; sourceTree = ""; }; 9866DC58251CA9C300612E3A /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = ""; }; 9866DC59251CA9C300612E3A /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/InfoPlist.strings; sourceTree = ""; }; - 9866DC5A251CA9CE00612E3A /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Autocomplete.strings; sourceTree = ""; }; 9866DC5B251CA9CE00612E3A /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Bookmarks.strings; sourceTree = ""; }; 9866DC5C251CA9CE00612E3A /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Feedback.strings; sourceTree = ""; }; 9866DC5D251CA9CE00612E3A /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Home.strings; sourceTree = ""; }; @@ -1854,7 +1839,6 @@ 9866DC6E251CA9D100612E3A /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/MainInterface.strings; sourceTree = ""; }; 9866DC6F251CA9D100612E3A /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; }; 9866DC70251CA9D100612E3A /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/InfoPlist.strings; sourceTree = ""; }; - 9866DC71251CA9E200612E3A /* lv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lv; path = lv.lproj/Autocomplete.strings; sourceTree = ""; }; 9866DC72251CA9E200612E3A /* lv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lv; path = lv.lproj/Bookmarks.strings; sourceTree = ""; }; 9866DC73251CA9E300612E3A /* lv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lv; path = lv.lproj/Feedback.strings; sourceTree = ""; }; 9866DC74251CA9E300612E3A /* lv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lv; path = lv.lproj/Home.strings; sourceTree = ""; }; @@ -1871,7 +1855,6 @@ 9866DC85251CA9E600612E3A /* lv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lv; path = lv.lproj/MainInterface.strings; sourceTree = ""; }; 9866DC86251CA9E600612E3A /* lv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lv; path = lv.lproj/Localizable.strings; sourceTree = ""; }; 9866DC87251CA9E600612E3A /* lv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lv; path = lv.lproj/InfoPlist.strings; sourceTree = ""; }; - 9866DC88251CA9F400612E3A /* lt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lt; path = lt.lproj/Autocomplete.strings; sourceTree = ""; }; 9866DC89251CA9F500612E3A /* lt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lt; path = lt.lproj/Bookmarks.strings; sourceTree = ""; }; 9866DC8A251CA9F500612E3A /* lt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lt; path = lt.lproj/Feedback.strings; sourceTree = ""; }; 9866DC8B251CA9F500612E3A /* lt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lt; path = lt.lproj/Home.strings; sourceTree = ""; }; @@ -1888,7 +1871,6 @@ 9866DC9C251CA9F700612E3A /* lt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lt; path = lt.lproj/MainInterface.strings; sourceTree = ""; }; 9866DC9D251CA9F700612E3A /* lt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lt; path = lt.lproj/Localizable.strings; sourceTree = ""; }; 9866DC9E251CA9F700612E3A /* lt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lt; path = lt.lproj/InfoPlist.strings; sourceTree = ""; }; - 9866DC9F251CAA0500612E3A /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Autocomplete.strings; sourceTree = ""; }; 9866DCA0251CAA0500612E3A /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Bookmarks.strings; sourceTree = ""; }; 9866DCA1251CAA0500612E3A /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Feedback.strings; sourceTree = ""; }; 9866DCA2251CAA0500612E3A /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Home.strings; sourceTree = ""; }; @@ -1905,7 +1887,6 @@ 9866DCB3251CAA0800612E3A /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/MainInterface.strings; sourceTree = ""; }; 9866DCB4251CAA0800612E3A /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; 9866DCB5251CAA0900612E3A /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = ""; }; - 9866DCB6251CAA2500612E3A /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/Autocomplete.strings; sourceTree = ""; }; 9866DCB7251CAA2600612E3A /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/Bookmarks.strings; sourceTree = ""; }; 9866DCB8251CAA2600612E3A /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/Feedback.strings; sourceTree = ""; }; 9866DCB9251CAA2600612E3A /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/Home.strings; sourceTree = ""; }; @@ -1922,7 +1903,6 @@ 9866DCCA251CAA2800612E3A /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/MainInterface.strings; sourceTree = ""; }; 9866DCCB251CAA2800612E3A /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/Localizable.strings; sourceTree = ""; }; 9866DCCC251CAA2800612E3A /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/InfoPlist.strings; sourceTree = ""; }; - 9866DCCD251CAA3300612E3A /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/Autocomplete.strings; sourceTree = ""; }; 9866DCCE251CAA3300612E3A /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/Bookmarks.strings; sourceTree = ""; }; 9866DCCF251CAA3400612E3A /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/Feedback.strings; sourceTree = ""; }; 9866DCD0251CAA3400612E3A /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/Home.strings; sourceTree = ""; }; @@ -1939,7 +1919,6 @@ 9866DCE1251CAA3600612E3A /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/MainInterface.strings; sourceTree = ""; }; 9866DCE2251CAA3700612E3A /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/Localizable.strings; sourceTree = ""; }; 9866DCE3251CAA3700612E3A /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/InfoPlist.strings; sourceTree = ""; }; - 9866DCE4251CAA4800612E3A /* sl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sl; path = sl.lproj/Autocomplete.strings; sourceTree = ""; }; 9866DCE5251CAA4800612E3A /* sl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sl; path = sl.lproj/Bookmarks.strings; sourceTree = ""; }; 9866DCE6251CAA4800612E3A /* sl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sl; path = sl.lproj/Feedback.strings; sourceTree = ""; }; 9866DCE7251CAA4800612E3A /* sl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sl; path = sl.lproj/Home.strings; sourceTree = ""; }; @@ -1963,13 +1942,6 @@ 9866DCFF251CAC8900612E3A /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/Localizable.strings; sourceTree = ""; }; 9866DD01251CAC8E00612E3A /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; 9866DD02251CAC8F00612E3A /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = ""; }; - 9866DD04251CACC500612E3A /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Autocomplete.strings; sourceTree = ""; }; - 9866DD06251CACC600612E3A /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Autocomplete.strings; sourceTree = ""; }; - 9866DD0A251CACCD00612E3A /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/Autocomplete.strings; sourceTree = ""; }; - 9866DD0C251CACCD00612E3A /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Autocomplete.strings; sourceTree = ""; }; - 9866DD0E251CACCE00612E3A /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Autocomplete.strings; sourceTree = ""; }; - 9866DD10251CACCF00612E3A /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Autocomplete.strings; sourceTree = ""; }; - 9866DD12251CACCF00612E3A /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Autocomplete.strings; sourceTree = ""; }; 9866DD14251CACDB00612E3A /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Bookmarks.strings; sourceTree = ""; }; 9866DD16251CACDC00612E3A /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Bookmarks.strings; sourceTree = ""; }; 9866DD1A251CACE400612E3A /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/Bookmarks.strings; sourceTree = ""; }; @@ -2300,8 +2272,6 @@ C12726ED2A5FF88C00215B02 /* EmailSignupPromptView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailSignupPromptView.swift; sourceTree = ""; }; C12726EF2A5FF89900215B02 /* EmailSignupPromptViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailSignupPromptViewModel.swift; sourceTree = ""; }; C12726F12A5FF8CB00215B02 /* EmailSignupPromptViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailSignupPromptViewController.swift; sourceTree = ""; }; - C12B6E792BED639500050D93 /* AutofillPixelReporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutofillPixelReporter.swift; sourceTree = ""; }; - C12B6E7B2BED69C100050D93 /* AutofillPixelReporterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillPixelReporterTests.swift; sourceTree = ""; }; C136C7532BEB8CFC00ACC3B0 /* PasswordsSurveyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordsSurveyView.swift; sourceTree = ""; }; C13B32D12A0E750700A59236 /* AutofillSettingStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutofillSettingStatus.swift; sourceTree = ""; }; C13F3F672B7F88100083BE40 /* AuthConfirmationPromptView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthConfirmationPromptView.swift; sourceTree = ""; }; @@ -2433,7 +2403,6 @@ D68DF81B2B58302E0023DBEA /* SubscriptionRestoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionRestoreView.swift; sourceTree = ""; }; D68DF81D2B5830380023DBEA /* SubscriptionRestoreViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionRestoreViewModel.swift; sourceTree = ""; }; D69FBF752B28BE3600B505F1 /* SettingsSubscriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSubscriptionView.swift; sourceTree = ""; }; - D6A4645B2BD6D6DA00F80DC2 /* UserDefaultsCacheKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsCacheKey.swift; sourceTree = ""; }; D6ACEA312BBD55BF008FADDF /* TabURLInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabURLInterceptor.swift; sourceTree = ""; }; D6BFCB5E2B7524AA0051FF81 /* SubscriptionPIRView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionPIRView.swift; sourceTree = ""; }; D6BFCB602B7525160051FF81 /* SubscriptionPIRViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionPIRViewModel.swift; sourceTree = ""; }; @@ -2462,14 +2431,10 @@ D6FEB8B02B7498A300C3615F /* HeadlessWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlessWebView.swift; sourceTree = ""; }; D6FEB8B22B74990D00C3615F /* HeadlessWebViewNavCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlessWebViewNavCoordinator.swift; sourceTree = ""; }; D6FEB8B42B74994000C3615F /* HeadlessWebViewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlessWebViewCoordinator.swift; sourceTree = ""; }; - D6FF22472BC95F0B008E7BCC /* AccountManager+AppGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountManager+AppGroup.swift"; sourceTree = ""; }; EA39B7E1268A1A35000C62CD /* privacy-reference-tests */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = "privacy-reference-tests"; path = "submodules/privacy-reference-tests"; sourceTree = SOURCE_ROOT; }; EAB19ED9268963510015D3EA /* DomainMatchingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DomainMatchingTests.swift; sourceTree = ""; }; EE0153E02A6EABE0002A8B26 /* NetworkProtectionConvenienceInitialisers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionConvenienceInitialisers.swift; sourceTree = ""; }; - EE0153E52A6FE106002A8B26 /* NetworkProtectionRootViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionRootViewModel.swift; sourceTree = ""; }; - EE0153EA2A6FF970002A8B26 /* NetworkProtectionRootViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionRootViewModelTests.swift; sourceTree = ""; }; EE0153EC2A6FF9E6002A8B26 /* NetworkProtectionRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionRootView.swift; sourceTree = ""; }; - EE0153EE2A70021E002A8B26 /* NetworkProtectionInviteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionInviteView.swift; sourceTree = ""; }; EE01EB3F2AFBD0000096AAC9 /* NetworkProtectionVPNSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionVPNSettingsViewModel.swift; sourceTree = ""; }; EE01EB422AFC1E0A0096AAC9 /* NetworkProtectionVPNLocationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionVPNLocationView.swift; sourceTree = ""; }; EE0798C42B179936000A4F64 /* NetworkProtectionVPNCountryLabelsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionVPNCountryLabelsModel.swift; sourceTree = ""; }; @@ -2479,7 +2444,6 @@ EE3B98EA2A9634CC002F63A0 /* DuckDuckGoAlpha.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DuckDuckGoAlpha.entitlements; sourceTree = ""; }; EE3B98EB2A963515002F63A0 /* WidgetsExtensionAlpha.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetsExtensionAlpha.entitlements; sourceTree = ""; }; EE3B98EC2A963538002F63A0 /* PacketTunnelProviderAlpha.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PacketTunnelProviderAlpha.entitlements; sourceTree = ""; }; - EE41BD182A729E9C00546C57 /* NetworkProtectionInviteViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionInviteViewModelTests.swift; sourceTree = ""; }; EE458D0C2AB1DA4600FC651A /* EventMapping+NetworkProtectionError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EventMapping+NetworkProtectionError.swift"; sourceTree = ""; }; EE458D132ABB652900FC651A /* NetworkProtectionDebugUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionDebugUtilities.swift; sourceTree = ""; }; EE4BE0082A740BED00CD6AA8 /* ClearTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClearTextField.swift; sourceTree = ""; }; @@ -2526,7 +2490,6 @@ EEEB80A22A421CE600386378 /* NetworkProtectionPacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionPacketTunnelProvider.swift; sourceTree = ""; }; EEF0F8CB2ABC832200630031 /* NetworkProtectionDebugFeatures.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionDebugFeatures.swift; sourceTree = ""; }; EEFC6A5F2AC0F2F80065027D /* UserText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserText.swift; sourceTree = ""; }; - EEFD562E2A65B6CA00DAEC48 /* NetworkProtectionInviteViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionInviteViewModel.swift; sourceTree = ""; }; EEFE9C722A603CE9005B0A26 /* NetworkProtectionStatusViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionStatusViewModelTests.swift; sourceTree = ""; }; F103073A1E7C91330059FEC7 /* BookmarksDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarksDataSource.swift; sourceTree = ""; }; F1075C911E9EF827006BE8A8 /* UserDefaultsExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDefaultsExtension.swift; sourceTree = ""; }; @@ -2565,6 +2528,7 @@ F1564F022B7B915F00D454A6 /* AppDelegate+SKAD4.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AppDelegate+SKAD4.swift"; sourceTree = ""; }; F159BDA31F0BDB5A00B4A01D /* TabViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabViewController.swift; sourceTree = ""; }; F15D431F1E706CC500BF2CDC /* AutocompleteViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutocompleteViewController.swift; sourceTree = ""; }; + F15E9F3D2BEE128200DEFDDE /* SubscriptionManageriOS14.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionManageriOS14.swift; sourceTree = ""; }; F1617C121E572E0300DEDCAF /* TabSwitcherViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabSwitcherViewController.swift; sourceTree = ""; }; F1617C141E57336D00DEDCAF /* TabManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabManager.swift; sourceTree = ""; }; F1617C181E573EA800DEDCAF /* TabSwitcherDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabSwitcherDelegate.swift; sourceTree = ""; }; @@ -2575,9 +2539,7 @@ F176699E1E40BC86003D3222 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Settings.storyboard; sourceTree = ""; }; F17669D61E43401C003D3222 /* MainViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; F17843E81F36226700390DCD /* MockFiles */ = {isa = PBXFileReference; lastKnownFileType = folder; path = MockFiles; sourceTree = ""; }; - F17922DD1E7192E6006E3D97 /* SuggestionTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SuggestionTableViewCell.swift; sourceTree = ""; }; F17922DF1E71BB59006E3D97 /* AutocompleteViewControllerDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutocompleteViewControllerDelegate.swift; sourceTree = ""; }; - F17922E11E71CD67006E3D97 /* NoSuggestionsTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoSuggestionsTableViewCell.swift; sourceTree = ""; }; F17D72381E8B35C6003E8B0E /* AppURLsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppURLsTests.swift; sourceTree = ""; }; F17D723B1E8BB374003E8B0E /* AppDeepLinkSchemes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDeepLinkSchemes.swift; sourceTree = ""; }; F189AED61F18F6DE001EBAE1 /* TabTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabTests.swift; sourceTree = ""; }; @@ -2614,6 +2576,8 @@ F1E90C1F1E678E7C005E7E21 /* HomeControllerDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeControllerDelegate.swift; sourceTree = ""; }; F1ED309C1EDC2EA400651986 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/TabSwitcher.storyboard; sourceTree = ""; }; F1F5337B1F26A9EF00D80D4F /* UserText.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserText.swift; sourceTree = ""; }; + F1FDC92F2BF4E0B3006B1435 /* SubscriptionEnvironment+Default.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SubscriptionEnvironment+Default.swift"; sourceTree = ""; }; + F1FDC9342BF51E41006B1435 /* VPNSettings+Environment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "VPNSettings+Environment.swift"; sourceTree = ""; }; F40F843528C938370081AE75 /* AutofillLoginListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillLoginListViewModelTests.swift; sourceTree = ""; }; F4147353283BF834004AA7A5 /* AutofillContentScopeFeatureToggles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillContentScopeFeatureToggles.swift; sourceTree = ""; }; F41610BB29E5DF65001F709D /* DeprecatedColors.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = DeprecatedColors.xcassets; sourceTree = ""; }; @@ -2697,6 +2661,8 @@ F486D3362506A037002D07D7 /* OHHTTPStubs in Frameworks */, F486D3382506A225002D07D7 /* OHHTTPStubsSwift in Frameworks */, 4BE67B052B96B9AB007335F7 /* ContentBlocking in Frameworks */, + F15531922BF215ED0029ED04 /* SubscriptionTestingUtilities in Frameworks */, + F15531902BF215ED0029ED04 /* Subscription in Frameworks */, F115ED9C2B4EFC8E001A0453 /* TestUtils in Frameworks */, EEFAB4672A73C230008A38E4 /* NetworkProtectionTestUtils in Frameworks */, 4BE67B072B96B9B0007335F7 /* Common in Frameworks */, @@ -2729,6 +2695,8 @@ 1E1D8B632995143200C96994 /* OHHTTPStubs in Frameworks */, 1E1D8B652995143200C96994 /* OHHTTPStubsSwift in Frameworks */, 4BE67B012B96B741007335F7 /* Common in Frameworks */, + F15531962BF215F60029ED04 /* SubscriptionTestingUtilities in Frameworks */, + F15531942BF215F60029ED04 /* Subscription in Frameworks */, 4BE67B032B96B864007335F7 /* ContentBlocking in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -3320,13 +3288,8 @@ 4BBBBA882B031B3300D965DA /* VPN */ = { isa = PBXGroup; children = ( - 4BBBBA8C2B031B4200D965DA /* VPNWaitlist.swift */, - 4BBBBA892B031B4200D965DA /* VPNWaitlistDebugViewController.swift */, - 4BBBBA8B2B031B4200D965DA /* VPNWaitlistView.swift */, - 4BBBBA8A2B031B4200D965DA /* VPNWaitlistViewController.swift */, - 4BCD146A2B05C4B5000B1E4C /* VPNWaitlistTermsAndConditionsViewController.swift */, - 4B78074B2B1823C5009DB2CF /* VPNWaitlistActivationDateStore.swift */, - 4B78074D2B183A1F009DB2CF /* SurveyURLBuilder.swift */, + 7BC5711F2BDBB877003B0CCE /* VPNActivationDateStore.swift */, + 4B78074D2B183A1F009DB2CF /* RemoteMessagingSurveyURLBuilder.swift */, BDFF031F2BA3D3AD00F324C9 /* Feature Visibility */, ); name = VPN; @@ -3668,6 +3631,15 @@ path = Widgets; sourceTree = ""; }; + 851672CD2BED1F8200592F24 /* Model */ = { + isa = PBXGroup; + children = ( + 851672CE2BED1F9500592F24 /* HistoryMessageManager.swift */, + 851672D22BED23FE00592F24 /* AutocompleteViewModel.swift */, + ); + name = Model; + sourceTree = ""; + }; 851DFD88212C5ED600D95F20 /* Main */ = { isa = PBXGroup; children = ( @@ -3710,6 +3682,7 @@ 8540BD5523D9E9C20057FDD2 /* PreserveLoginsSettingsViewController.swift */, 1E865AEF272042DB001C74F3 /* TextSizeSettingsViewController.swift */, 8531A08D1F9950E6000484F0 /* UnprotectedSitesViewController.swift */, + 6FBF0F8A2BD7C0A900136CF0 /* AllProtectedCell.swift */, D6E83C672B23B6A3006C8AFB /* FontSettings.swift */, ); name = UIkit; @@ -4437,7 +4410,9 @@ D664C7922B289AA000CBFA76 /* Subscription */ = { isa = PBXGroup; children = ( + F1FDC92F2BF4E0B3006B1435 /* SubscriptionEnvironment+Default.swift */, D60170BB2BA32DD6001911B5 /* Subscription.swift */, + F15E9F3D2BEE128200DEFDDE /* SubscriptionManageriOS14.swift */, D6D95CE42B6DA3F200960317 /* AsyncHeadlessWebview */, D664C7952B289AA000CBFA76 /* Subscription.storekit */, D664C7932B289AA000CBFA76 /* ViewModel */, @@ -4467,6 +4442,7 @@ D664C7962B289AA000CBFA76 /* Extensions */ = { isa = PBXGroup; children = ( + F1FDC9342BF51E41006B1435 /* VPNSettings+Environment.swift */, D664C7982B289AA000CBFA76 /* WKUserContentController+Handler.swift */, D670E5BC2BB6AA0000941A42 /* View+AppearModifiers.swift */, ); @@ -4552,14 +4528,6 @@ name = Model; sourceTree = ""; }; - D6FF22462BC95EF9008E7BCC /* Subscription */ = { - isa = PBXGroup; - children = ( - D6FF22472BC95F0B008E7BCC /* AccountManager+AppGroup.swift */, - ); - name = Subscription; - sourceTree = ""; - }; EA7EFE662677F5BD0075464E /* PrivacyReferenceTests */ = { isa = PBXGroup; children = ( @@ -4587,7 +4555,6 @@ EE0153E22A6FE031002A8B26 /* Root */ = { isa = PBXGroup; children = ( - EE0153E52A6FE106002A8B26 /* NetworkProtectionRootViewModel.swift */, EE0153EC2A6FF9E6002A8B26 /* NetworkProtectionRootView.swift */, EE276BE92A77F823009167B6 /* NetworkProtectionRootViewController.swift */, ); @@ -4643,8 +4610,6 @@ isa = PBXGroup; children = ( EEFE9C722A603CE9005B0A26 /* NetworkProtectionStatusViewModelTests.swift */, - EE0153EA2A6FF970002A8B26 /* NetworkProtectionRootViewModelTests.swift */, - EE41BD182A729E9C00546C57 /* NetworkProtectionInviteViewModelTests.swift */, 4BCD146C2B05DB09000B1E4C /* NetworkProtectionAccessControllerTests.swift */, EEC02C152B065BE00045CE11 /* NetworkProtectionVPNLocationViewModelTests.swift */, BDFF03242BA3D92E00F324C9 /* NetworkProtectionFeatureVisibilityTests.swift */, @@ -4681,7 +4646,6 @@ EE458D122ABB651500FC651A /* Debug */, EE0153E22A6FE031002A8B26 /* Root */, EE0153DF2A6EABAF002A8B26 /* Helpers */, - EEFD562D2A65B68B00DAEC48 /* Invite */, BD862E012B30D9FB0073E2EE /* Feedback */, EECD94B32A28B96C0085C66E /* Status */, 4B5C46282AF2A6DB002A4432 /* Intents */, @@ -4700,15 +4664,6 @@ name = Status; sourceTree = ""; }; - EEFD562D2A65B68B00DAEC48 /* Invite */ = { - isa = PBXGroup; - children = ( - EEFD562E2A65B6CA00DAEC48 /* NetworkProtectionInviteViewModel.swift */, - EE0153EE2A70021E002A8B26 /* NetworkProtectionInviteView.swift */, - ); - name = Invite; - sourceTree = ""; - }; F1134EA71F3E2B3500B73467 /* Statistics */ = { isa = PBXGroup; children = ( @@ -4950,7 +4905,6 @@ F143C2E51E4A4CD400CFDE3A /* Core */ = { isa = PBXGroup; children = ( - D6FF22462BC95EF9008E7BCC /* Subscription */, F1CE42A71ECA0A520074A8DF /* Bookmarks */, 837774491F8E1ECE00E17A29 /* ContentBlocker */, F143C2E61E4A4CD400CFDE3A /* Core.h */, @@ -5035,6 +4989,7 @@ F15D43211E70849A00BF2CDC /* Autocomplete */ = { isa = PBXGroup; children = ( + 851672CD2BED1F8200592F24 /* Model */, F17922D41E7109DB006E3D97 /* UI */, ); name = Autocomplete; @@ -5090,12 +5045,11 @@ F17922D41E7109DB006E3D97 /* UI */ = { isa = PBXGroup; children = ( - 984147AD24F025F700362052 /* Autocomplete.storyboard */, F15D431F1E706CC500BF2CDC /* AutocompleteViewController.swift */, + 6FDB3F182BD11A4400F7A307 /* AutocompleteSuggestionsModel.swift */, F17922DF1E71BB59006E3D97 /* AutocompleteViewControllerDelegate.swift */, - F17922E11E71CD67006E3D97 /* NoSuggestionsTableViewCell.swift */, - F17922DD1E7192E6006E3D97 /* SuggestionTableViewCell.swift */, 8562CE142B9B645C00E1D399 /* CachedBookmarkSuggestions.swift */, + 851672D02BED1FC900592F24 /* AutocompleteView.swift */, ); name = UI; sourceTree = ""; @@ -5221,6 +5175,7 @@ 98D16975250CE707009513CC /* OmniBar.xib */, F130D7391E5776C500C45811 /* OmniBarDelegate.swift */, F1D477C51F2126CC0031ED49 /* OmniBarState.swift */, + 6FE095D72BD90AFB00490FF8 /* UniversalOmniBarState.swift */, 85DFEDF024C7EEA400973FE7 /* LargeOmniBarState.swift */, 85DFEDEE24C7EA3B00973FE7 /* SmallOmniBarState.swift */, 98AA92B22456FBE100ED4B9E /* SearchFieldContainerView.swift */, @@ -5238,7 +5193,6 @@ CB24F70E29A3EB15006DCC58 /* AppConfigurationURLProvider.swift */, 84E341951E2F7EFB00BDBA6F /* AppDelegate.swift */, 85DB12EC2A1FED0C000A4A72 /* AppDelegate+AppDeepLinks.swift */, - 4BCD14682B05BDD5000B1E4C /* AppDelegate+Waitlists.swift */, F1564F022B7B915F00D454A6 /* AppDelegate+SKAD4.swift */, 98B31291218CCB8C00E54DE1 /* AppDependencyProvider.swift */, 85BA58591F3506AE00C6E8CA /* AppSettings.swift */, @@ -5250,7 +5204,6 @@ 85C8E61C2B0E47380029A6BD /* BookmarksDatabaseSetup.swift */, 9821234D2B6D0A6300F08C57 /* UserAuthenticator.swift */, 9821234F2B6D233E00F08C57 /* UserSession.swift */, - D6A4645B2BD6D6DA00F80DC2 /* UserDefaultsCacheKey.swift */, ); name = Application; sourceTree = ""; @@ -5427,7 +5380,6 @@ F40F843528C938370081AE75 /* AutofillLoginListViewModelTests.swift */, C1D21E2E293A599C006E5A05 /* AutofillLoginSessionTests.swift */, C1CDA31D2AFBF811006D1476 /* AutofillNeverPromptWebsitesManagerTests.swift */, - C12B6E7B2BED69C100050D93 /* AutofillPixelReporterTests.swift */, ); name = Autofill; sourceTree = ""; @@ -5439,7 +5391,6 @@ F4147353283BF834004AA7A5 /* AutofillContentScopeFeatureToggles.swift */, C1D21E2C293A5965006E5A05 /* AutofillLoginSession.swift */, C1CDA3152AFB9C7F006D1476 /* AutofillNeverPromptWebsitesManager.swift */, - C12B6E792BED639500050D93 /* AutofillPixelReporter.swift */, C13B32D12A0E750700A59236 /* AutofillSettingStatus.swift */, 319A370F28299A850079FBCE /* PasswordHider.swift */, C136C7532BEB8CFC00ACC3B0 /* PasswordsSurveyView.swift */, @@ -5621,6 +5572,8 @@ F115ED9B2B4EFC8E001A0453 /* TestUtils */, 4BE67B042B96B9AB007335F7 /* ContentBlocking */, 4BE67B062B96B9B0007335F7 /* Common */, + F155318F2BF215ED0029ED04 /* Subscription */, + F15531912BF215ED0029ED04 /* SubscriptionTestingUtilities */, ); productName = DuckDuckGoTests; productReference = 84E341A61E2F7EFB00BDBA6F /* UnitTests.xctest */; @@ -5683,6 +5636,8 @@ 1E1D8B642995143200C96994 /* OHHTTPStubsSwift */, 4BE67B002B96B741007335F7 /* Common */, 4BE67B022B96B864007335F7 /* ContentBlocking */, + F15531932BF215F60029ED04 /* Subscription */, + F15531952BF215F60029ED04 /* SubscriptionTestingUtilities */, ); productName = IntegrationTests; productReference = 85D33FCB25C97B6E002B91A6 /* IntegrationTests.xctest */; @@ -6008,7 +5963,6 @@ 1E162615296D910F0004127F /* cookie-icon-animated-40-dark.json in Resources */, 85514FFD2372DA0100DBC528 /* ios13-home-row.mp4 in Resources */, 85F98F98296F4CB100742F4A /* SyncAssets.xcassets in Resources */, - 984147AB24F025F700362052 /* Autocomplete.storyboard in Resources */, AA4D6A9423DE49A5007E8790 /* AppIconBlack29x29@2x.png in Resources */, 98B001B3251EABB40090EC07 /* InfoPlist.strings in Resources */, AA4D6ACE23DE4D27007E8790 /* AppIconPurple60x60@3x.png in Resources */, @@ -6322,12 +6276,14 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + F1FDC9312BF4E0B3006B1435 /* SubscriptionEnvironment+Default.swift in Sources */, BDFF03232BA3D8E300F324C9 /* NetworkProtectionFeatureVisibility.swift in Sources */, EEEB80A32A421CE600386378 /* NetworkProtectionPacketTunnelProvider.swift in Sources */, EE3766DE2AC5945500AAB575 /* NetworkProtectionUNNotificationPresenter.swift in Sources */, - 4BB697A52B1D99C5003699B5 /* VPNWaitlistActivationDateStore.swift in Sources */, + F1FDC9362BF51E41006B1435 /* VPNSettings+Environment.swift in Sources */, BDFF03212BA3D3CF00F324C9 /* NetworkProtectionVisibilityForTunnelProvider.swift in Sources */, EEFC6A602AC0F2F80065027D /* UserText.swift in Sources */, + 7BC571212BDBB977003B0CCE /* VPNActivationDateStore.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -6357,19 +6313,19 @@ 8528AE81212F15D600D0BD74 /* AppRatingPrompt.xcdatamodeld in Sources */, 1E24295E293F57FA00584836 /* LottieView.swift in Sources */, 8577A1C5255D2C0D00D43FCD /* HitTestingToolbar.swift in Sources */, + 6FE095D82BD90AFB00490FF8 /* UniversalOmniBarState.swift in Sources */, 1DEAADE82BA38AA500E25A97 /* SettingsGeneralView.swift in Sources */, - 4BB697A42B1D99C4003699B5 /* VPNWaitlistActivationDateStore.swift in Sources */, 853C5F5B21BFF0AE001F7A05 /* HomeCollectionView.swift in Sources */, 3132FA2627A0784600DD7A12 /* FilePreviewHelper.swift in Sources */, 9820FF502244FECC008D4782 /* UIScrollViewExtension.swift in Sources */, 8540BD5423D8D5080057FDD2 /* PreserveLoginsAlert.swift in Sources */, + F15E9F3E2BEE128200DEFDDE /* SubscriptionManageriOS14.swift in Sources */, 1E87615928A1517200C7C5CE /* PrivacyDashboardViewController.swift in Sources */, EE9D68D12AE00CF300B55EF4 /* NetworkProtectionVPNSettingsView.swift in Sources */, 319A371028299A850079FBCE /* PasswordHider.swift in Sources */, 982C87C42255559A00919035 /* UITableViewCellExtension.swift in Sources */, B623C1C42862CD670043013E /* WKDownloadSession.swift in Sources */, 6FD1BAE42B87A107000C475C /* AdAttributionPixelReporter.swift in Sources */, - EEFD562F2A65B6CA00DAEC48 /* NetworkProtectionInviteViewModel.swift in Sources */, 1E8AD1D927C4FEC100ABA377 /* DownloadsListSectioningHelper.swift in Sources */, D60170BD2BA34CE8001911B5 /* Subscription.swift in Sources */, 1E4DCF4827B6A35400961E25 /* DownloadsListModel.swift in Sources */, @@ -6378,6 +6334,7 @@ 31669B9A28020A460071CC18 /* SaveLoginViewModel.swift in Sources */, EE4FB1882A28D11900E5CBA7 /* NetworkProtectionStatusViewModel.swift in Sources */, 8540BD5623D9E9C20057FDD2 /* PreserveLoginsSettingsViewController.swift in Sources */, + 851672D12BED1FC900592F24 /* AutocompleteView.swift in Sources */, 3161D13227AC161B00285CF6 /* DownloadMetadata.swift in Sources */, D664C7C72B289AA200CBFA76 /* PurchaseInProgressView.swift in Sources */, D6E83C122B1E6AB3006C8AFB /* SettingsView.swift in Sources */, @@ -6394,7 +6351,6 @@ BDFF03222BA3D8E200F324C9 /* NetworkProtectionFeatureVisibility.swift in Sources */, B652DEFD287BE67400C12A9C /* UserScripts.swift in Sources */, 1D200C992BA3176D00108701 /* SettingsOthersView.swift in Sources */, - C12B6E7A2BED639500050D93 /* AutofillPixelReporter.swift in Sources */, 31DD208427395A5A008FB313 /* VoiceSearchHelper.swift in Sources */, 9874F9EE2187AFCE00CAF33D /* Themable.swift in Sources */, F44D279E27F331BB0037F371 /* AutofillLoginPromptViewModel.swift in Sources */, @@ -6421,7 +6377,6 @@ B652DF12287C336E00C12A9C /* ContentBlockingUpdating.swift in Sources */, 314C92BA27C3E7CB0042EC96 /* QuickLookContainerViewController.swift in Sources */, 855D914D2063EF6A00C4B448 /* TabSwitcherTransition.swift in Sources */, - 4BBBBA8F2B031B4200D965DA /* VPNWaitlistView.swift in Sources */, CB258D1229A4F24900DEBA24 /* ConfigurationManager.swift in Sources */, 8546A54A2A672959003929BF /* MainViewController+Email.swift in Sources */, F4F6DFB226E6AEC100ED7E12 /* AddOrEditBookmarkViewController.swift in Sources */, @@ -6431,6 +6386,7 @@ D664C7C92B289AA200CBFA76 /* AsyncHeadlessWebView.swift in Sources */, F1F5337C1F26A9EF00D80D4F /* UserText.swift in Sources */, D6E83C5E2B224676006C8AFB /* SettingsCustomizeView.swift in Sources */, + 6FDB3F192BD11A4400F7A307 /* AutocompleteSuggestionsModel.swift in Sources */, 1E8AD1C727BE9B2900ABA377 /* DownloadsListDataSource.swift in Sources */, 3157B43527F497F50042D3D7 /* SaveLoginViewController.swift in Sources */, C14D43012B45D6CD00ACA4DC /* AutofillDebugViewController.swift in Sources */, @@ -6451,7 +6407,7 @@ F1386BA41E6846C40062FC3C /* TabDelegate.swift in Sources */, 37CF91602BB4737300BADCAE /* CrashCollectionOnboarding.swift in Sources */, C1B924B72ACD6E6800EE7B06 /* AutofillNeverSavedTableViewCell.swift in Sources */, - 4B78074E2B183A1F009DB2CF /* SurveyURLBuilder.swift in Sources */, + 4B78074E2B183A1F009DB2CF /* RemoteMessagingSurveyURLBuilder.swift in Sources */, 3132FA2A27A0788F00DD7A12 /* QuickLookPreviewHelper.swift in Sources */, D670E5BB2BB6A75300941A42 /* SubscriptionNavigationCoordinator.swift in Sources */, C1D21E2D293A5965006E5A05 /* AutofillLoginSession.swift in Sources */, @@ -6462,6 +6418,7 @@ C1B7B529289420830098FD6A /* RemoteMessaging.xcdatamodeld in Sources */, 986B16C425E92DF0007D23E8 /* BrowsingMenuViewController.swift in Sources */, 988AC355257E47C100793C64 /* RequeryLogic.swift in Sources */, + 6FBF0F8B2BD7C0A900136CF0 /* AllProtectedCell.swift in Sources */, 1E4F4A5A297193DE00625985 /* MainViewController+CookiesManaged.swift in Sources */, 8586A10D24CBA7070049720E /* FindInPageActivity.swift in Sources */, 1E1626072968413B0004127F /* ViewExtension.swift in Sources */, @@ -6592,12 +6549,10 @@ 850365F323DE087800D0F787 /* UIImageViewExtension.swift in Sources */, 373608922ABB430D00629E7F /* FavoritesDisplayMode+UserDefaults.swift in Sources */, C136C7542BEB8CFC00ACC3B0 /* PasswordsSurveyView.swift in Sources */, - 4BBBBA8D2B031B4200D965DA /* VPNWaitlistDebugViewController.swift in Sources */, C160544129D6044D00B715A1 /* AutofillInterfaceUsernameTruncator.swift in Sources */, 31C70B5528045E3500FB6AD1 /* SecureVaultReporter.swift in Sources */, F4CE6D1B257EA33C00D0A6AA /* FireButtonAnimator.swift in Sources */, 85582E0029D7409700E9AE35 /* SyncSettingsViewController+PDFRendering.swift in Sources */, - EE0153EF2A70021E002A8B26 /* NetworkProtectionInviteView.swift in Sources */, 9888F77B2224980500C46159 /* FeedbackViewController.swift in Sources */, D6E83C662B23936F006C8AFB /* SettingsDebugView.swift in Sources */, C1641EB12BC2F52B0012607A /* ImportPasswordsView.swift in Sources */, @@ -6617,6 +6572,7 @@ 980891A72237D5D800313A70 /* FeedbackPresenter.swift in Sources */, 989B337522D7EF2100437824 /* EmptyCollectionReusableView.swift in Sources */, 8524CC94246C5C8900E59D45 /* DaxDialogViewController.swift in Sources */, + 851672CF2BED1F9500592F24 /* HistoryMessageManager.swift in Sources */, F42EF9312614BABE00101FB9 /* ActionSheetDaxDialogViewController.swift in Sources */, EEC02C142B0519DE0045CE11 /* NetworkProtectionVPNLocationViewModel.swift in Sources */, F13B4BC01F180D8A00814661 /* TabsModel.swift in Sources */, @@ -6624,7 +6580,6 @@ 85AE6690209724120014CF04 /* NotificationView.swift in Sources */, 1EA51376286596A000493C6A /* PrivacyIconLogic.swift in Sources */, 980891A92238504B00313A70 /* UILabelExtension.swift in Sources */, - 4BCD146B2B05C4B5000B1E4C /* VPNWaitlistTermsAndConditionsViewController.swift in Sources */, 984D035A24ACCC7D0066CFB8 /* TabViewCell.swift in Sources */, 31951E8E2823003200CAF535 /* AutofillLoginDetailsHeaderView.swift in Sources */, F194FAED1F14E2B3009B4DF8 /* UIFontExtension.swift in Sources */, @@ -6632,7 +6587,6 @@ 98F0FC2021FF18E700CE77AB /* AutoClearSettingsViewController.swift in Sources */, D67969112BC84CE700BA8B34 /* SubscriptionContainerViewModel.swift in Sources */, F1E90C201E678E7C005E7E21 /* HomeControllerDelegate.swift in Sources */, - F17922DE1E7192E6006E3D97 /* SuggestionTableViewCell.swift in Sources */, 85DB12ED2A1FED0C000A4A72 /* AppDelegate+AppDeepLinks.swift in Sources */, 98DA6ECA2181E41F00E65433 /* ThemeManager.swift in Sources */, F1D43AFC2B99C56000BAB743 /* RootDebugViewController+VanillaBrowser.swift in Sources */, @@ -6656,7 +6610,6 @@ CBD4F13C279EBF4A00B20FD7 /* HomeMessage.swift in Sources */, 1DEAADEC2BA45B4500E25A97 /* SettingsAccessibilityView.swift in Sources */, 85C8E61D2B0E47380029A6BD /* BookmarksDatabaseSetup.swift in Sources */, - 4BBBBA8E2B031B4200D965DA /* VPNWaitlistViewController.swift in Sources */, 3132FA2C27A07A1B00DD7A12 /* FilePreview.swift in Sources */, 85C861E628FF1B5F00189466 /* HomeViewSectionRenderersExtension.swift in Sources */, 1DDF40292BA04FCD006850D9 /* SettingsPrivacyProtectionsView.swift in Sources */, @@ -6675,10 +6628,10 @@ 982123502B6D233E00F08C57 /* UserSession.swift in Sources */, 85449EFD23FDA71F00512AAF /* KeyboardSettings.swift in Sources */, 980891A222369ADB00313A70 /* FeedbackUserText.swift in Sources */, - 4BCD14692B05BDD5000B1E4C /* AppDelegate+Waitlists.swift in Sources */, 1DEAADFF2BA7832F00E25A97 /* EmailProtectionView.swift in Sources */, 988F3DD3237DE8D900AEE34C /* ForgetDataAlert.swift in Sources */, D6FEB8B12B7498A300C3615F /* HeadlessWebView.swift in Sources */, + F1FDC9352BF51E41006B1435 /* VPNSettings+Environment.swift in Sources */, 850ABD012AC3961100A733DF /* MainViewController+Segues.swift in Sources */, 9817C9C321EF594700884F65 /* AutoClear.swift in Sources */, 9821234E2B6D0A6300F08C57 /* UserAuthenticator.swift in Sources */, @@ -6691,7 +6644,6 @@ 4BCD14672B05B682000B1E4C /* NetworkProtectionTermsAndConditionsStore.swift in Sources */, C1F341C92A6926920032057B /* EmailAddressPromptViewController.swift in Sources */, 311BD1B12836C0CA00AEF6C1 /* AutofillLoginListAuthenticator.swift in Sources */, - 4BBBBA902B031B4200D965DA /* VPNWaitlist.swift in Sources */, B652DF13287C373A00C12A9C /* ScriptSourceProviding.swift in Sources */, 854A012B2A54412600FCC628 /* ActivityViewController.swift in Sources */, F1CA3C391F045885005FADB3 /* PrivacyUserDefaults.swift in Sources */, @@ -6706,6 +6658,7 @@ 85058368219C49E000ED4EDB /* HomeViewSectionRenderers.swift in Sources */, 1DEAADEE2BA45DFE00E25A97 /* SettingsDataClearingView.swift in Sources */, EE01EB432AFC1E0A0096AAC9 /* NetworkProtectionVPNLocationView.swift in Sources */, + 7BC571202BDBB877003B0CCE /* VPNActivationDateStore.swift in Sources */, 9820EAF522613CD30089094D /* WebProgressWorker.swift in Sources */, B6CB93E5286445AB0090FEB4 /* Base64DownloadSession.swift in Sources */, 1EEF387D285B1A1100383393 /* TrackerImageCache.swift in Sources */, @@ -6726,7 +6679,6 @@ D60B1F272B9DDE5A00AE4760 /* SubscriptionGoogleView.swift in Sources */, 984D035C24AE15CD0066CFB8 /* TabSwitcherSettings.swift in Sources */, D6E83C562B21ECC1006C8AFB /* SettingsLegacyViewProvider.swift in Sources */, - D6A4645C2BD6D6DA00F80DC2 /* UserDefaultsCacheKey.swift in Sources */, 98B31292218CCB8C00E54DE1 /* AppDependencyProvider.swift in Sources */, D670E5BD2BB6AA0000941A42 /* View+AppearModifiers.swift in Sources */, D6ACEA322BBD55BF008FADDF /* TabURLInterceptor.swift in Sources */, @@ -6735,6 +6687,7 @@ 851624C72B96389D002D5CD7 /* HistoryDebugViewController.swift in Sources */, 8540BBA22440857A00017FE4 /* PreserveLoginsWorker.swift in Sources */, 85DFEDF924CF3D0E00973FE7 /* TabsBarCell.swift in Sources */, + 851672D32BED23FE00592F24 /* AutocompleteViewModel.swift in Sources */, 8562CE152B9B645C00E1D399 /* CachedBookmarkSuggestions.swift in Sources */, C13F3F682B7F88100083BE40 /* AuthConfirmationPromptView.swift in Sources */, F1617C191E573EA800DEDCAF /* TabSwitcherDelegate.swift in Sources */, @@ -6754,6 +6707,7 @@ EE4BE0092A740BED00CD6AA8 /* ClearTextField.swift in Sources */, F159BDA41F0BDB5A00B4A01D /* TabViewController.swift in Sources */, F44D279C27F331BB0037F371 /* AutofillLoginPromptView.swift in Sources */, + F1FDC9302BF4E0B3006B1435 /* SubscriptionEnvironment+Default.swift in Sources */, CBD4F13E279EBFAB00B20FD7 /* HomeMessageView.swift in Sources */, 851DFD87212C39D300D95F20 /* TabSwitcherButton.swift in Sources */, 8505836A219F424500ED4EDB /* UIAlertControllerExtension.swift in Sources */, @@ -6765,7 +6719,6 @@ 983D71B12A286E810072E26D /* SyncDebugViewController.swift in Sources */, 6FDA1FB32B59584400AC962A /* AddressDisplayHelper.swift in Sources */, F103073B1E7C91330059FEC7 /* BookmarksDataSource.swift in Sources */, - EE0153E62A6FE106002A8B26 /* NetworkProtectionRootViewModel.swift in Sources */, 85864FBC24D31EF300E756FF /* SuggestionTrayViewController.swift in Sources */, D64648AF2B5993890033090B /* SubscriptionEmailViewModel.swift in Sources */, 1EF24235273BB9D200DE3D02 /* IntervalSlider.swift in Sources */, @@ -6796,7 +6749,6 @@ 985892522260B1B200EEB31B /* ProgressView.swift in Sources */, 85BA585A1F3506AE00C6E8CA /* AppSettings.swift in Sources */, 3151F0EA27357FBA00226F58 /* SpeechRecognizer.swift in Sources */, - F17922E21E71CD67006E3D97 /* NoSuggestionsTableViewCell.swift in Sources */, 85B9814E2B5EB618009AC9A6 /* SwipeTabsCoordinator.swift in Sources */, 985AAE4524899369007A43EC /* HomeScreenTransition.swift in Sources */, 85E58C2C28FDA94F006A801A /* FavoritesViewController.swift in Sources */, @@ -6906,7 +6858,6 @@ 9F2510142BF5809E0096DB16 /* SubscriptionFlowViewModelTests.swift in Sources */, F189AED71F18F6DE001EBAE1 /* TabTests.swift in Sources */, F13B4BFB1F18E3D900814661 /* TabsModelPersistenceExtensionTests.swift in Sources */, - C12B6E7C2BED69C100050D93 /* AutofillPixelReporterTests.swift in Sources */, CB48D3372B90DF2000631D8B /* UserBehaviorMonitorTests.swift in Sources */, 8528AE7E212EF5FF00D0BD74 /* AppRatingPromptTests.swift in Sources */, 981FED692201FE69008488D7 /* AutoClearSettingsScreenTests.swift in Sources */, @@ -6957,7 +6908,6 @@ C14882E727F20DAB00D59F0C /* HtmlTestDataLoader.swift in Sources */, F17D72391E8B35C6003E8B0E /* AppURLsTests.swift in Sources */, F1134ED61F40F29F00B73467 /* StatisticsUserDefaultsTests.swift in Sources */, - EE41BD192A729E9C00546C57 /* NetworkProtectionInviteViewModelTests.swift in Sources */, C1B7B53028944E390098FD6A /* RemoteMessagingStoreTests.swift in Sources */, 98EA2C3C218B9AAD0023E1DC /* ThemeManagerTests.swift in Sources */, 569437292BDD487600C0881B /* SyncCredentialsAdapterTests.swift in Sources */, @@ -6975,7 +6925,6 @@ 8588026624E420BD00C24AB6 /* LargeOmniBarStateTests.swift in Sources */, 5694372E2BE3F5B300C0881B /* CapturingAlertPresenter.swift in Sources */, 85AFA1212B45D14F0028A504 /* BookmarksMigrationAssertionTests.swift in Sources */, - EE0153EB2A6FF970002A8B26 /* NetworkProtectionRootViewModelTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -7113,7 +7062,6 @@ 854858E32937BC550063610B /* CollectionExtension.swift in Sources */, 1E6A4D692984208800A371D3 /* LocaleExtension.swift in Sources */, 98F6EA472863124100720957 /* ContentBlockerRulesLists.swift in Sources */, - D6FF22482BC95F0B008E7BCC /* AccountManager+AppGroup.swift in Sources */, 566B73732BECE4F200FF1959 /* SyncErrorHandling.swift in Sources */, F1134EB01F40AC6300B73467 /* AtbParser.swift in Sources */, EE50052E29C369D300AE0773 /* FeatureFlag.swift in Sources */, @@ -7397,38 +7345,6 @@ name = Onboarding.storyboard; sourceTree = ""; }; - 984147AD24F025F700362052 /* Autocomplete.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 984147AC24F025F700362052 /* Base */, - 9866DB8B251CA8F300612E3A /* bg */, - 9866DBA2251CA91700612E3A /* hr */, - 9866DBB9251CA92A00612E3A /* cs */, - 9866DBD0251CA93800612E3A /* da */, - 9866DBE7251CA94E00612E3A /* nl */, - 9866DBFE251CA96200612E3A /* et */, - 9866DC15251CA99A00612E3A /* de */, - 9866DC2C251CA9AF00612E3A /* el */, - 9866DC43251CA9BF00612E3A /* hu */, - 9866DC5A251CA9CE00612E3A /* it */, - 9866DC71251CA9E200612E3A /* lv */, - 9866DC88251CA9F400612E3A /* lt */, - 9866DC9F251CAA0500612E3A /* pl */, - 9866DCB6251CAA2500612E3A /* ro */, - 9866DCCD251CAA3300612E3A /* sk */, - 9866DCE4251CAA4800612E3A /* sl */, - 9866DD04251CACC500612E3A /* fi */, - 9866DD06251CACC600612E3A /* fr */, - 9866DD0A251CACCD00612E3A /* pt */, - 9866DD0C251CACCD00612E3A /* ru */, - 9866DD0E251CACCE00612E3A /* es */, - 9866DD10251CACCF00612E3A /* sv */, - 9866DD12251CACCF00612E3A /* tr */, - 981685432521EEEF00FA91A1 /* nb */, - ); - name = Autocomplete.storyboard; - sourceTree = ""; - }; 984147B024F0261A00362052 /* Feedback.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -8057,7 +7973,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProvider.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -8094,7 +8010,7 @@ CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -8186,7 +8102,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 2; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -8214,7 +8130,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -8364,7 +8280,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -8390,7 +8306,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 2; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGo/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -8411,6 +8327,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGoTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -8419,6 +8336,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.mobile.ios.Tests; PRODUCT_NAME = "$(TARGET_NAME)"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = ""; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DuckDuckGo.app/DuckDuckGo"; }; @@ -8429,6 +8347,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGoTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -8437,6 +8356,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.mobile.ios.Tests; PRODUCT_NAME = "$(TARGET_NAME)"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = ""; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DuckDuckGo.app/DuckDuckGo"; }; @@ -8456,7 +8376,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 2; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -8491,7 +8411,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 2; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -8525,7 +8445,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 2; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -8556,7 +8476,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -8871,7 +8791,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -8903,7 +8823,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 2; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -8932,7 +8852,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 2; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -8966,7 +8886,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 2; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -8997,7 +8917,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProviderAlpha.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -9030,11 +8950,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 0; + DYLIB_CURRENT_VERSION = 2; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -9100,6 +9020,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGoTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -9108,6 +9029,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.mobile.ios.Tests; PRODUCT_NAME = "$(TARGET_NAME)"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = ""; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DuckDuckGo.app/DuckDuckGo"; }; @@ -9268,7 +9190,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9296,7 +9218,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9329,7 +9251,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9367,7 +9289,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 2; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9403,7 +9325,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9438,11 +9360,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 0; + DYLIB_CURRENT_VERSION = 2; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -9508,6 +9430,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGoTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -9516,6 +9439,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.mobile.ios.Tests; PRODUCT_NAME = "$(TARGET_NAME)"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = ""; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DuckDuckGo.app/DuckDuckGo"; }; @@ -9616,11 +9540,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 0; + DYLIB_CURRENT_VERSION = 2; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -9649,10 +9573,10 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 2; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 0; + DYLIB_CURRENT_VERSION = 2; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10099,6 +10023,26 @@ package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = TestUtils; }; + F155318F2BF215ED0029ED04 /* Subscription */ = { + isa = XCSwiftPackageProductDependency; + package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Subscription; + }; + F15531912BF215ED0029ED04 /* SubscriptionTestingUtilities */ = { + isa = XCSwiftPackageProductDependency; + package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = SubscriptionTestingUtilities; + }; + F15531932BF215F60029ED04 /* Subscription */ = { + isa = XCSwiftPackageProductDependency; + package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Subscription; + }; + F15531952BF215F60029ED04 /* SubscriptionTestingUtilities */ = { + isa = XCSwiftPackageProductDependency; + package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = SubscriptionTestingUtilities; + }; F1D43AF92B99C1D300BAB743 /* BareBonesBrowserKit */ = { isa = XCSwiftPackageProductDependency; package = F1D43AF82B99C1D300BAB743 /* XCRemoteSwiftPackageReference "BareBonesBrowser" */; diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index aaac00614a..8b81d05c0f 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "revision" : "bb8e7e62104ed6506c7bfd3ef7aa4aca3686ed4f", - "version" : "5.15.0" + "revision" : "fa861c4eccb21d235e34070b208b78bdc32ece08", + "version" : "5.17.0" } }, { diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo.xcscheme index f875782b6b..d2c6604405 100644 --- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo.xcscheme +++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo.xcscheme @@ -61,6 +61,9 @@ + + Bool { @@ -211,19 +208,26 @@ class AppDelegate: UIResponder, UIApplicationDelegate { PrivacyFeatures.httpsUpgrade.loadDataAsync() + let variantManager = DefaultVariantManager() + let historyMessageManager = HistoryMessageManager() + // assign it here, because "did become active" is already too late and "viewWillAppear" // has already been called on the HomeViewController so won't show the home row CTA AtbAndVariantCleanup.cleanup() - DefaultVariantManager().assignVariantIfNeeded { _ in + variantManager.assignVariantIfNeeded { _ in // MARK: perform first time launch logic here DaxDialogs.shared.primeForUse() + historyMessageManager.dismiss() + } + + if variantManager.isSupported(feature: .history) { + historyMessageManager.dismiss() } PixelExperimentForBrokenSites.install() - PixelExperiment.install() + PixelExperiment.cleanup() // MARK: Sync initialisation - #if DEBUG let defaultEnvironment = ServerEnvironment.development #else @@ -269,7 +273,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } let previewsSource = TabPreviewsSource() - let historyManager = makeHistoryManager() + let historyManager = makeHistoryManager(AppDependencyProvider.shared.appSettings, + AppDependencyProvider.shared.internalUserDecider, + ContentBlocking.shared.privacyConfigurationManager) let tabsModel = prepareTabsModel(previewsSource: previewsSource) let main = MainViewController(bookmarksDatabase: bookmarksDatabase, @@ -294,8 +300,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } autoClear = AutoClear(worker: main) + let applicationState = application.applicationState Task { - await autoClear?.clearDataIfEnabled(launching: true) + await autoClear?.clearDataIfEnabled(applicationState: .init(with: applicationState)) } AppDependencyProvider.shared.voiceSearchHelper.migrateSettingsFlagIfNecessary() @@ -323,19 +330,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate { #if NETWORK_PROTECTION widgetRefreshModel.beginObservingVPNStatus() - NetworkProtectionAccessController().refreshNetworkProtectionAccess() + AppDependencyProvider.shared.networkProtectionAccessController.refreshNetworkProtectionAccess() #endif - - setupSubscriptionsEnvironment() - - if vpnFeatureVisibility.shouldKeepVPNAccessViaWaitlist() { - clearDebugWaitlistState() - } AppDependencyProvider.shared.toggleProtectionsCounter.sendEventsIfNeeded() AppDependencyProvider.shared.userBehaviorMonitor.handleAction(.reopenApp) + setUpAutofillPixelReporter() + return true } @@ -360,24 +363,36 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return tabsModel } - private func makeHistoryManager() -> HistoryManager { - let historyManager = HistoryManager(privacyConfigManager: ContentBlocking.shared.privacyConfigurationManager, - variantManager: DefaultVariantManager(), - database: HistoryDatabase.make()) { error in - Pixel.fire(pixel: .historyStoreLoadFailed, error: error) - if error.isDiskFull { + private func makeHistoryManager(_ appSettings: AppSettings, + _ internalUserDecider: InternalUserDecider, + _ privacyConfigManager: PrivacyConfigurationManaging) -> HistoryManager { + + let db = HistoryDatabase.make() + var loadError: Error? + db.loadStore { _, error in + loadError = error + } + + if let loadError { + Pixel.fire(pixel: .historyStoreLoadFailed, error: loadError) + if loadError.isDiskFull { self.presentInsufficientDiskSpaceAlert() } else { self.presentPreemptiveCrashAlert() } } - // This is a compromise to support hot reloading via privacy config. - // * If the history is disabled this will do nothing. If it is subsequently enabled then it won't start collecting history - // until the app cold launches at least once. - // * If the history is enabled this loads the store sets up the history manager - // correctly. If the history manager is subsequently disabled it will stop working immediately. - historyManager.loadStore() + let historyManager = HistoryManager(privacyConfigManager: privacyConfigManager, + variantManager: DefaultVariantManager(), + database: db, + internalUserDecider: internalUserDecider, + isEnabledByUser: appSettings.recentlyVisitedSites) + + // Ensure we don't do this if the history is disabled in privacy confg + guard historyManager.isHistoryFeatureEnabled() else { return historyManager } + historyManager.loadStore(onCleanFinished: { + // Do future migrations after clean has finished. See macOS for an example. + }) return historyManager } @@ -406,7 +421,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { private func presentExpiredEntitlementNotificationIfNeeded() { let presenter = NetworkProtectionNotificationsPresenterTogglableDecorator( - settings: VPNSettings(defaults: .networkProtectionGroupDefaults), + settings: AppDependencyProvider.shared.vpnSettings, defaults: .networkProtectionGroupDefaults, wrappee: NetworkProtectionUNNotificationPresenter() ) @@ -431,21 +446,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } } - - private func setupSubscriptionsEnvironment() { - Task { - #if ALPHA || DEBUG - let defaultEnvironment = SubscriptionPurchaseEnvironment.ServiceEnvironment.staging - #else - let defaultEnvironment = SubscriptionPurchaseEnvironment.ServiceEnvironment.production - #endif - let environment = SubscriptionPurchaseEnvironment.ServiceEnvironment(rawValue: privacyProEnvironment) ?? defaultEnvironment - SubscriptionPurchaseEnvironment.currentServiceEnvironment = environment - VPNSettings(defaults: .networkProtectionGroupDefaults).selectedEnvironment = (environment == .production) ? .production : .staging - SubscriptionPurchaseEnvironment.current = .appStore - } - } - private func reportAdAttribution() { guard AdAttributionPixelReporter.isAdAttributionReportingEnabled else { return } @@ -498,10 +498,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } } - checkWaitlists() syncService.scheduler.notifyAppLifecycleEvent() fireFailedCompilationsPixelIfNeeded() - refreshShortcuts() + + Task { + await refreshShortcuts() + } #if NETWORK_PROTECTION widgetRefreshModel.refreshVPNWidget() @@ -522,16 +524,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } private func stopTunnelAndShowThankYouMessagingIfNeeded() { - if AccountManager().isUserAuthenticated { - tunnelDefaults.vpnEarlyAccessOverAlertAlreadyShown = true + if accountManager.isUserAuthenticated { return } - if vpnFeatureVisibility.shouldShowThankYouMessaging() && !tunnelDefaults.vpnEarlyAccessOverAlertAlreadyShown { - Task { - await self.stopAndRemoveVPN(with: "thank-you-dialog") - } - } else if vpnFeatureVisibility.isPrivacyProLaunched() && !AccountManager().isUserAuthenticated { + if AppDependencyProvider.shared.vpnFeatureVisibility.isPrivacyProLaunched() && !accountManager.isUserAuthenticated { Task { await self.stopAndRemoveVPN(with: "subscription-check") } @@ -539,41 +536,41 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } private func stopAndRemoveVPN(with reason: String) async { - let controller = NetworkProtectionTunnelController() - guard await controller.isInstalled else { + guard await AppDependencyProvider.shared.networkProtectionTunnelController.isInstalled else { return } - let isConnected = await controller.isConnected + let isConnected = await AppDependencyProvider.shared.networkProtectionTunnelController.isConnected DailyPixel.fireDailyAndCount(pixel: .privacyProVPNBetaStoppedWhenPrivacyProEnabled, withAdditionalParameters: [ "reason": reason, "vpn-connected": String(isConnected) ]) - await controller.stop() - await controller.removeVPN() + await AppDependencyProvider.shared.networkProtectionTunnelController.stop() + await AppDependencyProvider.shared.networkProtectionTunnelController.removeVPN() } func updateSubscriptionStatus() { Task { - let accountManager = AccountManager() - guard let token = accountManager.accessToken else { return } - - if case .success(let subscription) = await SubscriptionService.getSubscription(accessToken: token, + var subscriptionService: SubscriptionService { + AppDependencyProvider.shared.subscriptionManager.subscriptionService + } + if case .success(let subscription) = await subscriptionService.getSubscription(accessToken: token, cachePolicy: .reloadIgnoringLocalCacheData) { if subscription.isActive { DailyPixel.fire(pixel: .privacyProSubscriptionActive) } } - - _ = await accountManager.fetchEntitlements(cachePolicy: .reloadIgnoringLocalCacheData) + await accountManager.fetchEntitlements(cachePolicy: .reloadIgnoringLocalCacheData) } } func applicationWillResignActive(_ application: UIApplication) { - refreshShortcuts() + Task { + await refreshShortcuts() + } } private func fireAppLaunchPixel() { @@ -667,14 +664,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate { Task { @MainActor in await beginAuthentication() - await autoClear?.clearDataIfEnabledAndTimeExpired() + await autoClear?.clearDataIfEnabledAndTimeExpired(applicationState: .active) showKeyboardIfSettingOn = true syncService.scheduler.resumeSyncQueue() } AppDependencyProvider.shared.userBehaviorMonitor.handleAction(.reopenApp) - - autofillPixelReporter.checkIfOnboardedUser() } func applicationDidEnterBackground(_ application: UIApplication) { @@ -849,7 +844,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { if appIsLaunching { await autoClear?.clearDataIfEnabled() } else { - await autoClear?.clearDataIfEnabledAndTimeExpired() + await autoClear?.clearDataIfEnabledAndTimeExpired(applicationState: .active) } if shortcutItem.type == ShortcutKey.clipboard, let query = UIPasteboard.general.string { @@ -903,22 +898,47 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return window?.rootViewController as? MainViewController } - func refreshShortcuts() { + private func setUpAutofillPixelReporter() { + autofillPixelReporter = AutofillPixelReporter( + userDefaults: .standard, + eventMapping: EventMapping {event, _, params, _ in + switch event { + case .autofillActiveUser: + Pixel.fire(pixel: .autofillActiveUser) + case .autofillEnabledUser: + Pixel.fire(pixel: .autofillEnabledUser) + case .autofillOnboardedUser: + Pixel.fire(pixel: .autofillOnboardedUser) + case .autofillLoginsStacked: + Pixel.fire(pixel: .autofillLoginsStacked, withAdditionalParameters: params ?? [:]) + default: + break + } + }, + installDate: StatisticsUserDefaults().installDate ?? Date()) + } + + @MainActor + func refreshShortcuts() async { #if NETWORK_PROTECTION - guard vpnFeatureVisibility.shouldShowVPNShortcut() else { + guard AppDependencyProvider.shared.vpnFeatureVisibility.shouldShowVPNShortcut() else { UIApplication.shared.shortcutItems = nil return } - let items = [ - UIApplicationShortcutItem(type: ShortcutKey.openVPNSettings, - localizedTitle: UserText.netPOpenVPNQuickAction, - localizedSubtitle: nil, - icon: UIApplicationShortcutIcon(templateImageName: "VPN-16"), - userInfo: nil) - ] + if case .success(true) = await accountManager.hasEntitlement(for: .networkProtection, cachePolicy: .returnCacheDataDontLoad) { + let items = [ + UIApplicationShortcutItem(type: ShortcutKey.openVPNSettings, + localizedTitle: UserText.netPOpenVPNQuickAction, + localizedSubtitle: nil, + icon: UIApplicationShortcutIcon(templateImageName: "VPN-16"), + userInfo: nil) + ] - UIApplication.shared.shortcutItems = items + UIApplication.shared.shortcutItems = items + } else { + UIApplication.shared.shortcutItems = nil + } #endif } @@ -981,10 +1001,6 @@ extension AppDelegate: UNUserNotificationCenterDelegate { if NetworkProtectionNotificationIdentifier(rawValue: identifier) != nil { presentNetworkProtectionStatusSettingsModal() } - - if vpnFeatureVisibility.shouldKeepVPNAccessViaWaitlist(), identifier == VPNWaitlist.notificationIdentifier { - presentNetworkProtectionWaitlistModal() - } #endif } @@ -992,16 +1008,8 @@ extension AppDelegate: UNUserNotificationCenterDelegate { } #if NETWORK_PROTECTION - private func presentNetworkProtectionWaitlistModal() { - if #available(iOS 15, *) { - let networkProtectionRoot = VPNWaitlistViewController(nibName: nil, bundle: nil) - presentSettings(with: networkProtectionRoot) - } - } - func presentNetworkProtectionStatusSettingsModal() { Task { - let accountManager = AccountManager() if case .success(let hasEntitlements) = await accountManager.hasEntitlement(for: .networkProtection), hasEntitlements { if #available(iOS 15, *) { @@ -1043,6 +1051,22 @@ extension AppDelegate: UNUserNotificationCenterDelegate { } } +extension DataStoreWarmup.ApplicationState { + + init(with state: UIApplication.State) { + switch state { + case .inactive: + self = .inactive + case .active: + self = .active + case .background: + self = .background + @unknown default: + self = .unknown + } + } +} + private extension Error { var isDiskFull: Bool { diff --git a/DuckDuckGo/AppDependencyProvider.swift b/DuckDuckGo/AppDependencyProvider.swift index 09281657e9..76221f2fef 100644 --- a/DuckDuckGo/AppDependencyProvider.swift +++ b/DuckDuckGo/AppDependencyProvider.swift @@ -23,6 +23,8 @@ import BrowserServicesKit import DDGSync import Bookmarks import Subscription +import Common +import NetworkProtection protocol DependencyProvider { @@ -41,7 +43,14 @@ protocol DependencyProvider { var toggleProtectionsCounter: ToggleProtectionsCounter { get } var userBehaviorMonitor: UserBehaviorMonitor { get } var subscriptionFeatureAvailability: SubscriptionFeatureAvailability { get } - + var subscriptionManager: SubscriptionManaging { get } + var accountManager: AccountManaging { get } + var vpnFeatureVisibility: DefaultNetworkProtectionVisibility { get } + var networkProtectionKeychainTokenStore: NetworkProtectionKeychainTokenStore { get } + var networkProtectionAccessController: NetworkProtectionAccessController { get } + var networkProtectionTunnelController: NetworkProtectionTunnelController { get } + var connectionObserver: ConnectionStatusObserver { get } + var vpnSettings: VPNSettings { get } } /// Provides dependencies for objects that are not directly instantiated @@ -54,10 +63,7 @@ class AppDependencyProvider: DependencyProvider { let variantManager: VariantManager = DefaultVariantManager() let internalUserDecider: InternalUserDecider = ContentBlocking.shared.privacyConfigurationManager.internalUserDecider - lazy var featureFlagger: FeatureFlagger = DefaultFeatureFlagger( - internalUserDecider: internalUserDecider, - privacyConfigManager: ContentBlocking.shared.privacyConfigurationManager - ) + let featureFlagger: FeatureFlagger let remoteMessagingStore: RemoteMessagingStore = RemoteMessagingStore() lazy var homePageConfiguration: HomePageConfiguration = HomePageConfiguration(variantManager: variantManager, @@ -72,9 +78,89 @@ class AppDependencyProvider: DependencyProvider { let toggleProtectionsCounter: ToggleProtectionsCounter = ContentBlocking.shared.privacyConfigurationManager.toggleProtectionsCounter let userBehaviorMonitor = UserBehaviorMonitor() - + let subscriptionFeatureAvailability: SubscriptionFeatureAvailability = DefaultSubscriptionFeatureAvailability( privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager, purchasePlatform: .appStore) + // Subscription + let subscriptionManager: SubscriptionManaging + var accountManager: AccountManaging { + subscriptionManager.accountManager + } + let vpnFeatureVisibility: DefaultNetworkProtectionVisibility + let networkProtectionKeychainTokenStore: NetworkProtectionKeychainTokenStore + let networkProtectionAccessController: NetworkProtectionAccessController + let networkProtectionTunnelController: NetworkProtectionTunnelController + + let subscriptionAppGroup = Bundle.main.appGroup(bundle: .subs) + + let connectionObserver: ConnectionStatusObserver = ConnectionStatusObserverThroughSession() + let vpnSettings = VPNSettings(defaults: .networkProtectionGroupDefaults) + + // swiftlint:disable:next function_body_length + init() { + featureFlagger = DefaultFeatureFlagger(internalUserDecider: internalUserDecider, + privacyConfigManager: ContentBlocking.shared.privacyConfigurationManager) + + // MARK: - Configure Subscription + let subscriptionUserDefaults = UserDefaults(suiteName: subscriptionAppGroup)! + let subscriptionEnvironment = SubscriptionManager.getSavedOrDefaultEnvironment(userDefaults: subscriptionUserDefaults) + vpnSettings.alignTo(subscriptionEnvironment: subscriptionEnvironment) + + let entitlementsCache = UserDefaultsCache<[Entitlement]>(userDefaults: subscriptionUserDefaults, + key: UserDefaultsCacheKey.subscriptionEntitlements, + settings: UserDefaultsCacheSettings(defaultExpirationInterval: .minutes(20))) + let accessTokenStorage = SubscriptionTokenKeychainStorage(keychainType: .dataProtection(.named(subscriptionAppGroup))) + let subscriptionService = SubscriptionService(currentServiceEnvironment: subscriptionEnvironment.serviceEnvironment) + let authService = AuthService(currentServiceEnvironment: subscriptionEnvironment.serviceEnvironment) + let accountManager = AccountManager(accessTokenStorage: accessTokenStorage, + entitlementsCache: entitlementsCache, + subscriptionService: subscriptionService, + authService: authService) + if #available(iOS 15.0, *) { + subscriptionManager = SubscriptionManager(storePurchaseManager: StorePurchaseManager(), + accountManager: accountManager, + subscriptionService: subscriptionService, + authService: authService, + subscriptionEnvironment: subscriptionEnvironment) + } else { + // This is used just for iOS <15, it's a sort of mocked environment that will not be used. + subscriptionManager = SubscriptionManageriOS14(accountManager: accountManager) + } + + let subscriptionFeatureAvailability: SubscriptionFeatureAvailability = DefaultSubscriptionFeatureAvailability( + privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager, + purchasePlatform: .appStore) + let accessTokenProvider: () -> String? = { + func isSubscriptionEnabled() -> Bool { + if let subscriptionOverrideEnabled = UserDefaults.networkProtectionGroupDefaults.subscriptionOverrideEnabled { +#if ALPHA || DEBUG + return subscriptionOverrideEnabled +#else + return false +#endif + } + return subscriptionFeatureAvailability.isFeatureAvailable + } + + if isSubscriptionEnabled() { + return { accountManager.accessToken } + } + return { nil } + }() + networkProtectionKeychainTokenStore = NetworkProtectionKeychainTokenStore(keychainType: .dataProtection(.unspecified), + serviceName: "\(Bundle.main.bundleIdentifier!).authToken", + errorEvents: .networkProtectionAppDebugEvents, + isSubscriptionEnabled: true, + accessTokenProvider: accessTokenProvider) + networkProtectionTunnelController = NetworkProtectionTunnelController(accountManager: accountManager, + tokenStore: networkProtectionKeychainTokenStore) + networkProtectionAccessController = NetworkProtectionAccessController(featureFlagger: featureFlagger, + internalUserDecider: internalUserDecider, + tokenStore: networkProtectionKeychainTokenStore, + networkProtectionTunnelController: networkProtectionTunnelController) + vpnFeatureVisibility = DefaultNetworkProtectionVisibility(userDefaults: .networkProtectionGroupDefaults, + accountManager: accountManager) + } } diff --git a/DuckDuckGo/AppSettings.swift b/DuckDuckGo/AppSettings.swift index 0ff7cd133b..484198db9d 100644 --- a/DuckDuckGo/AppSettings.swift +++ b/DuckDuckGo/AppSettings.swift @@ -44,6 +44,7 @@ enum AddressBarPosition: String, CaseIterable, CustomStringConvertible { protocol AppSettings: AnyObject { var autocomplete: Bool { get set } + var recentlyVisitedSites: Bool { get set } var currentThemeName: ThemeName { get set } var autoClearAction: AutoClearSettingsModel.Action { get set } diff --git a/DuckDuckGo/AppUserDefaults.swift b/DuckDuckGo/AppUserDefaults.swift index 364e8254e0..dacbfe6831 100644 --- a/DuckDuckGo/AppUserDefaults.swift +++ b/DuckDuckGo/AppUserDefaults.swift @@ -45,6 +45,7 @@ public class AppUserDefaults: AppSettings { struct Keys { static let autocompleteKey = "com.duckduckgo.app.autocompleteDisabledKey" + static let recentlyVisitedSites = "com.duckduckgo.app.recentlyVisitedSitesKey" static let currentThemeNameKey = "com.duckduckgo.app.currentThemeNameKey" static let autoClearActionKey = "com.duckduckgo.app.autoClearActionKey" @@ -107,6 +108,18 @@ public class AppUserDefaults: AppSettings { } + var recentlyVisitedSites: Bool { + + get { + return userDefaults?.bool(forKey: Keys.recentlyVisitedSites, defaultValue: true) ?? true + } + + set { + userDefaults?.setValue(newValue, forKey: Keys.recentlyVisitedSites) + } + + } + var currentThemeName: ThemeName { get { diff --git a/DuckDuckGo/Assets.xcassets/DesignSystemIcons/16px/Arrow-Circle-Down-Left-16.imageset/Arrow-Circle-Down-Left-16.pdf b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/16px/Arrow-Circle-Down-Left-16.imageset/Arrow-Circle-Down-Left-16.pdf new file mode 100644 index 0000000000..2768c3de8f Binary files /dev/null and b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/16px/Arrow-Circle-Down-Left-16.imageset/Arrow-Circle-Down-Left-16.pdf differ diff --git a/DuckDuckGo/Assets.xcassets/DesignSystemIcons/16px/Arrow-Circle-Down-Left-16.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/16px/Arrow-Circle-Down-Left-16.imageset/Contents.json new file mode 100644 index 0000000000..d849acb219 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/16px/Arrow-Circle-Down-Left-16.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Arrow-Circle-Down-Left-16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/DuckDuckGo/Assets.xcassets/DesignSystemIcons/16px/Arrow-Circle-Up-Left-16.imageset/Arrow-Circle-Up-Left-16.pdf b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/16px/Arrow-Circle-Up-Left-16.imageset/Arrow-Circle-Up-Left-16.pdf new file mode 100644 index 0000000000..c7a653f1ba Binary files /dev/null and b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/16px/Arrow-Circle-Up-Left-16.imageset/Arrow-Circle-Up-Left-16.pdf differ diff --git a/DuckDuckGo/Assets.xcassets/DesignSystemIcons/16px/Arrow-Circle-Up-Left-16.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/16px/Arrow-Circle-Up-Left-16.imageset/Contents.json new file mode 100644 index 0000000000..07186f4673 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/16px/Arrow-Circle-Up-Left-16.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Arrow-Circle-Up-Left-16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/DuckDuckGo/AutoClear.swift b/DuckDuckGo/AutoClear.swift index 10acc03518..5f507f809a 100644 --- a/DuckDuckGo/AutoClear.swift +++ b/DuckDuckGo/AutoClear.swift @@ -19,56 +19,58 @@ import Foundation import UIKit +import Core protocol AutoClearWorker { - + func clearNavigationStack() func forgetData() async + func forgetData(applicationState: DataStoreWarmup.ApplicationState) async func forgetTabs() func clearDataFinished(_: AutoClear) } class AutoClear { - + private let worker: AutoClearWorker private var timestamp: TimeInterval? - + private let appSettings: AppSettings var isClearingEnabled: Bool { return AutoClearSettingsModel(settings: appSettings) != nil } - + init(worker: AutoClearWorker, appSettings: AppSettings = AppDependencyProvider.shared.appSettings) { self.worker = worker self.appSettings = appSettings } - + @MainActor - func clearDataIfEnabled(launching: Bool = false) async { + func clearDataIfEnabled(launching: Bool = false, applicationState: DataStoreWarmup.ApplicationState = .unknown) async { guard let settings = AutoClearSettingsModel(settings: appSettings) else { return } - + if settings.action.contains(.clearTabs) { worker.forgetTabs() } if settings.action.contains(.clearData) { - await worker.forgetData() + await worker.forgetData(applicationState: applicationState) } if !launching { worker.clearDataFinished(self) } } - + /// Note: function is parametrised because of tests. func startClearingTimer(_ time: TimeInterval = Date().timeIntervalSince1970) { timestamp = time } - + private func shouldClearData(elapsedTime: TimeInterval) -> Bool { guard let settings = AutoClearSettingsModel(settings: appSettings) else { return false } - + switch settings.timing { case .termination: return false @@ -82,15 +84,16 @@ class AutoClear { return elapsedTime > 60 * 60 } } - + @MainActor - func clearDataIfEnabledAndTimeExpired(baseTimeInterval: TimeInterval = Date().timeIntervalSince1970) async { + func clearDataIfEnabledAndTimeExpired(baseTimeInterval: TimeInterval = Date().timeIntervalSince1970, + applicationState: DataStoreWarmup.ApplicationState) async { guard isClearingEnabled, let timestamp = timestamp, shouldClearData(elapsedTime: baseTimeInterval - timestamp) else { return } self.timestamp = nil worker.clearNavigationStack() - await clearDataIfEnabled() + await clearDataIfEnabled(applicationState: applicationState) } } diff --git a/DuckDuckGo/AutocompleteSuggestionsModel.swift b/DuckDuckGo/AutocompleteSuggestionsModel.swift new file mode 100644 index 0000000000..cdbfe9d02f --- /dev/null +++ b/DuckDuckGo/AutocompleteSuggestionsModel.swift @@ -0,0 +1,129 @@ +// +// AutocompleteSuggestionsModel.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Suggestions + +struct AutocompleteSuggestionsModel { + + private let suggestions: [Suggestion] + private let sectionedSuggestions: [[IndexedSuggestion]] + + var isEmpty: Bool { suggestions.isEmpty } + var count: Int { suggestions.count } + + var numberOfSections: Int { sectionedSuggestions.count } + + init(suggestionsResult: SuggestionResult) { + self.suggestions = suggestionsResult.all + + let sectionsForDisplay = Self.makeSectionsForDisplay(using: suggestionsResult) + sectionedSuggestions = sectionsForDisplay + } + + func indexAfter(_ index: Int) -> Int { + (index + 1 >= count) ? 0 : index + 1 + } + + func indexBefore(_ index: Int) -> Int { + (index - 1 < 0) ? count - 1 : index - 1 + } + + func numberOfRows(in section: Int) -> Int { + guard sectionedSuggestions.indices.contains(section) else { return 0 } + + return sectionedSuggestions[section].count + } + + func suggestion(for index: Int) -> Suggestion? { + guard suggestions.indices.contains(index) else { return nil } + return suggestions[index] + } + + func index(for indexPath: IndexPath) -> Int? { + indexedSuggestion(for: indexPath)?.index + } + + func suggestion(for indexPath: IndexPath) -> Suggestion? { + indexedSuggestion(for: indexPath)?.suggestion + } + + func indexPath(for itemIndex: Int) -> IndexPath? { + guard suggestions.indices.contains(itemIndex) else { return nil } + + var section: Int = 0 + var row: Int = 0 + var currentIndex = itemIndex + + while true { + let currentSectionCount = sectionedSuggestions[section].count + if currentSectionCount > currentIndex { + row = currentIndex + break + } else { + currentIndex -= currentSectionCount + section += 1 + } + } + + return IndexPath(row: row, section: section) + } + + private func indexedSuggestion(for indexPath: IndexPath) -> IndexedSuggestion? { + guard sectionedSuggestions.indices.contains(indexPath.section), + sectionedSuggestions[indexPath.section].indices.contains(indexPath.row) else { + return nil + } + + return sectionedSuggestions[indexPath.section][indexPath.row] + } +} + +private extension AutocompleteSuggestionsModel { + static func makeSectionsForDisplay(using suggestionResult: SuggestionResult) -> [[IndexedSuggestion]] { + var index = -1 + var topResults = [IndexedSuggestion]() + var remoteSuggestions = [IndexedSuggestion]() + var auxResults = [IndexedSuggestion]() + + topResults = suggestionResult.topHits.map { + index += 1 + return IndexedSuggestion(index: index, suggestion: $0) + } + + remoteSuggestions = suggestionResult.duckduckgoSuggestions.map { + index += 1 + return IndexedSuggestion(index: index, suggestion: $0) + } + + auxResults = suggestionResult.localSuggestions.map { + index += 1 + return IndexedSuggestion(index: index, suggestion: $0) + } + + let results = [topResults, remoteSuggestions, auxResults] + + return results.filter { !$0.isEmpty } + } +} + +private struct IndexedSuggestion { + let index: Int + let suggestion: Suggestion +} diff --git a/DuckDuckGo/AutocompleteView.swift b/DuckDuckGo/AutocompleteView.swift new file mode 100644 index 0000000000..f014e31e91 --- /dev/null +++ b/DuckDuckGo/AutocompleteView.swift @@ -0,0 +1,302 @@ +// +// AutocompleteView.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftUI + +struct AutocompleteView: View { + + @ObservedObject var model: AutocompleteViewModel + + var body: some View { + List { + if model.isMessageVisible { + HistoryMessageView { + model.onDismissMessage() + } + .listRowBackground(Color(designSystemColor: .surface)) + .onAppear { + model.onShownToUser() + } + } + + SuggestionsSection(suggestions: model.topHits, + query: model.query, + onSuggestionSelected: model.onSuggestionSelected) + + SuggestionsSection(suggestions: model.ddgSuggestions, + query: model.query, + onSuggestionSelected: model.onSuggestionSelected) + + SuggestionsSection(suggestions: model.localResults, + query: model.query, + onSuggestionSelected: model.onSuggestionSelected) + + } + .offset(x: 0, y: -20) + .padding(.bottom, -20) + .modifier(HideScrollContentBackground()) + .background(Color(designSystemColor: .background)) + .modifier(CompactSectionSpacing()) + .modifier(DisableSelection()) + .modifier(DismissKeyboardOnSwipe()) + .environmentObject(model) + } + +} + +private struct HistoryMessageView: View { + + var onDismiss: () -> Void + + var body: some View { + ZStack(alignment: .topTrailing) { + Button { + onDismiss() + } label: { + Image("Close-24") + .foregroundColor(.primary) + } + .padding(.top, 4) + .buttonStyle(.plain) + + VStack { + Image("RemoteMessageAnnouncement") + .padding(8) + + Text(UserText.autocompleteHistoryWarningTitle) + .multilineTextAlignment(.center) + .daxHeadline() + .padding(2) + + Text(UserText.autocompleteHistoryWarningDescription) + .multilineTextAlignment(.center) + .daxFootnoteRegular() + .frame(maxWidth: 536) + } + .frame(maxWidth: .infinity) + } + .padding(.bottom, 8) + .frame(maxWidth: .infinity) + } + +} + +private struct DismissKeyboardOnSwipe: ViewModifier { + + func body(content: Content) -> some View { + if #available(iOS 16, *) { + content.scrollDismissesKeyboard(.immediately) + } else { + content + } + } + +} + +private struct DisableSelection: ViewModifier { + + func body(content: Content) -> some View { + if #available(iOS 17, *) { + content.selectionDisabled() + } else { + content + } + } + +} + +private struct CompactSectionSpacing: ViewModifier { + + func body(content: Content) -> some View { + if #available(iOS 17, *) { + content.listSectionSpacing(.compact) + } else { + content + } + } + +} + +private struct HideScrollContentBackground: ViewModifier { + func body(content: Content) -> some View { + if #available(iOS 16, *) { + content.scrollContentBackground(.hidden) + } else { + content + } + } +} + +private struct SuggestionsSection: View { + + @EnvironmentObject var autocompleteViewModel: AutocompleteViewModel + + let suggestions: [AutocompleteViewModel.SuggestionModel] + let query: String? + var onSuggestionSelected: (AutocompleteViewModel.SuggestionModel) -> Void + + let selectedColor = Color(designSystemColor: .accent) + let unselectedColor = Color(designSystemColor: .surface) + + var body: some View { + Section { + ForEach(suggestions.indices, id: \.self) { index in + Button { + onSuggestionSelected(suggestions[index]) + } label: { + SuggestionView(model: suggestions[index], query: query) + } + .listRowBackground(autocompleteViewModel.selection == suggestions[index] ? selectedColor : unselectedColor) + } + } + } + +} + +private struct SuggestionView: View { + + @EnvironmentObject var autocompleteModel: AutocompleteViewModel + + let model: AutocompleteViewModel.SuggestionModel + let query: String? + + var tapAheadImage: Image? { + guard model.canShowTapAhead else { return nil } + return Image(autocompleteModel.isAddressBarAtBottom ? + "Arrow-Circle-Down-Left-16" : "Arrow-Circle-Up-Left-16") + } + + var body: some View { + Group { + + switch model.suggestion { + case .phrase(let phrase): + SuggestionListItem(icon: Image("Find-Search-24"), + title: phrase, + query: query, + indicator: tapAheadImage) { + autocompleteModel.onTapAhead(model) + } + + case .website(let url): + SuggestionListItem(icon: Image("Globe-24"), + title: url.formattedForSuggestion()) + + case .bookmark(let title, let url, let isFavorite, _): + SuggestionListItem(icon: Image(isFavorite ? "Bookmark-Fav-24" :"Bookmark-24"), + title: title, + subtitle: url.formattedForSuggestion()) + + case .historyEntry(let title, let url, _): + SuggestionListItem(icon: Image("History-24"), + title: title ?? "", + subtitle: url.isDuckDuckGoSearch ? UserText.autocompleteSearchDuckDuckGo : url.formattedForSuggestion()) + + case .internalPage, .unknown: + FailedAssertionView("Unknown or unsupported suggestion type") + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } + +} + +private struct SuggestionListItem: View { + + let icon: Image + let title: String + let subtitle: String? + let query: String? + let indicator: Image? + let onTapIndicator: (() -> Void)? + + init(icon: Image, + title: String, + subtitle: String? = nil, + query: String? = nil, + indicator: Image? = nil, + onTapIndicator: ( () -> Void)? = nil) { + + self.icon = icon + self.title = title + self.subtitle = subtitle + self.query = query + self.indicator = indicator + self.onTapIndicator = onTapIndicator + } + + var body: some View { + + HStack { + icon + .resizable() + .frame(width: 24, height: 24) + .tintIfAvailable(Color(designSystemColor: .icons)) + + VStack(alignment: .leading, spacing: 2) { + + Group { + // Can't use dax modifiers because they are not typed for Text + if let query, title.hasPrefix(query) { + Text(query) + .font(Font(uiFont: UIFont.daxBodyRegular())) + .foregroundColor(Color(designSystemColor: .textPrimary)) + + + Text(title.dropping(prefix: query)) + .font(Font(uiFont: UIFont.daxBodyBold())) + .foregroundColor(Color(designSystemColor: .textPrimary)) + } else { + Text(title) + .font(Font(uiFont: UIFont.daxBodyRegular())) + .foregroundColor(Color(designSystemColor: .textPrimary)) + } + } + .lineLimit(1) + + if let subtitle { + Text(subtitle) + .daxFootnoteRegular() + .foregroundColor(Color(designSystemColor: .textSecondary)) + .lineLimit(1) + } + } + + if let indicator { + Spacer() + indicator + .highPriorityGesture(TapGesture().onEnded { + onTapIndicator?() + }) + .tintIfAvailable(Color.secondary) + } + } + } +} + +private extension URL { + + func formattedForSuggestion() -> String { + let string = absoluteString + .dropping(prefix: "https://") + .dropping(prefix: "http://") + return pathComponents.isEmpty ? string : string.dropping(suffix: "/") + } + +} diff --git a/DuckDuckGo/AutocompleteViewController.swift b/DuckDuckGo/AutocompleteViewController.swift index b6dc5abe8e..a2b417562f 100644 --- a/DuckDuckGo/AutocompleteViewController.swift +++ b/DuckDuckGo/AutocompleteViewController.swift @@ -28,154 +28,126 @@ import Persistence import History import Combine import BrowserServicesKit +import SwiftUI -class AutocompleteViewController: UIViewController { - +class AutocompleteViewController: UIHostingController { + + private static let debounceDelayMS = 100 private static let session = URLSession(configuration: .ephemeral) - struct Constants { - static let debounceDelay = 100 // millis - static let minItems = 1 - } + var selectedSuggestion: Suggestion? weak var delegate: AutocompleteViewControllerDelegate? weak var presentationDelegate: AutocompleteViewControllerPresentationDelegate? + private let historyManager: HistoryManager + var historyCoordinator: HistoryCoordinating { + historyManager.historyCoordinator + } + + private let bookmarksDatabase: CoreDataDatabase + private let appSettings: AppSettings + private let model: AutocompleteViewModel + private var task: URLSessionDataTask? - private var loader: SuggestionLoading? - private var receivedResponse = false - private var pendingRequest = false - - @Published fileprivate var query = "" - fileprivate var queryDebounceCancellable: AnyCancellable? - fileprivate var suggestions = [Suggestion]() - fileprivate var selectedItem = -1 - - private var historyCoordinator: HistoryCoordinating! - private var bookmarksDatabase: CoreDataDatabase! - private var appSettings: AppSettings! - private var variantManager: VariantManager! + @Published private var query = "" + private var queryDebounceCancellable: AnyCancellable? private lazy var cachedBookmarks: CachedBookmarks = { CachedBookmarks(bookmarksDatabase) }() - private var bookmarksStringSearch: BookmarksStringSearch! - - var backgroundColor: UIColor { - appSettings.currentAddressBarPosition.isBottom ? - UIColor(designSystemColor: .background) : - UIColor.black.withAlphaComponent(0.2) - } + private var lastResults: SuggestionResult? + private var loader: SuggestionLoader? - var showBackground = true { - didSet { - view.backgroundColor = showBackground ? backgroundColor : UIColor.clear - } - } + private var historyMessageManager: HistoryMessageManager - var selectedSuggestion: Suggestion? { - let state = (suggestions: self.suggestions, selectedIndex: self.selectedItem) - return state.suggestions.indices.contains(state.selectedIndex) ? state.suggestions[state.selectedIndex] : nil + init(historyManager: HistoryManager, + bookmarksDatabase: CoreDataDatabase, + appSettings: AppSettings, + historyMessageManager: HistoryMessageManager = HistoryMessageManager()) { + self.historyManager = historyManager + self.bookmarksDatabase = bookmarksDatabase + self.appSettings = appSettings + self.historyMessageManager = historyMessageManager + self.model = AutocompleteViewModel(isAddressBarAtBottom: appSettings.currentAddressBarPosition == .bottom, + showMessage: historyManager.isHistoryFeatureEnabled() && historyMessageManager.shouldShow()) + super.init(rootView: AutocompleteView(model: model)) + self.model.delegate = self } - - private var hidesBarsOnSwipeDefault = true - - @IBOutlet weak var tableView: UITableView! - var shouldOffsetY = false - static func loadFromStoryboard(bookmarksDatabase: CoreDataDatabase, - bookmarksStringSearch: BookmarksStringSearch, - historyCoordinator: HistoryCoordinating, - appSettings: AppSettings = AppDependencyProvider.shared.appSettings, - variantManager: VariantManager = DefaultVariantManager()) -> AutocompleteViewController { - let storyboard = UIStoryboard(name: "Autocomplete", bundle: nil) - guard let controller = storyboard.instantiateInitialViewController() as? AutocompleteViewController else { - fatalError("Failed to instatiate correct Autocomplete view controller") - } - controller.bookmarksDatabase = bookmarksDatabase - controller.bookmarksStringSearch = bookmarksStringSearch - controller.historyCoordinator = historyCoordinator - controller.appSettings = appSettings - controller.variantManager = variantManager - return controller + @MainActor required dynamic init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() - configureTableView() - decorate() + + view.backgroundColor = UIColor(designSystemColor: .background) queryDebounceCancellable = $query - .debounce(for: .milliseconds(Constants.debounceDelay), scheduler: RunLoop.main) + .debounce(for: .milliseconds(Self.debounceDelayMS), scheduler: RunLoop.main) .sink { [weak self] query in self?.requestSuggestions(query: query) } } - - private func configureTableView() { - tableView.backgroundColor = UIColor.clear - tableView.tableFooterView = UIView() - tableView.sectionFooterHeight = 1.0 / UIScreen.main.scale - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - adjustForInCall() - configureNavigationBar() - } - - // If auto complete is used after the in-call banner is shown it has the wrong y position (should be zero) - private func adjustForInCall() { - let frame = self.view.frame - self.view.frame = CGRect(x: 0, y: shouldOffsetY ? 45.5 : 0, width: frame.width, height: frame.height) - } - - private func configureNavigationBar() { - hidesBarsOnSwipeDefault = navigationController?.hidesBarsOnSwipe ?? hidesBarsOnSwipeDefault - navigationController?.hidesBarsOnSwipe = false - } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - resetNavigationBar() + historyMessageManager.incrementDisplayCount() + fireUsagePixels() } - private func resetNavigationBar() { - navigationController?.hidesBarsOnSwipe = hidesBarsOnSwipeDefault + func keyboardMoveSelectionDown() { + model.nextSelection() } - func updateQuery(query: String) { - selectedItem = -1 - cancelInFlightRequests() - self.query = query + func keyboardMoveSelectionUp() { + model.previousSelection() } - func willDismiss(with query: String) { - guard suggestions.indices.contains(selectedItem) else { return } - let suggestion = suggestions[selectedItem] - firePixelForSelectedSuggestion(suggestion) + func updateQuery(_ query: String) { + model.selection = nil + guard self.query != query else { return } + cancelInFlightRequests() + self.query = query + model.query = query } - private func firePixelForSelectedSuggestion(_ suggestion: Suggestion) { - switch suggestion { - case .phrase: - Pixel.fire(pixel: .autocompleteClickPhrase) - case .website: - Pixel.fire(pixel: .autocompleteClickWebsite) - case .bookmark(_, _, isFavorite: let isFavorite, _): - Pixel.fire(pixel: isFavorite ? .autocompleteClickFavorite : .autocompleteClickBookmark) - case .historyEntry: - Pixel.fire(pixel: .autocompleteClickHistory) - case .unknown(value: let value), .internalPage(title: let value, url: _): - assertionFailure("Unknown suggestion \(value)") + private func fireUsagePixels() { + var bookmark = false + var favorite = false + var history = false + + lastResults?.all.forEach { + switch $0 { + case .bookmark(_, _, isFavorite: let isFavorite, _): + if isFavorite { + favorite = true + } else { + bookmark = true + } + + case .historyEntry: + history = true + + default: break + } + } + + if bookmark { + Pixel.fire(pixel: .autocompleteDisplayedLocalBookmark) + } + + if favorite { + Pixel.fire(pixel: .autocompleteDisplayedLocalFavorite) + } + + if history { + Pixel.fire(pixel: .autocompleteDisplayedLocalHistory) } - } - @IBAction func onPlusButtonPressed(_ button: UIButton) { - let suggestion = suggestions[button.tag] - delegate?.autocomplete(pressedPlusButtonForSuggestion: suggestion) } private func cancelInFlightRequests() { @@ -184,18 +156,7 @@ class AutocompleteViewController: UIViewController { } private func requestSuggestions(query: String) { - selectedItem = -1 - tableView.reloadData() - - let bookmarks: [Suggestion] - - if variantManager.inSuggestionExperiment { - bookmarks = [] // We'll supply bookmarks elsewhere - } else { - bookmarks = bookmarksStringSearch.search(query: query).prefix(2).map { - .bookmark(title: $0.title, url: $0.url, isFavorite: $0.isFavorite, allowedInTopHits: true) - } - } + model.selection = nil loader = SuggestionLoader(dataSource: self, urlFactory: { phrase in guard let url = URL(trimmedAddressBarString: phrase), @@ -207,160 +168,90 @@ class AutocompleteViewController: UIViewController { return url }) - pendingRequest = true loader?.getSuggestions(query: query) { [weak self] result, error in - defer { - self?.pendingRequest = false - } - guard error == nil else { return } - - let remoteResults = result?.all ?? [] - - self?.updateSuggestions(bookmarks + remoteResults) + guard let self, error == nil else { return } + let updatedResults = result ?? .empty + self.lastResults = updatedResults + model.updateSuggestions(updatedResults) + updateHeight() } - } - - private func updateSuggestions(_ newSuggestions: [Suggestion]) { - receivedResponse = true - suggestions = newSuggestions - tableView.contentOffset = .zero - tableView.reloadData() - presentationDelegate?.autocompleteDidChangeContentHeight(height: tableView.contentSize.height) - } - - @IBAction func onAutocompleteDismissed(_ sender: Any) { - Pixel.fire(pixel: .addressBarGestureDismiss) - delegate?.autocompleteWasDismissed() - } -} -extension AutocompleteViewController: UITableViewDataSource { - - public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - if suggestions.isEmpty { - return noSuggestionsCell(forIndexPath: indexPath) - } - return suggestionsCell(forIndexPath: indexPath) } - private func suggestionsCell(forIndexPath indexPath: IndexPath) -> UITableViewCell { - let type = SuggestionTableViewCell.reuseIdentifier - guard let cell = tableView.dequeueReusableCell(withIdentifier: type, for: indexPath) as? SuggestionTableViewCell else { - fatalError("Failed to dequeue \(type) as SuggestionTableViewCell") - } - - let currentTheme = ThemeManager.shared.currentTheme - - cell.updateFor(query: query, - suggestion: suggestions[indexPath.row], - with: currentTheme, - isAddressBarAtBottom: appSettings.currentAddressBarPosition.isBottom) - cell.plusButton.tag = indexPath.row - - let baseBackgroundColor = isPad ? UIColor(designSystemColor: .panel) : UIColor(designSystemColor: .background) - let backgroundColor = indexPath.row == selectedItem ? currentTheme.tableCellSelectedColor : baseBackgroundColor - - cell.backgroundColor = backgroundColor - cell.tintColor = currentTheme.autocompleteCellAccessoryColor - - return cell - } + private func updateHeight() { + guard let lastResults else { return } - private func noSuggestionsCell(forIndexPath indexPath: IndexPath) -> UITableViewCell { - let type = NoSuggestionsTableViewCell.reuseIdentifier - guard let cell = tableView.dequeueReusableCell(withIdentifier: type, for: indexPath) as? NoSuggestionsTableViewCell else { - fatalError("Failed to dequeue \(type) as NoSuggestionTableViewCell") - } - - let currentTheme = ThemeManager.shared.currentTheme - cell.backgroundColor = appSettings.currentAddressBarPosition.isBottom ? - UIColor(designSystemColor: .background) : - UIColor(designSystemColor: .panel) + let messageHeight = model.isMessageVisible ? 196 : 0 + let cellHeight = 44 + let sectionPadding = 10 + let controllerPadding = 20 - cell.tintColor = currentTheme.autocompleteCellAccessoryColor - cell.label?.textColor = currentTheme.tableCellTextColor + let height = + (lastResults.topHits.count * cellHeight) + + (lastResults.topHits.isEmpty ? 0 : sectionPadding) + + (lastResults.duckduckgoSuggestions.count * cellHeight) + + (lastResults.duckduckgoSuggestions.isEmpty ? 0 : sectionPadding) + + (lastResults.localSuggestions.count * cellHeight) + + (lastResults.localSuggestions.isEmpty ? 0 : sectionPadding) + + messageHeight + + controllerPadding - return cell + presentationDelegate? + .autocompleteDidChangeContentHeight(height: CGFloat(height)) } - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - if appSettings.currentAddressBarPosition.isBottom && suggestions.isEmpty { - return view.frame.height - } +} - let defaultHeight: CGFloat = 46 - guard suggestions.indices.contains(indexPath.row) else { return defaultHeight } +extension AutocompleteViewController: AutocompleteViewModelDelegate { - switch suggestions[indexPath.row] { - case .bookmark, .historyEntry: - return 60 - default: - return defaultHeight - } + func onMessageDismissed() { + historyMessageManager.dismissedByUser() + updateHeight() } - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return receivedResponse ? max(Constants.minItems, suggestions.count) : 0 + func onMessageShown() { + historyMessageManager.shownToUser() } -} + func onSuggestionSelected(_ suggestion: Suggestion) { + switch suggestion { + case .bookmark(_, _, let isFavorite, _): + Pixel.fire(pixel: isFavorite ? .autocompleteClickFavorite : .autocompleteClickBookmark) -extension AutocompleteViewController: UITableViewDelegate { - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let suggestion = suggestions[indexPath.row] - delegate?.autocomplete(selectedSuggestion: suggestion) - firePixelForSelectedSuggestion(suggestion) - } -} + case .historyEntry(_, let url, _): + Pixel.fire(pixel: url.isDuckDuckGoSearch ? .autocompleteClickSearchHistory : .autocompleteClickSiteHistory) -extension AutocompleteViewController: UIGestureRecognizerDelegate { - func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { - return tableView == touch.view - } -} + case .phrase: + Pixel.fire(pixel: .autocompleteClickPhrase) -extension AutocompleteViewController { - private func decorate() { - let theme = ThemeManager.shared.currentTheme - tableView.separatorColor = theme.tableCellSeparatorColor - } -} + case .website: + Pixel.fire(pixel: .autocompleteClickWebsite) -extension AutocompleteViewController { - - func keyboardMoveSelectionDown() { - guard !pendingRequest, !suggestions.isEmpty else { return } - selectedItem = (selectedItem + 1 >= itemCount()) ? 0 : selectedItem + 1 - delegate?.autocomplete(highlighted: suggestions[selectedItem], for: query) - tableView.reloadData() + default: + // NO-OP + break + } + self.delegate?.autocomplete(selectedSuggestion: suggestion) } - func keyboardMoveSelectionUp() { - guard !pendingRequest, !suggestions.isEmpty else { return } - selectedItem = (selectedItem - 1 < 0) ? itemCount() - 1 : selectedItem - 1 - delegate?.autocomplete(highlighted: suggestions[selectedItem], for: query) - tableView.reloadData() - } - - func keyboardEscape() { - delegate?.autocompleteWasDismissed() - } - - private func itemCount() -> Int { - return suggestions.count + func onTapAhead(_ suggestion: Suggestion) { + self.delegate?.autocomplete(pressedPlusButtonForSuggestion: suggestion) } + func onSuggestionHighlighted(_ suggestion: Suggestion, forQuery query: String) { + self.delegate?.autocomplete(highlighted: suggestion, for: query) + } } extension AutocompleteViewController: SuggestionLoadingDataSource { func history(for suggestionLoading: Suggestions.SuggestionLoading) -> [HistorySuggestion] { - return variantManager.inSuggestionExperiment ? (historyCoordinator.history ?? []) : [] + return historyCoordinator.history ?? [] } func bookmarks(for suggestionLoading: Suggestions.SuggestionLoading) -> [Suggestions.Bookmark] { - return variantManager.inSuggestionExperiment ? cachedBookmarks.all : [] + return cachedBookmarks.all } func internalPages(for suggestionLoading: Suggestions.SuggestionLoading) -> [Suggestions.InternalPage] { @@ -383,6 +274,10 @@ extension AutocompleteViewController: SuggestionLoadingDataSource { } +private extension SuggestionResult { + static let empty = SuggestionResult(topHits: [], duckduckgoSuggestions: [], localSuggestions: []) +} + extension HistoryEntry: HistorySuggestion { public var numberOfVisits: Int { @@ -390,11 +285,3 @@ extension HistoryEntry: HistorySuggestion { } } - -extension VariantManager { - - var inSuggestionExperiment: Bool { - isSupported(feature: .newSuggestionLogic) || isSupported(feature: .history) - } - -} diff --git a/DuckDuckGo/AutocompleteViewControllerDelegate.swift b/DuckDuckGo/AutocompleteViewControllerDelegate.swift index e7f3671328..8a7f631eb4 100644 --- a/DuckDuckGo/AutocompleteViewControllerDelegate.swift +++ b/DuckDuckGo/AutocompleteViewControllerDelegate.swift @@ -22,6 +22,8 @@ import Suggestions protocol AutocompleteViewControllerDelegate: AnyObject { + func autocompleteDidEndWithUserQuery() + func autocomplete(selectedSuggestion suggestion: Suggestion) func autocomplete(highlighted suggestion: Suggestion, for query: String) diff --git a/DuckDuckGo/AutocompleteViewModel.swift b/DuckDuckGo/AutocompleteViewModel.swift new file mode 100644 index 0000000000..f5b4a1fc77 --- /dev/null +++ b/DuckDuckGo/AutocompleteViewModel.swift @@ -0,0 +1,129 @@ +// +// AutocompleteViewModel.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Core +import Suggestions +import SwiftUI + +protocol AutocompleteViewModelDelegate: NSObjectProtocol { + + func onSuggestionSelected(_ suggestion: Suggestion) + func onSuggestionHighlighted(_ suggestion: Suggestion, forQuery query: String) + func onTapAhead(_ suggestion: Suggestion) + func onMessageDismissed() + func onMessageShown() + +} + +class AutocompleteViewModel: ObservableObject { + + @Published var selection: SuggestionModel? { + didSet { + if let selection { + delegate?.onSuggestionHighlighted(selection.suggestion, + forQuery: query ?? "") + } + } + } + @Published var topHits = [SuggestionModel]() + @Published var ddgSuggestions = [SuggestionModel]() + @Published var localResults = [SuggestionModel]() + @Published var query: String? + @Published var isMessageVisible = true + @Published var emptySuggestion: [SuggestionModel]? + + weak var delegate: AutocompleteViewModelDelegate? + + let isAddressBarAtBottom: Bool + + init(isAddressBarAtBottom: Bool, showMessage: Bool) { + self.isAddressBarAtBottom = isAddressBarAtBottom + self.isMessageVisible = showMessage + } + + func updateSuggestions(_ suggestions: SuggestionResult) { + topHits = suggestions.topHits.map { SuggestionModel(suggestion: $0) } + ddgSuggestions = suggestions.duckduckgoSuggestions.map { SuggestionModel(suggestion: $0) } + localResults = suggestions.localSuggestions.map { SuggestionModel(suggestion: $0) } + if topHits.isEmpty && ddgSuggestions.isEmpty && localResults.isEmpty { + topHits = [SuggestionModel(suggestion: .phrase(phrase: query ?? ""), canShowTapAhead: false)] + } + } + + func onDismissMessage() { + withAnimation { + isMessageVisible = false + delegate?.onMessageDismissed() + } + } + + func onShownToUser() { + delegate?.onMessageShown() + } + + func onSuggestionSelected(_ model: SuggestionModel) { + delegate?.onSuggestionSelected(model.suggestion) + } + + func onTapAhead(_ model: SuggestionModel) { + delegate?.onTapAhead(model.suggestion) + } + + func nextSelection() { + let all = topHits + ddgSuggestions + localResults + guard let selection else { + selection = all.first + return + } + + guard let index = all.firstIndex(of: selection) else { + return + } + + let nextIndex = index + 1 + if all.indices.contains(nextIndex) { + self.selection = all[nextIndex] + } + } + + func previousSelection() { + guard let selection else { return } + let all = topHits + ddgSuggestions + localResults + + guard let index = all.firstIndex(of: selection) else { + return + } + + let nextIndex = index - 1 + if all.indices.contains(nextIndex) { + self.selection = all[nextIndex] + } + } + + struct SuggestionModel: Identifiable, Equatable { + let id = UUID() + let suggestion: Suggestion + var canShowTapAhead = true + + static func == (lhs: Self, rhs: Self) -> Bool { + lhs.id == rhs.id + } + } + +} diff --git a/DuckDuckGo/Autoconsent/autoconsent-bundle.js b/DuckDuckGo/Autoconsent/autoconsent-bundle.js index fec748df9f..a9db83772e 100644 --- a/DuckDuckGo/Autoconsent/autoconsent-bundle.js +++ b/DuckDuckGo/Autoconsent/autoconsent-bundle.js @@ -1 +1 @@ -!function(){"use strict";var e=class e{static setBase(t){e.base=t}static findElement(t,o=null,c=!1){let i=null;return i=null!=o?Array.from(o.querySelectorAll(t.selector)):null!=e.base?Array.from(e.base.querySelectorAll(t.selector)):Array.from(document.querySelectorAll(t.selector)),null!=t.textFilter&&(i=i.filter((e=>{const o=e.textContent.toLowerCase();if(Array.isArray(t.textFilter)){let e=!1;for(const c of t.textFilter)if(-1!==o.indexOf(c.toLowerCase())){e=!0;break}return e}if(null!=t.textFilter)return-1!==o.indexOf(t.textFilter.toLowerCase())}))),null!=t.styleFilters&&(i=i.filter((e=>{const o=window.getComputedStyle(e);let c=!0;for(const e of t.styleFilters){const t=o[e.option];c=e.negated?c&&t!==e.value:c&&t===e.value}return c}))),null!=t.displayFilter&&(i=i.filter((e=>t.displayFilter?0!==e.offsetHeight:0===e.offsetHeight))),null!=t.iframeFilter&&(i=i.filter((()=>t.iframeFilter?window.location!==window.parent.location:window.location===window.parent.location))),null!=t.childFilter&&(i=i.filter((o=>{const c=e.base;e.setBase(o);const i=e.find(t.childFilter);return e.setBase(c),null!=i.target}))),c?i:(i.length>1&&console.warn("Multiple possible targets: ",i,t,o),i[0])}static find(t,o=!1){const c=[];if(null!=t.parent){const i=e.findElement(t.parent,null,o);if(null!=i){if(i instanceof Array)return i.forEach((i=>{const n=e.findElement(t.target,i,o);n instanceof Array?n.forEach((e=>{c.push({parent:i,target:e})})):c.push({parent:i,target:n})})),c;{const n=e.findElement(t.target,i,o);n instanceof Array?n.forEach((e=>{c.push({parent:i,target:e})})):c.push({parent:i,target:n})}}}else{const i=e.findElement(t.target,null,o);i instanceof Array?i.forEach((e=>{c.push({parent:null,target:e})})):c.push({parent:null,target:i})}return 0===c.length&&c.push({parent:null,target:null}),o?c:(1!==c.length&&console.warn("Multiple results found, even though multiple false",c),c[0])}};e.base=null;var t=e;function o(e){const o=t.find(e);return"css"===e.type?!!o.target:"checkbox"===e.type?!!o.target&&o.target.checked:void 0}async function c(e,a){switch(e.type){case"click":return async function(e){const o=t.find(e);null!=o.target&&o.target.click();return n(i)}(e);case"list":return async function(e,t){for(const o of e.actions)await c(o,t)}(e,a);case"consent":return async function(e,t){for(const i of e.consents){const e=-1!==t.indexOf(i.type);if(i.matcher&&i.toggleAction){o(i.matcher)!==e&&await c(i.toggleAction)}else e?await c(i.trueAction):await c(i.falseAction)}}(e,a);case"ifcss":return async function(e,o){const i=t.find(e);i.target?e.falseAction&&await c(e.falseAction,o):e.trueAction&&await c(e.trueAction,o)}(e,a);case"waitcss":return async function(e){await new Promise((o=>{let c=e.retries||10;const i=e.waitTime||250,n=()=>{const a=t.find(e);(e.negated&&a.target||!e.negated&&!a.target)&&c>0?(c-=1,setTimeout(n,i)):o()};n()}))}(e);case"foreach":return async function(e,o){const i=t.find(e,!0),n=t.base;for(const n of i)n.target&&(t.setBase(n.target),await c(e.action,o));t.setBase(n)}(e,a);case"hide":return async function(e){const o=t.find(e);o.target&&o.target.classList.add("Autoconsent-Hidden")}(e);case"slide":return async function(e){const o=t.find(e),c=t.find(e.dragTarget);if(o.target){const e=o.target.getBoundingClientRect(),t=c.target.getBoundingClientRect();let i=t.top-e.top,n=t.left-e.left;"y"===this.config.axis.toLowerCase()&&(n=0),"x"===this.config.axis.toLowerCase()&&(i=0);const a=window.screenX+e.left+e.width/2,s=window.screenY+e.top+e.height/2,r=e.left+e.width/2,l=e.top+e.height/2,p=document.createEvent("MouseEvents");p.initMouseEvent("mousedown",!0,!0,window,0,a,s,r,l,!1,!1,!1,!1,0,o.target);const d=document.createEvent("MouseEvents");d.initMouseEvent("mousemove",!0,!0,window,0,a+n,s+i,r+n,l+i,!1,!1,!1,!1,0,o.target);const u=document.createEvent("MouseEvents");u.initMouseEvent("mouseup",!0,!0,window,0,a+n,s+i,r+n,l+i,!1,!1,!1,!1,0,o.target),o.target.dispatchEvent(p),await this.waitTimeout(10),o.target.dispatchEvent(d),await this.waitTimeout(10),o.target.dispatchEvent(u)}}(e);case"close":return async function(){window.close()}();case"wait":return async function(e){await n(e.waitTime)}(e);case"eval":return async function(e){return console.log("eval!",e.code),new Promise((t=>{try{e.async?(window.eval(e.code),setTimeout((()=>{t(window.eval("window.__consentCheckResult"))}),e.timeout||250)):t(window.eval(e.code))}catch(o){console.warn("eval error",o,e.code),t(!1)}}))}(e);default:throw"Unknown action type: "+e.type}}var i=0;function n(e){return new Promise((t=>{setTimeout((()=>{t()}),e)}))}function a(){return crypto&&void 0!==crypto.randomUUID?crypto.randomUUID():Math.random().toString()}var s=class{constructor(e,t=1e3){this.id=e,this.promise=new Promise(((e,t)=>{this.resolve=e,this.reject=t})),this.timer=window.setTimeout((()=>{this.reject(new Error("timeout"))}),t)}},r={pending:new Map,sendContentMessage:null};var l={EVAL_0:()=>console.log(1),EVAL_CONSENTMANAGER_1:()=>window.__cmp&&"object"==typeof __cmp("getCMPData"),EVAL_CONSENTMANAGER_2:()=>!__cmp("consentStatus").userChoiceExists,EVAL_CONSENTMANAGER_3:()=>__cmp("setConsent",0),EVAL_CONSENTMANAGER_4:()=>__cmp("setConsent",1),EVAL_CONSENTMANAGER_5:()=>__cmp("consentStatus").userChoiceExists,EVAL_COOKIEBOT_1:()=>!!window.Cookiebot,EVAL_COOKIEBOT_2:()=>!window.Cookiebot.hasResponse&&!0===window.Cookiebot.dialog?.visible,EVAL_COOKIEBOT_3:()=>window.Cookiebot.withdraw()||!0,EVAL_COOKIEBOT_4:()=>window.Cookiebot.hide()||!0,EVAL_COOKIEBOT_5:()=>!0===window.Cookiebot.declined,EVAL_KLARO_1:()=>{const e=globalThis.klaroConfig||globalThis.klaro?.getManager&&globalThis.klaro.getManager().config;if(!e)return!0;const t=(e.services||e.apps).filter((e=>!e.required)).map((e=>e.name));if(klaro&&klaro.getManager){const e=klaro.getManager();return t.every((t=>!e.consents[t]))}if(klaroConfig&&"cookie"===klaroConfig.storageMethod){const e=klaroConfig.cookieName||klaroConfig.storageName,o=JSON.parse(decodeURIComponent(document.cookie.split(";").find((t=>t.trim().startsWith(e))).split("=")[1]));return Object.keys(o).filter((e=>t.includes(e))).every((e=>!1===o[e]))}},EVAL_KLARO_OPEN_POPUP:()=>{klaro.show(void 0,!0)},EVAL_KLARO_TRY_API_OPT_OUT:()=>{if(window.klaro&&"function"==typeof klaro.show&&"function"==typeof klaro.getManager)try{return klaro.getManager().changeAll(!1),klaro.getManager().saveAndApplyConsents(),!0}catch(e){return console.warn(e),!1}return!1},EVAL_ONETRUST_1:()=>window.OnetrustActiveGroups.split(",").filter((e=>e.length>0)).length<=1,EVAL_TRUSTARC_TOP:()=>window&&window.truste&&"0"===window.truste.eu.bindMap.prefCookie,EVAL_ADROLL_0:()=>!document.cookie.includes("__adroll_fpc"),EVAL_ALMACMP_0:()=>document.cookie.includes('"name":"Google","consent":false'),EVAL_AFFINITY_SERIF_COM_0:()=>document.cookie.includes("serif_manage_cookies_viewed")&&!document.cookie.includes("serif_allow_analytics"),EVAL_ARBEITSAGENTUR_TEST:()=>document.cookie.includes("cookie_consent=denied"),EVAL_AXEPTIO_0:()=>document.cookie.includes("axeptio_authorized_vendors=%2C%2C"),EVAL_BAHN_TEST:()=>1===utag.gdpr.getSelectedCategories().length,EVAL_BING_0:()=>document.cookie.includes("AL=0")&&document.cookie.includes("AD=0")&&document.cookie.includes("SM=0"),EVAL_BLOCKSY_0:()=>document.cookie.includes("blocksy_cookies_consent_accepted=no"),EVAL_BORLABS_0:()=>!JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>-1!==e.indexOf("borlabs-cookie"))).split("=",2)[1])).consents.statistics,EVAL_BUNDESREGIERUNG_DE_0:()=>document.cookie.match("cookie-allow-tracking=0"),EVAL_CANVA_0:()=>!document.cookie.includes("gtm_fpc_engagement_event"),EVAL_CC_BANNER2_0:()=>!!document.cookie.match(/sncc=[^;]+D%3Dtrue/),EVAL_CLICKIO_0:()=>document.cookie.includes("__lxG__consent__v2_daisybit="),EVAL_CLINCH_0:()=>document.cookie.includes("ctc_rejected=1"),EVAL_COOKIECONSENT2_TEST:()=>document.cookie.includes("cc_cookie="),EVAL_COOKIECONSENT3_TEST:()=>document.cookie.includes("cc_cookie="),EVAL_COINBASE_0:()=>JSON.parse(decodeURIComponent(document.cookie.match(/cm_(eu|default)_preferences=([0-9a-zA-Z\\{\\}\\[\\]%:]*);?/)[2])).consent.length<=1,EVAL_COMPLIANZ_BANNER_0:()=>document.cookie.includes("cmplz_banner-status=dismissed"),EVAL_COOKIE_LAW_INFO_0:()=>CLI.disableAllCookies()||CLI.reject_close()||!0,EVAL_COOKIE_LAW_INFO_1:()=>-1===document.cookie.indexOf("cookielawinfo-checkbox-non-necessary=yes"),EVAL_COOKIE_LAW_INFO_DETECT:()=>!!window.CLI,EVAL_COOKIE_MANAGER_POPUP_0:()=>!1===JSON.parse(document.cookie.split(";").find((e=>e.trim().startsWith("CookieLevel"))).split("=")[1]).social,EVAL_COOKIEALERT_0:()=>document.querySelector("body").removeAttribute("style")||!0,EVAL_COOKIEALERT_1:()=>document.querySelector("body").removeAttribute("style")||!0,EVAL_COOKIEALERT_2:()=>!0===window.CookieConsent.declined,EVAL_COOKIEFIRST_0:()=>{return!1===(e=JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>-1!==e.indexOf("cookiefirst"))).trim()).split("=")[1])).performance&&!1===e.functional&&!1===e.advertising;var e},EVAL_COOKIEFIRST_1:()=>document.querySelectorAll("button[data-cookiefirst-accent-color=true][role=checkbox]:not([disabled])").forEach((e=>"true"==e.getAttribute("aria-checked")&&e.click()))||!0,EVAL_COOKIEINFORMATION_0:()=>CookieInformation.declineAllCategories()||!0,EVAL_COOKIEINFORMATION_1:()=>CookieInformation.submitAllCategories()||!0,EVAL_COOKIEINFORMATION_2:()=>document.cookie.includes("CookieInformationConsent="),EVAL_COOKIEYES_0:()=>document.cookie.includes("advertisement:no"),EVAL_DAILYMOTION_0:()=>!!document.cookie.match("dm-euconsent-v2"),EVAL_DNDBEYOND_TEST:()=>document.cookie.includes("cookie-consent=denied"),EVAL_DSGVO_0:()=>!document.cookie.includes("sp_dsgvo_cookie_settings"),EVAL_DUNELM_0:()=>document.cookie.includes("cc_functional=0")&&document.cookie.includes("cc_targeting=0"),EVAL_ETSY_0:()=>document.querySelectorAll(".gdpr-overlay-body input").forEach((e=>{e.checked=!1}))||!0,EVAL_ETSY_1:()=>document.querySelector(".gdpr-overlay-view button[data-wt-overlay-close]").click()||!0,EVAL_EU_COOKIE_COMPLIANCE_0:()=>-1===document.cookie.indexOf("cookie-agreed=2"),EVAL_EU_COOKIE_LAW_0:()=>!document.cookie.includes("euCookie"),EVAL_EZOIC_0:()=>ezCMP.handleAcceptAllClick(),EVAL_EZOIC_1:()=>!!document.cookie.match(/ez-consent-tcf/),EVAL_GOOGLE_0:()=>!!document.cookie.match(/SOCS=CAE/),EVAL_HEMA_TEST_0:()=>document.cookie.includes("cookies_rejected=1"),EVAL_IUBENDA_0:()=>document.querySelectorAll(".purposes-item input[type=checkbox]:not([disabled])").forEach((e=>{e.checked&&e.click()}))||!0,EVAL_IUBENDA_1:()=>!!document.cookie.match(/_iub_cs-\d+=/),EVAL_IWINK_TEST:()=>document.cookie.includes("cookie_permission_granted=no"),EVAL_JQUERY_COOKIEBAR_0:()=>!document.cookie.includes("cookies-state=accepted"),EVAL_MEDIAVINE_0:()=>document.querySelectorAll('[data-name="mediavine-gdpr-cmp"] input[type=checkbox]').forEach((e=>e.checked&&e.click()))||!0,EVAL_MICROSOFT_0:()=>Array.from(document.querySelectorAll("div > button")).filter((e=>e.innerText.match("Reject|Ablehnen")))[0].click()||!0,EVAL_MICROSOFT_1:()=>Array.from(document.querySelectorAll("div > button")).filter((e=>e.innerText.match("Accept|Annehmen")))[0].click()||!0,EVAL_MICROSOFT_2:()=>!!document.cookie.match("MSCC|GHCC"),EVAL_MOOVE_0:()=>document.querySelectorAll("#moove_gdpr_cookie_modal input").forEach((e=>{e.disabled||(e.checked="moove_gdpr_strict_cookies"===e.name||"moove_gdpr_strict_cookies"===e.id)}))||!0,EVAL_ONENINETWO_0:()=>document.cookie.includes("CC_ADVERTISING=NO")&&document.cookie.includes("CC_ANALYTICS=NO"),EVAL_OPERA_0:()=>document.cookie.includes("cookie_consent_essential=true")&&!document.cookie.includes("cookie_consent_marketing=true"),EVAL_PAYPAL_0:()=>!0===document.cookie.includes("cookie_prefs"),EVAL_PRIMEBOX_0:()=>!document.cookie.includes("cb-enabled=accepted"),EVAL_PUBTECH_0:()=>document.cookie.includes("euconsent-v2")&&(document.cookie.match(/.YAAAAAAAAAAA/)||document.cookie.match(/.aAAAAAAAAAAA/)||document.cookie.match(/.YAAACFgAAAAA/)),EVAL_REDDIT_0:()=>document.cookie.includes("eu_cookie={%22opted%22:true%2C%22nonessential%22:false}"),EVAL_SIBBO_0:()=>!!window.localStorage.getItem("euconsent-v2"),EVAL_SIRDATA_UNBLOCK_SCROLL:()=>(document.documentElement.classList.forEach((e=>{e.startsWith("sd-cmp-")&&document.documentElement.classList.remove(e)})),!0),EVAL_SNIGEL_0:()=>!!document.cookie.match("snconsent"),EVAL_STEAMPOWERED_0:()=>2===JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>e.trim().startsWith("cookieSettings"))).split("=")[1])).preference_state,EVAL_SVT_TEST:()=>document.cookie.includes('cookie-consent-1={"optedIn":true,"functionality":false,"statistics":false}'),EVAL_TAKEALOT_0:()=>document.body.classList.remove("freeze")||(document.body.style="")||!0,EVAL_TARTEAUCITRON_0:()=>tarteaucitron.userInterface.respondAll(!1)||!0,EVAL_TARTEAUCITRON_1:()=>tarteaucitron.userInterface.respondAll(!0)||!0,EVAL_TARTEAUCITRON_2:()=>document.cookie.match(/tarteaucitron=[^;]*/)?.[0].includes("false"),EVAL_TAUNTON_TEST:()=>document.cookie.includes("taunton_user_consent_submitted=true"),EVAL_TEALIUM_0:()=>void 0!==window.utag&&"object"==typeof utag.gdpr,EVAL_TEALIUM_1:()=>utag.gdpr.setConsentValue(!1)||!0,EVAL_TEALIUM_DONOTSELL:()=>utag.gdpr.dns?.setDnsState(!1)||!0,EVAL_TEALIUM_2:()=>utag.gdpr.setConsentValue(!0)||!0,EVAL_TEALIUM_3:()=>1!==utag.gdpr.getConsentState(),EVAL_TEALIUM_DONOTSELL_CHECK:()=>1!==utag.gdpr.dns?.getDnsState(),EVAL_TESTCMP_0:()=>"button_clicked"===window.results.results[0],EVAL_TESTCMP_COSMETIC_0:()=>"banner_hidden"===window.results.results[0],EVAL_THEFREEDICTIONARY_0:()=>cmpUi.showPurposes()||cmpUi.rejectAll()||!0,EVAL_THEFREEDICTIONARY_1:()=>cmpUi.allowAll()||!0,EVAL_THEVERGE_0:()=>document.cookie.includes("_duet_gdpr_acknowledged=1"),EVAL_UBUNTU_COM_0:()=>document.cookie.includes("_cookies_accepted=essential"),EVAL_UK_COOKIE_CONSENT_0:()=>!document.cookie.includes("catAccCookies"),EVAL_USERCENTRICS_API_0:()=>"object"==typeof UC_UI,EVAL_USERCENTRICS_API_1:()=>!!UC_UI.closeCMP(),EVAL_USERCENTRICS_API_2:()=>!!UC_UI.denyAllConsents(),EVAL_USERCENTRICS_API_3:()=>!!UC_UI.acceptAllConsents(),EVAL_USERCENTRICS_API_4:()=>!!UC_UI.closeCMP(),EVAL_USERCENTRICS_API_5:()=>!0===UC_UI.areAllConsentsAccepted(),EVAL_USERCENTRICS_API_6:()=>!1===UC_UI.areAllConsentsAccepted(),EVAL_USERCENTRICS_BUTTON_0:()=>JSON.parse(localStorage.getItem("usercentrics")).consents.every((e=>e.isEssential||!e.consentStatus)),EVAL_WAITROSE_0:()=>Array.from(document.querySelectorAll("label[id$=cookies-deny-label]")).forEach((e=>e.click()))||!0,EVAL_WAITROSE_1:()=>document.cookie.includes("wtr_cookies_advertising=0")&&document.cookie.includes("wtr_cookies_analytics=0"),EVAL_WP_COOKIE_NOTICE_0:()=>document.cookie.includes("wpl_viewed_cookie=no"),EVAL_XE_TEST:()=>document.cookie.includes("xeConsentState={%22performance%22:false%2C%22marketing%22:false%2C%22compliance%22:false}"),EVAL_XING_0:()=>document.cookie.includes("userConsent=%7B%22marketing%22%3Afalse"),EVAL_YOUTUBE_DESKTOP_0:()=>!!document.cookie.match(/SOCS=CAE/),EVAL_YOUTUBE_MOBILE_0:()=>!!document.cookie.match(/SOCS=CAE/)};var p={main:!0,frame:!1,urlPattern:""},d=class{constructor(e){this.runContext=p,this.autoconsent=e}get hasSelfTest(){throw new Error("Not Implemented")}get isIntermediate(){throw new Error("Not Implemented")}get isCosmetic(){throw new Error("Not Implemented")}mainWorldEval(e){const t=l[e];if(!t)return console.warn("Snippet not found",e),Promise.resolve(!1);const o=this.autoconsent.config.logs;if(this.autoconsent.config.isMainWorld){o.evals&&console.log("inline eval:",e,t);let c=!1;try{c=!!t.call(globalThis)}catch(t){o.evals&&console.error("error evaluating rule",e,t)}return Promise.resolve(c)}const c=`(${t.toString()})()`;return o.evals&&console.log("async eval:",e,c),function(e,t){const o=a();r.sendContentMessage({type:"eval",id:o,code:e,snippetId:t});const c=new s(o);return r.pending.set(c.id,c),c.promise}(c,e).catch((t=>(o.evals&&console.error("error evaluating rule",e,t),!1)))}checkRunContext(){const e={...p,...this.runContext},t=window.top===window;return!(t&&!e.main)&&(!(!t&&!e.frame)&&!(e.urlPattern&&!window.location.href.match(e.urlPattern)))}detectCmp(){throw new Error("Not Implemented")}async detectPopup(){return!1}optOut(){throw new Error("Not Implemented")}optIn(){throw new Error("Not Implemented")}openCmp(){throw new Error("Not Implemented")}async test(){return Promise.resolve(!0)}click(e,t=!1){return this.autoconsent.domActions.click(e,t)}elementExists(e){return this.autoconsent.domActions.elementExists(e)}elementVisible(e,t){return this.autoconsent.domActions.elementVisible(e,t)}waitForElement(e,t){return this.autoconsent.domActions.waitForElement(e,t)}waitForVisible(e,t,o){return this.autoconsent.domActions.waitForVisible(e,t,o)}waitForThenClick(e,t,o){return this.autoconsent.domActions.waitForThenClick(e,t,o)}wait(e){return this.autoconsent.domActions.wait(e)}hide(e,t){return this.autoconsent.domActions.hide(e,t)}prehide(e){return this.autoconsent.domActions.prehide(e)}undoPrehide(){return this.autoconsent.domActions.undoPrehide()}querySingleReplySelector(e,t){return this.autoconsent.domActions.querySingleReplySelector(e,t)}querySelectorChain(e){return this.autoconsent.domActions.querySelectorChain(e)}elementSelector(e){return this.autoconsent.domActions.elementSelector(e)}},u=class extends d{constructor(e,t){super(t),this.rule=e,this.name=e.name,this.runContext=e.runContext||p}get hasSelfTest(){return!!this.rule.test}get isIntermediate(){return!!this.rule.intermediate}get isCosmetic(){return!!this.rule.cosmetic}get prehideSelectors(){return this.rule.prehideSelectors}async detectCmp(){return!!this.rule.detectCmp&&this._runRulesParallel(this.rule.detectCmp)}async detectPopup(){return!!this.rule.detectPopup&&this._runRulesSequentially(this.rule.detectPopup)}async optOut(){const e=this.autoconsent.config.logs;return!!this.rule.optOut&&(e.lifecycle&&console.log("Initiated optOut()",this.rule.optOut),this._runRulesSequentially(this.rule.optOut))}async optIn(){const e=this.autoconsent.config.logs;return!!this.rule.optIn&&(e.lifecycle&&console.log("Initiated optIn()",this.rule.optIn),this._runRulesSequentially(this.rule.optIn))}async openCmp(){return!!this.rule.openCmp&&this._runRulesSequentially(this.rule.openCmp)}async test(){return this.hasSelfTest?this._runRulesSequentially(this.rule.test):super.test()}async evaluateRuleStep(e){const t=[],o=this.autoconsent.config.logs;if(e.exists&&t.push(this.elementExists(e.exists)),e.visible&&t.push(this.elementVisible(e.visible,e.check)),e.eval){const o=this.mainWorldEval(e.eval);t.push(o)}if(e.waitFor&&t.push(this.waitForElement(e.waitFor,e.timeout)),e.waitForVisible&&t.push(this.waitForVisible(e.waitForVisible,e.timeout,e.check)),e.click&&t.push(this.click(e.click,e.all)),e.waitForThenClick&&t.push(this.waitForThenClick(e.waitForThenClick,e.timeout,e.all)),e.wait&&t.push(this.wait(e.wait)),e.hide&&t.push(this.hide(e.hide,e.method)),e.if){if(!e.if.exists&&!e.if.visible)return console.error("invalid conditional rule",e.if),!1;const c=await this.evaluateRuleStep(e.if);o.rulesteps&&console.log("Condition is",c),c?t.push(this._runRulesSequentially(e.then)):e.else?t.push(this._runRulesSequentially(e.else)):t.push(!0)}if(e.any){for(const t of e.any)if(await this.evaluateRuleStep(t))return!0;return!1}if(0===t.length)return o.errors&&console.warn("Unrecognized rule",e),!1;return(await Promise.all(t)).reduce(((e,t)=>e&&t),!0)}async _runRulesParallel(e){const t=e.map((e=>this.evaluateRuleStep(e)));return(await Promise.all(t)).every((e=>!!e))}async _runRulesSequentially(e){const t=this.autoconsent.config.logs;for(const o of e){t.rulesteps&&console.log("Running rule...",o);const e=await this.evaluateRuleStep(o);if(t.rulesteps&&console.log("...rule result",e),!e&&!o.optional)return!1}return!0}},m=class{constructor(e,t){this.name=e,this.config=t,this.methods=new Map,this.runContext=p,this.isCosmetic=!1,t.methods.forEach((e=>{e.action&&this.methods.set(e.name,e.action)})),this.hasSelfTest=!1}get isIntermediate(){return!1}checkRunContext(){return!0}async detectCmp(){return this.config.detectors.map((e=>o(e.presentMatcher))).some((e=>!!e))}async detectPopup(){return this.config.detectors.map((e=>o(e.showingMatcher))).some((e=>!!e))}async executeAction(e,t){return!this.methods.has(e)||c(this.methods.get(e),t)}async optOut(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),await this.executeAction("HIDE_CMP"),await this.executeAction("DO_CONSENT",[]),await this.executeAction("SAVE_CONSENT"),!0}async optIn(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),await this.executeAction("HIDE_CMP"),await this.executeAction("DO_CONSENT",["D","A","B","E","F","X"]),await this.executeAction("SAVE_CONSENT"),!0}async openCmp(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),!0}async test(){return!0}};function h(e="autoconsent-css-rules"){const t=`style#${e}`,o=document.querySelector(t);if(o&&o instanceof HTMLStyleElement)return o;{const t=document.head||document.getElementsByTagName("head")[0]||document.documentElement,o=document.createElement("style");return o.id=e,t.appendChild(o),o}}function k(e,t,o="display"){const c=`${t} { ${"opacity"===o?"opacity: 0":"display: none"} !important; z-index: -1 !important; pointer-events: none !important; } `;return e instanceof HTMLStyleElement&&(e.innerText+=c,t.length>0)}async function b(e,t,o){const c=await e();return!c&&t>0?new Promise((c=>{setTimeout((async()=>{c(b(e,t-1,o))}),o)})):Promise.resolve(c)}function _(e){if(!e)return!1;if(null!==e.offsetParent)return!0;{const t=window.getComputedStyle(e);if("fixed"===t.position&&"none"!==t.display)return!0}return!1}function g(e){const t={enabled:!0,autoAction:"optOut",disabledCmps:[],enablePrehide:!0,enableCosmeticRules:!0,detectRetries:20,isMainWorld:!1,prehideTimeout:2e3,logs:{lifecycle:!1,rulesteps:!1,evals:!1,errors:!0,messages:!1}},o=(c=t,globalThis.structuredClone?structuredClone(c):JSON.parse(JSON.stringify(c)));var c;for(const c of Object.keys(t))void 0!==e[c]&&(o[c]=e[c]);return o}var y="#truste-show-consent",w="#truste-consent-track",C=[class extends d{constructor(e){super(e),this.name="TrustArc-top",this.prehideSelectors=[".trustarc-banner-container",`.truste_popframe,.truste_overlay,.truste_box_overlay,${w}`],this.runContext={main:!0,frame:!1},this._shortcutButton=null,this._optInDone=!1}get hasSelfTest(){return!1}get isIntermediate(){return!this._optInDone&&!this._shortcutButton}get isCosmetic(){return!1}async detectCmp(){const e=this.elementExists(`${y},${w}`);return e&&(this._shortcutButton=document.querySelector("#truste-consent-required")),e}async detectPopup(){return this.elementVisible(`#truste-consent-content,#trustarc-banner-overlay,${w}`,"all")}openFrame(){this.click(y)}async optOut(){return this._shortcutButton?(this._shortcutButton.click(),!0):(k(h(),`.truste_popframe, .truste_overlay, .truste_box_overlay, ${w}`),this.click(y),setTimeout((()=>{h().remove()}),1e4),!0)}async optIn(){return this._optInDone=!0,this.click("#truste-consent-button")}async openCmp(){return!0}async test(){return await this.mainWorldEval("EVAL_TRUSTARC_TOP")}},class extends d{constructor(){super(...arguments),this.name="TrustArc-frame",this.runContext={main:!1,frame:!0,urlPattern:"^https://consent-pref\\.trustarc\\.com/\\?"}}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return!0}async detectPopup(){return this.elementVisible("#defaultpreferencemanager","any")&&this.elementVisible(".mainContent","any")}async navigateToSettings(){return await b((async()=>this.elementExists(".shp")||this.elementVisible(".advance","any")||this.elementExists(".switch span:first-child")),10,500),this.elementExists(".shp")&&this.click(".shp"),await this.waitForElement(".prefPanel",5e3),this.elementVisible(".advance","any")&&this.click(".advance"),await b((()=>this.elementVisible(".switch span:first-child","any")),5,1e3)}async optOut(){return await b((()=>"complete"===document.readyState),20,100),await this.waitForElement(".mainContent[aria-hidden=false]",5e3),!!this.click(".rejectAll")||(this.elementExists(".prefPanel")&&await this.waitForElement('.prefPanel[style="visibility: visible;"]',3e3),this.click("#catDetails0")?(this.click(".submit"),this.waitForThenClick("#gwt-debug-close_id",5e3),!0):this.click(".required")?(this.waitForThenClick("#gwt-debug-close_id",5e3),!0):(await this.navigateToSettings(),this.click(".switch span:nth-child(1):not(.active)",!0),this.click(".submit"),this.waitForThenClick("#gwt-debug-close_id",3e5),!0))}async optIn(){return this.click(".call")||(await this.navigateToSettings(),this.click(".switch span:nth-child(2)",!0),this.click(".submit"),this.waitForElement("#gwt-debug-close_id",3e5).then((()=>{this.click("#gwt-debug-close_id")}))),!0}},class extends d{constructor(){super(...arguments),this.name="Cybotcookiebot",this.prehideSelectors=["#CybotCookiebotDialog,#CybotCookiebotDialogBodyUnderlay,#dtcookie-container,#cookiebanner,#cb-cookieoverlay,.modal--cookie-banner,#cookiebanner_outer,#CookieBanner"]}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return await this.mainWorldEval("EVAL_COOKIEBOT_1")}async detectPopup(){return this.mainWorldEval("EVAL_COOKIEBOT_2")}async optOut(){await this.wait(500);let e=await this.mainWorldEval("EVAL_COOKIEBOT_3");return await this.wait(500),e=e&&await this.mainWorldEval("EVAL_COOKIEBOT_4"),e}async optIn(){return this.elementExists("#dtcookie-container")?this.click(".h-dtcookie-accept"):(this.click(".CybotCookiebotDialogBodyLevelButton:not(:checked):enabled",!0),this.click("#CybotCookiebotDialogBodyLevelButtonAccept"),this.click("#CybotCookiebotDialogBodyButtonAccept"),!0)}async test(){return await this.wait(500),await this.mainWorldEval("EVAL_COOKIEBOT_5")}},class extends d{constructor(){super(...arguments),this.name="Sourcepoint-frame",this.prehideSelectors=["div[id^='sp_message_container_'],.message-overlay","#sp_privacy_manager_container"],this.ccpaNotice=!1,this.ccpaPopup=!1,this.runContext={main:!0,frame:!0}}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){const e=new URL(location.href);return e.searchParams.has("message_id")&&"ccpa-notice.sp-prod.net"===e.hostname?(this.ccpaNotice=!0,!0):"ccpa-pm.sp-prod.net"===e.hostname?(this.ccpaPopup=!0,!0):("/index.html"===e.pathname||"/privacy-manager/index.html"===e.pathname||"/ccpa_pm/index.html"===e.pathname)&&(e.searchParams.has("message_id")||e.searchParams.has("requestUUID")||e.searchParams.has("consentUUID"))}async detectPopup(){return!!this.ccpaNotice||(this.ccpaPopup?await this.waitForElement(".priv-save-btn",2e3):(await this.waitForElement(".sp_choice_type_11,.sp_choice_type_12,.sp_choice_type_13,.sp_choice_type_ACCEPT_ALL,.sp_choice_type_SAVE_AND_EXIT",2e3),!this.elementExists(".sp_choice_type_9")))}async optIn(){return await this.waitForElement(".sp_choice_type_11,.sp_choice_type_ACCEPT_ALL",2e3),!!this.click(".sp_choice_type_11")||!!this.click(".sp_choice_type_ACCEPT_ALL")}isManagerOpen(){return"/privacy-manager/index.html"===location.pathname||"/ccpa_pm/index.html"===location.pathname}async optOut(){const e=this.autoconsent.config.logs;if(this.ccpaPopup){const e=document.querySelectorAll(".priv-purpose-container .sp-switch-arrow-block a.neutral.on .right");for(const t of e)t.click();const t=document.querySelectorAll(".priv-purpose-container .sp-switch-arrow-block a.switch-bg.on");for(const e of t)e.click();return this.click(".priv-save-btn")}if(!this.isManagerOpen()){if(!await this.waitForElement(".sp_choice_type_12,.sp_choice_type_13"))return!1;if(!this.elementExists(".sp_choice_type_12"))return this.click(".sp_choice_type_13");this.click(".sp_choice_type_12"),await b((()=>this.isManagerOpen()),200,100)}await this.waitForElement(".type-modal",2e4),this.waitForThenClick(".ccpa-stack .pm-switch[aria-checked=true] .slider",500,!0);try{const e=".sp_choice_type_REJECT_ALL",t=".reject-toggle",o=await Promise.race([this.waitForElement(e,2e3).then((e=>e?0:-1)),this.waitForElement(t,2e3).then((e=>e?1:-1)),this.waitForElement(".pm-features",2e3).then((e=>e?2:-1))]);if(0===o)return await this.wait(1500),this.click(e);1===o?this.click(t):2===o&&(await this.waitForElement(".pm-features",1e4),this.click(".checked > span",!0),this.click(".chevron"))}catch(t){e.errors&&console.warn(t)}return this.click(".sp_choice_type_SAVE_AND_EXIT")}},class extends d{constructor(){super(...arguments),this.name="consentmanager.net",this.prehideSelectors=["#cmpbox,#cmpbox2"],this.apiAvailable=!1}get hasSelfTest(){return this.apiAvailable}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.apiAvailable=await this.mainWorldEval("EVAL_CONSENTMANAGER_1"),!!this.apiAvailable||this.elementExists("#cmpbox")}async detectPopup(){return this.apiAvailable?(await this.wait(500),await this.mainWorldEval("EVAL_CONSENTMANAGER_2")):this.elementVisible("#cmpbox .cmpmore","any")}async optOut(){return await this.wait(500),this.apiAvailable?await this.mainWorldEval("EVAL_CONSENTMANAGER_3"):!!this.click(".cmpboxbtnno")||(this.elementExists(".cmpwelcomeprpsbtn")?(this.click(".cmpwelcomeprpsbtn > a[aria-checked=true]",!0),this.click(".cmpboxbtnsave"),!0):(this.click(".cmpboxbtncustom"),await this.waitForElement(".cmptblbox",2e3),this.click(".cmptdchoice > a[aria-checked=true]",!0),this.click(".cmpboxbtnyescustomchoices"),this.hide("#cmpwrapper,#cmpbox","display"),!0))}async optIn(){return this.apiAvailable?await this.mainWorldEval("EVAL_CONSENTMANAGER_4"):this.click(".cmpboxbtnyes")}async test(){if(this.apiAvailable)return await this.mainWorldEval("EVAL_CONSENTMANAGER_5")}},class extends d{constructor(){super(...arguments),this.name="Evidon"}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("#_evidon_banner")}async detectPopup(){return this.elementVisible("#_evidon_banner","any")}async optOut(){return this.click("#_evidon-decline-button")||(k(h(),"#evidon-prefdiag-overlay,#evidon-prefdiag-background"),this.click("#_evidon-option-button"),await this.waitForElement("#evidon-prefdiag-overlay",5e3),this.click("#evidon-prefdiag-decline")),!0}async optIn(){return this.click("#_evidon-accept-button")}},class extends d{constructor(){super(...arguments),this.name="Onetrust",this.prehideSelectors=["#onetrust-banner-sdk,#onetrust-consent-sdk,.onetrust-pc-dark-filter,.js-consent-banner"],this.runContext={urlPattern:"^(?!.*https://www\\.nba\\.com/)"}}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("#onetrust-banner-sdk,#onetrust-pc-sdk")}async detectPopup(){return this.elementVisible("#onetrust-banner-sdk,#onetrust-pc-sdk","any")}async optOut(){return this.elementVisible("#onetrust-reject-all-handler,.ot-pc-refuse-all-handler,.js-reject-cookies","any")?this.click("#onetrust-reject-all-handler,.ot-pc-refuse-all-handler,.js-reject-cookies"):(this.elementExists("#onetrust-pc-btn-handler")?this.click("#onetrust-pc-btn-handler"):this.click(".ot-sdk-show-settings,button.js-cookie-settings"),await this.waitForElement("#onetrust-consent-sdk",2e3),await this.wait(1e3),this.click("#onetrust-consent-sdk input.category-switch-handler:checked,.js-editor-toggle-state:checked",!0),await this.wait(1e3),await this.waitForElement(".save-preference-btn-handler,.js-consent-save",2e3),this.click(".save-preference-btn-handler,.js-consent-save"),await this.waitForVisible("#onetrust-banner-sdk",5e3,"none"),!0)}async optIn(){return this.click("#onetrust-accept-btn-handler,#accept-recommended-btn-handler,.js-accept-cookies")}async test(){return await b((()=>this.mainWorldEval("EVAL_ONETRUST_1")),10,500)}},class extends d{constructor(){super(...arguments),this.name="Klaro",this.prehideSelectors=[".klaro"],this.settingsOpen=!1}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".klaro > .cookie-modal")?(this.settingsOpen=!0,!0):this.elementExists(".klaro > .cookie-notice")}async detectPopup(){return this.elementVisible(".klaro > .cookie-notice,.klaro > .cookie-modal","any")}async optOut(){return!!await this.mainWorldEval("EVAL_KLARO_TRY_API_OPT_OUT")||(!!this.click(".klaro .cn-decline")||(await this.mainWorldEval("EVAL_KLARO_OPEN_POPUP"),!!this.click(".klaro .cn-decline")||(this.click(".cm-purpose:not(.cm-toggle-all) > input:not(.half-checked,.required,.only-required),.cm-purpose:not(.cm-toggle-all) > div > input:not(.half-checked,.required,.only-required)",!0),this.click(".cm-btn-accept,.cm-button"))))}async optIn(){return!!this.click(".klaro .cm-btn-accept-all")||(this.settingsOpen?(this.click(".cm-purpose:not(.cm-toggle-all) > input.half-checked",!0),this.click(".cm-btn-accept")):this.click(".klaro .cookie-notice .cm-btn-success"))}async test(){return await this.mainWorldEval("EVAL_KLARO_1")}},class extends d{constructor(){super(...arguments),this.name="Uniconsent"}get prehideSelectors(){return[".unic",".modal:has(.unic)"]}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".unic .unic-box,.unic .unic-bar,.unic .unic-modal")}async detectPopup(){return this.elementVisible(".unic .unic-box,.unic .unic-bar,.unic .unic-modal","any")}async optOut(){if(await this.waitForElement(".unic button",1e3),document.querySelectorAll(".unic button").forEach((e=>{const t=e.textContent;(t.includes("Manage Options")||t.includes("Optionen verwalten"))&&e.click()})),await this.waitForElement(".unic input[type=checkbox]",1e3)){await this.waitForElement(".unic button",1e3),document.querySelectorAll(".unic input[type=checkbox]").forEach((e=>{e.checked&&e.click()}));for(const e of document.querySelectorAll(".unic button")){const t=e.textContent;for(const o of["Confirm Choices","Save Choices","Auswahl speichern"])if(t.includes(o))return e.click(),await this.wait(500),!0}}return!1}async optIn(){return this.waitForThenClick(".unic #unic-agree")}async test(){await this.wait(1e3);return!this.elementExists(".unic .unic-box,.unic .unic-bar")}},class extends d{constructor(){super(...arguments),this.prehideSelectors=[".cmp-root"],this.name="Conversant"}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".cmp-root .cmp-receptacle")}async detectPopup(){return this.elementVisible(".cmp-root .cmp-receptacle","any")}async optOut(){if(!await this.waitForThenClick(".cmp-main-button:not(.cmp-main-button--primary)"))return!1;if(!await this.waitForElement(".cmp-view-tab-tabs"))return!1;await this.waitForThenClick(".cmp-view-tab-tabs > :first-child"),await this.waitForThenClick(".cmp-view-tab-tabs > .cmp-view-tab--active:first-child");for(const e of Array.from(document.querySelectorAll(".cmp-accordion-item"))){e.querySelector(".cmp-accordion-item-title").click(),await b((()=>!!e.querySelector(".cmp-accordion-item-content.cmp-active")),10,50);const t=e.querySelector(".cmp-accordion-item-content.cmp-active");t.querySelectorAll(".cmp-toggle-actions .cmp-toggle-deny:not(.cmp-toggle-deny--active)").forEach((e=>e.click())),t.querySelectorAll(".cmp-toggle-actions .cmp-toggle-checkbox:not(.cmp-toggle-checkbox--active)").forEach((e=>e.click()))}return await this.click(".cmp-main-button:not(.cmp-main-button--primary)"),!0}async optIn(){return this.waitForThenClick(".cmp-main-button.cmp-main-button--primary")}async test(){return document.cookie.includes("cmp-data=0")}},class extends d{constructor(){super(...arguments),this.name="tiktok.com",this.runContext={urlPattern:"tiktok"}}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}getShadowRoot(){const e=document.querySelector("tiktok-cookie-banner");return e?e.shadowRoot:null}async detectCmp(){return this.elementExists("tiktok-cookie-banner")}async detectPopup(){return _(this.getShadowRoot().querySelector(".tiktok-cookie-banner"))}async optOut(){const e=this.autoconsent.config.logs,t=this.getShadowRoot().querySelector(".button-wrapper button:first-child");return t?(e.rulesteps&&console.log("[clicking]",t),t.click(),!0):(e.errors&&console.log("no decline button found"),!1)}async optIn(){const e=this.autoconsent.config.logs,t=this.getShadowRoot().querySelector(".button-wrapper button:last-child");return t?(e.rulesteps&&console.log("[clicking]",t),t.click(),!0):(e.errors&&console.log("no accept button found"),!1)}async test(){const e=document.cookie.match(/cookie-consent=([^;]+)/);if(!e)return!1;const t=JSON.parse(decodeURIComponent(e[1]));return Object.values(t).every((e=>"boolean"!=typeof e||!1===e))}},class extends d{constructor(){super(...arguments),this.runContext={urlPattern:"^https://(www\\.)?airbnb\\.[^/]+/"},this.prehideSelectors=["div[data-testid=main-cookies-banner-container]",'div:has(> div:first-child):has(> div:last-child):has(> section [data-testid="strictly-necessary-cookies"])']}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("div[data-testid=main-cookies-banner-container]")}async detectPopup(){return this.elementVisible("div[data-testid=main-cookies-banner-container","any")}async optOut(){let e;for(await this.waitForThenClick("div[data-testid=main-cookies-banner-container] button._snbhip0");e=document.querySelector("[data-testid=modal-container] button[aria-checked=true]:not([disabled])");)e.click();return this.waitForThenClick("button[data-testid=save-btn]")}async optIn(){return this.waitForThenClick("div[data-testid=main-cookies-banner-container] button._148dgdpk")}async test(){return await b((()=>!!document.cookie.match("OptanonAlertBoxClosed")),20,200)}},class extends d{constructor(){super(...arguments),this.name="tumblr-com",this.runContext={urlPattern:"^https://(www\\.)?tumblr\\.com/"}}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}get prehideSelectors(){return["#cmp-app-container"]}async detectCmp(){return this.elementExists("#cmp-app-container")}async detectPopup(){return this.elementVisible("#cmp-app-container","any")}async optOut(){let e=document.querySelector("#cmp-app-container iframe"),t=e.contentDocument?.querySelector(".cmp-components-button.is-secondary");return!!t&&(t.click(),await b((()=>{const e=document.querySelector("#cmp-app-container iframe");return!!e.contentDocument?.querySelector(".cmp__dialog input")}),5,500),e=document.querySelector("#cmp-app-container iframe"),t=e.contentDocument?.querySelector(".cmp-components-button.is-secondary"),!!t&&(t.click(),!0))}async optIn(){const e=document.querySelector("#cmp-app-container iframe").contentDocument.querySelector(".cmp-components-button.is-primary");return!!e&&(e.click(),!0)}}],v=class{constructor(e){this.autoconsentInstance=e}click(e,t=!1){const o=this.elementSelector(e);return this.autoconsentInstance.config.logs.rulesteps&&console.log("[click]",e,t,o),o.length>0&&(t?o.forEach((e=>e.click())):o[0].click()),o.length>0}elementExists(e){return this.elementSelector(e).length>0}elementVisible(e,t){const o=this.elementSelector(e),c=new Array(o.length);return o.forEach(((e,t)=>{c[t]=_(e)})),"none"===t?c.every((e=>!e)):0!==c.length&&("any"===t?c.some((e=>e)):c.every((e=>e)))}waitForElement(e,t=1e4){const o=Math.ceil(t/200);return this.autoconsentInstance.config.logs.rulesteps&&console.log("[waitForElement]",e),b((()=>this.elementSelector(e).length>0),o,200)}waitForVisible(e,t=1e4,o="any"){return b((()=>this.elementVisible(e,o)),Math.ceil(t/200),200)}async waitForThenClick(e,t=1e4,o=!1){return await this.waitForElement(e,t),this.click(e,o)}wait(e){return new Promise((t=>{setTimeout((()=>{t(!0)}),e)}))}hide(e,t){return k(h(),e,t)}prehide(e){const t=h("autoconsent-prehide");return this.autoconsentInstance.config.logs.lifecycle&&console.log("[prehide]",t,location.href),k(t,e,"opacity")}undoPrehide(){const e=h("autoconsent-prehide");return this.autoconsentInstance.config.logs.lifecycle&&console.log("[undoprehide]",e,location.href),e&&e.remove(),!!e}querySingleReplySelector(e,t=document){if(e.startsWith("aria/"))return[];if(e.startsWith("xpath/")){const o=e.slice(6),c=document.evaluate(o,t,null,XPathResult.ANY_TYPE,null);let i=null;const n=[];for(;i=c.iterateNext();)n.push(i);return n}return e.startsWith("text/")||e.startsWith("pierce/")?[]:t.shadowRoot?Array.from(t.shadowRoot.querySelectorAll(e)):Array.from(t.querySelectorAll(e))}querySelectorChain(e){let t,o=document;for(const c of e){if(t=this.querySingleReplySelector(c,o),0===t.length)return[];o=t[0]}return t}elementSelector(e){return"string"==typeof e?this.querySingleReplySelector(e):this.querySelectorChain(e)}};var f=[{name:"192.com",detectCmp:[{exists:".ont-cookies"}],detectPopup:[{visible:".ont-cookies"}],optIn:[{click:".ont-btn-main.ont-cookies-btn.js-ont-btn-ok2"}],optOut:[{click:".ont-cookes-btn-manage"},{click:".ont-btn-main.ont-cookies-btn.js-ont-btn-choose"}],test:[{eval:"EVAL_ONENINETWO_0"}]},{name:"1password-com",cosmetic:!0,prehideSelectors:['footer #footer-root [aria-label="Cookie Consent"]'],detectCmp:[{exists:'footer #footer-root [aria-label="Cookie Consent"]'}],detectPopup:[{visible:'footer #footer-root [aria-label="Cookie Consent"]'}],optIn:[{click:'footer #footer-root [aria-label="Cookie Consent"] button'}],optOut:[{hide:'footer #footer-root [aria-label="Cookie Consent"]'}]},{name:"abconcerts.be",vendorUrl:"https://unknown",intermediate:!1,prehideSelectors:["dialog.cookie-consent"],detectCmp:[{exists:"dialog.cookie-consent form.cookie-consent__form"}],detectPopup:[{visible:"dialog.cookie-consent form.cookie-consent__form"}],optIn:[{waitForThenClick:"dialog.cookie-consent form.cookie-consent__form button[value=yes]"}],optOut:[{if:{exists:"dialog.cookie-consent form.cookie-consent__form button[value=no]"},then:[{click:"dialog.cookie-consent form.cookie-consent__form button[value=no]"}],else:[{click:"dialog.cookie-consent form.cookie-consent__form button.cookie-consent__options-toggle"},{waitForThenClick:'dialog.cookie-consent form.cookie-consent__form button[value="save_options"]'}]}]},{name:"activobank.pt",runContext:{urlPattern:"^https://(www\\.)?activobank\\.pt"},prehideSelectors:["aside#cookies,.overlay-cookies"],detectCmp:[{exists:"#cookies .cookies-btn"}],detectPopup:[{visible:"#cookies #submitCookies"}],optIn:[{waitForThenClick:"#cookies #submitCookies"}],optOut:[{waitForThenClick:"#cookies #rejectCookies"}]},{name:"Adroll",prehideSelectors:["#adroll_consent_container"],detectCmp:[{exists:"#adroll_consent_container"}],detectPopup:[{visible:"#adroll_consent_container"}],optIn:[{waitForThenClick:"#adroll_consent_accept"}],optOut:[{waitForThenClick:"#adroll_consent_reject"}],test:[{eval:"EVAL_ADROLL_0"}]},{name:"affinity.serif.com",detectCmp:[{exists:".c-cookie-banner button[data-qa='allow-all-cookies']"}],detectPopup:[{visible:".c-cookie-banner"}],optIn:[{click:'button[data-qa="allow-all-cookies"]'}],optOut:[{click:'button[data-qa="manage-cookies"]'},{waitFor:'.c-cookie-banner ~ [role="dialog"]'},{waitForThenClick:'.c-cookie-banner ~ [role="dialog"] input[type="checkbox"][value="true"]',all:!0},{click:'.c-cookie-banner ~ [role="dialog"] .c-modal__action button'}],test:[{wait:500},{eval:"EVAL_AFFINITY_SERIF_COM_0"}]},{name:"agolde.com",cosmetic:!0,prehideSelectors:["#modal-1 div[data-micromodal-close]"],detectCmp:[{exists:"#modal-1 div[aria-labelledby=modal-1-title]"}],detectPopup:[{exists:"#modal-1 div[data-micromodal-close]"}],optIn:[{click:'button[aria-label="Close modal"]'}],optOut:[{hide:"#modal-1 div[data-micromodal-close]"}]},{name:"aliexpress",vendorUrl:"https://aliexpress.com/",runContext:{urlPattern:"^https://.*\\.aliexpress\\.com/"},prehideSelectors:["#gdpr-new-container"],detectCmp:[{exists:"#gdpr-new-container"}],detectPopup:[{visible:"#gdpr-new-container"}],optIn:[{waitForThenClick:"#gdpr-new-container .btn-accept"}],optOut:[{waitForThenClick:"#gdpr-new-container .btn-more"},{waitFor:"#gdpr-new-container .gdpr-dialog-switcher"},{click:"#gdpr-new-container .switcher-on",all:!0,optional:!0},{click:"#gdpr-new-container .btn-save"}]},{name:"almacmp",prehideSelectors:["#alma-cmpv2-container"],detectCmp:[{exists:"#alma-cmpv2-container"}],detectPopup:[{visible:"#alma-cmpv2-container #almacmp-modal-layer1"}],optIn:[{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer1 #almacmp-modalConfirmBtn"}],optOut:[{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer1 #almacmp-modalSettingBtn"},{waitFor:"#alma-cmpv2-container #almacmp-modal-layer2"},{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer2 #almacmp-reject-all-layer2"}],test:[{eval:"EVAL_ALMACMP_0"}]},{name:"altium.com",cosmetic:!0,prehideSelectors:[".altium-privacy-bar"],detectCmp:[{exists:".altium-privacy-bar"}],detectPopup:[{exists:".altium-privacy-bar"}],optIn:[{click:"a.altium-privacy-bar__btn"}],optOut:[{hide:".altium-privacy-bar"}]},{name:"amazon.com",prehideSelectors:['span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'],detectCmp:[{exists:'span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'}],detectPopup:[{visible:'span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'}],optIn:[{waitForVisible:"#sp-cc-accept"},{wait:500},{click:"#sp-cc-accept"}],optOut:[{waitForVisible:"#sp-cc-rejectall-link"},{wait:500},{click:"#sp-cc-rejectall-link"}]},{name:"aquasana.com",cosmetic:!0,prehideSelectors:["#consent-tracking"],detectCmp:[{exists:"#consent-tracking"}],detectPopup:[{exists:"#consent-tracking"}],optIn:[{click:"#accept_consent"}],optOut:[{hide:"#consent-tracking"}]},{name:"arbeitsagentur",vendorUrl:"https://www.arbeitsagentur.de/",prehideSelectors:[".modal-open bahf-cookie-disclaimer-dpl3"],detectCmp:[{exists:"bahf-cookie-disclaimer-dpl3"}],detectPopup:[{visible:"bahf-cookie-disclaimer-dpl3"}],optIn:[{waitForThenClick:["bahf-cookie-disclaimer-dpl3","bahf-cd-modal-dpl3 .ba-btn-primary"]}],optOut:[{waitForThenClick:["bahf-cookie-disclaimer-dpl3","bahf-cd-modal-dpl3 .ba-btn-contrast"]}],test:[{eval:"EVAL_ARBEITSAGENTUR_TEST"}]},{name:"asus",vendorUrl:"https://www.asus.com/",runContext:{urlPattern:"^https://www\\.asus\\.com/"},prehideSelectors:["#cookie-policy-info,#cookie-policy-info-bg"],detectCmp:[{exists:"#cookie-policy-info"}],detectPopup:[{visible:"#cookie-policy-info"}],optIn:[{waitForThenClick:'#cookie-policy-info [data-agree="Accept Cookies"]'}],optOut:[{if:{exists:"#cookie-policy-info .btn-reject"},then:[{waitForThenClick:"#cookie-policy-info .btn-reject"}],else:[{waitForThenClick:"#cookie-policy-info .btn-setting"},{waitForThenClick:'#cookie-policy-lightbox-wrapper [data-agree="Save Settings"]'}]}]},{name:"athlinks-com",runContext:{urlPattern:"^https://(www\\.)?athlinks\\.com/"},cosmetic:!0,prehideSelectors:["#footer-container ~ div"],detectCmp:[{exists:"#footer-container ~ div"}],detectPopup:[{visible:"#footer-container > div"}],optIn:[{click:"#footer-container ~ div button"}],optOut:[{hide:"#footer-container ~ div"}]},{name:"ausopen.com",cosmetic:!0,detectCmp:[{exists:".gdpr-popup__message"}],detectPopup:[{visible:".gdpr-popup__message"}],optOut:[{hide:".gdpr-popup__message"}],optIn:[{click:".gdpr-popup__message button"}]},{name:"automattic-cmp-optout",prehideSelectors:['form[class*="cookie-banner"][method="post"]'],detectCmp:[{exists:'form[class*="cookie-banner"][method="post"]'}],detectPopup:[{visible:'form[class*="cookie-banner"][method="post"]'}],optIn:[{click:'a[class*="accept-all-button"]'}],optOut:[{click:'form[class*="cookie-banner"] div[class*="simple-options"] a[class*="customize-button"]'},{waitForThenClick:"input[type=checkbox][checked]:not([disabled])",all:!0},{click:'a[class*="accept-selection-button"]'}]},{name:"aws.amazon.com",prehideSelectors:["#awsccc-cb-content","#awsccc-cs-container","#awsccc-cs-modalOverlay","#awsccc-cs-container-inner"],detectCmp:[{exists:"#awsccc-cb-content"}],detectPopup:[{visible:"#awsccc-cb-content"}],optIn:[{click:"button[data-id=awsccc-cb-btn-accept"}],optOut:[{click:"button[data-id=awsccc-cb-btn-customize]"},{waitFor:"input[aria-checked]"},{click:"input[aria-checked=true]",all:!0,optional:!0},{click:"button[data-id=awsccc-cs-btn-save]"}]},{name:"axeptio",prehideSelectors:[".axeptio_widget"],detectCmp:[{exists:".axeptio_widget"}],detectPopup:[{visible:".axeptio_widget"}],optIn:[{waitFor:".axeptio-widget--open"},{click:"button#axeptio_btn_acceptAll"}],optOut:[{waitFor:".axeptio-widget--open"},{click:"button#axeptio_btn_dismiss"}],test:[{eval:"EVAL_AXEPTIO_0"}]},{name:"baden-wuerttemberg.de",prehideSelectors:[".cookie-alert.t-dark"],cosmetic:!0,detectCmp:[{exists:".cookie-alert.t-dark"}],detectPopup:[{visible:".cookie-alert.t-dark"}],optIn:[{click:".cookie-alert__form input:not([disabled]):not([checked])"},{click:".cookie-alert__button button"}],optOut:[{hide:".cookie-alert.t-dark"}]},{name:"bahn-de",vendorUrl:"https://www.bahn.de/",cosmetic:!1,runContext:{main:!0,frame:!1,urlPattern:"^https://(www\\.)?bahn\\.de/"},intermediate:!1,prehideSelectors:[],detectCmp:[{exists:["body > div:first-child","#consent-layer"]}],detectPopup:[{visible:["body > div:first-child","#consent-layer"]}],optIn:[{waitForThenClick:["body > div:first-child","#consent-layer .js-accept-all-cookies"]}],optOut:[{waitForThenClick:["body > div:first-child","#consent-layer .js-accept-essential-cookies"]}],test:[{eval:"EVAL_BAHN_TEST"}]},{name:"bbb.org",runContext:{urlPattern:"^https://www\\.bbb\\.org/"},cosmetic:!0,prehideSelectors:['div[aria-label="use of cookies on bbb.org"]'],detectCmp:[{exists:'div[aria-label="use of cookies on bbb.org"]'}],detectPopup:[{visible:'div[aria-label="use of cookies on bbb.org"]'}],optIn:[{click:'div[aria-label="use of cookies on bbb.org"] button.bds-button-unstyled span.visually-hidden'}],optOut:[{hide:'div[aria-label="use of cookies on bbb.org"]'}]},{name:"bing.com",prehideSelectors:["#bnp_container"],detectCmp:[{exists:"#bnp_cookie_banner"}],detectPopup:[{visible:"#bnp_cookie_banner"}],optIn:[{click:"#bnp_btn_accept"}],optOut:[{click:"#bnp_btn_preference"},{click:"#mcp_savesettings"}],test:[{eval:"EVAL_BING_0"}]},{name:"blocksy",vendorUrl:"https://creativethemes.com/blocksy/docs/extensions/cookies-consent/",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,prehideSelectors:[".cookie-notification"],detectCmp:[{exists:"#blocksy-ext-cookies-consent-styles-css"}],detectPopup:[{visible:".cookie-notification"}],optIn:[{click:".cookie-notification .ct-cookies-decline-button"}],optOut:[{waitForThenClick:".cookie-notification .ct-cookies-decline-button"}],test:[{eval:"EVAL_BLOCKSY_0"}]},{name:"borlabs",detectCmp:[{exists:"._brlbs-block-content"}],detectPopup:[{visible:"._brlbs-bar-wrap,._brlbs-box-wrap"}],optIn:[{click:"a[data-cookie-accept-all]"}],optOut:[{click:"a[data-cookie-individual]"},{waitForVisible:".cookie-preference"},{click:"input[data-borlabs-cookie-checkbox]:checked",all:!0,optional:!0},{click:"#CookiePrefSave"},{wait:500}],prehideSelectors:["#BorlabsCookieBox"],test:[{eval:"EVAL_BORLABS_0"}]},{name:"bundesregierung.de",prehideSelectors:[".bpa-cookie-banner"],detectCmp:[{exists:".bpa-cookie-banner"}],detectPopup:[{visible:".bpa-cookie-banner .bpa-module-full-hero"}],optIn:[{click:".bpa-accept-all-button"}],optOut:[{wait:500,comment:"click is not immediately recognized"},{waitForThenClick:".bpa-close-button"}],test:[{eval:"EVAL_BUNDESREGIERUNG_DE_0"}]},{name:"burpee.com",cosmetic:!0,prehideSelectors:["#notice-cookie-block"],detectCmp:[{exists:"#notice-cookie-block"}],detectPopup:[{exists:"#html-body #notice-cookie-block"}],optIn:[{click:"#btn-cookie-allow"}],optOut:[{hide:"#html-body #notice-cookie-block, #notice-cookie"}]},{name:"canva.com",prehideSelectors:['div[role="dialog"] a[data-anchor-id="cookie-policy"]'],detectCmp:[{exists:'div[role="dialog"] a[data-anchor-id="cookie-policy"]'}],detectPopup:[{exists:'div[role="dialog"] a[data-anchor-id="cookie-policy"]'}],optIn:[{click:'div[role="dialog"] button:nth-child(1)'}],optOut:[{if:{exists:'div[role="dialog"] button:nth-child(3)'},then:[{click:'div[role="dialog"] button:nth-child(2)'}],else:[{click:'div[role="dialog"] button:nth-child(2)'},{waitFor:'div[role="dialog"] a[data-anchor-id="cookie-policy"]'},{waitFor:'div[role="dialog"] button[role=switch]'},{click:'div[role="dialog"] button:nth-child(2):not([role])'},{click:'div[role="dialog"] div:last-child button:only-child'}]}],test:[{eval:"EVAL_CANVA_0"}]},{name:"canyon.com",runContext:{urlPattern:"^https://www\\.canyon\\.com/"},prehideSelectors:["div.modal.cookiesModal.is-open"],detectCmp:[{exists:"div.modal.cookiesModal.is-open"}],detectPopup:[{visible:"div.modal.cookiesModal.is-open"}],optIn:[{click:'div.cookiesModal__buttonWrapper > button[data-closecause="close-by-submit"]'}],optOut:[{click:'div.cookiesModal__buttonWrapper > button[data-closecause="close-by-manage-cookies"]'},{waitForThenClick:"button#js-manage-data-privacy-save-button"}]},{name:"cc-banner-springer",prehideSelectors:[".cc-banner[data-cc-banner]"],detectCmp:[{exists:".cc-banner[data-cc-banner]"}],detectPopup:[{visible:".cc-banner[data-cc-banner]"}],optIn:[{waitForThenClick:".cc-banner[data-cc-banner] button[data-cc-action=accept]"}],optOut:[{if:{exists:".cc-banner[data-cc-banner] button[data-cc-action=reject]"},then:[{click:".cc-banner[data-cc-banner] button[data-cc-action=reject]"}],else:[{waitForThenClick:".cc-banner[data-cc-banner] button[data-cc-action=preferences]"},{waitFor:".cc-preferences[data-cc-preferences]"},{click:".cc-preferences[data-cc-preferences] input[type=radio][data-cc-action=toggle-category][value=off]",all:!0,optional:!0},{if:{exists:".cc-preferences[data-cc-preferences] button[data-cc-action=reject]"},then:[{click:".cc-preferences[data-cc-preferences] button[data-cc-action=reject]"}],else:[{click:".cc-preferences[data-cc-preferences] button[data-cc-action=save]"}]}]}],test:[{eval:"EVAL_CC_BANNER2_0"}]},{name:"cc_banner",cosmetic:!0,prehideSelectors:[".cc_banner-wrapper"],detectCmp:[{exists:".cc_banner-wrapper"}],detectPopup:[{visible:".cc_banner"}],optIn:[{click:".cc_btn_accept_all"}],optOut:[{hide:".cc_banner-wrapper"}]},{name:"ciaopeople.it",prehideSelectors:["#cp-gdpr-choices"],detectCmp:[{exists:"#cp-gdpr-choices"}],detectPopup:[{visible:"#cp-gdpr-choices"}],optIn:[{waitForThenClick:".gdpr-btm__right > button:nth-child(2)"}],optOut:[{waitForThenClick:".gdpr-top-content > button"},{waitFor:".gdpr-top-back"},{waitForThenClick:".gdpr-btm__right > button:nth-child(1)"}],test:[{visible:"#cp-gdpr-choices",check:"none"}]},{vendorUrl:"https://www.civicuk.com/cookie-control/",name:"civic-cookie-control",prehideSelectors:["#ccc-module,#ccc-overlay"],detectCmp:[{exists:"#ccc-module"}],detectPopup:[{visible:"#ccc"},{visible:"#ccc-module"}],optOut:[{click:"#ccc-reject-settings"}],optIn:[{click:"#ccc-recommended-settings"}]},{name:"click.io",prehideSelectors:["#cl-consent"],detectCmp:[{exists:"#cl-consent"}],detectPopup:[{visible:"#cl-consent"}],optIn:[{waitForThenClick:'#cl-consent [data-role="b_agree"]'}],optOut:[{waitFor:'#cl-consent [data-role="b_options"]'},{wait:500},{click:'#cl-consent [data-role="b_options"]'},{waitFor:'.cl-consent-popup.cl-consent-visible [data-role="alloff"]'},{click:'.cl-consent-popup.cl-consent-visible [data-role="alloff"]',all:!0},{click:'[data-role="b_save"]'}],test:[{eval:"EVAL_CLICKIO_0",comment:"TODO: this only checks if we interacted at all"}]},{name:"clinch",intermediate:!1,runContext:{frame:!1,main:!0},prehideSelectors:[".consent-modal[role=dialog]"],detectCmp:[{exists:".consent-modal[role=dialog]"}],detectPopup:[{visible:".consent-modal[role=dialog]"}],optIn:[{click:"#consent_agree"}],optOut:[{if:{exists:"#consent_reject"},then:[{click:"#consent_reject"}],else:[{click:"#manage_cookie_preferences"},{click:"#cookie_consent_preferences input:checked",all:!0,optional:!0},{click:"#consent_save"}]}],test:[{eval:"EVAL_CLINCH_0"}]},{name:"clustrmaps.com",runContext:{urlPattern:"^https://(www\\.)?clustrmaps\\.com/"},cosmetic:!0,prehideSelectors:["#gdpr-cookie-message"],detectCmp:[{exists:"#gdpr-cookie-message"}],detectPopup:[{visible:"#gdpr-cookie-message"}],optIn:[{click:"button#gdpr-cookie-accept"}],optOut:[{hide:"#gdpr-cookie-message"}]},{name:"coinbase",intermediate:!1,runContext:{frame:!0,main:!0,urlPattern:"^https://(www|help)\\.coinbase\\.com"},prehideSelectors:[],detectCmp:[{exists:"div[class^=CookieBannerContent__Container]"}],detectPopup:[{visible:"div[class^=CookieBannerContent__Container]"}],optIn:[{click:"div[class^=CookieBannerContent__CTA] :nth-last-child(1)"}],optOut:[{click:"button[class^=CookieBannerContent__Settings]"},{click:"div[class^=CookiePreferencesModal__CategoryContainer] input:checked",all:!0,optional:!0},{click:"div[class^=CookiePreferencesModal__ButtonContainer] > button"}],test:[{eval:"EVAL_COINBASE_0"}]},{name:"Complianz banner",prehideSelectors:["#cmplz-cookiebanner-container"],detectCmp:[{exists:"#cmplz-cookiebanner-container .cmplz-cookiebanner"}],detectPopup:[{visible:"#cmplz-cookiebanner-container .cmplz-cookiebanner",check:"any"}],optIn:[{waitForThenClick:".cmplz-cookiebanner .cmplz-accept"}],optOut:[{waitForThenClick:".cmplz-cookiebanner .cmplz-deny"}],test:[{eval:"EVAL_COMPLIANZ_BANNER_0"}]},{name:"Complianz categories",prehideSelectors:['.cc-type-categories[aria-describedby="cookieconsent:desc"]'],detectCmp:[{exists:'.cc-type-categories[aria-describedby="cookieconsent:desc"]'}],detectPopup:[{visible:'.cc-type-categories[aria-describedby="cookieconsent:desc"]'}],optIn:[{any:[{click:".cc-accept-all"},{click:".cc-allow-all"},{click:".cc-allow"},{click:".cc-dismiss"}]}],optOut:[{if:{exists:'.cc-type-categories[aria-describedby="cookieconsent:desc"] .cc-dismiss'},then:[{click:".cc-dismiss"}],else:[{click:".cc-type-categories input[type=checkbox]:not([disabled]):checked",all:!0,optional:!0},{click:".cc-save"}]}]},{name:"Complianz notice",prehideSelectors:['.cc-type-info[aria-describedby="cookieconsent:desc"]'],cosmetic:!0,detectCmp:[{exists:'.cc-type-info[aria-describedby="cookieconsent:desc"] .cc-compliance .cc-btn'}],detectPopup:[{visible:'.cc-type-info[aria-describedby="cookieconsent:desc"] .cc-compliance .cc-btn'}],optIn:[{click:".cc-accept-all",optional:!0},{click:".cc-allow",optional:!0},{click:".cc-dismiss",optional:!0}],optOut:[{if:{exists:".cc-deny"},then:[{click:".cc-deny"}],else:[{hide:'[aria-describedby="cookieconsent:desc"]'}]}]},{name:"Complianz opt-both",prehideSelectors:['[aria-describedby="cookieconsent:desc"] .cc-type-opt-both'],detectCmp:[{exists:'[aria-describedby="cookieconsent:desc"] .cc-type-opt-both'}],detectPopup:[{visible:'[aria-describedby="cookieconsent:desc"] .cc-type-opt-both'}],optIn:[{click:".cc-accept-all",optional:!0},{click:".cc-allow",optional:!0},{click:".cc-dismiss",optional:!0}],optOut:[{waitForThenClick:".cc-deny"}]},{name:"Complianz opt-out",prehideSelectors:['[aria-describedby="cookieconsent:desc"].cc-type-opt-out'],detectCmp:[{exists:'[aria-describedby="cookieconsent:desc"].cc-type-opt-out'}],detectPopup:[{visible:'[aria-describedby="cookieconsent:desc"].cc-type-opt-out'}],optIn:[{click:".cc-accept-all",optional:!0},{click:".cc-allow",optional:!0},{click:".cc-dismiss",optional:!0}],optOut:[{if:{exists:".cc-deny"},then:[{click:".cc-deny"}],else:[{hide:'[aria-describedby="cookieconsent:desc"]'}]}]},{name:"Complianz optin",prehideSelectors:['.cc-type-opt-in[aria-describedby="cookieconsent:desc"]'],detectCmp:[{exists:'.cc-type-opt-in[aria-describedby="cookieconsent:desc"]'}],detectPopup:[{visible:'.cc-type-opt-in[aria-describedby="cookieconsent:desc"]'}],optIn:[{any:[{click:".cc-accept-all"},{click:".cc-allow"},{click:".cc-dismiss"}]}],optOut:[{if:{visible:".cc-deny"},then:[{click:".cc-deny"}],else:[{if:{visible:".cc-settings"},then:[{waitForThenClick:".cc-settings"},{waitForVisible:".cc-settings-view"},{click:".cc-settings-view input[type=checkbox]:not([disabled]):checked",all:!0,optional:!0},{click:".cc-settings-view .cc-btn-accept-selected"}],else:[{click:".cc-dismiss"}]}]}]},{name:"cookie-law-info",prehideSelectors:["#cookie-law-info-bar"],detectCmp:[{exists:"#cookie-law-info-bar"},{eval:"EVAL_COOKIE_LAW_INFO_DETECT"}],detectPopup:[{visible:"#cookie-law-info-bar"}],optIn:[{click:'[data-cli_action="accept_all"]'}],optOut:[{hide:"#cookie-law-info-bar"},{eval:"EVAL_COOKIE_LAW_INFO_0"}],test:[{eval:"EVAL_COOKIE_LAW_INFO_1"}]},{name:"cookie-manager-popup",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,detectCmp:[{exists:"#notice-cookie-block #allow-functional-cookies, #notice-cookie-block #btn-cookie-settings"}],detectPopup:[{visible:"#notice-cookie-block"}],optIn:[{click:"#btn-cookie-allow"}],optOut:[{if:{exists:"#allow-functional-cookies"},then:[{click:"#allow-functional-cookies"}],else:[{waitForThenClick:"#btn-cookie-settings"},{waitForVisible:".modal-body"},{click:'.modal-body input:checked, .switch[data-switch="on"]',all:!0,optional:!0},{click:'[role="dialog"] .modal-footer button'}]}],prehideSelectors:["#btn-cookie-settings"],test:[{eval:"EVAL_COOKIE_MANAGER_POPUP_0"}]},{name:"cookie-notice",prehideSelectors:["#cookie-notice"],cosmetic:!0,detectCmp:[{visible:"#cookie-notice .cookie-notice-container"}],detectPopup:[{visible:"#cookie-notice"}],optIn:[{click:"#cn-accept-cookie"}],optOut:[{hide:"#cookie-notice"}]},{name:"cookie-script",vendorUrl:"https://cookie-script.com/",prehideSelectors:["#cookiescript_injected"],detectCmp:[{exists:"#cookiescript_injected"}],detectPopup:[{visible:"#cookiescript_injected"}],optOut:[{if:{exists:"#cookiescript_reject"},then:[{wait:100},{click:"#cookiescript_reject"}],else:[{click:"#cookiescript_manage"},{waitForVisible:".cookiescript_fsd_main"},{waitForThenClick:"#cookiescript_reject"}]}],optIn:[{click:"#cookiescript_accept"}]},{name:"cookieacceptbar",vendorUrl:"https://unknown",cosmetic:!0,prehideSelectors:["#cookieAcceptBar.cookieAcceptBar"],detectCmp:[{exists:"#cookieAcceptBar.cookieAcceptBar"}],detectPopup:[{visible:"#cookieAcceptBar.cookieAcceptBar"}],optIn:[{waitForThenClick:"#cookieAcceptBarConfirm"}],optOut:[{hide:"#cookieAcceptBar.cookieAcceptBar"}]},{name:"cookiealert",intermediate:!1,prehideSelectors:[],runContext:{frame:!0,main:!0},detectCmp:[{exists:".cookie-alert-extended"}],detectPopup:[{visible:".cookie-alert-extended-modal"}],optIn:[{click:"button[data-controller='cookie-alert/extended/button/accept']"},{eval:"EVAL_COOKIEALERT_0"}],optOut:[{click:"a[data-controller='cookie-alert/extended/detail-link']"},{click:".cookie-alert-configuration-input:checked",all:!0,optional:!0},{click:"button[data-controller='cookie-alert/extended/button/configuration']"},{eval:"EVAL_COOKIEALERT_0"}],test:[{eval:"EVAL_COOKIEALERT_2"}]},{name:"cookieconsent2",vendorUrl:"https://www.github.com/orestbida/cookieconsent",comment:"supports v2.x.x of the library",prehideSelectors:["#cc--main"],detectCmp:[{exists:"#cc--main"}],detectPopup:[{visible:"#cm"},{exists:"#s-all-bn"}],optIn:[{waitForThenClick:"#s-all-bn"}],optOut:[{waitForThenClick:"#s-rall-bn"}],test:[{eval:"EVAL_COOKIECONSENT2_TEST"}]},{name:"cookieconsent3",vendorUrl:"https://www.github.com/orestbida/cookieconsent",comment:"supports v3.x.x of the library",prehideSelectors:["#cc-main"],detectCmp:[{exists:"#cc-main"}],detectPopup:[{visible:"#cc-main .cm-wrapper"}],optIn:[{waitForThenClick:".cm__btn[data-role=all]"}],optOut:[{waitForThenClick:".cm__btn[data-role=necessary]"}],test:[{eval:"EVAL_COOKIECONSENT3_TEST"}]},{name:"cookiefirst.com",prehideSelectors:["#cookiefirst-root,.cookiefirst-root,[aria-labelledby=cookie-preference-panel-title]"],detectCmp:[{exists:"#cookiefirst-root,.cookiefirst-root"}],detectPopup:[{visible:"#cookiefirst-root,.cookiefirst-root"}],optIn:[{click:"button[data-cookiefirst-action=accept]"}],optOut:[{if:{exists:"button[data-cookiefirst-action=adjust]"},then:[{click:"button[data-cookiefirst-action=adjust]"},{waitForVisible:"[data-cookiefirst-widget=modal]",timeout:1e3},{eval:"EVAL_COOKIEFIRST_1"},{wait:1e3},{click:"button[data-cookiefirst-action=save]"}],else:[{click:"button[data-cookiefirst-action=reject]"}]}],test:[{eval:"EVAL_COOKIEFIRST_0"}]},{name:"Cookie Information Banner",prehideSelectors:["#cookie-information-template-wrapper"],detectCmp:[{exists:"#cookie-information-template-wrapper"}],detectPopup:[{visible:"#cookie-information-template-wrapper"}],optIn:[{eval:"EVAL_COOKIEINFORMATION_1"}],optOut:[{hide:"#cookie-information-template-wrapper",comment:"some templates don't hide the banner automatically"},{eval:"EVAL_COOKIEINFORMATION_0"}],test:[{eval:"EVAL_COOKIEINFORMATION_2"}]},{name:"cookieyes",prehideSelectors:[".cky-overlay,.cky-consent-container"],detectCmp:[{exists:".cky-consent-container"}],detectPopup:[{visible:".cky-consent-container"}],optIn:[{waitForThenClick:".cky-consent-container [data-cky-tag=accept-button]"}],optOut:[{if:{exists:".cky-consent-container [data-cky-tag=reject-button]"},then:[{waitForThenClick:".cky-consent-container [data-cky-tag=reject-button]"}],else:[{if:{exists:".cky-consent-container [data-cky-tag=settings-button]"},then:[{click:".cky-consent-container [data-cky-tag=settings-button]"},{waitFor:".cky-modal-open input[type=checkbox]"},{click:".cky-modal-open input[type=checkbox]:checked",all:!0,optional:!0},{waitForThenClick:".cky-modal [data-cky-tag=detail-save-button]"}],else:[{hide:".cky-consent-container,.cky-overlay"}]}]}],test:[{eval:"EVAL_COOKIEYES_0"}]},{name:"corona-in-zahlen.de",prehideSelectors:[".cookiealert"],detectCmp:[{exists:".cookiealert"}],detectPopup:[{visible:".cookiealert"}],optOut:[{click:".configurecookies"},{click:".confirmcookies"}],optIn:[{click:".acceptcookies"}]},{name:"crossfit-com",cosmetic:!0,prehideSelectors:['body #modal > div > div[class^="_wrapper_"]'],detectCmp:[{exists:'body #modal > div > div[class^="_wrapper_"]'}],detectPopup:[{visible:'body #modal > div > div[class^="_wrapper_"]'}],optIn:[{click:'button[aria-label="accept cookie policy"]'}],optOut:[{hide:'body #modal > div > div[class^="_wrapper_"]'}]},{name:"csu-landtag-de",runContext:{urlPattern:"^https://(www\\.|)?csu-landtag\\.de"},prehideSelectors:["#cookie-disclaimer"],detectCmp:[{exists:"#cookie-disclaimer"}],detectPopup:[{visible:"#cookie-disclaimer"}],optIn:[{click:"#cookieall"}],optOut:[{click:"#cookiesel"}]},{name:"dailymotion-us",cosmetic:!0,prehideSelectors:['div[class*="CookiePopup__desktopContainer"]:has(div[class*="CookiePopup"])'],detectCmp:[{exists:'div[class*="CookiePopup__desktopContainer"]'}],detectPopup:[{visible:'div[class*="CookiePopup__desktopContainer"]'}],optIn:[{click:'div[class*="CookiePopup__desktopContainer"] > button > span'}],optOut:[{hide:'div[class*="CookiePopup__desktopContainer"]'}]},{name:"dailymotion.com",runContext:{urlPattern:"^https://(www\\.)?dailymotion\\.com/"},prehideSelectors:['div[class*="Overlay__container"]:has(div[class*="TCF2Popup"])'],detectCmp:[{exists:'div[class*="TCF2Popup"]'}],detectPopup:[{visible:'[class*="TCF2Popup"] a[href^="https://www.dailymotion.com/legal/cookiemanagement"]'}],optIn:[{waitForThenClick:'button[class*="TCF2Popup__button"]:not([class*="TCF2Popup__personalize"])'}],optOut:[{waitForThenClick:'button[class*="TCF2ContinueWithoutAcceptingButton"]'}],test:[{eval:"EVAL_DAILYMOTION_0"}]},{name:"deepl.com",prehideSelectors:[".dl_cookieBanner_container"],detectCmp:[{exists:".dl_cookieBanner_container"}],detectPopup:[{visible:".dl_cookieBanner_container"}],optOut:[{click:".dl_cookieBanner--buttonSelected"}],optIn:[{click:".dl_cookieBanner--buttonAll"}]},{name:"delta.com",runContext:{urlPattern:"^https://www\\.delta\\.com/"},cosmetic:!0,prehideSelectors:["ngc-cookie-banner"],detectCmp:[{exists:"div.cookie-footer-container"}],detectPopup:[{visible:"div.cookie-footer-container"}],optIn:[{click:" button.cookie-close-icon"}],optOut:[{hide:"div.cookie-footer-container"}]},{name:"dmgmedia-us",prehideSelectors:["#mol-ads-cmp-iframe, div.mol-ads-cmp > form > div"],detectCmp:[{exists:"div.mol-ads-cmp > form > div"}],detectPopup:[{waitForVisible:"div.mol-ads-cmp > form > div"}],optIn:[{waitForThenClick:"button.mol-ads-cmp--btn-primary"}],optOut:[{waitForThenClick:"div.mol-ads-ccpa--message > u > a"},{waitForVisible:".mol-ads-cmp--modal-dialog"},{waitForThenClick:"a.mol-ads-cmp-footer-privacy"},{waitForThenClick:"button.mol-ads-cmp--btn-secondary"}]},{name:"dmgmedia",prehideSelectors:['[data-project="mol-fe-cmp"]'],detectCmp:[{exists:'[data-project="mol-fe-cmp"]'}],detectPopup:[{visible:'[data-project="mol-fe-cmp"]'}],optIn:[{waitForThenClick:'[data-project="mol-fe-cmp"] button[class*=primary]'}],optOut:[{waitForThenClick:'[data-project="mol-fe-cmp"] button[class*=basic]'},{waitForVisible:'[data-project="mol-fe-cmp"] div[class*="tabContent"]'},{waitForThenClick:'[data-project="mol-fe-cmp"] div[class*="toggle"][class*="enabled"]',all:!0},{waitForThenClick:'[data-project="mol-fe-cmp"] button[class*=white]'}]},{name:"dndbeyond",vendorUrl:"https://www.dndbeyond.com/",runContext:{urlPattern:"^https://(www\\.)?dndbeyond\\.com/"},prehideSelectors:["[id^=cookie-consent-banner]"],detectCmp:[{exists:"[id^=cookie-consent-banner]"}],detectPopup:[{visible:"[id^=cookie-consent-banner]"}],optIn:[{waitForThenClick:"#cookie-consent-granted"}],optOut:[{waitForThenClick:"#cookie-consent-denied"}],test:[{eval:"EVAL_DNDBEYOND_TEST"}]},{name:"Drupal",detectCmp:[{exists:"#drupalorg-crosssite-gdpr"}],detectPopup:[{visible:"#drupalorg-crosssite-gdpr"}],optOut:[{click:".no"}],optIn:[{click:".yes"}]},{name:"WP DSGVO Tools",link:"https://wordpress.org/plugins/shapepress-dsgvo/",prehideSelectors:[".sp-dsgvo"],cosmetic:!0,detectCmp:[{exists:".sp-dsgvo.sp-dsgvo-popup-overlay"}],detectPopup:[{visible:".sp-dsgvo.sp-dsgvo-popup-overlay",check:"any"}],optIn:[{click:".sp-dsgvo-privacy-btn-accept-all",all:!0}],optOut:[{hide:".sp-dsgvo.sp-dsgvo-popup-overlay"}],test:[{eval:"EVAL_DSGVO_0"}]},{name:"dunelm.com",prehideSelectors:["div[data-testid=cookie-consent-modal-backdrop]"],detectCmp:[{exists:"div[data-testid=cookie-consent-message-contents]"}],detectPopup:[{visible:"div[data-testid=cookie-consent-message-contents]"}],optIn:[{click:'[data-testid="cookie-consent-allow-all"]'}],optOut:[{click:"button[data-testid=cookie-consent-adjust-settings]"},{click:"button[data-testid=cookie-consent-preferences-save]"}],test:[{eval:"EVAL_DUNELM_0"}]},{name:"ecosia",vendorUrl:"https://www.ecosia.org/",runContext:{urlPattern:"^https://www\\.ecosia\\.org/"},prehideSelectors:[".cookie-wrapper"],detectCmp:[{exists:".cookie-wrapper > .cookie-notice"}],detectPopup:[{visible:".cookie-wrapper > .cookie-notice"}],optIn:[{waitForThenClick:"[data-test-id=cookie-notice-accept]"}],optOut:[{waitForThenClick:"[data-test-id=cookie-notice-reject]"}]},{name:"Ensighten ensModal",prehideSelectors:[".ensModal"],detectCmp:[{exists:".ensModal"}],detectPopup:[{visible:".ensModal"}],optIn:[{waitForThenClick:"#modalAcceptButton"}],optOut:[{waitForThenClick:".ensCheckbox:checked",all:!0},{waitForThenClick:"#ensSave"}]},{name:"Ensighten ensNotifyBanner",prehideSelectors:["#ensNotifyBanner"],detectCmp:[{exists:"#ensNotifyBanner"}],detectPopup:[{visible:"#ensNotifyBanner"}],optIn:[{waitForThenClick:"#ensCloseBanner"}],optOut:[{waitForThenClick:"#ensRejectAll,#rejectAll,#ensRejectBanner"}]},{name:"etsy",prehideSelectors:["#gdpr-single-choice-overlay","#gdpr-privacy-settings"],detectCmp:[{exists:"#gdpr-single-choice-overlay"}],detectPopup:[{visible:"#gdpr-single-choice-overlay"}],optOut:[{click:"button[data-gdpr-open-full-settings]"},{waitForVisible:".gdpr-overlay-body input",timeout:3e3},{wait:1e3},{eval:"EVAL_ETSY_0"},{eval:"EVAL_ETSY_1"}],optIn:[{click:"button[data-gdpr-single-choice-accept]"}]},{name:"eu-cookie-compliance-banner",detectCmp:[{exists:"body.eu-cookie-compliance-popup-open"}],detectPopup:[{exists:"body.eu-cookie-compliance-popup-open"}],optIn:[{click:".agree-button"}],optOut:[{if:{visible:".decline-button,.eu-cookie-compliance-save-preferences-button"},then:[{click:".decline-button,.eu-cookie-compliance-save-preferences-button"}]},{hide:".eu-cookie-compliance-banner-info, #sliding-popup"}],test:[{eval:"EVAL_EU_COOKIE_COMPLIANCE_0"}]},{name:"EU Cookie Law",prehideSelectors:[".pea_cook_wrapper,.pea_cook_more_info_popover"],cosmetic:!0,detectCmp:[{exists:".pea_cook_wrapper"}],detectPopup:[{wait:500},{visible:".pea_cook_wrapper"}],optIn:[{click:"#pea_cook_btn"}],optOut:[{hide:".pea_cook_wrapper"}],test:[{eval:"EVAL_EU_COOKIE_LAW_0"}]},{name:"europa-eu",vendorUrl:"https://ec.europa.eu/",runContext:{urlPattern:"^https://[^/]*europa\\.eu/"},prehideSelectors:["#cookie-consent-banner"],detectCmp:[{exists:".cck-container"}],detectPopup:[{visible:".cck-container"}],optIn:[{waitForThenClick:'.cck-actions-button[href="#accept"]'}],optOut:[{waitForThenClick:'.cck-actions-button[href="#refuse"]',hide:".cck-container"}]},{name:"EZoic",prehideSelectors:["#ez-cookie-dialog-wrapper"],detectCmp:[{exists:"#ez-cookie-dialog-wrapper"}],detectPopup:[{visible:"#ez-cookie-dialog-wrapper"}],optIn:[{click:"#ez-accept-all",optional:!0},{eval:"EVAL_EZOIC_0",optional:!0}],optOut:[{wait:500},{click:"#ez-manage-settings"},{waitFor:"#ez-cookie-dialog input[type=checkbox]"},{click:"#ez-cookie-dialog input[type=checkbox]:checked",all:!0},{click:"#ez-save-settings"}],test:[{eval:"EVAL_EZOIC_1"}]},{name:"facebook",runContext:{urlPattern:"^https://([a-z0-9-]+\\.)?facebook\\.com/"},prehideSelectors:['div[data-testid="cookie-policy-manage-dialog"]'],detectCmp:[{exists:'div[data-testid="cookie-policy-manage-dialog"]'}],detectPopup:[{visible:'div[data-testid="cookie-policy-manage-dialog"]'}],optIn:[{waitForThenClick:'button[data-cookiebanner="accept_button"]'},{waitForVisible:'div[data-testid="cookie-policy-manage-dialog"]',check:"none"}],optOut:[{waitForThenClick:'button[data-cookiebanner="accept_only_essential_button"]'},{waitForVisible:'div[data-testid="cookie-policy-manage-dialog"]',check:"none"}]},{name:"fides",vendorUrl:"https://github.com/ethyca/fides",prehideSelectors:["#fides-overlay"],detectCmp:[{exists:"#fides-overlay #fides-banner"}],detectPopup:[{visible:"#fides-overlay #fides-banner"}],optIn:[{waitForThenClick:'#fides-banner [data-testid="Accept all-btn"]'}],optOut:[{waitForThenClick:'#fides-banner [data-testid="Reject all-btn"]'}]},{name:"funding-choices",prehideSelectors:[".fc-consent-root,.fc-dialog-container,.fc-dialog-overlay,.fc-dialog-content"],detectCmp:[{exists:".fc-consent-root"}],detectPopup:[{exists:".fc-dialog-container"}],optOut:[{click:".fc-cta-do-not-consent,.fc-cta-manage-options"},{click:".fc-preference-consent:checked,.fc-preference-legitimate-interest:checked",all:!0,optional:!0},{click:".fc-confirm-choices",optional:!0}],optIn:[{click:".fc-cta-consent"}]},{name:"geeks-for-geeks",runContext:{urlPattern:"^https://www\\.geeksforgeeks\\.org/"},cosmetic:!0,prehideSelectors:[".cookie-consent"],detectCmp:[{exists:".cookie-consent"}],detectPopup:[{visible:".cookie-consent"}],optIn:[{click:".cookie-consent button.consent-btn"}],optOut:[{hide:".cookie-consent"}]},{name:"generic-cosmetic",cosmetic:!0,prehideSelectors:["#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"],detectCmp:[{exists:"#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"}],detectPopup:[{visible:"#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"}],optIn:[],optOut:[{hide:"#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"}]},{name:"google-consent-standalone",prehideSelectors:[],detectCmp:[{exists:'a[href^="https://policies.google.com/technologies/cookies"'},{exists:'form[action^="https://consent.google."][action$=".com/save"]'}],detectPopup:[{visible:'a[href^="https://policies.google.com/technologies/cookies"'}],optIn:[{waitForThenClick:'form[action^="https://consent.google."][action$=".com/save"]:has(input[name=set_eom][value=false]) button'}],optOut:[{waitForThenClick:'form[action^="https://consent.google."][action$=".com/save"]:has(input[name=set_eom][value=true]) button'}]},{name:"google.com",prehideSelectors:[".HTjtHe#xe7COe"],detectCmp:[{exists:".HTjtHe#xe7COe"},{exists:'.HTjtHe#xe7COe a[href^="https://policies.google.com/technologies/cookies"]'}],detectPopup:[{visible:".HTjtHe#xe7COe button#W0wltc"}],optIn:[{waitForThenClick:".HTjtHe#xe7COe button#L2AGLb"}],optOut:[{waitForThenClick:".HTjtHe#xe7COe button#W0wltc"}],test:[{eval:"EVAL_GOOGLE_0"}]},{name:"gov.uk",detectCmp:[{exists:"#global-cookie-message"}],detectPopup:[{exists:"#global-cookie-message"}],optIn:[{click:"button[data-accept-cookies=true]"}],optOut:[{click:"button[data-reject-cookies=true],#reject-cookies"},{click:"button[data-hide-cookie-banner=true],#hide-cookie-decision"}]},{name:"hashicorp",vendorUrl:"https://hashicorp.com/",runContext:{urlPattern:"^https://[^.]*\\.hashicorp\\.com/"},prehideSelectors:["[data-testid=consent-banner]"],detectCmp:[{exists:"[data-testid=consent-banner]"}],detectPopup:[{visible:"[data-testid=consent-banner]"}],optIn:[{waitForThenClick:"[data-testid=accept]"}],optOut:[{waitForThenClick:"[data-testid=manage-preferences]"},{waitForThenClick:"[data-testid=consent-mgr-dialog] [data-ga-button=save-preferences]"}]},{name:"healthline-media",prehideSelectors:["#modal-host > div.no-hash > div.window-wrapper"],detectCmp:[{exists:"#modal-host > div.no-hash > div.window-wrapper, div[data-testid=qualtrics-container]"}],detectPopup:[{exists:"#modal-host > div.no-hash > div.window-wrapper, div[data-testid=qualtrics-container]"}],optIn:[{click:"#modal-host > div.no-hash > div.window-wrapper > div:last-child button"}],optOut:[{if:{exists:'#modal-host > div.no-hash > div.window-wrapper > div:last-child a[href="/privacy-settings"]'},then:[{click:'#modal-host > div.no-hash > div.window-wrapper > div:last-child a[href="/privacy-settings"]'}],else:[{waitForVisible:"div#__next"},{click:"#__next div:nth-child(1) > button:first-child"}]}]},{name:"hema",prehideSelectors:[".cookie-modal"],detectCmp:[{visible:".cookie-modal .cookie-accept-btn"}],detectPopup:[{visible:".cookie-modal .cookie-accept-btn"}],optIn:[{waitForThenClick:".cookie-modal .cookie-accept-btn"}],optOut:[{waitForThenClick:".cookie-modal .js-cookie-reject-btn"}],test:[{eval:"EVAL_HEMA_TEST_0"}]},{name:"hetzner.com",runContext:{urlPattern:"^https://www\\.hetzner\\.com/"},prehideSelectors:["#CookieConsent"],detectCmp:[{exists:"#CookieConsent"}],detectPopup:[{visible:"#CookieConsent"}],optIn:[{click:"#CookieConsentGiven"}],optOut:[{click:"#CookieConsentDeclined"}]},{name:"hl.co.uk",prehideSelectors:[".cookieModalContent","#cookie-banner-overlay"],detectCmp:[{exists:"#cookie-banner-overlay"}],detectPopup:[{exists:"#cookie-banner-overlay"}],optIn:[{click:"#acceptCookieButton"}],optOut:[{click:"#manageCookie"},{hide:".cookieSettingsModal"},{waitFor:"#AOCookieToggle"},{click:"#AOCookieToggle[aria-pressed=true]",optional:!0},{waitFor:"#TPCookieToggle"},{click:"#TPCookieToggle[aria-pressed=true]",optional:!0},{click:"#updateCookieButton"}]},{name:"hu-manity",vendorUrl:"https://hu-manity.co/",prehideSelectors:["#hu.hu-wrapper"],detectCmp:[{exists:"#hu.hu-visible"}],detectPopup:[{visible:"#hu.hu-visible"}],optIn:[{waitForThenClick:"[data-hu-action=cookies-notice-consent-choices-3]"},{waitForThenClick:"#hu-cookies-save"}],optOut:[{waitForThenClick:"#hu-cookies-save"}]},{name:"hubspot",detectCmp:[{exists:"#hs-eu-cookie-confirmation"}],detectPopup:[{visible:"#hs-eu-cookie-confirmation"}],optIn:[{click:"#hs-eu-confirmation-button"}],optOut:[{click:"#hs-eu-decline-button"}]},{name:"indeed.com",cosmetic:!0,prehideSelectors:["#CookiePrivacyNotice"],detectCmp:[{exists:"#CookiePrivacyNotice"}],detectPopup:[{visible:"#CookiePrivacyNotice"}],optIn:[{click:"#CookiePrivacyNotice button[data-gnav-element-name=CookiePrivacyNoticeOk]"}],optOut:[{hide:"#CookiePrivacyNotice"}]},{name:"ing.de",runContext:{urlPattern:"^https://www\\.ing\\.de/"},cosmetic:!0,prehideSelectors:['div[slot="backdrop"]'],detectCmp:[{exists:'[data-tag-name="ing-cc-dialog-frame"]'}],detectPopup:[{visible:'[data-tag-name="ing-cc-dialog-frame"]'}],optIn:[{click:['[data-tag-name="ing-cc-dialog-level0"]','[data-tag-name="ing-cc-button"][class*="accept"]']}],optOut:[{click:['[data-tag-name="ing-cc-dialog-level0"]','[data-tag-name="ing-cc-button"][class*="more"]']}]},{name:"instagram",vendorUrl:"https://instagram.com",runContext:{urlPattern:"^https://www\\.instagram\\.com/"},prehideSelectors:[".x78zum5.xdt5ytf.xg6iff7.x1n2onr6"],detectCmp:[{exists:".x1qjc9v5.x9f619.x78zum5.xdt5ytf.x1iyjqo2.xl56j7k"}],detectPopup:[{visible:".x1qjc9v5.x9f619.x78zum5.xdt5ytf.x1iyjqo2.xl56j7k"}],optIn:[{waitForThenClick:"._a9--._a9_0"}],optOut:[{waitForThenClick:"._a9--._a9_1"},{wait:2e3}]},{name:"ionos.de",prehideSelectors:[".privacy-consent--backdrop",".privacy-consent--modal"],detectCmp:[{exists:".privacy-consent--modal"}],detectPopup:[{visible:".privacy-consent--modal"}],optIn:[{click:"#selectAll"}],optOut:[{click:".footer-config-link"},{click:"#confirmSelection"}]},{name:"itopvpn.com",cosmetic:!0,prehideSelectors:[".pop-cookie"],detectCmp:[{exists:".pop-cookie"}],detectPopup:[{exists:".pop-cookie"}],optIn:[{click:"#_pcookie"}],optOut:[{hide:".pop-cookie"}]},{name:"iubenda",prehideSelectors:["#iubenda-cs-banner"],detectCmp:[{exists:"#iubenda-cs-banner"}],detectPopup:[{visible:".iubenda-cs-accept-btn"}],optIn:[{click:".iubenda-cs-accept-btn"}],optOut:[{click:".iubenda-cs-customize-btn"},{eval:"EVAL_IUBENDA_0"},{click:"#iubFooterBtn"}],test:[{eval:"EVAL_IUBENDA_1"}]},{name:"iWink",prehideSelectors:["body.cookies-request #cookie-bar"],detectCmp:[{exists:"body.cookies-request #cookie-bar"}],detectPopup:[{visible:"body.cookies-request #cookie-bar"}],optIn:[{waitForThenClick:"body.cookies-request #cookie-bar .allow-cookies"}],optOut:[{waitForThenClick:"body.cookies-request #cookie-bar .disallow-cookies"}],test:[{eval:"EVAL_IWINK_TEST"}]},{name:"jdsports",vendorUrl:"https://www.jdsports.co.uk/",runContext:{urlPattern:"^https://(www|m)\\.jdsports\\."},prehideSelectors:[".miniConsent,#PrivacyPolicyBanner"],detectCmp:[{exists:".miniConsent,#PrivacyPolicyBanner"}],detectPopup:[{visible:".miniConsent,#PrivacyPolicyBanner"}],optIn:[{waitForThenClick:".miniConsent .accept-all-cookies"}],optOut:[{if:{exists:"#PrivacyPolicyBanner"},then:[{hide:"#PrivacyPolicyBanner"}],else:[{waitForThenClick:"#cookie-settings"},{waitForThenClick:"#reject-all-cookies"}]}]},{name:"johnlewis.com",prehideSelectors:["div[class^=pecr-cookie-banner-]"],detectCmp:[{exists:"div[class^=pecr-cookie-banner-]"}],detectPopup:[{exists:"div[class^=pecr-cookie-banner-]"}],optOut:[{click:"button[data-test^=manage-cookies]"},{wait:"500"},{click:"label[data-test^=toggle][class*=checked]:not([class*=disabled])",all:!0,optional:!0},{click:"button[data-test=save-preferences]"}],optIn:[{click:"button[data-test=allow-all]"}]},{name:"jquery.cookieBar",vendorUrl:"https://github.com/kovarp/jquery.cookieBar",prehideSelectors:[".cookie-bar"],cosmetic:!0,detectCmp:[{exists:".cookie-bar .cookie-bar__message,.cookie-bar .cookie-bar__buttons"}],detectPopup:[{visible:".cookie-bar .cookie-bar__message,.cookie-bar .cookie-bar__buttons",check:"any"}],optIn:[{click:".cookie-bar .cookie-bar__btn"}],optOut:[{hide:".cookie-bar"}],test:[{visible:".cookie-bar .cookie-bar__message,.cookie-bar .cookie-bar__buttons",check:"none"},{eval:"EVAL_JQUERY_COOKIEBAR_0"}]},{name:"justwatch.com",prehideSelectors:[".consent-banner"],detectCmp:[{exists:".consent-banner .consent-banner__actions"}],detectPopup:[{visible:".consent-banner .consent-banner__actions"}],optIn:[{click:".consent-banner__actions button.basic-button.primary"}],optOut:[{click:".consent-banner__actions button.basic-button.secondary"},{waitForThenClick:".consent-modal__footer button.basic-button.secondary"},{waitForThenClick:".consent-modal ion-content > div > a:nth-child(9)"},{click:"label.consent-switch input[type=checkbox]:checked",all:!0,optional:!0},{waitForVisible:".consent-modal__footer button.basic-button.primary"},{click:".consent-modal__footer button.basic-button.primary"}]},{name:"ketch",vendorUrl:"https://www.ketch.com",runContext:{frame:!1,main:!0},intermediate:!1,prehideSelectors:["#lanyard_root div[role='dialog']"],detectCmp:[{exists:"#lanyard_root div[role='dialog']"}],detectPopup:[{visible:"#lanyard_root div[role='dialog']"}],optIn:[{if:{exists:"#lanyard_root button[class='confirmButton']"},then:[{waitForThenClick:"#lanyard_root div[class*=buttons] > :nth-child(2)"},{click:"#lanyard_root button[class='confirmButton']"}],else:[{waitForThenClick:"#lanyard_root div[class*=buttons] > :nth-child(2)"}]}],optOut:[{if:{exists:"#lanyard_root [aria-describedby=banner-description]"},then:[{waitForThenClick:"#lanyard_root div[class*=buttons] > button[class*=secondaryButton]",comment:"can be either settings or reject button"}]},{waitFor:"#lanyard_root [aria-describedby=preference-description],#lanyard_root [aria-describedby=modal-description]",timeout:1e3,optional:!0},{if:{exists:"#lanyard_root [aria-describedby=preference-description],#lanyard_root [aria-describedby=modal-description]"},then:[{waitForThenClick:"#lanyard_root button[class*=rejectButton]"},{click:"#lanyard_root button[class*=confirmButton],#lanyard_root div[class*=actions_] > button:nth-child(1)"}]}]},{name:"kleinanzeigen-de",runContext:{urlPattern:"^https?://(www\\.)?kleinanzeigen\\.de"},prehideSelectors:["#gdpr-banner-container"],detectCmp:[{any:[{exists:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-cmp-button]"},{exists:"#ConsentManagementPage"}]}],detectPopup:[{any:[{visible:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-cmp-button]"},{visible:"#ConsentManagementPage"}]}],optIn:[{if:{exists:"#gdpr-banner-container #gdpr-banner"},then:[{click:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-accept]"}],else:[{click:"#ConsentManagementPage .Button-primary"}]}],optOut:[{if:{exists:"#gdpr-banner-container #gdpr-banner"},then:[{click:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-cmp-button]"}],else:[{click:"#ConsentManagementPage .Button-secondary"}]}]},{name:"lightbox",prehideSelectors:[".darken-layer.open,.lightbox.lightbox--cookie-consent"],detectCmp:[{exists:"body.cookie-consent-is-active div.lightbox--cookie-consent > div.lightbox__content > div.cookie-consent[data-jsb]"}],detectPopup:[{visible:"body.cookie-consent-is-active div.lightbox--cookie-consent > div.lightbox__content > div.cookie-consent[data-jsb]"}],optOut:[{click:".cookie-consent__footer > button[type='submit']:not([data-button='selectAll'])"}],optIn:[{click:".cookie-consent__footer > button[type='submit'][data-button='selectAll']"}]},{name:"lineagrafica",vendorUrl:"https://addons.prestashop.com/en/legal/8734-eu-cookie-law-gdpr-banner-blocker.html",cosmetic:!0,prehideSelectors:["#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"],detectCmp:[{exists:"#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"}],detectPopup:[{exists:"#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"}],optIn:[{waitForThenClick:"#lgcookieslaw_accept"}],optOut:[{hide:"#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"}]},{name:"linkedin.com",prehideSelectors:[".artdeco-global-alert[type=COOKIE_CONSENT]"],detectCmp:[{exists:".artdeco-global-alert[type=COOKIE_CONSENT]"}],detectPopup:[{visible:".artdeco-global-alert[type=COOKIE_CONSENT]"}],optIn:[{waitForVisible:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=ACCEPT]"},{wait:500},{waitForThenClick:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=ACCEPT]"}],optOut:[{waitForVisible:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=DENY]"},{wait:500},{waitForThenClick:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=DENY]"}],test:[{waitForVisible:".artdeco-global-alert[type=COOKIE_CONSENT]",check:"none"}]},{name:"livejasmin",vendorUrl:"https://www.livejasmin.com/",runContext:{urlPattern:"^https://(m|www)\\.livejasmin\\.com/"},prehideSelectors:["#consent_modal"],detectCmp:[{exists:"#consent_modal"}],detectPopup:[{visible:"#consent_modal"}],optIn:[{waitForThenClick:"#consent_modal button[data-testid=ButtonStyledButton]:first-of-type"}],optOut:[{waitForThenClick:"#consent_modal button[data-testid=ButtonStyledButton]:nth-of-type(2)"},{waitForVisible:"[data-testid=PrivacyPreferenceCenterWithConsentCookieContent]"},{click:"[data-testid=PrivacyPreferenceCenterWithConsentCookieContent] input[data-testid=PrivacyPreferenceCenterWithConsentCookieSwitch]:checked",optional:!0,all:!0},{waitForThenClick:"[data-testid=PrivacyPreferenceCenterWithConsentCookieContent] button[data-testid=ButtonStyledButton]:last-child"}]},{name:"macpaw.com",cosmetic:!0,prehideSelectors:['div[data-banner="cookies"]'],detectCmp:[{exists:'div[data-banner="cookies"]'}],detectPopup:[{exists:'div[data-banner="cookies"]'}],optIn:[{click:'button[data-banner-close="cookies"]'}],optOut:[{hide:'div[data-banner="cookies"]'}]},{name:"marksandspencer.com",cosmetic:!0,detectCmp:[{exists:".navigation-cookiebbanner"}],detectPopup:[{visible:".navigation-cookiebbanner"}],optOut:[{hide:".navigation-cookiebbanner"}],optIn:[{click:".navigation-cookiebbanner__submit"}]},{name:"mediamarkt.de",prehideSelectors:["div[aria-labelledby=pwa-consent-layer-title]","div[class^=StyledConsentLayerWrapper-]"],detectCmp:[{exists:"div[aria-labelledby^=pwa-consent-layer-title]"}],detectPopup:[{exists:"div[aria-labelledby^=pwa-consent-layer-title]"}],optOut:[{click:"button[data-test^=pwa-consent-layer-deny-all]"}],optIn:[{click:"button[data-test^=pwa-consent-layer-accept-all"}]},{name:"Mediavine",prehideSelectors:['[data-name="mediavine-gdpr-cmp"]'],detectCmp:[{exists:'[data-name="mediavine-gdpr-cmp"]'}],detectPopup:[{wait:500},{visible:'[data-name="mediavine-gdpr-cmp"]'}],optIn:[{waitForThenClick:'[data-name="mediavine-gdpr-cmp"] [format="primary"]'}],optOut:[{waitForThenClick:'[data-name="mediavine-gdpr-cmp"] [data-view="manageSettings"]'},{waitFor:'[data-name="mediavine-gdpr-cmp"] input[type=checkbox]'},{eval:"EVAL_MEDIAVINE_0",optional:!0},{click:'[data-name="mediavine-gdpr-cmp"] [format="secondary"]'}]},{name:"microsoft.com",prehideSelectors:["#wcpConsentBannerCtrl"],detectCmp:[{exists:"#wcpConsentBannerCtrl"}],detectPopup:[{exists:"#wcpConsentBannerCtrl"}],optOut:[{eval:"EVAL_MICROSOFT_0"}],optIn:[{eval:"EVAL_MICROSOFT_1"}],test:[{eval:"EVAL_MICROSOFT_2"}]},{name:"midway-usa",runContext:{urlPattern:"^https://www\\.midwayusa\\.com/"},cosmetic:!0,prehideSelectors:["#cookie-container"],detectCmp:[{exists:['div[aria-label="Cookie Policy Banner"]']}],detectPopup:[{visible:"#cookie-container"}],optIn:[{click:"button#cookie-btn"}],optOut:[{hide:'div[aria-label="Cookie Policy Banner"]'}]},{name:"moneysavingexpert.com",detectCmp:[{exists:"dialog[data-testid=accept-our-cookies-dialog]"}],detectPopup:[{visible:"dialog[data-testid=accept-our-cookies-dialog]"}],optIn:[{click:"#banner-accept"}],optOut:[{click:"#banner-manage"},{click:"#pc-confirm"}]},{name:"monzo.com",prehideSelectors:[".cookie-alert, cookie-alert__content"],detectCmp:[{exists:'div.cookie-alert[role="dialog"]'},{exists:'a[href*="monzo"]'}],detectPopup:[{visible:".cookie-alert__content"}],optIn:[{click:".js-accept-cookie-policy"}],optOut:[{click:".js-decline-cookie-policy"}]},{name:"Moove",prehideSelectors:["#moove_gdpr_cookie_info_bar"],detectCmp:[{exists:"#moove_gdpr_cookie_info_bar"}],detectPopup:[{visible:"#moove_gdpr_cookie_info_bar:not(.moove-gdpr-info-bar-hidden)"}],optIn:[{waitForThenClick:".moove-gdpr-infobar-allow-all"}],optOut:[{if:{exists:"#moove_gdpr_cookie_info_bar .change-settings-button"},then:[{click:"#moove_gdpr_cookie_info_bar .change-settings-button"},{waitForVisible:"#moove_gdpr_cookie_modal"},{eval:"EVAL_MOOVE_0"},{click:".moove-gdpr-modal-save-settings"}],else:[{hide:"#moove_gdpr_cookie_info_bar"}]}],test:[{visible:"#moove_gdpr_cookie_info_bar",check:"none"}]},{name:"national-lottery.co.uk",detectCmp:[{exists:".cuk_cookie_consent"}],detectPopup:[{visible:".cuk_cookie_consent",check:"any"}],optOut:[{click:".cuk_cookie_consent_manage_pref"},{click:".cuk_cookie_consent_save_pref"},{click:".cuk_cookie_consent_close"}],optIn:[{click:".cuk_cookie_consent_accept_all"}]},{name:"nba.com",runContext:{urlPattern:"^https://(www\\.)?nba.com/"},cosmetic:!0,prehideSelectors:["#onetrust-banner-sdk"],detectCmp:[{exists:"#onetrust-banner-sdk"}],detectPopup:[{visible:"#onetrust-banner-sdk"}],optIn:[{click:"#onetrust-accept-btn-handler"}],optOut:[{hide:"#onetrust-banner-sdk"}]},{name:"netflix.de",detectCmp:[{exists:"#cookie-disclosure"}],detectPopup:[{visible:".cookie-disclosure-message",check:"any"}],optIn:[{click:".btn-accept"}],optOut:[{hide:"#cookie-disclosure"},{click:".btn-reject"}]},{name:"nhs.uk",prehideSelectors:["#nhsuk-cookie-banner"],detectCmp:[{exists:"#nhsuk-cookie-banner"}],detectPopup:[{exists:"#nhsuk-cookie-banner"}],optOut:[{click:"#nhsuk-cookie-banner__link_accept"}],optIn:[{click:"#nhsuk-cookie-banner__link_accept_analytics"}]},{name:"notice-cookie",prehideSelectors:[".button--notice"],cosmetic:!0,detectCmp:[{exists:".notice--cookie"}],detectPopup:[{visible:".notice--cookie"}],optIn:[{click:".button--notice"}],optOut:[{hide:".notice--cookie"}]},{name:"nrk.no",cosmetic:!0,prehideSelectors:[".nrk-masthead__info-banner--cookie"],detectCmp:[{exists:".nrk-masthead__info-banner--cookie"}],detectPopup:[{exists:".nrk-masthead__info-banner--cookie"}],optIn:[{click:"div.nrk-masthead__info-banner--cookie button > span:has(+ svg.nrk-close)"}],optOut:[{hide:".nrk-masthead__info-banner--cookie"}]},{name:"obi.de",prehideSelectors:[".disc-cp--active"],detectCmp:[{exists:".disc-cp-modal__modal"}],detectPopup:[{visible:".disc-cp-modal__modal"}],optIn:[{click:".js-disc-cp-accept-all"}],optOut:[{click:".js-disc-cp-deny-all"}]},{name:"om",vendorUrl:"https://olli-machts.de/en/extension/cookie-manager",prehideSelectors:[".tx-om-cookie-consent"],detectCmp:[{exists:".tx-om-cookie-consent .active[data-omcookie-panel]"}],detectPopup:[{exists:".tx-om-cookie-consent .active[data-omcookie-panel]"}],optIn:[{waitForThenClick:"[data-omcookie-panel-save=all]"}],optOut:[{if:{exists:"[data-omcookie-panel-save=min]"},then:[{waitForThenClick:"[data-omcookie-panel-save=min]"}],else:[{click:"input[data-omcookie-panel-grp]:checked:not(:disabled)",all:!0,optional:!0},{waitForThenClick:"[data-omcookie-panel-save=save]"}]}]},{name:"onlyFans.com",runContext:{urlPattern:"^https://onlyfans\\.com/"},prehideSelectors:["div.b-cookies-informer"],detectCmp:[{exists:"div.b-cookies-informer"}],detectPopup:[{exists:"div.b-cookies-informer"}],optIn:[{click:"div.b-cookies-informer__nav > button:nth-child(2)"}],optOut:[{click:"div.b-cookies-informer__nav > button:nth-child(1)"},{if:{exists:"div.b-cookies-informer__switchers"},then:[{click:"div.b-cookies-informer__switchers input:not([disabled])",all:!0},{click:"div.b-cookies-informer__nav > button"}]}]},{name:"openli",vendorUrl:"https://openli.com",prehideSelectors:[".legalmonster-cleanslate"],detectCmp:[{exists:".legalmonster-cleanslate"}],detectPopup:[{visible:".legalmonster-cleanslate #lm-cookie-wall-container",check:"any"}],optIn:[{waitForThenClick:"#lm-accept-all"}],optOut:[{waitForThenClick:"#lm-accept-necessary"}]},{name:"opera.com",vendorUrl:"https://unknown",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,prehideSelectors:[],detectCmp:[{exists:"#cookie-consent .manage-cookies__btn"}],detectPopup:[{visible:"#cookie-consent .cookie-basic-consent__btn"}],optIn:[{waitForThenClick:"#cookie-consent .cookie-basic-consent__btn"}],optOut:[{waitForThenClick:"#cookie-consent .manage-cookies__btn"},{waitForThenClick:"#cookie-consent .active.marketing_option_switch.cookie-consent__switch",all:!0},{waitForThenClick:"#cookie-consent .cookie-selection__btn"}],test:[{eval:"EVAL_OPERA_0"}]},{name:"osano",prehideSelectors:[".osano-cm-window,.osano-cm-dialog"],detectCmp:[{exists:".osano-cm-window"}],detectPopup:[{visible:".osano-cm-dialog"}],optIn:[{click:".osano-cm-accept-all",optional:!0}],optOut:[{waitForThenClick:".osano-cm-denyAll"}]},{name:"otto.de",prehideSelectors:[".cookieBanner--visibility"],detectCmp:[{exists:".cookieBanner--visibility"}],detectPopup:[{visible:".cookieBanner__wrapper"}],optIn:[{click:".js_cookieBannerPermissionButton"}],optOut:[{click:".js_cookieBannerProhibitionButton"}]},{name:"ourworldindata",vendorUrl:"https://ourworldindata.org/",runContext:{urlPattern:"^https://ourworldindata\\.org/"},prehideSelectors:[".cookie-manager"],detectCmp:[{exists:".cookie-manager"}],detectPopup:[{visible:".cookie-manager .cookie-notice.open"}],optIn:[{waitForThenClick:".cookie-notice [data-test=accept]"}],optOut:[{waitForThenClick:".cookie-notice [data-test=reject]"}]},{name:"pabcogypsum",vendorUrl:"https://unknown",prehideSelectors:[".js-cookie-notice:has(#cookie_settings-form)"],detectCmp:[{exists:".js-cookie-notice #cookie_settings-form"}],detectPopup:[{visible:".js-cookie-notice #cookie_settings-form"}],optIn:[{waitForThenClick:".js-cookie-notice button[value=allow]"}],optOut:[{waitForThenClick:".js-cookie-notice button[value=disable]"}]},{name:"paypal-us",prehideSelectors:["#ccpaCookieContent_wrapper, article.ppvx_modal--overpanel"],detectCmp:[{exists:"#ccpaCookieBanner, .privacy-sheet-content"}],detectPopup:[{exists:"#ccpaCookieBanner, .privacy-sheet-content"}],optIn:[{click:"#acceptAllButton"}],optOut:[{if:{exists:"a#manageCookiesLink"},then:[{click:"a#manageCookiesLink"}],else:[{waitForVisible:".privacy-sheet-content #formContent"},{click:"#formContent .cookiepref-11m2iee-checkbox_base input:checked",all:!0,optional:!0},{click:".confirmCookie #submitCookiesBtn"}]}]},{name:"paypal.com",prehideSelectors:["#gdprCookieBanner"],detectCmp:[{exists:"#gdprCookieBanner"}],detectPopup:[{visible:"#gdprCookieContent_wrapper"}],optIn:[{click:"#acceptAllButton"}],optOut:[{wait:200},{click:".gdprCookieBanner_decline-button"}],test:[{wait:500},{eval:"EVAL_PAYPAL_0"}]},{name:"pinetools.com",cosmetic:!0,prehideSelectors:["#aviso_cookies"],detectCmp:[{exists:"#aviso_cookies"}],detectPopup:[{exists:".lang_en #aviso_cookies"}],optIn:[{click:"#aviso_cookies .a_boton_cerrar"}],optOut:[{hide:"#aviso_cookies"}]},{name:"pmc",cosmetic:!0,prehideSelectors:["#pmc-pp-tou--notice"],detectCmp:[{exists:"#pmc-pp-tou--notice"}],detectPopup:[{visible:"#pmc-pp-tou--notice"}],optIn:[{click:"span.pmc-pp-tou--notice-close-btn"}],optOut:[{hide:"#pmc-pp-tou--notice"}]},{name:"pornhub.com",runContext:{urlPattern:"^https://(www\\.)?pornhub\\.com/"},cosmetic:!0,prehideSelectors:[".cookiesBanner"],detectCmp:[{exists:".cookiesBanner"}],detectPopup:[{visible:".cookiesBanner"}],optIn:[{click:".cookiesBanner .okButton"}],optOut:[{hide:".cookiesBanner"}]},{name:"pornpics.com",cosmetic:!0,prehideSelectors:["#cookie-contract"],detectCmp:[{exists:"#cookie-contract"}],detectPopup:[{visible:"#cookie-contract"}],optIn:[{click:"#cookie-contract .icon-cross"}],optOut:[{hide:"#cookie-contract"}]},{name:"PrimeBox CookieBar",prehideSelectors:["#cookie-bar"],detectCmp:[{exists:"#cookie-bar .cb-enable,#cookie-bar .cb-disable,#cookie-bar .cb-policy"}],detectPopup:[{visible:"#cookie-bar .cb-enable,#cookie-bar .cb-disable,#cookie-bar .cb-policy",check:"any"}],optIn:[{waitForThenClick:"#cookie-bar .cb-enable"}],optOut:[{click:"#cookie-bar .cb-disable",optional:!0},{hide:"#cookie-bar"}],test:[{eval:"EVAL_PRIMEBOX_0"}]},{name:"privacymanager.io",prehideSelectors:["#gdpr-consent-tool-wrapper",'iframe[src^="https://cmp-consent-tool.privacymanager.io"]'],runContext:{urlPattern:"^https://cmp-consent-tool\\.privacymanager\\.io/",main:!1,frame:!0},detectCmp:[{exists:"button#save"}],detectPopup:[{visible:"button#save"}],optIn:[{click:"button#save"}],optOut:[{if:{exists:"#denyAll"},then:[{click:"#denyAll"},{waitForThenClick:".okButton"}],else:[{waitForThenClick:"#manageSettings"},{waitFor:".purposes-overview-list"},{waitFor:"button#saveAndExit"},{click:"span[role=checkbox][aria-checked=true]",all:!0,optional:!0},{click:"button#saveAndExit"}]}]},{name:"productz.com",vendorUrl:"https://productz.com/",runContext:{urlPattern:"^https://productz\\.com/"},prehideSelectors:[],detectCmp:[{exists:".c-modal.is-active"}],detectPopup:[{visible:".c-modal.is-active"}],optIn:[{waitForThenClick:".c-modal.is-active .is-accept"}],optOut:[{waitForThenClick:".c-modal.is-active .is-dismiss"}]},{name:"pubtech",prehideSelectors:["#pubtech-cmp"],detectCmp:[{exists:"#pubtech-cmp"}],detectPopup:[{visible:"#pubtech-cmp #pt-actions"}],optIn:[{if:{exists:"#pt-accept-all"},then:[{click:"#pubtech-cmp #pt-actions #pt-accept-all"}],else:[{click:"#pubtech-cmp #pt-actions button:nth-of-type(2)"}]}],optOut:[{click:"#pubtech-cmp #pt-close"}],test:[{eval:"EVAL_PUBTECH_0"}]},{name:"quantcast",prehideSelectors:["#qc-cmp2-main,#qc-cmp2-container"],detectCmp:[{exists:"#qc-cmp2-container"}],detectPopup:[{visible:"#qc-cmp2-ui"}],optOut:[{click:'.qc-cmp2-summary-buttons > button[mode="secondary"]'},{waitFor:"#qc-cmp2-ui"},{click:'.qc-cmp2-toggle-switch > button[aria-checked="true"]',all:!0,optional:!0},{click:'.qc-cmp2-main button[aria-label="REJECT ALL"]',optional:!0},{waitForThenClick:'.qc-cmp2-main button[aria-label="SAVE & EXIT"],.qc-cmp2-buttons-desktop > button[mode="primary"]',timeout:5e3}],optIn:[{click:'.qc-cmp2-summary-buttons > button[mode="primary"]'}]},{name:"reddit.com",runContext:{urlPattern:"^https://www\\.reddit\\.com/"},prehideSelectors:["[bundlename=reddit_cookie_banner]"],detectCmp:[{exists:"reddit-cookie-banner"}],detectPopup:[{visible:"reddit-cookie-banner"}],optIn:[{waitForThenClick:["reddit-cookie-banner","#accept-all-cookies-button > button"]}],optOut:[{waitForThenClick:["reddit-cookie-banner","#reject-nonessential-cookies-button > button"]}],test:[{eval:"EVAL_REDDIT_0"}]},{name:"rog-forum.asus.com",runContext:{urlPattern:"^https://rog-forum\\.asus\\.com/"},prehideSelectors:["#cookie-policy-info"],detectCmp:[{exists:"#cookie-policy-info"}],detectPopup:[{visible:"#cookie-policy-info"}],optIn:[{click:'div.cookie-btn-box > div[aria-label="Accept"]'}],optOut:[{click:'div.cookie-btn-box > div[aria-label="Reject"]'},{waitForThenClick:'.cookie-policy-lightbox-bottom > div[aria-label="Save Settings"]'}]},{name:"roofingmegastore.co.uk",runContext:{urlPattern:"^https://(www\\.)?roofingmegastore\\.co\\.uk"},prehideSelectors:["#m-cookienotice"],detectCmp:[{exists:"#m-cookienotice"}],detectPopup:[{visible:"#m-cookienotice"}],optIn:[{click:"#accept-cookies"}],optOut:[{click:"#manage-cookies"},{waitForThenClick:"#accept-selected"}]},{name:"samsung.com",runContext:{urlPattern:"^https://www\\.samsung\\.com/"},cosmetic:!0,prehideSelectors:["div.cookie-bar"],detectCmp:[{exists:"div.cookie-bar"}],detectPopup:[{visible:"div.cookie-bar"}],optIn:[{click:"div.cookie-bar__manage > a"}],optOut:[{hide:"div.cookie-bar"}]},{name:"setapp.com",vendorUrl:"https://setapp.com/",cosmetic:!0,runContext:{urlPattern:"^https://setapp\\.com/"},prehideSelectors:[],detectCmp:[{exists:".cookie-banner.js-cookie-banner"}],detectPopup:[{visible:".cookie-banner.js-cookie-banner"}],optIn:[{waitForThenClick:".cookie-banner.js-cookie-banner button"}],optOut:[{hide:".cookie-banner.js-cookie-banner"}]},{name:"sibbo",prehideSelectors:["sibbo-cmp-layout"],detectCmp:[{exists:"sibbo-cmp-layout"}],detectPopup:[{visible:"sibbo-cmp-layout"}],optIn:[{click:"sibbo-cmp-layout [data-accept-all]"}],optOut:[{click:'.sibbo-panel__aside__buttons a[data-nav="purposes"]'},{click:'.sibbo-panel__main__header__actions a[data-focusable="reject-all"]'},{if:{exists:"[data-view=purposes] .sibbo-panel__main__footer__actions [data-save-and-exit]"},then:[],else:[{waitFor:'.sibbo-panel__main__footer__actions a[data-focusable="next"]:not(.sibbo-cmp-button--disabled)'},{click:'.sibbo-panel__main__footer__actions a[data-focusable="next"]'},{click:'.sibbo-panel__main div[data-view="purposesLegInt"] a[data-focusable="reject-all"]'}]},{waitFor:".sibbo-panel__main__footer__actions [data-save-and-exit]:not(.sibbo-cmp-button--disabled)"},{click:".sibbo-panel__main__footer__actions [data-save-and-exit]:not(.sibbo-cmp-button--disabled)"}],test:[{eval:"EVAL_SIBBO_0"}]},{name:"similarweb.com",cosmetic:!0,prehideSelectors:[".app-cookies-notification"],detectCmp:[{exists:".app-cookies-notification"}],detectPopup:[{exists:".app-layout .app-cookies-notification"}],optIn:[{click:"button.app-cookies-notification__dismiss"}],optOut:[{hide:".app-layout .app-cookies-notification"}]},{name:"Sirdata",cosmetic:!1,prehideSelectors:["#sd-cmp"],detectCmp:[{exists:"#sd-cmp"}],detectPopup:[{visible:"#sd-cmp"}],optIn:[{waitForThenClick:"#sd-cmp .sd-cmp-3cRQ2"}],optOut:[{waitForThenClick:["#sd-cmp","xpath///span[contains(., 'Do not accept') or contains(., 'Acceptera inte') or contains(., 'No aceptar') or contains(., 'Ikke acceptere') or contains(., 'Nicht akzeptieren') or contains(., 'Не приемам') or contains(., 'Να μην γίνει αποδοχή') or contains(., 'Niet accepteren') or contains(., 'Nepřijímat') or contains(., 'Nie akceptuj') or contains(., 'Nu acceptați') or contains(., 'Não aceitar') or contains(., 'Continuer sans accepter') or contains(., 'Non accettare') or contains(., 'Nem fogad el')]"]}]},{name:"snigel",detectCmp:[{exists:".snigel-cmp-framework"}],detectPopup:[{visible:".snigel-cmp-framework"}],optOut:[{click:"#sn-b-custom"},{click:"#sn-b-save"}],test:[{eval:"EVAL_SNIGEL_0"}],optIn:[{click:".snigel-cmp-framework #accept-choices"}]},{name:"steampowered.com",detectCmp:[{exists:".cookiepreferences_popup"},{visible:".cookiepreferences_popup"}],detectPopup:[{visible:".cookiepreferences_popup"}],optOut:[{click:"#rejectAllButton"}],optIn:[{click:"#acceptAllButton"}],test:[{wait:1e3},{eval:"EVAL_STEAMPOWERED_0"}]},{name:"strato.de",prehideSelectors:[".consent__wrapper"],runContext:{urlPattern:"^https://www\\.strato\\.de/"},detectCmp:[{exists:".consent"}],detectPopup:[{visible:".consent"}],optIn:[{click:"button.consentAgree"}],optOut:[{click:"button.consentSettings"},{waitForThenClick:"button#consentSubmit"}]},{name:"svt.se",vendorUrl:"https://www.svt.se/",runContext:{urlPattern:"^https://www\\.svt\\.se/"},prehideSelectors:["[class*=CookieConsent__root___]"],detectCmp:[{exists:"[class*=CookieConsent__root___]"}],detectPopup:[{visible:"[class*=CookieConsent__modal___]"}],optIn:[{waitForThenClick:"[class*=CookieConsent__modal___] > div > button[class*=primary]"}],optOut:[{waitForThenClick:"[class*=CookieConsent__modal___] > div > button[class*=secondary]:nth-child(2)"}],test:[{eval:"EVAL_SVT_TEST"}]},{name:"takealot.com",cosmetic:!0,prehideSelectors:['div[class^="cookies-banner-module_"]'],detectCmp:[{exists:'div[class^="cookies-banner-module_cookie-banner_"]'}],detectPopup:[{exists:'div[class^="cookies-banner-module_cookie-banner_"]'}],optIn:[{click:'button[class*="cookies-banner-module_dismiss-button_"]'}],optOut:[{hide:'div[class^="cookies-banner-module_"]'},{if:{exists:'div[class^="cookies-banner-module_small-cookie-banner_"]'},then:[{eval:"EVAL_TAKEALOT_0"}],else:[]}]},{name:"tarteaucitron.js",prehideSelectors:["#tarteaucitronRoot"],detectCmp:[{exists:"#tarteaucitronRoot"}],detectPopup:[{visible:"#tarteaucitronRoot #tarteaucitronAlertBig",check:"any"}],optIn:[{eval:"EVAL_TARTEAUCITRON_1"}],optOut:[{eval:"EVAL_TARTEAUCITRON_0"}],test:[{eval:"EVAL_TARTEAUCITRON_2",comment:"sometimes there are required categories, so we check that at least something is false"}]},{name:"taunton",vendorUrl:"https://www.taunton.com/",prehideSelectors:["#taunton-user-consent__overlay"],detectCmp:[{exists:"#taunton-user-consent__overlay"}],detectPopup:[{exists:"#taunton-user-consent__overlay:not([aria-hidden=true])"}],optIn:[{click:"#taunton-user-consent__toolbar input[type=checkbox]:not(:checked)"},{click:"#taunton-user-consent__toolbar button[type=submit]"}],optOut:[{click:"#taunton-user-consent__toolbar input[type=checkbox]:checked",optional:!0,all:!0},{click:"#taunton-user-consent__toolbar button[type=submit]"}],test:[{eval:"EVAL_TAUNTON_TEST"}]},{name:"Tealium",prehideSelectors:["#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs,#__tealiumImplicitmodal,#consent-layer"],detectCmp:[{exists:"#__tealiumGDPRecModal *,#__tealiumGDPRcpPrefs *,#__tealiumImplicitmodal *"},{eval:"EVAL_TEALIUM_0"}],detectPopup:[{visible:"#__tealiumGDPRecModal *,#__tealiumGDPRcpPrefs *,#__tealiumImplicitmodal *",check:"any"}],optOut:[{eval:"EVAL_TEALIUM_1"},{eval:"EVAL_TEALIUM_DONOTSELL"},{hide:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs,#__tealiumImplicitmodal"},{waitForThenClick:"#cm-acceptNone,.js-accept-essential-cookies",timeout:1e3,optional:!0}],optIn:[{hide:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs"},{eval:"EVAL_TEALIUM_2"}],test:[{eval:"EVAL_TEALIUM_3"},{eval:"EVAL_TEALIUM_DONOTSELL_CHECK"},{visible:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs",check:"none"}]},{name:"temu",vendorUrl:"https://temu.com",runContext:{urlPattern:"^https://[^/]*temu\\.com/"},prehideSelectors:["._2d-8vq-W,._1UdBUwni"],detectCmp:[{exists:"._3YCsmIaS"}],detectPopup:[{visible:"._3YCsmIaS"}],optIn:[{waitForThenClick:"._3fKiu5wx._3zN5SumS._3tAK973O.IYOfhWEs.VGNGF1pA"}],optOut:[{waitForThenClick:"._3fKiu5wx._1_XToJBF._3tAK973O.IYOfhWEs.VGNGF1pA"}]},{name:"Termly",prehideSelectors:["#termly-code-snippet-support"],detectCmp:[{exists:"#termly-code-snippet-support"}],detectPopup:[{visible:"#termly-code-snippet-support div"}],optIn:[{waitForThenClick:'[data-tid="banner-accept"]'}],optOut:[{if:{exists:'[data-tid="banner-decline"]'},then:[{click:'[data-tid="banner-decline"]'}],else:[{click:".t-preference-button"},{wait:500},{if:{exists:".t-declineAllButton"},then:[{click:".t-declineAllButton"}],else:[{waitForThenClick:".t-preference-modal input[type=checkbox][checked]:not([disabled])",all:!0},{waitForThenClick:".t-saveButton"}]}]}]},{name:"termsfeed",vendorUrl:"https://termsfeed.com",comment:"v4.x.x",prehideSelectors:[".termsfeed-com---nb"],detectCmp:[{exists:".termsfeed-com---nb"}],detectPopup:[{visible:".termsfeed-com---nb"}],optIn:[{waitForThenClick:".cc-nb-okagree"}],optOut:[{waitForThenClick:".cc-nb-reject"}]},{name:"termsfeed3",vendorUrl:"https://termsfeed.com",comment:"v3.x.x",cosmetic:!0,prehideSelectors:[".cc_dialog.cc_css_reboot"],detectCmp:[{exists:".cc_dialog.cc_css_reboot"}],detectPopup:[{visible:".cc_dialog.cc_css_reboot"}],optIn:[{waitForThenClick:".cc_dialog.cc_css_reboot .cc_b_ok"}],optOut:[{hide:".cc_dialog.cc_css_reboot"}]},{name:"Test page cosmetic CMP",cosmetic:!0,prehideSelectors:["#privacy-test-page-cmp-test-prehide"],detectCmp:[{exists:"#privacy-test-page-cmp-test-banner"}],detectPopup:[{visible:"#privacy-test-page-cmp-test-banner"}],optIn:[{waitFor:"#accept-all"},{click:"#accept-all"}],optOut:[{hide:"#privacy-test-page-cmp-test-banner"}],test:[{wait:500},{eval:"EVAL_TESTCMP_COSMETIC_0"}]},{name:"Test page CMP",prehideSelectors:["#reject-all"],detectCmp:[{exists:"#privacy-test-page-cmp-test"}],detectPopup:[{visible:"#privacy-test-page-cmp-test"}],optIn:[{waitFor:"#accept-all"},{click:"#accept-all"}],optOut:[{waitFor:"#reject-all"},{click:"#reject-all"}],test:[{eval:"EVAL_TESTCMP_0"}]},{name:"thalia.de",prehideSelectors:[".consent-banner-box"],detectCmp:[{exists:"consent-banner[component=consent-banner]"}],detectPopup:[{visible:".consent-banner-box"}],optIn:[{click:".button-zustimmen"}],optOut:[{click:"button[data-consent=disagree]"}]},{name:"thefreedictionary.com",prehideSelectors:["#cmpBanner"],detectCmp:[{exists:"#cmpBanner"}],detectPopup:[{visible:"#cmpBanner"}],optIn:[{eval:"EVAL_THEFREEDICTIONARY_1"}],optOut:[{eval:"EVAL_THEFREEDICTIONARY_0"}]},{name:"theverge",runContext:{frame:!1,main:!0,urlPattern:"^https://(www)?\\.theverge\\.com"},intermediate:!1,prehideSelectors:[".duet--cta--cookie-banner"],detectCmp:[{exists:".duet--cta--cookie-banner"}],detectPopup:[{visible:".duet--cta--cookie-banner"}],optIn:[{click:".duet--cta--cookie-banner button.tracking-12",all:!1}],optOut:[{click:".duet--cta--cookie-banner button.tracking-12 > span"}],test:[{eval:"EVAL_THEVERGE_0"}]},{name:"tidbits-com",cosmetic:!0,prehideSelectors:["#eu_cookie_law_widget-2"],detectCmp:[{exists:"#eu_cookie_law_widget-2"}],detectPopup:[{visible:"#eu_cookie_law_widget-2"}],optIn:[{click:"#eu-cookie-law form > input.accept"}],optOut:[{hide:"#eu_cookie_law_widget-2"}]},{name:"tractor-supply",runContext:{urlPattern:"^https://www\\.tractorsupply\\.com/"},cosmetic:!0,prehideSelectors:[".tsc-cookie-banner"],detectCmp:[{exists:".tsc-cookie-banner"}],detectPopup:[{visible:".tsc-cookie-banner"}],optIn:[{click:"#cookie-banner-cancel"}],optOut:[{hide:".tsc-cookie-banner"}]},{name:"trader-joes-com",cosmetic:!0,prehideSelectors:['div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'],detectCmp:[{exists:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}],detectPopup:[{visible:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}],optIn:[{click:'div[class^="CookiesAlert_cookiesAlert__container__"] button'}],optOut:[{hide:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}]},{name:"transcend",vendorUrl:"https://unknown",cosmetic:!0,prehideSelectors:["#transcend-consent-manager"],detectCmp:[{exists:"#transcend-consent-manager"}],detectPopup:[{visible:"#transcend-consent-manager"}],optIn:[{waitForThenClick:["#transcend-consent-manager","#consentManagerMainDialog .inner-container button"]}],optOut:[{hide:"#transcend-consent-manager"}]},{name:"transip-nl",runContext:{urlPattern:"^https://www\\.transip\\.nl/"},prehideSelectors:["#consent-modal"],detectCmp:[{any:[{exists:"#consent-modal"},{exists:"#privacy-settings-content"}]}],detectPopup:[{any:[{visible:"#consent-modal"},{visible:"#privacy-settings-content"}]}],optIn:[{click:'button[type="submit"]'}],optOut:[{if:{exists:"#privacy-settings-content"},then:[{click:'button[type="submit"]'}],else:[{click:"div.one-modal__action-footer-column--secondary > a"}]}]},{name:"tropicfeel-com",prehideSelectors:["#shopify-section-cookies-controller"],detectCmp:[{exists:"#shopify-section-cookies-controller"}],detectPopup:[{visible:"#shopify-section-cookies-controller #cookies-controller-main-pane",check:"any"}],optIn:[{waitForThenClick:"#cookies-controller-main-pane form[data-form-allow-all] button"}],optOut:[{click:"#cookies-controller-main-pane a[data-tab-target=manage-cookies]"},{waitFor:"#manage-cookies-pane.active"},{click:"#manage-cookies-pane.active input[type=checkbox][checked]:not([disabled])",all:!0},{click:"#manage-cookies-pane.active button[type=submit]"}],test:[]},{name:"true-car",runContext:{urlPattern:"^https://www\\.truecar\\.com/"},cosmetic:!0,prehideSelectors:[['div[aria-labelledby="cookie-banner-heading"]']],detectCmp:[{exists:'div[aria-labelledby="cookie-banner-heading"]'}],detectPopup:[{visible:'div[aria-labelledby="cookie-banner-heading"]'}],optIn:[{click:'div[aria-labelledby="cookie-banner-heading"] > button[aria-label="Close"]'}],optOut:[{hide:'div[aria-labelledby="cookie-banner-heading"]'}]},{name:"truyo",prehideSelectors:["#truyo-consent-module"],detectCmp:[{exists:"#truyo-cookieBarContent"}],detectPopup:[{visible:"#truyo-consent-module"}],optIn:[{click:"button#acceptAllCookieButton"}],optOut:[{click:"button#declineAllCookieButton"}]},{name:"twitch-mobile",vendorUrl:"https://m.twitch.tv/",cosmetic:!0,runContext:{urlPattern:"^https?://m\\.twitch\\.tv"},prehideSelectors:[],detectCmp:[{exists:'.ReactModal__Overlay [href="https://www.twitch.tv/p/cookie-policy"]'}],detectPopup:[{visible:'.ReactModal__Overlay [href="https://www.twitch.tv/p/cookie-policy"]'}],optIn:[{waitForThenClick:'.ReactModal__Overlay:has([href="https://www.twitch.tv/p/cookie-policy"]) button'}],optOut:[{hide:'.ReactModal__Overlay:has([href="https://www.twitch.tv/p/cookie-policy"])'}]},{name:"twitch.tv",runContext:{urlPattern:"^https?://(www\\.)?twitch\\.tv"},prehideSelectors:["div:has(> .consent-banner .consent-banner__content--gdpr-v2),.ReactModalPortal:has([data-a-target=consent-modal-save])"],detectCmp:[{exists:".consent-banner .consent-banner__content--gdpr-v2"}],detectPopup:[{visible:".consent-banner .consent-banner__content--gdpr-v2"}],optIn:[{click:'button[data-a-target="consent-banner-accept"]'}],optOut:[{hide:"div:has(> .consent-banner .consent-banner__content--gdpr-v2)"},{click:'button[data-a-target="consent-banner-manage-preferences"]'},{waitFor:"input[type=checkbox][data-a-target=tw-checkbox]"},{click:"input[type=checkbox][data-a-target=tw-checkbox][checked]:not([disabled])",all:!0,optional:!0},{waitForThenClick:"[data-a-target=consent-modal-save]"},{waitForVisible:".ReactModalPortal:has([data-a-target=consent-modal-save])",check:"none"}]},{name:"twitter",runContext:{urlPattern:"^https://([a-z0-9-]+\\.)?twitter\\.com/"},prehideSelectors:['[data-testid="BottomBar"]'],detectCmp:[{exists:'[data-testid="BottomBar"] div'}],detectPopup:[{visible:'[data-testid="BottomBar"] div'}],optIn:[{waitForThenClick:'[data-testid="BottomBar"] > div:has(>div:first-child>div:last-child>span[role=button]) > div:last-child > div[role=button]:first-child'}],optOut:[{waitForThenClick:'[data-testid="BottomBar"] > div:has(>div:first-child>div:last-child>span[role=button]) > div:last-child > div[role=button]:last-child'}],TODOtest:[{eval:"EVAL_document.cookie.includes('d_prefs=MjoxLGNvbnNlbnRfdmVyc2lvbjoy')"}]},{name:"ubuntu.com",prehideSelectors:["dialog.cookie-policy"],detectCmp:[{any:[{exists:"dialog.cookie-policy header"},{exists:'xpath///*[@id="modal"]/div/header'}]}],detectPopup:[{any:[{visible:"dialog header"},{visible:'xpath///*[@id="modal"]/div/header'}]}],optIn:[{any:[{waitForThenClick:"#cookie-policy-button-accept"},{waitForThenClick:'xpath///*[@id="cookie-policy-button-accept"]'}]}],optOut:[{any:[{waitForThenClick:"button.js-manage"},{waitForThenClick:'xpath///*[@id="cookie-policy-content"]/p[4]/button[2]'}]},{waitForThenClick:"dialog.cookie-policy .p-switch__input:checked",optional:!0,all:!0,timeout:500},{any:[{waitForThenClick:"dialog.cookie-policy .js-save-preferences"},{waitForThenClick:'xpath///*[@id="modal"]/div/button'}]}],test:[{eval:"EVAL_UBUNTU_COM_0"}]},{name:"UK Cookie Consent",prehideSelectors:["#catapult-cookie-bar"],cosmetic:!0,detectCmp:[{exists:"#catapult-cookie-bar"}],detectPopup:[{exists:".has-cookie-bar #catapult-cookie-bar"}],optIn:[{click:"#catapultCookie"}],optOut:[{hide:"#catapult-cookie-bar"}],test:[{eval:"EVAL_UK_COOKIE_CONSENT_0"}]},{name:"urbanarmorgear-com",cosmetic:!0,prehideSelectors:['div[class^="Layout__CookieBannerContainer-"]'],detectCmp:[{exists:'div[class^="Layout__CookieBannerContainer-"]'}],detectPopup:[{visible:'div[class^="Layout__CookieBannerContainer-"]'}],optIn:[{click:'button[class^="CookieBanner__AcceptButton"]'}],optOut:[{hide:'div[class^="Layout__CookieBannerContainer-"]'}]},{name:"usercentrics-api",detectCmp:[{exists:"#usercentrics-root"}],detectPopup:[{eval:"EVAL_USERCENTRICS_API_0"},{exists:["#usercentrics-root","[data-testid=uc-container]"]},{waitForVisible:"#usercentrics-root",timeout:2e3}],optIn:[{eval:"EVAL_USERCENTRICS_API_3"},{eval:"EVAL_USERCENTRICS_API_1"},{eval:"EVAL_USERCENTRICS_API_5"}],optOut:[{eval:"EVAL_USERCENTRICS_API_1"},{eval:"EVAL_USERCENTRICS_API_2"}],test:[{eval:"EVAL_USERCENTRICS_API_6"}]},{name:"usercentrics-button",detectCmp:[{exists:"#usercentrics-button"}],detectPopup:[{visible:"#usercentrics-button #uc-btn-accept-banner"}],optIn:[{click:"#usercentrics-button #uc-btn-accept-banner"}],optOut:[{click:"#usercentrics-button #uc-btn-deny-banner"}],test:[{eval:"EVAL_USERCENTRICS_BUTTON_0"}]},{name:"uswitch.com",prehideSelectors:["#cookie-banner-wrapper"],detectCmp:[{exists:"#cookie-banner-wrapper"}],detectPopup:[{visible:"#cookie-banner-wrapper"}],optIn:[{click:"#cookie_banner_accept_mobile"}],optOut:[{click:"#cookie_banner_save"}]},{name:"vodafone.de",runContext:{urlPattern:"^https://www\\.vodafone\\.de/"},prehideSelectors:[".dip-consent,.dip-consent-container"],detectCmp:[{exists:".dip-consent-container"}],detectPopup:[{visible:".dip-consent-content"}],optOut:[{click:'.dip-consent-btn[tabindex="2"]'}],optIn:[{click:'.dip-consent-btn[tabindex="1"]'}]},{name:"waitrose.com",prehideSelectors:["div[aria-labelledby=CookieAlertModalHeading]","section[data-test=initial-waitrose-cookie-consent-banner]","section[data-test=cookie-consent-modal]"],detectCmp:[{exists:"section[data-test=initial-waitrose-cookie-consent-banner]"}],detectPopup:[{visible:"section[data-test=initial-waitrose-cookie-consent-banner]"}],optIn:[{click:"button[data-test=accept-all]"}],optOut:[{click:"button[data-test=manage-cookies]"},{wait:200},{eval:"EVAL_WAITROSE_0"},{click:"button[data-test=submit]"}],test:[{eval:"EVAL_WAITROSE_1"}]},{name:"webflow",vendorUrl:"https://webflow.com/",prehideSelectors:[".fs-cc-components"],detectCmp:[{exists:".fs-cc-components"}],detectPopup:[{visible:".fs-cc-components"},{visible:"[fs-cc=banner]"}],optIn:[{wait:500},{waitForThenClick:"[fs-cc=banner] [fs-cc=allow]"}],optOut:[{wait:500},{waitForThenClick:"[fs-cc=banner] [fs-cc=deny]"}]},{name:"wetransfer.com",detectCmp:[{exists:".welcome__cookie-notice"}],detectPopup:[{visible:".welcome__cookie-notice"}],optIn:[{click:".welcome__button--accept"}],optOut:[{click:".welcome__button--decline"}]},{name:"whitepages.com",runContext:{urlPattern:"^https://www\\.whitepages\\.com/"},cosmetic:!0,prehideSelectors:[".cookie-wrapper, .cookie-overlay"],detectCmp:[{exists:".cookie-wrapper"}],detectPopup:[{visible:".cookie-overlay"}],optIn:[{click:'button[aria-label="Got it"]'}],optOut:[{hide:".cookie-wrapper"}]},{name:"wolframalpha",vendorUrl:"https://www.wolframalpha.com",prehideSelectors:[],cosmetic:!0,runContext:{urlPattern:"^https://www\\.wolframalpha\\.com/"},detectCmp:[{exists:"section._a_yb"}],detectPopup:[{visible:"section._a_yb"}],optIn:[{waitForThenClick:"section._a_yb button"}],optOut:[{hide:"section._a_yb"}]},{name:"woo-commerce-com",prehideSelectors:[".wccom-comp-privacy-banner .wccom-privacy-banner"],detectCmp:[{exists:".wccom-comp-privacy-banner .wccom-privacy-banner"}],detectPopup:[{exists:".wccom-comp-privacy-banner .wccom-privacy-banner"}],optIn:[{click:".wccom-privacy-banner__content-buttons button.is-primary"}],optOut:[{click:".wccom-privacy-banner__content-buttons button.is-secondary"},{waitForThenClick:"input[type=checkbox][checked]:not([disabled])",all:!0},{click:"div.wccom-modal__footer > button"}]},{name:"WP Cookie Notice for GDPR",vendorUrl:"https://wordpress.org/plugins/gdpr-cookie-consent/",prehideSelectors:["#gdpr-cookie-consent-bar"],detectCmp:[{exists:"#gdpr-cookie-consent-bar"}],detectPopup:[{visible:"#gdpr-cookie-consent-bar"}],optIn:[{waitForThenClick:"#gdpr-cookie-consent-bar #cookie_action_accept"}],optOut:[{waitForThenClick:"#gdpr-cookie-consent-bar #cookie_action_reject"}],test:[{eval:"EVAL_WP_COOKIE_NOTICE_0"}]},{name:"wpcc",cosmetic:!0,prehideSelectors:[".wpcc-container"],detectCmp:[{exists:".wpcc-container"}],detectPopup:[{exists:".wpcc-container .wpcc-message"}],optIn:[{click:".wpcc-compliance .wpcc-btn"}],optOut:[{hide:".wpcc-container"}]},{name:"xe.com",vendorUrl:"https://www.xe.com/",runContext:{urlPattern:"^https://www\\.xe\\.com/"},prehideSelectors:["[class*=ConsentBanner]"],detectCmp:[{exists:"[class*=ConsentBanner]"}],detectPopup:[{visible:"[class*=ConsentBanner]"}],optIn:[{waitForThenClick:"[class*=ConsentBanner] .egnScw"}],optOut:[{wait:1e3},{waitForThenClick:"[class*=ConsentBanner] .frDWEu"},{waitForThenClick:"[class*=ConsentBanner] .hXIpFU"}],test:[{eval:"EVAL_XE_TEST"}]},{name:"xhamster-eu",prehideSelectors:[".cookies-modal"],detectCmp:[{exists:".cookies-modal"}],detectPopup:[{exists:".cookies-modal"}],optIn:[{click:"button.cmd-button-accept-all"}],optOut:[{click:"button.cmd-button-reject-all"}]},{name:"xhamster-us",runContext:{urlPattern:"^https://(www\\.)?xhamster\\d?\\.com"},cosmetic:!0,prehideSelectors:[".cookie-announce"],detectCmp:[{exists:".cookie-announce"}],detectPopup:[{visible:".cookie-announce .announce-text"}],optIn:[{click:".cookie-announce button.xh-button"}],optOut:[{hide:".cookie-announce"}]},{name:"xing.com",detectCmp:[{exists:"div[class^=cookie-consent-CookieConsent]"}],detectPopup:[{exists:"div[class^=cookie-consent-CookieConsent]"}],optIn:[{click:"#consent-accept-button"}],optOut:[{click:"#consent-settings-button"},{click:".consent-banner-button-accept-overlay"}],test:[{eval:"EVAL_XING_0"}]},{name:"xnxx-com",cosmetic:!0,prehideSelectors:["#cookies-use-alert"],detectCmp:[{exists:"#cookies-use-alert"}],detectPopup:[{visible:"#cookies-use-alert"}],optIn:[{click:"#cookies-use-alert .close"}],optOut:[{hide:"#cookies-use-alert"}]},{name:"xvideos",vendorUrl:"https://xvideos.com",runContext:{urlPattern:"^https://[^/]*xvideos\\.com/"},prehideSelectors:[],detectCmp:[{exists:".disclaimer-opened #disclaimer-cookies"}],detectPopup:[{visible:".disclaimer-opened #disclaimer-cookies"}],optIn:[{waitForThenClick:"#disclaimer-accept_cookies"}],optOut:[{waitForThenClick:"#disclaimer-reject_cookies"}]},{name:"Yahoo",runContext:{urlPattern:"^https://consent\\.yahoo\\.com/v2/"},prehideSelectors:["#reject-all"],detectCmp:[{exists:"#consent-page"}],detectPopup:[{visible:"#consent-page"}],optIn:[{waitForThenClick:"#consent-page button[value=agree]"}],optOut:[{waitForThenClick:"#consent-page button[value=reject]"}]},{name:"youporn.com",cosmetic:!0,prehideSelectors:[".euCookieModal, #js_euCookieModal"],detectCmp:[{exists:".euCookieModal"}],detectPopup:[{exists:".euCookieModal, #js_euCookieModal"}],optIn:[{click:'button[name="user_acceptCookie"]'}],optOut:[{hide:".euCookieModal"}]},{name:"youtube-desktop",prehideSelectors:["tp-yt-iron-overlay-backdrop.opened","ytd-consent-bump-v2-lightbox"],detectCmp:[{exists:"ytd-consent-bump-v2-lightbox tp-yt-paper-dialog"},{exists:'ytd-consent-bump-v2-lightbox tp-yt-paper-dialog a[href^="https://consent.youtube.com/"]'}],detectPopup:[{visible:"ytd-consent-bump-v2-lightbox tp-yt-paper-dialog"}],optIn:[{waitForThenClick:"ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:last-child #button,ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:last-child button"},{wait:500}],optOut:[{waitForThenClick:"ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:first-child #button,ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:first-child button"},{wait:500}],test:[{wait:500},{eval:"EVAL_YOUTUBE_DESKTOP_0"}]},{name:"youtube-mobile",prehideSelectors:[".consent-bump-v2-lightbox"],detectCmp:[{exists:"ytm-consent-bump-v2-renderer"}],detectPopup:[{visible:"ytm-consent-bump-v2-renderer"}],optIn:[{waitForThenClick:"ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons c3-material-button:first-child button, ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons ytm-button-renderer:first-child button"},{wait:500}],optOut:[{waitForThenClick:"ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons c3-material-button:nth-child(2) button, ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons ytm-button-renderer:nth-child(2) button"},{wait:500}],test:[{wait:500},{eval:"EVAL_YOUTUBE_MOBILE_0"}]},{name:"zdf",prehideSelectors:["#zdf-cmp-banner-sdk"],detectCmp:[{exists:"#zdf-cmp-banner-sdk"}],detectPopup:[{visible:"#zdf-cmp-main.zdf-cmp-show"}],optIn:[{waitForThenClick:"#zdf-cmp-main #zdf-cmp-accept-btn"}],optOut:[{waitForThenClick:"#zdf-cmp-main #zdf-cmp-deny-btn"}],test:[]}],A={"didomi.io":{detectors:[{presentMatcher:{target:{selector:"#didomi-host, #didomi-notice"},type:"css"},showingMatcher:{target:{selector:"body.didomi-popup-open, .didomi-notice-banner"},type:"css"}}],methods:[{action:{target:{selector:".didomi-popup-notice-buttons .didomi-button:not(.didomi-button-highlight), .didomi-notice-banner .didomi-learn-more-button"},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{retries:50,target:{selector:"#didomi-purpose-cookies"},type:"waitcss",waitTime:50},{consents:[{description:"Share (everything) with others",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-share_whith_others]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-share_whith_others]:last-child"},type:"click"},type:"X"},{description:"Information storage and access",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-cookies]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-cookies]:last-child"},type:"click"},type:"D"},{description:"Content selection, offers and marketing",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-CL-T1Rgm7]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-CL-T1Rgm7]:last-child"},type:"click"},type:"E"},{description:"Analytics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-analytics]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-analytics]:last-child"},type:"click"},type:"B"},{description:"Analytics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-M9NRHJe3G]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-M9NRHJe3G]:last-child"},type:"click"},type:"B"},{description:"Ad and content selection",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-advertising_personalization]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-advertising_personalization]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection",falseAction:{parent:{childFilter:{target:{selector:"#didomi-purpose-pub-ciblee"}},selector:".didomi-consent-popup-data-processing, .didomi-components-accordion-label-container"},target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-pub-ciblee]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-pub-ciblee]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - basics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-q4zlJqdcD]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-q4zlJqdcD]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - partners and subsidiaries",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-partenaire-cAsDe8jC]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-partenaire-cAsDe8jC]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-p4em9a8m]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-p4em9a8m]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - others",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-autres-pub]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-autres-pub]:last-child"},type:"click"},type:"F"},{description:"Social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-reseauxsociaux]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-reseauxsociaux]:last-child"},type:"click"},type:"A"},{description:"Social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-social_media]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-social_media]:last-child"},type:"click"},type:"A"},{description:"Content selection",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-content_personalization]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-content_personalization]:last-child"},type:"click"},type:"E"},{description:"Ad delivery",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-ad_delivery]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-ad_delivery]:last-child"},type:"click"},type:"F"}],type:"consent"},{action:{consents:[{matcher:{childFilter:{target:{selector:":not(.didomi-components-radio__option--selected)"}},type:"css"},trueAction:{target:{selector:":nth-child(2)"},type:"click"},falseAction:{target:{selector:":first-child"},type:"click"},type:"X"}],type:"consent"},target:{selector:".didomi-components-radio"},type:"foreach"}],type:"list"},name:"DO_CONSENT"},{action:{parent:{selector:".didomi-consent-popup-footer .didomi-consent-popup-actions"},target:{selector:".didomi-components-button:first-child"},type:"click"},name:"SAVE_CONSENT"}]},oil:{detectors:[{presentMatcher:{target:{selector:".as-oil-content-overlay"},type:"css"},showingMatcher:{target:{selector:".as-oil-content-overlay"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".as-js-advanced-settings"},type:"click"},{retries:"10",target:{selector:".as-oil-cpc__purpose-container"},type:"waitcss",waitTime:"250"}],type:"list"},name:"OPEN_OPTIONS"},{action:{actions:[{consents:[{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Information storage and access","Opbevaring af og adgang til oplysninger på din enhed"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Information storage and access","Opbevaring af og adgang til oplysninger på din enhed"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"D"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personlige annoncer","Personalisation"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personlige annoncer","Personalisation"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"E"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Annoncevalg, levering og rapportering","Ad selection, delivery, reporting"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Annoncevalg, levering og rapportering","Ad selection, delivery, reporting"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"F"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personalisering af indhold","Content selection, delivery, reporting"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personalisering af indhold","Content selection, delivery, reporting"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"E"},{matcher:{parent:{childFilter:{target:{selector:".as-oil-cpc__purpose-header",textFilter:["Måling","Measurement"]}},selector:".as-oil-cpc__purpose-container"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{childFilter:{target:{selector:".as-oil-cpc__purpose-header",textFilter:["Måling","Measurement"]}},selector:".as-oil-cpc__purpose-container"},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"B"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:"Google"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:"Google"},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"F"}],type:"consent"}],type:"list"},name:"DO_CONSENT"},{action:{target:{selector:".as-oil__btn-optin"},type:"click"},name:"SAVE_CONSENT"},{action:{target:{selector:"div.as-oil"},type:"hide"},name:"HIDE_CMP"}]},optanon:{detectors:[{presentMatcher:{target:{selector:"#optanon-menu, .optanon-alert-box-wrapper"},type:"css"},showingMatcher:{target:{displayFilter:!0,selector:".optanon-alert-box-wrapper"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".optanon-alert-box-wrapper .optanon-toggle-display, a[onclick*='OneTrust.ToggleInfoDisplay()'], a[onclick*='Optanon.ToggleInfoDisplay()']"},type:"click"}],type:"list"},name:"OPEN_OPTIONS"},{action:{actions:[{target:{selector:".preference-menu-item #Your-privacy"},type:"click"},{target:{selector:"#optanon-vendor-consent-text"},type:"click"},{action:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},target:{selector:"#optanon-vendor-consent-list .vendor-item"},type:"foreach"},{target:{selector:".vendor-consent-back-link"},type:"click"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-performance"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-performance"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-functional"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-functional"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-advertising"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-advertising"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-social"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-social"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Social Media Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Social Media Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalisation"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalisation"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Site monitoring cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Site monitoring cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Third party privacy-enhanced content"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Third party privacy-enhanced content"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Performance & Advertising Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Performance & Advertising Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Information storage and access"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Information storage and access"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"D"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad selection, delivery, reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad selection, delivery, reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content selection, delivery, reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content selection, delivery, reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Measurement"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Measurement"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Recommended Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Recommended Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Unclassified Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Unclassified Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Analytical Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Analytical Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Marketing Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Marketing Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalization"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalization"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad Selection, Delivery & Reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad Selection, Delivery & Reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content Selection, Delivery & Reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content Selection, Delivery & Reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"}],type:"list"},name:"DO_CONSENT"},{action:{parent:{selector:".optanon-save-settings-button"},target:{selector:".optanon-white-button-middle"},type:"click"},name:"SAVE_CONSENT"},{action:{actions:[{target:{selector:"#optanon-popup-wrapper"},type:"hide"},{target:{selector:"#optanon-popup-bg"},type:"hide"},{target:{selector:".optanon-alert-box-wrapper"},type:"hide"}],type:"list"},name:"HIDE_CMP"}]},quantcast2:{detectors:[{presentMatcher:{target:{selector:"[data-tracking-opt-in-overlay]"},type:"css"},showingMatcher:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-learn-more]"},type:"css"}}],methods:[{action:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-learn-more]"},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{type:"wait",waitTime:500},{action:{actions:[{target:{selector:"div",textFilter:["Information storage and access"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"D"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Personalization"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"F"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Ad selection, delivery, reporting"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"F"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Content selection, delivery, reporting"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"E"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Measurement"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"B"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Other Partners"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},type:"ifcss"}],type:"list"},parent:{childFilter:{target:{selector:"input"}},selector:"[data-tracking-opt-in-overlay] > div > div"},target:{childFilter:{target:{selector:"input"}},selector:":scope > div"},type:"foreach"}],type:"list"},name:"DO_CONSENT"},{action:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-save]"},type:"click"},name:"SAVE_CONSENT"}]},springer:{detectors:[{presentMatcher:{parent:null,target:{selector:".cmp-app_gdpr"},type:"css"},showingMatcher:{parent:null,target:{displayFilter:!0,selector:".cmp-popup_popup"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".cmp-intro_rejectAll"},type:"click"},{type:"wait",waitTime:250},{target:{selector:".cmp-purposes_purposeItem:not(.cmp-purposes_selectedPurpose)"},type:"click"}],type:"list"},name:"OPEN_OPTIONS"},{action:{consents:[{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Przechowywanie informacji na urządzeniu lub dostęp do nich",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Przechowywanie informacji na urządzeniu lub dostęp do nich",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"D"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór podstawowych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór podstawowych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"F"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"F"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"E"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"E"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Stosowanie badań rynkowych w celu generowania opinii odbiorców",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Stosowanie badań rynkowych w celu generowania opinii odbiorców",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"X"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Opracowywanie i ulepszanie produktów",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Opracowywanie i ulepszanie produktów",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"X"}],type:"consent"},name:"DO_CONSENT"},{action:{target:{selector:".cmp-details_save"},type:"click"},name:"SAVE_CONSENT"}]},wordpressgdpr:{detectors:[{presentMatcher:{parent:null,target:{selector:".wpgdprc-consent-bar"},type:"css"},showingMatcher:{parent:null,target:{displayFilter:!0,selector:".wpgdprc-consent-bar"},type:"css"}}],methods:[{action:{parent:null,target:{selector:".wpgdprc-consent-bar .wpgdprc-consent-bar__settings",textFilter:null},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{target:{selector:".wpgdprc-consent-modal .wpgdprc-button",textFilter:"Eyeota"},type:"click"},{consents:[{description:"Eyeota Cookies",matcher:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Eyeota"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Eyeota"},target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},{target:{selector:".wpgdprc-consent-modal .wpgdprc-button",textFilter:"Advertising"},type:"click"},{consents:[{description:"Advertising Cookies",matcher:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Advertising"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Advertising"},target:{selector:"label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},name:"DO_CONSENT"},{action:{parent:null,target:{selector:".wpgdprc-button",textFilter:"Save my settings"},type:"click"},name:"SAVE_CONSENT"}]}},E={autoconsent:f,consentomatic:A},x=Object.freeze({__proto__:null,autoconsent:f,consentomatic:A,default:E});const O=new class{constructor(e,t=null,o=null){if(this.id=a(),this.rules=[],this.foundCmp=null,this.state={lifecycle:"loading",prehideOn:!1,findCmpAttempts:0,detectedCmps:[],detectedPopups:[],selfTest:null},r.sendContentMessage=e,this.sendContentMessage=e,this.rules=[],this.updateState({lifecycle:"loading"}),this.addDynamicRules(),t)this.initialize(t,o);else{o&&this.parseDeclarativeRules(o);e({type:"init",url:window.location.href}),this.updateState({lifecycle:"waitingForInitResponse"})}this.domActions=new v(this)}initialize(e,t){const o=g(e);if(o.logs.lifecycle&&console.log("autoconsent init",window.location.href),this.config=o,o.enabled){if(t&&this.parseDeclarativeRules(t),this.rules=function(e,t){return e.filter((e=>(!t.disabledCmps||!t.disabledCmps.includes(e.name))&&(t.enableCosmeticRules||!e.isCosmetic)))}(this.rules,o),e.enablePrehide)if(document.documentElement)this.prehideElements();else{const e=()=>{window.removeEventListener("DOMContentLoaded",e),this.prehideElements()};window.addEventListener("DOMContentLoaded",e)}if("loading"===document.readyState){const e=()=>{window.removeEventListener("DOMContentLoaded",e),this.start()};window.addEventListener("DOMContentLoaded",e)}else this.start();this.updateState({lifecycle:"initialized"})}else o.logs.lifecycle&&console.log("autoconsent is disabled")}addDynamicRules(){C.forEach((e=>{this.rules.push(new e(this))}))}parseDeclarativeRules(e){Object.keys(e.consentomatic).forEach((t=>{this.addConsentomaticCMP(t,e.consentomatic[t])})),e.autoconsent.forEach((e=>{this.addDeclarativeCMP(e)}))}addDeclarativeCMP(e){this.rules.push(new u(e,this))}addConsentomaticCMP(e,t){this.rules.push(new m(`com_${e}`,t))}start(){window.requestIdleCallback?window.requestIdleCallback((()=>this._start()),{timeout:500}):this._start()}async _start(){const e=this.config.logs;e.lifecycle&&console.log(`Detecting CMPs on ${window.location.href}`),this.updateState({lifecycle:"started"});const t=await this.findCmp(this.config.detectRetries);if(this.updateState({detectedCmps:t.map((e=>e.name))}),0===t.length)return e.lifecycle&&console.log("no CMP found",location.href),this.config.enablePrehide&&this.undoPrehide(),this.updateState({lifecycle:"nothingDetected"}),!1;this.updateState({lifecycle:"cmpDetected"});const o=[],c=[];for(const e of t)e.isCosmetic?c.push(e):o.push(e);let i=!1,n=await this.detectPopups(o,(async e=>{i=await this.handlePopup(e)}));if(0===n.length&&(n=await this.detectPopups(c,(async e=>{i=await this.handlePopup(e)}))),0===n.length)return e.lifecycle&&console.log("no popup found"),this.config.enablePrehide&&this.undoPrehide(),!1;if(n.length>1){const t={msg:"Found multiple CMPs, check the detection rules.",cmps:n.map((e=>e.name))};e.errors&&console.warn(t.msg,t.cmps),this.sendContentMessage({type:"autoconsentError",details:t})}return i}async findCmp(e){const t=this.config.logs;this.updateState({findCmpAttempts:this.state.findCmpAttempts+1});const o=[];for(const e of this.rules)try{if(!e.checkRunContext())continue;await e.detectCmp()&&(t.lifecycle&&console.log(`Found CMP: ${e.name} ${window.location.href}`),this.sendContentMessage({type:"cmpDetected",url:location.href,cmp:e.name}),o.push(e))}catch(o){t.errors&&console.warn(`error detecting ${e.name}`,o)}return 0===o.length&&e>0?(await this.domActions.wait(500),this.findCmp(e-1)):o}async detectPopup(e){if(await this.waitForPopup(e).catch((t=>(this.config.logs.errors&&console.warn(`error waiting for a popup for ${e.name}`,t),!1))))return this.updateState({detectedPopups:this.state.detectedPopups.concat([e.name])}),this.sendContentMessage({type:"popupFound",cmp:e.name,url:location.href}),e;throw new Error("Popup is not shown")}async detectPopups(e,t){const o=e.map((e=>this.detectPopup(e)));await Promise.any(o).then((e=>{t(e)})).catch((()=>null));const c=await Promise.allSettled(o),i=[];for(const e of c)"fulfilled"===e.status&&i.push(e.value);return i}async handlePopup(e){return this.updateState({lifecycle:"openPopupDetected"}),this.config.enablePrehide&&!this.state.prehideOn&&this.prehideElements(),this.foundCmp=e,"optOut"===this.config.autoAction?await this.doOptOut():"optIn"===this.config.autoAction?await this.doOptIn():(this.config.logs.lifecycle&&console.log("waiting for opt-out signal...",location.href),!0)}async doOptOut(){const e=this.config.logs;let t;return this.updateState({lifecycle:"runningOptOut"}),this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: opt out on ${window.location.href}`),t=await this.foundCmp.optOut(),e.lifecycle&&console.log(`${this.foundCmp.name}: opt out result ${t}`)):(e.errors&&console.log("no CMP to opt out"),t=!1),this.config.enablePrehide&&this.undoPrehide(),this.sendContentMessage({type:"optOutResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,scheduleSelfTest:this.foundCmp&&this.foundCmp.hasSelfTest,url:location.href}),t&&!this.foundCmp.isIntermediate?(this.sendContentMessage({type:"autoconsentDone",cmp:this.foundCmp.name,isCosmetic:this.foundCmp.isCosmetic,url:location.href}),this.updateState({lifecycle:"done"})):this.updateState({lifecycle:t?"optOutSucceeded":"optOutFailed"}),t}async doOptIn(){const e=this.config.logs;let t;return this.updateState({lifecycle:"runningOptIn"}),this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: opt in on ${window.location.href}`),t=await this.foundCmp.optIn(),e.lifecycle&&console.log(`${this.foundCmp.name}: opt in result ${t}`)):(e.errors&&console.log("no CMP to opt in"),t=!1),this.config.enablePrehide&&this.undoPrehide(),this.sendContentMessage({type:"optInResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,scheduleSelfTest:!1,url:location.href}),t&&!this.foundCmp.isIntermediate?(this.sendContentMessage({type:"autoconsentDone",cmp:this.foundCmp.name,isCosmetic:this.foundCmp.isCosmetic,url:location.href}),this.updateState({lifecycle:"done"})):this.updateState({lifecycle:t?"optInSucceeded":"optInFailed"}),t}async doSelfTest(){const e=this.config.logs;let t;return this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: self-test on ${window.location.href}`),t=await this.foundCmp.test()):(e.errors&&console.log("no CMP to self test"),t=!1),this.sendContentMessage({type:"selfTestResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,url:location.href}),this.updateState({selfTest:t}),t}async waitForPopup(e,t=5,o=500){const c=this.config.logs;c.lifecycle&&console.log("checking if popup is open...",e.name);const i=await e.detectPopup().catch((t=>(c.errors&&console.warn(`error detecting popup for ${e.name}`,t),!1)));return!i&&t>0?(await this.domActions.wait(o),this.waitForPopup(e,t-1,o)):(c.lifecycle&&console.log(e.name,"popup is "+(i?"open":"not open")),i)}prehideElements(){const e=this.config.logs,t=this.rules.filter((e=>e.prehideSelectors&&e.checkRunContext())).reduce(((e,t)=>[...e,...t.prehideSelectors]),["#didomi-popup,.didomi-popup-container,.didomi-popup-notice,.didomi-consent-popup-preferences,#didomi-notice,.didomi-popup-backdrop,.didomi-screen-medium"]);return this.updateState({prehideOn:!0}),setTimeout((()=>{this.config.enablePrehide&&this.state.prehideOn&&!["runningOptOut","runningOptIn"].includes(this.state.lifecycle)&&(e.lifecycle&&console.log("Process is taking too long, unhiding elements"),this.undoPrehide())}),this.config.prehideTimeout||2e3),this.domActions.prehide(t.join(","))}undoPrehide(){return this.updateState({prehideOn:!1}),this.domActions.undoPrehide()}updateState(e){Object.assign(this.state,e),this.sendContentMessage({type:"report",instanceId:this.id,url:window.location.href,mainFrame:window.top===window.self,state:this.state})}async receiveMessageCallback(e){const t=this.config?.logs;switch(t?.messages&&console.log("received from background",e,window.location.href),e.type){case"initResp":this.initialize(e.config,e.rules);break;case"optIn":await this.doOptIn();break;case"optOut":await this.doOptOut();break;case"selfTest":await this.doSelfTest();break;case"evalResp":!function(e,t){const o=r.pending.get(e);o?(r.pending.delete(e),o.timer&&window.clearTimeout(o.timer),o.resolve(t)):console.warn("no eval #",e)}(e.id,e.result)}}}((e=>{window.webkit.messageHandlers[e.type]&&window.webkit.messageHandlers[e.type].postMessage(e).then((e=>{O.receiveMessageCallback(e)}))}),null,x);window.autoconsentMessageCallback=e=>{O.receiveMessageCallback(e)}}(); +!function(){"use strict";var e=class e{static setBase(t){e.base=t}static findElement(t,o=null,c=!1){let i=null;return i=null!=o?Array.from(o.querySelectorAll(t.selector)):null!=e.base?Array.from(e.base.querySelectorAll(t.selector)):Array.from(document.querySelectorAll(t.selector)),null!=t.textFilter&&(i=i.filter((e=>{const o=e.textContent.toLowerCase();if(Array.isArray(t.textFilter)){let e=!1;for(const c of t.textFilter)if(-1!==o.indexOf(c.toLowerCase())){e=!0;break}return e}if(null!=t.textFilter)return-1!==o.indexOf(t.textFilter.toLowerCase())}))),null!=t.styleFilters&&(i=i.filter((e=>{const o=window.getComputedStyle(e);let c=!0;for(const e of t.styleFilters){const t=o[e.option];c=e.negated?c&&t!==e.value:c&&t===e.value}return c}))),null!=t.displayFilter&&(i=i.filter((e=>t.displayFilter?0!==e.offsetHeight:0===e.offsetHeight))),null!=t.iframeFilter&&(i=i.filter((()=>t.iframeFilter?window.location!==window.parent.location:window.location===window.parent.location))),null!=t.childFilter&&(i=i.filter((o=>{const c=e.base;e.setBase(o);const i=e.find(t.childFilter);return e.setBase(c),null!=i.target}))),c?i:(i.length>1&&console.warn("Multiple possible targets: ",i,t,o),i[0])}static find(t,o=!1){const c=[];if(null!=t.parent){const i=e.findElement(t.parent,null,o);if(null!=i){if(i instanceof Array)return i.forEach((i=>{const n=e.findElement(t.target,i,o);n instanceof Array?n.forEach((e=>{c.push({parent:i,target:e})})):c.push({parent:i,target:n})})),c;{const n=e.findElement(t.target,i,o);n instanceof Array?n.forEach((e=>{c.push({parent:i,target:e})})):c.push({parent:i,target:n})}}}else{const i=e.findElement(t.target,null,o);i instanceof Array?i.forEach((e=>{c.push({parent:null,target:e})})):c.push({parent:null,target:i})}return 0===c.length&&c.push({parent:null,target:null}),o?c:(1!==c.length&&console.warn("Multiple results found, even though multiple false",c),c[0])}};e.base=null;var t=e;function o(e){const o=t.find(e);return"css"===e.type?!!o.target:"checkbox"===e.type?!!o.target&&o.target.checked:void 0}async function c(e,a){switch(e.type){case"click":return async function(e){const o=t.find(e);null!=o.target&&o.target.click();return n(i)}(e);case"list":return async function(e,t){for(const o of e.actions)await c(o,t)}(e,a);case"consent":return async function(e,t){for(const i of e.consents){const e=-1!==t.indexOf(i.type);if(i.matcher&&i.toggleAction){o(i.matcher)!==e&&await c(i.toggleAction)}else e?await c(i.trueAction):await c(i.falseAction)}}(e,a);case"ifcss":return async function(e,o){const i=t.find(e);i.target?e.falseAction&&await c(e.falseAction,o):e.trueAction&&await c(e.trueAction,o)}(e,a);case"waitcss":return async function(e){await new Promise((o=>{let c=e.retries||10;const i=e.waitTime||250,n=()=>{const a=t.find(e);(e.negated&&a.target||!e.negated&&!a.target)&&c>0?(c-=1,setTimeout(n,i)):o()};n()}))}(e);case"foreach":return async function(e,o){const i=t.find(e,!0),n=t.base;for(const n of i)n.target&&(t.setBase(n.target),await c(e.action,o));t.setBase(n)}(e,a);case"hide":return async function(e){const o=t.find(e);o.target&&o.target.classList.add("Autoconsent-Hidden")}(e);case"slide":return async function(e){const o=t.find(e),c=t.find(e.dragTarget);if(o.target){const e=o.target.getBoundingClientRect(),t=c.target.getBoundingClientRect();let i=t.top-e.top,n=t.left-e.left;"y"===this.config.axis.toLowerCase()&&(n=0),"x"===this.config.axis.toLowerCase()&&(i=0);const a=window.screenX+e.left+e.width/2,s=window.screenY+e.top+e.height/2,r=e.left+e.width/2,l=e.top+e.height/2,p=document.createEvent("MouseEvents");p.initMouseEvent("mousedown",!0,!0,window,0,a,s,r,l,!1,!1,!1,!1,0,o.target);const d=document.createEvent("MouseEvents");d.initMouseEvent("mousemove",!0,!0,window,0,a+n,s+i,r+n,l+i,!1,!1,!1,!1,0,o.target);const u=document.createEvent("MouseEvents");u.initMouseEvent("mouseup",!0,!0,window,0,a+n,s+i,r+n,l+i,!1,!1,!1,!1,0,o.target),o.target.dispatchEvent(p),await this.waitTimeout(10),o.target.dispatchEvent(d),await this.waitTimeout(10),o.target.dispatchEvent(u)}}(e);case"close":return async function(){window.close()}();case"wait":return async function(e){await n(e.waitTime)}(e);case"eval":return async function(e){return console.log("eval!",e.code),new Promise((t=>{try{e.async?(window.eval(e.code),setTimeout((()=>{t(window.eval("window.__consentCheckResult"))}),e.timeout||250)):t(window.eval(e.code))}catch(o){console.warn("eval error",o,e.code),t(!1)}}))}(e);default:throw"Unknown action type: "+e.type}}var i=0;function n(e){return new Promise((t=>{setTimeout((()=>{t()}),e)}))}function a(){return crypto&&void 0!==crypto.randomUUID?crypto.randomUUID():Math.random().toString()}var s=class{constructor(e,t=1e3){this.id=e,this.promise=new Promise(((e,t)=>{this.resolve=e,this.reject=t})),this.timer=window.setTimeout((()=>{this.reject(new Error("timeout"))}),t)}},r={pending:new Map,sendContentMessage:null};var l={EVAL_0:()=>console.log(1),EVAL_CONSENTMANAGER_1:()=>window.__cmp&&"object"==typeof __cmp("getCMPData"),EVAL_CONSENTMANAGER_2:()=>!__cmp("consentStatus").userChoiceExists,EVAL_CONSENTMANAGER_3:()=>__cmp("setConsent",0),EVAL_CONSENTMANAGER_4:()=>__cmp("setConsent",1),EVAL_CONSENTMANAGER_5:()=>__cmp("consentStatus").userChoiceExists,EVAL_COOKIEBOT_1:()=>!!window.Cookiebot,EVAL_COOKIEBOT_2:()=>!window.Cookiebot.hasResponse&&!0===window.Cookiebot.dialog?.visible,EVAL_COOKIEBOT_3:()=>window.Cookiebot.withdraw()||!0,EVAL_COOKIEBOT_4:()=>window.Cookiebot.hide()||!0,EVAL_COOKIEBOT_5:()=>!0===window.Cookiebot.declined,EVAL_KLARO_1:()=>{const e=globalThis.klaroConfig||globalThis.klaro?.getManager&&globalThis.klaro.getManager().config;if(!e)return!0;const t=(e.services||e.apps).filter((e=>!e.required)).map((e=>e.name));if(klaro&&klaro.getManager){const e=klaro.getManager();return t.every((t=>!e.consents[t]))}if(klaroConfig&&"cookie"===klaroConfig.storageMethod){const e=klaroConfig.cookieName||klaroConfig.storageName,o=JSON.parse(decodeURIComponent(document.cookie.split(";").find((t=>t.trim().startsWith(e))).split("=")[1]));return Object.keys(o).filter((e=>t.includes(e))).every((e=>!1===o[e]))}},EVAL_KLARO_OPEN_POPUP:()=>{klaro.show(void 0,!0)},EVAL_KLARO_TRY_API_OPT_OUT:()=>{if(window.klaro&&"function"==typeof klaro.show&&"function"==typeof klaro.getManager)try{return klaro.getManager().changeAll(!1),klaro.getManager().saveAndApplyConsents(),!0}catch(e){return console.warn(e),!1}return!1},EVAL_ONETRUST_1:()=>window.OnetrustActiveGroups.split(",").filter((e=>e.length>0)).length<=1,EVAL_TRUSTARC_TOP:()=>window&&window.truste&&"0"===window.truste.eu.bindMap.prefCookie,EVAL_TRUSTARC_FRAME_TEST:()=>window&&window.QueryString&&"0"===window.QueryString.preferences,EVAL_TRUSTARC_FRAME_GTM:()=>window&&window.QueryString&&"1"===window.QueryString.gtm,EVAL_ADROLL_0:()=>!document.cookie.includes("__adroll_fpc"),EVAL_ALMACMP_0:()=>document.cookie.includes('"name":"Google","consent":false'),EVAL_AFFINITY_SERIF_COM_0:()=>document.cookie.includes("serif_manage_cookies_viewed")&&!document.cookie.includes("serif_allow_analytics"),EVAL_ARBEITSAGENTUR_TEST:()=>document.cookie.includes("cookie_consent=denied"),EVAL_AXEPTIO_0:()=>document.cookie.includes("axeptio_authorized_vendors=%2C%2C"),EVAL_BAHN_TEST:()=>1===utag.gdpr.getSelectedCategories().length,EVAL_BING_0:()=>document.cookie.includes("AL=0")&&document.cookie.includes("AD=0")&&document.cookie.includes("SM=0"),EVAL_BLOCKSY_0:()=>document.cookie.includes("blocksy_cookies_consent_accepted=no"),EVAL_BORLABS_0:()=>!JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>-1!==e.indexOf("borlabs-cookie"))).split("=",2)[1])).consents.statistics,EVAL_BUNDESREGIERUNG_DE_0:()=>document.cookie.match("cookie-allow-tracking=0"),EVAL_CANVA_0:()=>!document.cookie.includes("gtm_fpc_engagement_event"),EVAL_CC_BANNER2_0:()=>!!document.cookie.match(/sncc=[^;]+D%3Dtrue/),EVAL_CLICKIO_0:()=>document.cookie.includes("__lxG__consent__v2_daisybit="),EVAL_CLINCH_0:()=>document.cookie.includes("ctc_rejected=1"),EVAL_COOKIECONSENT2_TEST:()=>document.cookie.includes("cc_cookie="),EVAL_COOKIECONSENT3_TEST:()=>document.cookie.includes("cc_cookie="),EVAL_COINBASE_0:()=>JSON.parse(decodeURIComponent(document.cookie.match(/cm_(eu|default)_preferences=([0-9a-zA-Z\\{\\}\\[\\]%:]*);?/)[2])).consent.length<=1,EVAL_COMPLIANZ_BANNER_0:()=>document.cookie.includes("cmplz_banner-status=dismissed"),EVAL_COOKIE_LAW_INFO_0:()=>CLI.disableAllCookies()||CLI.reject_close()||!0,EVAL_COOKIE_LAW_INFO_1:()=>-1===document.cookie.indexOf("cookielawinfo-checkbox-non-necessary=yes"),EVAL_COOKIE_LAW_INFO_DETECT:()=>!!window.CLI,EVAL_COOKIE_MANAGER_POPUP_0:()=>!1===JSON.parse(document.cookie.split(";").find((e=>e.trim().startsWith("CookieLevel"))).split("=")[1]).social,EVAL_COOKIEALERT_0:()=>document.querySelector("body").removeAttribute("style")||!0,EVAL_COOKIEALERT_1:()=>document.querySelector("body").removeAttribute("style")||!0,EVAL_COOKIEALERT_2:()=>!0===window.CookieConsent.declined,EVAL_COOKIEFIRST_0:()=>{return!1===(e=JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>-1!==e.indexOf("cookiefirst"))).trim()).split("=")[1])).performance&&!1===e.functional&&!1===e.advertising;var e},EVAL_COOKIEFIRST_1:()=>document.querySelectorAll("button[data-cookiefirst-accent-color=true][role=checkbox]:not([disabled])").forEach((e=>"true"==e.getAttribute("aria-checked")&&e.click()))||!0,EVAL_COOKIEINFORMATION_0:()=>CookieInformation.declineAllCategories()||!0,EVAL_COOKIEINFORMATION_1:()=>CookieInformation.submitAllCategories()||!0,EVAL_COOKIEINFORMATION_2:()=>document.cookie.includes("CookieInformationConsent="),EVAL_COOKIEYES_0:()=>document.cookie.includes("advertisement:no"),EVAL_DAILYMOTION_0:()=>!!document.cookie.match("dm-euconsent-v2"),EVAL_DNDBEYOND_TEST:()=>document.cookie.includes("cookie-consent=denied"),EVAL_DSGVO_0:()=>!document.cookie.includes("sp_dsgvo_cookie_settings"),EVAL_DUNELM_0:()=>document.cookie.includes("cc_functional=0")&&document.cookie.includes("cc_targeting=0"),EVAL_ETSY_0:()=>document.querySelectorAll(".gdpr-overlay-body input").forEach((e=>{e.checked=!1}))||!0,EVAL_ETSY_1:()=>document.querySelector(".gdpr-overlay-view button[data-wt-overlay-close]").click()||!0,EVAL_EU_COOKIE_COMPLIANCE_0:()=>-1===document.cookie.indexOf("cookie-agreed=2"),EVAL_EU_COOKIE_LAW_0:()=>!document.cookie.includes("euCookie"),EVAL_EZOIC_0:()=>ezCMP.handleAcceptAllClick(),EVAL_EZOIC_1:()=>!!document.cookie.match(/ez-consent-tcf/),EVAL_GOOGLE_0:()=>!!document.cookie.match(/SOCS=CAE/),EVAL_HEMA_TEST_0:()=>document.cookie.includes("cookies_rejected=1"),EVAL_IUBENDA_0:()=>document.querySelectorAll(".purposes-item input[type=checkbox]:not([disabled])").forEach((e=>{e.checked&&e.click()}))||!0,EVAL_IUBENDA_1:()=>!!document.cookie.match(/_iub_cs-\d+=/),EVAL_IWINK_TEST:()=>document.cookie.includes("cookie_permission_granted=no"),EVAL_JQUERY_COOKIEBAR_0:()=>!document.cookie.includes("cookies-state=accepted"),EVAL_MEDIAVINE_0:()=>document.querySelectorAll('[data-name="mediavine-gdpr-cmp"] input[type=checkbox]').forEach((e=>e.checked&&e.click()))||!0,EVAL_MICROSOFT_0:()=>Array.from(document.querySelectorAll("div > button")).filter((e=>e.innerText.match("Reject|Ablehnen")))[0].click()||!0,EVAL_MICROSOFT_1:()=>Array.from(document.querySelectorAll("div > button")).filter((e=>e.innerText.match("Accept|Annehmen")))[0].click()||!0,EVAL_MICROSOFT_2:()=>!!document.cookie.match("MSCC|GHCC"),EVAL_MOOVE_0:()=>document.querySelectorAll("#moove_gdpr_cookie_modal input").forEach((e=>{e.disabled||(e.checked="moove_gdpr_strict_cookies"===e.name||"moove_gdpr_strict_cookies"===e.id)}))||!0,EVAL_ONENINETWO_0:()=>document.cookie.includes("CC_ADVERTISING=NO")&&document.cookie.includes("CC_ANALYTICS=NO"),EVAL_OPERA_0:()=>document.cookie.includes("cookie_consent_essential=true")&&!document.cookie.includes("cookie_consent_marketing=true"),EVAL_PAYPAL_0:()=>!0===document.cookie.includes("cookie_prefs"),EVAL_PRIMEBOX_0:()=>!document.cookie.includes("cb-enabled=accepted"),EVAL_PUBTECH_0:()=>document.cookie.includes("euconsent-v2")&&(document.cookie.match(/.YAAAAAAAAAAA/)||document.cookie.match(/.aAAAAAAAAAAA/)||document.cookie.match(/.YAAACFgAAAAA/)),EVAL_REDDIT_0:()=>document.cookie.includes("eu_cookie={%22opted%22:true%2C%22nonessential%22:false}"),EVAL_SIRDATA_UNBLOCK_SCROLL:()=>(document.documentElement.classList.forEach((e=>{e.startsWith("sd-cmp-")&&document.documentElement.classList.remove(e)})),!0),EVAL_SNIGEL_0:()=>!!document.cookie.match("snconsent"),EVAL_STEAMPOWERED_0:()=>2===JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>e.trim().startsWith("cookieSettings"))).split("=")[1])).preference_state,EVAL_SVT_TEST:()=>document.cookie.includes('cookie-consent-1={"optedIn":true,"functionality":false,"statistics":false}'),EVAL_TAKEALOT_0:()=>document.body.classList.remove("freeze")||(document.body.style="")||!0,EVAL_TARTEAUCITRON_0:()=>tarteaucitron.userInterface.respondAll(!1)||!0,EVAL_TARTEAUCITRON_1:()=>tarteaucitron.userInterface.respondAll(!0)||!0,EVAL_TARTEAUCITRON_2:()=>document.cookie.match(/tarteaucitron=[^;]*/)?.[0].includes("false"),EVAL_TAUNTON_TEST:()=>document.cookie.includes("taunton_user_consent_submitted=true"),EVAL_TEALIUM_0:()=>void 0!==window.utag&&"object"==typeof utag.gdpr,EVAL_TEALIUM_1:()=>utag.gdpr.setConsentValue(!1)||!0,EVAL_TEALIUM_DONOTSELL:()=>utag.gdpr.dns?.setDnsState(!1)||!0,EVAL_TEALIUM_2:()=>utag.gdpr.setConsentValue(!0)||!0,EVAL_TEALIUM_3:()=>1!==utag.gdpr.getConsentState(),EVAL_TEALIUM_DONOTSELL_CHECK:()=>1!==utag.gdpr.dns?.getDnsState(),EVAL_TESTCMP_0:()=>"button_clicked"===window.results.results[0],EVAL_TESTCMP_COSMETIC_0:()=>"banner_hidden"===window.results.results[0],EVAL_THEFREEDICTIONARY_0:()=>cmpUi.showPurposes()||cmpUi.rejectAll()||!0,EVAL_THEFREEDICTIONARY_1:()=>cmpUi.allowAll()||!0,EVAL_THEVERGE_0:()=>document.cookie.includes("_duet_gdpr_acknowledged=1"),EVAL_UBUNTU_COM_0:()=>document.cookie.includes("_cookies_accepted=essential"),EVAL_UK_COOKIE_CONSENT_0:()=>!document.cookie.includes("catAccCookies"),EVAL_USERCENTRICS_API_0:()=>"object"==typeof UC_UI,EVAL_USERCENTRICS_API_1:()=>!!UC_UI.closeCMP(),EVAL_USERCENTRICS_API_2:()=>!!UC_UI.denyAllConsents(),EVAL_USERCENTRICS_API_3:()=>!!UC_UI.acceptAllConsents(),EVAL_USERCENTRICS_API_4:()=>!!UC_UI.closeCMP(),EVAL_USERCENTRICS_API_5:()=>!0===UC_UI.areAllConsentsAccepted(),EVAL_USERCENTRICS_API_6:()=>!1===UC_UI.areAllConsentsAccepted(),EVAL_USERCENTRICS_BUTTON_0:()=>JSON.parse(localStorage.getItem("usercentrics")).consents.every((e=>e.isEssential||!e.consentStatus)),EVAL_WAITROSE_0:()=>Array.from(document.querySelectorAll("label[id$=cookies-deny-label]")).forEach((e=>e.click()))||!0,EVAL_WAITROSE_1:()=>document.cookie.includes("wtr_cookies_advertising=0")&&document.cookie.includes("wtr_cookies_analytics=0"),EVAL_WP_COOKIE_NOTICE_0:()=>document.cookie.includes("wpl_viewed_cookie=no"),EVAL_XE_TEST:()=>document.cookie.includes("xeConsentState={%22performance%22:false%2C%22marketing%22:false%2C%22compliance%22:false}"),EVAL_XING_0:()=>document.cookie.includes("userConsent=%7B%22marketing%22%3Afalse"),EVAL_YOUTUBE_DESKTOP_0:()=>!!document.cookie.match(/SOCS=CAE/),EVAL_YOUTUBE_MOBILE_0:()=>!!document.cookie.match(/SOCS=CAE/)};var p={main:!0,frame:!1,urlPattern:""},d=class{constructor(e){this.runContext=p,this.autoconsent=e}get hasSelfTest(){throw new Error("Not Implemented")}get isIntermediate(){throw new Error("Not Implemented")}get isCosmetic(){throw new Error("Not Implemented")}mainWorldEval(e){const t=l[e];if(!t)return console.warn("Snippet not found",e),Promise.resolve(!1);const o=this.autoconsent.config.logs;if(this.autoconsent.config.isMainWorld){o.evals&&console.log("inline eval:",e,t);let c=!1;try{c=!!t.call(globalThis)}catch(t){o.evals&&console.error("error evaluating rule",e,t)}return Promise.resolve(c)}const c=`(${t.toString()})()`;return o.evals&&console.log("async eval:",e,c),function(e,t){const o=a();r.sendContentMessage({type:"eval",id:o,code:e,snippetId:t});const c=new s(o);return r.pending.set(c.id,c),c.promise}(c,e).catch((t=>(o.evals&&console.error("error evaluating rule",e,t),!1)))}checkRunContext(){const e={...p,...this.runContext},t=window.top===window;return!(t&&!e.main)&&(!(!t&&!e.frame)&&!(e.urlPattern&&!window.location.href.match(e.urlPattern)))}detectCmp(){throw new Error("Not Implemented")}async detectPopup(){return!1}optOut(){throw new Error("Not Implemented")}optIn(){throw new Error("Not Implemented")}openCmp(){throw new Error("Not Implemented")}async test(){return Promise.resolve(!0)}click(e,t=!1){return this.autoconsent.domActions.click(e,t)}elementExists(e){return this.autoconsent.domActions.elementExists(e)}elementVisible(e,t){return this.autoconsent.domActions.elementVisible(e,t)}waitForElement(e,t){return this.autoconsent.domActions.waitForElement(e,t)}waitForVisible(e,t,o){return this.autoconsent.domActions.waitForVisible(e,t,o)}waitForThenClick(e,t,o){return this.autoconsent.domActions.waitForThenClick(e,t,o)}wait(e){return this.autoconsent.domActions.wait(e)}hide(e,t){return this.autoconsent.domActions.hide(e,t)}prehide(e){return this.autoconsent.domActions.prehide(e)}undoPrehide(){return this.autoconsent.domActions.undoPrehide()}querySingleReplySelector(e,t){return this.autoconsent.domActions.querySingleReplySelector(e,t)}querySelectorChain(e){return this.autoconsent.domActions.querySelectorChain(e)}elementSelector(e){return this.autoconsent.domActions.elementSelector(e)}},u=class extends d{constructor(e,t){super(t),this.rule=e,this.name=e.name,this.runContext=e.runContext||p}get hasSelfTest(){return!!this.rule.test}get isIntermediate(){return!!this.rule.intermediate}get isCosmetic(){return!!this.rule.cosmetic}get prehideSelectors(){return this.rule.prehideSelectors}async detectCmp(){return!!this.rule.detectCmp&&this._runRulesParallel(this.rule.detectCmp)}async detectPopup(){return!!this.rule.detectPopup&&this._runRulesSequentially(this.rule.detectPopup)}async optOut(){const e=this.autoconsent.config.logs;return!!this.rule.optOut&&(e.lifecycle&&console.log("Initiated optOut()",this.rule.optOut),this._runRulesSequentially(this.rule.optOut))}async optIn(){const e=this.autoconsent.config.logs;return!!this.rule.optIn&&(e.lifecycle&&console.log("Initiated optIn()",this.rule.optIn),this._runRulesSequentially(this.rule.optIn))}async openCmp(){return!!this.rule.openCmp&&this._runRulesSequentially(this.rule.openCmp)}async test(){return this.hasSelfTest?this._runRulesSequentially(this.rule.test):super.test()}async evaluateRuleStep(e){const t=[],o=this.autoconsent.config.logs;if(e.exists&&t.push(this.elementExists(e.exists)),e.visible&&t.push(this.elementVisible(e.visible,e.check)),e.eval){const o=this.mainWorldEval(e.eval);t.push(o)}if(e.waitFor&&t.push(this.waitForElement(e.waitFor,e.timeout)),e.waitForVisible&&t.push(this.waitForVisible(e.waitForVisible,e.timeout,e.check)),e.click&&t.push(this.click(e.click,e.all)),e.waitForThenClick&&t.push(this.waitForThenClick(e.waitForThenClick,e.timeout,e.all)),e.wait&&t.push(this.wait(e.wait)),e.hide&&t.push(this.hide(e.hide,e.method)),e.if){if(!e.if.exists&&!e.if.visible)return console.error("invalid conditional rule",e.if),!1;const c=await this.evaluateRuleStep(e.if);o.rulesteps&&console.log("Condition is",c),c?t.push(this._runRulesSequentially(e.then)):e.else?t.push(this._runRulesSequentially(e.else)):t.push(!0)}if(e.any){for(const t of e.any)if(await this.evaluateRuleStep(t))return!0;return!1}if(0===t.length)return o.errors&&console.warn("Unrecognized rule",e),!1;return(await Promise.all(t)).reduce(((e,t)=>e&&t),!0)}async _runRulesParallel(e){const t=e.map((e=>this.evaluateRuleStep(e)));return(await Promise.all(t)).every((e=>!!e))}async _runRulesSequentially(e){const t=this.autoconsent.config.logs;for(const o of e){t.rulesteps&&console.log("Running rule...",o);const e=await this.evaluateRuleStep(o);if(t.rulesteps&&console.log("...rule result",e),!e&&!o.optional)return!1}return!0}},m=class{constructor(e,t){this.name=e,this.config=t,this.methods=new Map,this.runContext=p,this.isCosmetic=!1,t.methods.forEach((e=>{e.action&&this.methods.set(e.name,e.action)})),this.hasSelfTest=!1}get isIntermediate(){return!1}checkRunContext(){return!0}async detectCmp(){return this.config.detectors.map((e=>o(e.presentMatcher))).some((e=>!!e))}async detectPopup(){return this.config.detectors.map((e=>o(e.showingMatcher))).some((e=>!!e))}async executeAction(e,t){return!this.methods.has(e)||c(this.methods.get(e),t)}async optOut(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),await this.executeAction("HIDE_CMP"),await this.executeAction("DO_CONSENT",[]),await this.executeAction("SAVE_CONSENT"),!0}async optIn(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),await this.executeAction("HIDE_CMP"),await this.executeAction("DO_CONSENT",["D","A","B","E","F","X"]),await this.executeAction("SAVE_CONSENT"),!0}async openCmp(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),!0}async test(){return!0}};function h(e="autoconsent-css-rules"){const t=`style#${e}`,o=document.querySelector(t);if(o&&o instanceof HTMLStyleElement)return o;{const t=document.head||document.getElementsByTagName("head")[0]||document.documentElement,o=document.createElement("style");return o.id=e,t.appendChild(o),o}}function k(e,t,o="display"){const c=`${t} { ${"opacity"===o?"opacity: 0":"display: none"} !important; z-index: -1 !important; pointer-events: none !important; } `;return e instanceof HTMLStyleElement&&(e.innerText+=c,t.length>0)}async function b(e,t,o){const c=await e();return!c&&t>0?new Promise((c=>{setTimeout((async()=>{c(b(e,t-1,o))}),o)})):Promise.resolve(c)}function _(e){if(!e)return!1;if(null!==e.offsetParent)return!0;{const t=window.getComputedStyle(e);if("fixed"===t.position&&"none"!==t.display)return!0}return!1}function g(e){const t={enabled:!0,autoAction:"optOut",disabledCmps:[],enablePrehide:!0,enableCosmeticRules:!0,detectRetries:20,isMainWorld:!1,prehideTimeout:2e3,logs:{lifecycle:!1,rulesteps:!1,evals:!1,errors:!0,messages:!1}},o=(c=t,globalThis.structuredClone?structuredClone(c):JSON.parse(JSON.stringify(c)));var c;for(const c of Object.keys(t))void 0!==e[c]&&(o[c]=e[c]);return o}var y="#truste-show-consent",w="#truste-consent-track",C=[class extends d{constructor(e){super(e),this.name="TrustArc-top",this.prehideSelectors=[".trustarc-banner-container",`.truste_popframe,.truste_overlay,.truste_box_overlay,${w}`],this.runContext={main:!0,frame:!1},this._shortcutButton=null,this._optInDone=!1}get hasSelfTest(){return!0}get isIntermediate(){return!this._optInDone&&!this._shortcutButton}get isCosmetic(){return!1}async detectCmp(){const e=this.elementExists(`${y},${w}`);return e&&(this._shortcutButton=document.querySelector("#truste-consent-required")),e}async detectPopup(){return this.elementVisible(`#truste-consent-content,#trustarc-banner-overlay,${w}`,"all")}openFrame(){this.click(y)}async optOut(){return this._shortcutButton?(this._shortcutButton.click(),!0):(k(h(),`.truste_popframe, .truste_overlay, .truste_box_overlay, ${w}`),this.click(y),setTimeout((()=>{h().remove()}),1e4),!0)}async optIn(){return this._optInDone=!0,this.click("#truste-consent-button")}async openCmp(){return!0}async test(){return await this.wait(500),await this.mainWorldEval("EVAL_TRUSTARC_TOP")}},class extends d{constructor(){super(...arguments),this.name="TrustArc-frame",this.runContext={main:!1,frame:!0,urlPattern:"^https://consent-pref\\.trustarc\\.com/\\?"}}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return!0}async detectPopup(){return this.elementVisible("#defaultpreferencemanager","any")&&this.elementVisible(".mainContent","any")}async navigateToSettings(){return await b((async()=>this.elementExists(".shp")||this.elementVisible(".advance","any")||this.elementExists(".switch span:first-child")),10,500),this.elementExists(".shp")&&this.click(".shp"),await this.waitForElement(".prefPanel",5e3),this.elementVisible(".advance","any")&&this.click(".advance"),await b((()=>this.elementVisible(".switch span:first-child","any")),5,1e3)}async optOut(){if(await this.mainWorldEval("EVAL_TRUSTARC_FRAME_TEST"))return!0;let e=3e3;return await this.mainWorldEval("EVAL_TRUSTARC_FRAME_GTM")&&(e=1500),await b((()=>"complete"===document.readyState),20,100),await this.waitForElement(".mainContent[aria-hidden=false]",e),!!this.click(".rejectAll")||(this.elementExists(".prefPanel")&&await this.waitForElement('.prefPanel[style="visibility: visible;"]',e),this.click("#catDetails0")?(this.click(".submit"),this.waitForThenClick("#gwt-debug-close_id",e),!0):this.click(".required")?(this.waitForThenClick("#gwt-debug-close_id",e),!0):(await this.navigateToSettings(),this.click(".switch span:nth-child(1):not(.active)",!0),this.click(".submit"),this.waitForThenClick("#gwt-debug-close_id",10*e),!0))}async optIn(){return this.click(".call")||(await this.navigateToSettings(),this.click(".switch span:nth-child(2)",!0),this.click(".submit"),this.waitForElement("#gwt-debug-close_id",3e5).then((()=>{this.click("#gwt-debug-close_id")}))),!0}async test(){return await this.wait(500),await this.mainWorldEval("EVAL_TRUSTARC_FRAME_TEST")}},class extends d{constructor(){super(...arguments),this.name="Cybotcookiebot",this.prehideSelectors=["#CybotCookiebotDialog,#CybotCookiebotDialogBodyUnderlay,#dtcookie-container,#cookiebanner,#cb-cookieoverlay,.modal--cookie-banner,#cookiebanner_outer,#CookieBanner"]}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return await this.mainWorldEval("EVAL_COOKIEBOT_1")}async detectPopup(){return this.mainWorldEval("EVAL_COOKIEBOT_2")}async optOut(){await this.wait(500);let e=await this.mainWorldEval("EVAL_COOKIEBOT_3");return await this.wait(500),e=e&&await this.mainWorldEval("EVAL_COOKIEBOT_4"),e}async optIn(){return this.elementExists("#dtcookie-container")?this.click(".h-dtcookie-accept"):(this.click(".CybotCookiebotDialogBodyLevelButton:not(:checked):enabled",!0),this.click("#CybotCookiebotDialogBodyLevelButtonAccept"),this.click("#CybotCookiebotDialogBodyButtonAccept"),!0)}async test(){return await this.wait(500),await this.mainWorldEval("EVAL_COOKIEBOT_5")}},class extends d{constructor(){super(...arguments),this.name="Sourcepoint-frame",this.prehideSelectors=["div[id^='sp_message_container_'],.message-overlay","#sp_privacy_manager_container"],this.ccpaNotice=!1,this.ccpaPopup=!1,this.runContext={main:!0,frame:!0}}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){const e=new URL(location.href);return e.searchParams.has("message_id")&&"ccpa-notice.sp-prod.net"===e.hostname?(this.ccpaNotice=!0,!0):"ccpa-pm.sp-prod.net"===e.hostname?(this.ccpaPopup=!0,!0):("/index.html"===e.pathname||"/privacy-manager/index.html"===e.pathname||"/ccpa_pm/index.html"===e.pathname)&&(e.searchParams.has("message_id")||e.searchParams.has("requestUUID")||e.searchParams.has("consentUUID"))}async detectPopup(){return!!this.ccpaNotice||(this.ccpaPopup?await this.waitForElement(".priv-save-btn",2e3):(await this.waitForElement(".sp_choice_type_11,.sp_choice_type_12,.sp_choice_type_13,.sp_choice_type_ACCEPT_ALL,.sp_choice_type_SAVE_AND_EXIT",2e3),!this.elementExists(".sp_choice_type_9")))}async optIn(){return await this.waitForElement(".sp_choice_type_11,.sp_choice_type_ACCEPT_ALL",2e3),!!this.click(".sp_choice_type_11")||!!this.click(".sp_choice_type_ACCEPT_ALL")}isManagerOpen(){return"/privacy-manager/index.html"===location.pathname||"/ccpa_pm/index.html"===location.pathname}async optOut(){const e=this.autoconsent.config.logs;if(this.ccpaPopup){const e=document.querySelectorAll(".priv-purpose-container .sp-switch-arrow-block a.neutral.on .right");for(const t of e)t.click();const t=document.querySelectorAll(".priv-purpose-container .sp-switch-arrow-block a.switch-bg.on");for(const e of t)e.click();return this.click(".priv-save-btn")}if(!this.isManagerOpen()){if(!await this.waitForElement(".sp_choice_type_12,.sp_choice_type_13"))return!1;if(!this.elementExists(".sp_choice_type_12"))return this.click(".sp_choice_type_13");this.click(".sp_choice_type_12"),await b((()=>this.isManagerOpen()),200,100)}await this.waitForElement(".type-modal",2e4),this.waitForThenClick(".ccpa-stack .pm-switch[aria-checked=true] .slider",500,!0);try{const e=".sp_choice_type_REJECT_ALL",t=".reject-toggle",o=await Promise.race([this.waitForElement(e,2e3).then((e=>e?0:-1)),this.waitForElement(t,2e3).then((e=>e?1:-1)),this.waitForElement(".pm-features",2e3).then((e=>e?2:-1))]);if(0===o)return await this.wait(1500),this.click(e);1===o?this.click(t):2===o&&(await this.waitForElement(".pm-features",1e4),this.click(".checked > span",!0),this.click(".chevron"))}catch(t){e.errors&&console.warn(t)}return this.click(".sp_choice_type_SAVE_AND_EXIT")}},class extends d{constructor(){super(...arguments),this.name="consentmanager.net",this.prehideSelectors=["#cmpbox,#cmpbox2"],this.apiAvailable=!1}get hasSelfTest(){return this.apiAvailable}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.apiAvailable=await this.mainWorldEval("EVAL_CONSENTMANAGER_1"),!!this.apiAvailable||this.elementExists("#cmpbox")}async detectPopup(){return this.apiAvailable?(await this.wait(500),await this.mainWorldEval("EVAL_CONSENTMANAGER_2")):this.elementVisible("#cmpbox .cmpmore","any")}async optOut(){return await this.wait(500),this.apiAvailable?await this.mainWorldEval("EVAL_CONSENTMANAGER_3"):!!this.click(".cmpboxbtnno")||(this.elementExists(".cmpwelcomeprpsbtn")?(this.click(".cmpwelcomeprpsbtn > a[aria-checked=true]",!0),this.click(".cmpboxbtnsave"),!0):(this.click(".cmpboxbtncustom"),await this.waitForElement(".cmptblbox",2e3),this.click(".cmptdchoice > a[aria-checked=true]",!0),this.click(".cmpboxbtnyescustomchoices"),this.hide("#cmpwrapper,#cmpbox","display"),!0))}async optIn(){return this.apiAvailable?await this.mainWorldEval("EVAL_CONSENTMANAGER_4"):this.click(".cmpboxbtnyes")}async test(){if(this.apiAvailable)return await this.mainWorldEval("EVAL_CONSENTMANAGER_5")}},class extends d{constructor(){super(...arguments),this.name="Evidon"}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("#_evidon_banner")}async detectPopup(){return this.elementVisible("#_evidon_banner","any")}async optOut(){return this.click("#_evidon-decline-button")||(k(h(),"#evidon-prefdiag-overlay,#evidon-prefdiag-background"),this.click("#_evidon-option-button"),await this.waitForElement("#evidon-prefdiag-overlay",5e3),this.click("#evidon-prefdiag-decline")),!0}async optIn(){return this.click("#_evidon-accept-button")}},class extends d{constructor(){super(...arguments),this.name="Onetrust",this.prehideSelectors=["#onetrust-banner-sdk,#onetrust-consent-sdk,.onetrust-pc-dark-filter,.js-consent-banner"],this.runContext={urlPattern:"^(?!.*https://www\\.nba\\.com/)"}}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("#onetrust-banner-sdk,#onetrust-pc-sdk")}async detectPopup(){return this.elementVisible("#onetrust-banner-sdk,#onetrust-pc-sdk","any")}async optOut(){return this.elementVisible("#onetrust-reject-all-handler,.ot-pc-refuse-all-handler,.js-reject-cookies","any")?this.click("#onetrust-reject-all-handler,.ot-pc-refuse-all-handler,.js-reject-cookies"):(this.elementExists("#onetrust-pc-btn-handler")?this.click("#onetrust-pc-btn-handler"):this.click(".ot-sdk-show-settings,button.js-cookie-settings"),await this.waitForElement("#onetrust-consent-sdk",2e3),await this.wait(1e3),this.click("#onetrust-consent-sdk input.category-switch-handler:checked,.js-editor-toggle-state:checked",!0),await this.wait(1e3),await this.waitForElement(".save-preference-btn-handler,.js-consent-save",2e3),this.click(".save-preference-btn-handler,.js-consent-save"),await this.waitForVisible("#onetrust-banner-sdk",5e3,"none"),!0)}async optIn(){return this.click("#onetrust-accept-btn-handler,#accept-recommended-btn-handler,.js-accept-cookies")}async test(){return await b((()=>this.mainWorldEval("EVAL_ONETRUST_1")),10,500)}},class extends d{constructor(){super(...arguments),this.name="Klaro",this.prehideSelectors=[".klaro"],this.settingsOpen=!1}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".klaro > .cookie-modal")?(this.settingsOpen=!0,!0):this.elementExists(".klaro > .cookie-notice")}async detectPopup(){return this.elementVisible(".klaro > .cookie-notice,.klaro > .cookie-modal","any")}async optOut(){return!!await this.mainWorldEval("EVAL_KLARO_TRY_API_OPT_OUT")||(!!this.click(".klaro .cn-decline")||(await this.mainWorldEval("EVAL_KLARO_OPEN_POPUP"),!!this.click(".klaro .cn-decline")||(this.click(".cm-purpose:not(.cm-toggle-all) > input:not(.half-checked,.required,.only-required),.cm-purpose:not(.cm-toggle-all) > div > input:not(.half-checked,.required,.only-required)",!0),this.click(".cm-btn-accept,.cm-button"))))}async optIn(){return!!this.click(".klaro .cm-btn-accept-all")||(this.settingsOpen?(this.click(".cm-purpose:not(.cm-toggle-all) > input.half-checked",!0),this.click(".cm-btn-accept")):this.click(".klaro .cookie-notice .cm-btn-success"))}async test(){return await this.mainWorldEval("EVAL_KLARO_1")}},class extends d{constructor(){super(...arguments),this.name="Uniconsent"}get prehideSelectors(){return[".unic",".modal:has(.unic)"]}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".unic .unic-box,.unic .unic-bar,.unic .unic-modal")}async detectPopup(){return this.elementVisible(".unic .unic-box,.unic .unic-bar,.unic .unic-modal","any")}async optOut(){if(await this.waitForElement(".unic button",1e3),document.querySelectorAll(".unic button").forEach((e=>{const t=e.textContent;(t.includes("Manage Options")||t.includes("Optionen verwalten"))&&e.click()})),await this.waitForElement(".unic input[type=checkbox]",1e3)){await this.waitForElement(".unic button",1e3),document.querySelectorAll(".unic input[type=checkbox]").forEach((e=>{e.checked&&e.click()}));for(const e of document.querySelectorAll(".unic button")){const t=e.textContent;for(const o of["Confirm Choices","Save Choices","Auswahl speichern"])if(t.includes(o))return e.click(),await this.wait(500),!0}}return!1}async optIn(){return this.waitForThenClick(".unic #unic-agree")}async test(){await this.wait(1e3);return!this.elementExists(".unic .unic-box,.unic .unic-bar")}},class extends d{constructor(){super(...arguments),this.prehideSelectors=[".cmp-root"],this.name="Conversant"}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".cmp-root .cmp-receptacle")}async detectPopup(){return this.elementVisible(".cmp-root .cmp-receptacle","any")}async optOut(){if(!await this.waitForThenClick(".cmp-main-button:not(.cmp-main-button--primary)"))return!1;if(!await this.waitForElement(".cmp-view-tab-tabs"))return!1;await this.waitForThenClick(".cmp-view-tab-tabs > :first-child"),await this.waitForThenClick(".cmp-view-tab-tabs > .cmp-view-tab--active:first-child");for(const e of Array.from(document.querySelectorAll(".cmp-accordion-item"))){e.querySelector(".cmp-accordion-item-title").click(),await b((()=>!!e.querySelector(".cmp-accordion-item-content.cmp-active")),10,50);const t=e.querySelector(".cmp-accordion-item-content.cmp-active");t.querySelectorAll(".cmp-toggle-actions .cmp-toggle-deny:not(.cmp-toggle-deny--active)").forEach((e=>e.click())),t.querySelectorAll(".cmp-toggle-actions .cmp-toggle-checkbox:not(.cmp-toggle-checkbox--active)").forEach((e=>e.click()))}return await this.click(".cmp-main-button:not(.cmp-main-button--primary)"),!0}async optIn(){return this.waitForThenClick(".cmp-main-button.cmp-main-button--primary")}async test(){return document.cookie.includes("cmp-data=0")}},class extends d{constructor(){super(...arguments),this.name="tiktok.com",this.runContext={urlPattern:"tiktok"}}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}getShadowRoot(){const e=document.querySelector("tiktok-cookie-banner");return e?e.shadowRoot:null}async detectCmp(){return this.elementExists("tiktok-cookie-banner")}async detectPopup(){return _(this.getShadowRoot().querySelector(".tiktok-cookie-banner"))}async optOut(){const e=this.autoconsent.config.logs,t=this.getShadowRoot().querySelector(".button-wrapper button:first-child");return t?(e.rulesteps&&console.log("[clicking]",t),t.click(),!0):(e.errors&&console.log("no decline button found"),!1)}async optIn(){const e=this.autoconsent.config.logs,t=this.getShadowRoot().querySelector(".button-wrapper button:last-child");return t?(e.rulesteps&&console.log("[clicking]",t),t.click(),!0):(e.errors&&console.log("no accept button found"),!1)}async test(){const e=document.cookie.match(/cookie-consent=([^;]+)/);if(!e)return!1;const t=JSON.parse(decodeURIComponent(e[1]));return Object.values(t).every((e=>"boolean"!=typeof e||!1===e))}},class extends d{constructor(){super(...arguments),this.runContext={urlPattern:"^https://(www\\.)?airbnb\\.[^/]+/"},this.prehideSelectors=["div[data-testid=main-cookies-banner-container]",'div:has(> div:first-child):has(> div:last-child):has(> section [data-testid="strictly-necessary-cookies"])']}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("div[data-testid=main-cookies-banner-container]")}async detectPopup(){return this.elementVisible("div[data-testid=main-cookies-banner-container","any")}async optOut(){let e;for(await this.waitForThenClick("div[data-testid=main-cookies-banner-container] button._snbhip0");e=document.querySelector("[data-testid=modal-container] button[aria-checked=true]:not([disabled])");)e.click();return this.waitForThenClick("button[data-testid=save-btn]")}async optIn(){return this.waitForThenClick("div[data-testid=main-cookies-banner-container] button._148dgdpk")}async test(){return await b((()=>!!document.cookie.match("OptanonAlertBoxClosed")),20,200)}},class extends d{constructor(){super(...arguments),this.name="tumblr-com",this.runContext={urlPattern:"^https://(www\\.)?tumblr\\.com/"}}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}get prehideSelectors(){return["#cmp-app-container"]}async detectCmp(){return this.elementExists("#cmp-app-container")}async detectPopup(){return this.elementVisible("#cmp-app-container","any")}async optOut(){let e=document.querySelector("#cmp-app-container iframe"),t=e.contentDocument?.querySelector(".cmp-components-button.is-secondary");return!!t&&(t.click(),await b((()=>{const e=document.querySelector("#cmp-app-container iframe");return!!e.contentDocument?.querySelector(".cmp__dialog input")}),5,500),e=document.querySelector("#cmp-app-container iframe"),t=e.contentDocument?.querySelector(".cmp-components-button.is-secondary"),!!t&&(t.click(),!0))}async optIn(){const e=document.querySelector("#cmp-app-container iframe").contentDocument.querySelector(".cmp-components-button.is-primary");return!!e&&(e.click(),!0)}}],v=class{constructor(e){this.autoconsentInstance=e}click(e,t=!1){const o=this.elementSelector(e);return this.autoconsentInstance.config.logs.rulesteps&&console.log("[click]",e,t,o),o.length>0&&(t?o.forEach((e=>e.click())):o[0].click()),o.length>0}elementExists(e){return this.elementSelector(e).length>0}elementVisible(e,t){const o=this.elementSelector(e),c=new Array(o.length);return o.forEach(((e,t)=>{c[t]=_(e)})),"none"===t?c.every((e=>!e)):0!==c.length&&("any"===t?c.some((e=>e)):c.every((e=>e)))}waitForElement(e,t=1e4){const o=Math.ceil(t/200);return this.autoconsentInstance.config.logs.rulesteps&&console.log("[waitForElement]",e),b((()=>this.elementSelector(e).length>0),o,200)}waitForVisible(e,t=1e4,o="any"){return b((()=>this.elementVisible(e,o)),Math.ceil(t/200),200)}async waitForThenClick(e,t=1e4,o=!1){return await this.waitForElement(e,t),this.click(e,o)}wait(e){return new Promise((t=>{setTimeout((()=>{t(!0)}),e)}))}hide(e,t){return k(h(),e,t)}prehide(e){const t=h("autoconsent-prehide");return this.autoconsentInstance.config.logs.lifecycle&&console.log("[prehide]",t,location.href),k(t,e,"opacity")}undoPrehide(){const e=h("autoconsent-prehide");return this.autoconsentInstance.config.logs.lifecycle&&console.log("[undoprehide]",e,location.href),e&&e.remove(),!!e}querySingleReplySelector(e,t=document){if(e.startsWith("aria/"))return[];if(e.startsWith("xpath/")){const o=e.slice(6),c=document.evaluate(o,t,null,XPathResult.ANY_TYPE,null);let i=null;const n=[];for(;i=c.iterateNext();)n.push(i);return n}return e.startsWith("text/")||e.startsWith("pierce/")?[]:t.shadowRoot?Array.from(t.shadowRoot.querySelectorAll(e)):Array.from(t.querySelectorAll(e))}querySelectorChain(e){let t,o=document;for(const c of e){if(t=this.querySingleReplySelector(c,o),0===t.length)return[];o=t[0]}return t}elementSelector(e){return"string"==typeof e?this.querySingleReplySelector(e):this.querySelectorChain(e)}};var f=[{name:"192.com",detectCmp:[{exists:".ont-cookies"}],detectPopup:[{visible:".ont-cookies"}],optIn:[{click:".ont-btn-main.ont-cookies-btn.js-ont-btn-ok2"}],optOut:[{click:".ont-cookes-btn-manage"},{click:".ont-btn-main.ont-cookies-btn.js-ont-btn-choose"}],test:[{eval:"EVAL_ONENINETWO_0"}]},{name:"1password-com",cosmetic:!0,prehideSelectors:['footer #footer-root [aria-label="Cookie Consent"]'],detectCmp:[{exists:'footer #footer-root [aria-label="Cookie Consent"]'}],detectPopup:[{visible:'footer #footer-root [aria-label="Cookie Consent"]'}],optIn:[{click:'footer #footer-root [aria-label="Cookie Consent"] button'}],optOut:[{hide:'footer #footer-root [aria-label="Cookie Consent"]'}]},{name:"abconcerts.be",vendorUrl:"https://unknown",intermediate:!1,prehideSelectors:["dialog.cookie-consent"],detectCmp:[{exists:"dialog.cookie-consent form.cookie-consent__form"}],detectPopup:[{visible:"dialog.cookie-consent form.cookie-consent__form"}],optIn:[{waitForThenClick:"dialog.cookie-consent form.cookie-consent__form button[value=yes]"}],optOut:[{if:{exists:"dialog.cookie-consent form.cookie-consent__form button[value=no]"},then:[{click:"dialog.cookie-consent form.cookie-consent__form button[value=no]"}],else:[{click:"dialog.cookie-consent form.cookie-consent__form button.cookie-consent__options-toggle"},{waitForThenClick:'dialog.cookie-consent form.cookie-consent__form button[value="save_options"]'}]}]},{name:"activobank.pt",runContext:{urlPattern:"^https://(www\\.)?activobank\\.pt"},prehideSelectors:["aside#cookies,.overlay-cookies"],detectCmp:[{exists:"#cookies .cookies-btn"}],detectPopup:[{visible:"#cookies #submitCookies"}],optIn:[{waitForThenClick:"#cookies #submitCookies"}],optOut:[{waitForThenClick:"#cookies #rejectCookies"}]},{name:"Adroll",prehideSelectors:["#adroll_consent_container"],detectCmp:[{exists:"#adroll_consent_container"}],detectPopup:[{visible:"#adroll_consent_container"}],optIn:[{waitForThenClick:"#adroll_consent_accept"}],optOut:[{waitForThenClick:"#adroll_consent_reject"}],test:[{eval:"EVAL_ADROLL_0"}]},{name:"affinity.serif.com",detectCmp:[{exists:".c-cookie-banner button[data-qa='allow-all-cookies']"}],detectPopup:[{visible:".c-cookie-banner"}],optIn:[{click:'button[data-qa="allow-all-cookies"]'}],optOut:[{click:'button[data-qa="manage-cookies"]'},{waitFor:'.c-cookie-banner ~ [role="dialog"]'},{waitForThenClick:'.c-cookie-banner ~ [role="dialog"] input[type="checkbox"][value="true"]',all:!0},{click:'.c-cookie-banner ~ [role="dialog"] .c-modal__action button'}],test:[{wait:500},{eval:"EVAL_AFFINITY_SERIF_COM_0"}]},{name:"agolde.com",cosmetic:!0,prehideSelectors:["#modal-1 div[data-micromodal-close]"],detectCmp:[{exists:"#modal-1 div[aria-labelledby=modal-1-title]"}],detectPopup:[{exists:"#modal-1 div[data-micromodal-close]"}],optIn:[{click:'button[aria-label="Close modal"]'}],optOut:[{hide:"#modal-1 div[data-micromodal-close]"}]},{name:"aliexpress",vendorUrl:"https://aliexpress.com/",runContext:{urlPattern:"^https://.*\\.aliexpress\\.com/"},prehideSelectors:["#gdpr-new-container"],detectCmp:[{exists:"#gdpr-new-container"}],detectPopup:[{visible:"#gdpr-new-container"}],optIn:[{waitForThenClick:"#gdpr-new-container .btn-accept"}],optOut:[{waitForThenClick:"#gdpr-new-container .btn-more"},{waitFor:"#gdpr-new-container .gdpr-dialog-switcher"},{click:"#gdpr-new-container .switcher-on",all:!0,optional:!0},{click:"#gdpr-new-container .btn-save"}]},{name:"almacmp",prehideSelectors:["#alma-cmpv2-container"],detectCmp:[{exists:"#alma-cmpv2-container"}],detectPopup:[{visible:"#alma-cmpv2-container #almacmp-modal-layer1"}],optIn:[{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer1 #almacmp-modalConfirmBtn"}],optOut:[{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer1 #almacmp-modalSettingBtn"},{waitFor:"#alma-cmpv2-container #almacmp-modal-layer2"},{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer2 #almacmp-reject-all-layer2"}],test:[{eval:"EVAL_ALMACMP_0"}]},{name:"altium.com",cosmetic:!0,prehideSelectors:[".altium-privacy-bar"],detectCmp:[{exists:".altium-privacy-bar"}],detectPopup:[{exists:".altium-privacy-bar"}],optIn:[{click:"a.altium-privacy-bar__btn"}],optOut:[{hide:".altium-privacy-bar"}]},{name:"amazon.com",prehideSelectors:['span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'],detectCmp:[{exists:'span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'}],detectPopup:[{visible:'span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'}],optIn:[{waitForVisible:"#sp-cc-accept"},{wait:500},{click:"#sp-cc-accept"}],optOut:[{waitForVisible:"#sp-cc-rejectall-link"},{wait:500},{click:"#sp-cc-rejectall-link"}]},{name:"aquasana.com",cosmetic:!0,prehideSelectors:["#consent-tracking"],detectCmp:[{exists:"#consent-tracking"}],detectPopup:[{exists:"#consent-tracking"}],optIn:[{click:"#accept_consent"}],optOut:[{hide:"#consent-tracking"}]},{name:"arbeitsagentur",vendorUrl:"https://www.arbeitsagentur.de/",prehideSelectors:[".modal-open bahf-cookie-disclaimer-dpl3"],detectCmp:[{exists:"bahf-cookie-disclaimer-dpl3"}],detectPopup:[{visible:"bahf-cookie-disclaimer-dpl3"}],optIn:[{waitForThenClick:["bahf-cookie-disclaimer-dpl3","bahf-cd-modal-dpl3 .ba-btn-primary"]}],optOut:[{waitForThenClick:["bahf-cookie-disclaimer-dpl3","bahf-cd-modal-dpl3 .ba-btn-contrast"]}],test:[{eval:"EVAL_ARBEITSAGENTUR_TEST"}]},{name:"asus",vendorUrl:"https://www.asus.com/",runContext:{urlPattern:"^https://www\\.asus\\.com/"},prehideSelectors:["#cookie-policy-info,#cookie-policy-info-bg"],detectCmp:[{exists:"#cookie-policy-info"}],detectPopup:[{visible:"#cookie-policy-info"}],optIn:[{waitForThenClick:'#cookie-policy-info [data-agree="Accept Cookies"]'}],optOut:[{if:{exists:"#cookie-policy-info .btn-reject"},then:[{waitForThenClick:"#cookie-policy-info .btn-reject"}],else:[{waitForThenClick:"#cookie-policy-info .btn-setting"},{waitForThenClick:'#cookie-policy-lightbox-wrapper [data-agree="Save Settings"]'}]}]},{name:"athlinks-com",runContext:{urlPattern:"^https://(www\\.)?athlinks\\.com/"},cosmetic:!0,prehideSelectors:["#footer-container ~ div"],detectCmp:[{exists:"#footer-container ~ div"}],detectPopup:[{visible:"#footer-container > div"}],optIn:[{click:"#footer-container ~ div button"}],optOut:[{hide:"#footer-container ~ div"}]},{name:"ausopen.com",cosmetic:!0,detectCmp:[{exists:".gdpr-popup__message"}],detectPopup:[{visible:".gdpr-popup__message"}],optOut:[{hide:".gdpr-popup__message"}],optIn:[{click:".gdpr-popup__message button"}]},{name:"automattic-cmp-optout",prehideSelectors:['form[class*="cookie-banner"][method="post"]'],detectCmp:[{exists:'form[class*="cookie-banner"][method="post"]'}],detectPopup:[{visible:'form[class*="cookie-banner"][method="post"]'}],optIn:[{click:'a[class*="accept-all-button"]'}],optOut:[{click:'form[class*="cookie-banner"] div[class*="simple-options"] a[class*="customize-button"]'},{waitForThenClick:"input[type=checkbox][checked]:not([disabled])",all:!0},{click:'a[class*="accept-selection-button"]'}]},{name:"aws.amazon.com",prehideSelectors:["#awsccc-cb-content","#awsccc-cs-container","#awsccc-cs-modalOverlay","#awsccc-cs-container-inner"],detectCmp:[{exists:"#awsccc-cb-content"}],detectPopup:[{visible:"#awsccc-cb-content"}],optIn:[{click:"button[data-id=awsccc-cb-btn-accept"}],optOut:[{click:"button[data-id=awsccc-cb-btn-customize]"},{waitFor:"input[aria-checked]"},{click:"input[aria-checked=true]",all:!0,optional:!0},{click:"button[data-id=awsccc-cs-btn-save]"}]},{name:"axeptio",prehideSelectors:[".axeptio_widget"],detectCmp:[{exists:".axeptio_widget"}],detectPopup:[{visible:".axeptio_widget"}],optIn:[{waitFor:".axeptio-widget--open"},{click:"button#axeptio_btn_acceptAll"}],optOut:[{waitFor:".axeptio-widget--open"},{click:"button#axeptio_btn_dismiss"}],test:[{eval:"EVAL_AXEPTIO_0"}]},{name:"baden-wuerttemberg.de",prehideSelectors:[".cookie-alert.t-dark"],cosmetic:!0,detectCmp:[{exists:".cookie-alert.t-dark"}],detectPopup:[{visible:".cookie-alert.t-dark"}],optIn:[{click:".cookie-alert__form input:not([disabled]):not([checked])"},{click:".cookie-alert__button button"}],optOut:[{hide:".cookie-alert.t-dark"}]},{name:"bahn-de",vendorUrl:"https://www.bahn.de/",cosmetic:!1,runContext:{main:!0,frame:!1,urlPattern:"^https://(www\\.)?bahn\\.de/"},intermediate:!1,prehideSelectors:[],detectCmp:[{exists:["body > div:first-child","#consent-layer"]}],detectPopup:[{visible:["body > div:first-child","#consent-layer"]}],optIn:[{waitForThenClick:["body > div:first-child","#consent-layer .js-accept-all-cookies"]}],optOut:[{waitForThenClick:["body > div:first-child","#consent-layer .js-accept-essential-cookies"]}],test:[{eval:"EVAL_BAHN_TEST"}]},{name:"bbb.org",runContext:{urlPattern:"^https://www\\.bbb\\.org/"},cosmetic:!0,prehideSelectors:['div[aria-label="use of cookies on bbb.org"]'],detectCmp:[{exists:'div[aria-label="use of cookies on bbb.org"]'}],detectPopup:[{visible:'div[aria-label="use of cookies on bbb.org"]'}],optIn:[{click:'div[aria-label="use of cookies on bbb.org"] button.bds-button-unstyled span.visually-hidden'}],optOut:[{hide:'div[aria-label="use of cookies on bbb.org"]'}]},{name:"bing.com",prehideSelectors:["#bnp_container"],detectCmp:[{exists:"#bnp_cookie_banner"}],detectPopup:[{visible:"#bnp_cookie_banner"}],optIn:[{click:"#bnp_btn_accept"}],optOut:[{click:"#bnp_btn_preference"},{click:"#mcp_savesettings"}],test:[{eval:"EVAL_BING_0"}]},{name:"blocksy",vendorUrl:"https://creativethemes.com/blocksy/docs/extensions/cookies-consent/",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,prehideSelectors:[".cookie-notification"],detectCmp:[{exists:"#blocksy-ext-cookies-consent-styles-css"}],detectPopup:[{visible:".cookie-notification"}],optIn:[{click:".cookie-notification .ct-cookies-decline-button"}],optOut:[{waitForThenClick:".cookie-notification .ct-cookies-decline-button"}],test:[{eval:"EVAL_BLOCKSY_0"}]},{name:"borlabs",detectCmp:[{exists:"._brlbs-block-content"}],detectPopup:[{visible:"._brlbs-bar-wrap,._brlbs-box-wrap"}],optIn:[{click:"a[data-cookie-accept-all]"}],optOut:[{click:"a[data-cookie-individual]"},{waitForVisible:".cookie-preference"},{click:"input[data-borlabs-cookie-checkbox]:checked",all:!0,optional:!0},{click:"#CookiePrefSave"},{wait:500}],prehideSelectors:["#BorlabsCookieBox"],test:[{eval:"EVAL_BORLABS_0"}]},{name:"bundesregierung.de",prehideSelectors:[".bpa-cookie-banner"],detectCmp:[{exists:".bpa-cookie-banner"}],detectPopup:[{visible:".bpa-cookie-banner .bpa-module-full-hero"}],optIn:[{click:".bpa-accept-all-button"}],optOut:[{wait:500,comment:"click is not immediately recognized"},{waitForThenClick:".bpa-close-button"}],test:[{eval:"EVAL_BUNDESREGIERUNG_DE_0"}]},{name:"burpee.com",cosmetic:!0,prehideSelectors:["#notice-cookie-block"],detectCmp:[{exists:"#notice-cookie-block"}],detectPopup:[{exists:"#html-body #notice-cookie-block"}],optIn:[{click:"#btn-cookie-allow"}],optOut:[{hide:"#html-body #notice-cookie-block, #notice-cookie"}]},{name:"canva.com",prehideSelectors:['div[role="dialog"] a[data-anchor-id="cookie-policy"]'],detectCmp:[{exists:'div[role="dialog"] a[data-anchor-id="cookie-policy"]'}],detectPopup:[{exists:'div[role="dialog"] a[data-anchor-id="cookie-policy"]'}],optIn:[{click:'div[role="dialog"] button:nth-child(1)'}],optOut:[{if:{exists:'div[role="dialog"] button:nth-child(3)'},then:[{click:'div[role="dialog"] button:nth-child(2)'}],else:[{click:'div[role="dialog"] button:nth-child(2)'},{waitFor:'div[role="dialog"] a[data-anchor-id="cookie-policy"]'},{waitFor:'div[role="dialog"] button[role=switch]'},{click:'div[role="dialog"] button:nth-child(2):not([role])'},{click:'div[role="dialog"] div:last-child button:only-child'}]}],test:[{eval:"EVAL_CANVA_0"}]},{name:"canyon.com",runContext:{urlPattern:"^https://www\\.canyon\\.com/"},prehideSelectors:["div.modal.cookiesModal.is-open"],detectCmp:[{exists:"div.modal.cookiesModal.is-open"}],detectPopup:[{visible:"div.modal.cookiesModal.is-open"}],optIn:[{click:'div.cookiesModal__buttonWrapper > button[data-closecause="close-by-submit"]'}],optOut:[{click:'div.cookiesModal__buttonWrapper > button[data-closecause="close-by-manage-cookies"]'},{waitForThenClick:"button#js-manage-data-privacy-save-button"}]},{name:"cc-banner-springer",prehideSelectors:[".cc-banner[data-cc-banner]"],detectCmp:[{exists:".cc-banner[data-cc-banner]"}],detectPopup:[{visible:".cc-banner[data-cc-banner]"}],optIn:[{waitForThenClick:".cc-banner[data-cc-banner] button[data-cc-action=accept]"}],optOut:[{if:{exists:".cc-banner[data-cc-banner] button[data-cc-action=reject]"},then:[{click:".cc-banner[data-cc-banner] button[data-cc-action=reject]"}],else:[{waitForThenClick:".cc-banner[data-cc-banner] button[data-cc-action=preferences]"},{waitFor:".cc-preferences[data-cc-preferences]"},{click:".cc-preferences[data-cc-preferences] input[type=radio][data-cc-action=toggle-category][value=off]",all:!0,optional:!0},{if:{exists:".cc-preferences[data-cc-preferences] button[data-cc-action=reject]"},then:[{click:".cc-preferences[data-cc-preferences] button[data-cc-action=reject]"}],else:[{click:".cc-preferences[data-cc-preferences] button[data-cc-action=save]"}]}]}],test:[{eval:"EVAL_CC_BANNER2_0"}]},{name:"cc_banner",cosmetic:!0,prehideSelectors:[".cc_banner-wrapper"],detectCmp:[{exists:".cc_banner-wrapper"}],detectPopup:[{visible:".cc_banner"}],optIn:[{click:".cc_btn_accept_all"}],optOut:[{hide:".cc_banner-wrapper"}]},{name:"ciaopeople.it",prehideSelectors:["#cp-gdpr-choices"],detectCmp:[{exists:"#cp-gdpr-choices"}],detectPopup:[{visible:"#cp-gdpr-choices"}],optIn:[{waitForThenClick:".gdpr-btm__right > button:nth-child(2)"}],optOut:[{waitForThenClick:".gdpr-top-content > button"},{waitFor:".gdpr-top-back"},{waitForThenClick:".gdpr-btm__right > button:nth-child(1)"}],test:[{visible:"#cp-gdpr-choices",check:"none"}]},{vendorUrl:"https://www.civicuk.com/cookie-control/",name:"civic-cookie-control",prehideSelectors:["#ccc-module,#ccc-overlay"],detectCmp:[{exists:"#ccc-module"}],detectPopup:[{visible:"#ccc"},{visible:"#ccc-module"}],optOut:[{click:"#ccc-reject-settings"}],optIn:[{click:"#ccc-recommended-settings"}]},{name:"click.io",prehideSelectors:["#cl-consent"],detectCmp:[{exists:"#cl-consent"}],detectPopup:[{visible:"#cl-consent"}],optIn:[{waitForThenClick:'#cl-consent [data-role="b_agree"]'}],optOut:[{waitFor:'#cl-consent [data-role="b_options"]'},{wait:500},{click:'#cl-consent [data-role="b_options"]'},{waitFor:'.cl-consent-popup.cl-consent-visible [data-role="alloff"]'},{click:'.cl-consent-popup.cl-consent-visible [data-role="alloff"]',all:!0},{click:'[data-role="b_save"]'}],test:[{eval:"EVAL_CLICKIO_0",comment:"TODO: this only checks if we interacted at all"}]},{name:"clinch",intermediate:!1,runContext:{frame:!1,main:!0},prehideSelectors:[".consent-modal[role=dialog]"],detectCmp:[{exists:".consent-modal[role=dialog]"}],detectPopup:[{visible:".consent-modal[role=dialog]"}],optIn:[{click:"#consent_agree"}],optOut:[{if:{exists:"#consent_reject"},then:[{click:"#consent_reject"}],else:[{click:"#manage_cookie_preferences"},{click:"#cookie_consent_preferences input:checked",all:!0,optional:!0},{click:"#consent_save"}]}],test:[{eval:"EVAL_CLINCH_0"}]},{name:"clustrmaps.com",runContext:{urlPattern:"^https://(www\\.)?clustrmaps\\.com/"},cosmetic:!0,prehideSelectors:["#gdpr-cookie-message"],detectCmp:[{exists:"#gdpr-cookie-message"}],detectPopup:[{visible:"#gdpr-cookie-message"}],optIn:[{click:"button#gdpr-cookie-accept"}],optOut:[{hide:"#gdpr-cookie-message"}]},{name:"coinbase",intermediate:!1,runContext:{frame:!0,main:!0,urlPattern:"^https://(www|help)\\.coinbase\\.com"},prehideSelectors:[],detectCmp:[{exists:"div[class^=CookieBannerContent__Container]"}],detectPopup:[{visible:"div[class^=CookieBannerContent__Container]"}],optIn:[{click:"div[class^=CookieBannerContent__CTA] :nth-last-child(1)"}],optOut:[{click:"button[class^=CookieBannerContent__Settings]"},{click:"div[class^=CookiePreferencesModal__CategoryContainer] input:checked",all:!0,optional:!0},{click:"div[class^=CookiePreferencesModal__ButtonContainer] > button"}],test:[{eval:"EVAL_COINBASE_0"}]},{name:"Complianz banner",prehideSelectors:["#cmplz-cookiebanner-container"],detectCmp:[{exists:"#cmplz-cookiebanner-container .cmplz-cookiebanner"}],detectPopup:[{visible:"#cmplz-cookiebanner-container .cmplz-cookiebanner",check:"any"}],optIn:[{waitForThenClick:".cmplz-cookiebanner .cmplz-accept"}],optOut:[{waitForThenClick:".cmplz-cookiebanner .cmplz-deny"}],test:[{eval:"EVAL_COMPLIANZ_BANNER_0"}]},{name:"Complianz categories",prehideSelectors:['.cc-type-categories[aria-describedby="cookieconsent:desc"]'],detectCmp:[{exists:'.cc-type-categories[aria-describedby="cookieconsent:desc"]'}],detectPopup:[{visible:'.cc-type-categories[aria-describedby="cookieconsent:desc"]'}],optIn:[{any:[{click:".cc-accept-all"},{click:".cc-allow-all"},{click:".cc-allow"},{click:".cc-dismiss"}]}],optOut:[{if:{exists:'.cc-type-categories[aria-describedby="cookieconsent:desc"] .cc-dismiss'},then:[{click:".cc-dismiss"}],else:[{click:".cc-type-categories input[type=checkbox]:not([disabled]):checked",all:!0,optional:!0},{click:".cc-save"}]}]},{name:"Complianz notice",prehideSelectors:['.cc-type-info[aria-describedby="cookieconsent:desc"]'],cosmetic:!0,detectCmp:[{exists:'.cc-type-info[aria-describedby="cookieconsent:desc"] .cc-compliance .cc-btn'}],detectPopup:[{visible:'.cc-type-info[aria-describedby="cookieconsent:desc"] .cc-compliance .cc-btn'}],optIn:[{click:".cc-accept-all",optional:!0},{click:".cc-allow",optional:!0},{click:".cc-dismiss",optional:!0}],optOut:[{if:{exists:".cc-deny"},then:[{click:".cc-deny"}],else:[{hide:'[aria-describedby="cookieconsent:desc"]'}]}]},{name:"Complianz opt-both",prehideSelectors:['[aria-describedby="cookieconsent:desc"] .cc-type-opt-both'],detectCmp:[{exists:'[aria-describedby="cookieconsent:desc"] .cc-type-opt-both'}],detectPopup:[{visible:'[aria-describedby="cookieconsent:desc"] .cc-type-opt-both'}],optIn:[{click:".cc-accept-all",optional:!0},{click:".cc-allow",optional:!0},{click:".cc-dismiss",optional:!0}],optOut:[{waitForThenClick:".cc-deny"}]},{name:"Complianz opt-out",prehideSelectors:['[aria-describedby="cookieconsent:desc"].cc-type-opt-out'],detectCmp:[{exists:'[aria-describedby="cookieconsent:desc"].cc-type-opt-out'}],detectPopup:[{visible:'[aria-describedby="cookieconsent:desc"].cc-type-opt-out'}],optIn:[{click:".cc-accept-all",optional:!0},{click:".cc-allow",optional:!0},{click:".cc-dismiss",optional:!0}],optOut:[{if:{exists:".cc-deny"},then:[{click:".cc-deny"}],else:[{hide:'[aria-describedby="cookieconsent:desc"]'}]}]},{name:"Complianz optin",prehideSelectors:['.cc-type-opt-in[aria-describedby="cookieconsent:desc"]'],detectCmp:[{exists:'.cc-type-opt-in[aria-describedby="cookieconsent:desc"]'}],detectPopup:[{visible:'.cc-type-opt-in[aria-describedby="cookieconsent:desc"]'}],optIn:[{any:[{click:".cc-accept-all"},{click:".cc-allow"},{click:".cc-dismiss"}]}],optOut:[{if:{visible:".cc-deny"},then:[{click:".cc-deny"}],else:[{if:{visible:".cc-settings"},then:[{waitForThenClick:".cc-settings"},{waitForVisible:".cc-settings-view"},{click:".cc-settings-view input[type=checkbox]:not([disabled]):checked",all:!0,optional:!0},{click:".cc-settings-view .cc-btn-accept-selected"}],else:[{click:".cc-dismiss"}]}]}]},{name:"cookie-law-info",prehideSelectors:["#cookie-law-info-bar"],detectCmp:[{exists:"#cookie-law-info-bar"},{eval:"EVAL_COOKIE_LAW_INFO_DETECT"}],detectPopup:[{visible:"#cookie-law-info-bar"}],optIn:[{click:'[data-cli_action="accept_all"]'}],optOut:[{hide:"#cookie-law-info-bar"},{eval:"EVAL_COOKIE_LAW_INFO_0"}],test:[{eval:"EVAL_COOKIE_LAW_INFO_1"}]},{name:"cookie-manager-popup",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,detectCmp:[{exists:"#notice-cookie-block #allow-functional-cookies, #notice-cookie-block #btn-cookie-settings"}],detectPopup:[{visible:"#notice-cookie-block"}],optIn:[{click:"#btn-cookie-allow"}],optOut:[{if:{exists:"#allow-functional-cookies"},then:[{click:"#allow-functional-cookies"}],else:[{waitForThenClick:"#btn-cookie-settings"},{waitForVisible:".modal-body"},{click:'.modal-body input:checked, .switch[data-switch="on"]',all:!0,optional:!0},{click:'[role="dialog"] .modal-footer button'}]}],prehideSelectors:["#btn-cookie-settings"],test:[{eval:"EVAL_COOKIE_MANAGER_POPUP_0"}]},{name:"cookie-notice",prehideSelectors:["#cookie-notice"],cosmetic:!0,detectCmp:[{visible:"#cookie-notice .cookie-notice-container"}],detectPopup:[{visible:"#cookie-notice"}],optIn:[{click:"#cn-accept-cookie"}],optOut:[{hide:"#cookie-notice"}]},{name:"cookie-script",vendorUrl:"https://cookie-script.com/",prehideSelectors:["#cookiescript_injected"],detectCmp:[{exists:"#cookiescript_injected"}],detectPopup:[{visible:"#cookiescript_injected"}],optOut:[{if:{exists:"#cookiescript_reject"},then:[{wait:100},{click:"#cookiescript_reject"}],else:[{click:"#cookiescript_manage"},{waitForVisible:".cookiescript_fsd_main"},{waitForThenClick:"#cookiescript_reject"}]}],optIn:[{click:"#cookiescript_accept"}]},{name:"cookieacceptbar",vendorUrl:"https://unknown",cosmetic:!0,prehideSelectors:["#cookieAcceptBar.cookieAcceptBar"],detectCmp:[{exists:"#cookieAcceptBar.cookieAcceptBar"}],detectPopup:[{visible:"#cookieAcceptBar.cookieAcceptBar"}],optIn:[{waitForThenClick:"#cookieAcceptBarConfirm"}],optOut:[{hide:"#cookieAcceptBar.cookieAcceptBar"}]},{name:"cookiealert",intermediate:!1,prehideSelectors:[],runContext:{frame:!0,main:!0},detectCmp:[{exists:".cookie-alert-extended"}],detectPopup:[{visible:".cookie-alert-extended-modal"}],optIn:[{click:"button[data-controller='cookie-alert/extended/button/accept']"},{eval:"EVAL_COOKIEALERT_0"}],optOut:[{click:"a[data-controller='cookie-alert/extended/detail-link']"},{click:".cookie-alert-configuration-input:checked",all:!0,optional:!0},{click:"button[data-controller='cookie-alert/extended/button/configuration']"},{eval:"EVAL_COOKIEALERT_0"}],test:[{eval:"EVAL_COOKIEALERT_2"}]},{name:"cookieconsent2",vendorUrl:"https://www.github.com/orestbida/cookieconsent",comment:"supports v2.x.x of the library",prehideSelectors:["#cc--main"],detectCmp:[{exists:"#cc--main"}],detectPopup:[{visible:"#cm"},{exists:"#s-all-bn"}],optIn:[{waitForThenClick:"#s-all-bn"}],optOut:[{waitForThenClick:"#s-rall-bn"}],test:[{eval:"EVAL_COOKIECONSENT2_TEST"}]},{name:"cookieconsent3",vendorUrl:"https://www.github.com/orestbida/cookieconsent",comment:"supports v3.x.x of the library",prehideSelectors:["#cc-main"],detectCmp:[{exists:"#cc-main"}],detectPopup:[{visible:"#cc-main .cm-wrapper"}],optIn:[{waitForThenClick:".cm__btn[data-role=all]"}],optOut:[{waitForThenClick:".cm__btn[data-role=necessary]"}],test:[{eval:"EVAL_COOKIECONSENT3_TEST"}]},{name:"cookiefirst.com",prehideSelectors:["#cookiefirst-root,.cookiefirst-root,[aria-labelledby=cookie-preference-panel-title]"],detectCmp:[{exists:"#cookiefirst-root,.cookiefirst-root"}],detectPopup:[{visible:"#cookiefirst-root,.cookiefirst-root"}],optIn:[{click:"button[data-cookiefirst-action=accept]"}],optOut:[{if:{exists:"button[data-cookiefirst-action=adjust]"},then:[{click:"button[data-cookiefirst-action=adjust]"},{waitForVisible:"[data-cookiefirst-widget=modal]",timeout:1e3},{eval:"EVAL_COOKIEFIRST_1"},{wait:1e3},{click:"button[data-cookiefirst-action=save]"}],else:[{click:"button[data-cookiefirst-action=reject]"}]}],test:[{eval:"EVAL_COOKIEFIRST_0"}]},{name:"Cookie Information Banner",prehideSelectors:["#cookie-information-template-wrapper"],detectCmp:[{exists:"#cookie-information-template-wrapper"}],detectPopup:[{visible:"#cookie-information-template-wrapper"}],optIn:[{eval:"EVAL_COOKIEINFORMATION_1"}],optOut:[{hide:"#cookie-information-template-wrapper",comment:"some templates don't hide the banner automatically"},{eval:"EVAL_COOKIEINFORMATION_0"}],test:[{eval:"EVAL_COOKIEINFORMATION_2"}]},{name:"cookieyes",prehideSelectors:[".cky-overlay,.cky-consent-container"],detectCmp:[{exists:".cky-consent-container"}],detectPopup:[{visible:".cky-consent-container"}],optIn:[{waitForThenClick:".cky-consent-container [data-cky-tag=accept-button]"}],optOut:[{if:{exists:".cky-consent-container [data-cky-tag=reject-button]"},then:[{waitForThenClick:".cky-consent-container [data-cky-tag=reject-button]"}],else:[{if:{exists:".cky-consent-container [data-cky-tag=settings-button]"},then:[{click:".cky-consent-container [data-cky-tag=settings-button]"},{waitFor:".cky-modal-open input[type=checkbox]"},{click:".cky-modal-open input[type=checkbox]:checked",all:!0,optional:!0},{waitForThenClick:".cky-modal [data-cky-tag=detail-save-button]"}],else:[{hide:".cky-consent-container,.cky-overlay"}]}]}],test:[{eval:"EVAL_COOKIEYES_0"}]},{name:"corona-in-zahlen.de",prehideSelectors:[".cookiealert"],detectCmp:[{exists:".cookiealert"}],detectPopup:[{visible:".cookiealert"}],optOut:[{click:".configurecookies"},{click:".confirmcookies"}],optIn:[{click:".acceptcookies"}]},{name:"crossfit-com",cosmetic:!0,prehideSelectors:['body #modal > div > div[class^="_wrapper_"]'],detectCmp:[{exists:'body #modal > div > div[class^="_wrapper_"]'}],detectPopup:[{visible:'body #modal > div > div[class^="_wrapper_"]'}],optIn:[{click:'button[aria-label="accept cookie policy"]'}],optOut:[{hide:'body #modal > div > div[class^="_wrapper_"]'}]},{name:"csu-landtag-de",runContext:{urlPattern:"^https://(www\\.|)?csu-landtag\\.de"},prehideSelectors:["#cookie-disclaimer"],detectCmp:[{exists:"#cookie-disclaimer"}],detectPopup:[{visible:"#cookie-disclaimer"}],optIn:[{click:"#cookieall"}],optOut:[{click:"#cookiesel"}]},{name:"dailymotion-us",cosmetic:!0,prehideSelectors:['div[class*="CookiePopup__desktopContainer"]:has(div[class*="CookiePopup"])'],detectCmp:[{exists:'div[class*="CookiePopup__desktopContainer"]'}],detectPopup:[{visible:'div[class*="CookiePopup__desktopContainer"]'}],optIn:[{click:'div[class*="CookiePopup__desktopContainer"] > button > span'}],optOut:[{hide:'div[class*="CookiePopup__desktopContainer"]'}]},{name:"dailymotion.com",runContext:{urlPattern:"^https://(www\\.)?dailymotion\\.com/"},prehideSelectors:['div[class*="Overlay__container"]:has(div[class*="TCF2Popup"])'],detectCmp:[{exists:'div[class*="TCF2Popup"]'}],detectPopup:[{visible:'[class*="TCF2Popup"] a[href^="https://www.dailymotion.com/legal/cookiemanagement"]'}],optIn:[{waitForThenClick:'button[class*="TCF2Popup__button"]:not([class*="TCF2Popup__personalize"])'}],optOut:[{waitForThenClick:'button[class*="TCF2ContinueWithoutAcceptingButton"]'}],test:[{eval:"EVAL_DAILYMOTION_0"}]},{name:"deepl.com",prehideSelectors:[".dl_cookieBanner_container"],detectCmp:[{exists:".dl_cookieBanner_container"}],detectPopup:[{visible:".dl_cookieBanner_container"}],optOut:[{click:".dl_cookieBanner--buttonSelected"}],optIn:[{click:".dl_cookieBanner--buttonAll"}]},{name:"delta.com",runContext:{urlPattern:"^https://www\\.delta\\.com/"},cosmetic:!0,prehideSelectors:["ngc-cookie-banner"],detectCmp:[{exists:"div.cookie-footer-container"}],detectPopup:[{visible:"div.cookie-footer-container"}],optIn:[{click:" button.cookie-close-icon"}],optOut:[{hide:"div.cookie-footer-container"}]},{name:"dmgmedia-us",prehideSelectors:["#mol-ads-cmp-iframe, div.mol-ads-cmp > form > div"],detectCmp:[{exists:"div.mol-ads-cmp > form > div"}],detectPopup:[{waitForVisible:"div.mol-ads-cmp > form > div"}],optIn:[{waitForThenClick:"button.mol-ads-cmp--btn-primary"}],optOut:[{waitForThenClick:"div.mol-ads-ccpa--message > u > a"},{waitForVisible:".mol-ads-cmp--modal-dialog"},{waitForThenClick:"a.mol-ads-cmp-footer-privacy"},{waitForThenClick:"button.mol-ads-cmp--btn-secondary"}]},{name:"dmgmedia",prehideSelectors:['[data-project="mol-fe-cmp"]'],detectCmp:[{exists:'[data-project="mol-fe-cmp"]'}],detectPopup:[{visible:'[data-project="mol-fe-cmp"]'}],optIn:[{waitForThenClick:'[data-project="mol-fe-cmp"] button[class*=primary]'}],optOut:[{waitForThenClick:'[data-project="mol-fe-cmp"] button[class*=basic]'},{waitForVisible:'[data-project="mol-fe-cmp"] div[class*="tabContent"]'},{waitForThenClick:'[data-project="mol-fe-cmp"] div[class*="toggle"][class*="enabled"]',all:!0},{waitForThenClick:['[data-project="mol-fe-cmp"] [class*=footer]',"xpath///button[contains(., 'Save & Exit')]"]}]},{name:"dndbeyond",vendorUrl:"https://www.dndbeyond.com/",runContext:{urlPattern:"^https://(www\\.)?dndbeyond\\.com/"},prehideSelectors:["[id^=cookie-consent-banner]"],detectCmp:[{exists:"[id^=cookie-consent-banner]"}],detectPopup:[{visible:"[id^=cookie-consent-banner]"}],optIn:[{waitForThenClick:"#cookie-consent-granted"}],optOut:[{waitForThenClick:"#cookie-consent-denied"}],test:[{eval:"EVAL_DNDBEYOND_TEST"}]},{name:"Drupal",detectCmp:[{exists:"#drupalorg-crosssite-gdpr"}],detectPopup:[{visible:"#drupalorg-crosssite-gdpr"}],optOut:[{click:".no"}],optIn:[{click:".yes"}]},{name:"WP DSGVO Tools",link:"https://wordpress.org/plugins/shapepress-dsgvo/",prehideSelectors:[".sp-dsgvo"],cosmetic:!0,detectCmp:[{exists:".sp-dsgvo.sp-dsgvo-popup-overlay"}],detectPopup:[{visible:".sp-dsgvo.sp-dsgvo-popup-overlay",check:"any"}],optIn:[{click:".sp-dsgvo-privacy-btn-accept-all",all:!0}],optOut:[{hide:".sp-dsgvo.sp-dsgvo-popup-overlay"}],test:[{eval:"EVAL_DSGVO_0"}]},{name:"dunelm.com",prehideSelectors:["div[data-testid=cookie-consent-modal-backdrop]"],detectCmp:[{exists:"div[data-testid=cookie-consent-message-contents]"}],detectPopup:[{visible:"div[data-testid=cookie-consent-message-contents]"}],optIn:[{click:'[data-testid="cookie-consent-allow-all"]'}],optOut:[{click:"button[data-testid=cookie-consent-adjust-settings]"},{click:"button[data-testid=cookie-consent-preferences-save]"}],test:[{eval:"EVAL_DUNELM_0"}]},{name:"ecosia",vendorUrl:"https://www.ecosia.org/",runContext:{urlPattern:"^https://www\\.ecosia\\.org/"},prehideSelectors:[".cookie-wrapper"],detectCmp:[{exists:".cookie-wrapper > .cookie-notice"}],detectPopup:[{visible:".cookie-wrapper > .cookie-notice"}],optIn:[{waitForThenClick:"[data-test-id=cookie-notice-accept]"}],optOut:[{waitForThenClick:"[data-test-id=cookie-notice-reject]"}]},{name:"Ensighten ensModal",prehideSelectors:[".ensModal"],detectCmp:[{exists:".ensModal"}],detectPopup:[{visible:".ensModal"}],optIn:[{waitForThenClick:"#modalAcceptButton"}],optOut:[{waitForThenClick:".ensCheckbox:checked",all:!0},{waitForThenClick:"#ensSave"}]},{name:"Ensighten ensNotifyBanner",prehideSelectors:["#ensNotifyBanner"],detectCmp:[{exists:"#ensNotifyBanner"}],detectPopup:[{visible:"#ensNotifyBanner"}],optIn:[{waitForThenClick:"#ensCloseBanner"}],optOut:[{waitForThenClick:"#ensRejectAll,#rejectAll,#ensRejectBanner"}]},{name:"espace-personnel.agirc-arrco.fr",runContext:{urlPattern:"^https://espace-personnel\\.agirc-arrco\\.fr/"},prehideSelectors:[".cdk-overlay-container"],detectCmp:[{exists:".cdk-overlay-container app-esaa-cookie-component"}],detectPopup:[{visible:".cdk-overlay-container app-esaa-cookie-component"}],optIn:[{waitForThenClick:".btn-cookie-accepter"}],optOut:[{waitForThenClick:".btn-cookie-refuser"}]},{name:"etsy",prehideSelectors:["#gdpr-single-choice-overlay","#gdpr-privacy-settings"],detectCmp:[{exists:"#gdpr-single-choice-overlay"}],detectPopup:[{visible:"#gdpr-single-choice-overlay"}],optOut:[{click:"button[data-gdpr-open-full-settings]"},{waitForVisible:".gdpr-overlay-body input",timeout:3e3},{wait:1e3},{eval:"EVAL_ETSY_0"},{eval:"EVAL_ETSY_1"}],optIn:[{click:"button[data-gdpr-single-choice-accept]"}]},{name:"eu-cookie-compliance-banner",detectCmp:[{exists:"body.eu-cookie-compliance-popup-open"}],detectPopup:[{exists:"body.eu-cookie-compliance-popup-open"}],optIn:[{click:".agree-button"}],optOut:[{if:{visible:".decline-button,.eu-cookie-compliance-save-preferences-button"},then:[{click:".decline-button,.eu-cookie-compliance-save-preferences-button"}]},{hide:".eu-cookie-compliance-banner-info, #sliding-popup"}],test:[{eval:"EVAL_EU_COOKIE_COMPLIANCE_0"}]},{name:"EU Cookie Law",prehideSelectors:[".pea_cook_wrapper,.pea_cook_more_info_popover"],cosmetic:!0,detectCmp:[{exists:".pea_cook_wrapper"}],detectPopup:[{wait:500},{visible:".pea_cook_wrapper"}],optIn:[{click:"#pea_cook_btn"}],optOut:[{hide:".pea_cook_wrapper"}],test:[{eval:"EVAL_EU_COOKIE_LAW_0"}]},{name:"europa-eu",vendorUrl:"https://ec.europa.eu/",runContext:{urlPattern:"^https://[^/]*europa\\.eu/"},prehideSelectors:["#cookie-consent-banner"],detectCmp:[{exists:".cck-container"}],detectPopup:[{visible:".cck-container"}],optIn:[{waitForThenClick:'.cck-actions-button[href="#accept"]'}],optOut:[{waitForThenClick:'.cck-actions-button[href="#refuse"]',hide:".cck-container"}]},{name:"EZoic",prehideSelectors:["#ez-cookie-dialog-wrapper"],detectCmp:[{exists:"#ez-cookie-dialog-wrapper"}],detectPopup:[{visible:"#ez-cookie-dialog-wrapper"}],optIn:[{click:"#ez-accept-all",optional:!0},{eval:"EVAL_EZOIC_0",optional:!0}],optOut:[{wait:500},{click:"#ez-manage-settings"},{waitFor:"#ez-cookie-dialog input[type=checkbox]"},{click:"#ez-cookie-dialog input[type=checkbox]:checked",all:!0},{click:"#ez-save-settings"}],test:[{eval:"EVAL_EZOIC_1"}]},{name:"facebook",runContext:{urlPattern:"^https://([a-z0-9-]+\\.)?facebook\\.com/"},prehideSelectors:['div[data-testid="cookie-policy-manage-dialog"]'],detectCmp:[{exists:'div[data-testid="cookie-policy-manage-dialog"]'}],detectPopup:[{visible:'div[data-testid="cookie-policy-manage-dialog"]'}],optIn:[{waitForThenClick:'button[data-cookiebanner="accept_button"]'},{waitForVisible:'div[data-testid="cookie-policy-manage-dialog"]',check:"none"}],optOut:[{waitForThenClick:'button[data-cookiebanner="accept_only_essential_button"]'},{waitForVisible:'div[data-testid="cookie-policy-manage-dialog"]',check:"none"}]},{name:"fides",vendorUrl:"https://github.com/ethyca/fides",prehideSelectors:["#fides-overlay"],detectCmp:[{exists:"#fides-overlay #fides-banner"}],detectPopup:[{visible:"#fides-overlay #fides-banner"}],optIn:[{waitForThenClick:'#fides-banner [data-testid="Accept all-btn"]'}],optOut:[{waitForThenClick:'#fides-banner [data-testid="Reject all-btn"]'}]},{name:"funding-choices",prehideSelectors:[".fc-consent-root,.fc-dialog-container,.fc-dialog-overlay,.fc-dialog-content"],detectCmp:[{exists:".fc-consent-root"}],detectPopup:[{exists:".fc-dialog-container"}],optOut:[{click:".fc-cta-do-not-consent,.fc-cta-manage-options"},{click:".fc-preference-consent:checked,.fc-preference-legitimate-interest:checked",all:!0,optional:!0},{click:".fc-confirm-choices",optional:!0}],optIn:[{click:".fc-cta-consent"}]},{name:"geeks-for-geeks",runContext:{urlPattern:"^https://www\\.geeksforgeeks\\.org/"},cosmetic:!0,prehideSelectors:[".cookie-consent"],detectCmp:[{exists:".cookie-consent"}],detectPopup:[{visible:".cookie-consent"}],optIn:[{click:".cookie-consent button.consent-btn"}],optOut:[{hide:".cookie-consent"}]},{name:"generic-cosmetic",cosmetic:!0,prehideSelectors:["#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"],detectCmp:[{exists:"#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"}],detectPopup:[{visible:"#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"}],optIn:[],optOut:[{hide:"#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"}]},{name:"google-consent-standalone",prehideSelectors:[],detectCmp:[{exists:'a[href^="https://policies.google.com/technologies/cookies"'},{exists:'form[action^="https://consent.google."][action$=".com/save"]'}],detectPopup:[{visible:'a[href^="https://policies.google.com/technologies/cookies"'}],optIn:[{waitForThenClick:'form[action^="https://consent.google."][action$=".com/save"]:has(input[name=set_eom][value=false]) button'}],optOut:[{waitForThenClick:'form[action^="https://consent.google."][action$=".com/save"]:has(input[name=set_eom][value=true]) button'}]},{name:"google.com",prehideSelectors:[".HTjtHe#xe7COe"],detectCmp:[{exists:".HTjtHe#xe7COe"},{exists:'.HTjtHe#xe7COe a[href^="https://policies.google.com/technologies/cookies"]'}],detectPopup:[{visible:".HTjtHe#xe7COe button#W0wltc"}],optIn:[{waitForThenClick:".HTjtHe#xe7COe button#L2AGLb"}],optOut:[{waitForThenClick:".HTjtHe#xe7COe button#W0wltc"}],test:[{eval:"EVAL_GOOGLE_0"}]},{name:"gov.uk",detectCmp:[{exists:"#global-cookie-message"}],detectPopup:[{exists:"#global-cookie-message"}],optIn:[{click:"button[data-accept-cookies=true]"}],optOut:[{click:"button[data-reject-cookies=true],#reject-cookies"},{click:"button[data-hide-cookie-banner=true],#hide-cookie-decision"}]},{name:"hashicorp",vendorUrl:"https://hashicorp.com/",runContext:{urlPattern:"^https://[^.]*\\.hashicorp\\.com/"},prehideSelectors:["[data-testid=consent-banner]"],detectCmp:[{exists:"[data-testid=consent-banner]"}],detectPopup:[{visible:"[data-testid=consent-banner]"}],optIn:[{waitForThenClick:"[data-testid=accept]"}],optOut:[{waitForThenClick:"[data-testid=manage-preferences]"},{waitForThenClick:"[data-testid=consent-mgr-dialog] [data-ga-button=save-preferences]"}]},{name:"healthline-media",prehideSelectors:["#modal-host > div.no-hash > div.window-wrapper"],detectCmp:[{exists:"#modal-host > div.no-hash > div.window-wrapper, div[data-testid=qualtrics-container]"}],detectPopup:[{exists:"#modal-host > div.no-hash > div.window-wrapper, div[data-testid=qualtrics-container]"}],optIn:[{click:"#modal-host > div.no-hash > div.window-wrapper > div:last-child button"}],optOut:[{if:{exists:'#modal-host > div.no-hash > div.window-wrapper > div:last-child a[href="/privacy-settings"]'},then:[{click:'#modal-host > div.no-hash > div.window-wrapper > div:last-child a[href="/privacy-settings"]'}],else:[{waitForVisible:"div#__next"},{click:"#__next div:nth-child(1) > button:first-child"}]}]},{name:"hema",prehideSelectors:[".cookie-modal"],detectCmp:[{visible:".cookie-modal .cookie-accept-btn"}],detectPopup:[{visible:".cookie-modal .cookie-accept-btn"}],optIn:[{waitForThenClick:".cookie-modal .cookie-accept-btn"}],optOut:[{waitForThenClick:".cookie-modal .js-cookie-reject-btn"}],test:[{eval:"EVAL_HEMA_TEST_0"}]},{name:"hetzner.com",runContext:{urlPattern:"^https://www\\.hetzner\\.com/"},prehideSelectors:["#CookieConsent"],detectCmp:[{exists:"#CookieConsent"}],detectPopup:[{visible:"#CookieConsent"}],optIn:[{click:"#CookieConsentGiven"}],optOut:[{click:"#CookieConsentDeclined"}]},{name:"hl.co.uk",prehideSelectors:[".cookieModalContent","#cookie-banner-overlay"],detectCmp:[{exists:"#cookie-banner-overlay"}],detectPopup:[{exists:"#cookie-banner-overlay"}],optIn:[{click:"#acceptCookieButton"}],optOut:[{click:"#manageCookie"},{hide:".cookieSettingsModal"},{waitFor:"#AOCookieToggle"},{click:"#AOCookieToggle[aria-pressed=true]",optional:!0},{waitFor:"#TPCookieToggle"},{click:"#TPCookieToggle[aria-pressed=true]",optional:!0},{click:"#updateCookieButton"}]},{name:"hu-manity",vendorUrl:"https://hu-manity.co/",prehideSelectors:["#hu.hu-wrapper"],detectCmp:[{exists:"#hu.hu-visible"}],detectPopup:[{visible:"#hu.hu-visible"}],optIn:[{waitForThenClick:"[data-hu-action=cookies-notice-consent-choices-3]"},{waitForThenClick:"#hu-cookies-save"}],optOut:[{waitForThenClick:"#hu-cookies-save"}]},{name:"hubspot",detectCmp:[{exists:"#hs-eu-cookie-confirmation"}],detectPopup:[{visible:"#hs-eu-cookie-confirmation"}],optIn:[{click:"#hs-eu-confirmation-button"}],optOut:[{click:"#hs-eu-decline-button"}]},{name:"indeed.com",cosmetic:!0,prehideSelectors:["#CookiePrivacyNotice"],detectCmp:[{exists:"#CookiePrivacyNotice"}],detectPopup:[{visible:"#CookiePrivacyNotice"}],optIn:[{click:"#CookiePrivacyNotice button[data-gnav-element-name=CookiePrivacyNoticeOk]"}],optOut:[{hide:"#CookiePrivacyNotice"}]},{name:"ing.de",runContext:{urlPattern:"^https://www\\.ing\\.de/"},cosmetic:!0,prehideSelectors:['div[slot="backdrop"]'],detectCmp:[{exists:'[data-tag-name="ing-cc-dialog-frame"]'}],detectPopup:[{visible:'[data-tag-name="ing-cc-dialog-frame"]'}],optIn:[{click:['[data-tag-name="ing-cc-dialog-level0"]','[data-tag-name="ing-cc-button"][class*="accept"]']}],optOut:[{click:['[data-tag-name="ing-cc-dialog-level0"]','[data-tag-name="ing-cc-button"][class*="more"]']}]},{name:"instagram",vendorUrl:"https://instagram.com",runContext:{urlPattern:"^https://www\\.instagram\\.com/"},prehideSelectors:[".x78zum5.xdt5ytf.xg6iff7.x1n2onr6:has(._a9--)"],detectCmp:[{exists:".x1qjc9v5.x9f619.x78zum5.xdt5ytf.x1iyjqo2.xl56j7k"}],detectPopup:[{visible:".x1qjc9v5.x9f619.x78zum5.xdt5ytf.x1iyjqo2.xl56j7k"}],optIn:[{waitForThenClick:"._a9--._a9_0"}],optOut:[{waitForThenClick:"._a9--._a9_1"},{wait:2e3}]},{name:"ionos.de",prehideSelectors:[".privacy-consent--backdrop",".privacy-consent--modal"],detectCmp:[{exists:".privacy-consent--modal"}],detectPopup:[{visible:".privacy-consent--modal"}],optIn:[{click:"#selectAll"}],optOut:[{click:".footer-config-link"},{click:"#confirmSelection"}]},{name:"itopvpn.com",cosmetic:!0,prehideSelectors:[".pop-cookie"],detectCmp:[{exists:".pop-cookie"}],detectPopup:[{exists:".pop-cookie"}],optIn:[{click:"#_pcookie"}],optOut:[{hide:".pop-cookie"}]},{name:"iubenda",prehideSelectors:["#iubenda-cs-banner"],detectCmp:[{exists:"#iubenda-cs-banner"}],detectPopup:[{visible:".iubenda-cs-accept-btn"}],optIn:[{waitForThenClick:".iubenda-cs-accept-btn"}],optOut:[{waitForThenClick:".iubenda-cs-customize-btn"},{eval:"EVAL_IUBENDA_0"},{waitForThenClick:"#iubFooterBtn"}],test:[{eval:"EVAL_IUBENDA_1"}]},{name:"iWink",prehideSelectors:["body.cookies-request #cookie-bar"],detectCmp:[{exists:"body.cookies-request #cookie-bar"}],detectPopup:[{visible:"body.cookies-request #cookie-bar"}],optIn:[{waitForThenClick:"body.cookies-request #cookie-bar .allow-cookies"}],optOut:[{waitForThenClick:"body.cookies-request #cookie-bar .disallow-cookies"}],test:[{eval:"EVAL_IWINK_TEST"}]},{name:"jdsports",vendorUrl:"https://www.jdsports.co.uk/",runContext:{urlPattern:"^https://(www|m)\\.jdsports\\."},prehideSelectors:[".miniConsent,#PrivacyPolicyBanner"],detectCmp:[{exists:".miniConsent,#PrivacyPolicyBanner"}],detectPopup:[{visible:".miniConsent,#PrivacyPolicyBanner"}],optIn:[{waitForThenClick:".miniConsent .accept-all-cookies"}],optOut:[{if:{exists:"#PrivacyPolicyBanner"},then:[{hide:"#PrivacyPolicyBanner"}],else:[{waitForThenClick:"#cookie-settings"},{waitForThenClick:"#reject-all-cookies"}]}]},{name:"johnlewis.com",prehideSelectors:["div[class^=pecr-cookie-banner-]"],detectCmp:[{exists:"div[class^=pecr-cookie-banner-]"}],detectPopup:[{exists:"div[class^=pecr-cookie-banner-]"}],optOut:[{click:"button[data-test^=manage-cookies]"},{wait:"500"},{click:"label[data-test^=toggle][class*=checked]:not([class*=disabled])",all:!0,optional:!0},{click:"button[data-test=save-preferences]"}],optIn:[{click:"button[data-test=allow-all]"}]},{name:"jquery.cookieBar",vendorUrl:"https://github.com/kovarp/jquery.cookieBar",prehideSelectors:[".cookie-bar"],cosmetic:!0,detectCmp:[{exists:".cookie-bar .cookie-bar__message,.cookie-bar .cookie-bar__buttons"}],detectPopup:[{visible:".cookie-bar .cookie-bar__message,.cookie-bar .cookie-bar__buttons",check:"any"}],optIn:[{click:".cookie-bar .cookie-bar__btn"}],optOut:[{hide:".cookie-bar"}],test:[{visible:".cookie-bar .cookie-bar__message,.cookie-bar .cookie-bar__buttons",check:"none"},{eval:"EVAL_JQUERY_COOKIEBAR_0"}]},{name:"justwatch.com",prehideSelectors:[".consent-banner"],detectCmp:[{exists:".consent-banner .consent-banner__actions"}],detectPopup:[{visible:".consent-banner .consent-banner__actions"}],optIn:[{click:".consent-banner__actions button.basic-button.primary"}],optOut:[{click:".consent-banner__actions button.basic-button.secondary"},{waitForThenClick:".consent-modal__footer button.basic-button.secondary"},{waitForThenClick:".consent-modal ion-content > div > a:nth-child(9)"},{click:"label.consent-switch input[type=checkbox]:checked",all:!0,optional:!0},{waitForVisible:".consent-modal__footer button.basic-button.primary"},{click:".consent-modal__footer button.basic-button.primary"}]},{name:"ketch",vendorUrl:"https://www.ketch.com",runContext:{frame:!1,main:!0},intermediate:!1,prehideSelectors:["#lanyard_root div[role='dialog']"],detectCmp:[{exists:"#lanyard_root div[role='dialog']"}],detectPopup:[{visible:"#lanyard_root div[role='dialog']"}],optIn:[{if:{exists:"#lanyard_root button[class='confirmButton']"},then:[{waitForThenClick:"#lanyard_root div[class*=buttons] > :nth-child(2)"},{click:"#lanyard_root button[class='confirmButton']"}],else:[{waitForThenClick:"#lanyard_root div[class*=buttons] > :nth-child(2)"}]}],optOut:[{if:{exists:"#lanyard_root [aria-describedby=banner-description]"},then:[{waitForThenClick:"#lanyard_root div[class*=buttons] > button[class*=secondaryButton]",comment:"can be either settings or reject button"}]},{waitFor:"#lanyard_root [aria-describedby=preference-description],#lanyard_root [aria-describedby=modal-description]",timeout:1e3,optional:!0},{if:{exists:"#lanyard_root [aria-describedby=preference-description],#lanyard_root [aria-describedby=modal-description]"},then:[{waitForThenClick:"#lanyard_root button[class*=rejectButton]"},{click:"#lanyard_root button[class*=confirmButton],#lanyard_root div[class*=actions_] > button:nth-child(1)"}]}]},{name:"kleinanzeigen-de",runContext:{urlPattern:"^https?://(www\\.)?kleinanzeigen\\.de"},prehideSelectors:["#gdpr-banner-container"],detectCmp:[{any:[{exists:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-cmp-button]"},{exists:"#ConsentManagementPage"}]}],detectPopup:[{any:[{visible:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-cmp-button]"},{visible:"#ConsentManagementPage"}]}],optIn:[{if:{exists:"#gdpr-banner-container #gdpr-banner"},then:[{click:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-accept]"}],else:[{click:"#ConsentManagementPage .Button-primary"}]}],optOut:[{if:{exists:"#gdpr-banner-container #gdpr-banner"},then:[{click:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-cmp-button]"}],else:[{click:"#ConsentManagementPage .Button-secondary"}]}]},{name:"lightbox",prehideSelectors:[".darken-layer.open,.lightbox.lightbox--cookie-consent"],detectCmp:[{exists:"body.cookie-consent-is-active div.lightbox--cookie-consent > div.lightbox__content > div.cookie-consent[data-jsb]"}],detectPopup:[{visible:"body.cookie-consent-is-active div.lightbox--cookie-consent > div.lightbox__content > div.cookie-consent[data-jsb]"}],optOut:[{click:".cookie-consent__footer > button[type='submit']:not([data-button='selectAll'])"}],optIn:[{click:".cookie-consent__footer > button[type='submit'][data-button='selectAll']"}]},{name:"lineagrafica",vendorUrl:"https://addons.prestashop.com/en/legal/8734-eu-cookie-law-gdpr-banner-blocker.html",cosmetic:!0,prehideSelectors:["#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"],detectCmp:[{exists:"#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"}],detectPopup:[{exists:"#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"}],optIn:[{waitForThenClick:"#lgcookieslaw_accept"}],optOut:[{hide:"#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"}]},{name:"linkedin.com",prehideSelectors:[".artdeco-global-alert[type=COOKIE_CONSENT]"],detectCmp:[{exists:".artdeco-global-alert[type=COOKIE_CONSENT]"}],detectPopup:[{visible:".artdeco-global-alert[type=COOKIE_CONSENT]"}],optIn:[{waitForVisible:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=ACCEPT]"},{wait:500},{waitForThenClick:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=ACCEPT]"}],optOut:[{waitForVisible:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=DENY]"},{wait:500},{waitForThenClick:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=DENY]"}],test:[{waitForVisible:".artdeco-global-alert[type=COOKIE_CONSENT]",check:"none"}]},{name:"livejasmin",vendorUrl:"https://www.livejasmin.com/",runContext:{urlPattern:"^https://(m|www)\\.livejasmin\\.com/"},prehideSelectors:["#consent_modal"],detectCmp:[{exists:"#consent_modal"}],detectPopup:[{visible:"#consent_modal"}],optIn:[{waitForThenClick:"#consent_modal button[data-testid=ButtonStyledButton]:first-of-type"}],optOut:[{waitForThenClick:"#consent_modal button[data-testid=ButtonStyledButton]:nth-of-type(2)"},{waitForVisible:"[data-testid=PrivacyPreferenceCenterWithConsentCookieContent]"},{click:"[data-testid=PrivacyPreferenceCenterWithConsentCookieContent] input[data-testid=PrivacyPreferenceCenterWithConsentCookieSwitch]:checked",optional:!0,all:!0},{waitForThenClick:"[data-testid=PrivacyPreferenceCenterWithConsentCookieContent] button[data-testid=ButtonStyledButton]:last-child"}]},{name:"macpaw.com",cosmetic:!0,prehideSelectors:['div[data-banner="cookies"]'],detectCmp:[{exists:'div[data-banner="cookies"]'}],detectPopup:[{exists:'div[data-banner="cookies"]'}],optIn:[{click:'button[data-banner-close="cookies"]'}],optOut:[{hide:'div[data-banner="cookies"]'}]},{name:"marksandspencer.com",cosmetic:!0,detectCmp:[{exists:".navigation-cookiebbanner"}],detectPopup:[{visible:".navigation-cookiebbanner"}],optOut:[{hide:".navigation-cookiebbanner"}],optIn:[{click:".navigation-cookiebbanner__submit"}]},{name:"mediamarkt.de",prehideSelectors:["div[aria-labelledby=pwa-consent-layer-title]","div[class^=StyledConsentLayerWrapper-]"],detectCmp:[{exists:"div[aria-labelledby^=pwa-consent-layer-title]"}],detectPopup:[{exists:"div[aria-labelledby^=pwa-consent-layer-title]"}],optOut:[{click:"button[data-test^=pwa-consent-layer-deny-all]"}],optIn:[{click:"button[data-test^=pwa-consent-layer-accept-all"}]},{name:"Mediavine",prehideSelectors:['[data-name="mediavine-gdpr-cmp"]'],detectCmp:[{exists:'[data-name="mediavine-gdpr-cmp"]'}],detectPopup:[{wait:500},{visible:'[data-name="mediavine-gdpr-cmp"]'}],optIn:[{waitForThenClick:'[data-name="mediavine-gdpr-cmp"] [format="primary"]'}],optOut:[{waitForThenClick:'[data-name="mediavine-gdpr-cmp"] [data-view="manageSettings"]'},{waitFor:'[data-name="mediavine-gdpr-cmp"] input[type=checkbox]'},{eval:"EVAL_MEDIAVINE_0",optional:!0},{click:'[data-name="mediavine-gdpr-cmp"] [format="secondary"]'}]},{name:"microsoft.com",prehideSelectors:["#wcpConsentBannerCtrl"],detectCmp:[{exists:"#wcpConsentBannerCtrl"}],detectPopup:[{exists:"#wcpConsentBannerCtrl"}],optOut:[{eval:"EVAL_MICROSOFT_0"}],optIn:[{eval:"EVAL_MICROSOFT_1"}],test:[{eval:"EVAL_MICROSOFT_2"}]},{name:"midway-usa",runContext:{urlPattern:"^https://www\\.midwayusa\\.com/"},cosmetic:!0,prehideSelectors:["#cookie-container"],detectCmp:[{exists:['div[aria-label="Cookie Policy Banner"]']}],detectPopup:[{visible:"#cookie-container"}],optIn:[{click:"button#cookie-btn"}],optOut:[{hide:'div[aria-label="Cookie Policy Banner"]'}]},{name:"moneysavingexpert.com",detectCmp:[{exists:"dialog[data-testid=accept-our-cookies-dialog]"}],detectPopup:[{visible:"dialog[data-testid=accept-our-cookies-dialog]"}],optIn:[{click:"#banner-accept"}],optOut:[{click:"#banner-manage"},{click:"#pc-confirm"}]},{name:"monzo.com",prehideSelectors:[".cookie-alert, cookie-alert__content"],detectCmp:[{exists:'div.cookie-alert[role="dialog"]'},{exists:'a[href*="monzo"]'}],detectPopup:[{visible:".cookie-alert__content"}],optIn:[{click:".js-accept-cookie-policy"}],optOut:[{click:".js-decline-cookie-policy"}]},{name:"Moove",prehideSelectors:["#moove_gdpr_cookie_info_bar"],detectCmp:[{exists:"#moove_gdpr_cookie_info_bar"}],detectPopup:[{visible:"#moove_gdpr_cookie_info_bar:not(.moove-gdpr-info-bar-hidden)"}],optIn:[{waitForThenClick:".moove-gdpr-infobar-allow-all"}],optOut:[{if:{exists:"#moove_gdpr_cookie_info_bar .change-settings-button"},then:[{click:"#moove_gdpr_cookie_info_bar .change-settings-button"},{waitForVisible:"#moove_gdpr_cookie_modal"},{eval:"EVAL_MOOVE_0"},{click:".moove-gdpr-modal-save-settings"}],else:[{hide:"#moove_gdpr_cookie_info_bar"}]}],test:[{visible:"#moove_gdpr_cookie_info_bar",check:"none"}]},{name:"national-lottery.co.uk",detectCmp:[{exists:".cuk_cookie_consent"}],detectPopup:[{visible:".cuk_cookie_consent",check:"any"}],optOut:[{click:".cuk_cookie_consent_manage_pref"},{click:".cuk_cookie_consent_save_pref"},{click:".cuk_cookie_consent_close"}],optIn:[{click:".cuk_cookie_consent_accept_all"}]},{name:"nba.com",runContext:{urlPattern:"^https://(www\\.)?nba.com/"},cosmetic:!0,prehideSelectors:["#onetrust-banner-sdk"],detectCmp:[{exists:"#onetrust-banner-sdk"}],detectPopup:[{visible:"#onetrust-banner-sdk"}],optIn:[{click:"#onetrust-accept-btn-handler"}],optOut:[{hide:"#onetrust-banner-sdk"}]},{name:"netflix.de",detectCmp:[{exists:"#cookie-disclosure"}],detectPopup:[{visible:".cookie-disclosure-message",check:"any"}],optIn:[{click:".btn-accept"}],optOut:[{hide:"#cookie-disclosure"},{click:".btn-reject"}]},{name:"nhs.uk",prehideSelectors:["#nhsuk-cookie-banner"],detectCmp:[{exists:"#nhsuk-cookie-banner"}],detectPopup:[{exists:"#nhsuk-cookie-banner"}],optOut:[{click:"#nhsuk-cookie-banner__link_accept"}],optIn:[{click:"#nhsuk-cookie-banner__link_accept_analytics"}]},{name:"notice-cookie",prehideSelectors:[".button--notice"],cosmetic:!0,detectCmp:[{exists:".notice--cookie"}],detectPopup:[{visible:".notice--cookie"}],optIn:[{click:".button--notice"}],optOut:[{hide:".notice--cookie"}]},{name:"nrk.no",cosmetic:!0,prehideSelectors:[".nrk-masthead__info-banner--cookie"],detectCmp:[{exists:".nrk-masthead__info-banner--cookie"}],detectPopup:[{exists:".nrk-masthead__info-banner--cookie"}],optIn:[{click:"div.nrk-masthead__info-banner--cookie button > span:has(+ svg.nrk-close)"}],optOut:[{hide:".nrk-masthead__info-banner--cookie"}]},{name:"obi.de",prehideSelectors:[".disc-cp--active"],detectCmp:[{exists:".disc-cp-modal__modal"}],detectPopup:[{visible:".disc-cp-modal__modal"}],optIn:[{click:".js-disc-cp-accept-all"}],optOut:[{click:".js-disc-cp-deny-all"}]},{name:"om",vendorUrl:"https://olli-machts.de/en/extension/cookie-manager",prehideSelectors:[".tx-om-cookie-consent"],detectCmp:[{exists:".tx-om-cookie-consent .active[data-omcookie-panel]"}],detectPopup:[{exists:".tx-om-cookie-consent .active[data-omcookie-panel]"}],optIn:[{waitForThenClick:"[data-omcookie-panel-save=all]"}],optOut:[{if:{exists:"[data-omcookie-panel-save=min]"},then:[{waitForThenClick:"[data-omcookie-panel-save=min]"}],else:[{click:"input[data-omcookie-panel-grp]:checked:not(:disabled)",all:!0,optional:!0},{waitForThenClick:"[data-omcookie-panel-save=save]"}]}]},{name:"onlyFans.com",runContext:{urlPattern:"^https://onlyfans\\.com/"},prehideSelectors:["div.b-cookies-informer"],detectCmp:[{exists:"div.b-cookies-informer"}],detectPopup:[{exists:"div.b-cookies-informer"}],optIn:[{click:"div.b-cookies-informer__nav > button:nth-child(2)"}],optOut:[{click:"div.b-cookies-informer__nav > button:nth-child(1)"},{if:{exists:"div.b-cookies-informer__switchers"},then:[{click:"div.b-cookies-informer__switchers input:not([disabled])",all:!0},{click:"div.b-cookies-informer__nav > button"}]}]},{name:"openli",vendorUrl:"https://openli.com",prehideSelectors:[".legalmonster-cleanslate"],detectCmp:[{exists:".legalmonster-cleanslate"}],detectPopup:[{visible:".legalmonster-cleanslate #lm-cookie-wall-container",check:"any"}],optIn:[{waitForThenClick:"#lm-accept-all"}],optOut:[{waitForThenClick:"#lm-accept-necessary"}]},{name:"opera.com",vendorUrl:"https://unknown",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,prehideSelectors:[],detectCmp:[{exists:"#cookie-consent .manage-cookies__btn"}],detectPopup:[{visible:"#cookie-consent .cookie-basic-consent__btn"}],optIn:[{waitForThenClick:"#cookie-consent .cookie-basic-consent__btn"}],optOut:[{waitForThenClick:"#cookie-consent .manage-cookies__btn"},{waitForThenClick:"#cookie-consent .active.marketing_option_switch.cookie-consent__switch",all:!0},{waitForThenClick:"#cookie-consent .cookie-selection__btn"}],test:[{eval:"EVAL_OPERA_0"}]},{name:"osano",prehideSelectors:[".osano-cm-window,.osano-cm-dialog"],detectCmp:[{exists:".osano-cm-window"}],detectPopup:[{visible:".osano-cm-dialog"}],optIn:[{click:".osano-cm-accept-all",optional:!0}],optOut:[{waitForThenClick:".osano-cm-denyAll"}]},{name:"otto.de",prehideSelectors:[".cookieBanner--visibility"],detectCmp:[{exists:".cookieBanner--visibility"}],detectPopup:[{visible:".cookieBanner__wrapper"}],optIn:[{click:".js_cookieBannerPermissionButton"}],optOut:[{click:".js_cookieBannerProhibitionButton"}]},{name:"ourworldindata",vendorUrl:"https://ourworldindata.org/",runContext:{urlPattern:"^https://ourworldindata\\.org/"},prehideSelectors:[".cookie-manager"],detectCmp:[{exists:".cookie-manager"}],detectPopup:[{visible:".cookie-manager .cookie-notice.open"}],optIn:[{waitForThenClick:".cookie-notice [data-test=accept]"}],optOut:[{waitForThenClick:".cookie-notice [data-test=reject]"}]},{name:"pabcogypsum",vendorUrl:"https://unknown",prehideSelectors:[".js-cookie-notice:has(#cookie_settings-form)"],detectCmp:[{exists:".js-cookie-notice #cookie_settings-form"}],detectPopup:[{visible:".js-cookie-notice #cookie_settings-form"}],optIn:[{waitForThenClick:".js-cookie-notice button[value=allow]"}],optOut:[{waitForThenClick:".js-cookie-notice button[value=disable]"}]},{name:"paypal-us",prehideSelectors:["#ccpaCookieContent_wrapper, article.ppvx_modal--overpanel"],detectCmp:[{exists:"#ccpaCookieBanner, .privacy-sheet-content"}],detectPopup:[{exists:"#ccpaCookieBanner, .privacy-sheet-content"}],optIn:[{click:"#acceptAllButton"}],optOut:[{if:{exists:"a#manageCookiesLink"},then:[{click:"a#manageCookiesLink"}],else:[{waitForVisible:".privacy-sheet-content #formContent"},{click:"#formContent .cookiepref-11m2iee-checkbox_base input:checked",all:!0,optional:!0},{click:".confirmCookie #submitCookiesBtn"}]}]},{name:"paypal.com",prehideSelectors:["#gdprCookieBanner"],detectCmp:[{exists:"#gdprCookieBanner"}],detectPopup:[{visible:"#gdprCookieContent_wrapper"}],optIn:[{click:"#acceptAllButton"}],optOut:[{wait:200},{click:".gdprCookieBanner_decline-button"}],test:[{wait:500},{eval:"EVAL_PAYPAL_0"}]},{name:"pinetools.com",cosmetic:!0,prehideSelectors:["#aviso_cookies"],detectCmp:[{exists:"#aviso_cookies"}],detectPopup:[{exists:".lang_en #aviso_cookies"}],optIn:[{click:"#aviso_cookies .a_boton_cerrar"}],optOut:[{hide:"#aviso_cookies"}]},{name:"pmc",cosmetic:!0,prehideSelectors:["#pmc-pp-tou--notice"],detectCmp:[{exists:"#pmc-pp-tou--notice"}],detectPopup:[{visible:"#pmc-pp-tou--notice"}],optIn:[{click:"span.pmc-pp-tou--notice-close-btn"}],optOut:[{hide:"#pmc-pp-tou--notice"}]},{name:"pornhub.com",runContext:{urlPattern:"^https://(www\\.)?pornhub\\.com/"},cosmetic:!0,prehideSelectors:[".cookiesBanner"],detectCmp:[{exists:".cookiesBanner"}],detectPopup:[{visible:".cookiesBanner"}],optIn:[{click:".cookiesBanner .okButton"}],optOut:[{hide:".cookiesBanner"}]},{name:"pornpics.com",cosmetic:!0,prehideSelectors:["#cookie-contract"],detectCmp:[{exists:"#cookie-contract"}],detectPopup:[{visible:"#cookie-contract"}],optIn:[{click:"#cookie-contract .icon-cross"}],optOut:[{hide:"#cookie-contract"}]},{name:"PrimeBox CookieBar",prehideSelectors:["#cookie-bar"],detectCmp:[{exists:"#cookie-bar .cb-enable,#cookie-bar .cb-disable,#cookie-bar .cb-policy"}],detectPopup:[{visible:"#cookie-bar .cb-enable,#cookie-bar .cb-disable,#cookie-bar .cb-policy",check:"any"}],optIn:[{waitForThenClick:"#cookie-bar .cb-enable"}],optOut:[{click:"#cookie-bar .cb-disable",optional:!0},{hide:"#cookie-bar"}],test:[{eval:"EVAL_PRIMEBOX_0"}]},{name:"privacymanager.io",prehideSelectors:["#gdpr-consent-tool-wrapper",'iframe[src^="https://cmp-consent-tool.privacymanager.io"]'],runContext:{urlPattern:"^https://cmp-consent-tool\\.privacymanager\\.io/",main:!1,frame:!0},detectCmp:[{exists:"button#save"}],detectPopup:[{visible:"button#save"}],optIn:[{click:"button#save"}],optOut:[{if:{exists:"#denyAll"},then:[{click:"#denyAll"},{waitForThenClick:".okButton"}],else:[{waitForThenClick:"#manageSettings"},{waitFor:".purposes-overview-list"},{waitFor:"button#saveAndExit"},{click:"span[role=checkbox][aria-checked=true]",all:!0,optional:!0},{click:"button#saveAndExit"}]}]},{name:"productz.com",vendorUrl:"https://productz.com/",runContext:{urlPattern:"^https://productz\\.com/"},prehideSelectors:[],detectCmp:[{exists:".c-modal.is-active"}],detectPopup:[{visible:".c-modal.is-active"}],optIn:[{waitForThenClick:".c-modal.is-active .is-accept"}],optOut:[{waitForThenClick:".c-modal.is-active .is-dismiss"}]},{name:"pubtech",prehideSelectors:["#pubtech-cmp"],detectCmp:[{exists:"#pubtech-cmp"}],detectPopup:[{visible:"#pubtech-cmp #pt-actions"}],optIn:[{if:{exists:"#pt-accept-all"},then:[{click:"#pubtech-cmp #pt-actions #pt-accept-all"}],else:[{click:"#pubtech-cmp #pt-actions button:nth-of-type(2)"}]}],optOut:[{click:"#pubtech-cmp #pt-close"}],test:[{eval:"EVAL_PUBTECH_0"}]},{name:"quantcast",prehideSelectors:["#qc-cmp2-main,#qc-cmp2-container"],detectCmp:[{exists:"#qc-cmp2-container"}],detectPopup:[{visible:"#qc-cmp2-ui"}],optOut:[{click:'.qc-cmp2-summary-buttons > button[mode="secondary"]'},{waitFor:"#qc-cmp2-ui"},{click:'.qc-cmp2-toggle-switch > button[aria-checked="true"]',all:!0,optional:!0},{click:'.qc-cmp2-main button[aria-label="REJECT ALL"]',optional:!0},{waitForThenClick:'.qc-cmp2-main button[aria-label="SAVE & EXIT"],.qc-cmp2-buttons-desktop > button[mode="primary"]',timeout:5e3}],optIn:[{click:'.qc-cmp2-summary-buttons > button[mode="primary"]'}]},{name:"reddit.com",runContext:{urlPattern:"^https://www\\.reddit\\.com/"},prehideSelectors:["[bundlename=reddit_cookie_banner]"],detectCmp:[{exists:"reddit-cookie-banner"}],detectPopup:[{visible:"reddit-cookie-banner"}],optIn:[{waitForThenClick:["reddit-cookie-banner","#accept-all-cookies-button > button"]}],optOut:[{waitForThenClick:["reddit-cookie-banner","#reject-nonessential-cookies-button > button"]}],test:[{eval:"EVAL_REDDIT_0"}]},{name:"rog-forum.asus.com",runContext:{urlPattern:"^https://rog-forum\\.asus\\.com/"},prehideSelectors:["#cookie-policy-info"],detectCmp:[{exists:"#cookie-policy-info"}],detectPopup:[{visible:"#cookie-policy-info"}],optIn:[{click:'div.cookie-btn-box > div[aria-label="Accept"]'}],optOut:[{click:'div.cookie-btn-box > div[aria-label="Reject"]'},{waitForThenClick:'.cookie-policy-lightbox-bottom > div[aria-label="Save Settings"]'}]},{name:"roofingmegastore.co.uk",runContext:{urlPattern:"^https://(www\\.)?roofingmegastore\\.co\\.uk"},prehideSelectors:["#m-cookienotice"],detectCmp:[{exists:"#m-cookienotice"}],detectPopup:[{visible:"#m-cookienotice"}],optIn:[{click:"#accept-cookies"}],optOut:[{click:"#manage-cookies"},{waitForThenClick:"#accept-selected"}]},{name:"samsung.com",runContext:{urlPattern:"^https://www\\.samsung\\.com/"},cosmetic:!0,prehideSelectors:["div.cookie-bar"],detectCmp:[{exists:"div.cookie-bar"}],detectPopup:[{visible:"div.cookie-bar"}],optIn:[{click:"div.cookie-bar__manage > a"}],optOut:[{hide:"div.cookie-bar"}]},{name:"setapp.com",vendorUrl:"https://setapp.com/",cosmetic:!0,runContext:{urlPattern:"^https://setapp\\.com/"},prehideSelectors:[],detectCmp:[{exists:".cookie-banner.js-cookie-banner"}],detectPopup:[{visible:".cookie-banner.js-cookie-banner"}],optIn:[{waitForThenClick:".cookie-banner.js-cookie-banner button"}],optOut:[{hide:".cookie-banner.js-cookie-banner"}]},{name:"sibbo",prehideSelectors:["sibbo-cmp-layout"],detectCmp:[{exists:"sibbo-cmp-layout"}],detectPopup:[{visible:"#rejectAllMain"}],optIn:[{click:"#acceptAllMain"}],optOut:[{click:"#rejectAllMain"}]},{name:"similarweb.com",cosmetic:!0,prehideSelectors:[".app-cookies-notification"],detectCmp:[{exists:".app-cookies-notification"}],detectPopup:[{exists:".app-layout .app-cookies-notification"}],optIn:[{click:"button.app-cookies-notification__dismiss"}],optOut:[{hide:".app-layout .app-cookies-notification"}]},{name:"Sirdata",cosmetic:!1,prehideSelectors:["#sd-cmp"],detectCmp:[{exists:"#sd-cmp"}],detectPopup:[{visible:"#sd-cmp"}],optIn:[{waitForThenClick:"#sd-cmp .sd-cmp-3cRQ2"}],optOut:[{waitForThenClick:["#sd-cmp","xpath///span[contains(., 'Do not accept') or contains(., 'Acceptera inte') or contains(., 'No aceptar') or contains(., 'Ikke acceptere') or contains(., 'Nicht akzeptieren') or contains(., 'Не приемам') or contains(., 'Να μην γίνει αποδοχή') or contains(., 'Niet accepteren') or contains(., 'Nepřijímat') or contains(., 'Nie akceptuj') or contains(., 'Nu acceptați') or contains(., 'Não aceitar') or contains(., 'Continuer sans accepter') or contains(., 'Non accettare') or contains(., 'Nem fogad el')]"]}]},{name:"snigel",detectCmp:[{exists:".snigel-cmp-framework"}],detectPopup:[{visible:".snigel-cmp-framework"}],optOut:[{click:"#sn-b-custom"},{click:"#sn-b-save"}],test:[{eval:"EVAL_SNIGEL_0"}],optIn:[{click:".snigel-cmp-framework #accept-choices"}]},{name:"steampowered.com",detectCmp:[{exists:".cookiepreferences_popup"},{visible:".cookiepreferences_popup"}],detectPopup:[{visible:".cookiepreferences_popup"}],optOut:[{click:"#rejectAllButton"}],optIn:[{click:"#acceptAllButton"}],test:[{wait:1e3},{eval:"EVAL_STEAMPOWERED_0"}]},{name:"strato.de",prehideSelectors:[".consent__wrapper"],runContext:{urlPattern:"^https://www\\.strato\\.de/"},detectCmp:[{exists:".consent"}],detectPopup:[{visible:".consent"}],optIn:[{click:"button.consentAgree"}],optOut:[{click:"button.consentSettings"},{waitForThenClick:"button#consentSubmit"}]},{name:"svt.se",vendorUrl:"https://www.svt.se/",runContext:{urlPattern:"^https://www\\.svt\\.se/"},prehideSelectors:["[class*=CookieConsent__root___]"],detectCmp:[{exists:"[class*=CookieConsent__root___]"}],detectPopup:[{visible:"[class*=CookieConsent__modal___]"}],optIn:[{waitForThenClick:"[class*=CookieConsent__modal___] > div > button[class*=primary]"}],optOut:[{waitForThenClick:"[class*=CookieConsent__modal___] > div > button[class*=secondary]:nth-child(2)"}],test:[{eval:"EVAL_SVT_TEST"}]},{name:"takealot.com",cosmetic:!0,prehideSelectors:['div[class^="cookies-banner-module_"]'],detectCmp:[{exists:'div[class^="cookies-banner-module_cookie-banner_"]'}],detectPopup:[{exists:'div[class^="cookies-banner-module_cookie-banner_"]'}],optIn:[{click:'button[class*="cookies-banner-module_dismiss-button_"]'}],optOut:[{hide:'div[class^="cookies-banner-module_"]'},{if:{exists:'div[class^="cookies-banner-module_small-cookie-banner_"]'},then:[{eval:"EVAL_TAKEALOT_0"}],else:[]}]},{name:"tarteaucitron.js",prehideSelectors:["#tarteaucitronRoot"],detectCmp:[{exists:"#tarteaucitronRoot"}],detectPopup:[{visible:"#tarteaucitronRoot #tarteaucitronAlertBig",check:"any"}],optIn:[{eval:"EVAL_TARTEAUCITRON_1"}],optOut:[{eval:"EVAL_TARTEAUCITRON_0"}],test:[{eval:"EVAL_TARTEAUCITRON_2",comment:"sometimes there are required categories, so we check that at least something is false"}]},{name:"taunton",vendorUrl:"https://www.taunton.com/",prehideSelectors:["#taunton-user-consent__overlay"],detectCmp:[{exists:"#taunton-user-consent__overlay"}],detectPopup:[{exists:"#taunton-user-consent__overlay:not([aria-hidden=true])"}],optIn:[{click:"#taunton-user-consent__toolbar input[type=checkbox]:not(:checked)"},{click:"#taunton-user-consent__toolbar button[type=submit]"}],optOut:[{click:"#taunton-user-consent__toolbar input[type=checkbox]:checked",optional:!0,all:!0},{click:"#taunton-user-consent__toolbar button[type=submit]"}],test:[{eval:"EVAL_TAUNTON_TEST"}]},{name:"Tealium",prehideSelectors:["#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs,#__tealiumImplicitmodal,#consent-layer"],detectCmp:[{exists:"#__tealiumGDPRecModal *,#__tealiumGDPRcpPrefs *,#__tealiumImplicitmodal *"},{eval:"EVAL_TEALIUM_0"}],detectPopup:[{visible:"#__tealiumGDPRecModal *,#__tealiumGDPRcpPrefs *,#__tealiumImplicitmodal *",check:"any"}],optOut:[{eval:"EVAL_TEALIUM_1"},{eval:"EVAL_TEALIUM_DONOTSELL"},{hide:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs,#__tealiumImplicitmodal"},{waitForThenClick:"#cm-acceptNone,.js-accept-essential-cookies",timeout:1e3,optional:!0}],optIn:[{hide:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs"},{eval:"EVAL_TEALIUM_2"}],test:[{eval:"EVAL_TEALIUM_3"},{eval:"EVAL_TEALIUM_DONOTSELL_CHECK"},{visible:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs",check:"none"}]},{name:"temu",vendorUrl:"https://temu.com",runContext:{urlPattern:"^https://[^/]*temu\\.com/"},prehideSelectors:["._2d-8vq-W,._1UdBUwni"],detectCmp:[{exists:"._3YCsmIaS"}],detectPopup:[{visible:"._3YCsmIaS"}],optIn:[{waitForThenClick:"._3fKiu5wx._3zN5SumS._3tAK973O.IYOfhWEs.VGNGF1pA"}],optOut:[{waitForThenClick:"._3fKiu5wx._1_XToJBF._3tAK973O.IYOfhWEs.VGNGF1pA"}]},{name:"Termly",prehideSelectors:["#termly-code-snippet-support"],detectCmp:[{exists:"#termly-code-snippet-support"}],detectPopup:[{visible:"#termly-code-snippet-support div"}],optIn:[{waitForThenClick:'[data-tid="banner-accept"]'}],optOut:[{if:{exists:'[data-tid="banner-decline"]'},then:[{click:'[data-tid="banner-decline"]'}],else:[{click:".t-preference-button"},{wait:500},{if:{exists:".t-declineAllButton"},then:[{click:".t-declineAllButton"}],else:[{waitForThenClick:".t-preference-modal input[type=checkbox][checked]:not([disabled])",all:!0},{waitForThenClick:".t-saveButton"}]}]}]},{name:"termsfeed",vendorUrl:"https://termsfeed.com",comment:"v4.x.x",prehideSelectors:[".termsfeed-com---nb"],detectCmp:[{exists:".termsfeed-com---nb"}],detectPopup:[{visible:".termsfeed-com---nb"}],optIn:[{waitForThenClick:".cc-nb-okagree"}],optOut:[{waitForThenClick:".cc-nb-reject"}]},{name:"termsfeed3",vendorUrl:"https://termsfeed.com",comment:"v3.x.x",prehideSelectors:[".cc_dialog.cc_css_reboot,.cc_overlay_lock"],detectCmp:[{exists:".cc_dialog.cc_css_reboot"}],detectPopup:[{visible:".cc_dialog.cc_css_reboot"}],optIn:[{waitForThenClick:".cc_dialog.cc_css_reboot .cc_b_ok"}],optOut:[{if:{exists:".cc_dialog.cc_css_reboot .cc_b_cp"},then:[{click:".cc_dialog.cc_css_reboot .cc_b_cp"},{waitForVisible:".cookie-consent-preferences-dialog .cc_cp_f_save button"},{waitForThenClick:".cookie-consent-preferences-dialog .cc_cp_f_save button"}],else:[{hide:".cc_dialog.cc_css_reboot,.cc_overlay_lock"}]}]},{name:"Test page cosmetic CMP",cosmetic:!0,prehideSelectors:["#privacy-test-page-cmp-test-prehide"],detectCmp:[{exists:"#privacy-test-page-cmp-test-banner"}],detectPopup:[{visible:"#privacy-test-page-cmp-test-banner"}],optIn:[{waitFor:"#accept-all"},{click:"#accept-all"}],optOut:[{hide:"#privacy-test-page-cmp-test-banner"}],test:[{wait:500},{eval:"EVAL_TESTCMP_COSMETIC_0"}]},{name:"Test page CMP",prehideSelectors:["#reject-all"],detectCmp:[{exists:"#privacy-test-page-cmp-test"}],detectPopup:[{visible:"#privacy-test-page-cmp-test"}],optIn:[{waitFor:"#accept-all"},{click:"#accept-all"}],optOut:[{waitFor:"#reject-all"},{click:"#reject-all"}],test:[{eval:"EVAL_TESTCMP_0"}]},{name:"thalia.de",prehideSelectors:[".consent-banner-box"],detectCmp:[{exists:"consent-banner[component=consent-banner]"}],detectPopup:[{visible:".consent-banner-box"}],optIn:[{click:".button-zustimmen"}],optOut:[{click:"button[data-consent=disagree]"}]},{name:"thefreedictionary.com",prehideSelectors:["#cmpBanner"],detectCmp:[{exists:"#cmpBanner"}],detectPopup:[{visible:"#cmpBanner"}],optIn:[{eval:"EVAL_THEFREEDICTIONARY_1"}],optOut:[{eval:"EVAL_THEFREEDICTIONARY_0"}]},{name:"theverge",runContext:{frame:!1,main:!0,urlPattern:"^https://(www)?\\.theverge\\.com"},intermediate:!1,prehideSelectors:[".duet--cta--cookie-banner"],detectCmp:[{exists:".duet--cta--cookie-banner"}],detectPopup:[{visible:".duet--cta--cookie-banner"}],optIn:[{click:".duet--cta--cookie-banner button.tracking-12",all:!1}],optOut:[{click:".duet--cta--cookie-banner button.tracking-12 > span"}],test:[{eval:"EVAL_THEVERGE_0"}]},{name:"tidbits-com",cosmetic:!0,prehideSelectors:["#eu_cookie_law_widget-2"],detectCmp:[{exists:"#eu_cookie_law_widget-2"}],detectPopup:[{visible:"#eu_cookie_law_widget-2"}],optIn:[{click:"#eu-cookie-law form > input.accept"}],optOut:[{hide:"#eu_cookie_law_widget-2"}]},{name:"tractor-supply",runContext:{urlPattern:"^https://www\\.tractorsupply\\.com/"},cosmetic:!0,prehideSelectors:[".tsc-cookie-banner"],detectCmp:[{exists:".tsc-cookie-banner"}],detectPopup:[{visible:".tsc-cookie-banner"}],optIn:[{click:"#cookie-banner-cancel"}],optOut:[{hide:".tsc-cookie-banner"}]},{name:"trader-joes-com",cosmetic:!0,prehideSelectors:['div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'],detectCmp:[{exists:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}],detectPopup:[{visible:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}],optIn:[{click:'div[class^="CookiesAlert_cookiesAlert__container__"] button'}],optOut:[{hide:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}]},{name:"transcend",vendorUrl:"https://unknown",cosmetic:!0,prehideSelectors:["#transcend-consent-manager"],detectCmp:[{exists:"#transcend-consent-manager"}],detectPopup:[{visible:"#transcend-consent-manager"}],optIn:[{waitForThenClick:["#transcend-consent-manager","#consentManagerMainDialog .inner-container button"]}],optOut:[{hide:"#transcend-consent-manager"}]},{name:"transip-nl",runContext:{urlPattern:"^https://www\\.transip\\.nl/"},prehideSelectors:["#consent-modal"],detectCmp:[{any:[{exists:"#consent-modal"},{exists:"#privacy-settings-content"}]}],detectPopup:[{any:[{visible:"#consent-modal"},{visible:"#privacy-settings-content"}]}],optIn:[{click:'button[type="submit"]'}],optOut:[{if:{exists:"#privacy-settings-content"},then:[{click:'button[type="submit"]'}],else:[{click:"div.one-modal__action-footer-column--secondary > a"}]}]},{name:"tropicfeel-com",prehideSelectors:["#shopify-section-cookies-controller"],detectCmp:[{exists:"#shopify-section-cookies-controller"}],detectPopup:[{visible:"#shopify-section-cookies-controller #cookies-controller-main-pane",check:"any"}],optIn:[{waitForThenClick:"#cookies-controller-main-pane form[data-form-allow-all] button"}],optOut:[{click:"#cookies-controller-main-pane a[data-tab-target=manage-cookies]"},{waitFor:"#manage-cookies-pane.active"},{click:"#manage-cookies-pane.active input[type=checkbox][checked]:not([disabled])",all:!0},{click:"#manage-cookies-pane.active button[type=submit]"}],test:[]},{name:"true-car",runContext:{urlPattern:"^https://www\\.truecar\\.com/"},cosmetic:!0,prehideSelectors:[['div[aria-labelledby="cookie-banner-heading"]']],detectCmp:[{exists:'div[aria-labelledby="cookie-banner-heading"]'}],detectPopup:[{visible:'div[aria-labelledby="cookie-banner-heading"]'}],optIn:[{click:'div[aria-labelledby="cookie-banner-heading"] > button[aria-label="Close"]'}],optOut:[{hide:'div[aria-labelledby="cookie-banner-heading"]'}]},{name:"truyo",prehideSelectors:["#truyo-consent-module"],detectCmp:[{exists:"#truyo-cookieBarContent"}],detectPopup:[{visible:"#truyo-consent-module"}],optIn:[{click:"button#acceptAllCookieButton"}],optOut:[{click:"button#declineAllCookieButton"}]},{name:"twitch-mobile",vendorUrl:"https://m.twitch.tv/",cosmetic:!0,runContext:{urlPattern:"^https?://m\\.twitch\\.tv"},prehideSelectors:[],detectCmp:[{exists:'.ReactModal__Overlay [href="https://www.twitch.tv/p/cookie-policy"]'}],detectPopup:[{visible:'.ReactModal__Overlay [href="https://www.twitch.tv/p/cookie-policy"]'}],optIn:[{waitForThenClick:'.ReactModal__Overlay:has([href="https://www.twitch.tv/p/cookie-policy"]) button'}],optOut:[{hide:'.ReactModal__Overlay:has([href="https://www.twitch.tv/p/cookie-policy"])'}]},{name:"twitch.tv",runContext:{urlPattern:"^https?://(www\\.)?twitch\\.tv"},prehideSelectors:["div:has(> .consent-banner .consent-banner__content--gdpr-v2),.ReactModalPortal:has([data-a-target=consent-modal-save])"],detectCmp:[{exists:".consent-banner .consent-banner__content--gdpr-v2"}],detectPopup:[{visible:".consent-banner .consent-banner__content--gdpr-v2"}],optIn:[{click:'button[data-a-target="consent-banner-accept"]'}],optOut:[{hide:"div:has(> .consent-banner .consent-banner__content--gdpr-v2)"},{click:'button[data-a-target="consent-banner-manage-preferences"]'},{waitFor:"input[type=checkbox][data-a-target=tw-checkbox]"},{click:"input[type=checkbox][data-a-target=tw-checkbox][checked]:not([disabled])",all:!0,optional:!0},{waitForThenClick:"[data-a-target=consent-modal-save]"},{waitForVisible:".ReactModalPortal:has([data-a-target=consent-modal-save])",check:"none"}]},{name:"twitter",runContext:{urlPattern:"^https://([a-z0-9-]+\\.)?twitter\\.com/"},prehideSelectors:['[data-testid="BottomBar"]'],detectCmp:[{exists:'[data-testid="BottomBar"] div'}],detectPopup:[{visible:'[data-testid="BottomBar"] div'}],optIn:[{waitForThenClick:'[data-testid="BottomBar"] > div:has(>div:first-child>div:last-child>span[role=button]) > div:last-child > div[role=button]:first-child'}],optOut:[{waitForThenClick:'[data-testid="BottomBar"] > div:has(>div:first-child>div:last-child>span[role=button]) > div:last-child > div[role=button]:last-child'}],TODOtest:[{eval:"EVAL_document.cookie.includes('d_prefs=MjoxLGNvbnNlbnRfdmVyc2lvbjoy')"}]},{name:"ubuntu.com",prehideSelectors:["dialog.cookie-policy"],detectCmp:[{any:[{exists:"dialog.cookie-policy header"},{exists:'xpath///*[@id="modal"]/div/header'}]}],detectPopup:[{any:[{visible:"dialog header"},{visible:'xpath///*[@id="modal"]/div/header'}]}],optIn:[{any:[{waitForThenClick:"#cookie-policy-button-accept"},{waitForThenClick:'xpath///*[@id="cookie-policy-button-accept"]'}]}],optOut:[{any:[{waitForThenClick:"button.js-manage"},{waitForThenClick:'xpath///*[@id="cookie-policy-content"]/p[4]/button[2]'}]},{waitForThenClick:"dialog.cookie-policy .p-switch__input:checked",optional:!0,all:!0,timeout:500},{any:[{waitForThenClick:"dialog.cookie-policy .js-save-preferences"},{waitForThenClick:'xpath///*[@id="modal"]/div/button'}]}],test:[{eval:"EVAL_UBUNTU_COM_0"}]},{name:"UK Cookie Consent",prehideSelectors:["#catapult-cookie-bar"],cosmetic:!0,detectCmp:[{exists:"#catapult-cookie-bar"}],detectPopup:[{exists:".has-cookie-bar #catapult-cookie-bar"}],optIn:[{click:"#catapultCookie"}],optOut:[{hide:"#catapult-cookie-bar"}],test:[{eval:"EVAL_UK_COOKIE_CONSENT_0"}]},{name:"urbanarmorgear-com",cosmetic:!0,prehideSelectors:['div[class^="Layout__CookieBannerContainer-"]'],detectCmp:[{exists:'div[class^="Layout__CookieBannerContainer-"]'}],detectPopup:[{visible:'div[class^="Layout__CookieBannerContainer-"]'}],optIn:[{click:'button[class^="CookieBanner__AcceptButton"]'}],optOut:[{hide:'div[class^="Layout__CookieBannerContainer-"]'}]},{name:"usercentrics-api",detectCmp:[{exists:"#usercentrics-root"}],detectPopup:[{eval:"EVAL_USERCENTRICS_API_0"},{exists:["#usercentrics-root","[data-testid=uc-container]"]},{waitForVisible:"#usercentrics-root",timeout:2e3}],optIn:[{eval:"EVAL_USERCENTRICS_API_3"},{eval:"EVAL_USERCENTRICS_API_1"},{eval:"EVAL_USERCENTRICS_API_5"}],optOut:[{eval:"EVAL_USERCENTRICS_API_1"},{eval:"EVAL_USERCENTRICS_API_2"}],test:[{eval:"EVAL_USERCENTRICS_API_6"}]},{name:"usercentrics-button",detectCmp:[{exists:"#usercentrics-button"}],detectPopup:[{visible:"#usercentrics-button #uc-btn-accept-banner"}],optIn:[{click:"#usercentrics-button #uc-btn-accept-banner"}],optOut:[{click:"#usercentrics-button #uc-btn-deny-banner"}],test:[{eval:"EVAL_USERCENTRICS_BUTTON_0"}]},{name:"uswitch.com",prehideSelectors:["#cookie-banner-wrapper"],detectCmp:[{exists:"#cookie-banner-wrapper"}],detectPopup:[{visible:"#cookie-banner-wrapper"}],optIn:[{click:"#cookie_banner_accept_mobile"}],optOut:[{click:"#cookie_banner_save"}]},{name:"vodafone.de",runContext:{urlPattern:"^https://www\\.vodafone\\.de/"},prehideSelectors:[".dip-consent,.dip-consent-container"],detectCmp:[{exists:".dip-consent-container"}],detectPopup:[{visible:".dip-consent-content"}],optOut:[{click:'.dip-consent-btn[tabindex="2"]'}],optIn:[{click:'.dip-consent-btn[tabindex="1"]'}]},{name:"waitrose.com",prehideSelectors:["div[aria-labelledby=CookieAlertModalHeading]","section[data-test=initial-waitrose-cookie-consent-banner]","section[data-test=cookie-consent-modal]"],detectCmp:[{exists:"section[data-test=initial-waitrose-cookie-consent-banner]"}],detectPopup:[{visible:"section[data-test=initial-waitrose-cookie-consent-banner]"}],optIn:[{click:"button[data-test=accept-all]"}],optOut:[{click:"button[data-test=manage-cookies]"},{wait:200},{eval:"EVAL_WAITROSE_0"},{click:"button[data-test=submit]"}],test:[{eval:"EVAL_WAITROSE_1"}]},{name:"webflow",vendorUrl:"https://webflow.com/",prehideSelectors:[".fs-cc-components"],detectCmp:[{exists:".fs-cc-components"}],detectPopup:[{visible:".fs-cc-components"},{visible:"[fs-cc=banner]"}],optIn:[{wait:500},{waitForThenClick:"[fs-cc=banner] [fs-cc=allow]"}],optOut:[{wait:500},{waitForThenClick:"[fs-cc=banner] [fs-cc=deny]"}]},{name:"wetransfer.com",detectCmp:[{exists:".welcome__cookie-notice"}],detectPopup:[{visible:".welcome__cookie-notice"}],optIn:[{click:".welcome__button--accept"}],optOut:[{click:".welcome__button--decline"}]},{name:"whitepages.com",runContext:{urlPattern:"^https://www\\.whitepages\\.com/"},cosmetic:!0,prehideSelectors:[".cookie-wrapper, .cookie-overlay"],detectCmp:[{exists:".cookie-wrapper"}],detectPopup:[{visible:".cookie-overlay"}],optIn:[{click:'button[aria-label="Got it"]'}],optOut:[{hide:".cookie-wrapper"}]},{name:"wolframalpha",vendorUrl:"https://www.wolframalpha.com",prehideSelectors:[],cosmetic:!0,runContext:{urlPattern:"^https://www\\.wolframalpha\\.com/"},detectCmp:[{exists:"section._a_yb"}],detectPopup:[{visible:"section._a_yb"}],optIn:[{waitForThenClick:"section._a_yb button"}],optOut:[{hide:"section._a_yb"}]},{name:"woo-commerce-com",prehideSelectors:[".wccom-comp-privacy-banner .wccom-privacy-banner"],detectCmp:[{exists:".wccom-comp-privacy-banner .wccom-privacy-banner"}],detectPopup:[{exists:".wccom-comp-privacy-banner .wccom-privacy-banner"}],optIn:[{click:".wccom-privacy-banner__content-buttons button.is-primary"}],optOut:[{click:".wccom-privacy-banner__content-buttons button.is-secondary"},{waitForThenClick:"input[type=checkbox][checked]:not([disabled])",all:!0},{click:"div.wccom-modal__footer > button"}]},{name:"WP Cookie Notice for GDPR",vendorUrl:"https://wordpress.org/plugins/gdpr-cookie-consent/",prehideSelectors:["#gdpr-cookie-consent-bar"],detectCmp:[{exists:"#gdpr-cookie-consent-bar"}],detectPopup:[{visible:"#gdpr-cookie-consent-bar"}],optIn:[{waitForThenClick:"#gdpr-cookie-consent-bar #cookie_action_accept"}],optOut:[{waitForThenClick:"#gdpr-cookie-consent-bar #cookie_action_reject"}],test:[{eval:"EVAL_WP_COOKIE_NOTICE_0"}]},{name:"wpcc",cosmetic:!0,prehideSelectors:[".wpcc-container"],detectCmp:[{exists:".wpcc-container"}],detectPopup:[{exists:".wpcc-container .wpcc-message"}],optIn:[{click:".wpcc-compliance .wpcc-btn"}],optOut:[{hide:".wpcc-container"}]},{name:"xe.com",vendorUrl:"https://www.xe.com/",runContext:{urlPattern:"^https://www\\.xe\\.com/"},prehideSelectors:["[class*=ConsentBanner]"],detectCmp:[{exists:"[class*=ConsentBanner]"}],detectPopup:[{visible:"[class*=ConsentBanner]"}],optIn:[{waitForThenClick:"[class*=ConsentBanner] .egnScw"}],optOut:[{wait:1e3},{waitForThenClick:"[class*=ConsentBanner] .frDWEu"},{waitForThenClick:"[class*=ConsentBanner] .hXIpFU"}],test:[{eval:"EVAL_XE_TEST"}]},{name:"xhamster-eu",prehideSelectors:[".cookies-modal"],detectCmp:[{exists:".cookies-modal"}],detectPopup:[{exists:".cookies-modal"}],optIn:[{click:"button.cmd-button-accept-all"}],optOut:[{click:"button.cmd-button-reject-all"}]},{name:"xhamster-us",runContext:{urlPattern:"^https://(www\\.)?xhamster\\d?\\.com"},cosmetic:!0,prehideSelectors:[".cookie-announce"],detectCmp:[{exists:".cookie-announce"}],detectPopup:[{visible:".cookie-announce .announce-text"}],optIn:[{click:".cookie-announce button.xh-button"}],optOut:[{hide:".cookie-announce"}]},{name:"xing.com",detectCmp:[{exists:"div[class^=cookie-consent-CookieConsent]"}],detectPopup:[{exists:"div[class^=cookie-consent-CookieConsent]"}],optIn:[{click:"#consent-accept-button"}],optOut:[{click:"#consent-settings-button"},{click:".consent-banner-button-accept-overlay"}],test:[{eval:"EVAL_XING_0"}]},{name:"xnxx-com",cosmetic:!0,prehideSelectors:["#cookies-use-alert"],detectCmp:[{exists:"#cookies-use-alert"}],detectPopup:[{visible:"#cookies-use-alert"}],optIn:[{click:"#cookies-use-alert .close"}],optOut:[{hide:"#cookies-use-alert"}]},{name:"xvideos",vendorUrl:"https://xvideos.com",runContext:{urlPattern:"^https://[^/]*xvideos\\.com/"},prehideSelectors:[],detectCmp:[{exists:".disclaimer-opened #disclaimer-cookies"}],detectPopup:[{visible:".disclaimer-opened #disclaimer-cookies"}],optIn:[{waitForThenClick:"#disclaimer-accept_cookies"}],optOut:[{waitForThenClick:"#disclaimer-reject_cookies"}]},{name:"Yahoo",runContext:{urlPattern:"^https://consent\\.yahoo\\.com/v2/"},prehideSelectors:["#reject-all"],detectCmp:[{exists:"#consent-page"}],detectPopup:[{visible:"#consent-page"}],optIn:[{waitForThenClick:"#consent-page button[value=agree]"}],optOut:[{waitForThenClick:"#consent-page button[value=reject]"}]},{name:"youporn.com",cosmetic:!0,prehideSelectors:[".euCookieModal, #js_euCookieModal"],detectCmp:[{exists:".euCookieModal"}],detectPopup:[{exists:".euCookieModal, #js_euCookieModal"}],optIn:[{click:'button[name="user_acceptCookie"]'}],optOut:[{hide:".euCookieModal"}]},{name:"youtube-desktop",prehideSelectors:["tp-yt-iron-overlay-backdrop.opened","ytd-consent-bump-v2-lightbox"],detectCmp:[{exists:"ytd-consent-bump-v2-lightbox tp-yt-paper-dialog"},{exists:'ytd-consent-bump-v2-lightbox tp-yt-paper-dialog a[href^="https://consent.youtube.com/"]'}],detectPopup:[{visible:"ytd-consent-bump-v2-lightbox tp-yt-paper-dialog"}],optIn:[{waitForThenClick:"ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:last-child #button,ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:last-child button"},{wait:500}],optOut:[{waitForThenClick:"ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:first-child #button,ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:first-child button"},{wait:500}],test:[{wait:500},{eval:"EVAL_YOUTUBE_DESKTOP_0"}]},{name:"youtube-mobile",prehideSelectors:[".consent-bump-v2-lightbox"],detectCmp:[{exists:"ytm-consent-bump-v2-renderer"}],detectPopup:[{visible:"ytm-consent-bump-v2-renderer"}],optIn:[{waitForThenClick:"ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons c3-material-button:first-child button, ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons ytm-button-renderer:first-child button"},{wait:500}],optOut:[{waitForThenClick:"ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons c3-material-button:nth-child(2) button, ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons ytm-button-renderer:nth-child(2) button"},{wait:500}],test:[{wait:500},{eval:"EVAL_YOUTUBE_MOBILE_0"}]},{name:"zdf",prehideSelectors:["#zdf-cmp-banner-sdk"],detectCmp:[{exists:"#zdf-cmp-banner-sdk"}],detectPopup:[{visible:"#zdf-cmp-main.zdf-cmp-show"}],optIn:[{waitForThenClick:"#zdf-cmp-main #zdf-cmp-accept-btn"}],optOut:[{waitForThenClick:"#zdf-cmp-main #zdf-cmp-deny-btn"}],test:[]}],A={"didomi.io":{detectors:[{presentMatcher:{target:{selector:"#didomi-host, #didomi-notice"},type:"css"},showingMatcher:{target:{selector:"body.didomi-popup-open, .didomi-notice-banner"},type:"css"}}],methods:[{action:{target:{selector:".didomi-popup-notice-buttons .didomi-button:not(.didomi-button-highlight), .didomi-notice-banner .didomi-learn-more-button"},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{retries:50,target:{selector:"#didomi-purpose-cookies"},type:"waitcss",waitTime:50},{consents:[{description:"Share (everything) with others",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-share_whith_others]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-share_whith_others]:last-child"},type:"click"},type:"X"},{description:"Information storage and access",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-cookies]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-cookies]:last-child"},type:"click"},type:"D"},{description:"Content selection, offers and marketing",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-CL-T1Rgm7]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-CL-T1Rgm7]:last-child"},type:"click"},type:"E"},{description:"Analytics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-analytics]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-analytics]:last-child"},type:"click"},type:"B"},{description:"Analytics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-M9NRHJe3G]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-M9NRHJe3G]:last-child"},type:"click"},type:"B"},{description:"Ad and content selection",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-advertising_personalization]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-advertising_personalization]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection",falseAction:{parent:{childFilter:{target:{selector:"#didomi-purpose-pub-ciblee"}},selector:".didomi-consent-popup-data-processing, .didomi-components-accordion-label-container"},target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-pub-ciblee]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-pub-ciblee]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - basics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-q4zlJqdcD]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-q4zlJqdcD]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - partners and subsidiaries",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-partenaire-cAsDe8jC]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-partenaire-cAsDe8jC]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-p4em9a8m]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-p4em9a8m]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - others",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-autres-pub]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-autres-pub]:last-child"},type:"click"},type:"F"},{description:"Social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-reseauxsociaux]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-reseauxsociaux]:last-child"},type:"click"},type:"A"},{description:"Social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-social_media]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-social_media]:last-child"},type:"click"},type:"A"},{description:"Content selection",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-content_personalization]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-content_personalization]:last-child"},type:"click"},type:"E"},{description:"Ad delivery",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-ad_delivery]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-ad_delivery]:last-child"},type:"click"},type:"F"}],type:"consent"},{action:{consents:[{matcher:{childFilter:{target:{selector:":not(.didomi-components-radio__option--selected)"}},type:"css"},trueAction:{target:{selector:":nth-child(2)"},type:"click"},falseAction:{target:{selector:":first-child"},type:"click"},type:"X"}],type:"consent"},target:{selector:".didomi-components-radio"},type:"foreach"}],type:"list"},name:"DO_CONSENT"},{action:{parent:{selector:".didomi-consent-popup-footer .didomi-consent-popup-actions"},target:{selector:".didomi-components-button:first-child"},type:"click"},name:"SAVE_CONSENT"}]},oil:{detectors:[{presentMatcher:{target:{selector:".as-oil-content-overlay"},type:"css"},showingMatcher:{target:{selector:".as-oil-content-overlay"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".as-js-advanced-settings"},type:"click"},{retries:"10",target:{selector:".as-oil-cpc__purpose-container"},type:"waitcss",waitTime:"250"}],type:"list"},name:"OPEN_OPTIONS"},{action:{actions:[{consents:[{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Information storage and access","Opbevaring af og adgang til oplysninger på din enhed"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Information storage and access","Opbevaring af og adgang til oplysninger på din enhed"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"D"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personlige annoncer","Personalisation"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personlige annoncer","Personalisation"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"E"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Annoncevalg, levering og rapportering","Ad selection, delivery, reporting"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Annoncevalg, levering og rapportering","Ad selection, delivery, reporting"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"F"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personalisering af indhold","Content selection, delivery, reporting"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personalisering af indhold","Content selection, delivery, reporting"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"E"},{matcher:{parent:{childFilter:{target:{selector:".as-oil-cpc__purpose-header",textFilter:["Måling","Measurement"]}},selector:".as-oil-cpc__purpose-container"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{childFilter:{target:{selector:".as-oil-cpc__purpose-header",textFilter:["Måling","Measurement"]}},selector:".as-oil-cpc__purpose-container"},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"B"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:"Google"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:"Google"},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"F"}],type:"consent"}],type:"list"},name:"DO_CONSENT"},{action:{target:{selector:".as-oil__btn-optin"},type:"click"},name:"SAVE_CONSENT"},{action:{target:{selector:"div.as-oil"},type:"hide"},name:"HIDE_CMP"}]},optanon:{detectors:[{presentMatcher:{target:{selector:"#optanon-menu, .optanon-alert-box-wrapper"},type:"css"},showingMatcher:{target:{displayFilter:!0,selector:".optanon-alert-box-wrapper"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".optanon-alert-box-wrapper .optanon-toggle-display, a[onclick*='OneTrust.ToggleInfoDisplay()'], a[onclick*='Optanon.ToggleInfoDisplay()']"},type:"click"}],type:"list"},name:"OPEN_OPTIONS"},{action:{actions:[{target:{selector:".preference-menu-item #Your-privacy"},type:"click"},{target:{selector:"#optanon-vendor-consent-text"},type:"click"},{action:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},target:{selector:"#optanon-vendor-consent-list .vendor-item"},type:"foreach"},{target:{selector:".vendor-consent-back-link"},type:"click"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-performance"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-performance"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-functional"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-functional"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-advertising"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-advertising"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-social"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-social"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Social Media Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Social Media Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalisation"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalisation"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Site monitoring cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Site monitoring cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Third party privacy-enhanced content"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Third party privacy-enhanced content"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Performance & Advertising Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Performance & Advertising Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Information storage and access"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Information storage and access"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"D"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad selection, delivery, reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad selection, delivery, reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content selection, delivery, reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content selection, delivery, reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Measurement"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Measurement"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Recommended Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Recommended Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Unclassified Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Unclassified Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Analytical Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Analytical Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Marketing Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Marketing Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalization"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalization"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad Selection, Delivery & Reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad Selection, Delivery & Reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content Selection, Delivery & Reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content Selection, Delivery & Reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"}],type:"list"},name:"DO_CONSENT"},{action:{parent:{selector:".optanon-save-settings-button"},target:{selector:".optanon-white-button-middle"},type:"click"},name:"SAVE_CONSENT"},{action:{actions:[{target:{selector:"#optanon-popup-wrapper"},type:"hide"},{target:{selector:"#optanon-popup-bg"},type:"hide"},{target:{selector:".optanon-alert-box-wrapper"},type:"hide"}],type:"list"},name:"HIDE_CMP"}]},quantcast2:{detectors:[{presentMatcher:{target:{selector:"[data-tracking-opt-in-overlay]"},type:"css"},showingMatcher:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-learn-more]"},type:"css"}}],methods:[{action:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-learn-more]"},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{type:"wait",waitTime:500},{action:{actions:[{target:{selector:"div",textFilter:["Information storage and access"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"D"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Personalization"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"F"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Ad selection, delivery, reporting"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"F"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Content selection, delivery, reporting"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"E"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Measurement"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"B"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Other Partners"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},type:"ifcss"}],type:"list"},parent:{childFilter:{target:{selector:"input"}},selector:"[data-tracking-opt-in-overlay] > div > div"},target:{childFilter:{target:{selector:"input"}},selector:":scope > div"},type:"foreach"}],type:"list"},name:"DO_CONSENT"},{action:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-save]"},type:"click"},name:"SAVE_CONSENT"}]},springer:{detectors:[{presentMatcher:{parent:null,target:{selector:".cmp-app_gdpr"},type:"css"},showingMatcher:{parent:null,target:{displayFilter:!0,selector:".cmp-popup_popup"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".cmp-intro_rejectAll"},type:"click"},{type:"wait",waitTime:250},{target:{selector:".cmp-purposes_purposeItem:not(.cmp-purposes_selectedPurpose)"},type:"click"}],type:"list"},name:"OPEN_OPTIONS"},{action:{consents:[{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Przechowywanie informacji na urządzeniu lub dostęp do nich",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Przechowywanie informacji na urządzeniu lub dostęp do nich",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"D"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór podstawowych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór podstawowych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"F"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"F"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"E"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"E"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Stosowanie badań rynkowych w celu generowania opinii odbiorców",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Stosowanie badań rynkowych w celu generowania opinii odbiorców",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"X"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Opracowywanie i ulepszanie produktów",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Opracowywanie i ulepszanie produktów",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"X"}],type:"consent"},name:"DO_CONSENT"},{action:{target:{selector:".cmp-details_save"},type:"click"},name:"SAVE_CONSENT"}]},wordpressgdpr:{detectors:[{presentMatcher:{parent:null,target:{selector:".wpgdprc-consent-bar"},type:"css"},showingMatcher:{parent:null,target:{displayFilter:!0,selector:".wpgdprc-consent-bar"},type:"css"}}],methods:[{action:{parent:null,target:{selector:".wpgdprc-consent-bar .wpgdprc-consent-bar__settings",textFilter:null},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{target:{selector:".wpgdprc-consent-modal .wpgdprc-button",textFilter:"Eyeota"},type:"click"},{consents:[{description:"Eyeota Cookies",matcher:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Eyeota"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Eyeota"},target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},{target:{selector:".wpgdprc-consent-modal .wpgdprc-button",textFilter:"Advertising"},type:"click"},{consents:[{description:"Advertising Cookies",matcher:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Advertising"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Advertising"},target:{selector:"label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},name:"DO_CONSENT"},{action:{parent:null,target:{selector:".wpgdprc-button",textFilter:"Save my settings"},type:"click"},name:"SAVE_CONSENT"}]}},E={autoconsent:f,consentomatic:A},x=Object.freeze({__proto__:null,autoconsent:f,consentomatic:A,default:E});const O=new class{constructor(e,t=null,o=null){if(this.id=a(),this.rules=[],this.foundCmp=null,this.state={lifecycle:"loading",prehideOn:!1,findCmpAttempts:0,detectedCmps:[],detectedPopups:[],selfTest:null},r.sendContentMessage=e,this.sendContentMessage=e,this.rules=[],this.updateState({lifecycle:"loading"}),this.addDynamicRules(),t)this.initialize(t,o);else{o&&this.parseDeclarativeRules(o);e({type:"init",url:window.location.href}),this.updateState({lifecycle:"waitingForInitResponse"})}this.domActions=new v(this)}initialize(e,t){const o=g(e);if(o.logs.lifecycle&&console.log("autoconsent init",window.location.href),this.config=o,o.enabled){if(t&&this.parseDeclarativeRules(t),this.rules=function(e,t){return e.filter((e=>(!t.disabledCmps||!t.disabledCmps.includes(e.name))&&(t.enableCosmeticRules||!e.isCosmetic)))}(this.rules,o),e.enablePrehide)if(document.documentElement)this.prehideElements();else{const e=()=>{window.removeEventListener("DOMContentLoaded",e),this.prehideElements()};window.addEventListener("DOMContentLoaded",e)}if("loading"===document.readyState){const e=()=>{window.removeEventListener("DOMContentLoaded",e),this.start()};window.addEventListener("DOMContentLoaded",e)}else this.start();this.updateState({lifecycle:"initialized"})}else o.logs.lifecycle&&console.log("autoconsent is disabled")}addDynamicRules(){C.forEach((e=>{this.rules.push(new e(this))}))}parseDeclarativeRules(e){Object.keys(e.consentomatic).forEach((t=>{this.addConsentomaticCMP(t,e.consentomatic[t])})),e.autoconsent.forEach((e=>{this.addDeclarativeCMP(e)}))}addDeclarativeCMP(e){this.rules.push(new u(e,this))}addConsentomaticCMP(e,t){this.rules.push(new m(`com_${e}`,t))}start(){window.requestIdleCallback?window.requestIdleCallback((()=>this._start()),{timeout:500}):this._start()}async _start(){const e=this.config.logs;e.lifecycle&&console.log(`Detecting CMPs on ${window.location.href}`),this.updateState({lifecycle:"started"});const t=await this.findCmp(this.config.detectRetries);if(this.updateState({detectedCmps:t.map((e=>e.name))}),0===t.length)return e.lifecycle&&console.log("no CMP found",location.href),this.config.enablePrehide&&this.undoPrehide(),this.updateState({lifecycle:"nothingDetected"}),!1;this.updateState({lifecycle:"cmpDetected"});const o=[],c=[];for(const e of t)e.isCosmetic?c.push(e):o.push(e);let i=!1,n=await this.detectPopups(o,(async e=>{i=await this.handlePopup(e)}));if(0===n.length&&(n=await this.detectPopups(c,(async e=>{i=await this.handlePopup(e)}))),0===n.length)return e.lifecycle&&console.log("no popup found"),this.config.enablePrehide&&this.undoPrehide(),!1;if(n.length>1){const t={msg:"Found multiple CMPs, check the detection rules.",cmps:n.map((e=>e.name))};e.errors&&console.warn(t.msg,t.cmps),this.sendContentMessage({type:"autoconsentError",details:t})}return i}async findCmp(e){const t=this.config.logs;this.updateState({findCmpAttempts:this.state.findCmpAttempts+1});const o=[];for(const e of this.rules)try{if(!e.checkRunContext())continue;await e.detectCmp()&&(t.lifecycle&&console.log(`Found CMP: ${e.name} ${window.location.href}`),this.sendContentMessage({type:"cmpDetected",url:location.href,cmp:e.name}),o.push(e))}catch(o){t.errors&&console.warn(`error detecting ${e.name}`,o)}return 0===o.length&&e>0?(await this.domActions.wait(500),this.findCmp(e-1)):o}async detectPopup(e){if(await this.waitForPopup(e).catch((t=>(this.config.logs.errors&&console.warn(`error waiting for a popup for ${e.name}`,t),!1))))return this.updateState({detectedPopups:this.state.detectedPopups.concat([e.name])}),this.sendContentMessage({type:"popupFound",cmp:e.name,url:location.href}),e;throw new Error("Popup is not shown")}async detectPopups(e,t){const o=e.map((e=>this.detectPopup(e)));await Promise.any(o).then((e=>{t(e)})).catch((()=>null));const c=await Promise.allSettled(o),i=[];for(const e of c)"fulfilled"===e.status&&i.push(e.value);return i}async handlePopup(e){return this.updateState({lifecycle:"openPopupDetected"}),this.config.enablePrehide&&!this.state.prehideOn&&this.prehideElements(),this.foundCmp=e,"optOut"===this.config.autoAction?await this.doOptOut():"optIn"===this.config.autoAction?await this.doOptIn():(this.config.logs.lifecycle&&console.log("waiting for opt-out signal...",location.href),!0)}async doOptOut(){const e=this.config.logs;let t;return this.updateState({lifecycle:"runningOptOut"}),this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: opt out on ${window.location.href}`),t=await this.foundCmp.optOut(),e.lifecycle&&console.log(`${this.foundCmp.name}: opt out result ${t}`)):(e.errors&&console.log("no CMP to opt out"),t=!1),this.config.enablePrehide&&this.undoPrehide(),this.sendContentMessage({type:"optOutResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,scheduleSelfTest:this.foundCmp&&this.foundCmp.hasSelfTest,url:location.href}),t&&!this.foundCmp.isIntermediate?(this.sendContentMessage({type:"autoconsentDone",cmp:this.foundCmp.name,isCosmetic:this.foundCmp.isCosmetic,url:location.href}),this.updateState({lifecycle:"done"})):this.updateState({lifecycle:t?"optOutSucceeded":"optOutFailed"}),t}async doOptIn(){const e=this.config.logs;let t;return this.updateState({lifecycle:"runningOptIn"}),this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: opt in on ${window.location.href}`),t=await this.foundCmp.optIn(),e.lifecycle&&console.log(`${this.foundCmp.name}: opt in result ${t}`)):(e.errors&&console.log("no CMP to opt in"),t=!1),this.config.enablePrehide&&this.undoPrehide(),this.sendContentMessage({type:"optInResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,scheduleSelfTest:!1,url:location.href}),t&&!this.foundCmp.isIntermediate?(this.sendContentMessage({type:"autoconsentDone",cmp:this.foundCmp.name,isCosmetic:this.foundCmp.isCosmetic,url:location.href}),this.updateState({lifecycle:"done"})):this.updateState({lifecycle:t?"optInSucceeded":"optInFailed"}),t}async doSelfTest(){const e=this.config.logs;let t;return this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: self-test on ${window.location.href}`),t=await this.foundCmp.test()):(e.errors&&console.log("no CMP to self test"),t=!1),this.sendContentMessage({type:"selfTestResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,url:location.href}),this.updateState({selfTest:t}),t}async waitForPopup(e,t=5,o=500){const c=this.config.logs;c.lifecycle&&console.log("checking if popup is open...",e.name);const i=await e.detectPopup().catch((t=>(c.errors&&console.warn(`error detecting popup for ${e.name}`,t),!1)));return!i&&t>0?(await this.domActions.wait(o),this.waitForPopup(e,t-1,o)):(c.lifecycle&&console.log(e.name,"popup is "+(i?"open":"not open")),i)}prehideElements(){const e=this.config.logs,t=this.rules.filter((e=>e.prehideSelectors&&e.checkRunContext())).reduce(((e,t)=>[...e,...t.prehideSelectors]),["#didomi-popup,.didomi-popup-container,.didomi-popup-notice,.didomi-consent-popup-preferences,#didomi-notice,.didomi-popup-backdrop,.didomi-screen-medium"]);return this.updateState({prehideOn:!0}),setTimeout((()=>{this.config.enablePrehide&&this.state.prehideOn&&!["runningOptOut","runningOptIn"].includes(this.state.lifecycle)&&(e.lifecycle&&console.log("Process is taking too long, unhiding elements"),this.undoPrehide())}),this.config.prehideTimeout||2e3),this.domActions.prehide(t.join(","))}undoPrehide(){return this.updateState({prehideOn:!1}),this.domActions.undoPrehide()}updateState(e){Object.assign(this.state,e),this.sendContentMessage({type:"report",instanceId:this.id,url:window.location.href,mainFrame:window.top===window.self,state:this.state})}async receiveMessageCallback(e){const t=this.config?.logs;switch(t?.messages&&console.log("received from background",e,window.location.href),e.type){case"initResp":this.initialize(e.config,e.rules);break;case"optIn":await this.doOptIn();break;case"optOut":await this.doOptOut();break;case"selfTest":await this.doSelfTest();break;case"evalResp":!function(e,t){const o=r.pending.get(e);o?(r.pending.delete(e),o.timer&&window.clearTimeout(o.timer),o.resolve(t)):console.warn("no eval #",e)}(e.id,e.result)}}}((e=>{window.webkit.messageHandlers[e.type]&&window.webkit.messageHandlers[e.type].postMessage(e).then((e=>{O.receiveMessageCallback(e)}))}),null,x);window.autoconsentMessageCallback=e=>{O.receiveMessageCallback(e)}}(); diff --git a/DuckDuckGo/AutofillDebugViewController.swift b/DuckDuckGo/AutofillDebugViewController.swift index 1045f07941..2e369e1e79 100644 --- a/DuckDuckGo/AutofillDebugViewController.swift +++ b/DuckDuckGo/AutofillDebugViewController.swift @@ -20,6 +20,7 @@ import UIKit import BrowserServicesKit import Core +import Common class AutofillDebugViewController: UITableViewController { @@ -27,7 +28,8 @@ class AutofillDebugViewController: UITableViewController { case toggleAutofillDebugScript = 201 case resetEmailProtectionInContextSignUp = 202 case resetDaysSinceInstalledTo0 = 203 - case toggleAutofillSurvey = 204 + case resetAutofillData = 204 + case toggleAutofillSurvey = 205 } let defaults = AppUserDefaults() @@ -46,6 +48,14 @@ class AutofillDebugViewController: UITableViewController { defaults.autofillDebugScriptEnabled.toggle() cell.accessoryType = defaults.autofillDebugScriptEnabled ? .checkmark : .none NotificationCenter.default.post(Notification(name: AppUserDefaults.Notifications.autofillDebugScriptToggled)) + } else if cell.tag == Row.resetAutofillData.rawValue { + let secureVault = try? AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter.shared) + try? secureVault?.deleteAllWebsiteCredentials() + let autofillPixelReporter = AutofillPixelReporter( + userDefaults: .standard, + eventMapping: EventMapping { _, _, _, _ in }) + autofillPixelReporter.resetStoreDefaults() + ActionMessageView.present(message: "Autofill Data reset") } else if cell.tag == Row.toggleAutofillSurvey.rawValue { defaults.autofillSurveyEnabled = true ActionMessageView.present(message: "Passwords Survey enabled") diff --git a/DuckDuckGo/AutofillLoginDetailsViewModel.swift b/DuckDuckGo/AutofillLoginDetailsViewModel.swift index 3a189e4fa9..c55c9a7270 100644 --- a/DuckDuckGo/AutofillLoginDetailsViewModel.swift +++ b/DuckDuckGo/AutofillLoginDetailsViewModel.swift @@ -341,6 +341,7 @@ final class AutofillLoginDetailsViewModel: ObservableObject { self.updateData(with: newCredential.account) } + NotificationCenter.default.post(name: .autofillSaveEvent, object: nil) } catch let error { handleSecureVaultError(error) } diff --git a/DuckDuckGo/AutofillLoginListViewModel.swift b/DuckDuckGo/AutofillLoginListViewModel.swift index 040d6e1b02..04a13a25ba 100644 --- a/DuckDuckGo/AutofillLoginListViewModel.swift +++ b/DuckDuckGo/AutofillLoginListViewModel.swift @@ -129,7 +129,7 @@ final class AutofillLoginListViewModel: ObservableObject { // MARK: Public Methods func isAutofillSurveyEnabled() -> Bool { - return appSettings.autofillSurveyEnabled + return false } func disableAutofillSurvey() { diff --git a/DuckDuckGo/AutofillLoginSettingsListViewController.swift b/DuckDuckGo/AutofillLoginSettingsListViewController.swift index 7078a65db4..df88b9ef0f 100644 --- a/DuckDuckGo/AutofillLoginSettingsListViewController.swift +++ b/DuckDuckGo/AutofillLoginSettingsListViewController.swift @@ -220,7 +220,7 @@ final class AutofillLoginSettingsListViewController: UIViewController { let messageView = PasswordsSurveyView(surveyButtonAction: { [weak self] in let survey = "https://selfserve.decipherinc.com/survey/selfserve/32ab/240409" if let surveyURL = URL(string: survey) { - let surveyURLBuilder = DefaultSurveyURLBuilder() + let surveyURLBuilder = DefaultRemoteMessagingSurveyURLBuilder() let surveyURLWithParameters = surveyURLBuilder.addPasswordsCountSurveyParameter(to: surveyURL) LaunchTabNotification.postLaunchTabNotification(urlString: surveyURLWithParameters.absoluteString) } else { diff --git a/DuckDuckGo/AutofillPixelReporter.swift b/DuckDuckGo/AutofillPixelReporter.swift deleted file mode 100644 index 4cd08757f0..0000000000 --- a/DuckDuckGo/AutofillPixelReporter.swift +++ /dev/null @@ -1,178 +0,0 @@ -// -// AutofillPixelReporter.swift -// DuckDuckGo -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import Core -import BrowserServicesKit - -final class AutofillPixelReporter { - - @UserDefaultsWrapper(key: .autofillSearchDauDate, defaultValue: .distantPast) - var autofillSearchDauDate: Date - - @UserDefaultsWrapper(key: .autofillFillDate, defaultValue: .distantPast) - var autofillFillDate: Date - - @UserDefaultsWrapper(key: .autofillOnboardedUser, defaultValue: false) - var autofillOnboardedUser: Bool - - private let statisticsStorage: StatisticsStore - private var secureVault: (any AutofillSecureVault)? - - enum EventType { - case fill - case searchDAU - } - - init(statisticsStorage: StatisticsStore = StatisticsUserDefaults(), secureVault: (any AutofillSecureVault)? = nil) { - self.statisticsStorage = statisticsStorage - self.secureVault = secureVault - - createNotificationObservers() - - checkIfOnboardedUser() - } - - private func createNotificationObservers() { - NotificationCenter.default.addObserver(self, selector: #selector(didReceiveSearchDAU), name: .searchDAU, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(didReceiveFillEvent), name: .autofillFillEvent, object: nil) - } - - private func vault() -> (any AutofillSecureVault)? { - if secureVault == nil { - secureVault = try? AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter.shared) - } - return secureVault - } - - @objc - private func didReceiveSearchDAU() { - guard !Date().isSameDay(autofillSearchDauDate) else { - return - } - - autofillSearchDauDate = Date() - - firePixels(pixelsToFireFor(.searchDAU)) - } - - @objc - private func didReceiveFillEvent() { - guard !Date().isSameDay(autofillFillDate) else { - return - } - - autofillFillDate = Date() - - firePixels(pixelsToFireFor(.fill)) - } - - func checkIfOnboardedUser() { - guard !autofillOnboardedUser else { return } - - if shouldFireOnboardedUserPixel() { - firePixels([.autofillOnboardedUser]) - } - } - - func pixelsToFireFor(_ type: EventType) -> [Pixel.Event] { - var pixelsToFire: [Pixel.Event] = [] - - if shouldFireActiveUserPixel() { - pixelsToFire.append(.autofillActiveUser) - pixelsToFire.append(.autofillLoginsStacked) - } - - switch type { - case .searchDAU: - if shouldFireEnabledUserPixel() { - pixelsToFire.append(.autofillEnabledUser) - } - default: - break - } - - return pixelsToFire - } - - private func firePixels(_ pixels: [Pixel.Event]) { - for pixel in pixels { - switch pixel { - case .autofillLoginsStacked: - if let count = try? vault()?.accountsCount() { - DailyPixel.fire(pixel: pixel, withAdditionalParameters: [PixelParameters.countBucket: accountsBucketNameFrom(count: count)]) - } - default: - DailyPixel.fire(pixel: pixel) - } - } - } - - public func accountsBucketNameFrom(count: Int) -> String { - if count == 0 { - return "none" - } else if count < 4 { - return "few" - } else if count < 11 { - return "some" - } else if count < 50 { - return "many" - } else { - return "lots" - } - } - - private func shouldFireActiveUserPixel() -> Bool { - let today = Date() - if today.isSameDay(autofillSearchDauDate) && today.isSameDay(autofillFillDate) { - return true - } - return false - } - - private func shouldFireEnabledUserPixel() -> Bool { - if Date().isSameDay(autofillSearchDauDate), let count = try? vault()?.accountsCount(), count >= 10 { - return true - } - return false - } - - func shouldFireOnboardedUserPixel() -> Bool { - guard !autofillOnboardedUser, let installDate = statisticsStorage.installDate, UIApplication.shared.isProtectedDataAvailable else { - return false - } - - let pastWeek = Date().addingTimeInterval(.days(-7)) - - if installDate >= pastWeek { - if let count = try? vault()?.accountsCount(), count > 0 { - autofillOnboardedUser = true - return true - } - } else { - autofillOnboardedUser = true - } - return false - } - -} - -extension NSNotification.Name { - static let autofillFillEvent: NSNotification.Name = Notification.Name(rawValue: "com.duckduckgo.notification.autofillFillEvent") -} diff --git a/DuckDuckGo/Base.lproj/Autocomplete.storyboard b/DuckDuckGo/Base.lproj/Autocomplete.storyboard deleted file mode 100644 index 2e7d24e4c4..0000000000 --- a/DuckDuckGo/Base.lproj/Autocomplete.storyboard +++ /dev/null @@ -1,164 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/DuckDuckGo/Base.lproj/Settings.storyboard b/DuckDuckGo/Base.lproj/Settings.storyboard index 31f784d3cc..7f128195f3 100644 --- a/DuckDuckGo/Base.lproj/Settings.storyboard +++ b/DuckDuckGo/Base.lproj/Settings.storyboard @@ -1,9 +1,9 @@ - + - + @@ -546,7 +546,7 @@ - + @@ -1073,7 +1073,7 @@ - + diff --git a/DuckDuckGo/BlankSnapshotViewController.swift b/DuckDuckGo/BlankSnapshotViewController.swift index 83a62921b1..14ccee9272 100644 --- a/DuckDuckGo/BlankSnapshotViewController.swift +++ b/DuckDuckGo/BlankSnapshotViewController.swift @@ -162,7 +162,11 @@ extension BlankSnapshotViewController: OmniBarDelegate { func onVoiceSearchPressed() { // No-op } - + + func onEditingEnd() -> OmniBarEditingEndResult { + .dismissed + } + func selectedSuggestion() -> Suggestion? { return nil } diff --git a/DuckDuckGo/Debug.storyboard b/DuckDuckGo/Debug.storyboard index 3f9a833419..a448655a3d 100644 --- a/DuckDuckGo/Debug.storyboard +++ b/DuckDuckGo/Debug.storyboard @@ -305,9 +305,18 @@ - + + + + + + + + + + @@ -380,6 +389,15 @@ + + + + + + + + + @@ -387,7 +405,7 @@ - + @@ -396,7 +414,7 @@ - + @@ -916,34 +934,34 @@ - + - + - + - + diff --git a/DuckDuckGo/DefaultNetworkProtectionVisibility.swift b/DuckDuckGo/DefaultNetworkProtectionVisibility.swift index e71f0ead81..e8acd6b8dc 100644 --- a/DuckDuckGo/DefaultNetworkProtectionVisibility.swift +++ b/DuckDuckGo/DefaultNetworkProtectionVisibility.swift @@ -24,54 +24,22 @@ import BrowserServicesKit import Waitlist import NetworkProtection import Core +import Subscription struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { - private let privacyConfigurationManager: PrivacyConfigurationManaging - private let networkProtectionTokenStore: NetworkProtectionTokenStore? - private let networkProtectionAccessManager: NetworkProtectionAccess? - private let featureFlagger: FeatureFlagger private let userDefaults: UserDefaults + private let accountManager: AccountManaging - init(privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager, - networkProtectionTokenStore: NetworkProtectionTokenStore? = NetworkProtectionKeychainTokenStore(), - networkProtectionAccessManager: NetworkProtectionAccess? = NetworkProtectionAccessController(), - featureFlagger: FeatureFlagger = AppDependencyProvider.shared.featureFlagger, - userDefaults: UserDefaults = .networkProtectionGroupDefaults) { - self.privacyConfigurationManager = privacyConfigurationManager - self.networkProtectionTokenStore = networkProtectionTokenStore - self.networkProtectionAccessManager = networkProtectionAccessManager - self.featureFlagger = featureFlagger + init(userDefaults: UserDefaults, accountManager: AccountManaging) { self.userDefaults = userDefaults + self.accountManager = accountManager } - /// A lite version with fewer dependencies - /// We need this to run shouldMonitorEntitlement() check inside the token store - static func forTokenStore() -> DefaultNetworkProtectionVisibility { - DefaultNetworkProtectionVisibility(networkProtectionTokenStore: nil, networkProtectionAccessManager: nil) - } - - func isWaitlistBetaActive() -> Bool { - privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(NetworkProtectionSubfeature.waitlistBetaActive) - } - - func isWaitlistUser() -> Bool { - guard let networkProtectionTokenStore, let networkProtectionAccessManager else { - preconditionFailure("networkProtectionTokenStore and networkProtectionAccessManager must be non-nil") + var token: String? { + if shouldMonitorEntitlement() { + return accountManager.accessToken } - - let hasLegacyAuthToken = { - guard let authToken = try? networkProtectionTokenStore.fetchToken(), - !authToken.hasPrefix(NetworkProtectionKeychainTokenStore.authTokenPrefix) else { - return false - } - return true - }() - let hasBeenInvited = { - let vpnAccessType = networkProtectionAccessManager.networkProtectionAccessType() - return vpnAccessType == .waitlistInvited || vpnAccessType == .inviteCodeInvited - }() - - return hasLegacyAuthToken || hasBeenInvited + return nil } func isPrivacyProLaunched() -> Bool { @@ -89,6 +57,14 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { func shouldMonitorEntitlement() -> Bool { isPrivacyProLaunched() } + + func shouldShowVPNShortcut() -> Bool { + if isPrivacyProLaunched() { + return accountManager.isUserAuthenticated + } else { + return false + } + } } #endif diff --git a/DuckDuckGo/Feedback/VPNFeedbackFormView.swift b/DuckDuckGo/Feedback/VPNFeedbackFormView.swift index 2a37f41d22..a9deda27db 100644 --- a/DuckDuckGo/Feedback/VPNFeedbackFormView.swift +++ b/DuckDuckGo/Feedback/VPNFeedbackFormView.swift @@ -25,6 +25,9 @@ import NetworkProtection @available(iOS 15.0, *) struct VPNFeedbackFormCategoryView: View { @Environment(\.dismiss) private var dismiss + let collector = DefaultVPNMetadataCollector(statusObserver: AppDependencyProvider.shared.connectionObserver, + networkProtectionAccessManager: AppDependencyProvider.shared.networkProtectionAccessController, + tokenStore: AppDependencyProvider.shared.networkProtectionKeychainTokenStore) var body: some View { VStack { @@ -32,7 +35,7 @@ struct VPNFeedbackFormCategoryView: View { Section { ForEach(VPNFeedbackCategory.allCases, id: \.self) { category in NavigationLink { - VPNFeedbackFormView(viewModel: VPNFeedbackFormViewModel(category: category)) { + VPNFeedbackFormView(viewModel: VPNFeedbackFormViewModel(metadataCollector: collector, category: category)) { dismiss() DispatchQueue.main.async { ActionMessageView.present(message: UserText.vpnFeedbackFormSubmittedMessage, diff --git a/DuckDuckGo/Feedback/VPNFeedbackFormViewModel.swift b/DuckDuckGo/Feedback/VPNFeedbackFormViewModel.swift index 9dc1efc175..7f79dd674e 100644 --- a/DuckDuckGo/Feedback/VPNFeedbackFormViewModel.swift +++ b/DuckDuckGo/Feedback/VPNFeedbackFormViewModel.swift @@ -59,7 +59,9 @@ final class VPNFeedbackFormViewModel: ObservableObject { private let feedbackSender: VPNFeedbackSender private let category: VPNFeedbackCategory - init(metadataCollector: VPNMetadataCollector = DefaultVPNMetadataCollector(), feedbackSender: VPNFeedbackSender = DefaultVPNFeedbackSender(), category: VPNFeedbackCategory) { + init(metadataCollector: VPNMetadataCollector, + feedbackSender: VPNFeedbackSender = DefaultVPNFeedbackSender(), + category: VPNFeedbackCategory) { self.metadataCollector = metadataCollector self.feedbackSender = feedbackSender self.category = category diff --git a/DuckDuckGo/Feedback/VPNMetadataCollector.swift b/DuckDuckGo/Feedback/VPNMetadataCollector.swift index 755b8ba95a..ebe361a1c5 100644 --- a/DuckDuckGo/Feedback/VPNMetadataCollector.swift +++ b/DuckDuckGo/Feedback/VPNMetadataCollector.swift @@ -73,7 +73,6 @@ struct VPNMetadata: Encodable { // swiftlint:enable nesting let enableSource: Source - let betaParticipant: Bool let hasToken: Bool let subscriptionActive: Bool } @@ -128,10 +127,10 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { private let settings: VPNSettings private let defaults: UserDefaults - init(statusObserver: ConnectionStatusObserver = ConnectionStatusObserverThroughSession(), + init(statusObserver: ConnectionStatusObserver, serverInfoObserver: ConnectionServerInfoObserver = ConnectionServerInfoObserverThroughSession(), - networkProtectionAccessManager: NetworkProtectionAccessController = NetworkProtectionAccessController(), - tokenStore: NetworkProtectionTokenStore = NetworkProtectionKeychainTokenStore(), + networkProtectionAccessManager: NetworkProtectionAccessController, + tokenStore: NetworkProtectionTokenStore, settings: VPNSettings = .init(defaults: .networkProtectionGroupDefaults), defaults: UserDefaults = .networkProtectionGroupDefaults) { self.statusObserver = statusObserver @@ -265,7 +264,6 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { } func collectPrivacyProInfo() -> VPNMetadata.PrivacyProInfo { - let accessType = accessManager.networkProtectionAccessType() var hasToken: Bool { guard let token = try? tokenStore.fetchToken(), !token.hasPrefix(NetworkProtectionKeychainTokenStore.authTokenPrefix) else { @@ -276,9 +274,8 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { return .init( enableSource: .init(from: accessManager.networkProtectionAccessType()), - betaParticipant: accessType == .waitlistInvited, hasToken: hasToken, - subscriptionActive: AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)).isUserAuthenticated + subscriptionActive: AppDependencyProvider.shared.subscriptionManager.accountManager.isUserAuthenticated ) } } @@ -288,8 +285,6 @@ extension VPNMetadata.PrivacyProInfo.Source { switch accessType { case .inviteCodeInvited: self = .internal - case .waitlistInvited: - self = .waitlist default: self = .other } diff --git a/DuckDuckGo/HistoryMessageManager.swift b/DuckDuckGo/HistoryMessageManager.swift new file mode 100644 index 0000000000..301e8febb4 --- /dev/null +++ b/DuckDuckGo/HistoryMessageManager.swift @@ -0,0 +1,56 @@ +// +// HistoryMessageManager.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Core + +class HistoryMessageManager { + + @UserDefaultsWrapper(key: .historyMessageDisplayCount, defaultValue: 0) + private var count: Int + + @UserDefaultsWrapper(key: .historyMessageDismissed, defaultValue: false) + private var dismissed: Bool + + func incrementDisplayCount() { + guard !dismissed else { return } + count += 1 + if count >= 3 { + dismissed = true + } + } + + func dismiss() { + dismissed = true + } + + func dismissedByUser() { + dismissed = true + Pixel.fire(pixel: .autocompleteMessageDismissed) + } + + func shownToUser() { + Pixel.fire(pixel: .autocompleteMessageShown) + } + + func shouldShow() -> Bool { + return !dismissed + } + +} diff --git a/DuckDuckGo/HomeMessageViewModel.swift b/DuckDuckGo/HomeMessageViewModel.swift index e108700468..e6c7bbdd0c 100644 --- a/DuckDuckGo/HomeMessageViewModel.swift +++ b/DuckDuckGo/HomeMessageViewModel.swift @@ -139,20 +139,9 @@ struct HomeMessageViewModel { LaunchTabNotification.postLaunchTabNotification(urlString: value) onDidClose(buttonAction) } - case .surveyURL(let value): + case .survey(let value): return { -#if NETWORK_PROTECTION - if let surveyURL = URL(string: value) { - let surveyURLBuilder = DefaultSurveyURLBuilder() - let surveyURLWithParameters = surveyURLBuilder.addSurveyParameters(to: surveyURL) - LaunchTabNotification.postLaunchTabNotification(urlString: surveyURLWithParameters.absoluteString) - } else { - LaunchTabNotification.postLaunchTabNotification(urlString: value) - } -#else LaunchTabNotification.postLaunchTabNotification(urlString: value) -#endif - onDidClose(buttonAction) } case .appStore: diff --git a/DuckDuckGo/HomeMessageViewModelBuilder.swift b/DuckDuckGo/HomeMessageViewModelBuilder.swift index 4f54c60548..641be47a10 100644 --- a/DuckDuckGo/HomeMessageViewModelBuilder.swift +++ b/DuckDuckGo/HomeMessageViewModelBuilder.swift @@ -49,7 +49,7 @@ extension RemoteAction { case .share(let value, let title): return .share(value: value, title: title) - case .appStore, .url, .surveyURL: + case .appStore, .url, .survey: if isSecondaryAction { return .cancel } diff --git a/DuckDuckGo/MainViewController+Segues.swift b/DuckDuckGo/MainViewController+Segues.swift index 3c5c718f17..2d094368d0 100644 --- a/DuckDuckGo/MainViewController+Segues.swift +++ b/DuckDuckGo/MainViewController+Segues.swift @@ -250,10 +250,10 @@ extension MainViewController { syncPausedStateManager: syncPausedStateManager) let settingsViewModel = SettingsViewModel(legacyViewProvider: legacyViewProvider, - accountManager: AccountManager(), + subscriptionManager: AppDependencyProvider.shared.subscriptionManager, deepLink: deepLinkTarget, + historyManager: historyManager, syncPausedStateManager: syncPausedStateManager) - Pixel.fire(pixel: .settingsPresented, withAdditionalParameters: PixelExperiment.parameters) diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index ef952935b7..78b3589de1 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -70,7 +70,11 @@ class MainViewController: UIViewController { var contentUnderflow: CGFloat { return 3 + (allowContentUnderflow ? -viewCoordinator.navigationBarContainer.frame.size.height : 0) } - + + var isShowingAutocompleteSuggestions: Bool { + suggestionTrayController?.isShowingAutocompleteSuggestions == true + } + lazy var emailManager: EmailManager = { let emailManager = EmailManager() emailManager.aliasPermissionDelegate = self @@ -324,6 +328,7 @@ class MainViewController: UIViewController { Pixel.fire(pixel: .swipeToOpenNewTab) self?.newTab() } onSwipeStarted: { [weak self] in + self?.performCancel() self?.hideKeyboard() self?.updatePreviewForCurrentTab() } @@ -367,8 +372,7 @@ class MainViewController: UIViewController { SuggestionTrayViewController(coder: coder, favoritesViewModel: self.favoritesViewModel, bookmarksDatabase: self.bookmarksDatabase, - historyCoordinator: self.historyManager.historyCoordinator, - bookmarksStringSearch: self.bookmarksCachingSearch) + historyManager: self.historyManager) }) else { assertionFailure() return @@ -568,8 +572,8 @@ class MainViewController: UIViewController { if let suggestionsTray = suggestionTrayController { let suggestionsFrameInView = suggestionsTray.view.convert(suggestionsTray.contentFrame, to: view) - let overflow = suggestionsFrameInView.size.height + suggestionsFrameInView.origin.y - keyboardFrameInView.origin.y + 10 - if overflow > 0 { + let overflow = suggestionsFrameInView.intersection(keyboardFrameInView).height + if overflow > 0 && !appSettings.currentAddressBarPosition.isBottom { suggestionsTray.applyContentInset(UIEdgeInsets(top: 0, left: 0, bottom: overflow, right: 0)) } else { suggestionsTray.applyContentInset(.zero) @@ -673,6 +677,7 @@ class MainViewController: UIViewController { } @objc func dismissSuggestionTray() { + omniBar.cancel() dismissOmniBar() } @@ -770,6 +775,8 @@ class MainViewController: UIViewController { }) self.present(controller: alert, fromView: self.viewCoordinator.toolbar) } + + performCancel() } func onQuickFirePressed() { @@ -789,13 +796,16 @@ class MainViewController: UIViewController { @IBAction func onBackPressed() { Pixel.fire(pixel: .tabBarBackPressed) + performCancel() + hideSuggestionTray() hideNotificationBarIfBrokenSitePromptShown() currentTab?.goBack() - refreshOmniBar() } @IBAction func onForwardPressed() { Pixel.fire(pixel: .tabBarForwardPressed) + performCancel() + hideSuggestionTray() hideNotificationBarIfBrokenSitePromptShown() currentTab?.goForward() } @@ -1449,7 +1459,7 @@ class MainViewController: UIViewController { private func presentExpiredEntitlementNotification() { let presenter = NetworkProtectionNotificationsPresenterTogglableDecorator( - settings: VPNSettings(defaults: .networkProtectionGroupDefaults), + settings: AppDependencyProvider.shared.vpnSettings, defaults: .networkProtectionGroupDefaults, wrappee: NetworkProtectionUNNotificationPresenter() ) @@ -1459,45 +1469,45 @@ class MainViewController: UIViewController { @objc private func onNetworkProtectionAccountSignIn(_ notification: Notification) { tunnelDefaults.resetEntitlementMessaging() - tunnelDefaults.vpnEarlyAccessOverAlertAlreadyShown = true os_log("[NetP Subscription] Reset expired entitlement messaging", log: .networkProtection, type: .info) } + var networkProtectionTunnelController: NetworkProtectionTunnelController { + AppDependencyProvider.shared.networkProtectionTunnelController + } + @objc private func onEntitlementsChange(_ notification: Notification) { Task { - guard case .success(false) = await AccountManager().hasEntitlement(for: .networkProtection) else { return } + let accountManager = AppDependencyProvider.shared.subscriptionManager.accountManager + guard case .success(false) = await accountManager.hasEntitlement(for: .networkProtection) else { return } - let controller = NetworkProtectionTunnelController() - - if await controller.isInstalled { + if await networkProtectionTunnelController.isInstalled { tunnelDefaults.enableEntitlementMessaging() } - if await controller.isConnected { + if await networkProtectionTunnelController.isConnected { DailyPixel.fireDailyAndCount(pixel: .privacyProVPNBetaStoppedWhenPrivacyProEnabled, withAdditionalParameters: [ "reason": "entitlement-change" ]) } - await controller.stop() - await controller.removeVPN() + await networkProtectionTunnelController.stop() + await networkProtectionTunnelController.removeVPN() } } @objc private func onNetworkProtectionAccountSignOut(_ notification: Notification) { Task { - let controller = NetworkProtectionTunnelController() - - if await controller.isConnected { + if await networkProtectionTunnelController.isConnected { DailyPixel.fireDailyAndCount(pixel: .privacyProVPNBetaStoppedWhenPrivacyProEnabled, withAdditionalParameters: [ "reason": "account-signed-out" ]) } - await controller.stop() - await controller.removeVPN() + await networkProtectionTunnelController.stop() + await networkProtectionTunnelController.removeVPN() } } #endif @@ -1717,10 +1727,11 @@ extension MainViewController: OmniBarDelegate { } func onMenuPressed() { + omniBar.cancel() if !DaxDialogs.shared.shouldShowFireButtonPulse { ViewHighlighter.hideAll() } - hideSuggestionTray() + performCancel() ActionMessageView.dismissAllMessages() Task { await launchBrowsingMenu() @@ -1750,7 +1761,7 @@ extension MainViewController: OmniBarDelegate { if !DaxDialogs.shared.shouldShowFireButtonPulse { ViewHighlighter.hideAll() } - hideSuggestionTray() + performCancel() segueToBookmarks() } @@ -1762,10 +1773,6 @@ extension MainViewController: OmniBarDelegate { func onEnterPressed() { fireControllerAwarePixel(ntp: .keyboardGoWhileOnNTP, serp: .keyboardGoWhileOnSERP, website: .keyboardGoWhileOnWebsite) - - guard !viewCoordinator.suggestionTrayContainer.isHidden else { return } - - suggestionTrayController?.willDismiss(with: viewCoordinator.omniBar.textField.text ?? "") } func fireControllerAwarePixel(ntp: Pixel.Event, serp: Pixel.Event, website: Pixel.Event) { @@ -1780,8 +1787,13 @@ extension MainViewController: OmniBarDelegate { } } - func onDismissed() { - dismissOmniBar() + func onEditingEnd() -> OmniBarEditingEndResult { + if isShowingAutocompleteSuggestions { + return .suspended + } else { + dismissOmniBar() + return .dismissed + } } func onSettingsPressed() { @@ -1801,6 +1813,7 @@ extension MainViewController: OmniBarDelegate { func performCancel() { dismissOmniBar() + omniBar.cancel() hideSuggestionTray() homeController?.omniBarCancelPressed() self.showMenuHighlighterIfNeeded() @@ -1825,6 +1838,9 @@ extension MainViewController: OmniBarDelegate { } func onTextFieldWillBeginEditing(_ omniBar: OmniBar, tapped: Bool) { + // We don't want any action here if we're still in autocomplete context + guard !isShowingAutocompleteSuggestions else { return } + if let currentTab { viewCoordinator.omniBar.refreshText(forUrl: currentTab.url, forceFullURL: true) } @@ -1908,9 +1924,17 @@ extension MainViewController: FavoritesOverlayDelegate { extension MainViewController: AutocompleteViewControllerDelegate { + func autocompleteDidEndWithUserQuery() { + if let query = omniBar.textField.text { + onOmniQuerySubmitted(query + ) + } + } + func autocomplete(selectedSuggestion suggestion: Suggestion) { homeController?.chromeDelegate = nil dismissOmniBar() + viewCoordinator.omniBar.cancel() switch suggestion { case .phrase(phrase: let phrase): if let url = URL.makeSearchURL(text: phrase) { @@ -1972,6 +1996,10 @@ extension MainViewController: AutocompleteViewControllerDelegate { viewCoordinator.omniBar.selectTextToEnd(query.count) } case .historyEntry(title: let title, let url, _): + if url.isDuckDuckGoSearch, let query = url.searchQuery { + viewCoordinator.omniBar.textField.text = query + } + if (title ?? url.absoluteString).hasPrefix(query) { viewCoordinator.omniBar.selectTextToEnd(query.count) } @@ -2321,11 +2349,13 @@ extension MainViewController: TabSwitcherButtonDelegate { func launchNewTab(_ button: TabSwitcherButton) { Pixel.fire(pixel: .tabSwitchLongPressNewTab) + performCancel() newTab() } func showTabSwitcher(_ button: TabSwitcherButton) { Pixel.fire(pixel: .tabBarTabSwitcherPressed) + performCancel() showTabSwitcher() } @@ -2405,6 +2435,11 @@ extension MainViewController: AutoClearWorker { @MainActor func forgetData() async { + await forgetData(applicationState: .unknown) + } + + @MainActor + func forgetData(applicationState: DataStoreWarmup.ApplicationState) async { guard !clearInProgress else { assertionFailure("Shouldn't get called multiple times") return @@ -2413,7 +2448,7 @@ extension MainViewController: AutoClearWorker { // This needs to happen only once per app launch if let dataStoreWarmup { - await dataStoreWarmup.ensureReady() + await dataStoreWarmup.ensureReady(applicationState: applicationState) self.dataStoreWarmup = nil } diff --git a/DuckDuckGo/NetworkProtectionAccessController.swift b/DuckDuckGo/NetworkProtectionAccessController.swift index aa683bf228..904ad982c2 100644 --- a/DuckDuckGo/NetworkProtectionAccessController.swift +++ b/DuckDuckGo/NetworkProtectionAccessController.swift @@ -25,23 +25,12 @@ import ContentBlocking import Core import NetworkProtection import Waitlist +import Subscription enum NetworkProtectionAccessType { /// Used if the user does not have waitlist feature flag access case none - /// Used if the user has waitlist feature flag access, but has not joined the waitlist - case waitlistAvailable - - /// Used if the user has waitlist feature flag access, and has joined the waitlist - case waitlistJoined - - /// Used if the user has been invited via the waitlist, but needs to accept the Privacy Policy and Terms of Service - case waitlistInvitedPendingTermsAcceptance - - /// Used if the user has been invited via the waitlist and has accepted the Privacy Policy and Terms of Service - case waitlistInvited - /// Used if the user has been invited to test Network Protection directly case inviteCodeInvited } @@ -53,10 +42,11 @@ protocol NetworkProtectionAccess { struct NetworkProtectionAccessController: NetworkProtectionAccess { private let networkProtectionActivation: NetworkProtectionFeatureActivation - private let networkProtectionWaitlistStorage: WaitlistStorage private let networkProtectionTermsAndConditionsStore: NetworkProtectionTermsAndConditionsStore private let featureFlagger: FeatureFlagger private let internalUserDecider: InternalUserDecider + private let networkProtectionKeychainTokenStore: NetworkProtectionKeychainTokenStore + private let networkProtectionTunnelController: NetworkProtectionTunnelController private var isUserLocaleAllowed: Bool { var regionCode: String? @@ -70,18 +60,18 @@ struct NetworkProtectionAccessController: NetworkProtectionAccess { return (regionCode ?? "US") == "US" } - init( - networkProtectionActivation: NetworkProtectionFeatureActivation = NetworkProtectionKeychainTokenStore(), - networkProtectionWaitlistStorage: WaitlistStorage = WaitlistKeychainStore(waitlistIdentifier: VPNWaitlist.identifier), - networkProtectionTermsAndConditionsStore: NetworkProtectionTermsAndConditionsStore = NetworkProtectionTermsAndConditionsUserDefaultsStore(), - featureFlagger: FeatureFlagger = AppDependencyProvider.shared.featureFlagger, - internalUserDecider: InternalUserDecider = AppDependencyProvider.shared.internalUserDecider + init(networkProtectionTermsAndConditionsStore: NetworkProtectionTermsAndConditionsStore = NetworkProtectionTermsAndConditionsUserDefaultsStore(), + featureFlagger: FeatureFlagger = AppDependencyProvider.shared.featureFlagger, + internalUserDecider: InternalUserDecider = AppDependencyProvider.shared.internalUserDecider, + tokenStore: NetworkProtectionKeychainTokenStore, + networkProtectionTunnelController: NetworkProtectionTunnelController ) { - self.networkProtectionActivation = networkProtectionActivation - self.networkProtectionWaitlistStorage = networkProtectionWaitlistStorage + self.networkProtectionActivation = tokenStore + self.networkProtectionKeychainTokenStore = tokenStore self.networkProtectionTermsAndConditionsStore = networkProtectionTermsAndConditionsStore self.featureFlagger = featureFlagger self.internalUserDecider = internalUserDecider + self.networkProtectionTunnelController = networkProtectionTunnelController } func networkProtectionAccessType() -> NetworkProtectionAccessType { @@ -91,7 +81,7 @@ struct NetworkProtectionAccessController: NetworkProtectionAccess { } // Check for users who have activated the VPN via an invite code: - if networkProtectionActivation.isFeatureActivated && !networkProtectionWaitlistStorage.isInvited { + if networkProtectionActivation.isFeatureActivated { return .inviteCodeInvited } @@ -101,25 +91,6 @@ struct NetworkProtectionAccessController: NetworkProtectionAccess { return .none } - // Check if a waitlist user has NetP access and whether they need to accept T&C. - if networkProtectionActivation.isFeatureActivated && networkProtectionWaitlistStorage.isInvited { - if networkProtectionTermsAndConditionsStore.networkProtectionWaitlistTermsAndConditionsAccepted { - return .waitlistInvited - } else { - return .waitlistInvitedPendingTermsAcceptance - } - } - - // Check if the user has waitlist access at all and whether they've already joined. - let hasWaitlistAccess = featureFlagger.isFeatureOn(.networkProtectionWaitlistAccess) - if hasWaitlistAccess { - if networkProtectionWaitlistStorage.isOnWaitlist { - return .waitlistJoined - } else { - return .waitlistAvailable - } - } - return .none } @@ -134,15 +105,13 @@ struct NetworkProtectionAccessController: NetworkProtectionAccess { } func revokeNetworkProtectionAccess() { - try? NetworkProtectionKeychainTokenStore().deleteToken() + try? networkProtectionKeychainTokenStore.deleteToken() Task { - let controller = NetworkProtectionTunnelController() - await controller.stop() - await controller.removeVPN() + await networkProtectionTunnelController.stop() + await networkProtectionTunnelController.removeVPN() } } - } #endif diff --git a/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift b/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift index dcc76a9ac6..5873925cfc 100644 --- a/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift +++ b/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift @@ -56,34 +56,17 @@ extension ConnectionServerInfoObserverThroughSession { } } -extension NetworkProtectionKeychainTokenStore { - convenience init() { - let featureVisibility = DefaultNetworkProtectionVisibility.forTokenStore() - let isSubscriptionEnabled = featureVisibility.isPrivacyProLaunched() - let accessTokenProvider: () -> String? = { - if featureVisibility.shouldMonitorEntitlement() { - return { AccountManager().accessToken } - } - return { nil } - }() - - self.init(keychainType: .dataProtection(.unspecified), - serviceName: "\(Bundle.main.bundleIdentifier!).authToken", - errorEvents: .networkProtectionAppDebugEvents, - isSubscriptionEnabled: isSubscriptionEnabled, - accessTokenProvider: accessTokenProvider) - } -} - extension NetworkProtectionCodeRedemptionCoordinator { - convenience init(isManualCodeRedemptionFlow: Bool = false) { - let settings = VPNSettings(defaults: .networkProtectionGroupDefaults) + + convenience init(isManualCodeRedemptionFlow: Bool = false, accountManager: AccountManaging) { + let settings = AppDependencyProvider.shared.vpnSettings + let networkProtectionVisibility = AppDependencyProvider.shared.vpnFeatureVisibility self.init( environment: settings.selectedEnvironment, - tokenStore: NetworkProtectionKeychainTokenStore(), + tokenStore: AppDependencyProvider.shared.networkProtectionKeychainTokenStore, isManualCodeRedemptionFlow: isManualCodeRedemptionFlow, errorEvents: .networkProtectionAppDebugEvents, - isSubscriptionEnabled: DefaultNetworkProtectionVisibility().isPrivacyProLaunched() + isSubscriptionEnabled: networkProtectionVisibility.isPrivacyProLaunched() ) } } @@ -92,29 +75,31 @@ extension NetworkProtectionVPNSettingsViewModel { convenience init() { self.init( notificationsAuthorization: NotificationsAuthorizationController(), - settings: VPNSettings(defaults: .networkProtectionGroupDefaults) + settings: AppDependencyProvider.shared.vpnSettings ) } } extension NetworkProtectionLocationListCompositeRepository { - convenience init() { - let settings = VPNSettings(defaults: .networkProtectionGroupDefaults) + + convenience init(accountManager: AccountManaging) { + let settings = AppDependencyProvider.shared.vpnSettings self.init( environment: settings.selectedEnvironment, - tokenStore: NetworkProtectionKeychainTokenStore(), + tokenStore: AppDependencyProvider.shared.networkProtectionKeychainTokenStore, errorEvents: .networkProtectionAppDebugEvents, - isSubscriptionEnabled: DefaultNetworkProtectionVisibility().isPrivacyProLaunched() + isSubscriptionEnabled: AppDependencyProvider.shared.vpnFeatureVisibility.isPrivacyProLaunched() ) } } extension NetworkProtectionVPNLocationViewModel { - convenience init() { - let locationListRepository = NetworkProtectionLocationListCompositeRepository() + + convenience init(accountManager: AccountManaging) { + let locationListRepository = NetworkProtectionLocationListCompositeRepository(accountManager: accountManager) self.init( locationListRepository: locationListRepository, - settings: VPNSettings(defaults: .networkProtectionGroupDefaults) + settings: AppDependencyProvider.shared.vpnSettings ) } } diff --git a/DuckDuckGo/NetworkProtectionDebugViewController.swift b/DuckDuckGo/NetworkProtectionDebugViewController.swift index 837d962442..73bda35795 100644 --- a/DuckDuckGo/NetworkProtectionDebugViewController.swift +++ b/DuckDuckGo/NetworkProtectionDebugViewController.swift @@ -35,7 +35,6 @@ import NetworkExtension import NetworkProtection import Subscription - // swiftlint:disable:next type_body_length final class NetworkProtectionDebugViewController: UITableViewController { private let titles = [ @@ -96,7 +95,6 @@ final class NetworkProtectionDebugViewController: UITableViewController { case shutDown case showEntitlementMessaging case resetEntitlementMessaging - case resetThankYouMessaging } enum NetworkPathRows: Int, CaseIterable { @@ -138,21 +136,25 @@ final class NetworkProtectionDebugViewController: UITableViewController { private var connectionTestResults: [ConnectionTestResult] = [] private var connectionTestResultError: String? private let connectionTestQueue = DispatchQueue(label: "com.duckduckgo.ios.vpnDebugConnectionTestQueue") + private let accountManager: AccountManaging // MARK: Lifecycle - init?(coder: NSCoder, - tokenStore: NetworkProtectionTokenStore, - debugFeatures: NetworkProtectionDebugFeatures = NetworkProtectionDebugFeatures()) { - + required init?(coder: NSCoder, + tokenStore: NetworkProtectionTokenStore, + debugFeatures: NetworkProtectionDebugFeatures = NetworkProtectionDebugFeatures(), + accountManager: AccountManaging) { + self.debugFeatures = debugFeatures self.tokenStore = tokenStore + self.accountManager = accountManager super.init(coder: coder) } required convenience init?(coder: NSCoder) { - self.init(coder: coder, tokenStore: NetworkProtectionKeychainTokenStore()) + self.init(coder: coder, tokenStore: AppDependencyProvider.shared.networkProtectionKeychainTokenStore, + accountManager: AppDependencyProvider.shared.subscriptionManager.accountManager) } override func viewWillAppear(_ animated: Bool) { @@ -389,8 +391,6 @@ final class NetworkProtectionDebugViewController: UITableViewController { cell.textLabel?.text = "Show Entitlement Messaging" case .resetEntitlementMessaging: cell.textLabel?.text = "Reset Entitlement Messaging" - case .resetThankYouMessaging: - cell.textLabel?.text = "Reset Thank You Messaging" case .none: break } @@ -410,8 +410,6 @@ final class NetworkProtectionDebugViewController: UITableViewController { UserDefaults.networkProtectionGroupDefaults.enableEntitlementMessaging() case .resetEntitlementMessaging: UserDefaults.networkProtectionGroupDefaults.resetEntitlementMessaging() - case .resetThankYouMessaging: - UserDefaults.networkProtectionGroupDefaults.resetThankYouMessaging() case .none: break } @@ -628,7 +626,7 @@ final class NetworkProtectionDebugViewController: UITableViewController { private func configure(_ cell: UITableViewCell, forVisibilityRow row: Int) { switch FeatureVisibilityRows(rawValue: row) { case .toggleSelectedEnvironment: - let settings = VPNSettings(defaults: .networkProtectionGroupDefaults) + let settings = AppDependencyProvider.shared.vpnSettings if settings.selectedEnvironment == .production { cell.textLabel?.text = "Selected Environment: PRODUCTION" } else { @@ -642,18 +640,14 @@ final class NetworkProtectionDebugViewController: UITableViewController { cell.textLabel?.text = "Subscription Override: N/A" } case .debugInfo: - let vpnVisibility = DefaultNetworkProtectionVisibility() + let vpnVisibility = AppDependencyProvider.shared.vpnFeatureVisibility cell.textLabel?.font = .monospacedSystemFont(ofSize: 13.0, weight: .regular) cell.textLabel?.text = """ -Endpoint: \(VPNSettings(defaults: .networkProtectionGroupDefaults).selectedEnvironment.endpointURL.absoluteString) +Endpoint: \(AppDependencyProvider.shared.vpnSettings.selectedEnvironment.endpointURL.absoluteString) isPrivacyProLaunched: \(vpnVisibility.isPrivacyProLaunched() ? "YES" : "NO") -isWaitlistBetaActive: \(vpnVisibility.isWaitlistBetaActive() ? "YES" : "NO") -isWaitlistUser: \(vpnVisibility.isWaitlistUser() ? "YES" : "NO") -shouldShowThankYouMessaging: \(vpnVisibility.shouldShowThankYouMessaging() ? "YES" : "NO") -shouldKeepVPNAccessViaWaitlist: \(vpnVisibility.shouldKeepVPNAccessViaWaitlist() ? "YES" : "NO") shouldMonitorEntitlement: \(vpnVisibility.shouldMonitorEntitlement() ? "YES" : "NO") shouldShowVPNShortcut: \(vpnVisibility.shouldShowVPNShortcut() ? "YES" : "NO") """ @@ -664,7 +658,9 @@ shouldShowVPNShortcut: \(vpnVisibility.shouldShowVPNShortcut() ? "YES" : "NO") @MainActor private func refreshMetadata() async { - let collector = DefaultVPNMetadataCollector() + let collector = DefaultVPNMetadataCollector(statusObserver: AppDependencyProvider.shared.connectionObserver, + networkProtectionAccessManager: AppDependencyProvider.shared.networkProtectionAccessController, + tokenStore: AppDependencyProvider.shared.networkProtectionKeychainTokenStore) self.vpnMetadata = await collector.collectMetadata() self.tableView.reloadData() } @@ -692,7 +688,7 @@ shouldShowVPNShortcut: \(vpnVisibility.shouldShowVPNShortcut() ? "YES" : "NO") if let subscriptionOverrideEnabled = defaults.subscriptionOverrideEnabled { if subscriptionOverrideEnabled { defaults.subscriptionOverrideEnabled = false - AccountManager().signOut() + accountManager.signOut() } else { defaults.resetsubscriptionOverrideEnabled() } @@ -712,13 +708,10 @@ shouldShowVPNShortcut: \(vpnVisibility.shouldShowVPNShortcut() ? "YES" : "NO") } private func clearAllVPNData() { - let accessController = NetworkProtectionAccessController() - accessController.revokeNetworkProtectionAccess() + AppDependencyProvider.shared.networkProtectionAccessController.revokeNetworkProtectionAccess() } - } - extension NWConnection { var stateUpdateStream: AsyncStream { diff --git a/DuckDuckGo/NetworkProtectionFeatureVisibility.swift b/DuckDuckGo/NetworkProtectionFeatureVisibility.swift index b0d1be1cce..7bcff5c708 100644 --- a/DuckDuckGo/NetworkProtectionFeatureVisibility.swift +++ b/DuckDuckGo/NetworkProtectionFeatureVisibility.swift @@ -21,41 +21,13 @@ import Foundation import Subscription public protocol NetworkProtectionFeatureVisibility { - func isWaitlistBetaActive() -> Bool - func isWaitlistUser() -> Bool func isPrivacyProLaunched() -> Bool - /// Whether to show the thank-you messaging for current waitlist users - func shouldShowThankYouMessaging() -> Bool - - /// Whether to let the user continues to use the VPN via the waitlist - /// This should only before 100% roll out - func shouldKeepVPNAccessViaWaitlist() -> Bool - /// Whether to enforce entitlement check and show entitlement-related messaging /// This should always happen after 100% roll out /// N.B. Backend will independently check for valid entitlement regardless of this value func shouldMonitorEntitlement() -> Bool - /// Whether to show VPN shortcut on the homescreen + /// Whether to show VPN shortcut on the home screen func shouldShowVPNShortcut() -> Bool } - -public extension NetworkProtectionFeatureVisibility { - func shouldShowThankYouMessaging() -> Bool { - isPrivacyProLaunched() && isWaitlistUser() - } - - func shouldKeepVPNAccessViaWaitlist() -> Bool { - !isPrivacyProLaunched() && isWaitlistBetaActive() && isWaitlistUser() - } - - func shouldShowVPNShortcut() -> Bool { - if isPrivacyProLaunched() { - let accountManager = AccountManager() - return accountManager.isUserAuthenticated - } else { - return shouldKeepVPNAccessViaWaitlist() - } - } -} diff --git a/DuckDuckGo/NetworkProtectionInviteView.swift b/DuckDuckGo/NetworkProtectionInviteView.swift deleted file mode 100644 index a505118dc8..0000000000 --- a/DuckDuckGo/NetworkProtectionInviteView.swift +++ /dev/null @@ -1,157 +0,0 @@ -// -// NetworkProtectionInviteView.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. -// - -#if NETWORK_PROTECTION - -import SwiftUI -import DesignResourcesKit -import DuckUI - -struct NetworkProtectionInviteView: View { - @ObservedObject var model: NetworkProtectionInviteViewModel - - var body: some View { - Group { - switch model.currentStep { - case .codeEntry: - codeEntryView - case .success: - successView - } - } - .transition(.slideFromRight) - .animation(.default, value: model.currentStep.isSuccess) - } - - @ViewBuilder - private var codeEntryView: some View { - let messageData = NetworkProtectionInviteMessageData( - imageIdentifier: "InviteLock", - title: UserText.netPInviteTitle, - message: UserText.netPInviteMessage - ) - - NetworkProtectionInviteMessageView(messageData: messageData) { - ClearTextField( - placeholderText: UserText.netPInviteFieldPrompt, - text: $model.text, - keyboardType: .asciiCapable - ) - .frame(height: 44) - .padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)) - .background(Color(designSystemColor: .surface)) - .cornerRadius(10) - .disabled(model.shouldDisableTextField) - Button(UserText.inviteDialogContinueButton) { - Task { - await model.submit() - } - } - .buttonStyle(PrimaryButtonStyle(disabled: model.shouldDisableSubmit)) - .disabled(model.shouldDisableSubmit) - } - .alert(isPresented: $model.shouldShowAlert) { - Alert( - title: Text(model.errorText), - dismissButton: .default(Text(UserText.inviteDialogErrorAlertOKButton)) - ) - } - } - - @ViewBuilder - private var successView: some View { - let messageData = NetworkProtectionInviteMessageData( - imageIdentifier: "InviteLockSuccess", - title: UserText.netPInviteSuccessTitle, - message: UserText.netPInviteSuccessMessage - ) - - NetworkProtectionInviteMessageView(messageData: messageData) { - Button(UserText.inviteDialogGetStartedButton, action: model.getStarted) - .buttonStyle(PrimaryButtonStyle()) - } - } -} - -private struct NetworkProtectionInviteMessageView: View where Content: View { - let messageData: NetworkProtectionInviteMessageData - @ViewBuilder let interactiveContent: () -> Content - - var body: some View { - GeometryReader { proxy in - ScrollView { - VStack(spacing: 16) { - Image(messageData.imageIdentifier) - .resizable() - .scaledToFit() - .frame(height: 102.5) - Text(messageData.title) - .daxTitle2() - .multilineTextAlignment(.center) - .foregroundColor(.init(designSystemColor: .textPrimary)) - Text(messageData.message) - .daxBodyRegular() - .multilineTextAlignment(.center) - .foregroundColor(.init(designSystemColor: .textSecondary)) - .padding(.bottom, 16) - interactiveContent() - Spacer() - Text(UserText.networkProtectionWaitlistAvailabilityDisclaimer) - .foregroundColor(.init(designSystemColor: .textSecondary)) - .daxFootnoteRegular() - .multilineTextAlignment(.center) - } - .padding(24) - .frame(minHeight: proxy.size.height) - .background(Color(designSystemColor: .background)) - } - } - .background(Color(designSystemColor: .background)) - } -} - -private struct NetworkProtectionInviteMessageData { - let imageIdentifier: String - let title: String - let message: String - let footer = UserText.networkProtectionWaitlistAvailabilityDisclaimer -} - -extension AnyTransition { - static var slideFromRight: AnyTransition { - AnyTransition.asymmetric( - insertion: .move(edge: .trailing), - removal: .move(edge: .leading) - ) - } -} - -import NetworkProtection - -struct NetworkProtectionInviteView_Previews: PreviewProvider { - static var previews: some View { - NetworkProtectionInviteView( - model: NetworkProtectionInviteViewModel( - redemptionCoordinator: NetworkProtectionCodeRedemptionCoordinator() - ) { } - ) - } -} - -#endif diff --git a/DuckDuckGo/NetworkProtectionInviteViewModel.swift b/DuckDuckGo/NetworkProtectionInviteViewModel.swift deleted file mode 100644 index 0ee06a582e..0000000000 --- a/DuckDuckGo/NetworkProtectionInviteViewModel.swift +++ /dev/null @@ -1,94 +0,0 @@ -// -// NetworkProtectionInviteViewModel.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. -// - -#if NETWORK_PROTECTION - -import Combine -import NetworkProtection - -enum NetworkProtectionInviteStep { - case codeEntry, success - - var isSuccess: Bool { - self == .success - } -} - -final class NetworkProtectionInviteViewModel: ObservableObject { - @Published var currentStep: NetworkProtectionInviteStep = .codeEntry - @Published var text: String = "" { - didSet { - if oldValue != text { - text = text.uppercased() - shouldDisableSubmit = text.count == 0 - } - } - } - var errorText: String = "" - @Published var shouldShowAlert: Bool = false - @Published var shouldDisableSubmit: Bool = true - @Published var shouldDisableTextField: Bool = false - - private let redemptionCoordinator: NetworkProtectionCodeRedeeming - private let completion: () -> Void - - init(redemptionCoordinator: NetworkProtectionCodeRedeeming, completion: @escaping () -> Void) { - self.completion = completion - self.redemptionCoordinator = redemptionCoordinator - } - - private var isLoading = false - - @MainActor - func submit() async { - guard !isLoading else { - return - } - isLoading = true - shouldDisableTextField = true - defer { - shouldDisableTextField = false - isLoading = false - } - do { - try await redemptionCoordinator.redeem(text.trimmingWhitespace()) - } catch NetworkProtectionClientError.invalidInviteCode { - errorText = UserText.inviteDialogUnrecognizedCodeMessage - shouldShowAlert = true - return - } catch { - errorText = UserText.unknownErrorTryAgainMessage - shouldShowAlert = true - return - } - currentStep = .success - } - - func getStarted() { - completion() - } - - @Published var redeemedText: String? - - private func updateAuthenticatedText() { - redeemedText = NetworkProtectionKeychainTokenStore().isFeatureActivated ? "Already redeemed" : nil - } -} - -#endif diff --git a/DuckDuckGo/NetworkProtectionRootView.swift b/DuckDuckGo/NetworkProtectionRootView.swift index 4fd5c58998..bdad605a52 100644 --- a/DuckDuckGo/NetworkProtectionRootView.swift +++ b/DuckDuckGo/NetworkProtectionRootView.swift @@ -21,30 +21,25 @@ import SwiftUI import NetworkProtection +import Subscription @available(iOS 15, *) struct NetworkProtectionRootView: View { - let model = NetworkProtectionRootViewModel() - let inviteCompletion: () -> Void + let statusViewModel: NetworkProtectionStatusViewModel + init() { + let accountManager = AppDependencyProvider.shared.subscriptionManager.accountManager + let locationListRepository = NetworkProtectionLocationListCompositeRepository(accountManager: accountManager) + statusViewModel = NetworkProtectionStatusViewModel(tunnelController: AppDependencyProvider.shared.networkProtectionTunnelController, + settings: AppDependencyProvider.shared.vpnSettings, + statusObserver: AppDependencyProvider.shared.connectionObserver, + locationListRepository: locationListRepository) + } var body: some View { - let inviteViewModel = NetworkProtectionInviteViewModel( - redemptionCoordinator: NetworkProtectionCodeRedemptionCoordinator(isManualCodeRedemptionFlow: true), - completion: inviteCompletion - ) - if DefaultNetworkProtectionVisibility().isPrivacyProLaunched() { + if AppDependencyProvider.shared.vpnFeatureVisibility.isPrivacyProLaunched() { NetworkProtectionStatusView( - statusModel: NetworkProtectionStatusViewModel() + statusModel: statusViewModel ) - } else { - switch model.initialViewKind { - case .invite: - NetworkProtectionInviteView(model: inviteViewModel) - case .status: - NetworkProtectionStatusView( - statusModel: NetworkProtectionStatusViewModel() - ) - } } } } diff --git a/DuckDuckGo/NetworkProtectionRootViewController.swift b/DuckDuckGo/NetworkProtectionRootViewController.swift index 010aa988f6..013f6132bc 100644 --- a/DuckDuckGo/NetworkProtectionRootViewController.swift +++ b/DuckDuckGo/NetworkProtectionRootViewController.swift @@ -24,8 +24,8 @@ import SwiftUI @available(iOS 15, *) final class NetworkProtectionRootViewController: UIHostingController { - init(inviteCompletion: @escaping () -> Void = { }) { - let rootView = NetworkProtectionRootView(inviteCompletion: inviteCompletion) + init() { + let rootView = NetworkProtectionRootView() super.init(rootView: rootView) } diff --git a/DuckDuckGo/NetworkProtectionRootViewModel.swift b/DuckDuckGo/NetworkProtectionRootViewModel.swift deleted file mode 100644 index e47b5d03e9..0000000000 --- a/DuckDuckGo/NetworkProtectionRootViewModel.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// NetworkProtectionRootViewModel.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. -// - -#if NETWORK_PROTECTION - -import Foundation -import NetworkProtection - -enum NetworkProtectionInitialViewKind { - case invite - case status -} - -final class NetworkProtectionRootViewModel: ObservableObject { - var initialViewKind: NetworkProtectionInitialViewKind - - init(featureActivation: NetworkProtectionFeatureActivation = NetworkProtectionKeychainTokenStore()) { - initialViewKind = featureActivation.isFeatureActivated ? .status : .invite - } -} - -#endif diff --git a/DuckDuckGo/NetworkProtectionStatusViewModel.swift b/DuckDuckGo/NetworkProtectionStatusViewModel.swift index 0d317bbdcb..c9f29dfc4f 100644 --- a/DuckDuckGo/NetworkProtectionStatusViewModel.swift +++ b/DuckDuckGo/NetworkProtectionStatusViewModel.swift @@ -142,12 +142,12 @@ final class NetworkProtectionStatusViewModel: ObservableObject { @Published public var animationsOn: Bool = false - public init(tunnelController: TunnelController = NetworkProtectionTunnelController(), - settings: VPNSettings = VPNSettings(defaults: .networkProtectionGroupDefaults), - statusObserver: ConnectionStatusObserver = ConnectionStatusObserverThroughSession(), + public init(tunnelController: TunnelController, + settings: VPNSettings, + statusObserver: ConnectionStatusObserver, serverInfoObserver: ConnectionServerInfoObserver = ConnectionServerInfoObserverThroughSession(), errorObserver: ConnectionErrorObserver = ConnectionErrorObserverThroughSession(), - locationListRepository: NetworkProtectionLocationListRepository = NetworkProtectionLocationListCompositeRepository()) { + locationListRepository: NetworkProtectionLocationListRepository) { self.tunnelController = tunnelController self.settings = settings self.statusObserver = statusObserver diff --git a/DuckDuckGo/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtectionTunnelController.swift index 3aaedb8115..4952559934 100644 --- a/DuckDuckGo/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtectionTunnelController.swift @@ -24,12 +24,13 @@ import Combine import Core import NetworkExtension import NetworkProtection +import Subscription final class NetworkProtectionTunnelController: TunnelController { static var shouldSimulateFailure: Bool = false private let debugFeatures = NetworkProtectionDebugFeatures() - private let tokenStore = NetworkProtectionKeychainTokenStore() + private let tokenStore: NetworkProtectionKeychainTokenStore private let errorStore = NetworkProtectionTunnelErrorStore() private let notificationCenter: NotificationCenter = .default private var previousStatus: NEVPNStatus = .invalid @@ -72,7 +73,8 @@ final class NetworkProtectionTunnelController: TunnelController { } } - init() { + init(accountManager: AccountManaging, tokenStore: NetworkProtectionKeychainTokenStore) { + self.tokenStore = tokenStore subscribeToStatusChanges() } @@ -185,7 +187,7 @@ final class NetworkProtectionTunnelController: TunnelController { } catch { throw StartError.fetchAuthTokenFailed(error) } - options[NetworkProtectionOptionKey.selectedEnvironment] = VPNSettings(defaults: .networkProtectionGroupDefaults) + options[NetworkProtectionOptionKey.selectedEnvironment] = AppDependencyProvider.shared.vpnSettings .selectedEnvironment.rawValue as NSString do { diff --git a/DuckDuckGo/NetworkProtectionVPNLocationView.swift b/DuckDuckGo/NetworkProtectionVPNLocationView.swift index 2708c23d49..420a952e58 100644 --- a/DuckDuckGo/NetworkProtectionVPNLocationView.swift +++ b/DuckDuckGo/NetworkProtectionVPNLocationView.swift @@ -24,7 +24,7 @@ import SwiftUI @available(iOS 15, *) struct NetworkProtectionVPNLocationView: View { - @StateObject var model = NetworkProtectionVPNLocationViewModel() + @StateObject var model = NetworkProtectionVPNLocationViewModel(accountManager: AppDependencyProvider.shared.subscriptionManager.accountManager) var body: some View { List { diff --git a/DuckDuckGo/NetworkProtectionVisibilityForTunnelProvider.swift b/DuckDuckGo/NetworkProtectionVisibilityForTunnelProvider.swift index dce270a1a5..c034b3ed3d 100644 --- a/DuckDuckGo/NetworkProtectionVisibilityForTunnelProvider.swift +++ b/DuckDuckGo/NetworkProtectionVisibilityForTunnelProvider.swift @@ -23,21 +23,28 @@ import Foundation import Subscription struct NetworkProtectionVisibilityForTunnelProvider: NetworkProtectionFeatureVisibility { - func isWaitlistBetaActive() -> Bool { - preconditionFailure("Does not apply to Tunnel Provider") - } - func isWaitlistUser() -> Bool { - preconditionFailure("Does not apply to Tunnel Provider") + private let accountManager: AccountManaging + + init(accountManager: AccountManaging) { + self.accountManager = accountManager } - + func isPrivacyProLaunched() -> Bool { - AccountManager().isUserAuthenticated + accountManager.isUserAuthenticated } func shouldMonitorEntitlement() -> Bool { isPrivacyProLaunched() } + + func shouldShowVPNShortcut() -> Bool { + guard isPrivacyProLaunched() else { + return false + } + + return accountManager.isUserAuthenticated + } } #endif diff --git a/DuckDuckGo/NoSuggestionsTableViewCell.swift b/DuckDuckGo/NoSuggestionsTableViewCell.swift deleted file mode 100644 index db61a10047..0000000000 --- a/DuckDuckGo/NoSuggestionsTableViewCell.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// NoSuggestionsTableViewCell.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 - -class NoSuggestionsTableViewCell: UITableViewCell { - - static let reuseIdentifier = "NoSuggestionsTableViewCell" - - @IBOutlet weak var label: UILabel! -} diff --git a/DuckDuckGo/OmniBar.swift b/DuckDuckGo/OmniBar.swift index 1991df9c4f..7932bbf32a 100644 --- a/DuckDuckGo/OmniBar.swift +++ b/DuckDuckGo/OmniBar.swift @@ -229,6 +229,10 @@ class OmniBar: UIView { separatorToBottom.constant = 0 } + func cancel() { + refreshState(state.onEditingStoppedState) + } + func startBrowsing() { refreshState(state.onBrowsingStartedState) } @@ -446,6 +450,7 @@ class OmniBar: UIView { @IBAction func onCancelPressed(_ sender: Any) { omniDelegate?.onCancelPressed() + refreshState(state.onEditingStoppedState) } @IBAction func onRefreshPressed(_ sender: Any) { @@ -517,8 +522,12 @@ extension OmniBar: UITextFieldDelegate { } func textFieldDidEndEditing(_ textField: UITextField) { - omniDelegate?.onDismissed() - refreshState(state.onEditingStoppedState) + switch omniDelegate?.onEditingEnd() { + case .dismissed, .none: + refreshState(state.onEditingStoppedState) + case .suspended: + refreshState(state.onEditingSuspendedState) + } } } diff --git a/DuckDuckGo/OmniBarDelegate.swift b/DuckDuckGo/OmniBarDelegate.swift index ea9f7592ff..c861af47d0 100644 --- a/DuckDuckGo/OmniBarDelegate.swift +++ b/DuckDuckGo/OmniBarDelegate.swift @@ -20,6 +20,11 @@ import Foundation import Suggestions +enum OmniBarEditingEndResult { + case suspended + case dismissed +} + protocol OmniBarDelegate: AnyObject { func onOmniQueryUpdated(_ query: String) @@ -28,8 +33,8 @@ protocol OmniBarDelegate: AnyObject { func onOmniSuggestionSelected(_ suggestion: Suggestion) - func onDismissed() - + func onEditingEnd() -> OmniBarEditingEndResult + func onPrivacyIconPressed() func onMenuPressed() @@ -77,10 +82,6 @@ extension OmniBarDelegate { } - func onDismissed() { - - } - func onPrivacyIconPressed() { } diff --git a/DuckDuckGo/OmniBarState.swift b/DuckDuckGo/OmniBarState.swift index 7f8f21a34f..db7330bd9c 100644 --- a/DuckDuckGo/OmniBarState.swift +++ b/DuckDuckGo/OmniBarState.swift @@ -41,6 +41,7 @@ protocol OmniBarState { var showVoiceSearch: Bool { get } var name: String { get } var onEditingStoppedState: OmniBarState { get } + var onEditingSuspendedState: OmniBarState { get } var onEditingStartedState: OmniBarState { get } var onTextClearedState: OmniBarState { get } var onTextEnteredState: OmniBarState { get } @@ -50,3 +51,9 @@ protocol OmniBarState { var onEnterPadState: OmniBarState { get } var onReloadState: OmniBarState { get } } + +extension OmniBarState { + var onEditingSuspendedState: OmniBarState { + UniversalOmniBarState.EditingSuspendedState(baseState: self.onEditingStartedState) + } +} diff --git a/DuckDuckGo/PrivateSearchView.swift b/DuckDuckGo/PrivateSearchView.swift index 4f0dae3009..25848a3f54 100644 --- a/DuckDuckGo/PrivateSearchView.swift +++ b/DuckDuckGo/PrivateSearchView.swift @@ -58,26 +58,32 @@ struct PrivateSearchViewSettings: View { // Autocomplete Suggestions SettingsCellView(label: UserText.settingsAutocomplete, accesory: .toggle(isOn: viewModel.autocompletePrivateSearchBinding)) + + if viewModel.shouldShowRecentlyVisitedSites { + SettingsCellView(label: UserText.settingsAutocompleteRecentlyVisited, + accesory: .toggle(isOn: viewModel.autocompleteRecentlyVisitedSitesBinding)) + } + } - Section(footer: Text(UserText.voiceSearchFooter)) { - // Private Voice Search - if viewModel.state.speechRecognitionAvailable { + if viewModel.state.speechRecognitionAvailable { + Section(footer: Text(UserText.voiceSearchFooter)) { + // Private Voice Search SettingsCellView(label: UserText.settingsVoiceSearch, accesory: .toggle(isOn: viewModel.voiceSearchEnabledPrivateSearchBinding)) } - } - .alert(isPresented: $shouldShowNoMicrophonePermissionAlert) { - Alert(title: Text(UserText.noVoicePermissionAlertTitle), - message: Text(UserText.noVoicePermissionAlertMessage), - dismissButton: .default(Text(UserText.noVoicePermissionAlertOKbutton), - action: { - viewModel.shouldShowNoMicrophonePermissionAlert = false - }) - ) - } - .onChange(of: viewModel.shouldShowNoMicrophonePermissionAlert) { value in - shouldShowNoMicrophonePermissionAlert = value + .alert(isPresented: $shouldShowNoMicrophonePermissionAlert) { + Alert(title: Text(UserText.noVoicePermissionAlertTitle), + message: Text(UserText.noVoicePermissionAlertMessage), + dismissButton: .default(Text(UserText.noVoicePermissionAlertOKbutton), + action: { + viewModel.shouldShowNoMicrophonePermissionAlert = false + }) + ) + } + .onChange(of: viewModel.shouldShowNoMicrophonePermissionAlert) { value in + shouldShowNoMicrophonePermissionAlert = value + } } Section { diff --git a/DuckDuckGo/RemoteMessaging.swift b/DuckDuckGo/RemoteMessaging.swift index a0dd506002..a7ebf8dc4b 100644 --- a/DuckDuckGo/RemoteMessaging.swift +++ b/DuckDuckGo/RemoteMessaging.swift @@ -26,6 +26,7 @@ import Persistence import Bookmarks import RemoteMessaging import NetworkProtection +import Subscription struct RemoteMessaging { @@ -153,21 +154,12 @@ struct RemoteMessaging { case .success(let statusResponse): os_log("Successfully fetched remote messages", log: .remoteMessaging, type: .debug) - let isNetworkProtectionWaitlistUser: Bool - let daysSinceNetworkProtectionEnabled: Int + let isPrivacyProSubscriber = AppDependencyProvider.shared.subscriptionManager.accountManager.isUserAuthenticated + let canPurchase = AppDependencyProvider.shared.subscriptionManager.canPurchase -#if NETWORK_PROTECTION - let vpnAccess = NetworkProtectionAccessController() - let accessType = vpnAccess.networkProtectionAccessType() - let isVPNActivated = NetworkProtectionKeychainTokenStore().isFeatureActivated - let activationDateStore = DefaultVPNWaitlistActivationDateStore() - - isNetworkProtectionWaitlistUser = (accessType == .waitlistInvited) && isVPNActivated - daysSinceNetworkProtectionEnabled = activationDateStore.daysSinceActivation() ?? -1 -#else - isNetworkProtectionWaitlistUser = false - daysSinceNetworkProtectionEnabled = -1 -#endif + let activationDateStore = DefaultVPNActivationDateStore() + let daysSinceNetworkProtectionEnabled = activationDateStore.daysSinceActivation() ?? -1 + let surveyActionMapper = DefaultRemoteMessagingSurveyURLBuilder(statisticsStore: statisticsStore) let remoteMessagingConfigMatcher = RemoteMessagingConfigMatcher( appAttributeMatcher: AppAttributeMatcher(statisticsStore: statisticsStore, @@ -179,9 +171,11 @@ struct RemoteMessaging { favoritesCount: favoritesCount, appTheme: AppUserDefaults().currentThemeName.rawValue, isWidgetInstalled: isWidgetInstalled, - isNetPWaitlistUser: isNetworkProtectionWaitlistUser, - daysSinceNetPEnabled: daysSinceNetworkProtectionEnabled), + daysSinceNetPEnabled: daysSinceNetworkProtectionEnabled, + isPrivacyProEligibleUser: canPurchase, + isPrivacyProSubscriber: isPrivacyProSubscriber), percentileStore: RemoteMessagingPercentileUserDefaultsStore(userDefaults: .standard), + surveyActionMapper: surveyActionMapper, dismissedMessageIds: remoteMessagingStore.fetchDismissedRemoteMessageIds() ) diff --git a/DuckDuckGo/RemoteMessagingStore.swift b/DuckDuckGo/RemoteMessagingStore.swift index 645f85f65b..c67be8d6a3 100644 --- a/DuckDuckGo/RemoteMessagingStore.swift +++ b/DuckDuckGo/RemoteMessagingStore.swift @@ -55,12 +55,13 @@ final class RemoteMessagingStore: RemoteMessagingStoring { deleteScheduledRemoteMessages() save(remoteMessage: remoteMessage) - DispatchQueue.main.async { - self.notificationCenter.post(name: RemoteMessaging.Notifications.remoteMessagesDidChange, object: nil) - } } else { deleteScheduledRemoteMessages() } + + DispatchQueue.main.async { + self.notificationCenter.post(name: RemoteMessaging.Notifications.remoteMessagesDidChange, object: nil) + } } } diff --git a/DuckDuckGo/SurveyURLBuilder.swift b/DuckDuckGo/RemoteMessagingSurveyURLBuilder.swift similarity index 78% rename from DuckDuckGo/SurveyURLBuilder.swift rename to DuckDuckGo/RemoteMessagingSurveyURLBuilder.swift index 4d396c3a62..05b75d6118 100644 --- a/DuckDuckGo/SurveyURLBuilder.swift +++ b/DuckDuckGo/RemoteMessagingSurveyURLBuilder.swift @@ -1,5 +1,5 @@ // -// SurveyURLBuilder.swift +// RemoteMessagingSurveyURLBuilder.swift // DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. @@ -17,40 +17,25 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import Foundation import BrowserServicesKit +import RemoteMessaging import Core import Common -protocol SurveyURLBuilder { - func addSurveyParameters(to url: URL) -> URL -} - -struct DefaultSurveyURLBuilder: SurveyURLBuilder { - - enum SurveyURLParameters: String, CaseIterable { - case atb = "atb" - case atbVariant = "var" - case daysSinceActivated = "delta" - case iosVersion = "mv" - case appVersion = "ddgv" - case hardwareModel = "mo" - case lastActiveDate = "da" - } +struct DefaultRemoteMessagingSurveyURLBuilder: RemoteMessagingSurveyActionMapping { private let statisticsStore: StatisticsStore - private let activationDateStore: VPNWaitlistActivationDateStore + private let activationDateStore: VPNActivationDateStore init(statisticsStore: StatisticsStore = StatisticsUserDefaults(), - activationDateStore: VPNWaitlistActivationDateStore = DefaultVPNWaitlistActivationDateStore()) { + activationDateStore: VPNActivationDateStore = DefaultVPNActivationDateStore()) { self.statisticsStore = statisticsStore self.activationDateStore = activationDateStore } // swiftlint:disable:next cyclomatic_complexity - func addSurveyParameters(to surveyURL: URL) -> URL { + func add(parameters: [RemoteMessagingSurveyActionParameter], to surveyURL: URL) -> URL { guard var components = URLComponents(string: surveyURL.absoluteString) else { assertionFailure("Could not build URL components from survey URL") return surveyURL @@ -58,7 +43,7 @@ struct DefaultSurveyURLBuilder: SurveyURLBuilder { var queryItems = components.queryItems ?? [] - for parameter in SurveyURLParameters.allCases { + for parameter in parameters { switch parameter { case .atb: if let atb = statisticsStore.atb { @@ -68,11 +53,7 @@ struct DefaultSurveyURLBuilder: SurveyURLBuilder { if let variant = statisticsStore.variant { queryItems.append(URLQueryItem(name: parameter.rawValue, value: variant)) } - case .daysSinceActivated: - if let daysSinceActivated = activationDateStore.daysSinceActivation() { - queryItems.append(URLQueryItem(name: parameter.rawValue, value: String(describing: daysSinceActivated))) - } - case .iosVersion: + case .osVersion: queryItems.append(URLQueryItem(name: parameter.rawValue, value: AppVersion.shared.osVersion)) case .appVersion: queryItems.append(URLQueryItem(name: parameter.rawValue, value: AppVersion.shared.versionAndBuildNumber)) @@ -83,6 +64,11 @@ struct DefaultSurveyURLBuilder: SurveyURLBuilder { if let daysSinceLastActive = activationDateStore.daysSinceLastActive() { queryItems.append(URLQueryItem(name: parameter.rawValue, value: String(describing: daysSinceLastActive))) } + case .daysInstalled: + if let installDate = statisticsStore.installDate, + let daysSinceInstall = Calendar.current.numberOfDaysBetween(installDate, and: Date()) { + queryItems.append(URLQueryItem(name: parameter.rawValue, value: String(describing: daysSinceInstall))) + } } } @@ -92,7 +78,7 @@ struct DefaultSurveyURLBuilder: SurveyURLBuilder { } func addPasswordsCountSurveyParameter(to surveyURL: URL) -> URL { - let surveyURLWithParameters = addSurveyParameters(to: surveyURL) + let surveyURLWithParameters = add(parameters: RemoteMessagingSurveyActionParameter.allCases, to: surveyURL) guard var components = URLComponents(string: surveyURLWithParameters.absoluteString), let bucket = passwordsCountBucket() else { return surveyURLWithParameters @@ -129,5 +115,3 @@ struct DefaultSurveyURLBuilder: SurveyURLBuilder { } } - -#endif diff --git a/DuckDuckGo/RootDebugViewController.swift b/DuckDuckGo/RootDebugViewController.swift index d83856a29b..feb079c741 100644 --- a/DuckDuckGo/RootDebugViewController.swift +++ b/DuckDuckGo/RootDebugViewController.swift @@ -27,6 +27,7 @@ import Common import Configuration import Persistence import DDGSync +import NetworkProtection class RootDebugViewController: UITableViewController { @@ -34,6 +35,7 @@ class RootDebugViewController: UITableViewController { case resetAutoconsentPrompt = 665 case crashFatalError = 666 case crashMemory = 667 + case crashException = 673 case toggleInspectableWebViews = 668 case toggleInternalUserState = 669 case openVanillaBrowser = 670 @@ -136,6 +138,10 @@ class RootDebugViewController: UITableViewController { while 1 != 2 { arrays.append(UUID().uuidString) } + case .crashException: + tableView.beginUpdates() + tableView.deleteRows(at: [indexPath], with: .automatic) + tableView.endUpdates() case .toggleInspectableWebViews: let defaults = AppUserDefaults() defaults.inspectableWebViewEnabled.toggle() diff --git a/DuckDuckGo/RulesCompilationMonitor.swift b/DuckDuckGo/RulesCompilationMonitor.swift index bf85ceda92..1282bff99f 100644 --- a/DuckDuckGo/RulesCompilationMonitor.swift +++ b/DuckDuckGo/RulesCompilationMonitor.swift @@ -92,7 +92,7 @@ final class RulesCompilationMonitor { private func reportWaitTime(_ waitTime: TimeInterval, result: Pixel.Event.CompileRulesResult) { didReport = true Pixel.fire(pixel: .compilationResult(result: result, - waitTime: Pixel.Event.CompileRulesWaitTime(waitTime: waitTime), + waitTime: Pixel.Event.BucketAggregation(number: waitTime), appState: isOnboarding ? .onboarding : .regular), withAdditionalParameters: [Const.waitTime: String(waitTime)]) } diff --git a/DuckDuckGo/Settings.bundle/Root.plist b/DuckDuckGo/Settings.bundle/Root.plist index 3a0725b563..6410eafcc1 100644 --- a/DuckDuckGo/Settings.bundle/Root.plist +++ b/DuckDuckGo/Settings.bundle/Root.plist @@ -6,7 +6,7 @@ DefaultValue - 7.121.0 + 7.122.0 Key version Title diff --git a/DuckDuckGo/SettingsCell.swift b/DuckDuckGo/SettingsCell.swift index 31937a2315..5a90ca6618 100644 --- a/DuckDuckGo/SettingsCell.swift +++ b/DuckDuckGo/SettingsCell.swift @@ -107,10 +107,14 @@ struct SettingsCellView: View, Identifiable { cellContent .disabled(!enabled) } - .buttonStyle(PlainButtonStyle()) .contentShape(Rectangle()) } else { - cellContent + Button { + // No-op + } label: { + cellContent + } + .contentShape(Rectangle()) } }.frame(maxWidth: .infinity) @@ -281,7 +285,7 @@ struct SettingsCustomCell: View { .contentShape(Rectangle()) .frame(maxWidth: .infinity) .onTapGesture { - action() // We need this to make sute tap target is expanded to frame + action() // We need this to make sure tap target is expanded to frame } } .frame(maxWidth: .infinity) diff --git a/DuckDuckGo/SettingsCustomizeView.swift b/DuckDuckGo/SettingsCustomizeView.swift index 8560f047c3..fc4a72b3f7 100644 --- a/DuckDuckGo/SettingsCustomizeView.swift +++ b/DuckDuckGo/SettingsCustomizeView.swift @@ -37,7 +37,13 @@ struct SettingsCustomizeView: View { SettingsCellView(label: UserText.settingsAutocomplete, subtitle: viewModel.autocompleteSubtitle, accesory: .toggle(isOn: viewModel.autocompleteBinding)) - + + if viewModel.shouldShowRecentlyVisitedSites { + SettingsCellView(label: UserText.settingsAutocompleteRecentlyVisited, + accesory: .toggle(isOn: viewModel.autocompleteRecentlyVisitedSitesBinding)) + } + + if viewModel.state.speechRecognitionAvailable { SettingsCellView(label: UserText.settingsVoiceSearch, accesory: .toggle(isOn: viewModel.voiceSearchEnabledBinding)) diff --git a/DuckDuckGo/SettingsGeneralView.swift b/DuckDuckGo/SettingsGeneralView.swift index 1f5bc535b2..3cdb487b63 100644 --- a/DuckDuckGo/SettingsGeneralView.swift +++ b/DuckDuckGo/SettingsGeneralView.swift @@ -40,6 +40,12 @@ struct SettingsGeneralView: View { // Autocomplete Suggestions SettingsCellView(label: UserText.settingsAutocomplete, accesory: .toggle(isOn: viewModel.autocompleteGeneralBinding)) + + if viewModel.shouldShowRecentlyVisitedSites { + SettingsCellView(label: UserText.settingsAutocompleteRecentlyVisited, + accesory: .toggle(isOn: viewModel.autocompleteRecentlyVisitedSitesBinding)) + } + } Section(footer: Text(UserText.voiceSearchFooter)) { diff --git a/DuckDuckGo/SettingsHostingController.swift b/DuckDuckGo/SettingsHostingController.swift index 1e5547a0e1..c23ee713bd 100644 --- a/DuckDuckGo/SettingsHostingController.swift +++ b/DuckDuckGo/SettingsHostingController.swift @@ -47,10 +47,7 @@ class SettingsHostingController: UIHostingController { self?.navigationController?.dismiss(animated: true) } - let settingsView = PixelExperiment.cohort == .newSettings ? - AnyView(SettingsRootView(viewModel: viewModel)) : - AnyView(SettingsView(viewModel: viewModel)) - self.rootView = AnyView(settingsView) + self.rootView = AnyView(SettingsRootView(viewModel: viewModel)) decorateNavigationBar() } diff --git a/DuckDuckGo/SettingsLegacyViewProvider.swift b/DuckDuckGo/SettingsLegacyViewProvider.swift index 2e059776a4..1d61288603 100644 --- a/DuckDuckGo/SettingsLegacyViewProvider.swift +++ b/DuckDuckGo/SettingsLegacyViewProvider.swift @@ -84,9 +84,6 @@ class SettingsLegacyViewProvider: ObservableObject { var feedback: UIViewController { instantiate("Feedback", fromStoryboard: "Feedback") } var about: UIViewController { AboutViewControllerOld() } - @available(iOS 15, *) - var netPWaitlist: UIViewController { VPNWaitlistViewController(nibName: nil, bundle: nil) } - @available(iOS 15, *) var netP: UIViewController { NetworkProtectionRootViewController() } diff --git a/DuckDuckGo/SettingsRootView.swift b/DuckDuckGo/SettingsRootView.swift index f691b69421..f58aee312e 100644 --- a/DuckDuckGo/SettingsRootView.swift +++ b/DuckDuckGo/SettingsRootView.swift @@ -61,7 +61,6 @@ struct SettingsRootView: View { .accentColor(Color(designSystemColor: .textPrimary)) .environmentObject(viewModel) .conditionalInsetGroupedListStyle() - .onAppear { viewModel.onAppear() } @@ -115,9 +114,12 @@ struct SettingsRootView: View { case .itr: SubscriptionITPView() case let .subscriptionFlow(origin): - SubscriptionContainerViewFactory.makeSubscribeFlow(origin: origin, navigationCoordinator: subscriptionNavigationCoordinator) + SubscriptionContainerViewFactory.makeSubscribeFlow(origin: origin, + navigationCoordinator: subscriptionNavigationCoordinator, + subscriptionManager: AppDependencyProvider.shared.subscriptionManager) case .subscriptionRestoreFlow: - SubscriptionContainerViewFactory.makeRestoreFlow(navigationCoordinator: subscriptionNavigationCoordinator) + SubscriptionContainerViewFactory.makeRestoreFlow(navigationCoordinator: subscriptionNavigationCoordinator, + subscriptionManager: AppDependencyProvider.shared.subscriptionManager) default: EmptyView() } diff --git a/DuckDuckGo/SettingsState.swift b/DuckDuckGo/SettingsState.swift index 44a2eeae50..508926d79f 100644 --- a/DuckDuckGo/SettingsState.swift +++ b/DuckDuckGo/SettingsState.swift @@ -74,6 +74,7 @@ struct SettingsState { // Customization properties var autocomplete: Bool + var recentlyVisitedSites: Bool var longPressPreviews: Bool var allowUniversalLinks: Bool @@ -112,6 +113,7 @@ struct SettingsState { autoclearDataEnabled: false, applicationLock: false, autocomplete: true, + recentlyVisitedSites: true, longPressPreviews: true, allowUniversalLinks: true, activeWebsiteAccount: nil, diff --git a/DuckDuckGo/SettingsSubscriptionView.swift b/DuckDuckGo/SettingsSubscriptionView.swift index b5f6e50cb4..73cb67cabb 100644 --- a/DuckDuckGo/SettingsSubscriptionView.swift +++ b/DuckDuckGo/SettingsSubscriptionView.swift @@ -35,7 +35,7 @@ struct SettingsSubscriptionView: View { @State var isShowingStripeView = false @State var isShowingSubscriptionError = false @State var isShowingPrivacyPro = false - + enum Constants { static let purchaseDescriptionPadding = 5.0 static let topCellPadding = 3.0 @@ -43,6 +43,8 @@ struct SettingsSubscriptionView: View { static let navigationDelay = 0.3 static let infoIcon = "info-16" static let alertIcon = "Exclamation-Color-16" + + static let privacyPolicyURL = URL(string: "https://duckduckgo.com/pro/privacy-terms")! } private var subscriptionDescriptionView: some View { @@ -79,18 +81,23 @@ struct SettingsSubscriptionView: View { Text(UserText.settingsPProManageSubscription) .daxBodyRegular() } - + + private var subscriptionManager: SubscriptionManaging { + AppDependencyProvider.shared.subscriptionManager + } + @ViewBuilder private var purchaseSubscriptionView: some View { Group { SettingsCustomCell(content: { subscriptionDescriptionView }) - let subscribeView = SubscriptionContainerViewFactory.makeSubscribeFlow( - origin: nil, - navigationCoordinator: subscriptionNavigationCoordinator + let subscribeView = SubscriptionContainerViewFactory.makeSubscribeFlow(origin: nil, + navigationCoordinator: subscriptionNavigationCoordinator, + subscriptionManager: subscriptionManager ).navigationViewStyle(.stack) - let restoreView = SubscriptionContainerViewFactory.makeRestoreFlow(navigationCoordinator: subscriptionNavigationCoordinator) + let restoreView = SubscriptionContainerViewFactory.makeRestoreFlow(navigationCoordinator: subscriptionNavigationCoordinator, + subscriptionManager: subscriptionManager) .navigationViewStyle(.stack) .onFirstAppear { Pixel.fire(pixel: .privacyProRestorePurchaseClick) @@ -122,9 +129,9 @@ struct SettingsSubscriptionView: View { } }) - let subscribeView = SubscriptionContainerViewFactory.makeSubscribeFlow( - origin: nil, - navigationCoordinator: subscriptionNavigationCoordinator + let subscribeView = SubscriptionContainerViewFactory.makeSubscribeFlow(origin: nil, + navigationCoordinator: subscriptionNavigationCoordinator, + subscriptionManager: subscriptionManager ).navigationViewStyle(.stack) NavigationLink( destination: subscribeView, @@ -209,9 +216,12 @@ struct SettingsSubscriptionView: View { var body: some View { Group { if isShowingPrivacyPro { - - Section(header: Text(UserText.settingsPProSection)) { - + let footerLink = Link(UserText.settingsPProSectionFooter, + destination: Constants.privacyPolicyURL + ).daxFootnoteRegular().accentColor(Color.init(designSystemColor: .accent)) + + Section(header: Text(UserText.settingsPProSection), footer: footerLink) { + switch ( viewModel.state.subscription.isSignedIn, viewModel.state.subscription.hasActiveSubscription, @@ -248,7 +258,7 @@ struct SettingsSubscriptionView: View { isShowingSubscribeFlow = false } } - + } }.onReceive(viewModel.$state) { state in if state.subscription.enabled && state.subscription.canPurchase { diff --git a/DuckDuckGo/SettingsView.swift b/DuckDuckGo/SettingsView.swift index e768be3df4..d4b284fe71 100644 --- a/DuckDuckGo/SettingsView.swift +++ b/DuckDuckGo/SettingsView.swift @@ -124,9 +124,11 @@ struct SettingsView: View { case .itr: SubscriptionITPView() case let .subscriptionFlow(origin): - SubscriptionContainerViewFactory.makeSubscribeFlow(origin: origin, navigationCoordinator: subscriptionNavigationCoordinator) + SubscriptionContainerViewFactory.makeSubscribeFlow(origin: origin, navigationCoordinator: subscriptionNavigationCoordinator, + subscriptionManager: AppDependencyProvider.shared.subscriptionManager) case .subscriptionRestoreFlow: - SubscriptionContainerViewFactory.makeRestoreFlow(navigationCoordinator: subscriptionNavigationCoordinator) + SubscriptionContainerViewFactory.makeRestoreFlow(navigationCoordinator: subscriptionNavigationCoordinator, + subscriptionManager: AppDependencyProvider.shared.subscriptionManager) default: EmptyView() } diff --git a/DuckDuckGo/SettingsViewModel.swift b/DuckDuckGo/SettingsViewModel.swift index e6ba9f645b..cb3ba864f2 100644 --- a/DuckDuckGo/SettingsViewModel.swift +++ b/DuckDuckGo/SettingsViewModel.swift @@ -34,7 +34,6 @@ import NetworkProtection // swiftlint:disable type_body_length final class SettingsViewModel: ObservableObject { - // Dependencies private(set) lazy var appSettings = AppDependencyProvider.shared.appSettings private(set) var privacyStore = PrivacyUserDefaults() @@ -45,20 +44,18 @@ final class SettingsViewModel: ObservableObject { private let voiceSearchHelper: VoiceSearchHelperProtocol private let syncPausedStateManager: any SyncPausedStateManaging var emailManager: EmailManager { EmailManager() } + private let historyManager: HistoryManager // Subscription Dependencies - private var subscriptionAccountManager: AccountManager + private let subscriptionManager: SubscriptionManaging private var subscriptionSignOutObserver: Any? + private enum UserDefaultsCacheKey: String, UserDefaultsCacheKeyStore { + case subscriptionState = "com.duckduckgo.ios.subscription.state" + } // Used to cache the lasts subscription state for up to a week - private var subscriptionStateCache = UserDefaultsCache( - key: UserDefaultsCacheKey.subscriptionState, - settings: UserDefaultsCacheSettings(defaultExpirationInterval: .days(7))) - -#if NETWORK_PROTECTION - private let connectionObserver = ConnectionStatusObserverThroughSession() -#endif - + private let subscriptionStateCache = UserDefaultsCache(key: UserDefaultsCacheKey.subscriptionState, + settings: UserDefaultsCacheSettings(defaultExpirationInterval: .days(7))) // Properties private lazy var isPad = UIDevice.current.userInterfaceIdiom == .pad private var cancellables = Set() @@ -88,7 +85,9 @@ final class SettingsViewModel: ObservableObject { var shouldShowNoMicrophonePermissionAlert: Bool = false @Published var shouldShowEmailAlert: Bool = false var autocompleteSubtitle: String? - + + @Published var shouldShowRecentlyVisitedSites: Bool = true + // MARK: - Deep linking // Used to automatically navigate to a specific section // immediately after loading the Settings View @@ -178,6 +177,7 @@ final class SettingsViewModel: ObservableObject { set: { self.appSettings.autocomplete = $0 self.state.autocomplete = $0 + self.updateRecentlyVisitedSitesVisibility() if $0 { Pixel.fire(pixel: .settingsAutocompleteOn, withAdditionalParameters: PixelExperiment.parameters) @@ -196,6 +196,7 @@ final class SettingsViewModel: ObservableObject { set: { self.appSettings.autocomplete = $0 self.state.autocomplete = $0 + self.updateRecentlyVisitedSitesVisibility() if $0 { Pixel.fire(pixel: .settingsPrivateSearchAutocompleteOn, withAdditionalParameters: PixelExperiment.parameters) @@ -207,6 +208,16 @@ final class SettingsViewModel: ObservableObject { ) } + var autocompleteRecentlyVisitedSitesBinding: Binding { + Binding( + get: { self.state.recentlyVisitedSites }, + set: { + self.appSettings.recentlyVisitedSites = $0 + self.state.recentlyVisitedSites = $0 + } + ) + } + // Remove after Settings experiment var autocompleteGeneralBinding: Binding { Binding( @@ -214,6 +225,7 @@ final class SettingsViewModel: ObservableObject { set: { self.appSettings.autocomplete = $0 self.state.autocomplete = $0 + self.updateRecentlyVisitedSitesVisibility() if $0 { Pixel.fire(pixel: .settingsGeneralAutocompleteOn, withAdditionalParameters: PixelExperiment.parameters) @@ -390,22 +402,26 @@ final class SettingsViewModel: ObservableObject { // MARK: Default Init init(state: SettingsState? = nil, legacyViewProvider: SettingsLegacyViewProvider, - accountManager: AccountManager, + subscriptionManager: SubscriptionManaging, voiceSearchHelper: VoiceSearchHelperProtocol = AppDependencyProvider.shared.voiceSearchHelper, variantManager: VariantManager = AppDependencyProvider.shared.variantManager, deepLink: SettingsDeepLinkSection? = nil, + historyManager: HistoryManager, syncPausedStateManager: any SyncPausedStateManaging) { + self.state = SettingsState.defaults self.legacyViewProvider = legacyViewProvider - self.subscriptionAccountManager = accountManager + self.subscriptionManager = subscriptionManager self.voiceSearchHelper = voiceSearchHelper self.deepLinkTarget = deepLink + self.historyManager = historyManager self.syncPausedStateManager = syncPausedStateManager setupNotificationObservers() - autocompleteSubtitle = variantManager.isSupported(feature: .history) ? UserText.settingsAutocompleteSubtitle : nil + autocompleteSubtitle = UserText.settingsAutocompleteSubtitle + updateRecentlyVisitedSitesVisibility() } - + deinit { subscriptionSignOutObserver = nil } @@ -432,6 +448,7 @@ extension SettingsViewModel { autoclearDataEnabled: AutoClearSettingsModel(settings: appSettings) != nil, applicationLock: privacyStore.authenticationEnabled, autocomplete: appSettings.autocomplete, + recentlyVisitedSites: appSettings.recentlyVisitedSites, longPressPreviews: appSettings.longPressPreviews, allowUniversalLinks: appSettings.allowUniversalLinks, activeWebsiteAccount: nil, @@ -446,19 +463,19 @@ extension SettingsViewModel { sync: getSyncState() ) + updateRecentlyVisitedSitesVisibility() setupSubscribers() Task { await setupSubscriptionEnvironment() } - } - - private func getNetworkProtectionState() -> SettingsState.NetworkProtection { - var enabled = false -#if NETWORK_PROTECTION - if #available(iOS 15, *) { - enabled = DefaultNetworkProtectionVisibility().shouldKeepVPNAccessViaWaitlist() + + private func updateRecentlyVisitedSitesVisibility() { + withAnimation { + shouldShowRecentlyVisitedSites = historyManager.isHistoryFeatureEnabled() && state.autocomplete } -#endif - return SettingsState.NetworkProtection(enabled: enabled, status: "") + } + + private func getNetworkProtectionState() -> SettingsState.NetworkProtection { + return SettingsState.NetworkProtection(enabled: false, status: "") } private func getSyncState() -> SettingsState.SyncSettings { @@ -495,7 +512,7 @@ extension SettingsViewModel { #if NETWORK_PROTECTION private func updateNetPStatus(connectionStatus: ConnectionStatus) { - if DefaultNetworkProtectionVisibility().isPrivacyProLaunched() { + if AppDependencyProvider.shared.vpnFeatureVisibility.isPrivacyProLaunched() { switch connectionStatus { case .connected: self.state.networkProtection.status = UserText.netPCellConnected @@ -503,17 +520,7 @@ extension SettingsViewModel { self.state.networkProtection.status = UserText.netPCellDisconnected } } else { - switch NetworkProtectionAccessController().networkProtectionAccessType() { - case .none, .waitlistAvailable, .waitlistJoined, .waitlistInvitedPendingTermsAcceptance: - self.state.networkProtection.status = VPNWaitlist.shared.settingsSubtitle - case .waitlistInvited, .inviteCodeInvited: - switch connectionStatus { - case .connected: - self.state.networkProtection.status = UserText.netPCellConnected - default: - self.state.networkProtection.status = UserText.netPCellDisconnected - } - } + self.state.networkProtection.status = "" } } #endif @@ -524,10 +531,9 @@ extension SettingsViewModel { extension SettingsViewModel { private func setupSubscribers() { - #if NETWORK_PROTECTION - connectionObserver.publisher + AppDependencyProvider.shared.connectionObserver.publisher .receive(on: DispatchQueue.main) .sink { [weak self] hasActiveSubscription in self?.updateNetPStatus(connectionStatus: hasActiveSubscription) @@ -747,7 +753,6 @@ extension SettingsViewModel { @MainActor private func setupSubscriptionEnvironment() async { - // If there's cached data use it by default if let cachedSubscription = subscriptionStateCache.get() { state.subscription = cachedSubscription @@ -760,15 +765,15 @@ extension SettingsViewModel { state.subscription.enabled = AppDependencyProvider.shared.subscriptionFeatureAvailability.isFeatureAvailable // Update if can purchase based on App Store product availability - state.subscription.canPurchase = SubscriptionPurchaseEnvironment.canPurchase + state.subscription.canPurchase = subscriptionManager.canPurchase // Active subscription check - guard let token = subscriptionAccountManager.accessToken else { + guard let token = subscriptionManager.accountManager.accessToken else { subscriptionStateCache.set(state.subscription) // Sync cache return } - let subscriptionResult = await SubscriptionService.getSubscription(accessToken: token) + let subscriptionResult = await subscriptionManager.subscriptionService.getSubscription(accessToken: token) switch subscriptionResult { case .success(let subscription): @@ -782,7 +787,7 @@ extension SettingsViewModel { // Check entitlements and update state let entitlements: [Entitlement.ProductName] = [.networkProtection, .dataBrokerProtection, .identityTheftRestoration] for entitlement in entitlements { - if case .success = await AccountManager().hasEntitlement(for: entitlement) { + if case .success = await subscriptionManager.accountManager.hasEntitlement(for: entitlement) { switch entitlement { case .identityTheftRestoration: self.state.subscription.entitlements.append(.identityTheftRestoration) @@ -802,7 +807,6 @@ extension SettingsViewModel { case .failure: break - } // Sync Cache @@ -826,7 +830,8 @@ extension SettingsViewModel { @available(iOS 15.0, *) func restoreAccountPurchase() async { DispatchQueue.main.async { self.state.subscription.isRestoring = true } - let result = await AppStoreRestoreFlow.restoreAccountFromPastPurchase(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) + let appStoreRestoreFlow = AppStoreRestoreFlow(subscriptionManager: subscriptionManager) + let result = await appStoreRestoreFlow.restoreAccountFromPastPurchase() switch result { case .success: DispatchQueue.main.async { diff --git a/Core/AccountManager+AppGroup.swift b/DuckDuckGo/Subscription/Extensions/VPNSettings+Environment.swift similarity index 61% rename from Core/AccountManager+AppGroup.swift rename to DuckDuckGo/Subscription/Extensions/VPNSettings+Environment.swift index 5e6ae4f057..65329d0d12 100644 --- a/Core/AccountManager+AppGroup.swift +++ b/DuckDuckGo/Subscription/Extensions/VPNSettings+Environment.swift @@ -1,5 +1,5 @@ // -// AccountManager+AppGroup.swift +// VPNSettings+Environment.swift // DuckDuckGo // // Copyright © 2024 DuckDuckGo. All rights reserved. @@ -18,10 +18,18 @@ // import Foundation +import NetworkProtection import Subscription -public extension AccountManager { - convenience init() { - self.init(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) +public extension VPNSettings { + + /// Align VPN environment to the Subscription environment + func alignTo(subscriptionEnvironment: SubscriptionEnvironment) { + switch subscriptionEnvironment.serviceEnvironment { + case .production: + self.selectedEnvironment = .production + case .staging: + self.selectedEnvironment = .staging + } } } diff --git a/DuckDuckGo/Subscription/SubscriptionEnvironment+Default.swift b/DuckDuckGo/Subscription/SubscriptionEnvironment+Default.swift new file mode 100644 index 0000000000..821cd16378 --- /dev/null +++ b/DuckDuckGo/Subscription/SubscriptionEnvironment+Default.swift @@ -0,0 +1,43 @@ +// +// SubscriptionEnvironment+Default.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Subscription + +extension SubscriptionEnvironment { + + public static var `default`: SubscriptionEnvironment { +#if ALPHA || DEBUG + let environment: SubscriptionEnvironment.ServiceEnvironment = .staging +#else + let environment: SubscriptionEnvironment.ServiceEnvironment = .production +#endif + return SubscriptionEnvironment(serviceEnvironment: environment, purchasePlatform: .appStore) + } +} + +extension SubscriptionManager { + + static public func getSavedOrDefaultEnvironment(userDefaults: UserDefaults) -> SubscriptionEnvironment { + if let savedEnvironment = loadEnvironmentFrom(userDefaults: userDefaults) { + return savedEnvironment + } + return SubscriptionEnvironment.default + } +} diff --git a/DuckDuckGo/Subscription/SubscriptionManageriOS14.swift b/DuckDuckGo/Subscription/SubscriptionManageriOS14.swift new file mode 100644 index 0000000000..35d962f035 --- /dev/null +++ b/DuckDuckGo/Subscription/SubscriptionManageriOS14.swift @@ -0,0 +1,45 @@ +// +// SubscriptionManageriOS14.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Subscription + +class SubscriptionManageriOS14: SubscriptionManaging { + + var accountManager: AccountManaging + var subscriptionService: SubscriptionService = SubscriptionService(currentServiceEnvironment: .production) + var authService: AuthService = AuthService(currentServiceEnvironment: .production) + + @available(iOS 15, *) + func storePurchaseManager() -> StorePurchaseManaging { + StorePurchaseManager() + } + var currentEnvironment: SubscriptionEnvironment = SubscriptionEnvironment.default + var canPurchase: Bool = false + func loadInitialData() {} + func updateSubscriptionStatus(completion: @escaping (Bool) -> Void) {} + + func url(for type: SubscriptionURL) -> URL { + URL(string: "https://duckduckgo.com")! + } + + init(accountManager: AccountManaging) { + self.accountManager = accountManager + } +} diff --git a/DuckDuckGo/Subscription/UserScripts/IdentityTheftRestorationPagesFeature.swift b/DuckDuckGo/Subscription/UserScripts/IdentityTheftRestorationPagesFeature.swift index 1f1e1b7a37..5ee23bddb5 100644 --- a/DuckDuckGo/Subscription/UserScripts/IdentityTheftRestorationPagesFeature.swift +++ b/DuckDuckGo/Subscription/UserScripts/IdentityTheftRestorationPagesFeature.swift @@ -43,7 +43,12 @@ final class IdentityTheftRestorationPagesFeature: Subfeature, ObservableObject { static let getAccessToken = "getAccessToken" } - + private let accountManager: AccountManaging + + init(accountManager: AccountManaging) { + self.accountManager = accountManager + } + weak var broker: UserScriptMessageBroker? var featureName: String = Constants.featureName @@ -67,7 +72,7 @@ final class IdentityTheftRestorationPagesFeature: Subfeature, ObservableObject { } func getAccessToken(params: Any, original: WKScriptMessage) async throws -> Encodable? { - if let accessToken = AccountManager().accessToken { + if let accessToken = accountManager.accessToken { return [Constants.token: accessToken] } else { return [String: String]() diff --git a/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift b/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift index 7e953c4490..fa9c017526 100644 --- a/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift +++ b/DuckDuckGo/Subscription/UserScripts/SubscriptionPagesUseSubscriptionFeature.swift @@ -90,7 +90,20 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec accountCreationFailed, generalError } - + + private let subscriptionAttributionOrigin: String? + private let subscriptionManager: SubscriptionManaging + private var accountManager: AccountManaging { + subscriptionManager.accountManager + } + private let appStorePurchaseFlow: AppStorePurchaseFlow + + init(subscriptionManager: SubscriptionManaging, subscriptionAttributionOrigin: String?) { + self.subscriptionManager = subscriptionManager + self.appStorePurchaseFlow = AppStorePurchaseFlow(subscriptionManager: subscriptionManager) + self.subscriptionAttributionOrigin = subscriptionAttributionOrigin + } + // Transaction Status and errors are observed from ViewModels to handle errors in the UI @Published private(set) var transactionStatus: SubscriptionTransactionStatus = .idle @Published private(set) var transactionError: UseSubscriptionError? @@ -116,11 +129,6 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec var originalMessage: WKScriptMessage? - private let subscriptionAttributionOrigin: String? - init(subscriptionAttributionOrigin: String?) { - self.subscriptionAttributionOrigin = subscriptionAttributionOrigin - } - func with(broker: UserScriptMessageBroker) { self.broker = broker } @@ -176,21 +184,19 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec // MARK: Broker Methods (Called from WebView via UserScripts) func getSubscription(params: Any, original: WKScriptMessage) async -> Encodable? { - let authToken = AccountManager().authToken ?? Constants.empty + let authToken = accountManager.authToken ?? Constants.empty return [Constants.token: authToken] } func getSubscriptionOptions(params: Any, original: WKScriptMessage) async -> Encodable? { resetSubscriptionFlow() - - switch await AppStorePurchaseFlow.subscriptionOptions() { - case .success(let subscriptionOptions): + if let subscriptionOptions = await subscriptionManager.storePurchaseManager().subscriptionOptions() { if AppDependencyProvider.shared.subscriptionFeatureAvailability.isSubscriptionPurchaseAllowed { return subscriptionOptions } else { return SubscriptionOptions.empty } - case .failure: + } else { os_log("Failed to obtain subscription options", log: .subscription, type: .error) setTransactionError(.failedToGetSubscriptionOptions) return nil @@ -217,7 +223,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec } // Check for active subscriptions - if await PurchaseManager.hasActiveSubscription() { + if await subscriptionManager.storePurchaseManager().hasActiveSubscription() { setTransactionError(.hasActiveSubscription) Pixel.fire(pixel: .privacyProRestoreAfterPurchaseAttempt) setTransactionStatus(.idle) @@ -227,9 +233,8 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec let emailAccessToken = try? EmailManager().getToken() let purchaseTransactionJWS: String - switch await AppStorePurchaseFlow.purchaseSubscription(with: subscriptionSelection.id, - emailAccessToken: emailAccessToken, - subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) { + switch await appStorePurchaseFlow.purchaseSubscription(with: subscriptionSelection.id, + emailAccessToken: emailAccessToken) { case .success(let transactionJWS): purchaseTransactionJWS = transactionJWS @@ -252,8 +257,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec } setTransactionStatus(.polling) - switch await AppStorePurchaseFlow.completeSubscriptionPurchase(with: purchaseTransactionJWS, - subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) { + switch await appStorePurchaseFlow.completeSubscriptionPurchase(with: purchaseTransactionJWS) { case .success(let purchaseUpdate): DailyPixel.fireDailyAndCount(pixel: .privacyProPurchaseSuccess) UniquePixel.fire(pixel: .privacyProSubscriptionActivated) @@ -277,10 +281,9 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec } // Clear subscription Cache - SubscriptionService.signOut() - + subscriptionManager.subscriptionService.signOut() + let authToken = subscriptionValues.token - let accountManager = AccountManager() if case let .success(accessToken) = await accountManager.exchangeAuthTokenToAccessToken(authToken), case let .success(accountDetails) = await accountManager.fetchAccountDetails(with: accessToken) { accountManager.storeAuthToken(token: authToken) @@ -319,10 +322,9 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec } func backToSettings(params: Any, original: WKScriptMessage) async -> Encodable? { - let accountManager = AccountManager() if let accessToken = accountManager.accessToken, case let .success(accountDetails) = await accountManager.fetchAccountDetails(with: accessToken) { - switch await SubscriptionService.getSubscription(accessToken: accessToken) { + switch await subscriptionManager.subscriptionService.getSubscription(accessToken: accessToken) { case .success: accountManager.storeAccount(token: accessToken, @@ -341,7 +343,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec } func getAccessToken(params: Any, original: WKScriptMessage) async throws -> Encodable? { - if let accessToken = AccountManager().accessToken { + if let accessToken = subscriptionManager.accountManager.accessToken { return [Constants.token: accessToken] } else { return [String: String]() @@ -395,7 +397,8 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec func restoreAccountFromAppStorePurchase() async throws { setTransactionStatus(.restoring) - let result = await AppStoreRestoreFlow.restoreAccountFromPastPurchase(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) + let appStoreRestoreFlow = AppStoreRestoreFlow(subscriptionManager: subscriptionManager) + let result = await appStoreRestoreFlow.restoreAccountFromPastPurchase() switch result { case .success: setTransactionStatus(.idle) diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionContainerViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionContainerViewModel.swift index 49f127bba3..9eaaa63130 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionContainerViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionContainerViewModel.swift @@ -23,28 +23,31 @@ import Combine @available(iOS 15.0, *) final class SubscriptionContainerViewModel: ObservableObject { - + let userScript: SubscriptionPagesUserScript let subFeature: SubscriptionPagesUseSubscriptionFeature - + let flow: SubscriptionFlowViewModel let restore: SubscriptionRestoreViewModel let email: SubscriptionEmailViewModel - - - init( - origin: String?, - userScript: SubscriptionPagesUserScript, - subFeature: SubscriptionPagesUseSubscriptionFeature - ) { + + init(subscriptionManager: SubscriptionManaging, + origin: String?, + userScript: SubscriptionPagesUserScript, + subFeature: SubscriptionPagesUseSubscriptionFeature) { self.userScript = userScript - self.subFeature = subFeature - self.flow = SubscriptionFlowViewModel(origin: origin, userScript: userScript, subFeature: subFeature) - self.restore = SubscriptionRestoreViewModel(userScript: userScript, subFeature: subFeature) - self.email = SubscriptionEmailViewModel(userScript: userScript, subFeature: subFeature) - } - - deinit { subFeature.cleanup() + self.subFeature = subFeature + self.flow = SubscriptionFlowViewModel(origin: origin, + userScript: userScript, + subFeature: subFeature, + subscriptionManager: subscriptionManager) + self.restore = SubscriptionRestoreViewModel(userScript: userScript, + subFeature: subFeature, + subscriptionManager: subscriptionManager) + self.email = SubscriptionEmailViewModel(userScript: userScript, + subFeature: subFeature, + subscriptionManager: subscriptionManager) } + } diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift index bb7c5a2c64..3c209c3f1c 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift @@ -26,16 +26,17 @@ import Subscription @available(iOS 15.0, *) final class SubscriptionEmailViewModel: ObservableObject { - let accountManager: AccountManager + private let subscriptionManager: SubscriptionManaging let userScript: SubscriptionPagesUserScript let subFeature: SubscriptionPagesUseSubscriptionFeature private var canGoBackCancellable: AnyCancellable? private var urlCancellable: AnyCancellable? - var emailURL = URL.activateSubscriptionViaEmail + private var emailURL: URL var webViewModel: AsyncHeadlessWebViewViewModel - + + enum SelectedFeature { case netP, dbp, itr, none } @@ -69,23 +70,27 @@ final class SubscriptionEmailViewModel: ObservableObject { } private var cancellables = Set() - + var accountManager: AccountManaging { subscriptionManager.accountManager } + private var isWelcomePageOrSuccessPage: Bool { - webViewModel.url?.forComparison() == URL.subscriptionActivateSuccess.forComparison() || - webViewModel.url?.forComparison() == URL.subscriptionPurchase.forComparison() + let subscriptionActivateSuccessURL = subscriptionManager.url(for: .activateSuccess) + let subscriptionPurchaseURL = subscriptionManager.url(for: .purchase) + return webViewModel.url?.forComparison() == subscriptionActivateSuccessURL.forComparison() || + webViewModel.url?.forComparison() == subscriptionPurchaseURL.forComparison() } init(userScript: SubscriptionPagesUserScript, subFeature: SubscriptionPagesUseSubscriptionFeature, - accountManager: AccountManager = AccountManager()) { + subscriptionManager: SubscriptionManaging) { self.userScript = userScript self.subFeature = subFeature - self.accountManager = accountManager + self.subscriptionManager = subscriptionManager self.webViewModel = AsyncHeadlessWebViewViewModel(userScript: userScript, subFeature: subFeature, settings: AsyncHeadlessWebViewSettings(bounces: false, allowedDomains: Self.allowedDomains, contentBlocking: false)) + self.emailURL = subscriptionManager.url(for: .activateViaEmail) } @MainActor @@ -95,7 +100,8 @@ final class SubscriptionEmailViewModel: ObservableObject { } else { // If not in the Welcome page, dismiss the view, otherwise, assume we // came from Activation, so dismiss the entire stack - if webViewModel.url?.forComparison() != URL.subscriptionPurchase.forComparison() { + let subscriptionPurchaseURL = subscriptionManager.url(for: .purchase) + if webViewModel.url?.forComparison() != subscriptionPurchaseURL.forComparison() { state.shouldDismissView = true } else { state.shouldPopToAppSettings = true @@ -123,7 +129,9 @@ final class SubscriptionEmailViewModel: ObservableObject { // If the user is Authenticated & not in the Welcome page if accountManager.isUserAuthenticated && !isWelcomePageOrSuccessPage { // If user is authenticated, we want to "Add or manage email" instead of activating - emailURL = accountManager.email == nil ? URL.addEmailToSubscription : URL.manageSubscriptionEmail + let addEmailToSubscriptionURL = subscriptionManager.url(for: .addEmail) + let manageSubscriptionEmailURL = subscriptionManager.url(for: .manageEmail) + emailURL = accountManager.email == nil ? addEmailToSubscriptionURL : manageSubscriptionEmailURL state.viewTitle = accountManager.email == nil ? UserText.subscriptionRestoreAddEmailTitle : UserText.subscriptionManageEmailTitle // Also we assume subscription requires managing, and not activation diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift index 9489b4d3df..1e4bc0f48d 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift @@ -24,13 +24,13 @@ import Core import Subscription @available(iOS 15.0, *) -// swiftlint:disable type_body_length +// swiftlint:disable:next type_body_length final class SubscriptionFlowViewModel: ObservableObject { let userScript: SubscriptionPagesUserScript let subFeature: SubscriptionPagesUseSubscriptionFeature - let purchaseManager: PurchaseManager var webViewModel: AsyncHeadlessWebViewViewModel + let subscriptionManager: SubscriptionManaging let purchaseURL: URL private var cancellables = Set() @@ -71,16 +71,17 @@ final class SubscriptionFlowViewModel: ObservableObject { init(origin: String?, userScript: SubscriptionPagesUserScript, subFeature: SubscriptionPagesUseSubscriptionFeature, - purchaseManager: PurchaseManager = PurchaseManager.shared, + subscriptionManager: SubscriptionManaging, selectedFeature: SettingsViewModel.SettingsDeepLinkSection? = nil) { + let url = subscriptionManager.url(for: .purchase) if let origin { - purchaseURL = URL.subscriptionPurchase.appendingParameter(name: AttributionParameter.origin, value: origin) + purchaseURL = url.appendingParameter(name: AttributionParameter.origin, value: origin) } else { - purchaseURL = URL.subscriptionPurchase + purchaseURL = url } self.userScript = userScript self.subFeature = subFeature - self.purchaseManager = purchaseManager + self.subscriptionManager = subscriptionManager self.webViewModel = AsyncHeadlessWebViewViewModel(userScript: userScript, subFeature: subFeature, settings: webViewSettings) @@ -101,7 +102,9 @@ final class SubscriptionFlowViewModel: ObservableObject { subFeature.onBackToSettings = { - self.state.shouldGoBackToSettings = true + DispatchQueue.main.async { + self.state.shouldGoBackToSettings = true + } } subFeature.onActivateSubscription = { @@ -238,9 +241,11 @@ final class SubscriptionFlowViewModel: ObservableObject { strongSelf.state.canNavigateBack = false guard let currentURL = self?.webViewModel.url else { return } Task { await strongSelf.setTransactionStatus(.idle) } - if currentURL.forComparison() == URL.addEmailToSubscription.forComparison() || - currentURL.forComparison() == URL.addEmailToSubscriptionSuccess.forComparison() || - currentURL.forComparison() == URL.addEmailToSubscriptionSuccess.forComparison() { + + let addEmailURL = strongSelf.subscriptionManager.url(for: .addEmail) + let addEmailSuccessURL = strongSelf.subscriptionManager.url(for: .addEmailToSubscriptionSuccess) + if currentURL.forComparison() == addEmailURL.forComparison() || + currentURL.forComparison() == addEmailSuccessURL.forComparison() { strongSelf.state.viewTitle = UserText.subscriptionRestoreAddEmailTitle } else { strongSelf.state.viewTitle = UserText.subscriptionTitle @@ -248,11 +253,11 @@ final class SubscriptionFlowViewModel: ObservableObject { } } - + private func backButtonForURL(currentURL: URL) -> Bool { - return currentURL.forComparison() != URL.subscriptionBaseURL.forComparison() && - currentURL.forComparison() != URL.subscriptionActivateSuccess.forComparison() && - currentURL.forComparison() != URL.subscriptionPurchase.forComparison() + return currentURL.forComparison() != subscriptionManager.url(for: .baseURL).forComparison() && + currentURL.forComparison() != subscriptionManager.url(for: .activateSuccess).forComparison() && + currentURL.forComparison() != subscriptionManager.url(for: .purchase).forComparison() } private func cleanUp() { @@ -308,8 +313,8 @@ final class SubscriptionFlowViewModel: ObservableObject { DispatchQueue.main.async { self.resetState() } - if webViewModel.url != URL.subscriptionPurchase.forComparison() { - self.webViewModel.navigationCoordinator.navigateTo(url: self.purchaseURL) + if webViewModel.url != subscriptionManager.url(for: .purchase).forComparison() { + self.webViewModel.navigationCoordinator.navigateTo(url: purchaseURL) } await self.setupTransactionObserver() await self.setupWebViewObservers() @@ -344,4 +349,3 @@ final class SubscriptionFlowViewModel: ObservableObject { } } -// swiftlint:enable type_body_length diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionITPViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionITPViewModel.swift index a9ac238d38..fc9e3ef33e 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionITPViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionITPViewModel.swift @@ -28,7 +28,7 @@ final class SubscriptionITPViewModel: ObservableObject { var userScript: IdentityTheftRestorationPagesUserScript? var subFeature: IdentityTheftRestorationPagesFeature? - var manageITPURL = URL.identityTheftRestoration + let manageITPURL: URL var viewTitle = UserText.settingsPProITRTitle enum Constants { @@ -38,7 +38,7 @@ final class SubscriptionITPViewModel: ObservableObject { } // State variables - var itpURL = URL.identityTheftRestoration + let itpURL: URL @Published var canNavigateBack: Bool = false @Published var isDownloadableContent: Bool = false @Published var activityItems: [Any] = [] @@ -60,12 +60,13 @@ final class SubscriptionITPViewModel: ObservableObject { private var cancellables = Set() private var canGoBackCancellable: AnyCancellable? - - init(userScript: IdentityTheftRestorationPagesUserScript = IdentityTheftRestorationPagesUserScript(), - subFeature: IdentityTheftRestorationPagesFeature = IdentityTheftRestorationPagesFeature()) { - self.userScript = userScript - self.subFeature = subFeature - + + init(subscriptionManager: SubscriptionManaging) { + self.itpURL = subscriptionManager.url(for: .identityTheftRestoration) + self.manageITPURL = self.itpURL + self.userScript = IdentityTheftRestorationPagesUserScript() + self.subFeature = IdentityTheftRestorationPagesFeature(accountManager: subscriptionManager.accountManager) + let webViewSettings = AsyncHeadlessWebViewSettings(bounces: false, allowedDomains: Self.allowedDomains, contentBlocking: false) diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift index 3bde9d845b..bc84853d1e 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionRestoreViewModel.swift @@ -28,8 +28,11 @@ final class SubscriptionRestoreViewModel: ObservableObject { let userScript: SubscriptionPagesUserScript let subFeature: SubscriptionPagesUseSubscriptionFeature - let purchaseManager: PurchaseManager - let accountManager: AccountManager + let subscriptionManager: SubscriptionManaging + var accountManager: AccountManaging { + subscriptionManager.accountManager + } + let appStoreAccountManagementFlow: AppStoreAccountManagementFlow private var cancellables = Set() @@ -58,13 +61,12 @@ final class SubscriptionRestoreViewModel: ObservableObject { init(userScript: SubscriptionPagesUserScript, subFeature: SubscriptionPagesUseSubscriptionFeature, - purchaseManager: PurchaseManager = PurchaseManager.shared, - accountManager: AccountManager = AccountManager(), + subscriptionManager: SubscriptionManaging, isAddingDevice: Bool = false) { self.userScript = userScript self.subFeature = subFeature - self.purchaseManager = purchaseManager - self.accountManager = accountManager + self.subscriptionManager = subscriptionManager + self.appStoreAccountManagementFlow = AppStoreAccountManagementFlow(subscriptionManager: subscriptionManager) self.state.isAddingDevice = false } @@ -84,10 +86,10 @@ final class SubscriptionRestoreViewModel: ObservableObject { private func cleanUp() { cancellables.removeAll() } - + private func refreshToken() async { if state.isAddingDevice { - await AppStoreAccountManagementFlow.refreshAuthTokenIfNeeded(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) + await appStoreAccountManagementFlow.refreshAuthTokenIfNeeded() } } diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionSettingsViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionSettingsViewModel.swift index 6a654edb61..d5fbc1a00f 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionSettingsViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionSettingsViewModel.swift @@ -26,7 +26,7 @@ import Core @available(iOS 15.0, *) final class SubscriptionSettingsViewModel: ObservableObject { - let accountManager: AccountManager + private let subscriptionManager: SubscriptionManaging private var subscriptionUpdateTimer: Timer? private var signOutObserver: Any? @@ -39,7 +39,7 @@ final class SubscriptionSettingsViewModel: ObservableObject { var shouldDismissView: Bool = false var isShowingGoogleView: Bool = false var isShowingFAQView: Bool = false - var subscriptionInfo: SubscriptionService.GetSubscriptionResponse? + var subscriptionInfo: Subscription? var isLoadingSubscriptionInfo: Bool = false // Used to display stripe WebUI @@ -50,18 +50,25 @@ final class SubscriptionSettingsViewModel: ObservableObject { var isShowingConnectionError: Bool = false // Used to display the FAQ WebUI - var FAQViewModel: SubscriptionExternalLinkViewModel = SubscriptionExternalLinkViewModel(url: URL.subscriptionFAQ) + var faqViewModel: SubscriptionExternalLinkViewModel + + init(faqURL: URL) { + self.faqViewModel = SubscriptionExternalLinkViewModel(url: faqURL) + } } // Publish the currently selected feature @Published var selectedFeature: SettingsViewModel.SettingsDeepLinkSection? // Read only View State - Should only be modified from the VM - @Published private(set) var state = State() - + @Published private(set) var state: State + - init(accountManager: AccountManager = AccountManager()) { - self.accountManager = accountManager + init(subscriptionManager: SubscriptionManaging = AppDependencyProvider.shared.subscriptionManager) { + self.subscriptionManager = subscriptionManager + let subscriptionFAQURL = subscriptionManager.url(for: .faq) + self.state = State(faqURL: subscriptionFAQURL) + setupSubscriptionUpdater() setupNotificationObservers() } @@ -75,12 +82,13 @@ final class SubscriptionSettingsViewModel: ObservableObject { func onFirstAppear() { self.fetchAndUpdateSubscriptionDetails(cachePolicy: .returnCacheDataElseLoad) } - - private func fetchAndUpdateSubscriptionDetails(cachePolicy: SubscriptionService.CachePolicy = .returnCacheDataElseLoad, loadingIndicator: Bool = true) { + + private func fetchAndUpdateSubscriptionDetails(cachePolicy: SubscriptionService.CachePolicy = .returnCacheDataElseLoad, + loadingIndicator: Bool = true) { Task { if loadingIndicator { displayLoader(true) } - guard let token = self.accountManager.accessToken else { return } - let subscriptionResult = await SubscriptionService.getSubscription(accessToken: token, cachePolicy: cachePolicy) + guard let token = self.subscriptionManager.accountManager.accessToken else { return } + let subscriptionResult = await self.subscriptionManager.subscriptionService.getSubscription(accessToken: token, cachePolicy: cachePolicy) switch subscriptionResult { case .success(let subscription): DispatchQueue.main.async { @@ -154,7 +162,7 @@ final class SubscriptionSettingsViewModel: ObservableObject { } func removeSubscription() { - AccountManager().signOut() + subscriptionManager.accountManager.signOut() _ = ActionMessageView() ActionMessageView.present(message: UserText.subscriptionRemovalConfirmation, presentationLocation: .withoutBottomBar) @@ -196,7 +204,7 @@ final class SubscriptionSettingsViewModel: ObservableObject { @MainActor private func manageAppleSubscription() async { if state.subscriptionInfo?.isActive ?? false { - let url = URL.manageSubscriptionsInAppStoreAppURL + let url = subscriptionManager.url(for: .manageSubscriptionsInAppStore) if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene { do { try await AppStore.showManageSubscriptions(in: windowScene) @@ -210,9 +218,10 @@ final class SubscriptionSettingsViewModel: ObservableObject { } private func manageStripeSubscription() async { - guard let token = accountManager.accessToken, let externalID = accountManager.externalID else { return } - let serviceResponse = await SubscriptionService.getCustomerPortalURL(accessToken: token, externalID: externalID) - + guard let token = subscriptionManager.accountManager.accessToken, + let externalID = subscriptionManager.accountManager.externalID else { return } + let serviceResponse = await subscriptionManager.subscriptionService.getCustomerPortalURL(accessToken: token, externalID: externalID) + // Get Stripe Customer Portal URL and update the model if case .success(let response) = serviceResponse { guard let url = URL(string: response.customerPortalUrl) else { return } diff --git a/DuckDuckGo/Subscription/Views/SubscriptionContainerView.swift b/DuckDuckGo/Subscription/Views/SubscriptionContainerView.swift index df605dbcf9..5f56f8d7d6 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionContainerView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionContainerView.swift @@ -39,8 +39,6 @@ struct SubscriptionContainerView: View { viewModel: SubscriptionContainerViewModel) { _currentViewState = State(initialValue: currentView) self.viewModel = viewModel - let userScript = viewModel.userScript - let subFeature = viewModel.subFeature flowViewModel = viewModel.flow restoreViewModel = viewModel.restore emailViewModel = viewModel.email diff --git a/DuckDuckGo/Subscription/Views/SubscriptionContainerViewFactory.swift b/DuckDuckGo/Subscription/Views/SubscriptionContainerViewFactory.swift index 1d456c4ce0..5e6771a461 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionContainerViewFactory.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionContainerViewFactory.swift @@ -18,25 +18,30 @@ // import SwiftUI +import Subscription @available(iOS 15.0, *) enum SubscriptionContainerViewFactory { - static func makeSubscribeFlow(origin: String?, navigationCoordinator: SubscriptionNavigationCoordinator) -> some View { + static func makeSubscribeFlow(origin: String?, navigationCoordinator: SubscriptionNavigationCoordinator, subscriptionManager: SubscriptionManaging) -> some View { let viewModel = SubscriptionContainerViewModel( + subscriptionManager: subscriptionManager, origin: origin, userScript: SubscriptionPagesUserScript(), - subFeature: SubscriptionPagesUseSubscriptionFeature(subscriptionAttributionOrigin: origin) + subFeature: SubscriptionPagesUseSubscriptionFeature(subscriptionManager: subscriptionManager, + subscriptionAttributionOrigin: origin) ) return SubscriptionContainerView(currentView: .subscribe, viewModel: viewModel) .environmentObject(navigationCoordinator) } - static func makeRestoreFlow(navigationCoordinator: SubscriptionNavigationCoordinator) -> some View { + static func makeRestoreFlow(navigationCoordinator: SubscriptionNavigationCoordinator, subscriptionManager: SubscriptionManaging) -> some View { let viewModel = SubscriptionContainerViewModel( + subscriptionManager: subscriptionManager, origin: nil, userScript: SubscriptionPagesUserScript(), - subFeature: SubscriptionPagesUseSubscriptionFeature(subscriptionAttributionOrigin: nil) + subFeature: SubscriptionPagesUseSubscriptionFeature(subscriptionManager: subscriptionManager, + subscriptionAttributionOrigin: nil) ) return SubscriptionContainerView(currentView: .restore, viewModel: viewModel) .environmentObject(navigationCoordinator) diff --git a/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift b/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift index 84cda5b8d8..93f3cefb3a 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionEmailView.swift @@ -43,7 +43,7 @@ struct SubscriptionEmailView: View { var body: some View { // Hidden Navigation Links for Onboarding sections - NavigationLink(destination: NetworkProtectionRootView(inviteCompletion: {}).navigationViewStyle(.stack), + NavigationLink(destination: NetworkProtectionRootView().navigationViewStyle(.stack), isActive: $isShowingNetP, label: { EmptyView() }) NavigationLink(destination: SubscriptionITPView().navigationViewStyle(.stack), diff --git a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift index b9cac136dc..3134549d1a 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionFlowView.swift @@ -55,7 +55,7 @@ struct SubscriptionFlowView: View { var body: some View { // Hidden Navigation Links for Onboarding sections - NavigationLink(destination: NetworkProtectionRootView(inviteCompletion: {}).navigationViewStyle(.stack), + NavigationLink(destination: NetworkProtectionRootView().navigationViewStyle(.stack), isActive: $isShowingNetP, label: { EmptyView() }) NavigationLink(destination: SubscriptionITPView().navigationViewStyle(.stack), diff --git a/DuckDuckGo/Subscription/Views/SubscriptionITPView.swift b/DuckDuckGo/Subscription/Views/SubscriptionITPView.swift index 649284eda8..76ed85d96d 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionITPView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionITPView.swift @@ -36,7 +36,7 @@ struct SubscriptionActivityViewController: UIViewControllerRepresentable { struct SubscriptionITPView: View { @Environment(\.dismiss) var dismiss - @StateObject var viewModel = SubscriptionITPViewModel() + @StateObject var viewModel = SubscriptionITPViewModel(subscriptionManager: AppDependencyProvider.shared.subscriptionManager) @State private var shouldShowNavigationBar = false @State private var isShowingActivityView = false diff --git a/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift b/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift index ef9ca020dd..679bae8284 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift @@ -121,7 +121,9 @@ struct SubscriptionSettingsView: View { private var devicesSection: some View { Section(header: Text(UserText.subscriptionManageDevices)) { - NavigationLink(destination: SubscriptionContainerViewFactory.makeRestoreFlow(navigationCoordinator: subscriptionNavigationCoordinator), + NavigationLink(destination: SubscriptionContainerViewFactory.makeRestoreFlow( + navigationCoordinator: subscriptionNavigationCoordinator, + subscriptionManager: AppDependencyProvider.shared.subscriptionManager), isActive: $isShowingRestoreView) { SettingsCustomCell(content: { Text(UserText.subscriptionAddDeviceButton) @@ -250,7 +252,7 @@ struct SubscriptionSettingsView: View { } .sheet(isPresented: $isShowingFAQView, content: { - SubscriptionExternalLinkView(viewModel: viewModel.state.FAQViewModel, title: UserText.subscriptionFAQ) + SubscriptionExternalLinkView(viewModel: viewModel.state.faqViewModel, title: UserText.subscriptionFAQ) }) .onFirstAppear { diff --git a/DuckDuckGo/SubscriptionDebugViewController.swift b/DuckDuckGo/SubscriptionDebugViewController.swift index 718d7a0b97..72c5eafd6f 100644 --- a/DuckDuckGo/SubscriptionDebugViewController.swift +++ b/DuckDuckGo/SubscriptionDebugViewController.swift @@ -26,15 +26,14 @@ import Core import NetworkProtection #endif -@available(iOS 15.0, *) -final class SubscriptionDebugViewController: UITableViewController { - - private let accountManager = AccountManager() - fileprivate var purchaseManager: PurchaseManager = PurchaseManager.shared - - @UserDefaultsWrapper(key: .privacyProEnvironment, defaultValue: SubscriptionPurchaseEnvironment.ServiceEnvironment.default.description) - private var privacyProEnvironment: String - +// swiftlint:disable:next type_body_length +@available(iOS 15.0, *) final class SubscriptionDebugViewController: UITableViewController { + + let subscriptionAppGroup = Bundle.main.appGroup(bundle: .subs) + private var subscriptionManager: SubscriptionManaging { + AppDependencyProvider.shared.subscriptionManager + } + private let titles = [ Sections.authorization: "Authentication", Sections.subscription: "Subscription", @@ -69,7 +68,6 @@ final class SubscriptionDebugViewController: UITableViewController { case staging case production } - override func numberOfSections(in tableView: UITableView) -> Int { return Sections.allCases.count @@ -80,7 +78,6 @@ final class SubscriptionDebugViewController: UITableViewController { return titles[section] } - // swiftlint:disable cyclomatic_complexity override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) @@ -125,22 +122,20 @@ final class SubscriptionDebugViewController: UITableViewController { } case .environment: - let staging = SubscriptionPurchaseEnvironment.ServiceEnvironment.staging - let prod = SubscriptionPurchaseEnvironment.ServiceEnvironment.production + let currentEnv = subscriptionManager.currentEnvironment.serviceEnvironment switch EnvironmentRows(rawValue: indexPath.row) { case .staging: cell.textLabel?.text = "Staging" - cell.accessoryType = SubscriptionPurchaseEnvironment.currentServiceEnvironment == staging ? .checkmark : .none + cell.accessoryType = currentEnv == .staging ? .checkmark : .none case .production: cell.textLabel?.text = "Production" - cell.accessoryType = SubscriptionPurchaseEnvironment.currentServiceEnvironment == prod ? .checkmark : .none + cell.accessoryType = currentEnv == .production ? .checkmark : .none case .none: break } } return cell } - // swiftlint:enable cyclomatic_complexity override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { switch Sections(rawValue: section) { @@ -153,7 +148,7 @@ final class SubscriptionDebugViewController: UITableViewController { } } - // swiftlint:disable cyclomatic_complexity + // swiftlint:disable:next cyclomatic_complexity override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { switch Sections(rawValue: indexPath.section) { case .authorization: @@ -176,19 +171,46 @@ final class SubscriptionDebugViewController: UITableViewController { default: break } case .environment: - switch EnvironmentRows(rawValue: indexPath.row) { - case .staging: setEnvironment(.staging) - case .production: setEnvironment(.production) - default: break - } + guard let subEnv: EnvironmentRows = EnvironmentRows(rawValue: indexPath.row) else { return } + changeSubscriptionEnvironment(envRows: subEnv) case .none: break } - tableView.deselectRow(at: indexPath, animated: true) } - // swiftlint:enable cyclomatic_complexity + private func changeSubscriptionEnvironment(envRows: EnvironmentRows) { + var subEnvDesc: String + switch envRows { + case .staging: + subEnvDesc = "STAGING" + case .production: + subEnvDesc = "PRODUCTION" + } + let message = """ + Are you sure you want to change the environment to \(subEnvDesc)? + This setting IS persisted between app runs. This action will close the app, do you want to proceed? + """ + let alertController = UIAlertController(title: "⚠️ App restart required! The changes are persistent", + message: message, + preferredStyle: .actionSheet) + alertController.addAction(UIAlertAction(title: "Yes", style: .destructive) { [weak self] _ in + switch envRows { + case .staging: + self?.setEnvironment(.staging) + case .production: + self?.setEnvironment(.production) + } + // Close the app + exit(0) + }) + let okAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) + alertController.addAction(okAction) + DispatchQueue.main.async { + self.present(alertController, animated: true, completion: nil) + } + } + private func showAlert(title: String, message: String? = nil) { DispatchQueue.main.async { let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) @@ -198,32 +220,34 @@ final class SubscriptionDebugViewController: UITableViewController { } } +// func showAlert(title: String, message: String, alternativeAction) + // MARK: Account Status Actions private func clearAuthData() { - accountManager.signOut() + subscriptionManager.accountManager.signOut() showAlert(title: "Data cleared!") } private func injectCredentials() { - accountManager.storeAccount(token: "a-fake-token", + subscriptionManager.accountManager.storeAccount(token: "a-fake-token", email: "a.fake@email.com", externalID: "666") showAccountDetails() } private func showAccountDetails() { - let title = accountManager.isUserAuthenticated ? "Authenticated" : "Not Authenticated" - let message = accountManager.isUserAuthenticated ? - ["Service Environment: \(SubscriptionPurchaseEnvironment.currentServiceEnvironment.description)", - "AuthToken: \(accountManager.authToken ?? "")", - "AccessToken: \(accountManager.accessToken ?? "")", - "Email: \(accountManager.email ?? "")"].joined(separator: "\n") : nil + let title = subscriptionManager.accountManager.isUserAuthenticated ? "Authenticated" : "Not Authenticated" + let message = subscriptionManager.accountManager.isUserAuthenticated ? + ["Service Environment: \(subscriptionManager.currentEnvironment.serviceEnvironment.description)", + "AuthToken: \(subscriptionManager.accountManager.authToken ?? "")", + "AccessToken: \(subscriptionManager.accountManager.accessToken ?? "")", + "Email: \(subscriptionManager.accountManager.email ?? "")"].joined(separator: "\n") : nil showAlert(title: title, message: message) } private func syncAppleIDAccount() { Task { - switch await purchaseManager.syncAppleIDAccount() { + switch await subscriptionManager.storePurchaseManager().syncAppleIDAccount() { case .success: showAlert(title: "Account synced!", message: "") case .failure(let error): @@ -234,11 +258,11 @@ final class SubscriptionDebugViewController: UITableViewController { private func validateToken() { Task { - guard let token = accountManager.accessToken else { + guard let token = subscriptionManager.accountManager.accessToken else { showAlert(title: "Not authenticated", message: "No authenticated user found! - Token not available") return } - switch await AuthService.validateToken(accessToken: token) { + switch await subscriptionManager.authService.validateToken(accessToken: token) { case .success(let response): showAlert(title: "Token details", message: "\(response)") case .failure(let error): @@ -249,11 +273,11 @@ final class SubscriptionDebugViewController: UITableViewController { private func getSubscription() { Task { - guard let token = accountManager.accessToken else { + guard let token = subscriptionManager.accountManager.accessToken else { showAlert(title: "Not authenticated", message: "No authenticated user found! - Subscription not available") return } - switch await SubscriptionService.getSubscription(accessToken: token, cachePolicy: .reloadIgnoringLocalCacheData) { + switch await subscriptionManager.subscriptionService.getSubscription(accessToken: token, cachePolicy: .reloadIgnoringLocalCacheData) { case .success(let response): showAlert(title: "Subscription info", message: "\(response)") case .failure(let error): @@ -265,13 +289,14 @@ final class SubscriptionDebugViewController: UITableViewController { private func getEntitlements() { Task { var results: [String] = [] - guard accountManager.accessToken != nil else { + guard subscriptionManager.accountManager.accessToken != nil else { showAlert(title: "Not authenticated", message: "No authenticated user found! - Subscription not available") return } let entitlements: [Entitlement.ProductName] = [.networkProtection, .dataBrokerProtection, .identityTheftRestoration] for entitlement in entitlements { - if case let .success(result) = await AccountManager().hasEntitlement(for: entitlement, cachePolicy: .reloadIgnoringLocalCacheData) { + if case let .success(result) = await subscriptionManager.accountManager.hasEntitlement(for: entitlement, + cachePolicy: .reloadIgnoringLocalCacheData) { let resultSummary = "Entitlement check for \(entitlement.rawValue): \(result)" results.append(resultSummary) print(resultSummary) @@ -281,23 +306,28 @@ final class SubscriptionDebugViewController: UITableViewController { } } - private func setEnvironment(_ environment: SubscriptionPurchaseEnvironment.ServiceEnvironment) { - if environment.description != privacyProEnvironment { - - AccountManager().signOut() - - // Update Subscription environment - privacyProEnvironment = environment.rawValue - SubscriptionPurchaseEnvironment.currentServiceEnvironment = environment - - // Update VPN Environment - VPNSettings(defaults: .networkProtectionGroupDefaults).selectedEnvironment = environment == .production - ? .production - : .staging + private func setEnvironment(_ environment: SubscriptionEnvironment.ServiceEnvironment) { + + let subscriptionUserDefaults = UserDefaults(suiteName: subscriptionAppGroup)! + let currentSubscriptionEnvironment = SubscriptionManager.getSavedOrDefaultEnvironment(userDefaults: subscriptionUserDefaults) + var newSubscriptionEnvironment = SubscriptionEnvironment.default + newSubscriptionEnvironment.serviceEnvironment = environment + + if newSubscriptionEnvironment.serviceEnvironment != currentSubscriptionEnvironment.serviceEnvironment { + subscriptionManager.accountManager.signOut() + + // Save Subscription environment + SubscriptionManager.save(subscriptionEnvironment: newSubscriptionEnvironment, userDefaults: subscriptionUserDefaults) + + // The VPN environment is forced to match the subscription environment + let settings = AppDependencyProvider.shared.vpnSettings + switch newSubscriptionEnvironment.serviceEnvironment { + case .production: + settings.selectedEnvironment = .production + case .staging: + settings.selectedEnvironment = .staging + } NetworkProtectionLocationListCompositeRepository.clearCache() - - tableView.reloadData() } - } } diff --git a/DuckDuckGo/SuggestionTableViewCell.swift b/DuckDuckGo/SuggestionTableViewCell.swift deleted file mode 100644 index 2cc389b634..0000000000 --- a/DuckDuckGo/SuggestionTableViewCell.swift +++ /dev/null @@ -1,124 +0,0 @@ -// -// SuggestionTableViewCell.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 Suggestions - -class SuggestionTableViewCell: UITableViewCell { - - struct Constants { - static let cellHeight: CGFloat = 46.0 - } - - static let reuseIdentifier = "SuggestionTableViewCell" - - @IBOutlet weak var label: UILabel! - @IBOutlet weak var urlLabel: UILabel! - @IBOutlet weak var typeImage: UIImageView! - @IBOutlet weak var plusButton: UIButton! - - func updateFor(query: String, suggestion: Suggestion, with theme: Theme, isAddressBarAtBottom: Bool) { - - var text: String = "" - switch suggestion { - case .phrase(phrase: let phrase): - text = phrase - typeImage.image = UIImage(named: "Find-Search-20") - urlLabel.isHidden = true - self.accessibilityValue = UserText.voiceoverSuggestionTypeSearch - - case .website(url: let url): - text = url.absoluteString - typeImage.image = UIImage(named: "Globe-20") - urlLabel.isHidden = true - self.accessibilityValue = UserText.voiceoverSuggestionTypeSearch - - case .bookmark(title: let title, let url, _, _): - text = title - urlLabel.isHidden = url.isBookmarklet() - urlLabel.text = url.formattedForSuggestion() - typeImage.image = UIImage(named: "Bookmark-20") - self.accessibilityValue = UserText.voiceoverSuggestionTypeBookmark - - case .historyEntry(title: let title, url: let url, _): - if url.isDuckDuckGoSearch, let searchQuery = url.searchQuery { - text = searchQuery - } else { - text = title ?? url.absoluteString - } - urlLabel.isHidden = false - urlLabel.text = url.formattedForSuggestion() - typeImage.image = UIImage(named: "History-20") - self.accessibilityValue = UserText.voiceoverSuggestionTypeBookmark - - case .unknown(value: let value), .internalPage(title: let value, url: _): - assertionFailure("Unknown suggestion \(value)") - } - - self.plusButton.accessibilityLabel = UserText.voiceoverActionAutocomplete - if isAddressBarAtBottom { - self.plusButton.setImage(UIImage(named: "Arrow-Down-Left-24"), for: .normal) - } else { - self.plusButton.setImage(UIImage(named: "Arrow-Top-Left-24"), for: .normal) - } - - urlLabel.textColor = theme.tableCellTextColor - styleText(query: query, - text: text, - regularColor: theme.tableCellTextColor, - suggestionColor: theme.autocompleteSuggestionTextColor) - } - - private func styleText(query: String, text: String, regularColor: UIColor, suggestionColor: UIColor) { - - let regularAttributes = [ - NSAttributedString.Key.font: UIFont.appFont(ofSize: 16), - NSAttributedString.Key.foregroundColor: regularColor - ] - - let boldAttributes = [ - NSAttributedString.Key.font: UIFont.boldAppFont(ofSize: 16), - NSAttributedString.Key.foregroundColor: suggestionColor - ] - - let newText = NSMutableAttributedString(string: text) - - let queryLength = query.length() - if queryLength < newText.length, text.hasPrefix(query) { - newText.addAttributes(regularAttributes, range: NSRange(location: 0, length: queryLength)) - newText.addAttributes(boldAttributes, range: NSRange(location: queryLength, length: newText.length - queryLength)) - } else { - newText.addAttributes(regularAttributes, range: NSRange(location: 0, length: newText.length)) - } - - label.attributedText = newText - } -} - -private extension URL { - - func formattedForSuggestion() -> String { - let string = absoluteString - .dropping(prefix: "https://") - .dropping(prefix: "http://") - return pathComponents.isEmpty ? string : string.dropping(suffix: "/") - } - -} diff --git a/DuckDuckGo/SuggestionTray.storyboard b/DuckDuckGo/SuggestionTray.storyboard index 64c0f040aa..25374b6488 100644 --- a/DuckDuckGo/SuggestionTray.storyboard +++ b/DuckDuckGo/SuggestionTray.storyboard @@ -1,9 +1,9 @@ - + - + @@ -18,11 +18,11 @@ - + - + @@ -36,20 +36,21 @@ - + + - + - + diff --git a/DuckDuckGo/SuggestionTrayViewController.swift b/DuckDuckGo/SuggestionTrayViewController.swift index c10bc32fe1..0122b3219b 100644 --- a/DuckDuckGo/SuggestionTrayViewController.swift +++ b/DuckDuckGo/SuggestionTrayViewController.swift @@ -38,7 +38,10 @@ class SuggestionTrayViewController: UIViewController { weak var favoritesOverlayDelegate: FavoritesOverlayDelegate? var dismissHandler: (() -> Void)? - + var isShowingAutocompleteSuggestions: Bool { + autocompleteController != nil + } + private let appSettings = AppUserDefaults() private var autocompleteController: AutocompleteViewController? @@ -46,8 +49,7 @@ class SuggestionTrayViewController: UIViewController { private var willRemoveAutocomplete = false private let bookmarksDatabase: CoreDataDatabase private let favoritesModel: FavoritesListInteracting - private let historyCoordinator: HistoryCoordinating - private let bookmarksStringSearch: BookmarksStringSearch + private let historyManager: HistoryManager var selectedSuggestion: Suggestion? { autocompleteController?.selectedSuggestion @@ -77,11 +79,10 @@ class SuggestionTrayViewController: UIViewController { } } - required init?(coder: NSCoder, favoritesViewModel: FavoritesListInteracting, bookmarksDatabase: CoreDataDatabase, historyCoordinator: HistoryCoordinating, bookmarksStringSearch: BookmarksStringSearch) { + required init?(coder: NSCoder, favoritesViewModel: FavoritesListInteracting, bookmarksDatabase: CoreDataDatabase, historyManager: HistoryManager) { self.favoritesModel = favoritesViewModel self.bookmarksDatabase = bookmarksDatabase - self.historyCoordinator = historyCoordinator - self.bookmarksStringSearch = bookmarksStringSearch + self.historyManager = historyManager super.init(coder: coder) } @@ -128,15 +129,7 @@ class SuggestionTrayViewController: UIViewController { } } } - - func willDismiss(with query: String) { - guard !query.isEmpty else { return } - if let autocomplete = autocompleteController { - autocomplete.willDismiss(with: query) - } - } - var contentFrame: CGRect { return containerView.frame } @@ -155,8 +148,7 @@ class SuggestionTrayViewController: UIViewController { } func float(withWidth width: CGFloat) { - autocompleteController?.showBackground = false - + containerView.layer.cornerRadius = 16 containerView.layer.masksToBounds = true @@ -168,11 +160,11 @@ class SuggestionTrayViewController: UIViewController { backgroundView.layer.shadowOpacity = 0.3 backgroundView.layer.shadowRadius = 120 - topConstraint.constant = 15 - + topConstraint.constant = 4 + let isFirstPresentation = fullHeightConstraint.isActive if isFirstPresentation { - variableHeightConstraint.constant = SuggestionTableViewCell.Constants.cellHeight * 6 + variableHeightConstraint.constant = Constant.suggestionTrayInitialHeight } variableWidthConstraint.constant = width @@ -181,8 +173,6 @@ class SuggestionTrayViewController: UIViewController { } func fill() { - autocompleteController?.showBackground = true - containerView.layer.shadowColor = UIColor.clear.cgColor containerView.layer.cornerRadius = 0 @@ -240,13 +230,14 @@ class SuggestionTrayViewController: UIViewController { if autocompleteController == nil { installAutocompleteSuggestions() } - autocompleteController?.updateQuery(query: query) + autocompleteController?.updateQuery(query) } private func installAutocompleteSuggestions() { - let controller = AutocompleteViewController.loadFromStoryboard(bookmarksDatabase: bookmarksDatabase, - bookmarksStringSearch: bookmarksStringSearch, - historyCoordinator: historyCoordinator) + let controller = AutocompleteViewController(historyManager: historyManager, + bookmarksDatabase: bookmarksDatabase, + appSettings: appSettings) + install(controller: controller) controller.delegate = autocompleteDelegate controller.presentationDelegate = self @@ -272,6 +263,15 @@ class SuggestionTrayViewController: UIViewController { addChild(controller) controller.view.frame = containerView.bounds containerView.addSubview(controller.view) + + controller.view.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + containerView.topAnchor.constraint(equalTo: controller.view.topAnchor), + containerView.leftAnchor.constraint(equalTo: controller.view.leftAnchor), + containerView.bottomAnchor.constraint(equalTo: controller.view.bottomAnchor), + containerView.rightAnchor.constraint(equalTo: controller.view.rightAnchor) + ]) + controller.didMove(toParent: self) controller.view.alpha = 0 UIView.animate(withDuration: 0.2, animations: { @@ -284,8 +284,8 @@ class SuggestionTrayViewController: UIViewController { var contentInsets = UIEdgeInsets.zero func applyContentInset(_ inset: UIEdgeInsets) { self.contentInsets = inset - autocompleteController?.tableView.contentInset = inset favoritesOverlay?.collectionView.contentInset = inset + favoritesOverlay?.collectionView.scrollIndicatorInsets = inset } } @@ -298,7 +298,7 @@ extension SuggestionTrayViewController: AutocompleteViewControllerPresentationDe guard !fullHeightConstraint.isActive else { return } - if height > variableHeightConstraint.constant { + if height > Constant.suggestionTrayInitialHeight { variableHeightConstraint.constant = height } } @@ -317,3 +317,9 @@ extension SuggestionTrayViewController { } } + +private extension SuggestionTrayViewController { + enum Constant { + static let suggestionTrayInitialHeight = 312.0 // ie 52 * 6 + } +} diff --git a/DuckDuckGo/SwipeTabsCoordinator.swift b/DuckDuckGo/SwipeTabsCoordinator.swift index 2d89f176e4..7b9d577d1f 100644 --- a/DuckDuckGo/SwipeTabsCoordinator.swift +++ b/DuckDuckGo/SwipeTabsCoordinator.swift @@ -18,6 +18,7 @@ // import UIKit +import Core class SwipeTabsCoordinator: NSObject { @@ -85,6 +86,14 @@ class SwipeTabsCoordinator: NSObject { case starting(CGPoint) case swiping(CGPoint, FloatingPointSign) + var isIdle: Bool { + if case .idle = self { + return true + } + + return false + } + } var state: State = .idle @@ -217,6 +226,12 @@ extension SwipeTabsCoordinator: UICollectionViewDelegate { } func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + guard !state.isIdle else { + Pixel.fire(pixel: .swipeTabsIncorrectScrollState) + assertionFailure("invalid state") + return + } + defer { cleanUpViews() state = .idle @@ -291,7 +306,10 @@ extension SwipeTabsCoordinator: UICollectionViewDataSource { if !isEnabled || tabsModel.currentIndex == indexPath.row { cell.omniBar = coordinator.omniBar } else { - cell.omniBar = OmniBar.loadFromXib() + // Strong reference while we use the omnibar + let omniBar = OmniBar.loadFromXib() + + cell.omniBar = omniBar cell.omniBar?.translatesAutoresizingMaskIntoConstraints = false cell.updateConstraints() diff --git a/DuckDuckGo/TabSwitcherViewController.swift b/DuckDuckGo/TabSwitcherViewController.swift index fb1bdd35b1..9cac70c4d9 100644 --- a/DuckDuckGo/TabSwitcherViewController.swift +++ b/DuckDuckGo/TabSwitcherViewController.swift @@ -260,21 +260,35 @@ class TabSwitcherViewController: UIViewController { } @IBAction func onDisplayModeButtonPressed(_ sender: UIButton) { - tabSwitcherSettings.isGridViewEnabled = !tabSwitcherSettings.isGridViewEnabled - - if tabSwitcherSettings.isGridViewEnabled { - Pixel.fire(pixel: .tabSwitcherGridEnabled) - } else { - Pixel.fire(pixel: .tabSwitcherListEnabled) + guard isProcessingUpdates == false else { return } + + isProcessingUpdates = true + // Idea is here to wait for any pending processing of reconfigureItems on a cells, + // so when transition to/from grid happens we can request cells without any issues + // related to mismatched identifiers. + // Alternative is to use reloadItems instead of reconfigureItems but it looks very bad + // when tabs are reloading in the background. + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in + guard let self else { return } + + tabSwitcherSettings.isGridViewEnabled = !tabSwitcherSettings.isGridViewEnabled + + if tabSwitcherSettings.isGridViewEnabled { + Pixel.fire(pixel: .tabSwitcherGridEnabled) + } else { + Pixel.fire(pixel: .tabSwitcherListEnabled) + } + + self.refreshDisplayModeButton() + + UIView.transition(with: view, + duration: 0.3, + options: .transitionCrossDissolve, animations: { + self.collectionView.reloadData() + }, completion: { _ in + self.isProcessingUpdates = false + }) } - - refreshDisplayModeButton() - - UIView.transition(with: view, - duration: 0.3, - options: .transitionCrossDissolve, animations: { - self.collectionView.reloadData() - }, completion: nil) } @IBAction func onAddPressed(_ sender: UIBarButtonItem) { diff --git a/DuckDuckGo/TabURLInterceptor.swift b/DuckDuckGo/TabURLInterceptor.swift index 6726669845..d0b885e61c 100644 --- a/DuckDuckGo/TabURLInterceptor.swift +++ b/DuckDuckGo/TabURLInterceptor.swift @@ -37,6 +37,12 @@ protocol TabURLInterceptor { final class TabURLInterceptorDefault: TabURLInterceptor { + typealias CanPurchaseUpdater = () -> Bool + private let canPurchase: CanPurchaseUpdater + + init(canPurchase: @escaping CanPurchaseUpdater) { + self.canPurchase = canPurchase + } static let interceptedURLs: [InterceptedURLInfo] = [ InterceptedURLInfo(id: .privacyPro, path: "/pro") @@ -55,9 +61,8 @@ final class TabURLInterceptorDefault: TabURLInterceptor { guard let matchingURL = urlToIntercept(path: components.path) else { return true } - - return Self.handleURLInterception(interceptedURL: matchingURL.id, queryItems: components.percentEncodedQueryItems) + return handleURLInterception(interceptedURL: matchingURL.id, queryItems: components.percentEncodedQueryItems) } } @@ -79,25 +84,23 @@ extension TabURLInterceptorDefault { return URLComponents(string: "\(URL.URLProtocol.https.scheme)\(noScheme)") } - private static func handleURLInterception(interceptedURL: InterceptedURL, queryItems: [URLQueryItem]?) -> Bool { + private func handleURLInterception(interceptedURL: InterceptedURL, queryItems: [URLQueryItem]?) -> Bool { switch interceptedURL { - // Opens the Privacy Pro Subscription Purchase page (if user can purchase) - case .privacyPro: - if SubscriptionPurchaseEnvironment.canPurchase { - // If URL has an `origin` query parameter, append it to the `subscriptionPurchase` URL. - // Also forward the origin as it will need to be sent as parameter to the Pixel to track subcription attributions. - let originQueryItem = queryItems?.first(where: { $0.name == AttributionParameter.origin }) - NotificationCenter.default.post( - name: .urlInterceptPrivacyPro, - object: nil, - userInfo: [AttributionParameter.origin: originQueryItem?.value] - ) - return false - } + case .privacyPro: + if canPurchase() { + // If URL has an `origin` query parameter, append it to the `subscriptionPurchase` URL. + // Also forward the origin as it will need to be sent as parameter to the Pixel to track subcription attributions. + let originQueryItem = queryItems?.first(where: { $0.name == AttributionParameter.origin }) + NotificationCenter.default.post( + name: .urlInterceptPrivacyPro, + object: nil, + userInfo: [AttributionParameter.origin: originQueryItem?.value] + ) + return false } + } return true - } } diff --git a/DuckDuckGo/TabViewController.swift b/DuckDuckGo/TabViewController.swift index 96de57c04e..fee12e4e7d 100644 --- a/DuckDuckGo/TabViewController.swift +++ b/DuckDuckGo/TabViewController.swift @@ -127,11 +127,13 @@ class TabViewController: UIViewController { private var trackersInfoWorkItem: DispatchWorkItem? - private var tabURLInterceptor: TabURLInterceptor = TabURLInterceptorDefault() + private var tabURLInterceptor: TabURLInterceptor = TabURLInterceptorDefault { + return AppDependencyProvider.shared.subscriptionManager.canPurchase + } private var currentlyLoadedURL: URL? #if NETWORK_PROTECTION - private let netPConnectionObserver = ConnectionStatusObserverThroughSession() + private let netPConnectionObserver: ConnectionStatusObserver = AppDependencyProvider.shared.connectionObserver private var netPConnectionObserverCancellable: AnyCancellable? private var netPConnectionStatus: ConnectionStatus = .default private var netPConnected: Bool { @@ -2516,9 +2518,6 @@ extension TabViewController: SecureVaultManagerDelegate { promptUserWithGeneratedPassword password: String, completionHandler: @escaping (Bool) -> Void) { let passwordGenerationPromptViewController = PasswordGenerationPromptViewController(generatedPassword: password) { useGeneratedPassword in - if useGeneratedPassword { - NotificationCenter.default.post(name: .autofillFillEvent, object: nil) - } completionHandler(useGeneratedPassword) } @@ -2659,6 +2658,8 @@ extension TabViewController: SaveLoginViewControllerDelegate { with: AutofillSecureVaultFactory) confirmSavedCredentialsFor(credentialID: credentialID, message: message) syncService.scheduler.notifyDataChanged() + + NotificationCenter.default.post(name: .autofillSaveEvent, object: nil) } catch { os_log("%: failed to store credentials %s", type: .error, #function, error.localizedDescription) } diff --git a/DuckDuckGo/Theme+DesignSystem.swift b/DuckDuckGo/Theme+DesignSystem.swift index a98dfdbb0a..c3f20c2256 100644 --- a/DuckDuckGo/Theme+DesignSystem.swift +++ b/DuckDuckGo/Theme+DesignSystem.swift @@ -19,6 +19,7 @@ import UIKit import DesignResourcesKit +import DuckUI // Once all colours are from the design system we can consider removing having multiple themes. extension Theme { @@ -35,6 +36,7 @@ extension Theme { // New: var autocompleteCellAccessoryColor: UIColor { UIColor(designSystemColor: .icons) } + var autocompleteCellPlusButtonColor: UIColor { UIColor(lightColor: .shade(0.6), darkColor: .tint(0.6)) } var autofillDefaultTitleTextColor: UIColor { UIColor(designSystemColor: .textPrimary) } var autofillDefaultSubtitleTextColor: UIColor { UIColor(designSystemColor: .textSecondary) } @@ -86,6 +88,7 @@ extension Theme { var autocompleteSuggestionTextColor: UIColor { UIColor(designSystemColor: .textPrimary) } var tableCellTextColor: UIColor { UIColor(designSystemColor: .textPrimary) } + var tableCellSecondaryTextColor: UIColor { UIColor(designSystemColor: .textSecondary) } var tableCellSeparatorColor: UIColor { UIColor(designSystemColor: .lines) } // No design system colour yet, so fall back to SDK colours diff --git a/DuckDuckGo/Theme.swift b/DuckDuckGo/Theme.swift index 8533cf8c8f..86488b9549 100644 --- a/DuckDuckGo/Theme.swift +++ b/DuckDuckGo/Theme.swift @@ -78,11 +78,13 @@ protocol Theme { var autocompleteSuggestionTextColor: UIColor { get } var autocompleteCellAccessoryColor: UIColor { get } + var autocompleteCellPlusButtonColor: UIColor { get } var tableCellBackgroundColor: UIColor { get } var tableCellSelectedColor: UIColor { get } var tableCellSeparatorColor: UIColor { get } var tableCellTextColor: UIColor { get } + var tableCellSecondaryTextColor: UIColor { get } var tableCellAccessoryTextColor: UIColor { get } var tableCellAccessoryColor: UIColor { get } var tableCellHighlightedBackgroundColor: UIColor { get } diff --git a/DuckDuckGo/URLDownloadSession.swift b/DuckDuckGo/URLDownloadSession.swift index 85e4d5e6ae..c30169a6b2 100644 --- a/DuckDuckGo/URLDownloadSession.swift +++ b/DuckDuckGo/URLDownloadSession.swift @@ -39,7 +39,7 @@ class URLDownloadSession: NSObject, DownloadSession { self.session = session } else { let configuration = URLSessionConfiguration.ephemeral - let userAgent = DefaultUserAgentManager.shared.userAgent(isDesktop: false) + let userAgent = DefaultUserAgentManager.shared.userAgent(isDesktop: false, url: url) configuration.httpAdditionalHeaders = ["user-agent": userAgent] self.session = URLSession(configuration: configuration, delegate: self, delegateQueue: .main) } diff --git a/DuckDuckGo/UniversalOmniBarState.swift b/DuckDuckGo/UniversalOmniBarState.swift new file mode 100644 index 0000000000..2e15892c02 --- /dev/null +++ b/DuckDuckGo/UniversalOmniBarState.swift @@ -0,0 +1,54 @@ +// +// UniversalOmniBarState.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Core + +enum UniversalOmniBarState { + struct EditingSuspendedState: OmniBarState { + let baseState: OmniBarState + + var hasLargeWidth: Bool { baseState.hasLargeWidth } + var showBackButton: Bool { baseState.showBackButton } + var showForwardButton: Bool { baseState.showForwardButton } + var showBookmarksButton: Bool { baseState.showBookmarksButton } + var showShareButton: Bool { baseState.showShareButton } + var clearTextOnStart: Bool { baseState.clearTextOnStart } + var allowsTrackersAnimation: Bool { baseState.allowsTrackersAnimation } + var showSearchLoupe: Bool { baseState.showSearchLoupe } + var showCancel: Bool { true } + var showPrivacyIcon: Bool { baseState.showPrivacyIcon } + var showBackground: Bool { baseState.showBackground } + var showClear: Bool { false } + var showRefresh: Bool { baseState.showRefresh } + var showMenu: Bool { baseState.showMenu } + var showSettings: Bool { baseState.showSettings } + var showVoiceSearch: Bool { baseState.showVoiceSearch } + var name: String { Type.name(self) } + var onEditingStoppedState: OmniBarState { baseState.onEditingStoppedState } + var onEditingStartedState: OmniBarState { baseState.onEditingStartedState } + var onTextClearedState: OmniBarState { baseState.onTextClearedState } + var onTextEnteredState: OmniBarState { baseState.onTextEnteredState } + var onBrowsingStartedState: OmniBarState { baseState.onBrowsingStartedState } + var onBrowsingStoppedState: OmniBarState { baseState.onBrowsingStoppedState } + var onEnterPhoneState: OmniBarState { baseState.onEnterPhoneState } + var onEnterPadState: OmniBarState { baseState.onEnterPadState } + var onReloadState: OmniBarState { baseState.onReloadState } + } +} diff --git a/DuckDuckGo/UnprotectedSitesViewController.swift b/DuckDuckGo/UnprotectedSitesViewController.swift index 8ababbfda9..eaf31a3678 100644 --- a/DuckDuckGo/UnprotectedSitesViewController.swift +++ b/DuckDuckGo/UnprotectedSitesViewController.swift @@ -225,8 +225,8 @@ class UnprotectedSitesViewController: UITableViewController { } private func createAllProtectedCell(forRowAt indexPath: IndexPath) -> UITableViewCell { - guard let allProtectedCell = tableView.dequeueReusableCell(withIdentifier: "AllProtectedCell") as? NoSuggestionsTableViewCell else { - fatalError("Failed to dequeue NoSuggestionsTableViewCell using 'AllProtectedCell'") + guard let allProtectedCell = tableView.dequeueReusableCell(withIdentifier: "AllProtectedCell") as? AllProtectedCell else { + fatalError("Failed to dequeue AllProtectedCell using 'AllProtectedCell'") } let theme = ThemeManager.shared.currentTheme diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index 49c3a68a05..e5d78309d1 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -497,7 +497,7 @@ public struct UserText { static let netPStatusViewConnectionDetails = NSLocalizedString("network.protection.status.view.connection.details", value: "Connection Details", comment: "Connection details label shown in NetworkProtection's status view.") static let netPStatusViewSettingsSectionTitle = NSLocalizedString("network.protection.status.view.settings.section.title", value: "Manage", comment: "Label shown on the title of the settings section in NetworkProtection's status view.") static let netPVPNSettingsTitle = NSLocalizedString("network.protection.vpn.settings.title", value: "VPN Settings", comment: "Title for the VPN Settings screen.") - static let netPVPNSettingsFAQ = NSLocalizedString("network.protection.vpn.settings.faq", value: "Frequently Asked Questions", comment: "Title for the FAQ row in the VPN status screen.") + static let netPVPNSettingsFAQ = NSLocalizedString("network.protection.vpn.settings.faq", value: "FAQs and Support", comment: "Title for the FAQ row in the VPN status screen.") static let netPVPNSettingsShareFeedback = NSLocalizedString("network.protection.vpn.settings.share-feedback", value: "Share VPN Feedback", comment: "Title for the feedback row in the VPN status screen.") static func netPVPNSettingsLocationSubtitleFormattedCityAndCountry(city: String, country: String) -> String { let localized = NSLocalizedString("network.protection.vpn.location.subtitle.formatted.city.and.country", value: "%@, %@", comment: "Subtitle for the preferred location item that formats a city and country. E.g Chicago, United States") @@ -984,8 +984,10 @@ But if you *do* want a peek under the hood, you can find more information about // Subscription Section public static let settingsPProSection = NSLocalizedString("settings.ppro", value: "Privacy Pro", comment: "Product name for the subscription bundle") + public static let settingsPProSectionFooter = NSLocalizedString("settings.ppro.footer", value: "Privacy Policy and Terms of Service", comment: "Title for Link in the Footer of Privacy Pro section") + public static let settingsPProSubscribe = NSLocalizedString("settings.subscription.subscribe", value: "Subscribe to Privacy Pro", comment: "Call to action title for Privacy Pro") - public static let settingsPProDescription = NSLocalizedString("settings.subscription.description", value:"More seamless privacy with three new protections, including:", comment: "Privacy pro description subtext") + public static let settingsPProDescription = NSLocalizedString("settings.subscription.description", value:"More seamless privacy with three new protections:", comment: "Privacy pro description subtext") public static let settingsPProFeatures = NSLocalizedString("settings.subscription.features", value: """ • VPN @@ -1004,7 +1006,7 @@ But if you *do* want a peek under the hood, you can find more information about public static let settingsPProITRTitle = NSLocalizedString("settings.subscription.ITR.title", value: "Identity Theft Restoration", comment: "Identity theft restoration cell title for privacy pro") public static let settingsPProITRSubTitle = NSLocalizedString("settings.subscription.ITR.subtitle", value: "If your identity is stolen, we'll help restore it", comment: "Identity theft restoration cell subtitle for privacy pro") - public static let settingsPProActivationPendingTitle = NSLocalizedString("settings.subscription.activation.pending.title", value: "Your Subscription is Being Activated", comment: "Subscription activation pending title") + public static let settingsPProActivationPendingTitle = NSLocalizedString("settings.subscription.activation.pending.title", value: "Your Subscription is being activated", comment: "Subscription activation pending title") public static let settingsPProActivationPendingDescription = NSLocalizedString("settings.subscription.activation.pending.description", value: "This is taking longer than usual, please check back later.", comment: "Subscription activation pending description") // Expired Subscription @@ -1017,6 +1019,7 @@ But if you *do* want a peek under the hood, you can find more information about public static let settingsKeyboard = NSLocalizedString("settings.keyboard", value: "Keyboard", comment: "Settings screen cell for Keyboard") public static let settingsPreviews = NSLocalizedString("settings.previews", value: "Long-Press Previews", comment: "Settings screen cell for long press previews") public static let settingsAutocomplete = NSLocalizedString("settings.autocomplete", value: "Autocomplete Suggestions", comment: "Settings screen cell for autocomplete") + public static let settingsAutocompleteRecentlyVisited = NSLocalizedString("settings.autocomplete.recentlyvisited", value: "Recently Visited Sites", comment: "Settings label for enabling or disabling recently visited sites") // Hardcoded for the experiment public static let settingsAutocompleteSubtitle = "See search suggestions, including your bookmarks and recently visited sites" @@ -1121,7 +1124,7 @@ But if you *do* want a peek under the hood, you can find more information about public static let subscriptionRestoreAddEmailTitle = NSLocalizedString("subscription.add.email.title", value: "Add Email", comment: "View title for adding email to subscription") // Manage Subscription Email - public static let subscriptionManageEmailDescription = NSLocalizedString("subscription.manage.email.description", value: "You can use this email to activate your subscription from browser settings in the DuckDuckGo app on your other devices.", comment: "Description for Email Management options") + public static let subscriptionManageEmailDescription = NSLocalizedString("subscription.manage.email.description", value: "Use this email to activate your subscription from browser settings in the DuckDuckGo app on other devices..", comment: "Description for Email Management options") public static let subscriptionManageEmailButton = NSLocalizedString("subscription.activate.manage.email.button", value: "Manage", comment: "Restore button title for Managing Email") public static let subscriptionManageEmailTitle = NSLocalizedString("subscription.activate.manage.email.title", value: "Manage Email", comment: "View Title for managing your email account") public static let subscriptionManageEmailCancelButton = NSLocalizedString("subscription.activate.manage.email.cancel", value: "Cancel", comment: "Button title for cancelling email deletion") @@ -1160,8 +1163,14 @@ But if you *do* want a peek under the hood, you can find more information about public static let subscriptionPIRHeroDesktopMenuLocation = NSLocalizedString("subscription.pir.heroTextLocation", value: "Settings > Privacy Pro", comment: "Settings references a menu in the Desktop app, Privacy Pro, references our product name") public static let subscriptionPIRHeroDesktopMenuItem = NSLocalizedString("subscription.pir.heroTextMenyEntry", value: "I have a subscription", comment: "Menu item for enabling Personal Information Removal on Desktop") public static let subscriptionPIRWindows = NSLocalizedString("subscription.pir.windows", value: "Windows", comment: "Text for the 'Windows' button") - public static let subscriptionPIRMacOS = NSLocalizedString("subscription.pir.macos", value: "macOS", comment: "Text for the 'macOS' button") + public static let subscriptionPIRMacOS = NSLocalizedString("subscription.pir.macos", value: "Mac", comment: "Text for the 'macOS' button") + + // Autocomplete + public static let autocompleteHistoryWarningTitle = NSLocalizedString("autocomplete.history.warning.title", value: "Same privacy.\nBetter search suggestions!", comment: "Title for message show in suggestions") + public static let autocompleteHistoryWarningDescription = NSLocalizedString("autocomplete.history.warning.message", value: "Suggestions now include recently visited sites. These are stored on your device and never on DuckDuckGo's servers. Turn off in settings, or clear any time with the 🔥 Fire Button.", comment: "The message text shown in suggestions") + public static let autocompleteSearchDuckDuckGo = NSLocalizedString("autocomplete.history.search.duckduckgo", value: "Search DuckDuckGo", comment: "Subtitle for search history items") + // Site not working public static let siteNotWorkingTitle = NSLocalizedString("site.not.working.title", value: "Site not working? Let DuckDuckGo know.", comment: "Prompt asking user to send report to us if we suspect site may be broken") public static let siteNotWorkingSubtitle = NSLocalizedString("site.not.working.subtitle", value: "This helps us improve the browser.", comment: "Prompt asking user to send report to us if we suspect site may be broken") public static let siteNotWorkingDismiss = NSLocalizedString("site.not.working.dismiss", value: "Dismiss", comment: "Dismiss button") diff --git a/DuckDuckGo/VPNWaitlistActivationDateStore.swift b/DuckDuckGo/VPNActivationDateStore.swift similarity index 94% rename from DuckDuckGo/VPNWaitlistActivationDateStore.swift rename to DuckDuckGo/VPNActivationDateStore.swift index 08a57b5758..3018fe3543 100644 --- a/DuckDuckGo/VPNWaitlistActivationDateStore.swift +++ b/DuckDuckGo/VPNActivationDateStore.swift @@ -1,5 +1,5 @@ // -// VPNWaitlistActivationDateStore.swift +// VPNActivationDateStore.swift // DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. @@ -22,7 +22,7 @@ import Foundation import Core -protocol VPNWaitlistActivationDateStore { +protocol VPNActivationDateStore { func setActivationDateIfNecessary() func daysSinceActivation() -> Int? @@ -32,7 +32,7 @@ protocol VPNWaitlistActivationDateStore { } -struct DefaultVPNWaitlistActivationDateStore: VPNWaitlistActivationDateStore { +struct DefaultVPNActivationDateStore: VPNActivationDateStore { private enum Constants { static let networkProtectionActivationDateKey = "com.duckduckgo.network-protection.activation-date" diff --git a/DuckDuckGo/VPNWaitlist.swift b/DuckDuckGo/VPNWaitlist.swift deleted file mode 100644 index 522a31a414..0000000000 --- a/DuckDuckGo/VPNWaitlist.swift +++ /dev/null @@ -1,118 +0,0 @@ -// -// VPNWaitlist.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. -// - -#if NETWORK_PROTECTION - -import Foundation -import BrowserServicesKit -import Combine -import Core -import Waitlist -import NetworkProtection - -final class VPNWaitlist: Waitlist { - - enum AccessType { - /// Used if the user does not have waitlist feature flag access - case none - - /// Used if the user has waitlist feature flag access, but has not joined the waitlist - case waitlistAvailable - - /// Used if the user has waitlist feature flag access, and has joined the waitlist - case waitlistJoined - - /// Used if the user has been invited via the waitlist, but needs to accept the Privacy Policy and Terms of Service - case waitlistInvitedPendingTermsAcceptance - - /// Used if the user has been invited via the waitlist and has accepted the Privacy Policy and Terms of Service - case waitlistInvited - - /// Used if the user has been invited to test Network Protection directly - case inviteCodeInvited - } - - static let identifier: String = "vpn" - static let apiProductName: String = "networkprotection_ios" - static let downloadURL: URL = URL.windows - - static let shared: VPNWaitlist = .init() - - static let backgroundTaskName = "VPN Waitlist Status Task" - static let backgroundRefreshTaskIdentifier = "com.duckduckgo.app.vpnWaitlistStatus" - static let notificationIdentifier = "com.duckduckgo.ios.vpn.invite-code-available" - static let inviteAvailableNotificationTitle = UserText.networkProtectionWaitlistNotificationTitle - static let inviteAvailableNotificationBody = UserText.networkProtectionWaitlistNotificationText - - var isAvailable: Bool { - let hasWaitlistAccess = featureFlagger.isFeatureOn(.networkProtectionWaitlistAccess) - let isWaitlistActive = featureFlagger.isFeatureOn(.networkProtectionWaitlistActive) - return hasWaitlistAccess && isWaitlistActive - } - - var isWaitlistRemoved: Bool { - return false - } - - let waitlistStorage: WaitlistStorage - let waitlistRequest: WaitlistRequest - private let featureFlagger: FeatureFlagger - private let networkProtectionAccess: NetworkProtectionAccess - - init(store: WaitlistStorage, request: WaitlistRequest, featureFlagger: FeatureFlagger, networkProtectionAccess: NetworkProtectionAccess) { - self.waitlistStorage = store - self.waitlistRequest = request - self.featureFlagger = featureFlagger - self.networkProtectionAccess = networkProtectionAccess - } - - convenience init(store: WaitlistStorage, request: WaitlistRequest) { - self.init( - store: store, - request: request, - featureFlagger: AppDependencyProvider.shared.featureFlagger, - networkProtectionAccess: NetworkProtectionAccessController() - ) - } - - var settingsSubtitle: String { - switch networkProtectionAccess.networkProtectionAccessType() { - case .none: - return "" - case .waitlistAvailable: - return UserText.networkProtectionSettingsSubtitleNotJoined - case .waitlistJoined: - return UserText.networkProtectionSettingsSubtitleJoinedButNotInvited - case .waitlistInvitedPendingTermsAcceptance: - return UserText.networkProtectionSettingsSubtitleJoinedAndInvited - case .waitlistInvited, .inviteCodeInvited: - assertionFailure("These states should use the VPN connection status") - return "" - } - } - -} - -extension WaitlistViewModel.ViewCustomAction { - static var openNetworkProtectionInviteCodeScreen = WaitlistViewModel.ViewCustomAction(identifier: "openNetworkProtectionInviteCodeScreen") - static var openNetworkProtectionPrivacyPolicyScreen = WaitlistViewModel.ViewCustomAction(identifier: "openNetworkProtectionPrivacyPolicyScreen") - static var acceptNetworkProtectionTerms = WaitlistViewModel.ViewCustomAction(identifier: "acceptNetworkProtectionTerms") -} - -#endif diff --git a/DuckDuckGo/VPNWaitlistDebugViewController.swift b/DuckDuckGo/VPNWaitlistDebugViewController.swift deleted file mode 100644 index 9ca0438b59..0000000000 --- a/DuckDuckGo/VPNWaitlistDebugViewController.swift +++ /dev/null @@ -1,215 +0,0 @@ -// -// VPNWaitlistDebugViewController.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. -// - -#if NETWORK_PROTECTION - -import UIKit -import Core -import BackgroundTasks -import Waitlist - -final class VPNWaitlistDebugViewController: UITableViewController { - - enum Constants { - static let mockInviteCode = "ABCD1234" - } - - enum Sections: Int, CaseIterable { - - case waitlistInformation - case debuggingActions - - } - - private let waitlistInformationTitles = [ - WaitlistInformationRows.waitlistTimestamp: "Timestamp", - WaitlistInformationRows.waitlistToken: "Token", - WaitlistInformationRows.waitlistInviteCode: "Invite Code", - WaitlistInformationRows.termsAccepted: "T&C Accepted", - WaitlistInformationRows.backgroundTask: "Earliest Refresh Date" - ] - - enum WaitlistInformationRows: Int, CaseIterable { - - case waitlistTimestamp - case waitlistToken - case waitlistInviteCode - case termsAccepted - case backgroundTask - - } - - private let debuggingActionTitles = [ - DebuggingActionRows.resetTermsAndConditionsAcceptance: "Reset T&C Acceptance", - DebuggingActionRows.scheduleWaitlistNotification: "Fire Waitlist Notification in 3 seconds", - DebuggingActionRows.setMockInviteCode: "Set Mock Invite Code", - DebuggingActionRows.deleteInviteCode: "Delete Invite Code" - ] - - enum DebuggingActionRows: Int, CaseIterable { - - case resetTermsAndConditionsAcceptance - case scheduleWaitlistNotification - case setMockInviteCode - case deleteInviteCode - - } - - private let storage = WaitlistKeychainStore(waitlistIdentifier: VPNWaitlist.identifier) - - private var backgroundTaskExecutionDate: String? - - override func viewDidLoad() { - super.viewDidLoad() - - let clearDataItem = UIBarButtonItem(image: UIImage(systemName: "trash")!, - style: .done, - target: self, - action: #selector(presentClearDataPrompt(_:))) - clearDataItem.tintColor = .systemRed - navigationItem.rightBarButtonItem = clearDataItem - - BGTaskScheduler.shared.getPendingTaskRequests { tasks in - if let task = tasks.first(where: { $0.identifier == VPNWaitlist.backgroundRefreshTaskIdentifier }) { - let formatter = DateFormatter() - formatter.dateStyle = .short - formatter.timeStyle = .medium - - self.backgroundTaskExecutionDate = formatter.string(from: task.earliestBeginDate!) - - DispatchQueue.main.async { - self.tableView.reloadData() - } - } - } - } - - override func numberOfSections(in tableView: UITableView) -> Int { - return Sections.allCases.count - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - switch Sections(rawValue: section)! { - case .waitlistInformation: return WaitlistInformationRows.allCases.count - case .debuggingActions: return DebuggingActionRows.allCases.count - } - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let section = Sections(rawValue: indexPath.section)! - - switch section { - case .waitlistInformation: - let cell = tableView.dequeueReusableCell(withIdentifier: "DetailCell", for: indexPath) - let row = WaitlistInformationRows(rawValue: indexPath.row)! - cell.textLabel?.text = waitlistInformationTitles[row] - - switch row { - case .waitlistTimestamp: - if let timestamp = storage.getWaitlistTimestamp() { - cell.detailTextLabel?.text = String(timestamp) - } else { - cell.detailTextLabel?.text = "None" - } - - case .waitlistToken: - cell.detailTextLabel?.text = storage.getWaitlistToken() ?? "None" - - case .waitlistInviteCode: - cell.detailTextLabel?.text = storage.getWaitlistInviteCode() ?? "None" - - case .termsAccepted: - if NetworkProtectionTermsAndConditionsUserDefaultsStore().networkProtectionWaitlistTermsAndConditionsAccepted { - cell.detailTextLabel?.text = "Yes" - } else { - cell.detailTextLabel?.text = "No" - } - - case .backgroundTask: - cell.detailTextLabel?.text = backgroundTaskExecutionDate ?? "None" - } - - return cell - - case .debuggingActions: - let cell = tableView.dequeueReusableCell(withIdentifier: "ActionCell", for: indexPath) - let row = DebuggingActionRows(rawValue: indexPath.row)! - cell.textLabel?.text = debuggingActionTitles[row] - - return cell - } - - } - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let section = Sections(rawValue: indexPath.section)! - - switch section { - case .waitlistInformation: break - case .debuggingActions: - let row = DebuggingActionRows(rawValue: indexPath.row)! - - switch row { - case .resetTermsAndConditionsAcceptance: - var termsAndConditionsStore = NetworkProtectionTermsAndConditionsUserDefaultsStore() - termsAndConditionsStore.networkProtectionWaitlistTermsAndConditionsAccepted = false - case .scheduleWaitlistNotification: - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3) { - self.storage.store(inviteCode: Constants.mockInviteCode) - VPNWaitlist.shared.sendInviteCodeAvailableNotification() - } - case .setMockInviteCode: - storage.store(inviteCode: Constants.mockInviteCode) - case .deleteInviteCode: - storage.delete(field: .inviteCode) - tableView.reloadData() - } - } - - tableView.deselectRow(at: indexPath, animated: true) - tableView.reloadData() - } - - @objc - private func presentClearDataPrompt(_ sender: AnyObject) { - let alert = UIAlertController(title: "Clear Waitlist Data?", message: nil, preferredStyle: .actionSheet) - - if UIDevice.current.userInterfaceIdiom == .pad { - alert.popoverPresentationController?.barButtonItem = (sender as? UIBarButtonItem) - } - - alert.addAction(UIAlertAction(title: "Clear Data", style: .destructive, handler: { _ in - self.clearDataAndReload() - })) - - alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) - - present(alert, animated: true) - } - - private func clearDataAndReload() { - storage.deleteWaitlistState() - var termsAndConditionsStore = NetworkProtectionTermsAndConditionsUserDefaultsStore() - termsAndConditionsStore.networkProtectionWaitlistTermsAndConditionsAccepted = false - - tableView.reloadData() - } -} - -#endif diff --git a/DuckDuckGo/VPNWaitlistTermsAndConditionsViewController.swift b/DuckDuckGo/VPNWaitlistTermsAndConditionsViewController.swift deleted file mode 100644 index 29aba0356e..0000000000 --- a/DuckDuckGo/VPNWaitlistTermsAndConditionsViewController.swift +++ /dev/null @@ -1,73 +0,0 @@ -// -// VPNWaitlistTermsAndConditionsViewController.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. -// - -#if NETWORK_PROTECTION - -import UIKit -import SwiftUI -import Core -import Waitlist - -@available(iOS 15, *) -final class VPNWaitlistTermsAndConditionsViewController: UIViewController { - - override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - title = UserText.netPNavTitle - addHostingControllerToViewHierarchy() - } - - private func addHostingControllerToViewHierarchy() { - let waitlistView = VPNWaitlistPrivacyPolicyView { _ in - var termsAndConditionsStore = NetworkProtectionTermsAndConditionsUserDefaultsStore() - termsAndConditionsStore.networkProtectionWaitlistTermsAndConditionsAccepted = true - - self.navigationController?.popToRootViewController(animated: true) - let networkProtectionViewController = NetworkProtectionRootViewController() - self.navigationController?.pushViewController(networkProtectionViewController, animated: true) - } - - let waitlistViewController = UIHostingController(rootView: waitlistView) - waitlistViewController.view.backgroundColor = UIColor(designSystemColor: .background) - - addChild(waitlistViewController) - waitlistViewController.view.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(waitlistViewController.view) - waitlistViewController.didMove(toParent: self) - - NSLayoutConstraint.activate([ - waitlistViewController.view.widthAnchor.constraint(equalTo: view.widthAnchor), - waitlistViewController.view.heightAnchor.constraint(equalTo: view.heightAnchor), - waitlistViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), - waitlistViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor) - ]) - } - -} - -#endif diff --git a/DuckDuckGo/VPNWaitlistView.swift b/DuckDuckGo/VPNWaitlistView.swift deleted file mode 100644 index 594cdfe6f9..0000000000 --- a/DuckDuckGo/VPNWaitlistView.swift +++ /dev/null @@ -1,396 +0,0 @@ -// -// VPNWaitlistView.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. -// - -#if NETWORK_PROTECTION - -import SwiftUI -import Core -import Waitlist -import DesignResourcesKit - -@available(iOS 15, *) -struct VPNWaitlistView: View { - - @EnvironmentObject var viewModel: WaitlistViewModel - - var body: some View { - switch viewModel.viewState { - case .notJoinedQueue: - VPNWaitlistSignUpView(requestInFlight: false) { action in - Task { await viewModel.perform(action: action) } - } - case .joiningQueue: - VPNWaitlistSignUpView(requestInFlight: true) { action in - Task { await viewModel.perform(action: action) } - } - case .joinedQueue(let state): - VPNWaitlistJoinedWaitlistView(notificationState: state) { action in - Task { await viewModel.perform(action: action) } - } - case .invited: - VPNWaitlistInvitedView { action in - Task { await viewModel.perform(action: action) } - } - case .waitlistRemoved: - fatalError("State not supported for VPN waitlists") - } - } -} - -@available(iOS 15.0, *) -struct VPNWaitlistSignUpView: View { - - let requestInFlight: Bool - - let action: WaitlistViewActionHandler - - var body: some View { - GeometryReader { proxy in - ScrollView { - VStack(alignment: .center, spacing: 8) { - HeaderView(imageName: "JoinVPNWaitlist", title: UserText.netPNavTitle) - - Text(UserText.networkProtectionWaitlistJoinSubtitle1) - .daxBodyRegular() - .foregroundColor(.waitlistTextSecondary) - .multilineTextAlignment(.center) - .lineSpacing(6) - - Text(UserText.networkProtectionWaitlistJoinSubtitle2) - .daxBodyRegular() - .foregroundColor(.waitlistTextSecondary) - .multilineTextAlignment(.center) - .lineSpacing(6) - .padding(.top, 18) - - Button(UserText.networkProtectionWaitlistButtonJoinWaitlist, action: { action(.joinQueue) }) - .buttonStyle(RoundedButtonStyle(enabled: !requestInFlight)) - .padding(.top, 24) - - Button(UserText.networkProtectionWaitlistButtonExistingInviteCode, action: { - action(.custom(.openNetworkProtectionInviteCodeScreen)) - }) - .buttonStyle(RoundedButtonStyle(enabled: true, style: .bordered)) - .padding(.top, 8) - - if requestInFlight { - HStack { - Text(UserText.waitlistJoining) - .daxSubheadRegular() - .foregroundColor(.waitlistTextSecondary) - - ActivityIndicator(style: .medium) - } - .padding(.top, 14) - } - Spacer() - Text(UserText.networkProtectionWaitlistAvailabilityDisclaimer) - .font(.footnote) - .foregroundStyle(Color.secondary) - .padding(.top, 24) - } - .padding([.leading, .trailing], 24) - .frame(minHeight: proxy.size.height) - } - } - } - -} - -@available(iOS 15.0, *) -struct VPNWaitlistSignUpView_Previews: PreviewProvider { - static var previews: some View { - VPNWaitlistSignUpView(requestInFlight: false) { _ in } - } -} - -// MARK: - Joined Waitlist Views - -@available(iOS 15.0, *) -struct VPNWaitlistJoinedWaitlistView: View { - - let notificationState: WaitlistViewModel.NotificationPermissionState - - let action: (WaitlistViewModel.ViewAction) -> Void - - var body: some View { - VStack(spacing: 16) { - HeaderView(imageName: "JoinedVPNWaitlist", title: UserText.networkProtectionWaitlistJoinedTitle) - - switch notificationState { - case .notificationAllowed: - Text(UserText.networkProtectionWaitlistJoinedWithNotificationsSubtitle1) - .daxBodyRegular() - .foregroundColor(.waitlistTextSecondary) - .lineSpacing(6) - - Text(UserText.networkProtectionWaitlistJoinedWithNotificationsSubtitle2) - .daxBodyRegular() - .foregroundColor(.waitlistTextSecondary) - .lineSpacing(6) - - default: - Text(UserText.networkProtectionWaitlistJoinedWithNotificationsSubtitle1) - .daxBodyRegular() - .foregroundColor(.waitlistTextSecondary) - .lineSpacing(6) - - if notificationState == .notificationsDisabled { - AllowNotificationsView(action: action) - .padding(.top, 4) - } else { - Button(UserText.waitlistNotifyMe) { - action(.requestNotificationPermission) - } - .buttonStyle(RoundedButtonStyle(enabled: true)) - .padding(.top, 32) - } - } - - Spacer() - } - .padding([.leading, .trailing], 24) - .multilineTextAlignment(.center) - } - -} - -@available(iOS 15.0, *) -private struct AllowNotificationsView: View { - - let action: (WaitlistViewModel.ViewAction) -> Void - - var body: some View { - - VStack(spacing: 20) { - - Text(UserText.waitlistNotificationDisabled) - .daxBodyRegular() - .foregroundColor(.waitlistTextSecondary) - .fixMultilineScrollableText() - .lineSpacing(5) - - - Button { - action(.openNotificationSettings) - } label: { - HStack { - Image("Bell-16") - Text(UserText.waitlistAllowNotifications) - .daxButton() - } - } - .buttonStyle(RoundedButtonStyle(enabled: true)) - } - .padding(20) - .background(Color.waitlistNotificationBackground) - .cornerRadius(8) - .shadow(color: .black.opacity(0.05), radius: 12, x: 0, y: 4) - } - -} - -// MARK: - Invite Available Views - -private struct ShareButtonFramePreferenceKey: PreferenceKey { - static var defaultValue: CGRect = .zero - static func reduce(value: inout CGRect, nextValue: () -> CGRect) {} -} - -@available(iOS 15.0, *) -struct VPNWaitlistInvitedView: View { - - let benefitsList: [VPNWaitlistBenefit] = [ - .init( - imageName: "Shield-24", - title: UserText.networkProtectionWaitlistInvitedSection1Title, - subtitle: UserText.networkProtectionWaitlistInvitedSection1Subtitle - ), - .init( - imageName: "Rocket-24", - title: UserText.networkProtectionWaitlistInvitedSection2Title, - subtitle: UserText.networkProtectionWaitlistInvitedSection2Subtitle - ), - .init( - imageName: "Card-24", - title: UserText.networkProtectionWaitlistInvitedSection3Title, - subtitle: UserText.networkProtectionWaitlistInvitedSection3Subtitle - ), - ] - - let action: WaitlistViewActionHandler - - @State private var shareButtonFrame: CGRect = .zero - - var body: some View { - GeometryReader { proxy in - ScrollView { - VStack(alignment: .center, spacing: 0) { - HeaderView( - imageName: "InvitedVPNWaitlist", - title: UserText.networkProtectionWaitlistInvitedTitle - ) - - Text(UserText.networkProtectionWaitlistInvitedSubtitle) - .daxBodyRegular() - .foregroundColor(.waitlistTextSecondary) - .padding(.top, 16) - .lineSpacing(6) - .fixedSize(horizontal: false, vertical: true) - - VStack(spacing: 16.0) { - ForEach(benefitsList) { WaitlistListEntryView(viewData: $0) } - } - .padding(.top, 24) - - Button(UserText.networkProtectionWaitlistGetStarted, action: { action(.custom(.openNetworkProtectionPrivacyPolicyScreen)) }) - .buttonStyle(RoundedButtonStyle(enabled: true)) - .padding(.top, 32) - Spacer() - Text(UserText.networkProtectionWaitlistAvailabilityDisclaimer) - .font(.footnote) - .foregroundStyle(Color(designSystemColor: .textSecondary)) - .padding(.top, 24) - } - .frame(maxWidth: .infinity, minHeight: proxy.size.height) - .padding([.leading, .trailing], 18) - .multilineTextAlignment(.center) - } - } - } -} - -@available(iOS 15.0, *) -struct VPNWaitlistPrivacyPolicyView: View { - - let action: WaitlistViewActionHandler - - var body: some View { - ScrollView { - VStack(alignment: .leading, spacing: 0) { - Text(UserText.networkProtectionPrivacyPolicyTitle) - .font(.system(size: 17, weight: .bold)) - .multilineTextAlignment(.leading) - - Group { - Text(VPNWaitlistUserText.networkProtectionPrivacyPolicySection1Title).titleStyle() - - Text(.init(VPNWaitlistUserText.networkProtectionPrivacyPolicySection1ListMarkdown)).bodyStyle() - Text(VPNWaitlistUserText.networkProtectionPrivacyPolicySection2Title).titleStyle() - Text(VPNWaitlistUserText.networkProtectionPrivacyPolicySection2List).bodyStyle() - Text(VPNWaitlistUserText.networkProtectionPrivacyPolicySection3Title).titleStyle() - Text(VPNWaitlistUserText.networkProtectionPrivacyPolicySection3List).bodyStyle() - Text(VPNWaitlistUserText.networkProtectionPrivacyPolicySection4Title).titleStyle() - Text(VPNWaitlistUserText.networkProtectionPrivacyPolicySection4List).bodyStyle() - Text(VPNWaitlistUserText.networkProtectionPrivacyPolicySection5Title).titleStyle() - Text(VPNWaitlistUserText.networkProtectionPrivacyPolicySection5List).bodyStyle() - } - - Text(VPNWaitlistUserText.networkProtectionTermsOfServiceTitle) - .font(.system(size: 17, weight: .bold)) - .multilineTextAlignment(.leading) - .padding(.top, 28) - .padding(.bottom, 14) - - Group { - Text(VPNWaitlistUserText.networkProtectionTermsOfServiceSection1Title).titleStyle(topPadding: 0) - Text(VPNWaitlistUserText.networkProtectionTermsOfServiceSection1List).bodyStyle() - Text(VPNWaitlistUserText.networkProtectionTermsOfServiceSection2Title).titleStyle() - Text(.init(VPNWaitlistUserText.networkProtectionTermsOfServiceSection2ListMarkdown)).bodyStyle() - Text(VPNWaitlistUserText.networkProtectionTermsOfServiceSection3Title).titleStyle() - Text(VPNWaitlistUserText.networkProtectionTermsOfServiceSection3List).bodyStyle() - Text(VPNWaitlistUserText.networkProtectionTermsOfServiceSection4Title).titleStyle() - Text(VPNWaitlistUserText.networkProtectionTermsOfServiceSection4List).bodyStyle() - Text(VPNWaitlistUserText.networkProtectionTermsOfServiceSection5Title).titleStyle() - Text(VPNWaitlistUserText.networkProtectionTermsOfServiceSection5List).bodyStyle() - } - - Group { - Text(VPNWaitlistUserText.networkProtectionTermsOfServiceSection6Title).titleStyle() - Text(VPNWaitlistUserText.networkProtectionTermsOfServiceSection6List).bodyStyle() - Text(VPNWaitlistUserText.networkProtectionTermsOfServiceSection7Title).titleStyle() - Text(VPNWaitlistUserText.networkProtectionTermsOfServiceSection7List).bodyStyle() - Text(VPNWaitlistUserText.networkProtectionTermsOfServiceSection8Title).titleStyle() - Text(VPNWaitlistUserText.networkProtectionTermsOfServiceSection8List).bodyStyle() - } - - Button(UserText.networkProtectionWaitlistAgreeAndContinue, action: { action(.custom(.acceptNetworkProtectionTerms)) }) - .buttonStyle(RoundedButtonStyle(enabled: true)) - .padding(.top, 24) - } - .padding(.all, 20) - } - } - -} - -struct VPNWaitlistBenefit: Identifiable { - let id = UUID() - let imageName: String - let title: String - let subtitle: String -} - -private struct WaitlistListEntryView: View { - - let viewData: VPNWaitlistBenefit - - var body: some View { - HStack(alignment: .center, spacing: 16) { - Image(viewData.imageName) - - VStack(alignment: .leading, spacing: 2) { - Text(viewData.title) - .font(.system(size: 13, weight: .bold)) - .opacity(0.8) - .multilineTextAlignment(.leading) - .frame(maxWidth: .infinity, alignment: .leading) - - Text(viewData.subtitle) - .font(.system(size: 13)) - .opacity(0.6) - .multilineTextAlignment(.leading) - .frame(maxWidth: .infinity, alignment: .leading) - } - .frame(maxWidth: .infinity) - - Spacer() - } - } - -} - -private extension Text { - - func titleStyle(topPadding: CGFloat = 24, bottomPadding: CGFloat = 14) -> some View { - self - .font(.system(size: 13, weight: .bold)) - .multilineTextAlignment(.leading) - .padding(.top, topPadding) - .padding(.bottom, bottomPadding) - } - - func bodyStyle() -> some View { - self - .font(.system(size: 13)) - } - -} - -#endif diff --git a/DuckDuckGo/VPNWaitlistViewController.swift b/DuckDuckGo/VPNWaitlistViewController.swift deleted file mode 100644 index c1b6c3aa87..0000000000 --- a/DuckDuckGo/VPNWaitlistViewController.swift +++ /dev/null @@ -1,152 +0,0 @@ -// -// VPNWaitlistViewController.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. -// - -#if NETWORK_PROTECTION - -import UIKit -import SwiftUI -import Core -import Waitlist - -@available(iOS 15, *) -final class VPNWaitlistViewController: UIViewController { - - private let viewModel: WaitlistViewModel - - override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { - self.viewModel = WaitlistViewModel(waitlist: VPNWaitlist.shared) - super.init(nibName: nil, bundle: nil) - self.viewModel.delegate = self - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - title = UserText.netPNavTitle - - addHostingControllerToViewHierarchy() - - NotificationCenter.default.addObserver(self, - selector: #selector(updateViewState), - name: UIApplication.didBecomeActiveNotification, - object: nil) - - NotificationCenter.default.addObserver(self, - selector: #selector(updateViewState), - name: WaitlistKeychainStore.inviteCodeDidChangeNotification, - object: VPNWaitlist.identifier) - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - Task { - await self.viewModel.updateViewState() - } - } - - @objc - private func updateViewState() { - Task { - await self.viewModel.updateViewState() - } - } - - private func addHostingControllerToViewHierarchy() { - let waitlistView = VPNWaitlistView().environmentObject(viewModel) - let waitlistViewController = UIHostingController(rootView: waitlistView) - waitlistViewController.view.backgroundColor = UIColor(designSystemColor: .background) - - addChild(waitlistViewController) - waitlistViewController.view.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(waitlistViewController.view) - waitlistViewController.didMove(toParent: self) - - NSLayoutConstraint.activate([ - waitlistViewController.view.widthAnchor.constraint(equalTo: view.widthAnchor), - waitlistViewController.view.heightAnchor.constraint(equalTo: view.heightAnchor), - waitlistViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), - waitlistViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor) - ]) - } - -} - -@available(iOS 15.0, *) -extension VPNWaitlistViewController: WaitlistViewModelDelegate { - - func waitlistViewModelDidAskToReceiveJoinedNotification(_ viewModel: WaitlistViewModel) async -> Bool { - return await withCheckedContinuation { continuation in - let alertController = UIAlertController(title: UserText.networkProtectionNotificationPromptTitle, - message: UserText.networkProtectionNotificationPromptDescription, - preferredStyle: .alert) - - alertController.addAction(title: UserText.waitlistNoThanks) { - continuation.resume(returning: false) - } - let notifyMeAction = UIAlertAction(title: UserText.waitlistNotifyMe, style: .default) { _ in - continuation.resume(returning: true) - } - - alertController.addAction(notifyMeAction) - alertController.preferredAction = notifyMeAction - - present(alertController, animated: true) - } - } - - func waitlistViewModelDidJoinQueueWithNotificationsAllowed(_ viewModel: WaitlistViewModel) { - // no-op - } - - func waitlistViewModel(_ viewModel: WaitlistViewModel, didTriggerCustomAction action: WaitlistViewModel.ViewCustomAction) { - if action == .openNetworkProtectionInviteCodeScreen { - let networkProtectionViewController = NetworkProtectionRootViewController { [weak self] in - guard let self = self, let rootViewController = self.navigationController?.viewControllers.first else { - assertionFailure("Failed to show NetP status view") - return - } - - let networkProtectionRootViewController = NetworkProtectionRootViewController() - self.navigationController?.setViewControllers([rootViewController, networkProtectionRootViewController], animated: true) - } - - self.navigationController?.pushViewController(networkProtectionViewController, animated: true) - } - - if action == .openNetworkProtectionPrivacyPolicyScreen { - let termsAndConditionsViewController = VPNWaitlistTermsAndConditionsViewController() - self.navigationController?.pushViewController(termsAndConditionsViewController, animated: true) - } - } - - func waitlistViewModelDidOpenInviteCodeShareSheet(_ viewModel: WaitlistViewModel, inviteCode: String, senderFrame: CGRect) { - // The VPN waitlist doesn't support the share sheet - } - - func waitlistViewModelDidOpenDownloadURLShareSheet(_ viewModel: WaitlistViewModel, senderFrame: CGRect) { - // The VPN waitlist doesn't support the share sheet - } - -} - -#endif diff --git a/DuckDuckGo/bg.lproj/Autocomplete.strings b/DuckDuckGo/bg.lproj/Autocomplete.strings deleted file mode 100644 index c13c1eb60b..0000000000 --- a/DuckDuckGo/bg.lproj/Autocomplete.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Class = "UILabel"; text = "Label"; ObjectID = "5ag-a1-mlA"; */ -"5ag-a1-mlA.text" = "Етикет"; - -/* Class = "UILabel"; text = "Label"; ObjectID = "eiv-Cq-E2r"; */ -"eiv-Cq-E2r.text" = "Етикет"; - -/* Class = "UILabel"; text = "No Suggestions"; ObjectID = "gPe-Cv-14j"; */ -"gPe-Cv-14j.text" = "Няма предложения"; - -/* Class = "UIButton"; accessibilityLabel = "Autocomplete suggestion"; ObjectID = "N97-eI-Lps"; */ -"N97-eI-Lps.accessibilityLabel" = "Предложение за автоматично довършване"; - diff --git a/DuckDuckGo/bg.lproj/Localizable.strings b/DuckDuckGo/bg.lproj/Localizable.strings index 6491f4658a..39c35e8312 100644 --- a/DuckDuckGo/bg.lproj/Localizable.strings +++ b/DuckDuckGo/bg.lproj/Localizable.strings @@ -119,7 +119,7 @@ "addWidget.description" = "Получете бърз достъп до поверително търсене и до любимите си сайтове."; /* No comment provided by engineer. */ -"addWidget.settings.firstParagraph" = "Натиснете продължително началния екран, за да влезете в режим за пренареждане."; +"addWidget.settings.firstParagraph" = "Long-press on the Home Screen to enter jiggle mode."; /* Replacement string is a plus button icon. */ "addWidget.settings.secondParagraph.%@" = "Натиснете бутона плюс %@."; @@ -133,15 +133,48 @@ /* No comment provided by engineer. */ "alert.message.bookmarkAll" = "Съществуващите отметки няма да бъдат дублирани."; +/* Description for alert shown when sync error occurs because of bad data */ +"alert.sync-bad-data-error-description" = "Някои отметки или пароли са форматирани неправилно или са твърде дълги и не бяха синхронизирани."; + +/* Description for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-description" = "Достигнахте максималния брой отметки. Изтрийте някои отметки, за да възобновите синхронизирането."; + +/* Title for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-title" = "Синхронизирането на отметки е на пауза"; + +/* Description for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-description" = "Достигнахте максималния брой пароли. Изтрийте някои пароли, за да възобновите синхронизирането."; + +/* Title for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-title" = "Синхронизирането на пароли е на пауза"; + /* Title for sync error alert */ "alert.sync-error" = "Грешка при синхронизиране и архивиране"; +/* Sync error alert action button title, takes the user to the sync settings page. */ +"alert.sync-error-action" = "Настройки за синхронизиране"; + +/* Description for alert shown when user logged off from sync */ +"alert.sync-invalid-login-error-description" = "Синхронизирането е на пауза. Ако искате да продължите със синхронизирането на това устройство, свържете се отново с помощта на друго устройство или със своя код за възстановяване."; + /* Learn more button in alert */ "alert.sync-paused-alert-learn-more-button" = "Научете повече"; /* Confirmation button in alert */ "alert.sync-paused-alert-ok-button" = "ОК"; +/* Title for alert shown when sync paused for an error */ +"alert.sync-paused-title" = "Синхронизирането е на пауза"; + +/* Description for alert shown when sync error occurs because of too many requests */ +"alert.sync-too-many-requests-error-description" = "Sync & Backup временно не е налична."; + +/* Title of the warning message that tells the user that there was an error with the sync feature. */ +"alert.sync.warning.sync-error" = "Грешка при синхронизиране"; + +/* Title of the warning message */ +"alert.sync.warning.sync-paused" = "Функцията за синхронизиране и архивиране е на пауза"; + /* Question from confirmation dialog */ "alert.title.bookmarkAll" = "Поставяне на всички раздели в отметки?"; @@ -208,6 +241,15 @@ /* No comment provided by engineer. */ "autoclear.on" = "Включено"; +/* Subtitle for search history items */ +"autocomplete.history.search.duckduckgo" = "Търси с DuckDuckGo"; + +/* The message text shown in suggestions */ +"autocomplete.history.warning.message" = "Предложенията вече включват наскоро посетени сайтове. Те се съхраняват на вашето устройство и никога на сървърите на DuckDuckGo. Изключете ги в настройките, а можете и да ги изтриете по всяко време с 🔥 Fire Button."; + +/* Title for message show in suggestions */ +"autocomplete.history.warning.title" = "Същата поверителност.\nПо-добри предложения при търсене!"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Деактивирано"; @@ -1147,6 +1189,9 @@ /* Title for the get desktop browser feature */ "get.browser.title" = "Вземете DuckDuckGo за Mac или Windows"; +/* No comment provided by engineer. */ +"Help us improve!" = "Help us improve!"; + /* No comment provided by engineer. */ "home.row.onboarding.header" = "Добавете DuckDuckGo към началния си екран!"; @@ -1603,6 +1648,26 @@ /* Deactivate button */ "pm.deactivate" = "Deactivate"; +/* Button title for sync bookmarks limits exceeded warning to go to manage bookmarks */ +"prefrences.sync.bookmarks-limit-exceeded-action" = "Управление на отметки"; + +/* Description for sync bookmarks limits exceeded warning */ +"prefrences.sync.bookmarks-limit-exceeded-description" = "Достигнахте максималния брой отметки. Изтрийте някои от тях, за да възобновите синхронизирането."; + +/* Button title for sync credentials limits exceeded warning to go to manage passwords */ +"prefrences.sync.credentials-limit-exceeded-action" = "Управление на пароли…"; + +/* Description for sync credentials limits exceeded warning */ +"prefrences.sync.credentials-limit-exceeded-description" = "Достигнахте максималния брой пароли. Изтрийте някои от тях, за да възобновите синхронизирането."; + +/* Description invalid credentials error when syncing. + Description of incorrectly formatted data error when syncing. + Description of too many requests error when syncing. */ +"prefrences.sync.invalid-login-description" = "Грешка при синхронизирането. Опитайте да деактивирате синхронизирането на това устройство и след това се свържете отново чрез друго устройство или със своя код за възстановяване."; + +/* Title for sync limits exceeded warning */ +"prefrences.sync.limit-exceeded-title" = "Синхронизирането е на пауза"; + /* No comment provided by engineer. */ "preserveLogins.domain.list.footer" = "Уеб сайтовете разчитат на бисквитки, за да поддържат сесията ви активна. Когато направите сайта огнеустойчив, бисквитките няма да бъдат изтрити и ще останете вписани, дори след като използвате огнения бутон. Ние все пак блокираме тракери на трети страни, които се намират на огнеустойчиви сайтове."; @@ -1712,6 +1777,9 @@ /* Settings screen cell for autocomplete */ "settings.autocomplete" = "Показване на предложенията за автоматично довършване"; +/* Settings label for enabling or disabling recently visited sites */ +"settings.autocomplete.recentlyvisited" = "Наскоро посетени сайтове"; + /* Settings screen cell text for Application Lock */ "settings.autolock" = "Заключване на приложение"; @@ -2241,6 +2309,9 @@ /* Tab Switcher Accessibility Label */ "tab.switcher.accessibility.label" = "Превключване на раздели"; +/* No comment provided by engineer. */ +"Take Survey" = "Take Survey"; + /* Description text for the text size adjustment setting */ "textSize.description" = "Изберете предпочитания размер на текста. Уебсайтът, който разглеждате в DuckDuckGo, ще се регулира спрямо него."; @@ -2319,6 +2390,9 @@ /* Alert title for the alert when the Privacy Pro subscription expires */ "vpn.access-revoked.alert.title" = "VPN disconnected due to expired subscription"; +/* Title for the VPN widget onboarding screen */ +"vpn.addWidget.settings.title" = "Find and select DuckDuckGo. Then swipe to VPN and select Add Widget."; + /* Title for the Cancel button of the VPN feedback form */ "vpn.feedback-form.button.cancel" = "Cancel"; @@ -2388,6 +2462,9 @@ /* Title for each screen of the VPN feedback form */ "vpn.feedback-form.title" = "Help Improve the DuckDuckGo VPN"; +/* VPN settings screen cell text for adding the VPN widget to the home screen */ +"vpn.settings.add.widget" = "Add VPN Widget to Home Screen"; + /* Title for the button to enable push notifications in system settings */ "waitlist.allow-notifications" = "Разрешаване на известия"; @@ -2439,6 +2516,9 @@ /* Title for the share sheet entry */ "waitlist.share-sheet.title" = "Имате покана!"; +/* No comment provided by engineer. */ +"We want to make using passwords in DuckDuckGo better." = "We want to make using passwords in DuckDuckGo better."; + /* Confirmation message */ "web.url.remove.favorite.done" = "Премахнато от любими"; diff --git a/DuckDuckGo/cs.lproj/Autocomplete.strings b/DuckDuckGo/cs.lproj/Autocomplete.strings deleted file mode 100644 index ba2a8f82ed..0000000000 --- a/DuckDuckGo/cs.lproj/Autocomplete.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Class = "UILabel"; text = "Label"; ObjectID = "5ag-a1-mlA"; */ -"5ag-a1-mlA.text" = "Štítek"; - -/* Class = "UILabel"; text = "Label"; ObjectID = "eiv-Cq-E2r"; */ -"eiv-Cq-E2r.text" = "Štítek"; - -/* Class = "UILabel"; text = "No Suggestions"; ObjectID = "gPe-Cv-14j"; */ -"gPe-Cv-14j.text" = "Žádné návrhy"; - -/* Class = "UIButton"; accessibilityLabel = "Autocomplete suggestion"; ObjectID = "N97-eI-Lps"; */ -"N97-eI-Lps.accessibilityLabel" = "Použít návrh automatického doplňování"; - diff --git a/DuckDuckGo/cs.lproj/Localizable.strings b/DuckDuckGo/cs.lproj/Localizable.strings index c08fcd1648..bf8393a93b 100644 --- a/DuckDuckGo/cs.lproj/Localizable.strings +++ b/DuckDuckGo/cs.lproj/Localizable.strings @@ -119,7 +119,7 @@ "addWidget.description" = "Získejte rychlý přístup k soukromému vyhledávání a oblíbeným webům."; /* No comment provided by engineer. */ -"addWidget.settings.firstParagraph" = "Podržením prstu na ploše aktivuj režim úprav plochy."; +"addWidget.settings.firstParagraph" = "Long-press on the Home Screen to enter jiggle mode."; /* Replacement string is a plus button icon. */ "addWidget.settings.secondParagraph.%@" = "Klepni na tlačítko plus %@."; @@ -133,15 +133,48 @@ /* No comment provided by engineer. */ "alert.message.bookmarkAll" = "Existující záložky nebudou duplikovány."; +/* Description for alert shown when sync error occurs because of bad data */ +"alert.sync-bad-data-error-description" = "Některé záložky nebo hesla jsou nesprávně naformátované nebo příliš dlouhé, takže jsme je nemohli synchronizovat."; + +/* Description for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-description" = "Už máš maximální počet záložek. Jestli chceš synchronizaci obnovit, některé záložky smaž."; + +/* Title for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-title" = "Synchronizace záložek je pozastavená"; + +/* Description for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-description" = "Už máš maximální počet hesel. Jestli chceš synchronizaci obnovit, některá hesla smaž."; + +/* Title for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-title" = "Synchronizace hesel je pozastavená"; + /* Title for sync error alert */ "alert.sync-error" = "Chyba synchronizace a zálohování"; +/* Sync error alert action button title, takes the user to the sync settings page. */ +"alert.sync-error-action" = "Nastavení synchronizace"; + +/* Description for alert shown when user logged off from sync */ +"alert.sync-invalid-login-error-description" = "Synchronizace je pozastavená. Pokud chceš toto zařízení dál synchronizovat, znovu se připoj pomocí jiného zařízení nebo kódu pro obnovení."; + /* Learn more button in alert */ "alert.sync-paused-alert-learn-more-button" = "Více informací"; /* Confirmation button in alert */ "alert.sync-paused-alert-ok-button" = "DOBŘE"; +/* Title for alert shown when sync paused for an error */ +"alert.sync-paused-title" = "Synchronizace je pozastavená"; + +/* Description for alert shown when sync error occurs because of too many requests */ +"alert.sync-too-many-requests-error-description" = "Funkce Sync & Backup je dočasně nedostupná."; + +/* Title of the warning message that tells the user that there was an error with the sync feature. */ +"alert.sync.warning.sync-error" = "Chyba synchronizace"; + +/* Title of the warning message */ +"alert.sync.warning.sync-paused" = "Funkce synchronizace a zálohování je pozastavená"; + /* Question from confirmation dialog */ "alert.title.bookmarkAll" = "Dát do záložek všechny karty?"; @@ -208,6 +241,15 @@ /* No comment provided by engineer. */ "autoclear.on" = "Aktivní"; +/* Subtitle for search history items */ +"autocomplete.history.search.duckduckgo" = "Vyhledat prostřednictvím DuckDuckGo"; + +/* The message text shown in suggestions */ +"autocomplete.history.warning.message" = "Návrhy teď zahrnují poslední navštívené stránky. Ty se ukládají přímo v zařízení a nikdy na serverech DuckDuckGo. Vypnout se dají v nastaveních nebo je můžeš kdykoli vymazat tlačítkem 🔥 Fire Button."; + +/* Title for message show in suggestions */ +"autocomplete.history.warning.title" = "Stejné soukromí.\nLepší návrhy pro vyhledávání!"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Zakázáno"; @@ -1147,6 +1189,9 @@ /* Title for the get desktop browser feature */ "get.browser.title" = "Nainstaluj si DuckDuckGo pro Mac nebo Windows"; +/* No comment provided by engineer. */ +"Help us improve!" = "Help us improve!"; + /* No comment provided by engineer. */ "home.row.onboarding.header" = "Přidejte DuckDuckGo na svou domovskou obrazovku!"; @@ -1603,6 +1648,26 @@ /* Deactivate button */ "pm.deactivate" = "Deactivate"; +/* Button title for sync bookmarks limits exceeded warning to go to manage bookmarks */ +"prefrences.sync.bookmarks-limit-exceeded-action" = "Spravovat záložky"; + +/* Description for sync bookmarks limits exceeded warning */ +"prefrences.sync.bookmarks-limit-exceeded-description" = "Už máš maximální počet záložek. Jestli chceš synchronizaci obnovit, některé smaž."; + +/* Button title for sync credentials limits exceeded warning to go to manage passwords */ +"prefrences.sync.credentials-limit-exceeded-action" = "Spravovat hesla…"; + +/* Description for sync credentials limits exceeded warning */ +"prefrences.sync.credentials-limit-exceeded-description" = "Už máš maximální počet hesel. Jestli chceš synchronizaci obnovit, některá smaž."; + +/* Description invalid credentials error when syncing. + Description of incorrectly formatted data error when syncing. + Description of too many requests error when syncing. */ +"prefrences.sync.invalid-login-description" = "Při synchronizaci došlo k chybě. Zkus na tomto zařízení synchronizaci vypnout a pak se znovu připoj pomocí jiného zařízení nebo kódu pro obnovení."; + +/* Title for sync limits exceeded warning */ +"prefrences.sync.limit-exceeded-title" = "Synchronizace je pozastavená"; + /* No comment provided by engineer. */ "preserveLogins.domain.list.footer" = "Webové stránky spoléhají na soubory cookie, aby vás udržely přihlášené. Když na webu zapnete ochranu, soubory cookie se nevymažou a zůstanete přihlášeni, a to i po použití tlačítka pro mazání. Stále přitom blokujeme sledovací programy třetích stran nalezené na webových stránkách s ochranou."; @@ -1712,6 +1777,9 @@ /* Settings screen cell for autocomplete */ "settings.autocomplete" = "Zobrazit návrhy automatického doplňování"; +/* Settings label for enabling or disabling recently visited sites */ +"settings.autocomplete.recentlyvisited" = "Nedávno navštívené stránky"; + /* Settings screen cell text for Application Lock */ "settings.autolock" = "Zámek aplikace"; @@ -2241,6 +2309,9 @@ /* Tab Switcher Accessibility Label */ "tab.switcher.accessibility.label" = "Přepínač karet"; +/* No comment provided by engineer. */ +"Take Survey" = "Take Survey"; + /* Description text for the text size adjustment setting */ "textSize.description" = "Vyberte preferovanou velikost textu. Webové stránky, které zobrazíte v DuckDuckGo, se tomu přizpůsobí."; @@ -2319,6 +2390,9 @@ /* Alert title for the alert when the Privacy Pro subscription expires */ "vpn.access-revoked.alert.title" = "VPN disconnected due to expired subscription"; +/* Title for the VPN widget onboarding screen */ +"vpn.addWidget.settings.title" = "Find and select DuckDuckGo. Then swipe to VPN and select Add Widget."; + /* Title for the Cancel button of the VPN feedback form */ "vpn.feedback-form.button.cancel" = "Cancel"; @@ -2388,6 +2462,9 @@ /* Title for each screen of the VPN feedback form */ "vpn.feedback-form.title" = "Help Improve the DuckDuckGo VPN"; +/* VPN settings screen cell text for adding the VPN widget to the home screen */ +"vpn.settings.add.widget" = "Add VPN Widget to Home Screen"; + /* Title for the button to enable push notifications in system settings */ "waitlist.allow-notifications" = "Povolit oznámení"; @@ -2439,6 +2516,9 @@ /* Title for the share sheet entry */ "waitlist.share-sheet.title" = "Zveme tě!"; +/* No comment provided by engineer. */ +"We want to make using passwords in DuckDuckGo better." = "We want to make using passwords in DuckDuckGo better."; + /* Confirmation message */ "web.url.remove.favorite.done" = "Oblíbená položka odstraněna"; diff --git a/DuckDuckGo/da.lproj/Autocomplete.strings b/DuckDuckGo/da.lproj/Autocomplete.strings deleted file mode 100644 index 3ddfbc3908..0000000000 --- a/DuckDuckGo/da.lproj/Autocomplete.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Class = "UILabel"; text = "Label"; ObjectID = "5ag-a1-mlA"; */ -"5ag-a1-mlA.text" = "Etiket"; - -/* Class = "UILabel"; text = "Label"; ObjectID = "eiv-Cq-E2r"; */ -"eiv-Cq-E2r.text" = "Etiket"; - -/* Class = "UILabel"; text = "No Suggestions"; ObjectID = "gPe-Cv-14j"; */ -"gPe-Cv-14j.text" = "Ingen forslag"; - -/* Class = "UIButton"; accessibilityLabel = "Autocomplete suggestion"; ObjectID = "N97-eI-Lps"; */ -"N97-eI-Lps.accessibilityLabel" = "Autoudfyld forslaget"; - diff --git a/DuckDuckGo/da.lproj/Localizable.strings b/DuckDuckGo/da.lproj/Localizable.strings index 6d9268bf86..d732b3bb36 100644 --- a/DuckDuckGo/da.lproj/Localizable.strings +++ b/DuckDuckGo/da.lproj/Localizable.strings @@ -119,7 +119,7 @@ "addWidget.description" = "Få hurtig adgang til privat søgning og de websteder, du elsker."; /* No comment provided by engineer. */ -"addWidget.settings.firstParagraph" = "Tryk længe på startskærmen for at skifte til jiggle-tilstand."; +"addWidget.settings.firstParagraph" = "Long-press on the Home Screen to enter jiggle mode."; /* Replacement string is a plus button icon. */ "addWidget.settings.secondParagraph.%@" = "Tryk på %@-knappen."; @@ -133,15 +133,48 @@ /* No comment provided by engineer. */ "alert.message.bookmarkAll" = "Eksisterende bogmærker duplikeres ikke."; +/* Description for alert shown when sync error occurs because of bad data */ +"alert.sync-bad-data-error-description" = "Nogle bogmærker eller adgangskoder er formateret forkert eller er for lange og blev ikke synkroniseret."; + +/* Description for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-description" = "Du har nået det maksimale antal bogmærker. Slet nogle bogmærker for at genoptage synkroniseringen."; + +/* Title for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-title" = "Bogmærkesynkronisering er sat på pause"; + +/* Description for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-description" = "Du har nået det maksimale antal adgangskoder. Slet nogle adgangskoder for at genoptage synkroniseringen."; + +/* Title for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-title" = "Synkronisering af adgangskoder er sat på pause"; + /* Title for sync error alert */ "alert.sync-error" = "Fejl ved synkronisering og sikkerhedskopiering"; +/* Sync error alert action button title, takes the user to the sync settings page. */ +"alert.sync-error-action" = "Synkroniseringsindstillinger"; + +/* Description for alert shown when user logged off from sync */ +"alert.sync-invalid-login-error-description" = "Synkroniseringen er blevet sat på pause. Hvis du vil fortsætte med at synkronisere denne enhed, skal du oprette forbindelse igen ved hjælp af en anden enhed eller din gendannelseskode."; + /* Learn more button in alert */ "alert.sync-paused-alert-learn-more-button" = "Mere info"; /* Confirmation button in alert */ "alert.sync-paused-alert-ok-button" = "Okay"; +/* Title for alert shown when sync paused for an error */ +"alert.sync-paused-title" = "Synkronisering er sat på pause"; + +/* Description for alert shown when sync error occurs because of too many requests */ +"alert.sync-too-many-requests-error-description" = "Sync & Backup er midlertidigt utilgængelig."; + +/* Title of the warning message that tells the user that there was an error with the sync feature. */ +"alert.sync.warning.sync-error" = "Synkroniseringsfejl"; + +/* Title of the warning message */ +"alert.sync.warning.sync-paused" = "Synkronisering og sikkerhedskopiering er sat på pause"; + /* Question from confirmation dialog */ "alert.title.bookmarkAll" = "Bogmærk alle faner?"; @@ -208,6 +241,15 @@ /* No comment provided by engineer. */ "autoclear.on" = "Til"; +/* Subtitle for search history items */ +"autocomplete.history.search.duckduckgo" = "Søg med DuckDuckGo"; + +/* The message text shown in suggestions */ +"autocomplete.history.warning.message" = "Forslag inkluderer nu nyligt besøgte websteder. Disse gemmes på din enhed og aldrig på DuckDuckGos servere. Slå fra i indstillingerne, eller slet når som helst med 🔥 Fire Button."; + +/* Title for message show in suggestions */ +"autocomplete.history.warning.title" = "Samme fortrolighed.\nBedre søgeforslag!"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Deaktiveret"; @@ -1147,6 +1189,9 @@ /* Title for the get desktop browser feature */ "get.browser.title" = "Hent DuckDuckGo til Mac eller Windows"; +/* No comment provided by engineer. */ +"Help us improve!" = "Help us improve!"; + /* No comment provided by engineer. */ "home.row.onboarding.header" = "Føj DuckDuckGo til din startskærm!"; @@ -1603,6 +1648,26 @@ /* Deactivate button */ "pm.deactivate" = "Deactivate"; +/* Button title for sync bookmarks limits exceeded warning to go to manage bookmarks */ +"prefrences.sync.bookmarks-limit-exceeded-action" = "Administrer bogmærker"; + +/* Description for sync bookmarks limits exceeded warning */ +"prefrences.sync.bookmarks-limit-exceeded-description" = "Du har nået det maksimale antal bogmærker. Slet nogle af dem for at genoptage synkroniseringen."; + +/* Button title for sync credentials limits exceeded warning to go to manage passwords */ +"prefrences.sync.credentials-limit-exceeded-action" = "Administrer adgangskoder..."; + +/* Description for sync credentials limits exceeded warning */ +"prefrences.sync.credentials-limit-exceeded-description" = "Du har nået det maksimale antal adgangskoder. Slet nogle af dem for at genoptage synkroniseringen."; + +/* Description invalid credentials error when syncing. + Description of incorrectly formatted data error when syncing. + Description of too many requests error when syncing. */ +"prefrences.sync.invalid-login-description" = "Synkronisering stødte på en fejl. Prøv at deaktivere synkronisering på denne enhed, og opret derefter forbindelse igen ved hjælp af en anden enhed eller din gendannelseskode."; + +/* Title for sync limits exceeded warning */ +"prefrences.sync.limit-exceeded-title" = "Synkronisering sat på pause"; + /* No comment provided by engineer. */ "preserveLogins.domain.list.footer" = "Websider er afhængige af cookies for at bevare dig logget ind. Når du Brandsikrer et websted, slettes cookies ikke, og du forbliver logget ind, selv efter, at du har brugt Brand-knappen. Vi blokerer stadig tredjeparts-trackere, der findes på Brandsikre-websteder."; @@ -1712,6 +1777,9 @@ /* Settings screen cell for autocomplete */ "settings.autocomplete" = "Vis forslag til Autoudfyld"; +/* Settings label for enabling or disabling recently visited sites */ +"settings.autocomplete.recentlyvisited" = "Nyligt besøgte websteder"; + /* Settings screen cell text for Application Lock */ "settings.autolock" = "Applikationslås"; @@ -2241,6 +2309,9 @@ /* Tab Switcher Accessibility Label */ "tab.switcher.accessibility.label" = "Faneskifter"; +/* No comment provided by engineer. */ +"Take Survey" = "Take Survey"; + /* Description text for the text size adjustment setting */ "textSize.description" = "Vælg din foretrukne tekststørrelse. Websteder, som du ser i DuckDuckGo, tilpasser sig denne."; @@ -2319,6 +2390,9 @@ /* Alert title for the alert when the Privacy Pro subscription expires */ "vpn.access-revoked.alert.title" = "VPN disconnected due to expired subscription"; +/* Title for the VPN widget onboarding screen */ +"vpn.addWidget.settings.title" = "Find and select DuckDuckGo. Then swipe to VPN and select Add Widget."; + /* Title for the Cancel button of the VPN feedback form */ "vpn.feedback-form.button.cancel" = "Cancel"; @@ -2388,6 +2462,9 @@ /* Title for each screen of the VPN feedback form */ "vpn.feedback-form.title" = "Help Improve the DuckDuckGo VPN"; +/* VPN settings screen cell text for adding the VPN widget to the home screen */ +"vpn.settings.add.widget" = "Add VPN Widget to Home Screen"; + /* Title for the button to enable push notifications in system settings */ "waitlist.allow-notifications" = "Tillad meddelelser"; @@ -2439,6 +2516,9 @@ /* Title for the share sheet entry */ "waitlist.share-sheet.title" = "Du er inviteret!"; +/* No comment provided by engineer. */ +"We want to make using passwords in DuckDuckGo better." = "We want to make using passwords in DuckDuckGo better."; + /* Confirmation message */ "web.url.remove.favorite.done" = "Favorit fjernet"; diff --git a/DuckDuckGo/de.lproj/Autocomplete.strings b/DuckDuckGo/de.lproj/Autocomplete.strings deleted file mode 100644 index 4621e113a5..0000000000 --- a/DuckDuckGo/de.lproj/Autocomplete.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Class = "UILabel"; text = "Label"; ObjectID = "5ag-a1-mlA"; */ -"5ag-a1-mlA.text" = "Label"; - -/* Class = "UILabel"; text = "Label"; ObjectID = "eiv-Cq-E2r"; */ -"eiv-Cq-E2r.text" = "Label"; - -/* Class = "UILabel"; text = "No Suggestions"; ObjectID = "gPe-Cv-14j"; */ -"gPe-Cv-14j.text" = "Keine Vorschläge"; - -/* Class = "UIButton"; accessibilityLabel = "Autocomplete suggestion"; ObjectID = "N97-eI-Lps"; */ -"N97-eI-Lps.accessibilityLabel" = "Vorschläge für die Autovervollständigung"; - diff --git a/DuckDuckGo/de.lproj/Localizable.strings b/DuckDuckGo/de.lproj/Localizable.strings index 974b9508f3..add945f588 100644 --- a/DuckDuckGo/de.lproj/Localizable.strings +++ b/DuckDuckGo/de.lproj/Localizable.strings @@ -119,7 +119,7 @@ "addWidget.description" = "Erhalte schnellen Zugriff auf die private Suche und die Websites, die dir gefallen."; /* No comment provided by engineer. */ -"addWidget.settings.firstParagraph" = "Drücke lange auf den Startbildschirm, um in den Jiggle-Modus zu wechseln."; +"addWidget.settings.firstParagraph" = "Long-press on the Home Screen to enter jiggle mode."; /* Replacement string is a plus button icon. */ "addWidget.settings.secondParagraph.%@" = "Tippe auf die Plus-Schaltfläche %@."; @@ -133,15 +133,48 @@ /* No comment provided by engineer. */ "alert.message.bookmarkAll" = "Bestehende Lesezeichen werden nicht kopiert."; +/* Description for alert shown when sync error occurs because of bad data */ +"alert.sync-bad-data-error-description" = "Einige Lesezeichen oder Passwörter sind falsch formatiert oder zu lang und wurden nicht synchronisiert."; + +/* Description for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-description" = "Du hast die maximale Anzahl von Lesezeichen erreicht. Bitte lösche einige Lesezeichen, um die Synchronisierung fortzusetzen."; + +/* Title for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-title" = "Lesezeichen-Synchronisierung ist angehalten"; + +/* Description for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-description" = "Du hast die maximale Anzahl von Passwörtern erreicht. Lösche Passwörter, um die Synchronisierung fortzusetzen."; + +/* Title for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-title" = "Kennwortsynchronisierung ist angehalten"; + /* Title for sync error alert */ "alert.sync-error" = "Synchronisierungs- und Sicherungsfehler"; +/* Sync error alert action button title, takes the user to the sync settings page. */ +"alert.sync-error-action" = "Synchronisierungseinstellungen"; + +/* Description for alert shown when user logged off from sync */ +"alert.sync-invalid-login-error-description" = "Synchronisierung wurde angehalten. Wenn du die Synchronisierung dieses Geräts fortsetzen möchtest, stelle die Verbindung mit einem anderen Gerät oder deinem Wiederherstellungscode wieder her."; + /* Learn more button in alert */ "alert.sync-paused-alert-learn-more-button" = "Mehr erfahren"; /* Confirmation button in alert */ "alert.sync-paused-alert-ok-button" = "OK"; +/* Title for alert shown when sync paused for an error */ +"alert.sync-paused-title" = "Synchronisierung angehalten"; + +/* Description for alert shown when sync error occurs because of too many requests */ +"alert.sync-too-many-requests-error-description" = "Sync & Backup ist vorübergehend nicht verfügbar."; + +/* Title of the warning message that tells the user that there was an error with the sync feature. */ +"alert.sync.warning.sync-error" = "Synchronisierungsfehler"; + +/* Title of the warning message */ +"alert.sync.warning.sync-paused" = "Synchronisieren und sichern ist angehalten"; + /* Question from confirmation dialog */ "alert.title.bookmarkAll" = "Alle Tabs mit Lesezeichen versehen?"; @@ -208,6 +241,15 @@ /* No comment provided by engineer. */ "autoclear.on" = "An"; +/* Subtitle for search history items */ +"autocomplete.history.search.duckduckgo" = "Mit DuckDuckGo suchen"; + +/* The message text shown in suggestions */ +"autocomplete.history.warning.message" = "Vorschläge enthalten jetzt kürzlich besuchte Seiten. Diese werden auf deinem Gerät gespeichert, niemals auf den Servern von DuckDuckGo. Du kannst sie in den Einstellungen deaktivieren oder jederzeit mit dem 🔥 Fire Button löschen."; + +/* Title for message show in suggestions */ +"autocomplete.history.warning.title" = "Gleiche Privatsphäre.\nBessere Suchvorschläge!"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Deaktiviert"; @@ -1147,6 +1189,9 @@ /* Title for the get desktop browser feature */ "get.browser.title" = "Hole dir DuckDuckGo für Mac oder Windows"; +/* No comment provided by engineer. */ +"Help us improve!" = "Help us improve!"; + /* No comment provided by engineer. */ "home.row.onboarding.header" = "Füge DuckDuckGo zu deinem Startbildschirm hinzu."; @@ -1603,6 +1648,26 @@ /* Deactivate button */ "pm.deactivate" = "Deactivate"; +/* Button title for sync bookmarks limits exceeded warning to go to manage bookmarks */ +"prefrences.sync.bookmarks-limit-exceeded-action" = "Lesezeichen verwalten"; + +/* Description for sync bookmarks limits exceeded warning */ +"prefrences.sync.bookmarks-limit-exceeded-description" = "Du hast die maximale Anzahl von Lesezeichen erreicht. Lösche Lesezeichen, um die Synchronisierung fortzusetzen."; + +/* Button title for sync credentials limits exceeded warning to go to manage passwords */ +"prefrences.sync.credentials-limit-exceeded-action" = "Passwörter verwalten …"; + +/* Description for sync credentials limits exceeded warning */ +"prefrences.sync.credentials-limit-exceeded-description" = "Du hast die maximale Anzahl von Passwörtern erreicht. Lösche einige, um die Synchronisierung fortzusetzen."; + +/* Description invalid credentials error when syncing. + Description of incorrectly formatted data error when syncing. + Description of too many requests error when syncing. */ +"prefrences.sync.invalid-login-description" = "Bei der Synchronisierung ist ein Fehler aufgetreten. Versuche, die Synchronisierung auf diesem Gerät zu deaktivieren, und stelle dann die Verbindung mit einem anderen Gerät oder deinem Wiederherstellungscode wieder her."; + +/* Title for sync limits exceeded warning */ +"prefrences.sync.limit-exceeded-title" = "Synchronisierung angehalten"; + /* No comment provided by engineer. */ "preserveLogins.domain.list.footer" = "Websites sorgen mit Cookies dafür, dass du angemeldet bleibst. Wenn du eine Website feuerfest machst, werden Cookies nicht gelöscht und du bleibst angemeldet – auch nach Verwendung der Schaltfläche „Feuer“. Wir blockieren weiterhin auf feuerfest gemachten Websites gefundene Tracker von Dritten."; @@ -1712,6 +1777,9 @@ /* Settings screen cell for autocomplete */ "settings.autocomplete" = "Vorschläge für die Autovervollständigung"; +/* Settings label for enabling or disabling recently visited sites */ +"settings.autocomplete.recentlyvisited" = "Kürzlich besuchte Seiten"; + /* Settings screen cell text for Application Lock */ "settings.autolock" = "Anwendungssperre"; @@ -2241,6 +2309,9 @@ /* Tab Switcher Accessibility Label */ "tab.switcher.accessibility.label" = "Tabwechsel-Bildschirm"; +/* No comment provided by engineer. */ +"Take Survey" = "Take Survey"; + /* Description text for the text size adjustment setting */ "textSize.description" = "Wähle die gewünschte Textgröße aus. In DuckDuckGo angezeigte Websites werden sich daran anpassen."; @@ -2319,6 +2390,9 @@ /* Alert title for the alert when the Privacy Pro subscription expires */ "vpn.access-revoked.alert.title" = "VPN disconnected due to expired subscription"; +/* Title for the VPN widget onboarding screen */ +"vpn.addWidget.settings.title" = "Find and select DuckDuckGo. Then swipe to VPN and select Add Widget."; + /* Title for the Cancel button of the VPN feedback form */ "vpn.feedback-form.button.cancel" = "Cancel"; @@ -2388,6 +2462,9 @@ /* Title for each screen of the VPN feedback form */ "vpn.feedback-form.title" = "Help Improve the DuckDuckGo VPN"; +/* VPN settings screen cell text for adding the VPN widget to the home screen */ +"vpn.settings.add.widget" = "Add VPN Widget to Home Screen"; + /* Title for the button to enable push notifications in system settings */ "waitlist.allow-notifications" = "Benachrichtigungen zulassen"; @@ -2439,6 +2516,9 @@ /* Title for the share sheet entry */ "waitlist.share-sheet.title" = "Du bist eingeladen!"; +/* No comment provided by engineer. */ +"We want to make using passwords in DuckDuckGo better." = "We want to make using passwords in DuckDuckGo better."; + /* Confirmation message */ "web.url.remove.favorite.done" = "Favorit wurde entfernt"; diff --git a/DuckDuckGo/el.lproj/Autocomplete.strings b/DuckDuckGo/el.lproj/Autocomplete.strings deleted file mode 100644 index 6990a6f6b6..0000000000 --- a/DuckDuckGo/el.lproj/Autocomplete.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Class = "UILabel"; text = "Label"; ObjectID = "5ag-a1-mlA"; */ -"5ag-a1-mlA.text" = "Label"; - -/* Class = "UILabel"; text = "Label"; ObjectID = "eiv-Cq-E2r"; */ -"eiv-Cq-E2r.text" = "Label"; - -/* Class = "UILabel"; text = "No Suggestions"; ObjectID = "gPe-Cv-14j"; */ -"gPe-Cv-14j.text" = "Δεν υπάρχουν προτάσεις"; - -/* Class = "UIButton"; accessibilityLabel = "Autocomplete suggestion"; ObjectID = "N97-eI-Lps"; */ -"N97-eI-Lps.accessibilityLabel" = "Πρόταση αυτόματης συμπλήρωσης"; - diff --git a/DuckDuckGo/el.lproj/Localizable.strings b/DuckDuckGo/el.lproj/Localizable.strings index 6935776b5b..cbfc09afb2 100644 --- a/DuckDuckGo/el.lproj/Localizable.strings +++ b/DuckDuckGo/el.lproj/Localizable.strings @@ -119,7 +119,7 @@ "addWidget.description" = "Αποκτήστε γρήγορη πρόσβαση σε μια ιδιωτική αναζήτηση και στους ιστότοπους που αγαπάτε."; /* No comment provided by engineer. */ -"addWidget.settings.firstParagraph" = "Πατήστε παρατεταμένα στην αρχική οθόνη για να εισέλθετε σε λειτουργία αναδιάταξης."; +"addWidget.settings.firstParagraph" = "Long-press on the Home Screen to enter jiggle mode."; /* Replacement string is a plus button icon. */ "addWidget.settings.secondParagraph.%@" = "Πατήστε το κουμπί συν %@."; @@ -133,15 +133,48 @@ /* No comment provided by engineer. */ "alert.message.bookmarkAll" = "Οι υπάρχοντες σελιδοδείκτες δεν θα αναπαραχθούν."; +/* Description for alert shown when sync error occurs because of bad data */ +"alert.sync-bad-data-error-description" = "Ορισμένοι σελιδοδείκτες ή κωδικοί πρόσβασης δεν έχουν μορφοποιηθεί σωστά ή είναι πολύ μεγάλοι και δεν συγχρονίστηκαν."; + +/* Description for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-description" = "Έχετε φτάσει στον μέγιστο αριθμό σελιδοδεικτών. Διαγράψτε ορισμένους σελιδοδείκτες για να συνεχίσετε τον συγχρονισμό."; + +/* Title for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-title" = "Ο συγχρονισμός σελιδοδεικτών έχει τεθεί σε παύση"; + +/* Description for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-description" = "Έχετε φτάσει στον μέγιστο αριθμό κωδικών πρόσβασης. Διαγράψτε ορισμένους κωδικούς πρόσβασης για να συνεχίσετε τον συγχρονισμό."; + +/* Title for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-title" = "Ο συγχρονισμός κωδικών πρόσβασης έχει τεθεί σε παύση"; + /* Title for sync error alert */ "alert.sync-error" = "Σφάλμα συγχρονισμού και δημιουργίας αντιγράφων ασφαλείας"; +/* Sync error alert action button title, takes the user to the sync settings page. */ +"alert.sync-error-action" = "Ρυθμίσεις συγχρονισμού"; + +/* Description for alert shown when user logged off from sync */ +"alert.sync-invalid-login-error-description" = "Ο συγχρονισμός έχει τεθεί σε παύση. Αν θέλετε να συνεχίσετε με τον συγχρονισμό της συσκευής αυτής, συνδεθείτε ξανά χρησιμοποιώντας άλλη συσκευή ή τον κωδικό ανάκτησής σας."; + /* Learn more button in alert */ "alert.sync-paused-alert-learn-more-button" = "Μάθετε περισσότερα"; /* Confirmation button in alert */ "alert.sync-paused-alert-ok-button" = "Εντάξει"; +/* Title for alert shown when sync paused for an error */ +"alert.sync-paused-title" = "Ο συγχρονισμός έχει τεθεί σε παύση"; + +/* Description for alert shown when sync error occurs because of too many requests */ +"alert.sync-too-many-requests-error-description" = "Το Sync & Backup είναι προσωρινά μη διαθέσιμο."; + +/* Title of the warning message that tells the user that there was an error with the sync feature. */ +"alert.sync.warning.sync-error" = "Σφάλμα συγχρονισμού"; + +/* Title of the warning message */ +"alert.sync.warning.sync-paused" = "Η λειτουργία Συγχρονισμός και δημιουργία αντιγράφων ασφαλείας είναι σε παύση"; + /* Question from confirmation dialog */ "alert.title.bookmarkAll" = "Προσθήκη σελιδοδείκτη για όλες τις καρτέλες;"; @@ -208,6 +241,15 @@ /* No comment provided by engineer. */ "autoclear.on" = "Ενεργό"; +/* Subtitle for search history items */ +"autocomplete.history.search.duckduckgo" = "Αναζήτηση DuckDuckGo"; + +/* The message text shown in suggestions */ +"autocomplete.history.warning.message" = "Οι προτάσεις περιλαμβάνουν πλέον ιστότοπους που επισκεφθήκατε πρόσφατα. Αποθηκεύονται στη συσκευή σας και ποτέ στους διακομιστές του DuckDuckGo. Απενεργοποιήστε τη λειτουργία από τις ρυθμίσεις ή καταργήστε τη οποιαδήποτε στιγμή με το 🔥 Fire Button."; + +/* Title for message show in suggestions */ +"autocomplete.history.warning.title" = "Ίδιο απόρρητο.\nΚαλύτερες προτάσεις αναζήτησης!"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Απενεργοποιήθηκε"; @@ -1147,6 +1189,9 @@ /* Title for the get desktop browser feature */ "get.browser.title" = "Αποκτήστε το DuckDuckGo για Mac ή Windows"; +/* No comment provided by engineer. */ +"Help us improve!" = "Help us improve!"; + /* No comment provided by engineer. */ "home.row.onboarding.header" = "Προσθήκη του DuckDuckGo στην αρχική οθόνη σας!"; @@ -1603,6 +1648,26 @@ /* Deactivate button */ "pm.deactivate" = "Deactivate"; +/* Button title for sync bookmarks limits exceeded warning to go to manage bookmarks */ +"prefrences.sync.bookmarks-limit-exceeded-action" = "Διαχείριση σελιδοδεικτών"; + +/* Description for sync bookmarks limits exceeded warning */ +"prefrences.sync.bookmarks-limit-exceeded-description" = "Έχετε φτάσει στον μέγιστο αριθμό σελιδοδεικτών. Διαγράψτε ορισμένους για να συνεχίσετε τον συγχρονισμό."; + +/* Button title for sync credentials limits exceeded warning to go to manage passwords */ +"prefrences.sync.credentials-limit-exceeded-action" = "Διαχείριση κωδικών πρόσβασης…"; + +/* Description for sync credentials limits exceeded warning */ +"prefrences.sync.credentials-limit-exceeded-description" = "Έχετε φτάσει στον μέγιστο αριθμό κωδικών πρόσβασης. Διαγράψτε ορισμένους για να συνεχίσετε τον συγχρονισμό."; + +/* Description invalid credentials error when syncing. + Description of incorrectly formatted data error when syncing. + Description of too many requests error when syncing. */ +"prefrences.sync.invalid-login-description" = "Ο συγχρονισμός αντιμετώπισε σφάλμα. Δοκιμάστε να απενεργοποιήσετε τον συγχρονισμό στη συσκευή αυτή και έπειτα συνδεθείτε ξανά χρησιμοποιώντας άλλη συσκευή ή τον κωδικό ανάκτησής σας."; + +/* Title for sync limits exceeded warning */ +"prefrences.sync.limit-exceeded-title" = "Παύση συγχρονισμού"; + /* No comment provided by engineer. */ "preserveLogins.domain.list.footer" = "Οι ιστότοποι βασίζονται σε cookies για να σας διατηρούν συνδεδεμένους. Όταν ενεργοποιείτε τη διαγραφή δραστηριότητας για έναν ιστότοπο, τα cookies δεν διαγράφονται και παραμένετε συνδεδεμένοι, ακόμα και μετά τη χρήση του Κουμπιού διαγραφής δραστηριότητας. Εξακολουθούμε να αποκλείουμε εφαρμογές παρακολούθησης από τρίτους που υπάρχουν σε ιστότοπους με διαγραφή δραστηριότητας."; @@ -1712,6 +1777,9 @@ /* Settings screen cell for autocomplete */ "settings.autocomplete" = "Εμφάνιση προτάσεων αυτόματης συμπλήρωσης"; +/* Settings label for enabling or disabling recently visited sites */ +"settings.autocomplete.recentlyvisited" = "Πρόσφατα επισκεφθέντες ιστότοποι"; + /* Settings screen cell text for Application Lock */ "settings.autolock" = "Κλείδωμα εφαρμογής"; @@ -2241,6 +2309,9 @@ /* Tab Switcher Accessibility Label */ "tab.switcher.accessibility.label" = "Εναλλαγή καρτελών"; +/* No comment provided by engineer. */ +"Take Survey" = "Take Survey"; + /* Description text for the text size adjustment setting */ "textSize.description" = "Επιλέξτε το προτιμώμενο μέγεθος κειμένου. Οι ιστότοποι που βλέπετε στο DuckDuckGo θα προσαρμοστούν σε αυτό."; @@ -2319,6 +2390,9 @@ /* Alert title for the alert when the Privacy Pro subscription expires */ "vpn.access-revoked.alert.title" = "VPN disconnected due to expired subscription"; +/* Title for the VPN widget onboarding screen */ +"vpn.addWidget.settings.title" = "Find and select DuckDuckGo. Then swipe to VPN and select Add Widget."; + /* Title for the Cancel button of the VPN feedback form */ "vpn.feedback-form.button.cancel" = "Cancel"; @@ -2388,6 +2462,9 @@ /* Title for each screen of the VPN feedback form */ "vpn.feedback-form.title" = "Help Improve the DuckDuckGo VPN"; +/* VPN settings screen cell text for adding the VPN widget to the home screen */ +"vpn.settings.add.widget" = "Add VPN Widget to Home Screen"; + /* Title for the button to enable push notifications in system settings */ "waitlist.allow-notifications" = "Επιτρέψτε ειδοποιήσεις"; @@ -2439,6 +2516,9 @@ /* Title for the share sheet entry */ "waitlist.share-sheet.title" = "Έχετε προσκληθεί!"; +/* No comment provided by engineer. */ +"We want to make using passwords in DuckDuckGo better." = "We want to make using passwords in DuckDuckGo better."; + /* Confirmation message */ "web.url.remove.favorite.done" = "Το Αγαπημένο αφαιρέθηκε"; diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index 1d9b897306..91077fe7e4 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -238,6 +238,15 @@ /* No comment provided by engineer. */ "autoclear.on" = "On"; +/* Subtitle for search history items */ +"autocomplete.history.search.duckduckgo" = "Search DuckDuckGo"; + +/* The message text shown in suggestions */ +"autocomplete.history.warning.message" = "Suggestions now include recently visited sites. These are stored on your device and never on DuckDuckGo's servers. Turn off in settings, or clear any time with the 🔥 Fire Button."; + +/* Title for message show in suggestions */ +"autocomplete.history.warning.title" = "Same privacy.\nBetter search suggestions!"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Disabled"; @@ -1565,7 +1574,7 @@ https://duckduckgo.com/mac"; "network.protection.vpn.secure.dns.setting.footer" = "Our VPN uses Secure DNS to keep your online activity private, so that your Internet provider can't see what websites you visit."; /* Title for the FAQ row in the VPN status screen. */ -"network.protection.vpn.settings.faq" = "Frequently Asked Questions"; +"network.protection.vpn.settings.faq" = "FAQs and Support"; /* Title for the feedback row in the VPN status screen. */ "network.protection.vpn.settings.share-feedback" = "Share VPN Feedback"; @@ -1756,6 +1765,9 @@ But if you *do* want a peek under the hood, you can find more information about /* Settings screen cell for autocomplete */ "settings.autocomplete" = "Autocomplete Suggestions"; +/* Settings label for enabling or disabling recently visited sites */ +"settings.autocomplete.recentlyvisited" = "Recently Visited Sites"; + /* Settings screen cell text for Application Lock */ "settings.autolock" = "Application Lock"; @@ -1859,6 +1871,9 @@ But if you *do* want a peek under the hood, you can find more information about /* Product name for the subscription bundle */ "settings.ppro" = "Privacy Pro"; +/* Title for Link in the Footer of Privacy Pro section */ +"settings.ppro.footer" = "Privacy Policy and Terms of Service"; + /* Settings screen cell for long press previews */ "settings.previews" = "Long-Press Previews"; @@ -1887,7 +1902,7 @@ But if you *do* want a peek under the hood, you can find more information about "settings.subscription.activation.pending.description" = "This is taking longer than usual, please check back later."; /* Subscription activation pending title */ -"settings.subscription.activation.pending.title" = "Your Subscription is Being Activated"; +"settings.subscription.activation.pending.title" = "Your Subscription is being activated"; /* Data Broker protection cell subtitle for privacy pro */ "settings.subscription.DBP.subtitle" = "Remove your info from sites that sell it"; @@ -1896,7 +1911,7 @@ But if you *do* want a peek under the hood, you can find more information about "settings.subscription.DBP.title" = "Personal Information Removal"; /* Privacy pro description subtext */ -"settings.subscription.description" = "More seamless privacy with three new protections, including:"; +"settings.subscription.description" = "More seamless privacy with three new protections:"; /* I have a Subscription button text for privacy pro */ "settings.subscription.existing.subscription" = "I Have a Subscription"; @@ -2135,7 +2150,7 @@ But if you *do* want a peek under the hood, you can find more information about "subscription.manage.devices" = "Manage Devices"; /* Description for Email Management options */ -"subscription.manage.email.description" = "You can use this email to activate your subscription from browser settings in the DuckDuckGo app on your other devices."; +"subscription.manage.email.description" = "Use this email to activate your subscription from browser settings in the DuckDuckGo app on other devices.."; /* Manage Plan header */ "subscription.manage.plan" = "Manage Plan"; @@ -2168,7 +2183,7 @@ But if you *do* want a peek under the hood, you can find more information about "subscription.pir.heroTextMenyEntry" = "I have a subscription"; /* Text for the 'macOS' button */ -"subscription.pir.macos" = "macOS"; +"subscription.pir.macos" = "Mac"; /* Text for the 'Windows' button */ "subscription.pir.windows" = "Windows"; diff --git a/DuckDuckGo/es.lproj/Autocomplete.strings b/DuckDuckGo/es.lproj/Autocomplete.strings deleted file mode 100644 index 8364cbc3f0..0000000000 --- a/DuckDuckGo/es.lproj/Autocomplete.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Class = "UILabel"; text = "Label"; ObjectID = "5ag-a1-mlA"; */ -"5ag-a1-mlA.text" = "Etiqueta"; - -/* Class = "UILabel"; text = "Label"; ObjectID = "eiv-Cq-E2r"; */ -"eiv-Cq-E2r.text" = "Etiqueta"; - -/* Class = "UILabel"; text = "No Suggestions"; ObjectID = "gPe-Cv-14j"; */ -"gPe-Cv-14j.text" = "No hay sugerencias"; - -/* Class = "UIButton"; accessibilityLabel = "Autocomplete suggestion"; ObjectID = "N97-eI-Lps"; */ -"N97-eI-Lps.accessibilityLabel" = "Autocompletar sugerencia"; - diff --git a/DuckDuckGo/es.lproj/Localizable.strings b/DuckDuckGo/es.lproj/Localizable.strings index d60e4bfdf1..800cfcf9bd 100644 --- a/DuckDuckGo/es.lproj/Localizable.strings +++ b/DuckDuckGo/es.lproj/Localizable.strings @@ -119,7 +119,7 @@ "addWidget.description" = "Obtén acceso rápido a búsquedas privadas y a los sitios que más te gustan."; /* No comment provided by engineer. */ -"addWidget.settings.firstParagraph" = "Mantén pulsada la pantalla de inicio para entrar en el modo jiggle."; +"addWidget.settings.firstParagraph" = "Long-press on the Home Screen to enter jiggle mode."; /* Replacement string is a plus button icon. */ "addWidget.settings.secondParagraph.%@" = "Pulsa el botón más %@."; @@ -133,15 +133,48 @@ /* No comment provided by engineer. */ "alert.message.bookmarkAll" = "Los marcadores existentes no se duplicarán."; +/* Description for alert shown when sync error occurs because of bad data */ +"alert.sync-bad-data-error-description" = "Algunos marcadores o contraseñas tienen un formato incorrecto o demasiado largo y no se han sincronizado."; + +/* Description for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-description" = "Has alcanzado el número máximo de marcadores. Elimina algunos marcadores para reanudar la sincronización."; + +/* Title for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-title" = "La sincronización de marcadores está en pausa"; + +/* Description for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-description" = "Has alcanzado el número máximo de contraseñas. Elimina algunas contraseñas para reanudar la sincronización."; + +/* Title for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-title" = "La sincronización de contraseñas está en pausa"; + /* Title for sync error alert */ "alert.sync-error" = "Error de sincronización y copia de seguridad"; +/* Sync error alert action button title, takes the user to the sync settings page. */ +"alert.sync-error-action" = "Ajustes de sincronización"; + +/* Description for alert shown when user logged off from sync */ +"alert.sync-invalid-login-error-description" = "La sincronización se ha pausado. Si quieres seguir sincronizando este dispositivo, vuelve a conectarte con otro dispositivo o con tu código de recuperación."; + /* Learn more button in alert */ "alert.sync-paused-alert-learn-more-button" = "Más información"; /* Confirmation button in alert */ "alert.sync-paused-alert-ok-button" = "De acuerdo"; +/* Title for alert shown when sync paused for an error */ +"alert.sync-paused-title" = "La sincronización está en pausa"; + +/* Description for alert shown when sync error occurs because of too many requests */ +"alert.sync-too-many-requests-error-description" = "La sincronización y la copia de seguridad no están disponibles temporalmente."; + +/* Title of the warning message that tells the user that there was an error with the sync feature. */ +"alert.sync.warning.sync-error" = "Error de sincronización"; + +/* Title of the warning message */ +"alert.sync.warning.sync-paused" = "Sincronización y copia de seguridad en pausa"; + /* Question from confirmation dialog */ "alert.title.bookmarkAll" = "¿Añadir todas las pestañas a marcadores?"; @@ -208,6 +241,15 @@ /* No comment provided by engineer. */ "autoclear.on" = "Activado"; +/* Subtitle for search history items */ +"autocomplete.history.search.duckduckgo" = "Buscar en DuckDuckGo"; + +/* The message text shown in suggestions */ +"autocomplete.history.warning.message" = "Las sugerencias ahora incluyen sitios visitados recientemente. Estos se almacenan en tu dispositivo y nunca en los servidores de DuckDuckGo. Desactívalas en ajustes o elimínalas en cualquier momento con el 🔥 Fire Button."; + +/* Title for message show in suggestions */ +"autocomplete.history.warning.title" = "Misma privacidad.\n¡Mejores sugerencias de búsqueda!"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Desactivado"; @@ -1147,6 +1189,9 @@ /* Title for the get desktop browser feature */ "get.browser.title" = "Consigue DuckDuckGo para Mac o Windows"; +/* No comment provided by engineer. */ +"Help us improve!" = "Help us improve!"; + /* No comment provided by engineer. */ "home.row.onboarding.header" = "Añade DuckDuckGo a tu pantalla de inicio."; @@ -1603,6 +1648,26 @@ /* Deactivate button */ "pm.deactivate" = "Deactivate"; +/* Button title for sync bookmarks limits exceeded warning to go to manage bookmarks */ +"prefrences.sync.bookmarks-limit-exceeded-action" = "Gestionar marcadores"; + +/* Description for sync bookmarks limits exceeded warning */ +"prefrences.sync.bookmarks-limit-exceeded-description" = "Has alcanzado el número máximo de marcadores. Elimina algunos para reanudar la sincronización."; + +/* Button title for sync credentials limits exceeded warning to go to manage passwords */ +"prefrences.sync.credentials-limit-exceeded-action" = "Administrar contraseñas..."; + +/* Description for sync credentials limits exceeded warning */ +"prefrences.sync.credentials-limit-exceeded-description" = "Has alcanzado el número máximo de contraseñas. Elimina algunas para reanudar la sincronización."; + +/* Description invalid credentials error when syncing. + Description of incorrectly formatted data error when syncing. + Description of too many requests error when syncing. */ +"prefrences.sync.invalid-login-description" = "Se ha producido un error en la sincronización. Intenta desactivar la sincronización en este dispositivo y, a continuación, vuelve a conectarte con otro dispositivo o con tu código de recuperación."; + +/* Title for sync limits exceeded warning */ +"prefrences.sync.limit-exceeded-title" = "Sincronización pausada"; + /* No comment provided by engineer. */ "preserveLogins.domain.list.footer" = "Los sitios web dependen de cookies para mantener tu sesión iniciada. Cuando marcas una web como «a prueba de fuego» no se borrarán las cookies y tu sesión permanecerá iniciada, incluso después de utilizar el botón Fuego. Seguiremos bloqueando rastreadores de terceros que se encuentren en sitios web a prueba de fuego."; @@ -1712,6 +1777,9 @@ /* Settings screen cell for autocomplete */ "settings.autocomplete" = "Sugerencias de autocompletado"; +/* Settings label for enabling or disabling recently visited sites */ +"settings.autocomplete.recentlyvisited" = "Sitios visitados recientemente"; + /* Settings screen cell text for Application Lock */ "settings.autolock" = "Bloqueo de aplicación"; @@ -2241,6 +2309,9 @@ /* Tab Switcher Accessibility Label */ "tab.switcher.accessibility.label" = "Cambiar pestañas"; +/* No comment provided by engineer. */ +"Take Survey" = "Take Survey"; + /* Description text for the text size adjustment setting */ "textSize.description" = "Elige tu tamaño de texto preferido. Los sitios web que veas en DuckDuckGo se ajustarán a él."; @@ -2319,6 +2390,9 @@ /* Alert title for the alert when the Privacy Pro subscription expires */ "vpn.access-revoked.alert.title" = "VPN disconnected due to expired subscription"; +/* Title for the VPN widget onboarding screen */ +"vpn.addWidget.settings.title" = "Find and select DuckDuckGo. Then swipe to VPN and select Add Widget."; + /* Title for the Cancel button of the VPN feedback form */ "vpn.feedback-form.button.cancel" = "Cancel"; @@ -2388,6 +2462,9 @@ /* Title for each screen of the VPN feedback form */ "vpn.feedback-form.title" = "Help Improve the DuckDuckGo VPN"; +/* VPN settings screen cell text for adding the VPN widget to the home screen */ +"vpn.settings.add.widget" = "Add VPN Widget to Home Screen"; + /* Title for the button to enable push notifications in system settings */ "waitlist.allow-notifications" = "Permitir notificaciones"; @@ -2439,6 +2516,9 @@ /* Title for the share sheet entry */ "waitlist.share-sheet.title" = "¡Has recibido una invitación!"; +/* No comment provided by engineer. */ +"We want to make using passwords in DuckDuckGo better." = "We want to make using passwords in DuckDuckGo better."; + /* Confirmation message */ "web.url.remove.favorite.done" = "Favorito eliminado"; diff --git a/DuckDuckGo/et.lproj/Autocomplete.strings b/DuckDuckGo/et.lproj/Autocomplete.strings deleted file mode 100644 index a7f037e623..0000000000 --- a/DuckDuckGo/et.lproj/Autocomplete.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Class = "UILabel"; text = "Label"; ObjectID = "5ag-a1-mlA"; */ -"5ag-a1-mlA.text" = "Label"; - -/* Class = "UILabel"; text = "Label"; ObjectID = "eiv-Cq-E2r"; */ -"eiv-Cq-E2r.text" = "Silt"; - -/* Class = "UILabel"; text = "No Suggestions"; ObjectID = "gPe-Cv-14j"; */ -"gPe-Cv-14j.text" = "Ettepanekud puuduvad"; - -/* Class = "UIButton"; accessibilityLabel = "Autocomplete suggestion"; ObjectID = "N97-eI-Lps"; */ -"N97-eI-Lps.accessibilityLabel" = "Kasuta ettepaneku automaatteksti"; - diff --git a/DuckDuckGo/et.lproj/Localizable.strings b/DuckDuckGo/et.lproj/Localizable.strings index 35b3bfd431..f73bf211bb 100644 --- a/DuckDuckGo/et.lproj/Localizable.strings +++ b/DuckDuckGo/et.lproj/Localizable.strings @@ -119,7 +119,7 @@ "addWidget.description" = "Kasuta kiiret juurdepääsu privaatsele otsingule ja lemmiksaitidele."; /* No comment provided by engineer. */ -"addWidget.settings.firstParagraph" = "Pikk vajutus avaekraanile rakenduse ümberkorraldusrežiimi sisenemiseks."; +"addWidget.settings.firstParagraph" = "Long-press on the Home Screen to enter jiggle mode."; /* Replacement string is a plus button icon. */ "addWidget.settings.secondParagraph.%@" = "Vajutage plussnupule %@."; @@ -133,15 +133,48 @@ /* No comment provided by engineer. */ "alert.message.bookmarkAll" = "Olemasolevaid järjehoidjaid ei dubleerita."; +/* Description for alert shown when sync error occurs because of bad data */ +"alert.sync-bad-data-error-description" = "Mõned järjehoidjad või paroolid on valesti vormindatud või liiga pikad ja neid ei sünkroonitud."; + +/* Description for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-description" = "Oled saavutanud maksimaalse järjehoidjate arvu. Sünkroonimise jätkamiseks kustuta mõned järjehoidjad."; + +/* Title for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-title" = "Järjehoidja sünkroonimine on peatatud"; + +/* Description for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-description" = "Oled saavutanud maksimaalse paroolide arvu. Sünkroonimise jätkamiseks kustuta mõned paroolid."; + +/* Title for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-title" = "Parooli sünkroonimine on peatatud"; + /* Title for sync error alert */ "alert.sync-error" = "Sünkroonimise ja varundamise tõrge"; +/* Sync error alert action button title, takes the user to the sync settings page. */ +"alert.sync-error-action" = "Sünkroonimise sätted"; + +/* Description for alert shown when user logged off from sync */ +"alert.sync-invalid-login-error-description" = "Sünkroonimine on peatatud. Kui soovid selle seadme sünkroonimist jätkata, loo uuesti ühendus, kasutades mõnda teist seadet või taastamiskoodi."; + /* Learn more button in alert */ "alert.sync-paused-alert-learn-more-button" = "Loe edasi"; /* Confirmation button in alert */ "alert.sync-paused-alert-ok-button" = "OK"; +/* Title for alert shown when sync paused for an error */ +"alert.sync-paused-title" = "Sünkroonimine on peatatud"; + +/* Description for alert shown when sync error occurs because of too many requests */ +"alert.sync-too-many-requests-error-description" = "Sync & Backup pole ajutiselt saadaval."; + +/* Title of the warning message that tells the user that there was an error with the sync feature. */ +"alert.sync.warning.sync-error" = "Sünkroonimisviga"; + +/* Title of the warning message */ +"alert.sync.warning.sync-paused" = "Sünkroonimine ja varundamine on peatatud"; + /* Question from confirmation dialog */ "alert.title.bookmarkAll" = "Kas lisada kõik vahekaardid järjehoidjasse?"; @@ -208,6 +241,15 @@ /* No comment provided by engineer. */ "autoclear.on" = "Sees"; +/* Subtitle for search history items */ +"autocomplete.history.search.duckduckgo" = "Otsi DuckDuckGo'st"; + +/* The message text shown in suggestions */ +"autocomplete.history.warning.message" = "Soovitused hõlmavad nüüd hiljuti külastatud saite. Need salvestatakse sinu seadmesse ja mitte kunagi DuckDuckGo serveritesse. Saad selle välja lülitada seadetes või tühjendada igal ajal nupu 🔥 Fire Button abil."; + +/* Title for message show in suggestions */ +"autocomplete.history.warning.title" = "Sama privaatsus.\nParemad otsingusoovitused!"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Keelatud"; @@ -1147,6 +1189,9 @@ /* Title for the get desktop browser feature */ "get.browser.title" = "Hangi DuckDuckGo Maci või Windowsi jaoks"; +/* No comment provided by engineer. */ +"Help us improve!" = "Help us improve!"; + /* No comment provided by engineer. */ "home.row.onboarding.header" = "Lisa DuckDuckGo oma avaekraanile!"; @@ -1603,6 +1648,26 @@ /* Deactivate button */ "pm.deactivate" = "Deactivate"; +/* Button title for sync bookmarks limits exceeded warning to go to manage bookmarks */ +"prefrences.sync.bookmarks-limit-exceeded-action" = "Halda järjehoidjaid"; + +/* Description for sync bookmarks limits exceeded warning */ +"prefrences.sync.bookmarks-limit-exceeded-description" = "Oled saavutanud maksimaalse järjehoidjate arvu. Kustuta mõni, et jätkata sünkroonimisega."; + +/* Button title for sync credentials limits exceeded warning to go to manage passwords */ +"prefrences.sync.credentials-limit-exceeded-action" = "Halda paroole…"; + +/* Description for sync credentials limits exceeded warning */ +"prefrences.sync.credentials-limit-exceeded-description" = "Oled saavutanud maksimaalse paroolide arvu. Kustuta mõni, et jätkata sünkroonimisega."; + +/* Description invalid credentials error when syncing. + Description of incorrectly formatted data error when syncing. + Description of too many requests error when syncing. */ +"prefrences.sync.invalid-login-description" = "Sünkroonimisel ilmnes tõrge. Proovi sellel seadmel sünkroonimine keelata ja seejärel loo uuesti ühendus, kasutades mõnda teist seadet või taastamiskoodi."; + +/* Title for sync limits exceeded warning */ +"prefrences.sync.limit-exceeded-title" = "Sünkroonimine peatatud"; + /* No comment provided by engineer. */ "preserveLogins.domain.list.footer" = "Veebisaidid kasutavad küpsiseid, et hoida sind sisse logitud. Kui teed saidi tulekindlaks, ei kustutata küpsiseid ja sa jääd sisse logituks isegi pärast tulenupu kasutamist. Me blokeerime endiselt kolmanda poole jälitajad, kelle me tulekindlatelt lehtedelt leiame."; @@ -1712,6 +1777,9 @@ /* Settings screen cell for autocomplete */ "settings.autocomplete" = "Automaatselt täidetavad ettepanekud"; +/* Settings label for enabling or disabling recently visited sites */ +"settings.autocomplete.recentlyvisited" = "Hiljuti külastatud saidid"; + /* Settings screen cell text for Application Lock */ "settings.autolock" = "Rakenduse lukk"; @@ -2241,6 +2309,9 @@ /* Tab Switcher Accessibility Label */ "tab.switcher.accessibility.label" = "Vahekaardi vahetaja"; +/* No comment provided by engineer. */ +"Take Survey" = "Take Survey"; + /* Description text for the text size adjustment setting */ "textSize.description" = "Valige soovitud teksti suurus. Veebisaidid, mida DuckDuckGos vaatate, kohanduvad sellega."; @@ -2319,6 +2390,9 @@ /* Alert title for the alert when the Privacy Pro subscription expires */ "vpn.access-revoked.alert.title" = "VPN disconnected due to expired subscription"; +/* Title for the VPN widget onboarding screen */ +"vpn.addWidget.settings.title" = "Find and select DuckDuckGo. Then swipe to VPN and select Add Widget."; + /* Title for the Cancel button of the VPN feedback form */ "vpn.feedback-form.button.cancel" = "Cancel"; @@ -2388,6 +2462,9 @@ /* Title for each screen of the VPN feedback form */ "vpn.feedback-form.title" = "Help Improve the DuckDuckGo VPN"; +/* VPN settings screen cell text for adding the VPN widget to the home screen */ +"vpn.settings.add.widget" = "Add VPN Widget to Home Screen"; + /* Title for the button to enable push notifications in system settings */ "waitlist.allow-notifications" = "Luba teavitused"; @@ -2439,6 +2516,9 @@ /* Title for the share sheet entry */ "waitlist.share-sheet.title" = "Oled kutsutud!"; +/* No comment provided by engineer. */ +"We want to make using passwords in DuckDuckGo better." = "We want to make using passwords in DuckDuckGo better."; + /* Confirmation message */ "web.url.remove.favorite.done" = "Lemmik on eemaldatud"; diff --git a/DuckDuckGo/fi.lproj/Autocomplete.strings b/DuckDuckGo/fi.lproj/Autocomplete.strings deleted file mode 100644 index e737717174..0000000000 --- a/DuckDuckGo/fi.lproj/Autocomplete.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Class = "UILabel"; text = "Label"; ObjectID = "5ag-a1-mlA"; */ -"5ag-a1-mlA.text" = "Label"; - -/* Class = "UILabel"; text = "Label"; ObjectID = "eiv-Cq-E2r"; */ -"eiv-Cq-E2r.text" = "Label"; - -/* Class = "UILabel"; text = "No Suggestions"; ObjectID = "gPe-Cv-14j"; */ -"gPe-Cv-14j.text" = "Ei ehdotuksia"; - -/* Class = "UIButton"; accessibilityLabel = "Autocomplete suggestion"; ObjectID = "N97-eI-Lps"; */ -"N97-eI-Lps.accessibilityLabel" = "Automaattisen täydennyksen ehdotus"; - diff --git a/DuckDuckGo/fi.lproj/Localizable.strings b/DuckDuckGo/fi.lproj/Localizable.strings index 65d9b9e2f0..e3964ca4e8 100644 --- a/DuckDuckGo/fi.lproj/Localizable.strings +++ b/DuckDuckGo/fi.lproj/Localizable.strings @@ -119,7 +119,7 @@ "addWidget.description" = "Nopea pääsy yksityiseen hakuun ja suosikkisivustoihin."; /* No comment provided by engineer. */ -"addWidget.settings.firstParagraph" = "Paina pitkään aloitusnäyttöä siirtyäksesi heilutustilaan."; +"addWidget.settings.firstParagraph" = "Long-press on the Home Screen to enter jiggle mode."; /* Replacement string is a plus button icon. */ "addWidget.settings.secondParagraph.%@" = "Napauta pluspainiketta %@."; @@ -133,15 +133,48 @@ /* No comment provided by engineer. */ "alert.message.bookmarkAll" = "Olemassa olevia kirjanmerkkejä ei kopioida."; +/* Description for alert shown when sync error occurs because of bad data */ +"alert.sync-bad-data-error-description" = "Jotkin kirjanmerkit tai salasanat on väärin muotoiltuja tai liian pitkiä, eikä niitä ole synkronoitu."; + +/* Description for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-description" = "Kirjanmerkkikiintiösi on täynnä. Poista joitakin kirjanmerkkejä jatkaaksesi synkronointia."; + +/* Title for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-title" = "Kirjanmerkkien synkronointi on keskeytetty"; + +/* Description for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-description" = "Salasanakiintiösi on täynnä. Poista joitakin salasanoja jatkaaksesi synkronointia."; + +/* Title for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-title" = "Salasanojen synkronointi on keskeytetty"; + /* Title for sync error alert */ "alert.sync-error" = "Synkronointi- ja varmuuskopiointivirhe"; +/* Sync error alert action button title, takes the user to the sync settings page. */ +"alert.sync-error-action" = "Synkronointiasetukset"; + +/* Description for alert shown when user logged off from sync */ +"alert.sync-invalid-login-error-description" = "Synkronointi on keskeytetty. Jos haluat jatkaa tämän laitteen synkronointia, muodosta yhteys uudelleen toisella laitteella tai palautuskoodilla."; + /* Learn more button in alert */ "alert.sync-paused-alert-learn-more-button" = "Lue lisää"; /* Confirmation button in alert */ "alert.sync-paused-alert-ok-button" = "OK"; +/* Title for alert shown when sync paused for an error */ +"alert.sync-paused-title" = "Synkronointi on keskeytetty"; + +/* Description for alert shown when sync error occurs because of too many requests */ +"alert.sync-too-many-requests-error-description" = "Synkronointi ja varmuuskopiointi on tilapäisesti poissa käytöstä."; + +/* Title of the warning message that tells the user that there was an error with the sync feature. */ +"alert.sync.warning.sync-error" = "Synkronointivirhe"; + +/* Title of the warning message */ +"alert.sync.warning.sync-paused" = "Sync & Backup on keskeytetty"; + /* Question from confirmation dialog */ "alert.title.bookmarkAll" = "Lisätäänkö kaikki välilehdet kirjanmerkkeihin?"; @@ -208,6 +241,15 @@ /* No comment provided by engineer. */ "autoclear.on" = "Päällä"; +/* Subtitle for search history items */ +"autocomplete.history.search.duckduckgo" = "Hae DuckDuckGo:sta"; + +/* The message text shown in suggestions */ +"autocomplete.history.warning.message" = "Äskettäin käydyt sivustot sisältyvät nyt ehdotuksiin. Ne tallennetaan laitteellesi, ei koskaan DuckDuckGon palvelimille. Voit poistaa tämän käytöstä asetuksissa tai tyhjentää sivustotiedot milloin tahansa 🔥 Fire Buttonilla."; + +/* Title for message show in suggestions */ +"autocomplete.history.warning.title" = "Sama tietoja,\nparempia hakuehdotuksia!"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Pois käytöstä"; @@ -1147,6 +1189,9 @@ /* Title for the get desktop browser feature */ "get.browser.title" = "Hanki DuckDuckGo Macille tai Windowsille"; +/* No comment provided by engineer. */ +"Help us improve!" = "Help us improve!"; + /* No comment provided by engineer. */ "home.row.onboarding.header" = "Lisää DuckDuckGo koti-valikkoon!"; @@ -1603,6 +1648,26 @@ /* Deactivate button */ "pm.deactivate" = "Deactivate"; +/* Button title for sync bookmarks limits exceeded warning to go to manage bookmarks */ +"prefrences.sync.bookmarks-limit-exceeded-action" = "Hallitse kirjanmerkkejä"; + +/* Description for sync bookmarks limits exceeded warning */ +"prefrences.sync.bookmarks-limit-exceeded-description" = "Kirjanmerkkikiintiösi on täynnä. Poista jotakin niistä jatkaaksesi synkronointia."; + +/* Button title for sync credentials limits exceeded warning to go to manage passwords */ +"prefrences.sync.credentials-limit-exceeded-action" = "Hallitse salasanoja…"; + +/* Description for sync credentials limits exceeded warning */ +"prefrences.sync.credentials-limit-exceeded-description" = "Salasanakiintiösi on täynnä. Poista joitain salasanoja jatkaaksesi synkronointia."; + +/* Description invalid credentials error when syncing. + Description of incorrectly formatted data error when syncing. + Description of too many requests error when syncing. */ +"prefrences.sync.invalid-login-description" = "Synkronoinnin aikana havaittiin virhe. Yritä poistaa synkronointi käytöstä tällä laitteella. Muodosta sitten yhteys uudelleen toisella laitteella tai palautuskoodilla."; + +/* Title for sync limits exceeded warning */ +"prefrences.sync.limit-exceeded-title" = "Synkronointi keskeytetty"; + /* No comment provided by engineer. */ "preserveLogins.domain.list.footer" = "Verkkosivustot käyttävät evästeitä, joilla pysyt kirjautuneena sisään. Kun teet sivustosta palonkestävän, evästeitä ei poisteta ja pysyt kirjautuneena sisään, vaikka käytät Liekki-painiketta. Estämme edelleen kolmannen osapuolen seurannan palonkestävillä sivustoilla."; @@ -1712,6 +1777,9 @@ /* Settings screen cell for autocomplete */ "settings.autocomplete" = "Näytä automaattisen täydennyksen ehdotukset"; +/* Settings label for enabling or disabling recently visited sites */ +"settings.autocomplete.recentlyvisited" = "Viimeksi käydyt sivustot"; + /* Settings screen cell text for Application Lock */ "settings.autolock" = "Sovelluksen lukitus"; @@ -2241,6 +2309,9 @@ /* Tab Switcher Accessibility Label */ "tab.switcher.accessibility.label" = "Välilehtien vaihtotyökalu"; +/* No comment provided by engineer. */ +"Take Survey" = "Take Survey"; + /* Description text for the text size adjustment setting */ "textSize.description" = "Valitse tekstin koko. DuckDuckGossa tarkastelemasi verkkosivustot käyttävät sitä."; @@ -2319,6 +2390,9 @@ /* Alert title for the alert when the Privacy Pro subscription expires */ "vpn.access-revoked.alert.title" = "VPN disconnected due to expired subscription"; +/* Title for the VPN widget onboarding screen */ +"vpn.addWidget.settings.title" = "Find and select DuckDuckGo. Then swipe to VPN and select Add Widget."; + /* Title for the Cancel button of the VPN feedback form */ "vpn.feedback-form.button.cancel" = "Cancel"; @@ -2388,6 +2462,9 @@ /* Title for each screen of the VPN feedback form */ "vpn.feedback-form.title" = "Help Improve the DuckDuckGo VPN"; +/* VPN settings screen cell text for adding the VPN widget to the home screen */ +"vpn.settings.add.widget" = "Add VPN Widget to Home Screen"; + /* Title for the button to enable push notifications in system settings */ "waitlist.allow-notifications" = "Salli ilmoitukset"; @@ -2439,6 +2516,9 @@ /* Title for the share sheet entry */ "waitlist.share-sheet.title" = "Sinut on kutsuttu!"; +/* No comment provided by engineer. */ +"We want to make using passwords in DuckDuckGo better." = "We want to make using passwords in DuckDuckGo better."; + /* Confirmation message */ "web.url.remove.favorite.done" = "Suosikki poistettu"; diff --git a/DuckDuckGo/fr.lproj/Autocomplete.strings b/DuckDuckGo/fr.lproj/Autocomplete.strings deleted file mode 100644 index be60c7e41b..0000000000 --- a/DuckDuckGo/fr.lproj/Autocomplete.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Class = "UILabel"; text = "Label"; ObjectID = "5ag-a1-mlA"; */ -"5ag-a1-mlA.text" = "Label"; - -/* Class = "UILabel"; text = "Label"; ObjectID = "eiv-Cq-E2r"; */ -"eiv-Cq-E2r.text" = "Label"; - -/* Class = "UILabel"; text = "No Suggestions"; ObjectID = "gPe-Cv-14j"; */ -"gPe-Cv-14j.text" = "Aucune suggestion"; - -/* Class = "UIButton"; accessibilityLabel = "Autocomplete suggestion"; ObjectID = "N97-eI-Lps"; */ -"N97-eI-Lps.accessibilityLabel" = "Saisie semi-automatique de la suggestion"; - diff --git a/DuckDuckGo/fr.lproj/Localizable.strings b/DuckDuckGo/fr.lproj/Localizable.strings index ff3fb5795a..f74a3110c4 100644 --- a/DuckDuckGo/fr.lproj/Localizable.strings +++ b/DuckDuckGo/fr.lproj/Localizable.strings @@ -119,7 +119,7 @@ "addWidget.description" = "Accédez rapidement à la recherche privée et aux sites que vous aimez."; /* No comment provided by engineer. */ -"addWidget.settings.firstParagraph" = "Appuyez longuement sur l'écran d'accueil pour passer en mode Jiggle."; +"addWidget.settings.firstParagraph" = "Long-press on the Home Screen to enter jiggle mode."; /* Replacement string is a plus button icon. */ "addWidget.settings.secondParagraph.%@" = "Appuyez sur le bouton plus %@."; @@ -133,15 +133,48 @@ /* No comment provided by engineer. */ "alert.message.bookmarkAll" = "Les signets existants ne seront pas dupliqués."; +/* Description for alert shown when sync error occurs because of bad data */ +"alert.sync-bad-data-error-description" = "Certains signets ou mots de passe sont mal formatés ou trop longs et n'ont pas été synchronisés."; + +/* Description for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-description" = "Vous avez atteint le nombre maximal de signets. Veuillez en supprimer quelques-uns pour reprendre la synchronisation."; + +/* Title for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-title" = "La synchronisation des signets est suspendue"; + +/* Description for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-description" = "Vous avez atteint le nombre maximal de mots de passe. Veuillez en supprimer quelques-uns pour reprendre la synchronisation."; + +/* Title for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-title" = "La synchronisation des mots de passe est suspendue"; + /* Title for sync error alert */ "alert.sync-error" = "Erreur de synchronisation et de sauvegarde"; +/* Sync error alert action button title, takes the user to the sync settings page. */ +"alert.sync-error-action" = "Réglages de synchronisation"; + +/* Description for alert shown when user logged off from sync */ +"alert.sync-invalid-login-error-description" = "La synchronisation a été suspendue. Si vous souhaitez continuer à synchroniser cet appareil, reconnectez-vous à l’aide d’un autre appareil ou de votre code de récupération."; + /* Learn more button in alert */ "alert.sync-paused-alert-learn-more-button" = "En savoir plus"; /* Confirmation button in alert */ "alert.sync-paused-alert-ok-button" = "OK"; +/* Title for alert shown when sync paused for an error */ +"alert.sync-paused-title" = "La synchronisation est suspendue"; + +/* Description for alert shown when sync error occurs because of too many requests */ +"alert.sync-too-many-requests-error-description" = "Sync & Backup est temporairement indisponible."; + +/* Title of the warning message that tells the user that there was an error with the sync feature. */ +"alert.sync.warning.sync-error" = "Erreur de synchronisation"; + +/* Title of the warning message */ +"alert.sync.warning.sync-paused" = "La synchronisation et la sauvegarde sont suspendues"; + /* Question from confirmation dialog */ "alert.title.bookmarkAll" = "Ajouter tous les onglets aux signets ?"; @@ -208,6 +241,15 @@ /* No comment provided by engineer. */ "autoclear.on" = "Activé"; +/* Subtitle for search history items */ +"autocomplete.history.search.duckduckgo" = "Rechercher avec DuckDuckGo"; + +/* The message text shown in suggestions */ +"autocomplete.history.warning.message" = "Les suggestions incluent désormais les sites récemment visités. Ils sont stockés sur votre appareil, jamais sur les serveurs de DuckDuckGo. Désactivez cette option dans les paramètres ou effacez à tout moment grâce au Fire Button 🔥."; + +/* Title for message show in suggestions */ +"autocomplete.history.warning.title" = "La confidentialité reste la même.\nLes suggestions de recherche s'améliorent !"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Désactivé"; @@ -1147,6 +1189,9 @@ /* Title for the get desktop browser feature */ "get.browser.title" = "Procurez-vous DuckDuckGo pour Mac ou Windows"; +/* No comment provided by engineer. */ +"Help us improve!" = "Help us improve!"; + /* No comment provided by engineer. */ "home.row.onboarding.header" = "Ajoutez DuckDuckGo à votre écran d'accueil !"; @@ -1603,6 +1648,26 @@ /* Deactivate button */ "pm.deactivate" = "Deactivate"; +/* Button title for sync bookmarks limits exceeded warning to go to manage bookmarks */ +"prefrences.sync.bookmarks-limit-exceeded-action" = "Gérer les signets"; + +/* Description for sync bookmarks limits exceeded warning */ +"prefrences.sync.bookmarks-limit-exceeded-description" = "Vous avez atteint le nombre maximal de signets. Veuillez en supprimer quelques-uns pour reprendre la synchronisation."; + +/* Button title for sync credentials limits exceeded warning to go to manage passwords */ +"prefrences.sync.credentials-limit-exceeded-action" = "Gérer les mots de passe…"; + +/* Description for sync credentials limits exceeded warning */ +"prefrences.sync.credentials-limit-exceeded-description" = "Vous avez atteint le nombre maximal de mots de passe. Veuillez en supprimer quelques-uns pour reprendre la synchronisation."; + +/* Description invalid credentials error when syncing. + Description of incorrectly formatted data error when syncing. + Description of too many requests error when syncing. */ +"prefrences.sync.invalid-login-description" = "La synchronisation a rencontré une erreur. Essayez de désactiver la synchronisation sur cet appareil, puis reconnectez-vous à l’aide d’un autre appareil ou de votre code de récupération."; + +/* Title for sync limits exceeded warning */ +"prefrences.sync.limit-exceeded-title" = "Synchronisation suspendue"; + /* No comment provided by engineer. */ "preserveLogins.domain.list.footer" = "Les sites Web utilisent des cookies pour maintenir votre connexion. Quand vous placez un site en mode coupe-feu, les cookies sont conservés et vous restez connecté(e), même après avoir utilisé le bouton en forme de flamme. Ceci dit, les traqueurs tiers sont bloqués sur les sites coupe-feu."; @@ -1712,6 +1777,9 @@ /* Settings screen cell for autocomplete */ "settings.autocomplete" = "Afficher les suggestions de saisie semi-automatique"; +/* Settings label for enabling or disabling recently visited sites */ +"settings.autocomplete.recentlyvisited" = "Sites récemment visités"; + /* Settings screen cell text for Application Lock */ "settings.autolock" = "Verrouillage de l'application"; @@ -2241,6 +2309,9 @@ /* Tab Switcher Accessibility Label */ "tab.switcher.accessibility.label" = "Outil de changement d'onglet"; +/* No comment provided by engineer. */ +"Take Survey" = "Take Survey"; + /* Description text for the text size adjustment setting */ "textSize.description" = "Choisissez la taille du texte par défaut. La taille des polices des sites Web que vous consultez à partir de Duckduckgo sera ajustée en conséquence."; @@ -2319,6 +2390,9 @@ /* Alert title for the alert when the Privacy Pro subscription expires */ "vpn.access-revoked.alert.title" = "VPN disconnected due to expired subscription"; +/* Title for the VPN widget onboarding screen */ +"vpn.addWidget.settings.title" = "Find and select DuckDuckGo. Then swipe to VPN and select Add Widget."; + /* Title for the Cancel button of the VPN feedback form */ "vpn.feedback-form.button.cancel" = "Cancel"; @@ -2388,6 +2462,9 @@ /* Title for each screen of the VPN feedback form */ "vpn.feedback-form.title" = "Help Improve the DuckDuckGo VPN"; +/* VPN settings screen cell text for adding the VPN widget to the home screen */ +"vpn.settings.add.widget" = "Add VPN Widget to Home Screen"; + /* Title for the button to enable push notifications in system settings */ "waitlist.allow-notifications" = "Autoriser les notifications"; @@ -2439,6 +2516,9 @@ /* Title for the share sheet entry */ "waitlist.share-sheet.title" = "Vous avez reçu une invitation !"; +/* No comment provided by engineer. */ +"We want to make using passwords in DuckDuckGo better." = "We want to make using passwords in DuckDuckGo better."; + /* Confirmation message */ "web.url.remove.favorite.done" = "Favori supprimé"; diff --git a/DuckDuckGo/hr.lproj/Autocomplete.strings b/DuckDuckGo/hr.lproj/Autocomplete.strings deleted file mode 100644 index a7c09203da..0000000000 --- a/DuckDuckGo/hr.lproj/Autocomplete.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Class = "UILabel"; text = "Label"; ObjectID = "5ag-a1-mlA"; */ -"5ag-a1-mlA.text" = "Label"; - -/* Class = "UILabel"; text = "Label"; ObjectID = "eiv-Cq-E2r"; */ -"eiv-Cq-E2r.text" = "Label"; - -/* Class = "UILabel"; text = "No Suggestions"; ObjectID = "gPe-Cv-14j"; */ -"gPe-Cv-14j.text" = "Nema prijedloga"; - -/* Class = "UIButton"; accessibilityLabel = "Autocomplete suggestion"; ObjectID = "N97-eI-Lps"; */ -"N97-eI-Lps.accessibilityLabel" = "Prijedlog za automatsko ispunjavanje"; - diff --git a/DuckDuckGo/hr.lproj/Localizable.strings b/DuckDuckGo/hr.lproj/Localizable.strings index 3388a75dfc..ad19e1ecd7 100644 --- a/DuckDuckGo/hr.lproj/Localizable.strings +++ b/DuckDuckGo/hr.lproj/Localizable.strings @@ -119,7 +119,7 @@ "addWidget.description" = "Čeka vas brzi pristup privatnom pretraživanju i omiljenim mrežnim mjestima."; /* No comment provided by engineer. */ -"addWidget.settings.firstParagraph" = "Duže zadržite prst na početnom zaslonu za ulaz u način tresenja."; +"addWidget.settings.firstParagraph" = "Long-press on the Home Screen to enter jiggle mode."; /* Replacement string is a plus button icon. */ "addWidget.settings.secondParagraph.%@" = "Dodirnite gumb plus %@."; @@ -133,15 +133,48 @@ /* No comment provided by engineer. */ "alert.message.bookmarkAll" = "Postojeće knjižne oznake neće se duplicirati."; +/* Description for alert shown when sync error occurs because of bad data */ +"alert.sync-bad-data-error-description" = "Neke oznake ili lozinke formatirane su pogrešno, ili su preduge i nisu sinkronizirane."; + +/* Description for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-description" = "Dosegnut je maksimalni broj knjižnih oznaka. Izbriši neke oznake da bi nastavio sinkronizaciju."; + +/* Title for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-title" = "Sinkronizacija knjižnih oznaka je pauzirana"; + +/* Description for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-description" = "Dosegnut je maksimalni broj lozinki. Izbriši neke lozinke za nastavak sinkronizacije."; + +/* Title for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-title" = "Sinkronizacija lozinke je pauzirana"; + /* Title for sync error alert */ "alert.sync-error" = "Pogreška Sinkronizacije i sigurnosnog kopiranja"; +/* Sync error alert action button title, takes the user to the sync settings page. */ +"alert.sync-error-action" = "Postavke sinkronizacije"; + +/* Description for alert shown when user logged off from sync */ +"alert.sync-invalid-login-error-description" = "Sinkronizacija je pauzirana. Ako želiš nastaviti sinkronizirati ovaj uređaj, ponovno se poveži pomoću drugog uređaja ili šifre za oporavak."; + /* Learn more button in alert */ "alert.sync-paused-alert-learn-more-button" = "Saznajte više"; /* Confirmation button in alert */ "alert.sync-paused-alert-ok-button" = "U redu"; +/* Title for alert shown when sync paused for an error */ +"alert.sync-paused-title" = "Sinkronizacija je pauzirana"; + +/* Description for alert shown when sync error occurs because of too many requests */ +"alert.sync-too-many-requests-error-description" = "Sinkronizacija i sigurnosno kopiranje Sync & Backup privremeno su nedostupni."; + +/* Title of the warning message that tells the user that there was an error with the sync feature. */ +"alert.sync.warning.sync-error" = "Pogreška pri sinkronizaciji"; + +/* Title of the warning message */ +"alert.sync.warning.sync-paused" = "Sync & Backup pauziran"; + /* Question from confirmation dialog */ "alert.title.bookmarkAll" = "Označi sve kartice?"; @@ -208,6 +241,15 @@ /* No comment provided by engineer. */ "autoclear.on" = "Uključeno"; +/* Subtitle for search history items */ +"autocomplete.history.search.duckduckgo" = "Pretraži DuckDuckGo"; + +/* The message text shown in suggestions */ +"autocomplete.history.warning.message" = "Prijedlozi sad uključuju nedavno posjećena web-mjesta. Ona se pohranjuju na tvom uređaju, a ne na DuckDuckGo poslužiteljima. Isključi u postavkama ili izbriši u bilo kojem trenutku pomoću Fire Buttona 🔥."; + +/* Title for message show in suggestions */ +"autocomplete.history.warning.title" = "Ista privatnost.\nBolji prijedlozi za pretraživanje!"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Onemogućeno"; @@ -1147,6 +1189,9 @@ /* Title for the get desktop browser feature */ "get.browser.title" = "Nabavi DuckDuckGo za Mac ili Windows"; +/* No comment provided by engineer. */ +"Help us improve!" = "Help us improve!"; + /* No comment provided by engineer. */ "home.row.onboarding.header" = "Dodaj DuckDuckGo na svoj početni zaslon!"; @@ -1603,6 +1648,26 @@ /* Deactivate button */ "pm.deactivate" = "Deactivate"; +/* Button title for sync bookmarks limits exceeded warning to go to manage bookmarks */ +"prefrences.sync.bookmarks-limit-exceeded-action" = "Upravljanje knjižnim oznakama"; + +/* Description for sync bookmarks limits exceeded warning */ +"prefrences.sync.bookmarks-limit-exceeded-description" = "Dosegnut je maksimalni broj knjižnih oznaka. Izbriši neke za nastavak sinkronizacije."; + +/* Button title for sync credentials limits exceeded warning to go to manage passwords */ +"prefrences.sync.credentials-limit-exceeded-action" = "Upravljanje lozinkama…"; + +/* Description for sync credentials limits exceeded warning */ +"prefrences.sync.credentials-limit-exceeded-description" = "Dosegnut je maksimalni broj lozinki. Izbriši neke za nastavak sinkronizacije."; + +/* Description invalid credentials error when syncing. + Description of incorrectly formatted data error when syncing. + Description of too many requests error when syncing. */ +"prefrences.sync.invalid-login-description" = "Sinkronizacija je naišla na pogrešku. Pokušaj onemogućiti sinkronizaciju na ovom uređaju, a zatim se ponovno poveži pomoću drugog uređaja ili šifre za oporavak."; + +/* Title for sync limits exceeded warning */ +"prefrences.sync.limit-exceeded-title" = "Sinkronizacija je zaustavljena"; + /* No comment provided by engineer. */ "preserveLogins.domain.list.footer" = "Web-mjesta oslanjaju se na kolačiće kako bi te zadržala prijavljenim/om. Kada je web-mjesto označeno kao zaštićeno, kolačići neće biti izbrisani i ostat ćeš prijavljen/a čak i nakon upotrebe gumba Fire. I dalje blokiramo alate za praćenje trećih strana koje pronađemo na mrežnim mjestima označenim kao Fireproof."; @@ -1712,6 +1777,9 @@ /* Settings screen cell for autocomplete */ "settings.autocomplete" = "Prikaži prijedloge za automatsko ispunjavanje"; +/* Settings label for enabling or disabling recently visited sites */ +"settings.autocomplete.recentlyvisited" = "Nedavno posjećena web-mjesta"; + /* Settings screen cell text for Application Lock */ "settings.autolock" = "Zaključavanje aplikacije"; @@ -2241,6 +2309,9 @@ /* Tab Switcher Accessibility Label */ "tab.switcher.accessibility.label" = "Mjenjač kartica"; +/* No comment provided by engineer. */ +"Take Survey" = "Take Survey"; + /* Description text for the text size adjustment setting */ "textSize.description" = "Odaberite željenu veličinu teksta. Web stranice koje pregledavate u DuckDuckGou prilagodit će se ovoj postavci."; @@ -2319,6 +2390,9 @@ /* Alert title for the alert when the Privacy Pro subscription expires */ "vpn.access-revoked.alert.title" = "VPN disconnected due to expired subscription"; +/* Title for the VPN widget onboarding screen */ +"vpn.addWidget.settings.title" = "Find and select DuckDuckGo. Then swipe to VPN and select Add Widget."; + /* Title for the Cancel button of the VPN feedback form */ "vpn.feedback-form.button.cancel" = "Cancel"; @@ -2388,6 +2462,9 @@ /* Title for each screen of the VPN feedback form */ "vpn.feedback-form.title" = "Help Improve the DuckDuckGo VPN"; +/* VPN settings screen cell text for adding the VPN widget to the home screen */ +"vpn.settings.add.widget" = "Add VPN Widget to Home Screen"; + /* Title for the button to enable push notifications in system settings */ "waitlist.allow-notifications" = "Dopusti obavijesti"; @@ -2439,6 +2516,9 @@ /* Title for the share sheet entry */ "waitlist.share-sheet.title" = "Pozvani ste!"; +/* No comment provided by engineer. */ +"We want to make using passwords in DuckDuckGo better." = "We want to make using passwords in DuckDuckGo better."; + /* Confirmation message */ "web.url.remove.favorite.done" = "Omiljena je stavka uklonjena"; diff --git a/DuckDuckGo/hu.lproj/Autocomplete.strings b/DuckDuckGo/hu.lproj/Autocomplete.strings deleted file mode 100644 index 2e992007a1..0000000000 --- a/DuckDuckGo/hu.lproj/Autocomplete.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Class = "UILabel"; text = "Label"; ObjectID = "5ag-a1-mlA"; */ -"5ag-a1-mlA.text" = "Címke"; - -/* Class = "UILabel"; text = "Label"; ObjectID = "eiv-Cq-E2r"; */ -"eiv-Cq-E2r.text" = "Címke"; - -/* Class = "UILabel"; text = "No Suggestions"; ObjectID = "gPe-Cv-14j"; */ -"gPe-Cv-14j.text" = "Nincs javaslat"; - -/* Class = "UIButton"; accessibilityLabel = "Autocomplete suggestion"; ObjectID = "N97-eI-Lps"; */ -"N97-eI-Lps.accessibilityLabel" = "Automatikus kiegészítési javaslat"; - diff --git a/DuckDuckGo/hu.lproj/Localizable.strings b/DuckDuckGo/hu.lproj/Localizable.strings index 4877ac6f61..3f3f516662 100644 --- a/DuckDuckGo/hu.lproj/Localizable.strings +++ b/DuckDuckGo/hu.lproj/Localizable.strings @@ -119,7 +119,7 @@ "addWidget.description" = "Gyors hozzáférés a privát kereséshez és kedvenc weboldalaidhoz."; /* No comment provided by engineer. */ -"addWidget.settings.firstParagraph" = "Az ugráló ikonos üzemmódba lépéshez tartsd nyomva hosszan a kezdőképernyőt."; +"addWidget.settings.firstParagraph" = "Long-press on the Home Screen to enter jiggle mode."; /* Replacement string is a plus button icon. */ "addWidget.settings.secondParagraph.%@" = "Koppints a plusz %@ gombra."; @@ -133,15 +133,48 @@ /* No comment provided by engineer. */ "alert.message.bookmarkAll" = "A meglévő könyvjelzők nem kettőződnek."; +/* Description for alert shown when sync error occurs because of bad data */ +"alert.sync-bad-data-error-description" = "Egyes könyvjelzők vagy jelszavak helytelen formátumúak vagy túl hosszúak, és nem lettek szinkronizálva."; + +/* Description for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-description" = "Elérted a könyvjelzők maximális számát. A szinkronizálás folytatásához törölj néhány könyvjelzőt."; + +/* Title for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-title" = "A könyvjelző-szinkronizálás szünetel"; + +/* Description for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-description" = "Elérted a jelszavak maximális számát. A szinkronizálás folytatásához törölj néhány jelszót."; + +/* Title for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-title" = "A jelszó-szinkronizálás szünetel"; + /* Title for sync error alert */ "alert.sync-error" = "Szinkronizálási és biztonsági mentési hiba"; +/* Sync error alert action button title, takes the user to the sync settings page. */ +"alert.sync-error-action" = "Beállítások szinkronizálása"; + +/* Description for alert shown when user logged off from sync */ +"alert.sync-invalid-login-error-description" = "A szinkronizálás szünetel. Ha folytatni szeretnéd az eszköz szinkronizálását, csatlakozz újra egy másik eszközzel vagy a helyreállítási kóddal."; + /* Learn more button in alert */ "alert.sync-paused-alert-learn-more-button" = "További részletek"; /* Confirmation button in alert */ "alert.sync-paused-alert-ok-button" = "OK"; +/* Title for alert shown when sync paused for an error */ +"alert.sync-paused-title" = "A szinkronizálás szünetel"; + +/* Description for alert shown when sync error occurs because of too many requests */ +"alert.sync-too-many-requests-error-description" = "A szinkronizálás és biztonsági mentés átmenetileg nem érhető el."; + +/* Title of the warning message that tells the user that there was an error with the sync feature. */ +"alert.sync.warning.sync-error" = "Szinkronizálási hiba"; + +/* Title of the warning message */ +"alert.sync.warning.sync-paused" = "A szinkronizálás és biztonsági mentés szünetel"; + /* Question from confirmation dialog */ "alert.title.bookmarkAll" = "Az összes lapot könyvjelzőzöd?"; @@ -208,6 +241,15 @@ /* No comment provided by engineer. */ "autoclear.on" = "Be"; +/* Subtitle for search history items */ +"autocomplete.history.search.duckduckgo" = "DuckDuckGo keresés"; + +/* The message text shown in suggestions */ +"autocomplete.history.warning.message" = "A javaslatok között már a legutóbb meglátogatott webhelyek is szerepelnek. Ezeket az eszközödön tároljuk, és soha nem a DuckDuckGo szerverein. A beállításokban kikapcsolhatod, vagy bármikor törölheted a 🔥 Tűz gombbal."; + +/* Title for message show in suggestions */ +"autocomplete.history.warning.title" = "Ugyanolyan adatvédelem.\nJobb keresési javaslatok!"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Letiltva"; @@ -1147,6 +1189,9 @@ /* Title for the get desktop browser feature */ "get.browser.title" = "Mac vagy Windows verziójú DuckDuckGo letöltése"; +/* No comment provided by engineer. */ +"Help us improve!" = "Help us improve!"; + /* No comment provided by engineer. */ "home.row.onboarding.header" = "Add hozzá a DuckDuckGo-t a kezdőképernyődhöz!"; @@ -1603,6 +1648,26 @@ /* Deactivate button */ "pm.deactivate" = "Deactivate"; +/* Button title for sync bookmarks limits exceeded warning to go to manage bookmarks */ +"prefrences.sync.bookmarks-limit-exceeded-action" = "Könyvjelzők kezelése"; + +/* Description for sync bookmarks limits exceeded warning */ +"prefrences.sync.bookmarks-limit-exceeded-description" = "Elérted a könyvjelzők maximális számát. A szinkronizálás folytatásához törölj néhányat."; + +/* Button title for sync credentials limits exceeded warning to go to manage passwords */ +"prefrences.sync.credentials-limit-exceeded-action" = "Jelszavak kezelése…"; + +/* Description for sync credentials limits exceeded warning */ +"prefrences.sync.credentials-limit-exceeded-description" = "Elérted a jelszavak maximális számát. A szinkronizálás folytatásához törölj néhányat."; + +/* Description invalid credentials error when syncing. + Description of incorrectly formatted data error when syncing. + Description of too many requests error when syncing. */ +"prefrences.sync.invalid-login-description" = "Szinkronizálási hiba történt. Próbáld meg kikapcsolni a szinkronizálást ezen az eszközön, majd csatlakozz újra egy másik eszközzel vagy a helyreállítási kóddal."; + +/* Title for sync limits exceeded warning */ +"prefrences.sync.limit-exceeded-title" = "Szinkronizálás szüneteltetve"; + /* No comment provided by engineer. */ "preserveLogins.domain.list.footer" = "A webhelyek sütik segítségével teszik lehetővé, hogy bejelentkezve maradhass. Ha tűzállóvá teszel egy weboldalt, a sütik nem törlődnek, és még a Tűz gomb használata után is bejelentkezve maradsz. A tűzálló webhelyeken a külső felek nyomkövetőit azonban blokkoljuk."; @@ -1712,6 +1777,9 @@ /* Settings screen cell for autocomplete */ "settings.autocomplete" = "Automatikus kiegészítési javaslatok megjelenítése"; +/* Settings label for enabling or disabling recently visited sites */ +"settings.autocomplete.recentlyvisited" = "Legutóbb meglátogatott webhelyek"; + /* Settings screen cell text for Application Lock */ "settings.autolock" = "Alkalmazás zárolás"; @@ -2241,6 +2309,9 @@ /* Tab Switcher Accessibility Label */ "tab.switcher.accessibility.label" = "Lapváltó"; +/* No comment provided by engineer. */ +"Take Survey" = "Take Survey"; + /* Description text for the text size adjustment setting */ "textSize.description" = "Válaszd ki a kívánt szövegméretet. A DuckDuckGo ennek használatával jeleníti meg a webhelyeket."; @@ -2319,6 +2390,9 @@ /* Alert title for the alert when the Privacy Pro subscription expires */ "vpn.access-revoked.alert.title" = "VPN disconnected due to expired subscription"; +/* Title for the VPN widget onboarding screen */ +"vpn.addWidget.settings.title" = "Find and select DuckDuckGo. Then swipe to VPN and select Add Widget."; + /* Title for the Cancel button of the VPN feedback form */ "vpn.feedback-form.button.cancel" = "Cancel"; @@ -2388,6 +2462,9 @@ /* Title for each screen of the VPN feedback form */ "vpn.feedback-form.title" = "Help Improve the DuckDuckGo VPN"; +/* VPN settings screen cell text for adding the VPN widget to the home screen */ +"vpn.settings.add.widget" = "Add VPN Widget to Home Screen"; + /* Title for the button to enable push notifications in system settings */ "waitlist.allow-notifications" = "Értesítések engedélyezése"; @@ -2439,6 +2516,9 @@ /* Title for the share sheet entry */ "waitlist.share-sheet.title" = "Meghívót kaptál!"; +/* No comment provided by engineer. */ +"We want to make using passwords in DuckDuckGo better." = "We want to make using passwords in DuckDuckGo better."; + /* Confirmation message */ "web.url.remove.favorite.done" = "Kedvenc eltávolítva"; diff --git a/DuckDuckGo/it.lproj/Autocomplete.strings b/DuckDuckGo/it.lproj/Autocomplete.strings deleted file mode 100644 index fc86e7818a..0000000000 --- a/DuckDuckGo/it.lproj/Autocomplete.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Class = "UILabel"; text = "Label"; ObjectID = "5ag-a1-mlA"; */ -"5ag-a1-mlA.text" = "Etichetta"; - -/* Class = "UILabel"; text = "Label"; ObjectID = "eiv-Cq-E2r"; */ -"eiv-Cq-E2r.text" = "Etichetta"; - -/* Class = "UILabel"; text = "No Suggestions"; ObjectID = "gPe-Cv-14j"; */ -"gPe-Cv-14j.text" = "Nessun suggerimento"; - -/* Class = "UIButton"; accessibilityLabel = "Autocomplete suggestion"; ObjectID = "N97-eI-Lps"; */ -"N97-eI-Lps.accessibilityLabel" = "Mostra suggerimenti di completamento automatico"; - diff --git a/DuckDuckGo/it.lproj/Localizable.strings b/DuckDuckGo/it.lproj/Localizable.strings index c4b3e492d7..df6af96d5a 100644 --- a/DuckDuckGo/it.lproj/Localizable.strings +++ b/DuckDuckGo/it.lproj/Localizable.strings @@ -119,7 +119,7 @@ "addWidget.description" = "Accedi rapidamente alla navigazione in incognito e ai tuoi siti preferiti."; /* No comment provided by engineer. */ -"addWidget.settings.firstParagraph" = "Premi a lungo sulla schermata iniziale per accedere alla modalità Jiggle."; +"addWidget.settings.firstParagraph" = "Long-press on the Home Screen to enter jiggle mode."; /* Replacement string is a plus button icon. */ "addWidget.settings.secondParagraph.%@" = "Tocca il pulsante più %@."; @@ -133,15 +133,48 @@ /* No comment provided by engineer. */ "alert.message.bookmarkAll" = "I segnalibri già presenti non saranno duplicati."; +/* Description for alert shown when sync error occurs because of bad data */ +"alert.sync-bad-data-error-description" = "Alcuni segnalibri o password sono formattati in modo errato o sono troppo lunghi e non sono stati sincronizzati."; + +/* Description for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-description" = "Hai raggiunto il numero massimo di segnalibri. Elimina alcuni segnalibri per riprendere la sincronizzazione."; + +/* Title for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-title" = "Sync dei segnalibri è in pausa"; + +/* Description for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-description" = "Hai raggiunto il numero massimo di password. Elimina alcune password per riprendere la sincronizzazione."; + +/* Title for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-title" = "Sync delle password è in pausa"; + /* Title for sync error alert */ "alert.sync-error" = "Errore di sincronizzazione e backup"; +/* Sync error alert action button title, takes the user to the sync settings page. */ +"alert.sync-error-action" = "Impostazioni Sync"; + +/* Description for alert shown when user logged off from sync */ +"alert.sync-invalid-login-error-description" = "La sincronizzazione è stata sospesa. Se vuoi continuare a sincronizzare questo dispositivo, riconnettiti utilizzando un altro dispositivo o il tuo codice di ripristino."; + /* Learn more button in alert */ "alert.sync-paused-alert-learn-more-button" = "Ulteriori informazioni"; /* Confirmation button in alert */ "alert.sync-paused-alert-ok-button" = "OK"; +/* Title for alert shown when sync paused for an error */ +"alert.sync-paused-title" = "Sync è in pausa"; + +/* Description for alert shown when sync error occurs because of too many requests */ +"alert.sync-too-many-requests-error-description" = "Sync & Backup non è temporaneamente disponibile."; + +/* Title of the warning message that tells the user that there was an error with the sync feature. */ +"alert.sync.warning.sync-error" = "Errore di sincronizzazione"; + +/* Title of the warning message */ +"alert.sync.warning.sync-paused" = "Sincronizzazione e Backup è in pausa"; + /* Question from confirmation dialog */ "alert.title.bookmarkAll" = "Creare un segnalibro con tutte le schede?"; @@ -208,6 +241,15 @@ /* No comment provided by engineer. */ "autoclear.on" = "On"; +/* Subtitle for search history items */ +"autocomplete.history.search.duckduckgo" = "Cerca DuckDuckGo"; + +/* The message text shown in suggestions */ +"autocomplete.history.warning.message" = "Ora i suggerimenti includono i siti visitati di recente. Vengono archiviati sul tuo dispositivo e mai sui server di DuckDuckGo. Disattiva la funzione nelle impostazioni o cancellala in qualsiasi momento con il 🔥 Fire Button."; + +/* Title for message show in suggestions */ +"autocomplete.history.warning.title" = "La stessa privacy.\nSuggerimenti di ricerca migliori!"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Disattivato"; @@ -1147,6 +1189,9 @@ /* Title for the get desktop browser feature */ "get.browser.title" = "Scarica DuckDuckGo per Mac o Windows"; +/* No comment provided by engineer. */ +"Help us improve!" = "Help us improve!"; + /* No comment provided by engineer. */ "home.row.onboarding.header" = "Aggiungi DuckDuckGo alla tua schermata iniziale!"; @@ -1603,6 +1648,26 @@ /* Deactivate button */ "pm.deactivate" = "Deactivate"; +/* Button title for sync bookmarks limits exceeded warning to go to manage bookmarks */ +"prefrences.sync.bookmarks-limit-exceeded-action" = "Gestisci segnalibri"; + +/* Description for sync bookmarks limits exceeded warning */ +"prefrences.sync.bookmarks-limit-exceeded-description" = "Hai raggiunto il numero massimo di segnalibri. Eliminane alcuni per riprendere la sincronizzazione."; + +/* Button title for sync credentials limits exceeded warning to go to manage passwords */ +"prefrences.sync.credentials-limit-exceeded-action" = "Gestisci password…"; + +/* Description for sync credentials limits exceeded warning */ +"prefrences.sync.credentials-limit-exceeded-description" = "Hai raggiunto il numero massimo di password. Eliminane alcuni per riprendere la sincronizzazione."; + +/* Description invalid credentials error when syncing. + Description of incorrectly formatted data error when syncing. + Description of too many requests error when syncing. */ +"prefrences.sync.invalid-login-description" = "Sync ha riscontrato un errore. Prova a disattivare la sincronizzazione su questo dispositivo e riconnettiti utilizzando un altro dispositivo o il codice di ripristino."; + +/* Title for sync limits exceeded warning */ +"prefrences.sync.limit-exceeded-title" = "Sincronizzazione in pausa"; + /* No comment provided by engineer. */ "preserveLogins.domain.list.footer" = "Per mantenere l'accesso degli utenti, i siti web si affidano ai cookie. Quando attivi la Protezione dal fuoco per un sito, i cookie non verranno cancellati e manterrai l'accesso, anche dopo aver usato il Fire Button (icona del fuoco). Continueremo a bloccare i tracker di terze parti anche sui siti ignifughi."; @@ -1712,6 +1777,9 @@ /* Settings screen cell for autocomplete */ "settings.autocomplete" = "Mostra suggerimenti di completamento automatico"; +/* Settings label for enabling or disabling recently visited sites */ +"settings.autocomplete.recentlyvisited" = "Siti visitati di recente"; + /* Settings screen cell text for Application Lock */ "settings.autolock" = "Blocco applicazione"; @@ -2241,6 +2309,9 @@ /* Tab Switcher Accessibility Label */ "tab.switcher.accessibility.label" = "Selettore schede"; +/* No comment provided by engineer. */ +"Take Survey" = "Take Survey"; + /* Description text for the text size adjustment setting */ "textSize.description" = "Scegli le dimensioni del testo che preferisci. I siti web che visualizzi su DuckDuckGo si adatteranno a questa impostazione."; @@ -2319,6 +2390,9 @@ /* Alert title for the alert when the Privacy Pro subscription expires */ "vpn.access-revoked.alert.title" = "VPN disconnected due to expired subscription"; +/* Title for the VPN widget onboarding screen */ +"vpn.addWidget.settings.title" = "Find and select DuckDuckGo. Then swipe to VPN and select Add Widget."; + /* Title for the Cancel button of the VPN feedback form */ "vpn.feedback-form.button.cancel" = "Cancel"; @@ -2388,6 +2462,9 @@ /* Title for each screen of the VPN feedback form */ "vpn.feedback-form.title" = "Help Improve the DuckDuckGo VPN"; +/* VPN settings screen cell text for adding the VPN widget to the home screen */ +"vpn.settings.add.widget" = "Add VPN Widget to Home Screen"; + /* Title for the button to enable push notifications in system settings */ "waitlist.allow-notifications" = "Consenti notifiche"; @@ -2439,6 +2516,9 @@ /* Title for the share sheet entry */ "waitlist.share-sheet.title" = "Hai ricevuto un invito!"; +/* No comment provided by engineer. */ +"We want to make using passwords in DuckDuckGo better." = "We want to make using passwords in DuckDuckGo better."; + /* Confirmation message */ "web.url.remove.favorite.done" = "Preferito rimosso"; diff --git a/DuckDuckGo/lt.lproj/Autocomplete.strings b/DuckDuckGo/lt.lproj/Autocomplete.strings deleted file mode 100644 index 5a588567ea..0000000000 --- a/DuckDuckGo/lt.lproj/Autocomplete.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Class = "UILabel"; text = "Label"; ObjectID = "5ag-a1-mlA"; */ -"5ag-a1-mlA.text" = "Etiketė"; - -/* Class = "UILabel"; text = "Label"; ObjectID = "eiv-Cq-E2r"; */ -"eiv-Cq-E2r.text" = "Etiketė"; - -/* Class = "UILabel"; text = "No Suggestions"; ObjectID = "gPe-Cv-14j"; */ -"gPe-Cv-14j.text" = "Pasiūlymų nėra"; - -/* Class = "UIButton"; accessibilityLabel = "Autocomplete suggestion"; ObjectID = "N97-eI-Lps"; */ -"N97-eI-Lps.accessibilityLabel" = "Automatinio užbaigimo pasiūlymas"; - diff --git a/DuckDuckGo/lt.lproj/Localizable.strings b/DuckDuckGo/lt.lproj/Localizable.strings index 963d45207f..54fffb5065 100644 --- a/DuckDuckGo/lt.lproj/Localizable.strings +++ b/DuckDuckGo/lt.lproj/Localizable.strings @@ -119,7 +119,7 @@ "addWidget.description" = "Greitai pasiekite privačią paiešką ir mėgstamas svetaines."; /* No comment provided by engineer. */ -"addWidget.settings.firstParagraph" = "Ilgai spauskite pagrindinį ekraną, kad įjungtumėte į programėlių perkėlimo ir ištrynimo režimą."; +"addWidget.settings.firstParagraph" = "Long-press on the Home Screen to enter jiggle mode."; /* Replacement string is a plus button icon. */ "addWidget.settings.secondParagraph.%@" = "Bakstelėkite pliuso mygtuką %@."; @@ -133,15 +133,48 @@ /* No comment provided by engineer. */ "alert.message.bookmarkAll" = "Esamos žymės nebus kopijuojamos."; +/* Description for alert shown when sync error occurs because of bad data */ +"alert.sync-bad-data-error-description" = "Kai kurios žymės ar slaptažodžiai suformatuoti neteisingai arba yra per ilgi ir nebuvo sinchronizuoti."; + +/* Description for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-description" = "Pasiekėte didžiausią žymių skaičių. Ištrinkite kai kurias žymes, kad tęstumėte sinchronizavimą."; + +/* Title for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-title" = "Žymių sinchronizavimas pristabdytas"; + +/* Description for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-description" = "Pasiekėte maksimalų slaptažodžių skaičių. Ištrinkite kai kuriuos slaptažodžius, kad tęstumėte sinchronizavimą."; + +/* Title for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-title" = "Slaptažodžių sinchronizavimas pristabdytas"; + /* Title for sync error alert */ "alert.sync-error" = "Sinchronizavimo ir atsarginės kopijos kūrimo klaida"; +/* Sync error alert action button title, takes the user to the sync settings page. */ +"alert.sync-error-action" = "Sinchronizavimo parametrai"; + +/* Description for alert shown when user logged off from sync */ +"alert.sync-invalid-login-error-description" = "Sinchronizavimas buvo sustabdytas. Jei norite toliau sinchronizuoti šį įrenginį, iš naujo prisijunkite naudodami kitą įrenginį arba atkūrimo kodą."; + /* Learn more button in alert */ "alert.sync-paused-alert-learn-more-button" = "Sužinoti daugiau"; /* Confirmation button in alert */ "alert.sync-paused-alert-ok-button" = "GERAI"; +/* Title for alert shown when sync paused for an error */ +"alert.sync-paused-title" = "Sinchronizavimas pristabdytas"; + +/* Description for alert shown when sync error occurs because of too many requests */ +"alert.sync-too-many-requests-error-description" = "Sinchronizavimas ir atsarginės kopijos kūrimas laikinai nepasiekiami."; + +/* Title of the warning message that tells the user that there was an error with the sync feature. */ +"alert.sync.warning.sync-error" = "Sinchronizavimo klaida"; + +/* Title of the warning message */ +"alert.sync.warning.sync-paused" = "Sinchronizavimas ir atsarginės kopijos kūrimas pristabdyti"; + /* Question from confirmation dialog */ "alert.title.bookmarkAll" = "Pažymėti visus skirtukus?"; @@ -208,6 +241,15 @@ /* No comment provided by engineer. */ "autoclear.on" = "Įjungti"; +/* Subtitle for search history items */ +"autocomplete.history.search.duckduckgo" = "Ieškoti DuckDuckGo"; + +/* The message text shown in suggestions */ +"autocomplete.history.warning.message" = "Dabar į pasiūlymus įtraukiamos neseniai aplankytos svetainės. Jos saugomos jūsų įrenginyje ir niekada – „DuckDuckGo“ serveriuose. Išjunkite nustatymuose arba bet kada ištrinkite naudodami mygtuką 🔥 „Fire Button“."; + +/* Title for message show in suggestions */ +"autocomplete.history.warning.title" = "Tas pats privatumas.\nGeresni paieškos pasiūlymai!"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Išjungta"; @@ -1147,6 +1189,9 @@ /* Title for the get desktop browser feature */ "get.browser.title" = "Gaukite „DuckDuckGo“, skirtą „Mac“ arba „Windows“"; +/* No comment provided by engineer. */ +"Help us improve!" = "Help us improve!"; + /* No comment provided by engineer. */ "home.row.onboarding.header" = "Pridėti „DuckDuckGo“ prie pagrindinio ekrano!"; @@ -1603,6 +1648,26 @@ /* Deactivate button */ "pm.deactivate" = "Deactivate"; +/* Button title for sync bookmarks limits exceeded warning to go to manage bookmarks */ +"prefrences.sync.bookmarks-limit-exceeded-action" = "Tvarkyti žymes"; + +/* Description for sync bookmarks limits exceeded warning */ +"prefrences.sync.bookmarks-limit-exceeded-description" = "Pasiekėte didžiausią žymių skaičių. Ištrinkite kai kurias žymes, kad tęstumėte sinchronizavimą."; + +/* Button title for sync credentials limits exceeded warning to go to manage passwords */ +"prefrences.sync.credentials-limit-exceeded-action" = "Slaptažodžių valdymas…"; + +/* Description for sync credentials limits exceeded warning */ +"prefrences.sync.credentials-limit-exceeded-description" = "Pasiekėte maksimalų slaptažodžių skaičių. Ištrinkite kai kuriuos slaptažodžius, kad tęstumėte sinchronizavimą."; + +/* Description invalid credentials error when syncing. + Description of incorrectly formatted data error when syncing. + Description of too many requests error when syncing. */ +"prefrences.sync.invalid-login-description" = "Sinchronizuojant įvyko klaida. Pabandykite išjungti sinchronizavimą šiame prietaise ir vėl prisijungti naudodami kitą prietaisą arba atkūrimo kodą."; + +/* Title for sync limits exceeded warning */ +"prefrences.sync.limit-exceeded-title" = "Sinchronizavimas pristabdytas"; + /* No comment provided by engineer. */ "preserveLogins.domain.list.footer" = "Svetainės pagrįstos slapukais, kad liktumėte prisijungę. Apsaugojus svetainę, slapukai nėra ištrinami ir liksite prisijungę net paspaudę apsaugojimo mygtuką. Vis tiek blokuosime trečiųjų šalių stebėjimo priemones, randamas svetainėse, kuriose pasinaudosite parinktimi „Fireproof“."; @@ -1712,6 +1777,9 @@ /* Settings screen cell for autocomplete */ "settings.autocomplete" = "Rodyti automatinio užbaigimo pasiūlymus"; +/* Settings label for enabling or disabling recently visited sites */ +"settings.autocomplete.recentlyvisited" = "Neseniai aplankytos svetainės"; + /* Settings screen cell text for Application Lock */ "settings.autolock" = "Programos užraktas"; @@ -2241,6 +2309,9 @@ /* Tab Switcher Accessibility Label */ "tab.switcher.accessibility.label" = "Skirtukų perjungiklis"; +/* No comment provided by engineer. */ +"Take Survey" = "Take Survey"; + /* Description text for the text size adjustment setting */ "textSize.description" = "Pasirinkite norimą teksto dydį. „DuckDuckGo“ rodomos svetainės atitiks šį nustatymą."; @@ -2319,6 +2390,9 @@ /* Alert title for the alert when the Privacy Pro subscription expires */ "vpn.access-revoked.alert.title" = "VPN disconnected due to expired subscription"; +/* Title for the VPN widget onboarding screen */ +"vpn.addWidget.settings.title" = "Find and select DuckDuckGo. Then swipe to VPN and select Add Widget."; + /* Title for the Cancel button of the VPN feedback form */ "vpn.feedback-form.button.cancel" = "Cancel"; @@ -2388,6 +2462,9 @@ /* Title for each screen of the VPN feedback form */ "vpn.feedback-form.title" = "Help Improve the DuckDuckGo VPN"; +/* VPN settings screen cell text for adding the VPN widget to the home screen */ +"vpn.settings.add.widget" = "Add VPN Widget to Home Screen"; + /* Title for the button to enable push notifications in system settings */ "waitlist.allow-notifications" = "Leisti pranešimus"; @@ -2439,6 +2516,9 @@ /* Title for the share sheet entry */ "waitlist.share-sheet.title" = "Esate pakviesti!"; +/* No comment provided by engineer. */ +"We want to make using passwords in DuckDuckGo better." = "We want to make using passwords in DuckDuckGo better."; + /* Confirmation message */ "web.url.remove.favorite.done" = "Mėgstamas pašalintas"; diff --git a/DuckDuckGo/lv.lproj/Autocomplete.strings b/DuckDuckGo/lv.lproj/Autocomplete.strings deleted file mode 100644 index d07ece2fba..0000000000 --- a/DuckDuckGo/lv.lproj/Autocomplete.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Class = "UILabel"; text = "Label"; ObjectID = "5ag-a1-mlA"; */ -"5ag-a1-mlA.text" = "Etiķete"; - -/* Class = "UILabel"; text = "Label"; ObjectID = "eiv-Cq-E2r"; */ -"eiv-Cq-E2r.text" = "Etiķete"; - -/* Class = "UILabel"; text = "No Suggestions"; ObjectID = "gPe-Cv-14j"; */ -"gPe-Cv-14j.text" = "Nav ieteikumu"; - -/* Class = "UIButton"; accessibilityLabel = "Autocomplete suggestion"; ObjectID = "N97-eI-Lps"; */ -"N97-eI-Lps.accessibilityLabel" = "Automātiski pabeigt ieteikumu"; - diff --git a/DuckDuckGo/lv.lproj/Localizable.strings b/DuckDuckGo/lv.lproj/Localizable.strings index 4b4f1d907f..2a617016c7 100644 --- a/DuckDuckGo/lv.lproj/Localizable.strings +++ b/DuckDuckGo/lv.lproj/Localizable.strings @@ -119,7 +119,7 @@ "addWidget.description" = "Iegūsti ātru piekļuvi privātajai meklēšanai un savām mīļākajām vietnēm."; /* No comment provided by engineer. */ -"addWidget.settings.firstParagraph" = "Ilgi nospied sākuma ekrānā, lai pārietu pārkārtošanas režīmā."; +"addWidget.settings.firstParagraph" = "Long-press on the Home Screen to enter jiggle mode."; /* Replacement string is a plus button icon. */ "addWidget.settings.secondParagraph.%@" = "Pieskaries plus %@ pogai."; @@ -133,15 +133,48 @@ /* No comment provided by engineer. */ "alert.message.bookmarkAll" = "Esošās grāmatzīmes netiks dublētas."; +/* Description for alert shown when sync error occurs because of bad data */ +"alert.sync-bad-data-error-description" = "Dažas grāmatzīmes vai paroles ir formatētas nepareizi vai pārāk garas un netika sinhronizētas."; + +/* Description for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-description" = "Tu esi sasniedzis maksimālo grāmatzīmju skaitu. Lūdzu, izdzēs dažas grāmatzīmes, lai atsāktu sinhronizāciju."; + +/* Title for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-title" = "Grāmatzīmju sinhronizācija ir apturēta"; + +/* Description for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-description" = "Tu esi sasniedzis maksimālo paroļu skaitu. Lūdzu, izdzēs dažas paroles, lai atsāktu sinhronizāciju."; + +/* Title for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-title" = "Paroļu sinhronizācija ir apturēta"; + /* Title for sync error alert */ "alert.sync-error" = "Sinhronizācijas un dublēšanas kļūda"; +/* Sync error alert action button title, takes the user to the sync settings page. */ +"alert.sync-error-action" = "Sinhronizācijas iestatījumi"; + +/* Description for alert shown when user logged off from sync */ +"alert.sync-invalid-login-error-description" = "Sinhronizācija ir apturēta. Ja vēlies turpināt sinhronizēt šo ierīci, izveido savienojumu no jauna, izmantojot citu ierīci vai savu atgūšanas kodu."; + /* Learn more button in alert */ "alert.sync-paused-alert-learn-more-button" = "Uzzināt vairāk"; /* Confirmation button in alert */ "alert.sync-paused-alert-ok-button" = "Labi"; +/* Title for alert shown when sync paused for an error */ +"alert.sync-paused-title" = "Sinhronizācija ir apturēta"; + +/* Description for alert shown when sync error occurs because of too many requests */ +"alert.sync-too-many-requests-error-description" = "Sync & Backup uz laiku nav pieejams."; + +/* Title of the warning message that tells the user that there was an error with the sync feature. */ +"alert.sync.warning.sync-error" = "Sinhronizācijas kļūda"; + +/* Title of the warning message */ +"alert.sync.warning.sync-paused" = "Sinhronizācija un dublēšana ir pauzēta"; + /* Question from confirmation dialog */ "alert.title.bookmarkAll" = "Vai pievienot grāmatzīmes visām cilnēm?"; @@ -208,6 +241,15 @@ /* No comment provided by engineer. */ "autoclear.on" = "Ieslēgts"; +/* Subtitle for search history items */ +"autocomplete.history.search.duckduckgo" = "Meklēt DuckDuckGo"; + +/* The message text shown in suggestions */ +"autocomplete.history.warning.message" = "Ieteikumi tagad ietver nesen apmeklētās vietnes. Tās tiek saglabātas tikai tavā ierīcē, nevis DuckDuckGo serveros. Izslēdz iestatījumos vai jebkurā laikā notīri ar 🔥 Fire Button."; + +/* Title for message show in suggestions */ +"autocomplete.history.warning.title" = "Tas pats privātums.\nLabāki meklēšanas ieteikumi!"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Atspējota"; @@ -1147,6 +1189,9 @@ /* Title for the get desktop browser feature */ "get.browser.title" = "Iegūsti DuckDuckGo Mac vai Windows datoram"; +/* No comment provided by engineer. */ +"Help us improve!" = "Help us improve!"; + /* No comment provided by engineer. */ "home.row.onboarding.header" = "Pievieno DuckDuckGo savam sākuma ekrānam!"; @@ -1603,6 +1648,26 @@ /* Deactivate button */ "pm.deactivate" = "Deactivate"; +/* Button title for sync bookmarks limits exceeded warning to go to manage bookmarks */ +"prefrences.sync.bookmarks-limit-exceeded-action" = "Pārvaldīt grāmatzīmes"; + +/* Description for sync bookmarks limits exceeded warning */ +"prefrences.sync.bookmarks-limit-exceeded-description" = "Tu esi sasniedzis maksimālo grāmatzīmju skaitu. Lūdzu, izdzēs dažas, lai atsāktu sinhronizāciju."; + +/* Button title for sync credentials limits exceeded warning to go to manage passwords */ +"prefrences.sync.credentials-limit-exceeded-action" = "Pārvaldīt paroles…"; + +/* Description for sync credentials limits exceeded warning */ +"prefrences.sync.credentials-limit-exceeded-description" = "Tu esi sasniedzis maksimālo paroļu skaitu. Lūdzu, izdzēs dažas, lai atsāktu sinhronizāciju."; + +/* Description invalid credentials error when syncing. + Description of incorrectly formatted data error when syncing. + Description of too many requests error when syncing. */ +"prefrences.sync.invalid-login-description" = "Sinhronizācijā radās kļūda. Mēģini atspējot sinhronizāciju šajā ierīcē un pēc tam atkārtoti izveido savienojumu, izmantojot citu ierīci vai atgūšanas kodu."; + +/* Title for sync limits exceeded warning */ +"prefrences.sync.limit-exceeded-title" = "Sinhronizācija pārtraukta"; + /* No comment provided by engineer. */ "preserveLogins.domain.list.footer" = "Vietnes lieto sīkfailus, lai tu varētu turpināt būt pierakstījies(-usies). Padarot vietni ugunsdrošu, sīkfaili netiek dzēsti, bet tu vari turpināt būt pierakstījies(-usies) arī pēc Uguns pogas lietošanas. Mēs joprojām bloķēsim trešo pušu izsekotājus, kas atrodami ugunsdrošās vietnēs."; @@ -1712,6 +1777,9 @@ /* Settings screen cell for autocomplete */ "settings.autocomplete" = "Automātiski pabeigt ieteikumus"; +/* Settings label for enabling or disabling recently visited sites */ +"settings.autocomplete.recentlyvisited" = "Nesen apmeklētās vietnes"; + /* Settings screen cell text for Application Lock */ "settings.autolock" = "Lietojumprogrammas bloķēšana"; @@ -2241,6 +2309,9 @@ /* Tab Switcher Accessibility Label */ "tab.switcher.accessibility.label" = "Ciļņu pārslēdzējs"; +/* No comment provided by engineer. */ +"Take Survey" = "Take Survey"; + /* Description text for the text size adjustment setting */ "textSize.description" = "Izvēlies burtu izmēru. Vietnes tavā DuckDuckGo tam pielāgosies."; @@ -2319,6 +2390,9 @@ /* Alert title for the alert when the Privacy Pro subscription expires */ "vpn.access-revoked.alert.title" = "VPN disconnected due to expired subscription"; +/* Title for the VPN widget onboarding screen */ +"vpn.addWidget.settings.title" = "Find and select DuckDuckGo. Then swipe to VPN and select Add Widget."; + /* Title for the Cancel button of the VPN feedback form */ "vpn.feedback-form.button.cancel" = "Cancel"; @@ -2388,6 +2462,9 @@ /* Title for each screen of the VPN feedback form */ "vpn.feedback-form.title" = "Help Improve the DuckDuckGo VPN"; +/* VPN settings screen cell text for adding the VPN widget to the home screen */ +"vpn.settings.add.widget" = "Add VPN Widget to Home Screen"; + /* Title for the button to enable push notifications in system settings */ "waitlist.allow-notifications" = "Atļaut paziņojumus"; @@ -2439,6 +2516,9 @@ /* Title for the share sheet entry */ "waitlist.share-sheet.title" = "Tu esi uzaicināts!"; +/* No comment provided by engineer. */ +"We want to make using passwords in DuckDuckGo better." = "We want to make using passwords in DuckDuckGo better."; + /* Confirmation message */ "web.url.remove.favorite.done" = "Izlase ir noņemta"; diff --git a/DuckDuckGo/nb.lproj/Autocomplete.strings b/DuckDuckGo/nb.lproj/Autocomplete.strings deleted file mode 100644 index ea1b6d87ca..0000000000 --- a/DuckDuckGo/nb.lproj/Autocomplete.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Class = "UILabel"; text = "Label"; ObjectID = "5ag-a1-mlA"; */ -"5ag-a1-mlA.text" = "Etikett"; - -/* Class = "UILabel"; text = "Label"; ObjectID = "eiv-Cq-E2r"; */ -"eiv-Cq-E2r.text" = "Etikett"; - -/* Class = "UILabel"; text = "No Suggestions"; ObjectID = "gPe-Cv-14j"; */ -"gPe-Cv-14j.text" = "Ingen forslag"; - -/* Class = "UIButton"; accessibilityLabel = "Autocomplete suggestion"; ObjectID = "N97-eI-Lps"; */ -"N97-eI-Lps.accessibilityLabel" = "Forslag fra autofullfør"; - diff --git a/DuckDuckGo/nb.lproj/Localizable.strings b/DuckDuckGo/nb.lproj/Localizable.strings index b46b0ba257..de0277cea6 100644 --- a/DuckDuckGo/nb.lproj/Localizable.strings +++ b/DuckDuckGo/nb.lproj/Localizable.strings @@ -119,7 +119,7 @@ "addWidget.description" = "Få rask tilgang til privat søk og nettstedene du liker best."; /* No comment provided by engineer. */ -"addWidget.settings.firstParagraph" = "Trykk lenge på startskjermen for å gå inn i ristemodus."; +"addWidget.settings.firstParagraph" = "Long-press on the Home Screen to enter jiggle mode."; /* Replacement string is a plus button icon. */ "addWidget.settings.secondParagraph.%@" = "Trykk på %@-knappen."; @@ -133,15 +133,48 @@ /* No comment provided by engineer. */ "alert.message.bookmarkAll" = "Eksisterende bokmerker blir ikke duplisert."; +/* Description for alert shown when sync error occurs because of bad data */ +"alert.sync-bad-data-error-description" = "Noen bokmerker eller passord er formatert feil eller for lange og ble ikke synkronisert."; + +/* Description for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-description" = "Du har nådd det maksimale antallet bokmerker. Slett noen bokmerker for å gjenoppta synkronisering."; + +/* Title for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-title" = "Synkronisering av bokmerker er satt på pause"; + +/* Description for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-description" = "Du har nådd det maksimale antallet passord. Slett noen passord for å gjenoppta synkronisering."; + +/* Title for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-title" = "Synkronisering av passord er satt på pause"; + /* Title for sync error alert */ "alert.sync-error" = "Synkroniserings- og sikkerhetskopieringsfeil"; +/* Sync error alert action button title, takes the user to the sync settings page. */ +"alert.sync-error-action" = "Synkroniser innstillinger"; + +/* Description for alert shown when user logged off from sync */ +"alert.sync-invalid-login-error-description" = "Synkronisering er satt på pause. Hvis du vil fortsette å synkronisere denne enheten, må du koble til på nytt ved hjelp av en annen enhet eller gjenopprettingskoden."; + /* Learn more button in alert */ "alert.sync-paused-alert-learn-more-button" = "Finn ut mer"; /* Confirmation button in alert */ "alert.sync-paused-alert-ok-button" = "OK"; +/* Title for alert shown when sync paused for an error */ +"alert.sync-paused-title" = "Synkronisering er satt på pause"; + +/* Description for alert shown when sync error occurs because of too many requests */ +"alert.sync-too-many-requests-error-description" = "Sync & Backup er midlertidig utilgjengelig."; + +/* Title of the warning message that tells the user that there was an error with the sync feature. */ +"alert.sync.warning.sync-error" = "Synkroniseringsfeil"; + +/* Title of the warning message */ +"alert.sync.warning.sync-paused" = "Synkronisering og sikkerhetskopiering er satt på pause"; + /* Question from confirmation dialog */ "alert.title.bookmarkAll" = "Legg til alle faner i bokmerker?"; @@ -208,6 +241,15 @@ /* No comment provided by engineer. */ "autoclear.on" = "På"; +/* Subtitle for search history items */ +"autocomplete.history.search.duckduckgo" = "Søk i DuckDuckGo"; + +/* The message text shown in suggestions */ +"autocomplete.history.warning.message" = "Forslag inkluderer nå nylig besøkte nettsteder. Disse er lagret på enheten din og aldri på DuckDuckGos servere. Slå dem av i innstillingene, eller slett dem når som helst med Fire Button 🔥."; + +/* Title for message show in suggestions */ +"autocomplete.history.warning.title" = "Samme personvern.\nBedre søkeforslag!"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Deaktivert"; @@ -1147,6 +1189,9 @@ /* Title for the get desktop browser feature */ "get.browser.title" = "Skaff deg DuckDuckGo for Mac eller Windows"; +/* No comment provided by engineer. */ +"Help us improve!" = "Help us improve!"; + /* No comment provided by engineer. */ "home.row.onboarding.header" = "Legg til DuckDuckGo på startskjermen!"; @@ -1603,6 +1648,26 @@ /* Deactivate button */ "pm.deactivate" = "Deactivate"; +/* Button title for sync bookmarks limits exceeded warning to go to manage bookmarks */ +"prefrences.sync.bookmarks-limit-exceeded-action" = "Administrer bokmerker"; + +/* Description for sync bookmarks limits exceeded warning */ +"prefrences.sync.bookmarks-limit-exceeded-description" = "Du har nådd det maksimale antallet bokmerker. Slett noen for å gjenoppta synkronisering."; + +/* Button title for sync credentials limits exceeded warning to go to manage passwords */ +"prefrences.sync.credentials-limit-exceeded-action" = "Administrer passord …"; + +/* Description for sync credentials limits exceeded warning */ +"prefrences.sync.credentials-limit-exceeded-description" = "Du har nådd det maksimale antallet passord. Slett noen for å gjenoppta synkronisering."; + +/* Description invalid credentials error when syncing. + Description of incorrectly formatted data error when syncing. + Description of too many requests error when syncing. */ +"prefrences.sync.invalid-login-description" = "Synkronisering oppdaget en feil. Prøv å slå av synkronisering på denne enheten og koble til igjen med en annen enhet eller gjenopprettingskoden."; + +/* Title for sync limits exceeded warning */ +"prefrences.sync.limit-exceeded-title" = "Synkronisering satt på pause"; + /* No comment provided by engineer. */ "preserveLogins.domain.list.footer" = "Nettsider anvender informasjonskapsler for å holde deg innlogget. Når du gjør en side brannsikker, slettes ikke informasjonskapslene, og du forblir innlogget, selv etter å ha brukt brannknappen. Vi blokkerer fortsatt tredjeparters sporingsanordninger som blir funnet på Brannsikre nettsteder."; @@ -1712,6 +1777,9 @@ /* Settings screen cell for autocomplete */ "settings.autocomplete" = "Vis forslag fra autofullføring"; +/* Settings label for enabling or disabling recently visited sites */ +"settings.autocomplete.recentlyvisited" = "Nylig besøkte nettsteder"; + /* Settings screen cell text for Application Lock */ "settings.autolock" = "Applås"; @@ -2241,6 +2309,9 @@ /* Tab Switcher Accessibility Label */ "tab.switcher.accessibility.label" = "Fanebytter"; +/* No comment provided by engineer. */ +"Take Survey" = "Take Survey"; + /* Description text for the text size adjustment setting */ "textSize.description" = "Velg ønsket tekststørrelse. Nettsteder du ser på i DuckDuckGo tilpasser seg den."; @@ -2319,6 +2390,9 @@ /* Alert title for the alert when the Privacy Pro subscription expires */ "vpn.access-revoked.alert.title" = "VPN disconnected due to expired subscription"; +/* Title for the VPN widget onboarding screen */ +"vpn.addWidget.settings.title" = "Find and select DuckDuckGo. Then swipe to VPN and select Add Widget."; + /* Title for the Cancel button of the VPN feedback form */ "vpn.feedback-form.button.cancel" = "Cancel"; @@ -2388,6 +2462,9 @@ /* Title for each screen of the VPN feedback form */ "vpn.feedback-form.title" = "Help Improve the DuckDuckGo VPN"; +/* VPN settings screen cell text for adding the VPN widget to the home screen */ +"vpn.settings.add.widget" = "Add VPN Widget to Home Screen"; + /* Title for the button to enable push notifications in system settings */ "waitlist.allow-notifications" = "Tillat varsler"; @@ -2439,6 +2516,9 @@ /* Title for the share sheet entry */ "waitlist.share-sheet.title" = "Du er invitert!"; +/* No comment provided by engineer. */ +"We want to make using passwords in DuckDuckGo better." = "We want to make using passwords in DuckDuckGo better."; + /* Confirmation message */ "web.url.remove.favorite.done" = "Favoritt er fjernet"; diff --git a/DuckDuckGo/nl.lproj/Autocomplete.strings b/DuckDuckGo/nl.lproj/Autocomplete.strings deleted file mode 100644 index 48aa629891..0000000000 --- a/DuckDuckGo/nl.lproj/Autocomplete.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Class = "UILabel"; text = "Label"; ObjectID = "5ag-a1-mlA"; */ -"5ag-a1-mlA.text" = "Label"; - -/* Class = "UILabel"; text = "Label"; ObjectID = "eiv-Cq-E2r"; */ -"eiv-Cq-E2r.text" = "Label"; - -/* Class = "UILabel"; text = "No Suggestions"; ObjectID = "gPe-Cv-14j"; */ -"gPe-Cv-14j.text" = "Geen suggesties"; - -/* Class = "UIButton"; accessibilityLabel = "Autocomplete suggestion"; ObjectID = "N97-eI-Lps"; */ -"N97-eI-Lps.accessibilityLabel" = "Suggestie automatisch aanvullen"; - diff --git a/DuckDuckGo/nl.lproj/Localizable.strings b/DuckDuckGo/nl.lproj/Localizable.strings index e8ac3a5227..099c05d528 100644 --- a/DuckDuckGo/nl.lproj/Localizable.strings +++ b/DuckDuckGo/nl.lproj/Localizable.strings @@ -119,7 +119,7 @@ "addWidget.description" = "Krijg snel toegang tot privézoekopdrachten en de sites die je leuk vindt."; /* No comment provided by engineer. */ -"addWidget.settings.firstParagraph" = "Druk lang op het startscherm om de schudmodus te gebruiken."; +"addWidget.settings.firstParagraph" = "Long-press on the Home Screen to enter jiggle mode."; /* Replacement string is a plus button icon. */ "addWidget.settings.secondParagraph.%@" = "Tik op de plus %@-knop."; @@ -133,15 +133,48 @@ /* No comment provided by engineer. */ "alert.message.bookmarkAll" = "Bestaande bladwijzers worden niet gedupliceerd."; +/* Description for alert shown when sync error occurs because of bad data */ +"alert.sync-bad-data-error-description" = "Sommige bladwijzers of wachtwoorden hebben de verkeerd indeling of te lang en werden niet gesynchroniseerd."; + +/* Description for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-description" = "Je hebt het maximumaantal bladwijzers bereikt. Verwijder enkele bladwijzers om de synchronisatie te hervatten."; + +/* Title for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-title" = "Synchronisatie van bladwijzers is gepauzeerd"; + +/* Description for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-description" = "Je hebt het maximumaantal wachtwoorden bereikt. Verwijder enkele wachtwoorden om de synchronisatie te hervatten."; + +/* Title for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-title" = "Synchronisatie van wachtwoorden is gepauzeerd"; + /* Title for sync error alert */ "alert.sync-error" = "Synchronisatie- en back-upfout"; +/* Sync error alert action button title, takes the user to the sync settings page. */ +"alert.sync-error-action" = "Synchronisatie-instellingen"; + +/* Description for alert shown when user logged off from sync */ +"alert.sync-invalid-login-error-description" = "Synchronisatie is onderbroken. Als je dit apparaat wilt blijven synchroniseren, maak dan opnieuw verbinding met een ander apparaat of met je herstelcode."; + /* Learn more button in alert */ "alert.sync-paused-alert-learn-more-button" = "Meer informatie"; /* Confirmation button in alert */ "alert.sync-paused-alert-ok-button" = "OK"; +/* Title for alert shown when sync paused for an error */ +"alert.sync-paused-title" = "Synchronisatie is gepauzeerd"; + +/* Description for alert shown when sync error occurs because of too many requests */ +"alert.sync-too-many-requests-error-description" = "'Synchronisatie en back-up' is tijdelijk niet beschikbaar."; + +/* Title of the warning message that tells the user that there was an error with the sync feature. */ +"alert.sync.warning.sync-error" = "Synchronisatiefout"; + +/* Title of the warning message */ +"alert.sync.warning.sync-paused" = "Synchronisatie en back-up is gepauzeerd"; + /* Question from confirmation dialog */ "alert.title.bookmarkAll" = "Alle tabbladen toevoegen als bladwijzer?"; @@ -208,6 +241,15 @@ /* No comment provided by engineer. */ "autoclear.on" = "Aan"; +/* Subtitle for search history items */ +"autocomplete.history.search.duckduckgo" = "Zoek in DuckDuckGo"; + +/* The message text shown in suggestions */ +"autocomplete.history.warning.message" = "Suggesties bevatten nu ook onlangs bezochte sites. Deze worden op je apparaat opgeslagen en nooit op de servers van DuckDuckGo. Je kunt suggesties uitschakelen in de instellingen of ze op elk gewenst moment wissen met de 🔥 Fire Button."; + +/* Title for message show in suggestions */ +"autocomplete.history.warning.title" = "Dezelfde privacy.\nBetere zoeksuggesties!"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Uitgeschakeld"; @@ -1147,6 +1189,9 @@ /* Title for the get desktop browser feature */ "get.browser.title" = "DuckDuckGo downloaden voor Mac of Windows"; +/* No comment provided by engineer. */ +"Help us improve!" = "Help us improve!"; + /* No comment provided by engineer. */ "home.row.onboarding.header" = "Voeg DuckDuckGo toe aan je beginscherm!"; @@ -1603,6 +1648,26 @@ /* Deactivate button */ "pm.deactivate" = "Deactivate"; +/* Button title for sync bookmarks limits exceeded warning to go to manage bookmarks */ +"prefrences.sync.bookmarks-limit-exceeded-action" = "Bladwijzers beheren"; + +/* Description for sync bookmarks limits exceeded warning */ +"prefrences.sync.bookmarks-limit-exceeded-description" = "Je hebt het maximumaantal bladwijzers bereikt. Verwijder er een paar om de synchronisatie te hervatten."; + +/* Button title for sync credentials limits exceeded warning to go to manage passwords */ +"prefrences.sync.credentials-limit-exceeded-action" = "Wachtwoorden beheren …"; + +/* Description for sync credentials limits exceeded warning */ +"prefrences.sync.credentials-limit-exceeded-description" = "Je hebt het maximumaantal wachtwoorden bereikt. Verwijder er een paar om de synchronisatie te hervatten."; + +/* Description invalid credentials error when syncing. + Description of incorrectly formatted data error when syncing. + Description of too many requests error when syncing. */ +"prefrences.sync.invalid-login-description" = "Er is een fout opgetreden bij het synchroniseren. Probeer synchronisatie op dit apparaat uit te schakelen en maak dan opnieuw verbinding met een ander apparaat of met je herstelcode."; + +/* Title for sync limits exceeded warning */ +"prefrences.sync.limit-exceeded-title" = "Synchronisatie onderbroken"; + /* No comment provided by engineer. */ "preserveLogins.domain.list.footer" = "Websites gebruiken cookies om te zorgen dat je ingelogd blijft. Als je een site brandveilig maakt, worden de cookies niet gewist en blijf je ingelogd, zelfs nadat je de vuurknop hebt gebruikt. Trackers van derden worden ook op brandveilige websites geblokkeerd."; @@ -1712,6 +1777,9 @@ /* Settings screen cell for autocomplete */ "settings.autocomplete" = "Suggesties voor automatisch aanvullen weergeven"; +/* Settings label for enabling or disabling recently visited sites */ +"settings.autocomplete.recentlyvisited" = "Onlangs bezochte websites"; + /* Settings screen cell text for Application Lock */ "settings.autolock" = "App-vergrendeling"; @@ -2241,6 +2309,9 @@ /* Tab Switcher Accessibility Label */ "tab.switcher.accessibility.label" = "Wisselen tussen tabbladen"; +/* No comment provided by engineer. */ +"Take Survey" = "Take Survey"; + /* Description text for the text size adjustment setting */ "textSize.description" = "Kies de gewenste tekstgrootte. Websites die je in DuckDuckGo bekijkt, worden aan de tekstgrootte aangepast."; @@ -2319,6 +2390,9 @@ /* Alert title for the alert when the Privacy Pro subscription expires */ "vpn.access-revoked.alert.title" = "VPN disconnected due to expired subscription"; +/* Title for the VPN widget onboarding screen */ +"vpn.addWidget.settings.title" = "Find and select DuckDuckGo. Then swipe to VPN and select Add Widget."; + /* Title for the Cancel button of the VPN feedback form */ "vpn.feedback-form.button.cancel" = "Cancel"; @@ -2388,6 +2462,9 @@ /* Title for each screen of the VPN feedback form */ "vpn.feedback-form.title" = "Help Improve the DuckDuckGo VPN"; +/* VPN settings screen cell text for adding the VPN widget to the home screen */ +"vpn.settings.add.widget" = "Add VPN Widget to Home Screen"; + /* Title for the button to enable push notifications in system settings */ "waitlist.allow-notifications" = "Meldingen toestaan"; @@ -2439,6 +2516,9 @@ /* Title for the share sheet entry */ "waitlist.share-sheet.title" = "Je bent uitgenodigd!"; +/* No comment provided by engineer. */ +"We want to make using passwords in DuckDuckGo better." = "We want to make using passwords in DuckDuckGo better."; + /* Confirmation message */ "web.url.remove.favorite.done" = "Favoriet verwijderd"; diff --git a/DuckDuckGo/pl.lproj/Autocomplete.strings b/DuckDuckGo/pl.lproj/Autocomplete.strings deleted file mode 100644 index fa6834a1c0..0000000000 --- a/DuckDuckGo/pl.lproj/Autocomplete.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Class = "UILabel"; text = "Label"; ObjectID = "5ag-a1-mlA"; */ -"5ag-a1-mlA.text" = "Etykieta"; - -/* Class = "UILabel"; text = "Label"; ObjectID = "eiv-Cq-E2r"; */ -"eiv-Cq-E2r.text" = "Etykieta"; - -/* Class = "UILabel"; text = "No Suggestions"; ObjectID = "gPe-Cv-14j"; */ -"gPe-Cv-14j.text" = "Brak sugestii"; - -/* Class = "UIButton"; accessibilityLabel = "Autocomplete suggestion"; ObjectID = "N97-eI-Lps"; */ -"N97-eI-Lps.accessibilityLabel" = "Propozycja autouzupełniania"; - diff --git a/DuckDuckGo/pl.lproj/Localizable.strings b/DuckDuckGo/pl.lproj/Localizable.strings index c7b6bf7a9b..3effd1b59e 100644 --- a/DuckDuckGo/pl.lproj/Localizable.strings +++ b/DuckDuckGo/pl.lproj/Localizable.strings @@ -119,7 +119,7 @@ "addWidget.description" = "Uzyskaj szybki dostęp do prywatnego wyszukiwania i ulubionych witryn."; /* No comment provided by engineer. */ -"addWidget.settings.firstParagraph" = "Przyciśnij i przytrzymaj na ekranie głównym, aby przejść do trybu porządkowania."; +"addWidget.settings.firstParagraph" = "Long-press on the Home Screen to enter jiggle mode."; /* Replacement string is a plus button icon. */ "addWidget.settings.secondParagraph.%@" = "Dotknij przycisku ze znakiem plus %@."; @@ -133,15 +133,48 @@ /* No comment provided by engineer. */ "alert.message.bookmarkAll" = "Istniejące zakładki nie zostaną zduplikowane."; +/* Description for alert shown when sync error occurs because of bad data */ +"alert.sync-bad-data-error-description" = "Niektóre zakładki lub hasła mają niepoprawny format lub są zbyt długie i nie zostały zsynchronizowane."; + +/* Description for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-description" = "Osiągnięto maksymalną liczbę zakładek. Usuń część zakładek, aby wznowić synchronizację."; + +/* Title for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-title" = "Synchronizacja zakładek jest wstrzymana"; + +/* Description for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-description" = "Osiągnięto maksymalną liczbę haseł. Usuń część haseł, aby wznowić synchronizację."; + +/* Title for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-title" = "Synchronizacja haseł jest wstrzymana"; + /* Title for sync error alert */ "alert.sync-error" = "Błąd synchronizacji i kopii zapasowej"; +/* Sync error alert action button title, takes the user to the sync settings page. */ +"alert.sync-error-action" = "Ustawienia synchronizacji"; + +/* Description for alert shown when user logged off from sync */ +"alert.sync-invalid-login-error-description" = "Synchronizacja została wstrzymana. Jeśli chcesz kontynuować synchronizowanie tego urządzenia, ponownie nawiąż połączenie za pomocą innego urządzenia lub użyj kodu odzyskiwania."; + /* Learn more button in alert */ "alert.sync-paused-alert-learn-more-button" = "Dowiedz się więcej"; /* Confirmation button in alert */ "alert.sync-paused-alert-ok-button" = "OK"; +/* Title for alert shown when sync paused for an error */ +"alert.sync-paused-title" = "Synchronizacja jest wstrzymana"; + +/* Description for alert shown when sync error occurs because of too many requests */ +"alert.sync-too-many-requests-error-description" = "Funkcja Sync & Backup jest tymczasowo niedostępna."; + +/* Title of the warning message that tells the user that there was an error with the sync feature. */ +"alert.sync.warning.sync-error" = "Błąd synchronizacji"; + +/* Title of the warning message */ +"alert.sync.warning.sync-paused" = "Synchronizacja i kopia zapasowa jest wstrzymana"; + /* Question from confirmation dialog */ "alert.title.bookmarkAll" = "Dodać wszystkie karty do zakładek?"; @@ -208,6 +241,15 @@ /* No comment provided by engineer. */ "autoclear.on" = "Włączone"; +/* Subtitle for search history items */ +"autocomplete.history.search.duckduckgo" = "Przeszukaj DuckDuckGo"; + +/* The message text shown in suggestions */ +"autocomplete.history.warning.message" = "Obecnie sugestie obejmują ostatnio odwiedzone witryny. Są one przechowywane na Twoim urządzeniu, nigdy na serwerach DuckDuckGo. Możesz je wyłączyć w ustawieniach lub w każdej chwili wyczyścić za pomocą przycisku 🔥 Fire Button."; + +/* Title for message show in suggestions */ +"autocomplete.history.warning.title" = "Ta sama prywatność.\nLepsze sugestie podczas wyszukiwania!"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Wyłączone"; @@ -1147,6 +1189,9 @@ /* Title for the get desktop browser feature */ "get.browser.title" = "Pobierz DuckDuckGo dla komputerów Mac lub systemu Windows"; +/* No comment provided by engineer. */ +"Help us improve!" = "Help us improve!"; + /* No comment provided by engineer. */ "home.row.onboarding.header" = "Dodaj DuckDuckGo do ekranu głównego!"; @@ -1603,6 +1648,26 @@ /* Deactivate button */ "pm.deactivate" = "Deactivate"; +/* Button title for sync bookmarks limits exceeded warning to go to manage bookmarks */ +"prefrences.sync.bookmarks-limit-exceeded-action" = "Zarządzaj zakładkami"; + +/* Description for sync bookmarks limits exceeded warning */ +"prefrences.sync.bookmarks-limit-exceeded-description" = "Osiągnięto maksymalną liczbę zakładek. Usuń część z nich, aby wznowić synchronizację."; + +/* Button title for sync credentials limits exceeded warning to go to manage passwords */ +"prefrences.sync.credentials-limit-exceeded-action" = "Zarządzaj hasłami…"; + +/* Description for sync credentials limits exceeded warning */ +"prefrences.sync.credentials-limit-exceeded-description" = "Osiągnięto maksymalną liczbę haseł. Usuń część z nich, aby wznowić synchronizację."; + +/* Description invalid credentials error when syncing. + Description of incorrectly formatted data error when syncing. + Description of too many requests error when syncing. */ +"prefrences.sync.invalid-login-description" = "Podczas synchronizacji wystąpił błąd. Spróbuj wyłączyć synchronizację na tym urządzeniu, a następnie ponownie nawiąż połączenie za pomocą innego urządzenia lub kodu odzyskiwania."; + +/* Title for sync limits exceeded warning */ +"prefrences.sync.limit-exceeded-title" = "Synchronizacja wstrzymana"; + /* No comment provided by engineer. */ "preserveLogins.domain.list.footer" = "W witrynach internetowych są wykorzystywane pliki cookie. Gdy użyjesz w witrynie funkcji zabezpieczenia, pliki cookie nie zostaną usunięte i pozostaniesz zalogowany(-a) nawet po użyciu przycisku Zabezpiecz. Nadal blokujemy narzędzia śledzące innych firm znalezione w zabezpieczonych witrynach."; @@ -1712,6 +1777,9 @@ /* Settings screen cell for autocomplete */ "settings.autocomplete" = "Pokaż sugestie autouzupełniania"; +/* Settings label for enabling or disabling recently visited sites */ +"settings.autocomplete.recentlyvisited" = "Ostatnio odwiedzone witryny"; + /* Settings screen cell text for Application Lock */ "settings.autolock" = "Blokada aplikacji"; @@ -2241,6 +2309,9 @@ /* Tab Switcher Accessibility Label */ "tab.switcher.accessibility.label" = "Przełącznik kart"; +/* No comment provided by engineer. */ +"Take Survey" = "Take Survey"; + /* Description text for the text size adjustment setting */ "textSize.description" = "Wybierz preferowany rozmiar tekstu. Ustawienie będzie miało wpływ na witryny wyświetlane w przeglądarce DuckDuckGo."; @@ -2319,6 +2390,9 @@ /* Alert title for the alert when the Privacy Pro subscription expires */ "vpn.access-revoked.alert.title" = "VPN disconnected due to expired subscription"; +/* Title for the VPN widget onboarding screen */ +"vpn.addWidget.settings.title" = "Find and select DuckDuckGo. Then swipe to VPN and select Add Widget."; + /* Title for the Cancel button of the VPN feedback form */ "vpn.feedback-form.button.cancel" = "Cancel"; @@ -2388,6 +2462,9 @@ /* Title for each screen of the VPN feedback form */ "vpn.feedback-form.title" = "Help Improve the DuckDuckGo VPN"; +/* VPN settings screen cell text for adding the VPN widget to the home screen */ +"vpn.settings.add.widget" = "Add VPN Widget to Home Screen"; + /* Title for the button to enable push notifications in system settings */ "waitlist.allow-notifications" = "Zezwalaj na powiadomienia"; @@ -2439,6 +2516,9 @@ /* Title for the share sheet entry */ "waitlist.share-sheet.title" = "Masz zaproszenie!"; +/* No comment provided by engineer. */ +"We want to make using passwords in DuckDuckGo better." = "We want to make using passwords in DuckDuckGo better."; + /* Confirmation message */ "web.url.remove.favorite.done" = "Usunięto ulubione"; diff --git a/DuckDuckGo/pt.lproj/Autocomplete.strings b/DuckDuckGo/pt.lproj/Autocomplete.strings deleted file mode 100644 index ab195fb256..0000000000 --- a/DuckDuckGo/pt.lproj/Autocomplete.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Class = "UILabel"; text = "Label"; ObjectID = "5ag-a1-mlA"; */ -"5ag-a1-mlA.text" = "Rótulo"; - -/* Class = "UILabel"; text = "Label"; ObjectID = "eiv-Cq-E2r"; */ -"eiv-Cq-E2r.text" = "Rótulo"; - -/* Class = "UILabel"; text = "No Suggestions"; ObjectID = "gPe-Cv-14j"; */ -"gPe-Cv-14j.text" = "Sem sugestões"; - -/* Class = "UIButton"; accessibilityLabel = "Autocomplete suggestion"; ObjectID = "N97-eI-Lps"; */ -"N97-eI-Lps.accessibilityLabel" = "Sugestão de preenchimento automático"; - diff --git a/DuckDuckGo/pt.lproj/Localizable.strings b/DuckDuckGo/pt.lproj/Localizable.strings index c05fb9be84..aa4ece6315 100644 --- a/DuckDuckGo/pt.lproj/Localizable.strings +++ b/DuckDuckGo/pt.lproj/Localizable.strings @@ -119,7 +119,7 @@ "addWidget.description" = "Tenha acesso rápido à pesquisa privada e aos sites que mais gosta."; /* No comment provided by engineer. */ -"addWidget.settings.firstParagraph" = "Prima prolongadamente o ecrã inicial para entrar no modo jiggle."; +"addWidget.settings.firstParagraph" = "Long-press on the Home Screen to enter jiggle mode."; /* Replacement string is a plus button icon. */ "addWidget.settings.secondParagraph.%@" = "Toque no botão de mais %@."; @@ -133,15 +133,48 @@ /* No comment provided by engineer. */ "alert.message.bookmarkAll" = "Os marcadores existentes não serão duplicados."; +/* Description for alert shown when sync error occurs because of bad data */ +"alert.sync-bad-data-error-description" = "Alguns marcadores ou palavras-passe estão formatados incorretamente ou são demasiado longos e não foram sincronizados."; + +/* Description for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-description" = "Atingiste o número máximo de marcadores. Elimina alguns marcadores para retomar a sincronização."; + +/* Title for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-title" = "A sincronização de marcadores está em pausa"; + +/* Description for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-description" = "Atingiste o número máximo de palavras-passe. Elimina algumas palavras-passe para retomar a sincronização."; + +/* Title for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-title" = "A sincronização de palavras-passe está em pausa"; + /* Title for sync error alert */ "alert.sync-error" = "Erro de sincronização e cópia de segurança"; +/* Sync error alert action button title, takes the user to the sync settings page. */ +"alert.sync-error-action" = "Definições de sincronização"; + +/* Description for alert shown when user logged off from sync */ +"alert.sync-invalid-login-error-description" = "A sincronização foi pausada. Se quiseres continuar a sincronizar este dispositivo, inicia sessão novamente noutro dispositivo ou com o teu código de recuperação."; + /* Learn more button in alert */ "alert.sync-paused-alert-learn-more-button" = "Saiba mais"; /* Confirmation button in alert */ "alert.sync-paused-alert-ok-button" = "OK"; +/* Title for alert shown when sync paused for an error */ +"alert.sync-paused-title" = "A sincronização está em pausa"; + +/* Description for alert shown when sync error occurs because of too many requests */ +"alert.sync-too-many-requests-error-description" = "O Sync & Backup está temporariamente indisponível."; + +/* Title of the warning message that tells the user that there was an error with the sync feature. */ +"alert.sync.warning.sync-error" = "Erro de sincronização"; + +/* Title of the warning message */ +"alert.sync.warning.sync-paused" = "A sincronização e a cópia de segurança estão em pausa"; + /* Question from confirmation dialog */ "alert.title.bookmarkAll" = "Marcar todos os separadores?"; @@ -208,6 +241,15 @@ /* No comment provided by engineer. */ "autoclear.on" = "Ligado"; +/* Subtitle for search history items */ +"autocomplete.history.search.duckduckgo" = "Pesquisar no DuckDuckGo"; + +/* The message text shown in suggestions */ +"autocomplete.history.warning.message" = "As sugestões incluem agora sites visitados recentemente. Estes são armazenados no teu dispositivo e nunca nos servidores da DuckDuckGo. Desativa esta funcionalidade nas definições ou apaga os sites guardados em qualquer altura com o 🔥 Fire Button."; + +/* Title for message show in suggestions */ +"autocomplete.history.warning.title" = "A mesma privacidade.\nMelhores sugestões de pesquisa!"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Desativado"; @@ -1147,6 +1189,9 @@ /* Title for the get desktop browser feature */ "get.browser.title" = "Obtém o DuckDuckGo para Mac ou Windows"; +/* No comment provided by engineer. */ +"Help us improve!" = "Help us improve!"; + /* No comment provided by engineer. */ "home.row.onboarding.header" = "Adicione o DuckDuckGo ao seu ecrã inicial!"; @@ -1603,6 +1648,26 @@ /* Deactivate button */ "pm.deactivate" = "Deactivate"; +/* Button title for sync bookmarks limits exceeded warning to go to manage bookmarks */ +"prefrences.sync.bookmarks-limit-exceeded-action" = "Gerir marcadores"; + +/* Description for sync bookmarks limits exceeded warning */ +"prefrences.sync.bookmarks-limit-exceeded-description" = "Atingiste o número máximo de marcadores. Elimina algumas para retomar a sincronização."; + +/* Button title for sync credentials limits exceeded warning to go to manage passwords */ +"prefrences.sync.credentials-limit-exceeded-action" = "Gerir palavras-passe…"; + +/* Description for sync credentials limits exceeded warning */ +"prefrences.sync.credentials-limit-exceeded-description" = "Atingiste o número máximo de palavras-passe. Elimina algumas para retomar a sincronização."; + +/* Description invalid credentials error when syncing. + Description of incorrectly formatted data error when syncing. + Description of too many requests error when syncing. */ +"prefrences.sync.invalid-login-description" = "Erro durante a sincronização. Experimenta desativar a sincronização neste dispositivo e, em seguida, inicia sessão novamente noutro dispositivo ou com o teu código de recuperação."; + +/* Title for sync limits exceeded warning */ +"prefrences.sync.limit-exceeded-title" = "Sincronização em pausa"; + /* No comment provided by engineer. */ "preserveLogins.domain.list.footer" = "Os websites confiam nos cookies para o manter ligado. Quando se usa uma barreira de segurança num site, os cookies não são apagados e permanecerá ligado, mesmo após usar o botão de segurança. Ainda bloqueamos rastreadores de terceiros encontrados em sites com barreiras de segurança."; @@ -1712,6 +1777,9 @@ /* Settings screen cell for autocomplete */ "settings.autocomplete" = "Mostrar sugestões de preenchimento automático"; +/* Settings label for enabling or disabling recently visited sites */ +"settings.autocomplete.recentlyvisited" = "Sites visitados recentemente"; + /* Settings screen cell text for Application Lock */ "settings.autolock" = "Bloqueio de aplicações"; @@ -2241,6 +2309,9 @@ /* Tab Switcher Accessibility Label */ "tab.switcher.accessibility.label" = "Alternar de separador"; +/* No comment provided by engineer. */ +"Take Survey" = "Take Survey"; + /* Description text for the text size adjustment setting */ "textSize.description" = "Escolha o tamanho de texto da sua preferência. Os sites que vê no DuckDuckGo ajustar-se-ão."; @@ -2319,6 +2390,9 @@ /* Alert title for the alert when the Privacy Pro subscription expires */ "vpn.access-revoked.alert.title" = "VPN disconnected due to expired subscription"; +/* Title for the VPN widget onboarding screen */ +"vpn.addWidget.settings.title" = "Find and select DuckDuckGo. Then swipe to VPN and select Add Widget."; + /* Title for the Cancel button of the VPN feedback form */ "vpn.feedback-form.button.cancel" = "Cancel"; @@ -2388,6 +2462,9 @@ /* Title for each screen of the VPN feedback form */ "vpn.feedback-form.title" = "Help Improve the DuckDuckGo VPN"; +/* VPN settings screen cell text for adding the VPN widget to the home screen */ +"vpn.settings.add.widget" = "Add VPN Widget to Home Screen"; + /* Title for the button to enable push notifications in system settings */ "waitlist.allow-notifications" = "Permitir notificações"; @@ -2439,6 +2516,9 @@ /* Title for the share sheet entry */ "waitlist.share-sheet.title" = "Está convidado!"; +/* No comment provided by engineer. */ +"We want to make using passwords in DuckDuckGo better." = "We want to make using passwords in DuckDuckGo better."; + /* Confirmation message */ "web.url.remove.favorite.done" = "Favorito removido"; diff --git a/DuckDuckGo/ro.lproj/Autocomplete.strings b/DuckDuckGo/ro.lproj/Autocomplete.strings deleted file mode 100644 index c40b95b6ec..0000000000 --- a/DuckDuckGo/ro.lproj/Autocomplete.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Class = "UILabel"; text = "Label"; ObjectID = "5ag-a1-mlA"; */ -"5ag-a1-mlA.text" = "Label"; - -/* Class = "UILabel"; text = "Label"; ObjectID = "eiv-Cq-E2r"; */ -"eiv-Cq-E2r.text" = "Label"; - -/* Class = "UILabel"; text = "No Suggestions"; ObjectID = "gPe-Cv-14j"; */ -"gPe-Cv-14j.text" = "Fără sugestii"; - -/* Class = "UIButton"; accessibilityLabel = "Autocomplete suggestion"; ObjectID = "N97-eI-Lps"; */ -"N97-eI-Lps.accessibilityLabel" = "Sugestie de completare automată"; - diff --git a/DuckDuckGo/ro.lproj/Localizable.strings b/DuckDuckGo/ro.lproj/Localizable.strings index 81cc0cb634..562ae34771 100644 --- a/DuckDuckGo/ro.lproj/Localizable.strings +++ b/DuckDuckGo/ro.lproj/Localizable.strings @@ -119,7 +119,7 @@ "addWidget.description" = "Obține acces rapid la căutarea privată și la site-urile care îți plac."; /* No comment provided by engineer. */ -"addWidget.settings.firstParagraph" = "Ține apăsat pe ecranul de întâmpinare pentru a intra în modul în care elementele „tremură”."; +"addWidget.settings.firstParagraph" = "Long-press on the Home Screen to enter jiggle mode."; /* Replacement string is a plus button icon. */ "addWidget.settings.secondParagraph.%@" = "Atinge butonul plus %@."; @@ -133,15 +133,48 @@ /* No comment provided by engineer. */ "alert.message.bookmarkAll" = "Marcajele existente nu vor fi duplicate."; +/* Description for alert shown when sync error occurs because of bad data */ +"alert.sync-bad-data-error-description" = "Unele semne de carte sau parole sunt formatate incorect sau sunt prea lungi și nu au fost sincronizate."; + +/* Description for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-description" = "Ai atins numărul maxim de marcaje. Șterge câteva marcaje pentru a relua sincronizarea."; + +/* Title for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-title" = "Sincronizarea marcajelor este întreruptă"; + +/* Description for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-description" = "Ai atins numărul maxim de parole. Șterge câteva parole pentru a relua sincronizarea."; + +/* Title for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-title" = "Sincronizarea parolelor este întreruptă"; + /* Title for sync error alert */ "alert.sync-error" = "Eroare de sincronizare și backup"; +/* Sync error alert action button title, takes the user to the sync settings page. */ +"alert.sync-error-action" = "Setări de sincronizare"; + +/* Description for alert shown when user logged off from sync */ +"alert.sync-invalid-login-error-description" = "Sincronizarea a fost întreruptă. Dacă dorești să continui sincronizarea acestui dispozitiv, reconectează-te folosind un alt dispozitiv sau codul de recuperare."; + /* Learn more button in alert */ "alert.sync-paused-alert-learn-more-button" = "Află mai multe"; /* Confirmation button in alert */ "alert.sync-paused-alert-ok-button" = "OK"; +/* Title for alert shown when sync paused for an error */ +"alert.sync-paused-title" = "Sincronizarea este întreruptă"; + +/* Description for alert shown when sync error occurs because of too many requests */ +"alert.sync-too-many-requests-error-description" = "Sync & Backup sunt temporar indisponibile."; + +/* Title of the warning message that tells the user that there was an error with the sync feature. */ +"alert.sync.warning.sync-error" = "Eroare de sincronizare"; + +/* Title of the warning message */ +"alert.sync.warning.sync-paused" = "Sincronizarea și copierea de rezervă este întreruptă"; + /* Question from confirmation dialog */ "alert.title.bookmarkAll" = "Marchezi toate filele?"; @@ -208,6 +241,15 @@ /* No comment provided by engineer. */ "autoclear.on" = "Activat"; +/* Subtitle for search history items */ +"autocomplete.history.search.duckduckgo" = "Căutare DuckDuckGo"; + +/* The message text shown in suggestions */ +"autocomplete.history.warning.message" = "Sugestiile includ acum site-uri vizitate recent. Acestea sunt stocate pe dispozitivul tău și niciodată pe serverele DuckDuckGo. Dezactivează-le din setări sau șterge-le în orice moment cu ajutorul Fire Button 🔥."; + +/* Title for message show in suggestions */ +"autocomplete.history.warning.title" = "Aceeași confidențialitate.\nSugestii de căutare mai bune!"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Dezactivat"; @@ -1147,6 +1189,9 @@ /* Title for the get desktop browser feature */ "get.browser.title" = "Obține DuckDuckGo pentru Mac sau Windows"; +/* No comment provided by engineer. */ +"Help us improve!" = "Help us improve!"; + /* No comment provided by engineer. */ "home.row.onboarding.header" = "Adaugă DuckDuckGo pe ecranul de start!"; @@ -1603,6 +1648,26 @@ /* Deactivate button */ "pm.deactivate" = "Deactivate"; +/* Button title for sync bookmarks limits exceeded warning to go to manage bookmarks */ +"prefrences.sync.bookmarks-limit-exceeded-action" = "Gestionează marcajele"; + +/* Description for sync bookmarks limits exceeded warning */ +"prefrences.sync.bookmarks-limit-exceeded-description" = "Ai atins numărul maxim de marcaje. Șterge câteva pentru a relua sincronizarea."; + +/* Button title for sync credentials limits exceeded warning to go to manage passwords */ +"prefrences.sync.credentials-limit-exceeded-action" = "Gestionează parolele…"; + +/* Description for sync credentials limits exceeded warning */ +"prefrences.sync.credentials-limit-exceeded-description" = "Ai atins numărul maxim de parole. Șterge câteva pentru a relua sincronizarea."; + +/* Description invalid credentials error when syncing. + Description of incorrectly formatted data error when syncing. + Description of too many requests error when syncing. */ +"prefrences.sync.invalid-login-description" = "A apărut o eroare în timpul sincronizării. Încearcă să dezactivezi sincronizarea pe acest dispozitiv și apoi reconectează-te folosind un alt dispozitiv sau codul de recuperare."; + +/* Title for sync limits exceeded warning */ +"prefrences.sync.limit-exceeded-title" = "Sincronizarea întreruptă"; + /* No comment provided by engineer. */ "preserveLogins.domain.list.footer" = "Site-urile web se bazează pe cookie-uri pentru a te menține conectat. Atunci când ștergi activitatea și istoricul unui site, cookie-urile nu vor fi șterse și vei rămâne conectat, chiar și după utilizarea butonului Ștergere activitate și istoric. Blocăm totuși instrumentele de urmărire terțe găsite pe site-urile web cu ștergerea activității și istoricului din browser la ieșire."; @@ -1712,6 +1777,9 @@ /* Settings screen cell for autocomplete */ "settings.autocomplete" = "Afișează sugestiile de completare automată"; +/* Settings label for enabling or disabling recently visited sites */ +"settings.autocomplete.recentlyvisited" = "Site-uri vizitate recent"; + /* Settings screen cell text for Application Lock */ "settings.autolock" = "Blocarea aplicației"; @@ -2241,6 +2309,9 @@ /* Tab Switcher Accessibility Label */ "tab.switcher.accessibility.label" = "Comutator între file"; +/* No comment provided by engineer. */ +"Take Survey" = "Take Survey"; + /* Description text for the text size adjustment setting */ "textSize.description" = "Alege dimensiunea preferată a textului. Site-urile pe care le vizitezi în DuckDuckGo se vor adapta la aceasta."; @@ -2319,6 +2390,9 @@ /* Alert title for the alert when the Privacy Pro subscription expires */ "vpn.access-revoked.alert.title" = "VPN disconnected due to expired subscription"; +/* Title for the VPN widget onboarding screen */ +"vpn.addWidget.settings.title" = "Find and select DuckDuckGo. Then swipe to VPN and select Add Widget."; + /* Title for the Cancel button of the VPN feedback form */ "vpn.feedback-form.button.cancel" = "Cancel"; @@ -2388,6 +2462,9 @@ /* Title for each screen of the VPN feedback form */ "vpn.feedback-form.title" = "Help Improve the DuckDuckGo VPN"; +/* VPN settings screen cell text for adding the VPN widget to the home screen */ +"vpn.settings.add.widget" = "Add VPN Widget to Home Screen"; + /* Title for the button to enable push notifications in system settings */ "waitlist.allow-notifications" = "Permite notificările"; @@ -2439,6 +2516,9 @@ /* Title for the share sheet entry */ "waitlist.share-sheet.title" = "Ești invitat(ă)!"; +/* No comment provided by engineer. */ +"We want to make using passwords in DuckDuckGo better." = "We want to make using passwords in DuckDuckGo better."; + /* Confirmation message */ "web.url.remove.favorite.done" = "Favorit eliminat"; diff --git a/DuckDuckGo/ru.lproj/Autocomplete.strings b/DuckDuckGo/ru.lproj/Autocomplete.strings deleted file mode 100644 index 87759e51c7..0000000000 --- a/DuckDuckGo/ru.lproj/Autocomplete.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Class = "UILabel"; text = "Label"; ObjectID = "5ag-a1-mlA"; */ -"5ag-a1-mlA.text" = "Метка"; - -/* Class = "UILabel"; text = "Label"; ObjectID = "eiv-Cq-E2r"; */ -"eiv-Cq-E2r.text" = "Метка"; - -/* Class = "UILabel"; text = "No Suggestions"; ObjectID = "gPe-Cv-14j"; */ -"gPe-Cv-14j.text" = "Нет предложений"; - -/* Class = "UIButton"; accessibilityLabel = "Autocomplete suggestion"; ObjectID = "N97-eI-Lps"; */ -"N97-eI-Lps.accessibilityLabel" = "Предложение для автозаполнения"; - diff --git a/DuckDuckGo/ru.lproj/Localizable.strings b/DuckDuckGo/ru.lproj/Localizable.strings index eca0afe73c..7cf74211a0 100644 --- a/DuckDuckGo/ru.lproj/Localizable.strings +++ b/DuckDuckGo/ru.lproj/Localizable.strings @@ -119,7 +119,7 @@ "addWidget.description" = "Быстрый доступ к конфиденциальному поиску и любимым сайтам."; /* No comment provided by engineer. */ -"addWidget.settings.firstParagraph" = "Удерживайте нажатие на домашнем экране, чтобы запустить режим настройки."; +"addWidget.settings.firstParagraph" = "Long-press on the Home Screen to enter jiggle mode."; /* Replacement string is a plus button icon. */ "addWidget.settings.secondParagraph.%@" = "Нажмите кнопку «плюс» %@."; @@ -133,15 +133,48 @@ /* No comment provided by engineer. */ "alert.message.bookmarkAll" = "Существующие закладки дублироваться не будут."; +/* Description for alert shown when sync error occurs because of bad data */ +"alert.sync-bad-data-error-description" = "Некоторые закладки или пароли не синхронизируются из-за неверного формата или превышения ограничений по длине."; + +/* Description for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-description" = "Достигнуто максимальное число закладок. Чтобы возобновить синхронизацию, удалите некоторые из них."; + +/* Title for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-title" = "Синхронизация закладок приостановлена"; + +/* Description for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-description" = "Достигнуто максимальное число паролей. Чтобы возобновить синхронизацию, удалите некоторые из них."; + +/* Title for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-title" = "Синхронизация паролей приостановлена"; + /* Title for sync error alert */ "alert.sync-error" = "Ошибка синхронизации и резервного копирования"; +/* Sync error alert action button title, takes the user to the sync settings page. */ +"alert.sync-error-action" = "Синхронизация настроек"; + +/* Description for alert shown when user logged off from sync */ +"alert.sync-invalid-login-error-description" = "Синхронизация приостановлена. Чтобы возобновить ее, выполните повторную привязку, используя другое устройство или код восстановления."; + /* Learn more button in alert */ "alert.sync-paused-alert-learn-more-button" = "Узнать больше"; /* Confirmation button in alert */ "alert.sync-paused-alert-ok-button" = "Хорошо"; +/* Title for alert shown when sync paused for an error */ +"alert.sync-paused-title" = "Синхронизация приостановлена"; + +/* Description for alert shown when sync error occurs because of too many requests */ +"alert.sync-too-many-requests-error-description" = "Функция «Синхронизация и резервное копирование» временно недоступна."; + +/* Title of the warning message that tells the user that there was an error with the sync feature. */ +"alert.sync.warning.sync-error" = "Ошибка синхронизации"; + +/* Title of the warning message */ +"alert.sync.warning.sync-paused" = "Синхронизация и резервное копирование на паузе"; + /* Question from confirmation dialog */ "alert.title.bookmarkAll" = "Добавить все вкладки в закладки?"; @@ -208,6 +241,15 @@ /* No comment provided by engineer. */ "autoclear.on" = "Включено"; +/* Subtitle for search history items */ +"autocomplete.history.search.duckduckgo" = "Поиск в DuckDuckGo"; + +/* The message text shown in suggestions */ +"autocomplete.history.warning.message" = "Теперь рекомендации включают недавно посещенные сайты. Они хранятся на вашем устройстве, а не на серверах DuckDuckGo. Их можно отключить в настройках или очистить с помощью кнопки Fire Button 🔥."; + +/* Title for message show in suggestions */ +"autocomplete.history.warning.title" = "Та же конфиденциальность.\nЛучшие поисковые рекомендации!"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Отключено"; @@ -1147,6 +1189,9 @@ /* Title for the get desktop browser feature */ "get.browser.title" = "DuckDuckGo для Mac и Windows"; +/* No comment provided by engineer. */ +"Help us improve!" = "Help us improve!"; + /* No comment provided by engineer. */ "home.row.onboarding.header" = "Добавьте DuckDuckGo на домашний экран!"; @@ -1603,6 +1648,26 @@ /* Deactivate button */ "pm.deactivate" = "Deactivate"; +/* Button title for sync bookmarks limits exceeded warning to go to manage bookmarks */ +"prefrences.sync.bookmarks-limit-exceeded-action" = "Управление закладками"; + +/* Description for sync bookmarks limits exceeded warning */ +"prefrences.sync.bookmarks-limit-exceeded-description" = "Достигнуто максимальное число закладок. Чтобы возобновить синхронизацию, удалите некоторые из них."; + +/* Button title for sync credentials limits exceeded warning to go to manage passwords */ +"prefrences.sync.credentials-limit-exceeded-action" = "Управление паролями..."; + +/* Description for sync credentials limits exceeded warning */ +"prefrences.sync.credentials-limit-exceeded-description" = "Достигнуто максимальное число паролей. Чтобы возобновить синхронизацию, удалите некоторые из них."; + +/* Description invalid credentials error when syncing. + Description of incorrectly formatted data error when syncing. + Description of too many requests error when syncing. */ +"prefrences.sync.invalid-login-description" = "Произошла ошибка синхронизации. Попробуйте отключить синхронизацию на этом устройстве, а затем выполнить повторную привязку с помощью другого устройства или кода восстановления."; + +/* Title for sync limits exceeded warning */ +"prefrences.sync.limit-exceeded-title" = "Синхронизация на паузе"; + /* No comment provided by engineer. */ "preserveLogins.domain.list.footer" = "Чтобы вам не приходилось каждый раз входить в свою учетную запись, сайты используют куки-файлы. Если сделать сайт огнеупорным, мы не будем стирать его куки-файлы, даже если вы воспользуетесь кнопкой «Огонь». Но мы по-прежнему будем блокировать сторонние трекеры, в том числе на огнеупорных сайтах."; @@ -1712,6 +1777,9 @@ /* Settings screen cell for autocomplete */ "settings.autocomplete" = "Показать предложения автозаполнения"; +/* Settings label for enabling or disabling recently visited sites */ +"settings.autocomplete.recentlyvisited" = "Недавно посещенные сайты"; + /* Settings screen cell text for Application Lock */ "settings.autolock" = "Блокировка"; @@ -2241,6 +2309,9 @@ /* Tab Switcher Accessibility Label */ "tab.switcher.accessibility.label" = "Переключатель вкладок"; +/* No comment provided by engineer. */ +"Take Survey" = "Take Survey"; + /* Description text for the text size adjustment setting */ "textSize.description" = "Выберите удобный для вас размер шрифта. DuckDuckGo настроит вид сайтов автоматически."; @@ -2319,6 +2390,9 @@ /* Alert title for the alert when the Privacy Pro subscription expires */ "vpn.access-revoked.alert.title" = "VPN disconnected due to expired subscription"; +/* Title for the VPN widget onboarding screen */ +"vpn.addWidget.settings.title" = "Find and select DuckDuckGo. Then swipe to VPN and select Add Widget."; + /* Title for the Cancel button of the VPN feedback form */ "vpn.feedback-form.button.cancel" = "Cancel"; @@ -2388,6 +2462,9 @@ /* Title for each screen of the VPN feedback form */ "vpn.feedback-form.title" = "Help Improve the DuckDuckGo VPN"; +/* VPN settings screen cell text for adding the VPN widget to the home screen */ +"vpn.settings.add.widget" = "Add VPN Widget to Home Screen"; + /* Title for the button to enable push notifications in system settings */ "waitlist.allow-notifications" = "Разрешить уведомления"; @@ -2439,6 +2516,9 @@ /* Title for the share sheet entry */ "waitlist.share-sheet.title" = "Вас пригласили!"; +/* No comment provided by engineer. */ +"We want to make using passwords in DuckDuckGo better." = "We want to make using passwords in DuckDuckGo better."; + /* Confirmation message */ "web.url.remove.favorite.done" = "Страница удалена из избранного"; diff --git a/DuckDuckGo/sk.lproj/Autocomplete.strings b/DuckDuckGo/sk.lproj/Autocomplete.strings deleted file mode 100644 index db65058ab1..0000000000 --- a/DuckDuckGo/sk.lproj/Autocomplete.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Class = "UILabel"; text = "Label"; ObjectID = "5ag-a1-mlA"; */ -"5ag-a1-mlA.text" = "Označenie"; - -/* Class = "UILabel"; text = "Label"; ObjectID = "eiv-Cq-E2r"; */ -"eiv-Cq-E2r.text" = "Označenie"; - -/* Class = "UILabel"; text = "No Suggestions"; ObjectID = "gPe-Cv-14j"; */ -"gPe-Cv-14j.text" = "Žiadne návrhy"; - -/* Class = "UIButton"; accessibilityLabel = "Autocomplete suggestion"; ObjectID = "N97-eI-Lps"; */ -"N97-eI-Lps.accessibilityLabel" = "Zobraziť návrhy automatického dopĺňania"; - diff --git a/DuckDuckGo/sk.lproj/Localizable.strings b/DuckDuckGo/sk.lproj/Localizable.strings index 7a286265d9..08bcd423a1 100644 --- a/DuckDuckGo/sk.lproj/Localizable.strings +++ b/DuckDuckGo/sk.lproj/Localizable.strings @@ -119,7 +119,7 @@ "addWidget.description" = "Získajte prístup k súkromnému vyhľadávaniu a svojim obľúbeným webovým lokalitám."; /* No comment provided by engineer. */ -"addWidget.settings.firstParagraph" = "Dlhým stlačením na domovskej obrazovke spustíte režim zatrasenia."; +"addWidget.settings.firstParagraph" = "Long-press on the Home Screen to enter jiggle mode."; /* Replacement string is a plus button icon. */ "addWidget.settings.secondParagraph.%@" = "Klepnite na tlačidlo plus %@."; @@ -133,15 +133,48 @@ /* No comment provided by engineer. */ "alert.message.bookmarkAll" = "Existujúce záložky sa nebudú duplikovať."; +/* Description for alert shown when sync error occurs because of bad data */ +"alert.sync-bad-data-error-description" = "Niektoré záložky alebo heslá sú nesprávne naformátované alebo príliš dlhé a neboli synchronizované."; + +/* Description for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-description" = "Dosiahli ste maximálny počet záložiek. Ak chcete obnoviť synchronizáciu, odstráňte niektoré záložky."; + +/* Title for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-title" = "Synchronizácia záložiek je pozastavená."; + +/* Description for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-description" = "Dosiahli ste maximálny počet hesiel. Ak chcete obnoviť synchronizáciu, odstráňte niektoré heslá."; + +/* Title for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-title" = "Synchronizácia hesiel je pozastavená."; + /* Title for sync error alert */ "alert.sync-error" = "Chyba synchronizácie a zálohovania"; +/* Sync error alert action button title, takes the user to the sync settings page. */ +"alert.sync-error-action" = "Nastavenia synchronizácie"; + +/* Description for alert shown when user logged off from sync */ +"alert.sync-invalid-login-error-description" = "Synchronizácia bola pozastavená. Ak chcete pokračovať v synchronizácii tohto zariadenia, znova sa pripojte pomocou iného zariadenia alebo prostredníctvom kódu na obnovenie."; + /* Learn more button in alert */ "alert.sync-paused-alert-learn-more-button" = "Zistite viac"; /* Confirmation button in alert */ "alert.sync-paused-alert-ok-button" = "OK"; +/* Title for alert shown when sync paused for an error */ +"alert.sync-paused-title" = "Synchronizácia bola pozastavená."; + +/* Description for alert shown when sync error occurs because of too many requests */ +"alert.sync-too-many-requests-error-description" = "Služba Sync & Backup je dočasne nedostupná."; + +/* Title of the warning message that tells the user that there was an error with the sync feature. */ +"alert.sync.warning.sync-error" = "Chyba synchronizácie"; + +/* Title of the warning message */ +"alert.sync.warning.sync-paused" = "Synchronizácia a zálohovanie sú pozastavené"; + /* Question from confirmation dialog */ "alert.title.bookmarkAll" = "Uložiť všetky karty medzi záložky?"; @@ -208,6 +241,15 @@ /* No comment provided by engineer. */ "autoclear.on" = "Zapnuté"; +/* Subtitle for search history items */ +"autocomplete.history.search.duckduckgo" = "Vyhľadávajte cez DuckDuckGo"; + +/* The message text shown in suggestions */ +"autocomplete.history.warning.message" = "Návrhy teraz zahŕňajú nedávno navštívené stránky. Tie sú uložené vo vašom zariadení a nikdy nie na serveroch DuckDuckGo. Vypnite ich v nastaveniach alebo ich kedykoľvek zrušte pomocou tlačidla 🔥Fire Button."; + +/* Title for message show in suggestions */ +"autocomplete.history.warning.title" = "Rovnaké súkromie.\nLepšie návrhy vyhľadávania!"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Zakázané"; @@ -1147,6 +1189,9 @@ /* Title for the get desktop browser feature */ "get.browser.title" = "Získajte DuckDuckGo pre Mac alebo Windows"; +/* No comment provided by engineer. */ +"Help us improve!" = "Help us improve!"; + /* No comment provided by engineer. */ "home.row.onboarding.header" = "Pridajte DuckDuckGo na svoju domovskú obrazovku!"; @@ -1603,6 +1648,26 @@ /* Deactivate button */ "pm.deactivate" = "Deactivate"; +/* Button title for sync bookmarks limits exceeded warning to go to manage bookmarks */ +"prefrences.sync.bookmarks-limit-exceeded-action" = "Správa záložiek"; + +/* Description for sync bookmarks limits exceeded warning */ +"prefrences.sync.bookmarks-limit-exceeded-description" = "Dosiahli ste maximálny počet záložiek. Ak chcete obnoviť synchronizáciu, odstráňte niektoré záložky."; + +/* Button title for sync credentials limits exceeded warning to go to manage passwords */ +"prefrences.sync.credentials-limit-exceeded-action" = "Spravovať heslá…"; + +/* Description for sync credentials limits exceeded warning */ +"prefrences.sync.credentials-limit-exceeded-description" = "Dosiahli ste maximálny počet hesiel. Ak chcete obnoviť synchronizáciu, odstráňte niektoré záložky."; + +/* Description invalid credentials error when syncing. + Description of incorrectly formatted data error when syncing. + Description of too many requests error when syncing. */ +"prefrences.sync.invalid-login-description" = "Pri synchronizácii sa vyskytla chyba. Skúste vypnúť synchronizáciu v tomto zariadení a potom sa znovu pripojte pomocou iného zariadenia alebo kódu na obnovenie."; + +/* Title for sync limits exceeded warning */ +"prefrences.sync.limit-exceeded-title" = "Synchronizácia bola pozastavená"; + /* No comment provided by engineer. */ "preserveLogins.domain.list.footer" = "Webové stránky sa spoliehajú na súbory cookie, aby ste zostali prihlásení. Keď webovú stránku zabezpečíte funkciou ohňovzdornosti, súbory cookie nebudú zmazané a zostanete prihlásení aj po použití tlačidla Oheň. Stále budeme blokovať sledovacie zariadenia tretích strán, ktoré sa nachádzajú na webových stránkach zabezpečených funkciou ohňovzdornosti."; @@ -1712,6 +1777,9 @@ /* Settings screen cell for autocomplete */ "settings.autocomplete" = "Zobraziť návrhy automatického dopĺňania"; +/* Settings label for enabling or disabling recently visited sites */ +"settings.autocomplete.recentlyvisited" = "Nedávno navštívené weby"; + /* Settings screen cell text for Application Lock */ "settings.autolock" = "Zámok aplikácie"; @@ -2241,6 +2309,9 @@ /* Tab Switcher Accessibility Label */ "tab.switcher.accessibility.label" = "Prepínač kariet"; +/* No comment provided by engineer. */ +"Take Survey" = "Take Survey"; + /* Description text for the text size adjustment setting */ "textSize.description" = "Vyberte si preferovanú veľkosť textu. Webové stránky zobrazené v DuckDuckGo sa tomu prispôsobia."; @@ -2319,6 +2390,9 @@ /* Alert title for the alert when the Privacy Pro subscription expires */ "vpn.access-revoked.alert.title" = "VPN disconnected due to expired subscription"; +/* Title for the VPN widget onboarding screen */ +"vpn.addWidget.settings.title" = "Find and select DuckDuckGo. Then swipe to VPN and select Add Widget."; + /* Title for the Cancel button of the VPN feedback form */ "vpn.feedback-form.button.cancel" = "Cancel"; @@ -2388,6 +2462,9 @@ /* Title for each screen of the VPN feedback form */ "vpn.feedback-form.title" = "Help Improve the DuckDuckGo VPN"; +/* VPN settings screen cell text for adding the VPN widget to the home screen */ +"vpn.settings.add.widget" = "Add VPN Widget to Home Screen"; + /* Title for the button to enable push notifications in system settings */ "waitlist.allow-notifications" = "Povoliť oznámenia"; @@ -2439,6 +2516,9 @@ /* Title for the share sheet entry */ "waitlist.share-sheet.title" = "Ste pozvaný/-á!"; +/* No comment provided by engineer. */ +"We want to make using passwords in DuckDuckGo better." = "We want to make using passwords in DuckDuckGo better."; + /* Confirmation message */ "web.url.remove.favorite.done" = "Obľúbená položka bola odstránená"; diff --git a/DuckDuckGo/sl.lproj/Autocomplete.strings b/DuckDuckGo/sl.lproj/Autocomplete.strings deleted file mode 100644 index bab9dfb63b..0000000000 --- a/DuckDuckGo/sl.lproj/Autocomplete.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Class = "UILabel"; text = "Label"; ObjectID = "5ag-a1-mlA"; */ -"5ag-a1-mlA.text" = "Oznaka"; - -/* Class = "UILabel"; text = "Label"; ObjectID = "eiv-Cq-E2r"; */ -"eiv-Cq-E2r.text" = "Oznaka"; - -/* Class = "UILabel"; text = "No Suggestions"; ObjectID = "gPe-Cv-14j"; */ -"gPe-Cv-14j.text" = "Ni predlogov"; - -/* Class = "UIButton"; accessibilityLabel = "Autocomplete suggestion"; ObjectID = "N97-eI-Lps"; */ -"N97-eI-Lps.accessibilityLabel" = "Pokaži predlog za samodokončanje"; - diff --git a/DuckDuckGo/sl.lproj/Localizable.strings b/DuckDuckGo/sl.lproj/Localizable.strings index 1d6bcc2886..1f4a415e85 100644 --- a/DuckDuckGo/sl.lproj/Localizable.strings +++ b/DuckDuckGo/sl.lproj/Localizable.strings @@ -119,7 +119,7 @@ "addWidget.description" = "Zagotovite si hiter dostop do zasebnega iskanja in spletnih mest, ki so vam všeč."; /* No comment provided by engineer. */ -"addWidget.settings.firstParagraph" = "Pritisnite in pridržite točko na začetnem zaslonu, da vstopite v način premikanja."; +"addWidget.settings.firstParagraph" = "Long-press on the Home Screen to enter jiggle mode."; /* Replacement string is a plus button icon. */ "addWidget.settings.secondParagraph.%@" = "Tapnite gumb plus %@."; @@ -133,15 +133,48 @@ /* No comment provided by engineer. */ "alert.message.bookmarkAll" = "Obstoječi zaznamki ne bodo podvojeni."; +/* Description for alert shown when sync error occurs because of bad data */ +"alert.sync-bad-data-error-description" = "Nekateri zaznamki ali gesla so napačno oblikovani ali predolgi in niso bili sinhronizirani."; + +/* Description for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-description" = "Dosegli ste največje število zaznamkov. Izbrišite nekaj zaznamkov, da nadaljujete sinhronizacijo."; + +/* Title for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-title" = "Sinhronizacija zaznamkov je zaustavljena"; + +/* Description for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-description" = "Dosegli ste največje število gesel. Izbrišite nekatera gesla, da nadaljujete sinhronizacijo."; + +/* Title for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-title" = "Sinhronizacija gesla je zaustavljena"; + /* Title for sync error alert */ "alert.sync-error" = "Napaka pri sinhronizaciji in varnostnem kopiranju"; +/* Sync error alert action button title, takes the user to the sync settings page. */ +"alert.sync-error-action" = "Nastavitve sinhronizacije"; + +/* Description for alert shown when user logged off from sync */ +"alert.sync-invalid-login-error-description" = "Sinhronizacija je bila začasno zaustavljena. Če želite še naprej sinhronizirati to napravo, znova vzpostavite povezavo z uporabo druge naprave ali obnovitvene kode."; + /* Learn more button in alert */ "alert.sync-paused-alert-learn-more-button" = "Več"; /* Confirmation button in alert */ "alert.sync-paused-alert-ok-button" = "V REDU"; +/* Title for alert shown when sync paused for an error */ +"alert.sync-paused-title" = "Sinhronizacija je začasno zaustavljena"; + +/* Description for alert shown when sync error occurs because of too many requests */ +"alert.sync-too-many-requests-error-description" = "Sinhronizacija in varnostno kopiranje sta začasno nedostopna."; + +/* Title of the warning message that tells the user that there was an error with the sync feature. */ +"alert.sync.warning.sync-error" = "Napaka pri sinhronizaciji"; + +/* Title of the warning message */ +"alert.sync.warning.sync-paused" = "Sinhronizacija in varnostno kopiranje sta zaustavljena"; + /* Question from confirmation dialog */ "alert.title.bookmarkAll" = "Želiš dodati vse zavihke med zaznamke?"; @@ -208,6 +241,15 @@ /* No comment provided by engineer. */ "autoclear.on" = "Vključeno"; +/* Subtitle for search history items */ +"autocomplete.history.search.duckduckgo" = "Iščite po DuckDuckGo"; + +/* The message text shown in suggestions */ +"autocomplete.history.warning.message" = "Predlogi zdaj vključujejo nedavno obiskana spletna mesta. Ta so shranjena v vaši napravi in nikoli v strežnikih DuckDuckGo. Funkcijo izklopite v nastavitvah ali pa seznam kadar koli počistite z gumbom Fire Button 🔥."; + +/* Title for message show in suggestions */ +"autocomplete.history.warning.title" = "Enaka raven zasebnosti.\nBoljši predlogi iskanja!"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Onemogočeno"; @@ -1147,6 +1189,9 @@ /* Title for the get desktop browser feature */ "get.browser.title" = "Prenesite DuckDuckGo za računalnike Mac ali Windows"; +/* No comment provided by engineer. */ +"Help us improve!" = "Help us improve!"; + /* No comment provided by engineer. */ "home.row.onboarding.header" = "Dodaj DuckDuckGo na domači zaslon!"; @@ -1603,6 +1648,26 @@ /* Deactivate button */ "pm.deactivate" = "Deactivate"; +/* Button title for sync bookmarks limits exceeded warning to go to manage bookmarks */ +"prefrences.sync.bookmarks-limit-exceeded-action" = "Upravljanje zaznamkov"; + +/* Description for sync bookmarks limits exceeded warning */ +"prefrences.sync.bookmarks-limit-exceeded-description" = "Dosegli ste največje število zaznamkov. Izbrišite jih nekaj, če želite nadaljevati sinhronizacijo."; + +/* Button title for sync credentials limits exceeded warning to go to manage passwords */ +"prefrences.sync.credentials-limit-exceeded-action" = "Upravljanje gesel…"; + +/* Description for sync credentials limits exceeded warning */ +"prefrences.sync.credentials-limit-exceeded-description" = "Dosegli ste največje število gesel. Izbrišite jih nekaj, če želite nadaljevati sinhronizacijo."; + +/* Description invalid credentials error when syncing. + Description of incorrectly formatted data error when syncing. + Description of too many requests error when syncing. */ +"prefrences.sync.invalid-login-description" = "Sinhronizacija je naletela na napako. Poskusite onemogočiti sinhronizacijo v tej napravi in nato znova vzpostavite povezavo z drugo napravo ali obnovitveno kodo."; + +/* Title for sync limits exceeded warning */ +"prefrences.sync.limit-exceeded-title" = "Sinhronizacija je začasno zaustavljena"; + /* No comment provided by engineer. */ "preserveLogins.domain.list.footer" = "Spletne strani se zanašajo na piškotke, da boš vedno prijavljen. Ko spletno mesto požarno zaščitiš, piškotki ne bodo izbrisani in ostal boš prijavljen tudi po uporabi gumba Fire. Še vedno blokiramo zunanje sledilce, ki jih najdemo na požarno izoliranih spletnih mestih."; @@ -1712,6 +1777,9 @@ /* Settings screen cell for autocomplete */ "settings.autocomplete" = "Pokaži predloge za samodokončanje"; +/* Settings label for enabling or disabling recently visited sites */ +"settings.autocomplete.recentlyvisited" = "Nedavno obiskana spletna mesta"; + /* Settings screen cell text for Application Lock */ "settings.autolock" = "Zaklepanje aplikacije"; @@ -2241,6 +2309,9 @@ /* Tab Switcher Accessibility Label */ "tab.switcher.accessibility.label" = "Preklopi med zavihki"; +/* No comment provided by engineer. */ +"Take Survey" = "Take Survey"; + /* Description text for the text size adjustment setting */ "textSize.description" = "Izberite želeno velikost besedila. Spletna mesta, ki se prikazujejo v iskalniku DuckDuckGo, se temu prilagodijo."; @@ -2319,6 +2390,9 @@ /* Alert title for the alert when the Privacy Pro subscription expires */ "vpn.access-revoked.alert.title" = "VPN disconnected due to expired subscription"; +/* Title for the VPN widget onboarding screen */ +"vpn.addWidget.settings.title" = "Find and select DuckDuckGo. Then swipe to VPN and select Add Widget."; + /* Title for the Cancel button of the VPN feedback form */ "vpn.feedback-form.button.cancel" = "Cancel"; @@ -2388,6 +2462,9 @@ /* Title for each screen of the VPN feedback form */ "vpn.feedback-form.title" = "Help Improve the DuckDuckGo VPN"; +/* VPN settings screen cell text for adding the VPN widget to the home screen */ +"vpn.settings.add.widget" = "Add VPN Widget to Home Screen"; + /* Title for the button to enable push notifications in system settings */ "waitlist.allow-notifications" = "Dovoli obvestila"; @@ -2439,6 +2516,9 @@ /* Title for the share sheet entry */ "waitlist.share-sheet.title" = "Povabljeni ste!"; +/* No comment provided by engineer. */ +"We want to make using passwords in DuckDuckGo better." = "We want to make using passwords in DuckDuckGo better."; + /* Confirmation message */ "web.url.remove.favorite.done" = "Priljubljeni odstranjeni"; diff --git a/DuckDuckGo/sv.lproj/Autocomplete.strings b/DuckDuckGo/sv.lproj/Autocomplete.strings deleted file mode 100644 index cf3ef8d060..0000000000 --- a/DuckDuckGo/sv.lproj/Autocomplete.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Class = "UILabel"; text = "Label"; ObjectID = "5ag-a1-mlA"; */ -"5ag-a1-mlA.text" = "Etikett"; - -/* Class = "UILabel"; text = "Label"; ObjectID = "eiv-Cq-E2r"; */ -"eiv-Cq-E2r.text" = "Etikett"; - -/* Class = "UILabel"; text = "No Suggestions"; ObjectID = "gPe-Cv-14j"; */ -"gPe-Cv-14j.text" = "Inga förslag"; - -/* Class = "UIButton"; accessibilityLabel = "Autocomplete suggestion"; ObjectID = "N97-eI-Lps"; */ -"N97-eI-Lps.accessibilityLabel" = "Komplettera automatiskt-förslag"; - diff --git a/DuckDuckGo/sv.lproj/Localizable.strings b/DuckDuckGo/sv.lproj/Localizable.strings index d23e66d060..01c7291be2 100644 --- a/DuckDuckGo/sv.lproj/Localizable.strings +++ b/DuckDuckGo/sv.lproj/Localizable.strings @@ -119,7 +119,7 @@ "addWidget.description" = "Få snabb tillgång till privat sökning och dina favoritwebbplatser."; /* No comment provided by engineer. */ -"addWidget.settings.firstParagraph" = "Tryck länge på startskärmen tills apparna börjar vicka."; +"addWidget.settings.firstParagraph" = "Long-press on the Home Screen to enter jiggle mode."; /* Replacement string is a plus button icon. */ "addWidget.settings.secondParagraph.%@" = "Tryck på plusknappen för %@."; @@ -133,15 +133,48 @@ /* No comment provided by engineer. */ "alert.message.bookmarkAll" = "Befintliga bokmärken kommer inte att kopieras."; +/* Description for alert shown when sync error occurs because of bad data */ +"alert.sync-bad-data-error-description" = "Vissa bokmärken eller lösenord är felaktigt formaterade eller för långa och synkroniserades inte."; + +/* Description for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-description" = "Du har nått det maximala antalet bokmärken. Ta bort några bokmärken för att återuppta synkroniseringen."; + +/* Title for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-title" = "Bokmärkessynkronisering är pausad"; + +/* Description for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-description" = "Du har nått det maximala antalet lösenord. Ta bort några lösenord för att återuppta synkroniseringen."; + +/* Title for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-title" = "Lösenordssynkronisering är pausad"; + /* Title for sync error alert */ "alert.sync-error" = "Synkroniserings- och säkerhetskopieringsfel"; +/* Sync error alert action button title, takes the user to the sync settings page. */ +"alert.sync-error-action" = "Synkronisera inställningar"; + +/* Description for alert shown when user logged off from sync */ +"alert.sync-invalid-login-error-description" = "Synkroniseringen har pausats. Om du vill fortsätta att synkronisera den här enheten ansluter du igen från en annan enhet eller med din återställningskod."; + /* Learn more button in alert */ "alert.sync-paused-alert-learn-more-button" = "Läs mer"; /* Confirmation button in alert */ "alert.sync-paused-alert-ok-button" = "OK"; +/* Title for alert shown when sync paused for an error */ +"alert.sync-paused-title" = "Synkroniseringen är pausad"; + +/* Description for alert shown when sync error occurs because of too many requests */ +"alert.sync-too-many-requests-error-description" = "Sync & Backup är inte tillgängligt just nu."; + +/* Title of the warning message that tells the user that there was an error with the sync feature. */ +"alert.sync.warning.sync-error" = "Synkroniseringsfel"; + +/* Title of the warning message */ +"alert.sync.warning.sync-paused" = "Synkronisering och säkerhetskopiering har pausats"; + /* Question from confirmation dialog */ "alert.title.bookmarkAll" = "Bokmärk alla flikar?"; @@ -208,6 +241,15 @@ /* No comment provided by engineer. */ "autoclear.on" = "På"; +/* Subtitle for search history items */ +"autocomplete.history.search.duckduckgo" = "Sök med DuckDuckGo"; + +/* The message text shown in suggestions */ +"autocomplete.history.warning.message" = "Nyligen besökta webbplatser inkluderas nu i förslag. Dessa lagras på din enhet – aldrig på servrar hos DuckDuckGo. Stäng av funktionen i inställningarna, eller rensa när som helst med 🔥 Fire Button."; + +/* Title for message show in suggestions */ +"autocomplete.history.warning.title" = "Samma integritet.\nBättre sökförslag!"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Inaktiverad"; @@ -1147,6 +1189,9 @@ /* Title for the get desktop browser feature */ "get.browser.title" = "Hämta DuckDuckGo för Mac eller Windows"; +/* No comment provided by engineer. */ +"Help us improve!" = "Help us improve!"; + /* No comment provided by engineer. */ "home.row.onboarding.header" = "Lägg till DuckDuckGo på din hemskärm!"; @@ -1603,6 +1648,26 @@ /* Deactivate button */ "pm.deactivate" = "Deactivate"; +/* Button title for sync bookmarks limits exceeded warning to go to manage bookmarks */ +"prefrences.sync.bookmarks-limit-exceeded-action" = "Hantera bokmärken"; + +/* Description for sync bookmarks limits exceeded warning */ +"prefrences.sync.bookmarks-limit-exceeded-description" = "Du har nått det maximala antalet bokmärken. Ta bort några för att återuppta synkronisering."; + +/* Button title for sync credentials limits exceeded warning to go to manage passwords */ +"prefrences.sync.credentials-limit-exceeded-action" = "Hantera lösenord…"; + +/* Description for sync credentials limits exceeded warning */ +"prefrences.sync.credentials-limit-exceeded-description" = "Du har nått det maximala antalet lösenord. Ta bort några för att återuppta synkroniseringen."; + +/* Description invalid credentials error when syncing. + Description of incorrectly formatted data error when syncing. + Description of too many requests error when syncing. */ +"prefrences.sync.invalid-login-description" = "Det inträffade ett fel under synkroniseringen. Prova att inaktivera synkronisering på den här enheten och anslut sedan på nytt från en annan enhet eller med din återställningskod."; + +/* Title for sync limits exceeded warning */ +"prefrences.sync.limit-exceeded-title" = "Synkronisering pausad"; + /* No comment provided by engineer. */ "preserveLogins.domain.list.footer" = "Webbplatser använder kakor för att hålla dig inloggad. Om du brandsäkrar en webbplats kommer kakor inte att raderas och du förblir inloggad även efter att du har använt brännarknappen. Vi blockerar fortfarande tredje parters spårare som finns på brandsäkrade webbplatser."; @@ -1712,6 +1777,9 @@ /* Settings screen cell for autocomplete */ "settings.autocomplete" = "Visa Autoslutför förslag"; +/* Settings label for enabling or disabling recently visited sites */ +"settings.autocomplete.recentlyvisited" = "Senast besökta webbplatser"; + /* Settings screen cell text for Application Lock */ "settings.autolock" = "App-lås"; @@ -2241,6 +2309,9 @@ /* Tab Switcher Accessibility Label */ "tab.switcher.accessibility.label" = "Flikväxlare"; +/* No comment provided by engineer. */ +"Take Survey" = "Take Survey"; + /* Description text for the text size adjustment setting */ "textSize.description" = "Välj önskad textstorlek. Webbplatser som du visar i DuckDuckGo anpassas efter din inställning."; @@ -2319,6 +2390,9 @@ /* Alert title for the alert when the Privacy Pro subscription expires */ "vpn.access-revoked.alert.title" = "VPN disconnected due to expired subscription"; +/* Title for the VPN widget onboarding screen */ +"vpn.addWidget.settings.title" = "Find and select DuckDuckGo. Then swipe to VPN and select Add Widget."; + /* Title for the Cancel button of the VPN feedback form */ "vpn.feedback-form.button.cancel" = "Cancel"; @@ -2388,6 +2462,9 @@ /* Title for each screen of the VPN feedback form */ "vpn.feedback-form.title" = "Help Improve the DuckDuckGo VPN"; +/* VPN settings screen cell text for adding the VPN widget to the home screen */ +"vpn.settings.add.widget" = "Add VPN Widget to Home Screen"; + /* Title for the button to enable push notifications in system settings */ "waitlist.allow-notifications" = "Tillåt aviseringar"; @@ -2439,6 +2516,9 @@ /* Title for the share sheet entry */ "waitlist.share-sheet.title" = "Du är inbjuden!"; +/* No comment provided by engineer. */ +"We want to make using passwords in DuckDuckGo better." = "We want to make using passwords in DuckDuckGo better."; + /* Confirmation message */ "web.url.remove.favorite.done" = "Favorit borttagen"; diff --git a/DuckDuckGo/tr.lproj/Autocomplete.strings b/DuckDuckGo/tr.lproj/Autocomplete.strings deleted file mode 100644 index 630e85e222..0000000000 --- a/DuckDuckGo/tr.lproj/Autocomplete.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Class = "UILabel"; text = "Label"; ObjectID = "5ag-a1-mlA"; */ -"5ag-a1-mlA.text" = "Label"; - -/* Class = "UILabel"; text = "Label"; ObjectID = "eiv-Cq-E2r"; */ -"eiv-Cq-E2r.text" = "Label"; - -/* Class = "UILabel"; text = "No Suggestions"; ObjectID = "gPe-Cv-14j"; */ -"gPe-Cv-14j.text" = "Öneri yok"; - -/* Class = "UIButton"; accessibilityLabel = "Autocomplete suggestion"; ObjectID = "N97-eI-Lps"; */ -"N97-eI-Lps.accessibilityLabel" = "Otomatik tamamlama önerisi"; - diff --git a/DuckDuckGo/tr.lproj/Localizable.strings b/DuckDuckGo/tr.lproj/Localizable.strings index 02629e46cd..2f4a79a711 100644 --- a/DuckDuckGo/tr.lproj/Localizable.strings +++ b/DuckDuckGo/tr.lproj/Localizable.strings @@ -119,7 +119,7 @@ "addWidget.description" = "Özel aramaya ve sevdiğiniz sitelere hızlı erişim elde edin."; /* No comment provided by engineer. */ -"addWidget.settings.firstParagraph" = "Sallama moduna girmek için ana ekranda uzun basın."; +"addWidget.settings.firstParagraph" = "Long-press on the Home Screen to enter jiggle mode."; /* Replacement string is a plus button icon. */ "addWidget.settings.secondParagraph.%@" = "Artı %@ düğmesine dokunun."; @@ -133,15 +133,48 @@ /* No comment provided by engineer. */ "alert.message.bookmarkAll" = "Mevcut yer imleri çoğaltılmayacaktır."; +/* Description for alert shown when sync error occurs because of bad data */ +"alert.sync-bad-data-error-description" = "Bazı yer işaretleri veya parolalar yanlış veya çok uzun biçimlendirilmiş ve senkronize edilmemiş."; + +/* Description for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-description" = "Maksimum yer işareti sayısına ulaştınız. Senkronizasyona devam etmek için lütfen bazı yer işaretlerini silin."; + +/* Title for alert shown when sync bookmarks paused for too many items */ +"alert.sync-bookmarks-paused-title" = "Yer İşaretleri Senkronizasyonu Duraklatıldı"; + +/* Description for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-description" = "Maksimum şifre sayısına ulaştınız. Lütfen senkronizasyona devam etmek için bazı şifreleri silin."; + +/* Title for alert shown when sync credentials paused for too many items */ +"alert.sync-credentials-paused-title" = "Şifre Senkronizasyonu Duraklatıldı"; + /* Title for sync error alert */ "alert.sync-error" = "Senkronizasyon ve Yedekleme Hatası"; +/* Sync error alert action button title, takes the user to the sync settings page. */ +"alert.sync-error-action" = "Senkronizasyon Ayarları"; + +/* Description for alert shown when user logged off from sync */ +"alert.sync-invalid-login-error-description" = "Senkronizasyon duraklatıldı. Bu cihazın senkronizasyonuna devam etmek istiyorsanız, başka bir cihaz veya kurtarma kodunuzu kullanarak yeniden bağlanın."; + /* Learn more button in alert */ "alert.sync-paused-alert-learn-more-button" = "Daha Fazla Bilgi"; /* Confirmation button in alert */ "alert.sync-paused-alert-ok-button" = "Tamam"; +/* Title for alert shown when sync paused for an error */ +"alert.sync-paused-title" = "Senkronizasyon Duraklatıldı"; + +/* Description for alert shown when sync error occurs because of too many requests */ +"alert.sync-too-many-requests-error-description" = "Senkronizasyon ve Yedekleme geçici olarak kullanılamıyor."; + +/* Title of the warning message that tells the user that there was an error with the sync feature. */ +"alert.sync.warning.sync-error" = "Senkronizasyon Hatası"; + +/* Title of the warning message */ +"alert.sync.warning.sync-paused" = "Senkronizasyon ve Yedekleme Duraklatıldı"; + /* Question from confirmation dialog */ "alert.title.bookmarkAll" = "Tüm Sekmelere Yer İmi Eklensin mi?"; @@ -208,6 +241,15 @@ /* No comment provided by engineer. */ "autoclear.on" = "Açık"; +/* Subtitle for search history items */ +"autocomplete.history.search.duckduckgo" = "DuckDuckGo'da Ara"; + +/* The message text shown in suggestions */ +"autocomplete.history.warning.message" = "Öneriler artık son ziyaret edilen siteleri de içeriyor. Bunlar cihazınızda saklanır, hiçbir zaman DuckDuckGo'nun sunucularında saklanmaz. Bunları ayarlardan kapatabilir veya 🔥 Fire Button ile istediğiniz zaman temizleyebilirsiniz."; + +/* Title for message show in suggestions */ +"autocomplete.history.warning.title" = "Aynı gizlilik.\nDaha iyi arama önerileri!"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Devre Dışı"; @@ -1147,6 +1189,9 @@ /* Title for the get desktop browser feature */ "get.browser.title" = "Mac veya Windows için DuckDuckGo'yu edinin"; +/* No comment provided by engineer. */ +"Help us improve!" = "Help us improve!"; + /* No comment provided by engineer. */ "home.row.onboarding.header" = "DuckDuckGo'yu ana ekranınıza ekleyin!"; @@ -1603,6 +1648,26 @@ /* Deactivate button */ "pm.deactivate" = "Deactivate"; +/* Button title for sync bookmarks limits exceeded warning to go to manage bookmarks */ +"prefrences.sync.bookmarks-limit-exceeded-action" = "Yer İmlerini Yönet"; + +/* Description for sync bookmarks limits exceeded warning */ +"prefrences.sync.bookmarks-limit-exceeded-description" = "Maksimum yer işareti sayısına ulaştınız. Lütfen senkronizasyona devam etmek için bazılarını silin."; + +/* Button title for sync credentials limits exceeded warning to go to manage passwords */ +"prefrences.sync.credentials-limit-exceeded-action" = "Şifreleri yönet…"; + +/* Description for sync credentials limits exceeded warning */ +"prefrences.sync.credentials-limit-exceeded-description" = "Maksimum şifre sayısına ulaştınız. Lütfen senkronizasyona devam etmek için bazılarını silin."; + +/* Description invalid credentials error when syncing. + Description of incorrectly formatted data error when syncing. + Description of too many requests error when syncing. */ +"prefrences.sync.invalid-login-description" = "Senkronizasyon bir hatayla karşılaştı. Bu cihazda senkronizasyonu devre dışı bırakmayı deneyin ve ardından başka bir cihaz veya kurtarma kodunuzu kullanarak yeniden bağlanın."; + +/* Title for sync limits exceeded warning */ +"prefrences.sync.limit-exceeded-title" = "Senkronizasyon Duraklatıldı"; + /* No comment provided by engineer. */ "preserveLogins.domain.list.footer" = "Web siteleri oturumunuzu açık tutmak için çerezler kullanır. Bir siteyi Korumalı hâle getirdiğinizde çerezler silinmez ve Yangın Düğmesi'ni kullandıktan sonra bile oturumunuz açık kalır. Korumalı web sitelerinde bulunan üçüncü taraf izleyicileri yine engelleriz."; @@ -1712,6 +1777,9 @@ /* Settings screen cell for autocomplete */ "settings.autocomplete" = "Otomatik Tamamlama Önerileri"; +/* Settings label for enabling or disabling recently visited sites */ +"settings.autocomplete.recentlyvisited" = "Son Ziyaret Edilen Siteler"; + /* Settings screen cell text for Application Lock */ "settings.autolock" = "Uygulama Kilidi"; @@ -2241,6 +2309,9 @@ /* Tab Switcher Accessibility Label */ "tab.switcher.accessibility.label" = "Sekme Değiştirici"; +/* No comment provided by engineer. */ +"Take Survey" = "Take Survey"; + /* Description text for the text size adjustment setting */ "textSize.description" = "Tercih ettiğiniz metin boyutunu seçin. DuckDuckGo'da görüntülediğiniz web siteleri bu boyuta ayarlanır."; @@ -2319,6 +2390,9 @@ /* Alert title for the alert when the Privacy Pro subscription expires */ "vpn.access-revoked.alert.title" = "VPN disconnected due to expired subscription"; +/* Title for the VPN widget onboarding screen */ +"vpn.addWidget.settings.title" = "Find and select DuckDuckGo. Then swipe to VPN and select Add Widget."; + /* Title for the Cancel button of the VPN feedback form */ "vpn.feedback-form.button.cancel" = "Cancel"; @@ -2388,6 +2462,9 @@ /* Title for each screen of the VPN feedback form */ "vpn.feedback-form.title" = "Help Improve the DuckDuckGo VPN"; +/* VPN settings screen cell text for adding the VPN widget to the home screen */ +"vpn.settings.add.widget" = "Add VPN Widget to Home Screen"; + /* Title for the button to enable push notifications in system settings */ "waitlist.allow-notifications" = "Bildirimlere İzin Ver"; @@ -2439,6 +2516,9 @@ /* Title for the share sheet entry */ "waitlist.share-sheet.title" = "Davetlisiniz!"; +/* No comment provided by engineer. */ +"We want to make using passwords in DuckDuckGo better." = "We want to make using passwords in DuckDuckGo better."; + /* Confirmation message */ "web.url.remove.favorite.done" = "Favori kaldırıldı"; diff --git a/DuckDuckGoTests/AppSettingsMock.swift b/DuckDuckGoTests/AppSettingsMock.swift index fac9db41d3..e49b522144 100644 --- a/DuckDuckGoTests/AppSettingsMock.swift +++ b/DuckDuckGoTests/AppSettingsMock.swift @@ -22,6 +22,9 @@ import Foundation @testable import DuckDuckGo class AppSettingsMock: AppSettings { + + var recentlyVisitedSites: Bool = false + var isSyncBookmarksPaused: Bool = false var isSyncCredentialsPaused: Bool = false diff --git a/DuckDuckGoTests/AutoClearTests.swift b/DuckDuckGoTests/AutoClearTests.swift index b20c744a38..1cc8383bf9 100644 --- a/DuckDuckGoTests/AutoClearTests.swift +++ b/DuckDuckGoTests/AutoClearTests.swift @@ -24,7 +24,7 @@ import XCTest class AutoClearTests: XCTestCase { class MockWorker: AutoClearWorker { - + var clearNavigationStackInvocationCount = 0 var forgetDataInvocationCount = 0 var forgetTabsInvocationCount = 0 @@ -37,7 +37,12 @@ class AutoClearTests: XCTestCase { func forgetData() { forgetDataInvocationCount += 1 } - + + func forgetData(applicationState: Core.DataStoreWarmup.ApplicationState) { + forgetDataInvocationCount += 1 + } + + func forgetTabs() { forgetTabsInvocationCount += 1 } @@ -59,13 +64,13 @@ class AutoClearTests: XCTestCase { appSettings.autoClearAction = .clearData appSettings.autoClearTiming = .termination - await logic.clearDataIfEnabledAndTimeExpired() + await logic.clearDataIfEnabledAndTimeExpired(applicationState: .unknown) logic.startClearingTimer() XCTAssertEqual(worker.clearNavigationStackInvocationCount, 0) XCTAssertEqual(worker.forgetDataInvocationCount, 0) - await logic.clearDataIfEnabledAndTimeExpired() + await logic.clearDataIfEnabledAndTimeExpired(applicationState: .unknown) XCTAssertEqual(worker.clearNavigationStackInvocationCount, 0) XCTAssertEqual(worker.forgetDataInvocationCount, 0) @@ -89,13 +94,13 @@ class AutoClearTests: XCTestCase { // Swift Concurrency appears to sometimes get delayed so we pass the base time internal to use just for tests // otherwise it's not computed until the functional is called - await logic.clearDataIfEnabledAndTimeExpired(baseTimeInterval: Date().timeIntervalSince1970) + await logic.clearDataIfEnabledAndTimeExpired(baseTimeInterval: Date().timeIntervalSince1970, applicationState: .unknown) XCTAssertEqual(worker.clearNavigationStackInvocationCount, iterationCount) XCTAssertEqual(worker.forgetDataInvocationCount, iterationCount) logic.startClearingTimer(Date().timeIntervalSince1970 - delay - 1) - await logic.clearDataIfEnabledAndTimeExpired(baseTimeInterval: Date().timeIntervalSince1970) + await logic.clearDataIfEnabledAndTimeExpired(baseTimeInterval: Date().timeIntervalSince1970, applicationState: .unknown) iterationCount += 1 XCTAssertEqual(worker.clearNavigationStackInvocationCount, iterationCount) diff --git a/DuckDuckGoTests/AutoconsentMessageProtocolTests.swift b/DuckDuckGoTests/AutoconsentMessageProtocolTests.swift index 3b0ce23cce..f762bbe73a 100644 --- a/DuckDuckGoTests/AutoconsentMessageProtocolTests.swift +++ b/DuckDuckGoTests/AutoconsentMessageProtocolTests.swift @@ -116,6 +116,7 @@ final class AutoconsentMessageProtocolTests: XCTestCase { waitForExpectations(timeout: 1.0) } + // Flaky test that fails often, to re-evaluate. See 15s timeout, something wrong here @MainActor func testEval() { let message = MockWKScriptMessage(name: "eval", body: [ diff --git a/DuckDuckGoTests/AutofillPixelReporterTests.swift b/DuckDuckGoTests/AutofillPixelReporterTests.swift deleted file mode 100644 index 0c915d5b18..0000000000 --- a/DuckDuckGoTests/AutofillPixelReporterTests.swift +++ /dev/null @@ -1,193 +0,0 @@ -// -// AutofillPixelReporterTests.swift -// DuckDuckGo -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import XCTest -@testable import DuckDuckGo -@testable import Core -@testable import BrowserServicesKit - -final class AutofillPixelReporterTests: XCTestCase { - - private var statisticsStorage: MockStatisticsStore! - private let vault = (try? MockSecureVaultFactory.makeVault(reporter: nil))! - private var autofillPixelReporter: AutofillPixelReporter! - - override func setUpWithError() throws { - statisticsStorage = MockStatisticsStore() - - setupUserDefault(with: #file) - UserDefaults.app.removeObject(forKey: UserDefaultsWrapper.Key.autofillSearchDauDate.rawValue) - UserDefaults.app.removeObject(forKey: UserDefaultsWrapper.Key.autofillFillDate.rawValue) - UserDefaults.app.removeObject(forKey: UserDefaultsWrapper.Key.autofillOnboardedUser.rawValue) - - autofillPixelReporter = AutofillPixelReporter(statisticsStorage: statisticsStorage, - secureVault: vault) - } - - override func tearDownWithError() throws { - statisticsStorage = nil - autofillPixelReporter = nil - } - - func testWhenUserSearchDauIsNotTodayAndAutofillDateIsNotTodayAndEventTypeIsSearchDauThenNoPixelsWillBeFired() { - autofillPixelReporter.autofillSearchDauDate = Date().addingTimeInterval(.days(-2)) - autofillPixelReporter.autofillFillDate = Date().addingTimeInterval(.days(-2)) - - XCTAssertTrue(autofillPixelReporter.pixelsToFireFor(.searchDAU).isEmpty) - } - - func testWhenUserSearchDauIsNotTodayAndAutofillDateIsTodayAndEventTypeIsSearchDauThenNoPixelsWillBeFired() { - autofillPixelReporter.autofillSearchDauDate = Date().addingTimeInterval(.days(-2)) - autofillPixelReporter.autofillFillDate = Date() - - XCTAssertTrue(autofillPixelReporter.pixelsToFireFor(.searchDAU).isEmpty) - } - - func testWhenUserSearchDauIsNotTodayAndEventTypeIsFillThenNoPixelsWillBeFired() { - autofillPixelReporter.autofillSearchDauDate = Date().addingTimeInterval(-2 * 60 * 60 * 24) - autofillPixelReporter.autofillFillDate = Date().addingTimeInterval(-2 * 60 * 60 * 24) - - XCTAssertTrue(autofillPixelReporter.pixelsToFireFor(.fill).isEmpty) - } - - func testWhenUserSearchDauIsTodayAndAutofillDateIsTodayAndEventTypeIsSearchDauAndAccountsCountIsLessThanTenThenTwoPixelWillBeFired() { - autofillPixelReporter.autofillSearchDauDate = Date() - autofillPixelReporter.autofillFillDate = Date() - createAccountsInVault(count: 4) - - XCTAssertEqual(autofillPixelReporter.pixelsToFireFor(.searchDAU).count, 2) - } - - func testWhenUserSearchDauIsTodayAndAutofillDateIsTodayAndEventTypeIsSearchDauAndAccountsCountIsTenThenThreePixelsWillBeFired() { - autofillPixelReporter.autofillSearchDauDate = Date() - autofillPixelReporter.autofillFillDate = Date() - createAccountsInVault(count: 10) - - XCTAssertEqual(autofillPixelReporter.pixelsToFireFor(.searchDAU).count, 3) - } - - func testWhenUserSearchDauIsTodayAndAutofillDateIsTodayAndEventTypeIsSearchDauAndAccountsCountIsGreaterThanTenThenThreePixelsWillBeFired() { - autofillPixelReporter.autofillSearchDauDate = Date() - autofillPixelReporter.autofillFillDate = Date() - createAccountsInVault(count: 15) - - XCTAssertEqual(autofillPixelReporter.pixelsToFireFor(.searchDAU).count, 3) - } - - func testWhenUserSearchDauIsTodayAndAutofillDateIsNotTodayAndEventTypeIsSearchDauAndAccountsCountIsLessThanTenThenNoPixelsWillBeFired() { - autofillPixelReporter.autofillSearchDauDate = Date() - autofillPixelReporter.autofillFillDate = Date().addingTimeInterval(-2 * 60 * 60 * 24) - createAccountsInVault(count: 4) - - XCTAssertTrue(autofillPixelReporter.pixelsToFireFor(.searchDAU).isEmpty) - } - - func testWhenUserSearchDauIsTodayAndAutofillDateIsNotTodayAndEventTypeIsSearchDauAndAccountsCountIsTenThenOnePixelWillBeFired() { - autofillPixelReporter.autofillSearchDauDate = Date() - autofillPixelReporter.autofillFillDate = Date().addingTimeInterval(-2 * 60 * 60 * 24) - createAccountsInVault(count: 10) - - XCTAssertEqual(autofillPixelReporter.pixelsToFireFor(.searchDAU).count, 1) - } - - func testWhenUserSearchDauIsTodayAndAutofillDateIsNotTodayAndEventTypeIsSearchDauAndAccountsCountIsGreaterThanTenThenOnePixelWillBeFired() { - autofillPixelReporter.autofillSearchDauDate = Date() - autofillPixelReporter.autofillFillDate = Date().addingTimeInterval(-2 * 60 * 60 * 24) - createAccountsInVault(count: 15) - - XCTAssertEqual(autofillPixelReporter.pixelsToFireFor(.searchDAU).count, 1) - } - - func testWhenUserIsNotOnboardedAndInstallDateIsNilThenOnboardedUserPixelShouldNotBeFired() { - autofillPixelReporter.autofillOnboardedUser = false - statisticsStorage.installDate = nil - - XCTAssertFalse(autofillPixelReporter.shouldFireOnboardedUserPixel()) - } - - func testWhenUserIsNotOnboardedAndInstallDateIsTodayThenOnboardedUserPixelShouldNotBeFired() { - autofillPixelReporter.autofillOnboardedUser = false - statisticsStorage.installDate = Date() - - XCTAssertFalse(autofillPixelReporter.shouldFireOnboardedUserPixel()) - } - - func testWhenUserIsNotOnboardedAndInstallDateIsYesterdayAndAccountsCountIsZeroThenOnboardedUserPixelShouldNotBeFired() { - autofillPixelReporter.autofillOnboardedUser = false - statisticsStorage.installDate = Date().addingTimeInterval(.days(-1)) - createAccountsInVault(count: 0) - - XCTAssertFalse(autofillPixelReporter.shouldFireOnboardedUserPixel()) - } - - func testWhenUserIsNotOnboardedAndInstallDateIsYesterdayAndAccountsCountIsGreaterThanZeroThenOnboardedUserPixelShouldBeFiredAndAutofillOnboardedUserShouldBeTrue() { - autofillPixelReporter.autofillOnboardedUser = false - statisticsStorage.installDate = Date().addingTimeInterval(.days(-1)) - createAccountsInVault(count: 4) - - XCTAssertTrue(autofillPixelReporter.shouldFireOnboardedUserPixel()) - XCTAssertTrue(autofillPixelReporter.autofillOnboardedUser) - } - - func testWhenUserIsNotOnboardedAndInstallDateIsLessThanSevenDaysAgoAndAccountsCountIsZeroThenOnboardedUserPixelShouldNotBeFired() { - autofillPixelReporter.autofillOnboardedUser = false - statisticsStorage.installDate = Date().addingTimeInterval(.days(-4)) - createAccountsInVault(count: 0) - - XCTAssertFalse(autofillPixelReporter.shouldFireOnboardedUserPixel()) - } - - func testWhenUserIsNotOnboardedAndInstallDateIsLessThanSevenDaysAgoAndAccountsCountIsGreaterThanZeroThenOnboardedUserPixelShouldBeFiredAndAutofillOnboardedUserShouldBeTrue() { - autofillPixelReporter.autofillOnboardedUser = false - statisticsStorage.installDate = Date().addingTimeInterval(.days(-4)) - createAccountsInVault(count: 4) - - XCTAssertTrue(autofillPixelReporter.shouldFireOnboardedUserPixel()) - XCTAssertTrue(autofillPixelReporter.autofillOnboardedUser) - } - - func testWhenUserIsNotOnboardedAndInstallDateIsMoreThanSevenDaysAgoThenOnboardedUserPixelShouldNotBeFiredAndAutofillOnboardedUserShouldBeTrue() { - autofillPixelReporter.autofillOnboardedUser = false - statisticsStorage.installDate = Date().addingTimeInterval(.days(-8)) - - XCTAssertFalse(autofillPixelReporter.shouldFireOnboardedUserPixel()) - XCTAssertTrue(autofillPixelReporter.autofillOnboardedUser) - } - - func testWhenUserIsOnboardedThenOnboardedUserPixelShouldNotBeFired() { - autofillPixelReporter.autofillOnboardedUser = true - - XCTAssertFalse(autofillPixelReporter.shouldFireOnboardedUserPixel()) - } - - private func createAccountsInVault(count: Int) { - try? vault.deleteAllWebsiteCredentials() - - for i in 0.. HistoryCapture { return HistoryCapture(historyManager: MockHistoryManager(historyCoordinator: mockHistoryCoordinator)) } @@ -85,7 +112,6 @@ class MockHistoryManager: HistoryManaging { self.historyCoordinator = historyCoordinator } - func loadStore() { + func loadStore(onCleanFinished: @escaping () -> Void) throws { } - } diff --git a/DuckDuckGoTests/HistoryManagerTests.swift b/DuckDuckGoTests/HistoryManagerTests.swift index 50828b8eda..22ec55a5f3 100644 --- a/DuckDuckGoTests/HistoryManagerTests.swift +++ b/DuckDuckGoTests/HistoryManagerTests.swift @@ -26,70 +26,110 @@ import History final class HistoryManagerTests: XCTestCase { + let privacyConfig = MockPrivacyConfiguration() let privacyConfigManager = MockPrivacyConfigurationManager() var variantManager = MockVariantManager() + let internalUserStore = MockInternalUserStoring() + var enabledByUser = true func test() { struct Condition { + let privacyConfig: Bool let variant: Bool - let privacy: Bool + let inRollOut: Bool + let internalUser: Bool let expected: Bool } let conditions = [ - Condition(variant: true, privacy: true, expected: true), - Condition(variant: false, privacy: true, expected: false), - Condition(variant: true, privacy: false, expected: false), - ] + // Users in the experiment should get the feature + Condition(privacyConfig: true, variant: true, inRollOut: false, internalUser: false, expected: true), + Condition(privacyConfig: true, variant: true, inRollOut: true, internalUser: false, expected: true), + + // If not previously in the experiment then check for the rollout + Condition(privacyConfig: true, variant: false, inRollOut: false, internalUser: false, expected: false), + Condition(privacyConfig: true, variant: false, inRollOut: true, internalUser: false, expected: true), - let privacyConfig = MockPrivacyConfiguration() - let privacyConfigManager = MockPrivacyConfigurationManager() - var variantManager = MockVariantManager() + // Internal users also get the feature + Condition(privacyConfig: true, variant: false, inRollOut: false, internalUser: true, expected: true), + Condition(privacyConfig: true, variant: false, inRollOut: true, internalUser: true, expected: true), + + // Privacy config is the ultimate on/off switch though + Condition(privacyConfig: false, variant: true, inRollOut: true, internalUser: true, expected: false), + ] - for condition in conditions { + for index in conditions.indices { + let condition = conditions[index] privacyConfig.isFeatureKeyEnabled = { feature, _ in XCTAssertEqual(feature, .history) - return condition.privacy + return condition.privacyConfig } + privacyConfig.isSubfeatureKeyEnabled = { subFeature, _ in + XCTAssertEqual(subFeature as? HistorySubFeature, HistorySubFeature.onByDefault) + return condition.inRollOut + } + + internalUserStore.isInternalUser = condition.internalUser privacyConfigManager.privacyConfig = privacyConfig variantManager.isSupportedReturns = condition.variant let model = CoreDataDatabase.loadModel(from: History.bundle, named: "BrowsingHistory")! let db = CoreDataDatabase(name: "Test", containerLocation: tempDBDir(), model: model) + db.loadStore() - let historyManager = HistoryManager(privacyConfigManager: privacyConfigManager, variantManager: variantManager, database: db) { + let historyManager = makeHistoryManager(db) { XCTFail("DB Error \($0)") } - XCTAssertEqual(condition.expected, historyManager.isHistoryFeatureEnabled(), String(describing: condition)) + + XCTAssertEqual(condition.expected, historyManager.isHistoryFeatureEnabled(), "\(index): \(condition)") + + if condition.expected { + XCTAssertTrue(historyManager.historyCoordinator is HistoryCoordinator) + } else { + XCTAssertTrue(historyManager.historyCoordinator is NullHistoryCoordinator) + } + } } - func test_WhenManagerFailsToLoadStore_ThenThrowsError() { - let privacyConfig = MockPrivacyConfiguration() - let privacyConfigManager = MockPrivacyConfigurationManager() - var variantManager = MockVariantManager() + func test_WhenUserHasDisabledSetting_ThenDontStoreOrLoadHistory() { privacyConfig.isFeatureKeyEnabled = { feature, _ in XCTAssertEqual(feature, .history) return true } + internalUserStore.isInternalUser = true privacyConfigManager.privacyConfig = privacyConfig - variantManager.isSupportedReturns = true + enabledByUser = false let model = CoreDataDatabase.loadModel(from: History.bundle, named: "BrowsingHistory")! - let db = CoreDataDatabase(name: "Test", containerLocation: URL.aboutLink, model: model) + let db = CoreDataDatabase(name: "Test", containerLocation: tempDBDir(), model: model) + db.loadStore() - var error: Error? - let historyManager = HistoryManager(privacyConfigManager: privacyConfigManager, variantManager: variantManager, database: db) { - error = $0 + let historyManager = makeHistoryManager(db) { + XCTFail("DB Error \($0)") } - _ = historyManager.historyCoordinator - XCTAssertNotNil(error) + + XCTAssertTrue(historyManager.historyCoordinator is NullHistoryCoordinator) + } + + private func makeHistoryManager(_ db: CoreDataDatabase, onStoreLoadFailed: @escaping (Error) -> Void) -> HistoryManager { + let manager = HistoryManager(privacyConfigManager: privacyConfigManager, + variantManager: variantManager, + database: db, + internalUserDecider: DefaultInternalUserDecider(mockedStore: internalUserStore), + isEnabledByUser: self.isEnabledByUser()) + return manager + } + + private func isEnabledByUser() -> Bool { + return enabledByUser } + } diff --git a/DuckDuckGoTests/MockDependencyProvider.swift b/DuckDuckGoTests/MockDependencyProvider.swift index b49e71fd48..5a143cf727 100644 --- a/DuckDuckGoTests/MockDependencyProvider.swift +++ b/DuckDuckGoTests/MockDependencyProvider.swift @@ -22,10 +22,11 @@ import Core import BrowserServicesKit import DDGSync import Subscription +import SubscriptionTestingUtilities +import NetworkProtection @testable import DuckDuckGo class MockDependencyProvider: DependencyProvider { - var appSettings: AppSettings var variantManager: VariantManager var featureFlagger: FeatureFlagger @@ -41,6 +42,14 @@ class MockDependencyProvider: DependencyProvider { var userBehaviorMonitor: UserBehaviorMonitor var toggleProtectionsCounter: ToggleProtectionsCounter var subscriptionFeatureAvailability: SubscriptionFeatureAvailability + var subscriptionManager: SubscriptionManaging + var accountManager: AccountManaging + var vpnFeatureVisibility: DefaultNetworkProtectionVisibility + var networkProtectionKeychainTokenStore: NetworkProtectionKeychainTokenStore + var networkProtectionAccessController: NetworkProtectionAccessController + var networkProtectionTunnelController: NetworkProtectionTunnelController + var connectionObserver: NetworkProtection.ConnectionStatusObserver + var vpnSettings: NetworkProtection.VPNSettings init() { let defaultProvider = AppDependencyProvider() @@ -59,5 +68,42 @@ class MockDependencyProvider: DependencyProvider { userBehaviorMonitor = defaultProvider.userBehaviorMonitor toggleProtectionsCounter = defaultProvider.toggleProtectionsCounter subscriptionFeatureAvailability = defaultProvider.subscriptionFeatureAvailability + + accountManager = AccountManagerMock(isUserAuthenticated: true) + if #available(iOS 15.0, *) { + let subscriptionService = SubscriptionService(currentServiceEnvironment: .production) + let authService = AuthService(currentServiceEnvironment: .production) + let storePurchaseManaging = StorePurchaseManager() + subscriptionManager = SubscriptionManagerMock(accountManager: accountManager, + subscriptionService: subscriptionService, + authService: authService, + storePurchaseManager: storePurchaseManaging, + currentEnvironment: SubscriptionEnvironment(serviceEnvironment: .production, + purchasePlatform: .appStore), + canPurchase: true) + } else { + // This is used just for iOS <15, it's a sort of mocked environment that will not be used. + subscriptionManager = SubscriptionManageriOS14(accountManager: accountManager) + } + + let accessTokenProvider: () -> String? = { { "sometoken" } }() + let featureFlagger = DefaultFeatureFlagger(internalUserDecider: internalUserDecider, + privacyConfigManager: ContentBlocking.shared.privacyConfigurationManager) + networkProtectionKeychainTokenStore = NetworkProtectionKeychainTokenStore(keychainType: .dataProtection(.unspecified), + serviceName: "\(Bundle.main.bundleIdentifier!).authToken", + errorEvents: .networkProtectionAppDebugEvents, + isSubscriptionEnabled: accountManager.isUserAuthenticated, + accessTokenProvider: accessTokenProvider) + networkProtectionTunnelController = NetworkProtectionTunnelController(accountManager: accountManager, + tokenStore: networkProtectionKeychainTokenStore) + networkProtectionAccessController = NetworkProtectionAccessController(featureFlagger: featureFlagger, + internalUserDecider: internalUserDecider, + tokenStore: networkProtectionKeychainTokenStore, + networkProtectionTunnelController: networkProtectionTunnelController) + vpnFeatureVisibility = DefaultNetworkProtectionVisibility(userDefaults: .networkProtectionGroupDefaults, + accountManager: accountManager) + + connectionObserver = ConnectionStatusObserverThroughSession() + vpnSettings = VPNSettings(defaults: .networkProtectionGroupDefaults) } } diff --git a/DuckDuckGoTests/MockSecureVault.swift b/DuckDuckGoTests/MockSecureVault.swift index 6a196771b0..323594968e 100644 --- a/DuckDuckGoTests/MockSecureVault.swift +++ b/DuckDuckGoTests/MockSecureVault.swift @@ -180,6 +180,10 @@ final class MockSecureVault: AutofillSecureVault { return storedCards } + func creditCardsCount() throws -> Int { + return storedCards.count + } + func creditCardFor(id: Int64) throws -> SecureVaultModels.CreditCard? { return storedCards.first { $0.id == id } } @@ -384,6 +388,10 @@ class MockDatabaseProvider: AutofillDatabaseProvider { return Array(_creditCards.values) } + func creditCardsCount() throws -> Int { + return _creditCards.count + } + func creditCardForCardId(_ cardId: Int64) throws -> SecureVaultModels.CreditCard? { return _creditCards[cardId] } diff --git a/DuckDuckGoTests/MockUserAgent.swift b/DuckDuckGoTests/MockUserAgent.swift index ce8147a4c8..1e0ab0c086 100644 --- a/DuckDuckGoTests/MockUserAgent.swift +++ b/DuckDuckGoTests/MockUserAgent.swift @@ -48,6 +48,10 @@ class MockUserAgentManager: UserAgentManager { return userAgent.agent(forUrl: nil, isDesktop: isDesktop, privacyConfig: privacyConfig) } + func userAgent(isDesktop: Bool, url: URL?) -> String { + return userAgent.agent(forUrl: url, isDesktop: isDesktop) + } + public func update(request: inout URLRequest, isDesktop: Bool) { request.addValue(userAgent.agent(forUrl: nil, isDesktop: isDesktop, privacyConfig: privacyConfig), forHTTPHeaderField: "User-Agent") } diff --git a/DuckDuckGoTests/NetworkProtectionAccessControllerTests.swift b/DuckDuckGoTests/NetworkProtectionAccessControllerTests.swift index 2f4f358689..6940893609 100644 --- a/DuckDuckGoTests/NetworkProtectionAccessControllerTests.swift +++ b/DuckDuckGoTests/NetworkProtectionAccessControllerTests.swift @@ -17,6 +17,9 @@ // limitations under the License. // +// Re-implement this in a way that makes sense + +/* #if NETWORK_PROTECTION import XCTest @@ -25,8 +28,13 @@ import NetworkProtection import NetworkExtension import NetworkProtectionTestUtils import WaitlistMocks +import SubscriptionTestingUtilities @testable import DuckDuckGo +final class MockNetworkProtectionFeatureActivation: NetworkProtectionFeatureActivation { + var isFeatureActivated: Bool = false +} + final class NetworkProtectionAccessControllerTests: XCTestCase { var internalUserDeciderStore: MockInternalUserStoring! @@ -69,54 +77,6 @@ final class NetworkProtectionAccessControllerTests: XCTestCase { XCTAssertEqual(controller.networkProtectionAccessType(), .inviteCodeInvited) } - func testWhenFeatureFlagsAreEnabled_AndTheUserHasNotSignedUp_ThenNetworkProtectionIsAccessible() { - let controller = createMockAccessController( - featureActivated: false, - termsAccepted: false, - featureFlagsEnabled: true, - hasJoinedWaitlist: false, - hasBeenInvited: false - ) - - XCTAssertEqual(controller.networkProtectionAccessType(), .waitlistAvailable) - } - - func testWhenFeatureFlagsAreEnabled_AndTheUserHasSignedUp_ThenNetworkProtectionIsAccessible() { - let controller = createMockAccessController( - featureActivated: false, - termsAccepted: false, - featureFlagsEnabled: true, - hasJoinedWaitlist: true, - hasBeenInvited: false - ) - - XCTAssertEqual(controller.networkProtectionAccessType(), .waitlistJoined) - } - - func testWhenFeatureFlagsAreEnabled_AndTheUserHasBeenInvited_ThenNetworkProtectionIsAccessible() { - let controller = createMockAccessController( - featureActivated: true, - termsAccepted: false, - featureFlagsEnabled: true, - hasJoinedWaitlist: true, - hasBeenInvited: true - ) - - XCTAssertEqual(controller.networkProtectionAccessType(), .waitlistInvitedPendingTermsAcceptance) - } - - func testWhenFeatureFlagsAreEnabled_AndTheUserHasAcceptedTerms_ThenNetworkProtectionIsAccessible() { - let controller = createMockAccessController( - featureActivated: true, - termsAccepted: true, - featureFlagsEnabled: true, - hasJoinedWaitlist: true, - hasBeenInvited: true - ) - - XCTAssertEqual(controller.networkProtectionAccessType(), .waitlistInvited) - } - // MARK: - Mock Creation private func createMockAccessController( @@ -150,7 +110,6 @@ final class NetworkProtectionAccessControllerTests: XCTestCase { return NetworkProtectionAccessController( networkProtectionActivation: mockActivation, - networkProtectionWaitlistStorage: mockWaitlistStorage, networkProtectionTermsAndConditionsStore: mockTermsAndConditionsStore, featureFlagger: mockFeatureFlagger, internalUserDecider: internalUserDecider @@ -183,3 +142,4 @@ private class MockNetworkProtectionTermsAndConditionsStore: NetworkProtectionTer } #endif +*/ diff --git a/DuckDuckGoTests/NetworkProtectionFeatureVisibilityTests.swift b/DuckDuckGoTests/NetworkProtectionFeatureVisibilityTests.swift index 579ed042c3..9423c86b93 100644 --- a/DuckDuckGoTests/NetworkProtectionFeatureVisibilityTests.swift +++ b/DuckDuckGoTests/NetworkProtectionFeatureVisibilityTests.swift @@ -19,99 +19,70 @@ import XCTest @testable import DuckDuckGo +import Subscription +import SubscriptionTestingUtilities +import Common /// Test all permutations according to https://app.asana.com/0/0/1206812323779606/f final class NetworkProtectionFeatureVisibilityTests: XCTestCase { + func testPrivacyProNotYetLaunched() { - // Current waitlist user -> VPN works as usual, no thank-you, no entitlement check - let mockWithVPNAccess = NetworkProtectionFeatureVisibilityMocks(with: [.isWaitlistBetaActive, .isWaitlistUser]) - XCTAssertFalse(mockWithVPNAccess.shouldMonitorEntitlement()) - XCTAssertTrue(mockWithVPNAccess.shouldKeepVPNAccessViaWaitlist()) - XCTAssertFalse(mockWithVPNAccess.shouldShowThankYouMessaging()) - XCTAssertTrue(mockWithVPNAccess.shouldShowVPNShortcut()) - - // Not current waitlist user -> Show nothing, use nothing - let mockWithBetaActive = NetworkProtectionFeatureVisibilityMocks(with: [.isWaitlistBetaActive]) - XCTAssertFalse(mockWithBetaActive.shouldMonitorEntitlement()) - XCTAssertFalse(mockWithBetaActive.shouldKeepVPNAccessViaWaitlist()) - XCTAssertFalse(mockWithBetaActive.shouldShowThankYouMessaging()) - XCTAssertFalse(mockWithBetaActive.shouldShowVPNShortcut()) - - // Waitlist beta OFF, current waitlist user -> Show nothing, use nothing - let mockWithBetaInactive = NetworkProtectionFeatureVisibilityMocks(with: [.isWaitlistUser]) - XCTAssertFalse(mockWithBetaInactive.shouldMonitorEntitlement()) - XCTAssertFalse(mockWithBetaInactive.shouldKeepVPNAccessViaWaitlist()) - XCTAssertFalse(mockWithBetaInactive.shouldShowThankYouMessaging()) - XCTAssertFalse(mockWithBetaInactive.shouldShowVPNShortcut()) - // Waitlist beta OFF, not current waitlist user -> Show nothing, use nothing let mockWithNothing = NetworkProtectionFeatureVisibilityMocks(with: []) XCTAssertFalse(mockWithNothing.shouldMonitorEntitlement()) - XCTAssertFalse(mockWithNothing.shouldKeepVPNAccessViaWaitlist()) - XCTAssertFalse(mockWithNothing.shouldShowThankYouMessaging()) XCTAssertFalse(mockWithNothing.shouldShowVPNShortcut()) } func testPrivacyProLaunched() { - let baseMock = NetworkProtectionFeatureVisibilityMocks(with: [.isPrivacyProLaunched]) - - // Current waitlist user -> Show thank-you, enforce entitlement check, no more VPN use - let mockWithVPNAccess = baseMock.adding([.isWaitlistUser, .isWaitlistBetaActive]) - XCTAssertTrue(mockWithVPNAccess.shouldMonitorEntitlement()) - XCTAssertFalse(mockWithVPNAccess.shouldKeepVPNAccessViaWaitlist()) - XCTAssertTrue(mockWithVPNAccess.shouldShowThankYouMessaging()) - XCTAssertFalse(mockWithVPNAccess.shouldShowVPNShortcut()) - - // Not current waitlist user -> Enforce entitlement check, no more VPN use, no thank-you - let mockWithBetaActive = baseMock.adding([.isWaitlistBetaActive]) - XCTAssertTrue(mockWithBetaActive.shouldMonitorEntitlement()) - XCTAssertFalse(mockWithBetaActive.shouldKeepVPNAccessViaWaitlist()) - XCTAssertFalse(mockWithBetaActive.shouldShowThankYouMessaging()) - XCTAssertFalse(mockWithBetaActive.shouldShowVPNShortcut()) - - // Waitlist beta OFF, current waitlist user -> Show thank-you, enforce entitlement check, no more VPN use - let mockWithBetaInactive = baseMock.adding([.isWaitlistUser]) - XCTAssertTrue(mockWithBetaInactive.shouldMonitorEntitlement()) - XCTAssertFalse(mockWithBetaInactive.shouldKeepVPNAccessViaWaitlist()) - XCTAssertTrue(mockWithBetaInactive.shouldShowThankYouMessaging()) - XCTAssertFalse(mockWithBetaInactive.shouldShowVPNShortcut()) - // Waitlist beta OFF, not current waitlist user -> Enforce entitlement check, nothing else - let mockWithNothingElse = baseMock + let mockWithNothingElse = NetworkProtectionFeatureVisibilityMocks(with: [.isPrivacyProLaunched]) XCTAssertTrue(mockWithNothingElse.shouldMonitorEntitlement()) - XCTAssertFalse(mockWithNothingElse.shouldKeepVPNAccessViaWaitlist()) - XCTAssertFalse(mockWithNothingElse.shouldShowThankYouMessaging()) XCTAssertFalse(mockWithNothingElse.shouldShowVPNShortcut()) } } struct NetworkProtectionFeatureVisibilityMocks: NetworkProtectionFeatureVisibility { + + let accountManager: AccountManager + + func shouldShowVPNShortcut() -> Bool { + if isPrivacyProLaunched() { + return accountManager.isUserAuthenticated + } else { + return false + } + } + struct Options: OptionSet { let rawValue: Int - static let isWaitlistBetaActive = Options(rawValue: 1 << 0) - static let isWaitlistUser = Options(rawValue: 1 << 1) - static let isPrivacyProLaunched = Options(rawValue: 1 << 2) + static let isPrivacyProLaunched = Options(rawValue: 1 << 0) } let options: Options init(with options: Options) { self.options = options + + let subscriptionAppGroup = Bundle.main.appGroup(bundle: .subs) + let subscriptionUserDefaults = UserDefaults(suiteName: subscriptionAppGroup)! + let subscriptionEnvironment = SubscriptionManager.getSavedOrDefaultEnvironment(userDefaults: subscriptionUserDefaults) + let entitlementsCache = UserDefaultsCache<[Entitlement]>(userDefaults: subscriptionUserDefaults, + key: UserDefaultsCacheKey.subscriptionEntitlements, + settings: UserDefaultsCacheSettings(defaultExpirationInterval: .minutes(20))) + let accessTokenStorage = SubscriptionTokenKeychainStorage(keychainType: .dataProtection(.named(subscriptionAppGroup))) + let subscriptionService = SubscriptionService(currentServiceEnvironment: subscriptionEnvironment.serviceEnvironment) + let authService = AuthService(currentServiceEnvironment: subscriptionEnvironment.serviceEnvironment) + accountManager = AccountManager(accessTokenStorage: accessTokenStorage, + entitlementsCache: entitlementsCache, + subscriptionService: subscriptionService, + authService: authService) } func adding(_ additionalOptions: Options) -> NetworkProtectionFeatureVisibilityMocks { NetworkProtectionFeatureVisibilityMocks(with: options.union(additionalOptions)) } - func isWaitlistBetaActive() -> Bool { - options.contains(.isWaitlistBetaActive) - } - - func isWaitlistUser() -> Bool { - options.contains(.isWaitlistUser) - } - func isPrivacyProLaunched() -> Bool { options.contains(.isPrivacyProLaunched) } diff --git a/DuckDuckGoTests/NetworkProtectionInviteViewModelTests.swift b/DuckDuckGoTests/NetworkProtectionInviteViewModelTests.swift deleted file mode 100644 index 08c396103b..0000000000 --- a/DuckDuckGoTests/NetworkProtectionInviteViewModelTests.swift +++ /dev/null @@ -1,147 +0,0 @@ -// -// NetworkProtectionInviteViewModelTests.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 XCTest -@testable import DuckDuckGo -import NetworkProtection -import NetworkProtectionTestUtils -import Combine - -final class NetworkProtectionInviteViewModelTests: XCTestCase { - - func test_text_alwaysUppercased() { - let viewModel = viewModel() - viewModel.text = "abcdefg" - XCTAssertEqual(viewModel.text, "ABCDEFG") - } - - func test_text_emptyString_disableSubmit() { - let viewModel = viewModel() - viewModel.text = "" - XCTAssertTrue(viewModel.shouldDisableSubmit) - } - - func test_text_nonEmptyString_enableSubmit() { - let viewModel = viewModel() - for _ in 0..<5 { - viewModel.text.append("D") - XCTAssertFalse(viewModel.shouldDisableSubmit) - } - } - - func test_submit_successfulRedemption_changesCurrentStepToSuccess() async { - let viewModel = viewModel(withInjectedRedemptionCoordinator: .whereRedeemSucceeds()) - await viewModel.submit() - XCTAssertEqual(viewModel.currentStep, .success) - } - - private var cancellable: AnyCancellable? - - func test_submit_disablesTextField() async { - let viewModel = viewModel() - viewModel.text = "INVITE" - - let expectation = XCTestExpectation() - - cancellable = viewModel.$shouldDisableTextField.sink { - if $0 == true { - expectation.fulfill() - } - } - - await viewModel.submit() - await fulfillment(of: [expectation], timeout: 2) - cancellable = nil - } - - func test_submit_failedRedemption_unrecognizedCode_enablesTextField() async { - await onSubmit_failedRedemption_unrecognizedCode { viewModel in - XCTAssertFalse(viewModel.shouldDisableTextField) - } - } - - // Disabled this test but keeping it around to document the behaviour. It is failing inexplicably. - func x_test_submit_failedRedemption_unrecognizedCode_showsAlert_withUnrecognizedCodeMessage() async { - let viewModel = viewModel(withInjectedRedemptionCoordinator: .whereRedeemFails(returning: .invalidInviteCode)) - await viewModel.submit() - XCTAssertTrue(viewModel.shouldShowAlert) - XCTAssertEqual(viewModel.errorText, UserText.inviteDialogUnrecognizedCodeMessage) - } - - func test_submit_failedRedemption_otherErrors_enablesTextField() async { - await onSubmit_failedRedemption_otherErrors { viewModel in - XCTAssertFalse(viewModel.shouldDisableTextField) - } - } - - func test_submit_failedRedemption_otherErrors_showsAlert_withUnknownErrorMessage() async { - await onSubmit_failedRedemption_otherErrors { viewModel in - XCTAssertTrue(viewModel.shouldShowAlert) - XCTAssertEqual(viewModel.errorText, UserText.unknownErrorTryAgainMessage) - } - } - - func test_submit_failedRedemption_doesNOTMoveToSuccess() async { - let viewModel = viewModel(withInjectedRedemptionCoordinator: .whereRedeemFails()) - await viewModel.submit() - XCTAssertEqual(viewModel.currentStep, .codeEntry) - } - - func test_getStarted_callsDidCompleteOnDelegate() async { - var didCallCompletion = false - let viewModel = viewModel(withInjectedRedemptionCoordinator: .stubbed()) { - didCallCompletion.toggle() - } - viewModel.getStarted() - XCTAssertTrue(didCallCompletion) - } - - private func viewModel(withInjectedRedemptionCoordinator coordinator: NetworkProtectionCodeRedemptionCoordinator = .stubbed(), completion: @escaping () -> Void = {}) -> NetworkProtectionInviteViewModel { - NetworkProtectionInviteViewModel(redemptionCoordinator: coordinator, completion: completion) - } - - private func onSubmit_failedRedemption_unrecognizedCode(run block: @escaping (NetworkProtectionInviteViewModel) -> Void) async { - let viewModel = viewModel(withInjectedRedemptionCoordinator: .whereRedeemFails(returning: .invalidInviteCode)) - await viewModel.submit() - block(viewModel) - } - - private func onSubmit_failedRedemption_otherErrors(run block: @escaping (NetworkProtectionInviteViewModel) -> Void) async { - let errors: [NetworkProtectionClientError] = [.failedToEncodeRedeemRequest, .invalidAuthToken, .failedToEncodeRegisterKeyRequest] - for error in errors { - let viewModel = viewModel(withInjectedRedemptionCoordinator: .whereRedeemFails(returning: error)) - await viewModel.submit() - block(viewModel) - } - } -} - -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/NetworkProtectionRootViewModelTests.swift b/DuckDuckGoTests/NetworkProtectionRootViewModelTests.swift deleted file mode 100644 index ee5a3727be..0000000000 --- a/DuckDuckGoTests/NetworkProtectionRootViewModelTests.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// NetworkProtectionRootViewModelTests.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 -import XCTest -@testable import DuckDuckGo -import NetworkProtection - -final class NetworkProtectionRootViewModelTests: XCTestCase { - - func test_initialViewKind_featureVisibilityFalse_isInvite() { - let featureActivation = MockNetworkProtectionFeatureActivation() - featureActivation.isFeatureActivated = false - let viewModel = NetworkProtectionRootViewModel(featureActivation: featureActivation) - XCTAssertEqual(viewModel.initialViewKind, .invite) - } - - func test_initialViewKind_featureVisibilityTrue_isStatus() { - let featureActivation = MockNetworkProtectionFeatureActivation() - featureActivation.isFeatureActivated = true - let viewModel = NetworkProtectionRootViewModel(featureActivation: featureActivation) - XCTAssertEqual(viewModel.initialViewKind, .status) - } -} - -final class MockNetworkProtectionFeatureActivation: NetworkProtectionFeatureActivation { - var isFeatureActivated: Bool = false -} diff --git a/DuckDuckGoTests/NetworkProtectionStatusViewModelTests.swift b/DuckDuckGoTests/NetworkProtectionStatusViewModelTests.swift index 38ae56b519..c182b6cae2 100644 --- a/DuckDuckGoTests/NetworkProtectionStatusViewModelTests.swift +++ b/DuckDuckGoTests/NetworkProtectionStatusViewModelTests.swift @@ -21,6 +21,7 @@ import XCTest import NetworkProtection import NetworkExtension import NetworkProtectionTestUtils +import SubscriptionTestingUtilities @testable import DuckDuckGo final class NetworkProtectionStatusViewModelTests: XCTestCase { @@ -39,11 +40,11 @@ final class NetworkProtectionStatusViewModelTests: XCTestCase { tunnelController = MockTunnelController() statusObserver = MockConnectionStatusObserver() serverInfoObserver = MockConnectionServerInfoObserver() - viewModel = NetworkProtectionStatusViewModel( - tunnelController: tunnelController, - statusObserver: statusObserver, - serverInfoObserver: serverInfoObserver - ) + viewModel = NetworkProtectionStatusViewModel(tunnelController: tunnelController, + settings: VPNSettings(defaults: .networkProtectionGroupDefaults), + statusObserver: statusObserver, + serverInfoObserver: serverInfoObserver, + locationListRepository: MockNetworkProtectionLocationListRepository()) } override func tearDown() { @@ -54,12 +55,6 @@ final class NetworkProtectionStatusViewModelTests: XCTestCase { super.tearDown() } - func testInit_prefetchesLocationList() throws { - let locationListRepo = MockNetworkProtectionLocationListRepository() - viewModel = NetworkProtectionStatusViewModel(locationListRepository: locationListRepo) - waitFor(condition: locationListRepo.didCallFetchLocationList) - } - func testStatusUpdate_connected_setsIsNetPEnabledToTrue() throws { whenStatusUpdate_connected() } diff --git a/DuckDuckGoTests/RemoteMessagingStoreTests.swift b/DuckDuckGoTests/RemoteMessagingStoreTests.swift index 02e052c72b..191ae48b11 100644 --- a/DuckDuckGoTests/RemoteMessagingStoreTests.swift +++ b/DuckDuckGoTests/RemoteMessagingStoreTests.swift @@ -141,9 +141,11 @@ class RemoteMessagingStoreTests: XCTestCase { favoritesCount: 0, appTheme: "light", isWidgetInstalled: false, - isNetPWaitlistUser: false, - daysSinceNetPEnabled: -1), + daysSinceNetPEnabled: -1, + isPrivacyProEligibleUser: false, + isPrivacyProSubscriber: false), percentileStore: RemoteMessagingPercentileUserDefaultsStore(userDefaults: self.defaults), + surveyActionMapper: MockRemoteMessagingSurveyActionMapper(), dismissedMessageIds: [] ) @@ -160,3 +162,11 @@ class RemoteMessagingStoreTests: XCTestCase { } } } + +private final class MockRemoteMessagingSurveyActionMapper: RemoteMessagingSurveyActionMapping { + + func add(parameters: [RemoteMessagingSurveyActionParameter], to url: URL) -> URL { + return url + } + +} diff --git a/DuckDuckGoTests/SubscriptionContainerViewModelTests.swift b/DuckDuckGoTests/SubscriptionContainerViewModelTests.swift index 00039d336b..74fab67672 100644 --- a/DuckDuckGoTests/SubscriptionContainerViewModelTests.swift +++ b/DuckDuckGoTests/SubscriptionContainerViewModelTests.swift @@ -19,19 +19,26 @@ import XCTest @testable import DuckDuckGo +@testable import Subscription +import SubscriptionTestingUtilities @available(iOS 15.0, *) final class SubscriptionContainerViewModelTests: XCTestCase { - private var sut: SubscriptionContainerViewModel! + var sut: SubscriptionContainerViewModel! + let mockDependencyProvider = MockDependencyProvider() func testWhenInitWithOriginThenSubscriptionFlowPurchaseURLHasOriginSet() { // GIVEN let origin = "test_origin" let queryParameter = URLQueryItem(name: "origin", value: "test_origin") - let expectedURL = URL.subscriptionPurchase.appending(percentEncodedQueryItem: queryParameter) + let expectedURL = SubscriptionURL.purchase.subscriptionURL(environment: .production).appending(percentEncodedQueryItem: queryParameter) // WHEN - sut = .init(origin: origin, userScript: .init(), subFeature: .init(subscriptionAttributionOrigin: nil)) + sut = .init(subscriptionManager: mockDependencyProvider.subscriptionManager, + origin: origin, + userScript: .init(), + subFeature: .init(subscriptionManager: mockDependencyProvider.subscriptionManager, + subscriptionAttributionOrigin: nil)) // THEN XCTAssertEqual(sut.flow.purchaseURL, expectedURL) @@ -39,10 +46,14 @@ final class SubscriptionContainerViewModelTests: XCTestCase { func testWhenInitWithoutOriginThenSubscriptionFlowPurchaseURLDoesNotHaveOriginSet() { // WHEN - sut = .init(origin: nil, userScript: .init(), subFeature: .init(subscriptionAttributionOrigin: nil)) + sut = .init(subscriptionManager: mockDependencyProvider.subscriptionManager, + origin: nil, + userScript: .init(), + subFeature: .init(subscriptionManager: mockDependencyProvider.subscriptionManager, + subscriptionAttributionOrigin: nil)) // THEN - XCTAssertEqual(sut.flow.purchaseURL, URL.subscriptionPurchase) + XCTAssertEqual(sut.flow.purchaseURL, SubscriptionURL.purchase.subscriptionURL(environment: .production)) } } diff --git a/DuckDuckGoTests/SubscriptionFlowViewModelTests.swift b/DuckDuckGoTests/SubscriptionFlowViewModelTests.swift index 3552d74b95..eaf6cbdaa3 100644 --- a/DuckDuckGoTests/SubscriptionFlowViewModelTests.swift +++ b/DuckDuckGoTests/SubscriptionFlowViewModelTests.swift @@ -19,19 +19,25 @@ import XCTest @testable import DuckDuckGo +@testable import Subscription +import SubscriptionTestingUtilities @available(iOS 15.0, *) final class SubscriptionFlowViewModelTests: XCTestCase { private var sut: SubscriptionFlowViewModel! + let mockDependencyProvider = MockDependencyProvider() + func testWhenInitWithOriginThenSubscriptionFlowPurchaseURLHasOriginSet() { // GIVEN let origin = "test_origin" let queryParameter = URLQueryItem(name: "origin", value: "test_origin") - let expectedURL = URL.subscriptionPurchase.appending(percentEncodedQueryItem: queryParameter) + let expectedURL = SubscriptionURL.purchase.subscriptionURL(environment: .production).appending(percentEncodedQueryItem: queryParameter) // WHEN - sut = .init(origin: origin, userScript: .init(), subFeature: .init(subscriptionAttributionOrigin: nil)) + sut = .init(origin: origin, userScript: .init(), subFeature: .init(subscriptionManager: mockDependencyProvider.subscriptionManager, + subscriptionAttributionOrigin: nil), + subscriptionManager: mockDependencyProvider.subscriptionManager) // THEN XCTAssertEqual(sut.purchaseURL, expectedURL) @@ -39,10 +45,12 @@ final class SubscriptionFlowViewModelTests: XCTestCase { func testWhenInitWithoutOriginThenSubscriptionFlowPurchaseURLDoesNotHaveOriginSet() { // WHEN - sut = .init(origin: nil, userScript: .init(), subFeature: .init(subscriptionAttributionOrigin: nil)) + sut = .init(origin: nil, userScript: .init(), subFeature: .init(subscriptionManager: mockDependencyProvider.subscriptionManager, + subscriptionAttributionOrigin: nil), + subscriptionManager: mockDependencyProvider.subscriptionManager) // THEN - XCTAssertEqual(sut.purchaseURL, URL.subscriptionPurchase) + XCTAssertEqual(sut.purchaseURL, SubscriptionURL.purchase.subscriptionURL(environment: .production)) } } diff --git a/DuckDuckGoTests/TabURLInterceptorTests.swift b/DuckDuckGoTests/TabURLInterceptorTests.swift index 6bf3b6eee5..9826a6e82e 100644 --- a/DuckDuckGoTests/TabURLInterceptorTests.swift +++ b/DuckDuckGoTests/TabURLInterceptorTests.swift @@ -19,6 +19,7 @@ import XCTest import Subscription +import SubscriptionTestingUtilities @testable import DuckDuckGo class TabURLInterceptorDefaultTests: XCTestCase { @@ -27,9 +28,9 @@ class TabURLInterceptorDefaultTests: XCTestCase { override func setUp() { super.setUp() - // Simulate purchase allowance - SubscriptionPurchaseEnvironment.canPurchase = true - urlInterceptor = TabURLInterceptorDefault() + urlInterceptor = TabURLInterceptorDefault(canPurchase: { + true + }) } override func tearDown() { diff --git a/Gemfile.lock b/Gemfile.lock index 202ba36252..ac5a095e11 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -171,7 +171,8 @@ GEM trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.2.6) + rexml (3.2.8) + strscan (>= 3.0.9) rouge (2.0.7) ruby2_keywords (0.0.5) rubyzip (2.3.2) @@ -184,6 +185,7 @@ GEM simctl (1.6.10) CFPropertyList naturally + strscan (3.1.0) terminal-notifier (2.0.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) diff --git a/LocalPackages/DuckUI/Sources/DuckUI/Color.swift b/LocalPackages/DuckUI/Sources/DuckUI/Color.swift index c735ac88e7..4b7c9fee5a 100644 --- a/LocalPackages/DuckUI/Sources/DuckUI/Color.swift +++ b/LocalPackages/DuckUI/Sources/DuckUI/Color.swift @@ -120,4 +120,24 @@ public extension Color { } } +public extension Color { + static func shade(_ percent: Double) -> Color { + Self.black.opacity(percent) + } + + static func tint(_ percent: Double) -> Color { + Self.white.opacity(percent) + } +} + +public extension UIColor { + static func shade(_ percent: Double) -> UIColor { + UIColor(.shade(percent)) + } + + static func tint(_ percent: Double) -> UIColor { + UIColor(.tint(percent)) + } +} + #endif diff --git a/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift b/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift index 1411af7994..ed9dcea87e 100644 --- a/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift +++ b/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift @@ -35,6 +35,7 @@ import WidgetKit final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { private var cancellables = Set() + private let accountManager: AccountManaging // MARK: - PacketTunnelProvider.Event reporting @@ -253,25 +254,46 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { super.stopTunnel(with: reason, completionHandler: completionHandler) } + // swiftlint:disable:next function_body_length @objc init() { - let featureVisibility = NetworkProtectionVisibilityForTunnelProvider() - let isSubscriptionEnabled = featureVisibility.isPrivacyProLaunched() - let accessTokenProvider: () -> String? = { - if featureVisibility.shouldMonitorEntitlement() { - return { AccountManager().accessToken } + + let settings = VPNSettings(defaults: .networkProtectionGroupDefaults) + + // Align Subscription environment to the VPN environment + var subscriptionEnvironment = SubscriptionEnvironment.default + switch settings.selectedEnvironment { + case .production: + subscriptionEnvironment.serviceEnvironment = .production + case .staging: + subscriptionEnvironment.serviceEnvironment = .staging } - return { nil } - }() - let tokenStore = NetworkProtectionKeychainTokenStore( - keychainType: .dataProtection(.unspecified), - errorEvents: nil, - isSubscriptionEnabled: isSubscriptionEnabled, - accessTokenProvider: accessTokenProvider - ) + + // MARK: - Configure Subscription + let entitlementsCache = UserDefaultsCache<[Entitlement]>(userDefaults: UserDefaults.standard, + key: UserDefaultsCacheKey.subscriptionEntitlements, + settings: UserDefaultsCacheSettings(defaultExpirationInterval: .minutes(20))) + let accessTokenStorage = SubscriptionTokenKeychainStorage(keychainType: .dataProtection(.unspecified)) + let subscriptionService = SubscriptionService(currentServiceEnvironment: subscriptionEnvironment.serviceEnvironment) + let authService = AuthService(currentServiceEnvironment: subscriptionEnvironment.serviceEnvironment) + let accountManager = AccountManager(accessTokenStorage: accessTokenStorage, + entitlementsCache: entitlementsCache, + subscriptionService: subscriptionService, + authService: authService) + self.accountManager = accountManager + let featureVisibility = NetworkProtectionVisibilityForTunnelProvider(accountManager: accountManager) + let accessTokenProvider: () -> String? = { + if featureVisibility.shouldMonitorEntitlement() { + return { accountManager.accessToken } + } + return { nil } }() + let tokenStore = NetworkProtectionKeychainTokenStore(keychainType: .dataProtection(.unspecified), + errorEvents: nil, + isSubscriptionEnabled: true, + accessTokenProvider: accessTokenProvider) let errorStore = NetworkProtectionTunnelErrorStore() let notificationsPresenter = NetworkProtectionUNNotificationPresenter() - let settings = VPNSettings(defaults: .networkProtectionGroupDefaults) + let notificationsPresenterDecorator = NetworkProtectionNotificationsPresenterTogglableDecorator( settings: settings, defaults: .networkProtectionGroupDefaults, @@ -293,8 +315,8 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { providerEvents: Self.packetTunnelProviderEvents, settings: settings, defaults: .networkProtectionGroupDefaults, - isSubscriptionEnabled: isSubscriptionEnabled, - entitlementCheck: Self.entitlementCheck) + isSubscriptionEnabled: true, + entitlementCheck: { return await Self.entitlementCheck(accountManager: accountManager) }) startMonitoringMemoryPressureEvents() observeServerChanges() APIRequest.Headers.setUserAgent(DefaultUserAgentManager.duckDuckGoUserAgent) @@ -332,7 +354,7 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { .store(in: &cancellables) } - private let activationDateStore = DefaultVPNWaitlistActivationDateStore() + private let activationDateStore = DefaultVPNActivationDateStore() public override func handleConnectionStatusChange(old: ConnectionStatus, new: ConnectionStatus) { super.handleConnectionStatusChange(old: old, new: new) @@ -342,17 +364,13 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { WidgetCenter.shared.reloadTimelines(ofKind: "VPNStatusWidget") } - private static func entitlementCheck() async -> Result { - guard NetworkProtectionVisibilityForTunnelProvider().shouldMonitorEntitlement() else { + private static func entitlementCheck(accountManager: AccountManaging) async -> Result { + + guard NetworkProtectionVisibilityForTunnelProvider(accountManager: accountManager).shouldMonitorEntitlement() else { return .success(true) } - if VPNSettings(defaults: .networkProtectionGroupDefaults).selectedEnvironment == .staging { - SubscriptionPurchaseEnvironment.currentServiceEnvironment = .staging - } - - let result = await AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) - .hasEntitlement(for: .networkProtection) + let result = await accountManager.hasEntitlement(for: .networkProtection) switch result { case .success(let hasEntitlement): return .success(hasEntitlement) diff --git a/Widgets/bg.lproj/Localizable.strings b/Widgets/bg.lproj/Localizable.strings index 5482eda3fa..db0f40e562 100644 --- a/Widgets/bg.lproj/Localizable.strings +++ b/Widgets/bg.lproj/Localizable.strings @@ -1,12 +1,3 @@ -/* No comment provided by engineer. */ -"Connect" = "Connect"; - -/* No comment provided by engineer. */ -"Disconnect" = "Disconnect"; - -/* No comment provided by engineer. */ -"Error" = "Грешка"; - /* No comment provided by engineer. */ "iOS 17 required" = "iOS 17 required"; @@ -49,9 +40,6 @@ /* Title shown to the user when adding the Voice Search lock screen widget */ "lock.screen.widget.voice.title" = "Гласово търсене"; -/* No comment provided by engineer. */ -"VPN Not Configured" = "VPN Not Configured"; - /* Description of search passwords widget in widget gallery */ "widget.gallery.passwords.description" = "Бързо търсене на запазените пароли за DuckDuckGo."; @@ -70,6 +58,12 @@ /* Display name for search only widget in widget gallery */ "widget.gallery.search.display.name" = "Търсене"; +/* Description of VPN widget in widget gallery */ +"widget.gallery.vpn.description" = "View and manage your VPN connection. Requires a Privacy Pro subscription."; + +/* Display name for VPN widget in widget gallery */ +"widget.gallery.vpn.display.name" = "VPN"; + /* CTA shown in the favorites widget empty state. */ "widget.no.favorites.cta" = "Добавяне към любими"; @@ -82,3 +76,18 @@ /* Placeholder text in search field on the search and favorites widget */ "widget.search.duckduckgo" = "Търси с DuckDuckGo"; +/* VPN connect button text */ +"widget.vpn.button.connect" = "Connect"; + +/* VPN disconnect button text */ +"widget.vpn.button.disconnect" = "Disconnect"; + +/* Message describing VPN connected status */ +"widget.vpn.status.connected" = "VPN is On"; + +/* Message describing VPN disconnected status */ +"widget.vpn.status.disconnected" = "VPN is Off"; + +/* Subtitle describing VPN disconnected status */ +"widget.vpn.subtitle.disconnected" = "Not connected"; + diff --git a/Widgets/cs.lproj/Localizable.strings b/Widgets/cs.lproj/Localizable.strings index cc31a7a6ff..128520e2fc 100644 --- a/Widgets/cs.lproj/Localizable.strings +++ b/Widgets/cs.lproj/Localizable.strings @@ -1,12 +1,3 @@ -/* No comment provided by engineer. */ -"Connect" = "Connect"; - -/* No comment provided by engineer. */ -"Disconnect" = "Disconnect"; - -/* No comment provided by engineer. */ -"Error" = "Chyba"; - /* No comment provided by engineer. */ "iOS 17 required" = "iOS 17 required"; @@ -49,9 +40,6 @@ /* Title shown to the user when adding the Voice Search lock screen widget */ "lock.screen.widget.voice.title" = "Hlasové vyhledávání"; -/* No comment provided by engineer. */ -"VPN Not Configured" = "VPN Not Configured"; - /* Description of search passwords widget in widget gallery */ "widget.gallery.passwords.description" = "Rychle hledej svá hesla uložená v DuckDuckGo."; @@ -70,6 +58,12 @@ /* Display name for search only widget in widget gallery */ "widget.gallery.search.display.name" = "Hledat"; +/* Description of VPN widget in widget gallery */ +"widget.gallery.vpn.description" = "View and manage your VPN connection. Requires a Privacy Pro subscription."; + +/* Display name for VPN widget in widget gallery */ +"widget.gallery.vpn.display.name" = "VPN"; + /* CTA shown in the favorites widget empty state. */ "widget.no.favorites.cta" = "Přidat oblíbené"; @@ -82,3 +76,18 @@ /* Placeholder text in search field on the search and favorites widget */ "widget.search.duckduckgo" = "Vyhledat prostřednictvím DuckDuckGo"; +/* VPN connect button text */ +"widget.vpn.button.connect" = "Connect"; + +/* VPN disconnect button text */ +"widget.vpn.button.disconnect" = "Disconnect"; + +/* Message describing VPN connected status */ +"widget.vpn.status.connected" = "VPN is On"; + +/* Message describing VPN disconnected status */ +"widget.vpn.status.disconnected" = "VPN is Off"; + +/* Subtitle describing VPN disconnected status */ +"widget.vpn.subtitle.disconnected" = "Not connected"; + diff --git a/Widgets/da.lproj/Localizable.strings b/Widgets/da.lproj/Localizable.strings index 897007f1ee..95017c162a 100644 --- a/Widgets/da.lproj/Localizable.strings +++ b/Widgets/da.lproj/Localizable.strings @@ -1,12 +1,3 @@ -/* No comment provided by engineer. */ -"Connect" = "Connect"; - -/* No comment provided by engineer. */ -"Disconnect" = "Disconnect"; - -/* No comment provided by engineer. */ -"Error" = "Fejl"; - /* No comment provided by engineer. */ "iOS 17 required" = "iOS 17 required"; @@ -49,9 +40,6 @@ /* Title shown to the user when adding the Voice Search lock screen widget */ "lock.screen.widget.voice.title" = "Stemmesøgning"; -/* No comment provided by engineer. */ -"VPN Not Configured" = "VPN Not Configured"; - /* Description of search passwords widget in widget gallery */ "widget.gallery.passwords.description" = "Søg hurtigt i dine gemte DuckDuckGo-adgangskoder."; @@ -70,6 +58,12 @@ /* Display name for search only widget in widget gallery */ "widget.gallery.search.display.name" = "Søg"; +/* Description of VPN widget in widget gallery */ +"widget.gallery.vpn.description" = "View and manage your VPN connection. Requires a Privacy Pro subscription."; + +/* Display name for VPN widget in widget gallery */ +"widget.gallery.vpn.display.name" = "VPN"; + /* CTA shown in the favorites widget empty state. */ "widget.no.favorites.cta" = "Tilføj til favoritter"; @@ -82,3 +76,18 @@ /* Placeholder text in search field on the search and favorites widget */ "widget.search.duckduckgo" = "Søg med DuckDuckGo"; +/* VPN connect button text */ +"widget.vpn.button.connect" = "Connect"; + +/* VPN disconnect button text */ +"widget.vpn.button.disconnect" = "Disconnect"; + +/* Message describing VPN connected status */ +"widget.vpn.status.connected" = "VPN is On"; + +/* Message describing VPN disconnected status */ +"widget.vpn.status.disconnected" = "VPN is Off"; + +/* Subtitle describing VPN disconnected status */ +"widget.vpn.subtitle.disconnected" = "Not connected"; + diff --git a/Widgets/de.lproj/Localizable.strings b/Widgets/de.lproj/Localizable.strings index eac25b6188..dcd7e5b6dd 100644 --- a/Widgets/de.lproj/Localizable.strings +++ b/Widgets/de.lproj/Localizable.strings @@ -1,12 +1,3 @@ -/* No comment provided by engineer. */ -"Connect" = "Connect"; - -/* No comment provided by engineer. */ -"Disconnect" = "Disconnect"; - -/* No comment provided by engineer. */ -"Error" = "Fehler"; - /* No comment provided by engineer. */ "iOS 17 required" = "iOS 17 required"; @@ -49,9 +40,6 @@ /* Title shown to the user when adding the Voice Search lock screen widget */ "lock.screen.widget.voice.title" = "Sprachsuche"; -/* No comment provided by engineer. */ -"VPN Not Configured" = "VPN Not Configured"; - /* Description of search passwords widget in widget gallery */ "widget.gallery.passwords.description" = "Durchsuche schnell deine gespeicherten DuckDuckGo-Passwörter."; @@ -70,6 +58,12 @@ /* Display name for search only widget in widget gallery */ "widget.gallery.search.display.name" = "Suche"; +/* Description of VPN widget in widget gallery */ +"widget.gallery.vpn.description" = "View and manage your VPN connection. Requires a Privacy Pro subscription."; + +/* Display name for VPN widget in widget gallery */ +"widget.gallery.vpn.display.name" = "VPN"; + /* CTA shown in the favorites widget empty state. */ "widget.no.favorites.cta" = "Favoriten hinzufügen"; @@ -82,3 +76,18 @@ /* Placeholder text in search field on the search and favorites widget */ "widget.search.duckduckgo" = "Mit DuckDuckGo suchen"; +/* VPN connect button text */ +"widget.vpn.button.connect" = "Connect"; + +/* VPN disconnect button text */ +"widget.vpn.button.disconnect" = "Disconnect"; + +/* Message describing VPN connected status */ +"widget.vpn.status.connected" = "VPN is On"; + +/* Message describing VPN disconnected status */ +"widget.vpn.status.disconnected" = "VPN is Off"; + +/* Subtitle describing VPN disconnected status */ +"widget.vpn.subtitle.disconnected" = "Not connected"; + diff --git a/Widgets/el.lproj/Localizable.strings b/Widgets/el.lproj/Localizable.strings index 444b86daf7..7b83237741 100644 --- a/Widgets/el.lproj/Localizable.strings +++ b/Widgets/el.lproj/Localizable.strings @@ -1,12 +1,3 @@ -/* No comment provided by engineer. */ -"Connect" = "Connect"; - -/* No comment provided by engineer. */ -"Disconnect" = "Disconnect"; - -/* No comment provided by engineer. */ -"Error" = "Σφάλμα"; - /* No comment provided by engineer. */ "iOS 17 required" = "iOS 17 required"; @@ -49,9 +40,6 @@ /* Title shown to the user when adding the Voice Search lock screen widget */ "lock.screen.widget.voice.title" = "Φωνητική αναζήτηση"; -/* No comment provided by engineer. */ -"VPN Not Configured" = "VPN Not Configured"; - /* Description of search passwords widget in widget gallery */ "widget.gallery.passwords.description" = "Αναζητήστε γρήγορα τους αποθηκευμένους κωδικούς πρόσβασής σας στο DuckDuckGo."; @@ -70,6 +58,12 @@ /* Display name for search only widget in widget gallery */ "widget.gallery.search.display.name" = "Αναζήτηση"; +/* Description of VPN widget in widget gallery */ +"widget.gallery.vpn.description" = "View and manage your VPN connection. Requires a Privacy Pro subscription."; + +/* Display name for VPN widget in widget gallery */ +"widget.gallery.vpn.display.name" = "VPN"; + /* CTA shown in the favorites widget empty state. */ "widget.no.favorites.cta" = "Προσθήκη στα Αγαπημένα"; @@ -82,3 +76,18 @@ /* Placeholder text in search field on the search and favorites widget */ "widget.search.duckduckgo" = "Αναζήτηση DuckDuckGo"; +/* VPN connect button text */ +"widget.vpn.button.connect" = "Connect"; + +/* VPN disconnect button text */ +"widget.vpn.button.disconnect" = "Disconnect"; + +/* Message describing VPN connected status */ +"widget.vpn.status.connected" = "VPN is On"; + +/* Message describing VPN disconnected status */ +"widget.vpn.status.disconnected" = "VPN is Off"; + +/* Subtitle describing VPN disconnected status */ +"widget.vpn.subtitle.disconnected" = "Not connected"; + diff --git a/Widgets/es.lproj/Localizable.strings b/Widgets/es.lproj/Localizable.strings index f14ce6328d..7aea0909c5 100644 --- a/Widgets/es.lproj/Localizable.strings +++ b/Widgets/es.lproj/Localizable.strings @@ -1,12 +1,3 @@ -/* No comment provided by engineer. */ -"Connect" = "Connect"; - -/* No comment provided by engineer. */ -"Disconnect" = "Disconnect"; - -/* No comment provided by engineer. */ -"Error" = "Error"; - /* No comment provided by engineer. */ "iOS 17 required" = "iOS 17 required"; @@ -49,9 +40,6 @@ /* Title shown to the user when adding the Voice Search lock screen widget */ "lock.screen.widget.voice.title" = "Búsqueda por voz"; -/* No comment provided by engineer. */ -"VPN Not Configured" = "VPN Not Configured"; - /* Description of search passwords widget in widget gallery */ "widget.gallery.passwords.description" = "Busca rápidamente tus contraseñas de DuckDuckGo guardadas."; @@ -70,6 +58,12 @@ /* Display name for search only widget in widget gallery */ "widget.gallery.search.display.name" = "Buscar"; +/* Description of VPN widget in widget gallery */ +"widget.gallery.vpn.description" = "View and manage your VPN connection. Requires a Privacy Pro subscription."; + +/* Display name for VPN widget in widget gallery */ +"widget.gallery.vpn.display.name" = "VPN"; + /* CTA shown in the favorites widget empty state. */ "widget.no.favorites.cta" = "Añadir favoritos"; @@ -82,3 +76,18 @@ /* Placeholder text in search field on the search and favorites widget */ "widget.search.duckduckgo" = "Buscar en DuckDuckGo"; +/* VPN connect button text */ +"widget.vpn.button.connect" = "Connect"; + +/* VPN disconnect button text */ +"widget.vpn.button.disconnect" = "Disconnect"; + +/* Message describing VPN connected status */ +"widget.vpn.status.connected" = "VPN is On"; + +/* Message describing VPN disconnected status */ +"widget.vpn.status.disconnected" = "VPN is Off"; + +/* Subtitle describing VPN disconnected status */ +"widget.vpn.subtitle.disconnected" = "Not connected"; + diff --git a/Widgets/et.lproj/Localizable.strings b/Widgets/et.lproj/Localizable.strings index adac40a019..5e8943a3fc 100644 --- a/Widgets/et.lproj/Localizable.strings +++ b/Widgets/et.lproj/Localizable.strings @@ -1,12 +1,3 @@ -/* No comment provided by engineer. */ -"Connect" = "Connect"; - -/* No comment provided by engineer. */ -"Disconnect" = "Disconnect"; - -/* No comment provided by engineer. */ -"Error" = "Viga"; - /* No comment provided by engineer. */ "iOS 17 required" = "iOS 17 required"; @@ -49,9 +40,6 @@ /* Title shown to the user when adding the Voice Search lock screen widget */ "lock.screen.widget.voice.title" = "Häälotsing"; -/* No comment provided by engineer. */ -"VPN Not Configured" = "VPN Not Configured"; - /* Description of search passwords widget in widget gallery */ "widget.gallery.passwords.description" = "Otsi kiiresti oma salvestatud DuckDuckGo paroole."; @@ -70,6 +58,12 @@ /* Display name for search only widget in widget gallery */ "widget.gallery.search.display.name" = "Otsi"; +/* Description of VPN widget in widget gallery */ +"widget.gallery.vpn.description" = "View and manage your VPN connection. Requires a Privacy Pro subscription."; + +/* Display name for VPN widget in widget gallery */ +"widget.gallery.vpn.display.name" = "VPN"; + /* CTA shown in the favorites widget empty state. */ "widget.no.favorites.cta" = "Lisa lemmikud"; @@ -82,3 +76,18 @@ /* Placeholder text in search field on the search and favorites widget */ "widget.search.duckduckgo" = "Otsi DuckDuckGo'st"; +/* VPN connect button text */ +"widget.vpn.button.connect" = "Connect"; + +/* VPN disconnect button text */ +"widget.vpn.button.disconnect" = "Disconnect"; + +/* Message describing VPN connected status */ +"widget.vpn.status.connected" = "VPN is On"; + +/* Message describing VPN disconnected status */ +"widget.vpn.status.disconnected" = "VPN is Off"; + +/* Subtitle describing VPN disconnected status */ +"widget.vpn.subtitle.disconnected" = "Not connected"; + diff --git a/Widgets/fi.lproj/Localizable.strings b/Widgets/fi.lproj/Localizable.strings index c3c5cc91e7..4f6c5d8f69 100644 --- a/Widgets/fi.lproj/Localizable.strings +++ b/Widgets/fi.lproj/Localizable.strings @@ -1,12 +1,3 @@ -/* No comment provided by engineer. */ -"Connect" = "Connect"; - -/* No comment provided by engineer. */ -"Disconnect" = "Disconnect"; - -/* No comment provided by engineer. */ -"Error" = "Virhe"; - /* No comment provided by engineer. */ "iOS 17 required" = "iOS 17 required"; @@ -49,9 +40,6 @@ /* Title shown to the user when adding the Voice Search lock screen widget */ "lock.screen.widget.voice.title" = "Äänihaku"; -/* No comment provided by engineer. */ -"VPN Not Configured" = "VPN Not Configured"; - /* Description of search passwords widget in widget gallery */ "widget.gallery.passwords.description" = "Hae nopeasti DuckDuckGohon tallennettuja salasanojasi."; @@ -70,6 +58,12 @@ /* Display name for search only widget in widget gallery */ "widget.gallery.search.display.name" = "Etsi"; +/* Description of VPN widget in widget gallery */ +"widget.gallery.vpn.description" = "View and manage your VPN connection. Requires a Privacy Pro subscription."; + +/* Display name for VPN widget in widget gallery */ +"widget.gallery.vpn.display.name" = "VPN"; + /* CTA shown in the favorites widget empty state. */ "widget.no.favorites.cta" = "Lisää suosikkeja"; @@ -82,3 +76,18 @@ /* Placeholder text in search field on the search and favorites widget */ "widget.search.duckduckgo" = "Hae DuckDuckGo:sta"; +/* VPN connect button text */ +"widget.vpn.button.connect" = "Connect"; + +/* VPN disconnect button text */ +"widget.vpn.button.disconnect" = "Disconnect"; + +/* Message describing VPN connected status */ +"widget.vpn.status.connected" = "VPN is On"; + +/* Message describing VPN disconnected status */ +"widget.vpn.status.disconnected" = "VPN is Off"; + +/* Subtitle describing VPN disconnected status */ +"widget.vpn.subtitle.disconnected" = "Not connected"; + diff --git a/Widgets/fr.lproj/Localizable.strings b/Widgets/fr.lproj/Localizable.strings index 4be99b26a5..77c760d1ff 100644 --- a/Widgets/fr.lproj/Localizable.strings +++ b/Widgets/fr.lproj/Localizable.strings @@ -1,12 +1,3 @@ -/* No comment provided by engineer. */ -"Connect" = "Connect"; - -/* No comment provided by engineer. */ -"Disconnect" = "Disconnect"; - -/* No comment provided by engineer. */ -"Error" = "Erreur"; - /* No comment provided by engineer. */ "iOS 17 required" = "iOS 17 required"; @@ -49,9 +40,6 @@ /* Title shown to the user when adding the Voice Search lock screen widget */ "lock.screen.widget.voice.title" = "Recherche vocale"; -/* No comment provided by engineer. */ -"VPN Not Configured" = "VPN Not Configured"; - /* Description of search passwords widget in widget gallery */ "widget.gallery.passwords.description" = "Recherchez rapidement vos mots de passe DuckDuckGo enregistrés."; @@ -70,6 +58,12 @@ /* Display name for search only widget in widget gallery */ "widget.gallery.search.display.name" = "Rechercher"; +/* Description of VPN widget in widget gallery */ +"widget.gallery.vpn.description" = "View and manage your VPN connection. Requires a Privacy Pro subscription."; + +/* Display name for VPN widget in widget gallery */ +"widget.gallery.vpn.display.name" = "VPN"; + /* CTA shown in the favorites widget empty state. */ "widget.no.favorites.cta" = "Ajouter des favoris"; @@ -82,3 +76,18 @@ /* Placeholder text in search field on the search and favorites widget */ "widget.search.duckduckgo" = "Rechercher avec DuckDuckGo"; +/* VPN connect button text */ +"widget.vpn.button.connect" = "Connect"; + +/* VPN disconnect button text */ +"widget.vpn.button.disconnect" = "Disconnect"; + +/* Message describing VPN connected status */ +"widget.vpn.status.connected" = "VPN is On"; + +/* Message describing VPN disconnected status */ +"widget.vpn.status.disconnected" = "VPN is Off"; + +/* Subtitle describing VPN disconnected status */ +"widget.vpn.subtitle.disconnected" = "Not connected"; + diff --git a/Widgets/hr.lproj/Localizable.strings b/Widgets/hr.lproj/Localizable.strings index 57c9e6786e..42832a9f00 100644 --- a/Widgets/hr.lproj/Localizable.strings +++ b/Widgets/hr.lproj/Localizable.strings @@ -1,12 +1,3 @@ -/* No comment provided by engineer. */ -"Connect" = "Connect"; - -/* No comment provided by engineer. */ -"Disconnect" = "Disconnect"; - -/* No comment provided by engineer. */ -"Error" = "Pogreška"; - /* No comment provided by engineer. */ "iOS 17 required" = "iOS 17 required"; @@ -49,9 +40,6 @@ /* Title shown to the user when adding the Voice Search lock screen widget */ "lock.screen.widget.voice.title" = "Glasovno pretraživanje"; -/* No comment provided by engineer. */ -"VPN Not Configured" = "VPN Not Configured"; - /* Description of search passwords widget in widget gallery */ "widget.gallery.passwords.description" = "Brzo pretraži svoje spremljene DuckDuckGo lozinke."; @@ -70,6 +58,12 @@ /* Display name for search only widget in widget gallery */ "widget.gallery.search.display.name" = "Traži"; +/* Description of VPN widget in widget gallery */ +"widget.gallery.vpn.description" = "View and manage your VPN connection. Requires a Privacy Pro subscription."; + +/* Display name for VPN widget in widget gallery */ +"widget.gallery.vpn.display.name" = "VPN"; + /* CTA shown in the favorites widget empty state. */ "widget.no.favorites.cta" = "Dodaj u omiljene"; @@ -82,3 +76,18 @@ /* Placeholder text in search field on the search and favorites widget */ "widget.search.duckduckgo" = "Pretraži DuckDuckGo"; +/* VPN connect button text */ +"widget.vpn.button.connect" = "Connect"; + +/* VPN disconnect button text */ +"widget.vpn.button.disconnect" = "Disconnect"; + +/* Message describing VPN connected status */ +"widget.vpn.status.connected" = "VPN is On"; + +/* Message describing VPN disconnected status */ +"widget.vpn.status.disconnected" = "VPN is Off"; + +/* Subtitle describing VPN disconnected status */ +"widget.vpn.subtitle.disconnected" = "Not connected"; + diff --git a/Widgets/hu.lproj/Localizable.strings b/Widgets/hu.lproj/Localizable.strings index d3eaf530bb..bd3eac0b01 100644 --- a/Widgets/hu.lproj/Localizable.strings +++ b/Widgets/hu.lproj/Localizable.strings @@ -1,12 +1,3 @@ -/* No comment provided by engineer. */ -"Connect" = "Connect"; - -/* No comment provided by engineer. */ -"Disconnect" = "Disconnect"; - -/* No comment provided by engineer. */ -"Error" = "Hiba"; - /* No comment provided by engineer. */ "iOS 17 required" = "iOS 17 required"; @@ -49,9 +40,6 @@ /* Title shown to the user when adding the Voice Search lock screen widget */ "lock.screen.widget.voice.title" = "Beszédhangalapú keresés"; -/* No comment provided by engineer. */ -"VPN Not Configured" = "VPN Not Configured"; - /* Description of search passwords widget in widget gallery */ "widget.gallery.passwords.description" = "Gyorsan kereshetsz a DuckDuckGo mentett jelszavai között."; @@ -70,6 +58,12 @@ /* Display name for search only widget in widget gallery */ "widget.gallery.search.display.name" = "Keresés"; +/* Description of VPN widget in widget gallery */ +"widget.gallery.vpn.description" = "View and manage your VPN connection. Requires a Privacy Pro subscription."; + +/* Display name for VPN widget in widget gallery */ +"widget.gallery.vpn.display.name" = "VPN"; + /* CTA shown in the favorites widget empty state. */ "widget.no.favorites.cta" = "Kedvencek hozzáadása"; @@ -82,3 +76,18 @@ /* Placeholder text in search field on the search and favorites widget */ "widget.search.duckduckgo" = "DuckDuckGo keresés"; +/* VPN connect button text */ +"widget.vpn.button.connect" = "Connect"; + +/* VPN disconnect button text */ +"widget.vpn.button.disconnect" = "Disconnect"; + +/* Message describing VPN connected status */ +"widget.vpn.status.connected" = "VPN is On"; + +/* Message describing VPN disconnected status */ +"widget.vpn.status.disconnected" = "VPN is Off"; + +/* Subtitle describing VPN disconnected status */ +"widget.vpn.subtitle.disconnected" = "Not connected"; + diff --git a/Widgets/it.lproj/Localizable.strings b/Widgets/it.lproj/Localizable.strings index df33b790b6..b062d5f3a8 100644 --- a/Widgets/it.lproj/Localizable.strings +++ b/Widgets/it.lproj/Localizable.strings @@ -1,12 +1,3 @@ -/* No comment provided by engineer. */ -"Connect" = "Connect"; - -/* No comment provided by engineer. */ -"Disconnect" = "Disconnect"; - -/* No comment provided by engineer. */ -"Error" = "Errore"; - /* No comment provided by engineer. */ "iOS 17 required" = "iOS 17 required"; @@ -49,9 +40,6 @@ /* Title shown to the user when adding the Voice Search lock screen widget */ "lock.screen.widget.voice.title" = "Ricerca vocale"; -/* No comment provided by engineer. */ -"VPN Not Configured" = "VPN Not Configured"; - /* Description of search passwords widget in widget gallery */ "widget.gallery.passwords.description" = "Cerca rapidamente le password DuckDuckGo salvate."; @@ -70,6 +58,12 @@ /* Display name for search only widget in widget gallery */ "widget.gallery.search.display.name" = "Ricerca"; +/* Description of VPN widget in widget gallery */ +"widget.gallery.vpn.description" = "View and manage your VPN connection. Requires a Privacy Pro subscription."; + +/* Display name for VPN widget in widget gallery */ +"widget.gallery.vpn.display.name" = "VPN"; + /* CTA shown in the favorites widget empty state. */ "widget.no.favorites.cta" = "Aggiungi preferiti"; @@ -82,3 +76,18 @@ /* Placeholder text in search field on the search and favorites widget */ "widget.search.duckduckgo" = "Cerca DuckDuckGo"; +/* VPN connect button text */ +"widget.vpn.button.connect" = "Connect"; + +/* VPN disconnect button text */ +"widget.vpn.button.disconnect" = "Disconnect"; + +/* Message describing VPN connected status */ +"widget.vpn.status.connected" = "VPN is On"; + +/* Message describing VPN disconnected status */ +"widget.vpn.status.disconnected" = "VPN is Off"; + +/* Subtitle describing VPN disconnected status */ +"widget.vpn.subtitle.disconnected" = "Not connected"; + diff --git a/Widgets/lt.lproj/Localizable.strings b/Widgets/lt.lproj/Localizable.strings index 7a5f84cb4d..5dbcbabd17 100644 --- a/Widgets/lt.lproj/Localizable.strings +++ b/Widgets/lt.lproj/Localizable.strings @@ -1,12 +1,3 @@ -/* No comment provided by engineer. */ -"Connect" = "Connect"; - -/* No comment provided by engineer. */ -"Disconnect" = "Disconnect"; - -/* No comment provided by engineer. */ -"Error" = "Klaida"; - /* No comment provided by engineer. */ "iOS 17 required" = "iOS 17 required"; @@ -49,9 +40,6 @@ /* Title shown to the user when adding the Voice Search lock screen widget */ "lock.screen.widget.voice.title" = "Paieška balsu"; -/* No comment provided by engineer. */ -"VPN Not Configured" = "VPN Not Configured"; - /* Description of search passwords widget in widget gallery */ "widget.gallery.passwords.description" = "Greitai ieškokite išsaugotų „DuckDuckGo“ slaptažodžių."; @@ -70,6 +58,12 @@ /* Display name for search only widget in widget gallery */ "widget.gallery.search.display.name" = "Paieška"; +/* Description of VPN widget in widget gallery */ +"widget.gallery.vpn.description" = "View and manage your VPN connection. Requires a Privacy Pro subscription."; + +/* Display name for VPN widget in widget gallery */ +"widget.gallery.vpn.display.name" = "VPN"; + /* CTA shown in the favorites widget empty state. */ "widget.no.favorites.cta" = "Pridėti mėgstamus"; @@ -82,3 +76,18 @@ /* Placeholder text in search field on the search and favorites widget */ "widget.search.duckduckgo" = "Ieškoti DuckDuckGo"; +/* VPN connect button text */ +"widget.vpn.button.connect" = "Connect"; + +/* VPN disconnect button text */ +"widget.vpn.button.disconnect" = "Disconnect"; + +/* Message describing VPN connected status */ +"widget.vpn.status.connected" = "VPN is On"; + +/* Message describing VPN disconnected status */ +"widget.vpn.status.disconnected" = "VPN is Off"; + +/* Subtitle describing VPN disconnected status */ +"widget.vpn.subtitle.disconnected" = "Not connected"; + diff --git a/Widgets/lv.lproj/Localizable.strings b/Widgets/lv.lproj/Localizable.strings index e8e53097d3..865a3ef3e1 100644 --- a/Widgets/lv.lproj/Localizable.strings +++ b/Widgets/lv.lproj/Localizable.strings @@ -1,12 +1,3 @@ -/* No comment provided by engineer. */ -"Connect" = "Connect"; - -/* No comment provided by engineer. */ -"Disconnect" = "Disconnect"; - -/* No comment provided by engineer. */ -"Error" = "Kļūda"; - /* No comment provided by engineer. */ "iOS 17 required" = "iOS 17 required"; @@ -49,9 +40,6 @@ /* Title shown to the user when adding the Voice Search lock screen widget */ "lock.screen.widget.voice.title" = "Balss meklēšana"; -/* No comment provided by engineer. */ -"VPN Not Configured" = "VPN Not Configured"; - /* Description of search passwords widget in widget gallery */ "widget.gallery.passwords.description" = "Ātri meklē savas saglabātās DuckDuckGo paroles."; @@ -70,6 +58,12 @@ /* Display name for search only widget in widget gallery */ "widget.gallery.search.display.name" = "Meklēt"; +/* Description of VPN widget in widget gallery */ +"widget.gallery.vpn.description" = "View and manage your VPN connection. Requires a Privacy Pro subscription."; + +/* Display name for VPN widget in widget gallery */ +"widget.gallery.vpn.display.name" = "VPN"; + /* CTA shown in the favorites widget empty state. */ "widget.no.favorites.cta" = "Pievienot izlasei"; @@ -82,3 +76,18 @@ /* Placeholder text in search field on the search and favorites widget */ "widget.search.duckduckgo" = "Meklēt DuckDuckGo"; +/* VPN connect button text */ +"widget.vpn.button.connect" = "Connect"; + +/* VPN disconnect button text */ +"widget.vpn.button.disconnect" = "Disconnect"; + +/* Message describing VPN connected status */ +"widget.vpn.status.connected" = "VPN is On"; + +/* Message describing VPN disconnected status */ +"widget.vpn.status.disconnected" = "VPN is Off"; + +/* Subtitle describing VPN disconnected status */ +"widget.vpn.subtitle.disconnected" = "Not connected"; + diff --git a/Widgets/nb.lproj/Localizable.strings b/Widgets/nb.lproj/Localizable.strings index 5315fab5f8..35c13fb043 100644 --- a/Widgets/nb.lproj/Localizable.strings +++ b/Widgets/nb.lproj/Localizable.strings @@ -1,12 +1,3 @@ -/* No comment provided by engineer. */ -"Connect" = "Connect"; - -/* No comment provided by engineer. */ -"Disconnect" = "Disconnect"; - -/* No comment provided by engineer. */ -"Error" = "Feil"; - /* No comment provided by engineer. */ "iOS 17 required" = "iOS 17 required"; @@ -49,9 +40,6 @@ /* Title shown to the user when adding the Voice Search lock screen widget */ "lock.screen.widget.voice.title" = "Talesøk"; -/* No comment provided by engineer. */ -"VPN Not Configured" = "VPN Not Configured"; - /* Description of search passwords widget in widget gallery */ "widget.gallery.passwords.description" = "Søk raskt i de lagrede DuckDuckGo-passordene dine."; @@ -70,6 +58,12 @@ /* Display name for search only widget in widget gallery */ "widget.gallery.search.display.name" = "Søk"; +/* Description of VPN widget in widget gallery */ +"widget.gallery.vpn.description" = "View and manage your VPN connection. Requires a Privacy Pro subscription."; + +/* Display name for VPN widget in widget gallery */ +"widget.gallery.vpn.display.name" = "VPN"; + /* CTA shown in the favorites widget empty state. */ "widget.no.favorites.cta" = "Legg til favoritter"; @@ -82,3 +76,18 @@ /* Placeholder text in search field on the search and favorites widget */ "widget.search.duckduckgo" = "Søk i DuckDuckGo"; +/* VPN connect button text */ +"widget.vpn.button.connect" = "Connect"; + +/* VPN disconnect button text */ +"widget.vpn.button.disconnect" = "Disconnect"; + +/* Message describing VPN connected status */ +"widget.vpn.status.connected" = "VPN is On"; + +/* Message describing VPN disconnected status */ +"widget.vpn.status.disconnected" = "VPN is Off"; + +/* Subtitle describing VPN disconnected status */ +"widget.vpn.subtitle.disconnected" = "Not connected"; + diff --git a/Widgets/nl.lproj/Localizable.strings b/Widgets/nl.lproj/Localizable.strings index 75679e3121..fd5d8a6696 100644 --- a/Widgets/nl.lproj/Localizable.strings +++ b/Widgets/nl.lproj/Localizable.strings @@ -1,12 +1,3 @@ -/* No comment provided by engineer. */ -"Connect" = "Connect"; - -/* No comment provided by engineer. */ -"Disconnect" = "Disconnect"; - -/* No comment provided by engineer. */ -"Error" = "Fout"; - /* No comment provided by engineer. */ "iOS 17 required" = "iOS 17 required"; @@ -49,9 +40,6 @@ /* Title shown to the user when adding the Voice Search lock screen widget */ "lock.screen.widget.voice.title" = "Zoeken via spraak"; -/* No comment provided by engineer. */ -"VPN Not Configured" = "VPN Not Configured"; - /* Description of search passwords widget in widget gallery */ "widget.gallery.passwords.description" = "Zoek snel je opgeslagen DuckDuckGo-wachtwoorden."; @@ -70,6 +58,12 @@ /* Display name for search only widget in widget gallery */ "widget.gallery.search.display.name" = "Zoeken"; +/* Description of VPN widget in widget gallery */ +"widget.gallery.vpn.description" = "View and manage your VPN connection. Requires a Privacy Pro subscription."; + +/* Display name for VPN widget in widget gallery */ +"widget.gallery.vpn.display.name" = "VPN"; + /* CTA shown in the favorites widget empty state. */ "widget.no.favorites.cta" = "Favorieten toevoegen"; @@ -82,3 +76,18 @@ /* Placeholder text in search field on the search and favorites widget */ "widget.search.duckduckgo" = "Zoek in DuckDuckGo"; +/* VPN connect button text */ +"widget.vpn.button.connect" = "Connect"; + +/* VPN disconnect button text */ +"widget.vpn.button.disconnect" = "Disconnect"; + +/* Message describing VPN connected status */ +"widget.vpn.status.connected" = "VPN is On"; + +/* Message describing VPN disconnected status */ +"widget.vpn.status.disconnected" = "VPN is Off"; + +/* Subtitle describing VPN disconnected status */ +"widget.vpn.subtitle.disconnected" = "Not connected"; + diff --git a/Widgets/pl.lproj/Localizable.strings b/Widgets/pl.lproj/Localizable.strings index cf7b783a92..8db989bd95 100644 --- a/Widgets/pl.lproj/Localizable.strings +++ b/Widgets/pl.lproj/Localizable.strings @@ -1,12 +1,3 @@ -/* No comment provided by engineer. */ -"Connect" = "Connect"; - -/* No comment provided by engineer. */ -"Disconnect" = "Disconnect"; - -/* No comment provided by engineer. */ -"Error" = "Błąd"; - /* No comment provided by engineer. */ "iOS 17 required" = "iOS 17 required"; @@ -49,9 +40,6 @@ /* Title shown to the user when adding the Voice Search lock screen widget */ "lock.screen.widget.voice.title" = "Wyszukiwanie głosowe"; -/* No comment provided by engineer. */ -"VPN Not Configured" = "VPN Not Configured"; - /* Description of search passwords widget in widget gallery */ "widget.gallery.passwords.description" = "Szybko wyszukuj zapisane hasła DuckDuckGo."; @@ -70,6 +58,12 @@ /* Display name for search only widget in widget gallery */ "widget.gallery.search.display.name" = "Szukaj"; +/* Description of VPN widget in widget gallery */ +"widget.gallery.vpn.description" = "View and manage your VPN connection. Requires a Privacy Pro subscription."; + +/* Display name for VPN widget in widget gallery */ +"widget.gallery.vpn.display.name" = "VPN"; + /* CTA shown in the favorites widget empty state. */ "widget.no.favorites.cta" = "Dodaj ulubione"; @@ -82,3 +76,18 @@ /* Placeholder text in search field on the search and favorites widget */ "widget.search.duckduckgo" = "Przeszukaj DuckDuckGo"; +/* VPN connect button text */ +"widget.vpn.button.connect" = "Connect"; + +/* VPN disconnect button text */ +"widget.vpn.button.disconnect" = "Disconnect"; + +/* Message describing VPN connected status */ +"widget.vpn.status.connected" = "VPN is On"; + +/* Message describing VPN disconnected status */ +"widget.vpn.status.disconnected" = "VPN is Off"; + +/* Subtitle describing VPN disconnected status */ +"widget.vpn.subtitle.disconnected" = "Not connected"; + diff --git a/Widgets/pt.lproj/Localizable.strings b/Widgets/pt.lproj/Localizable.strings index b4c756609f..d5ab7f3399 100644 --- a/Widgets/pt.lproj/Localizable.strings +++ b/Widgets/pt.lproj/Localizable.strings @@ -1,12 +1,3 @@ -/* No comment provided by engineer. */ -"Connect" = "Connect"; - -/* No comment provided by engineer. */ -"Disconnect" = "Disconnect"; - -/* No comment provided by engineer. */ -"Error" = "Erro"; - /* No comment provided by engineer. */ "iOS 17 required" = "iOS 17 required"; @@ -49,9 +40,6 @@ /* Title shown to the user when adding the Voice Search lock screen widget */ "lock.screen.widget.voice.title" = "Pesquisa por voz"; -/* No comment provided by engineer. */ -"VPN Not Configured" = "VPN Not Configured"; - /* Description of search passwords widget in widget gallery */ "widget.gallery.passwords.description" = "Pesquisa rapidamente as tuas palavras-passe guardadas do DuckDuckGo."; @@ -70,6 +58,12 @@ /* Display name for search only widget in widget gallery */ "widget.gallery.search.display.name" = "Pesquisar"; +/* Description of VPN widget in widget gallery */ +"widget.gallery.vpn.description" = "View and manage your VPN connection. Requires a Privacy Pro subscription."; + +/* Display name for VPN widget in widget gallery */ +"widget.gallery.vpn.display.name" = "VPN"; + /* CTA shown in the favorites widget empty state. */ "widget.no.favorites.cta" = "Adicionar Favoritos"; @@ -82,3 +76,18 @@ /* Placeholder text in search field on the search and favorites widget */ "widget.search.duckduckgo" = "Pesquisar no DuckDuckGo"; +/* VPN connect button text */ +"widget.vpn.button.connect" = "Connect"; + +/* VPN disconnect button text */ +"widget.vpn.button.disconnect" = "Disconnect"; + +/* Message describing VPN connected status */ +"widget.vpn.status.connected" = "VPN is On"; + +/* Message describing VPN disconnected status */ +"widget.vpn.status.disconnected" = "VPN is Off"; + +/* Subtitle describing VPN disconnected status */ +"widget.vpn.subtitle.disconnected" = "Not connected"; + diff --git a/Widgets/ro.lproj/Localizable.strings b/Widgets/ro.lproj/Localizable.strings index 02e3b66233..19f95667c2 100644 --- a/Widgets/ro.lproj/Localizable.strings +++ b/Widgets/ro.lproj/Localizable.strings @@ -1,12 +1,3 @@ -/* No comment provided by engineer. */ -"Connect" = "Connect"; - -/* No comment provided by engineer. */ -"Disconnect" = "Disconnect"; - -/* No comment provided by engineer. */ -"Error" = "Eroare"; - /* No comment provided by engineer. */ "iOS 17 required" = "iOS 17 required"; @@ -49,9 +40,6 @@ /* Title shown to the user when adding the Voice Search lock screen widget */ "lock.screen.widget.voice.title" = "Căutare vocală"; -/* No comment provided by engineer. */ -"VPN Not Configured" = "VPN Not Configured"; - /* Description of search passwords widget in widget gallery */ "widget.gallery.passwords.description" = "Caută rapid parolele tale DuckDuckGo salvate."; @@ -70,6 +58,12 @@ /* Display name for search only widget in widget gallery */ "widget.gallery.search.display.name" = "Caută"; +/* Description of VPN widget in widget gallery */ +"widget.gallery.vpn.description" = "View and manage your VPN connection. Requires a Privacy Pro subscription."; + +/* Display name for VPN widget in widget gallery */ +"widget.gallery.vpn.display.name" = "VPN"; + /* CTA shown in the favorites widget empty state. */ "widget.no.favorites.cta" = "Adaugă preferințe"; @@ -82,3 +76,18 @@ /* Placeholder text in search field on the search and favorites widget */ "widget.search.duckduckgo" = "Căutare DuckDuckGo"; +/* VPN connect button text */ +"widget.vpn.button.connect" = "Connect"; + +/* VPN disconnect button text */ +"widget.vpn.button.disconnect" = "Disconnect"; + +/* Message describing VPN connected status */ +"widget.vpn.status.connected" = "VPN is On"; + +/* Message describing VPN disconnected status */ +"widget.vpn.status.disconnected" = "VPN is Off"; + +/* Subtitle describing VPN disconnected status */ +"widget.vpn.subtitle.disconnected" = "Not connected"; + diff --git a/Widgets/ru.lproj/Localizable.strings b/Widgets/ru.lproj/Localizable.strings index 16b1a1f59c..e26bf632e3 100644 --- a/Widgets/ru.lproj/Localizable.strings +++ b/Widgets/ru.lproj/Localizable.strings @@ -1,12 +1,3 @@ -/* No comment provided by engineer. */ -"Connect" = "Connect"; - -/* No comment provided by engineer. */ -"Disconnect" = "Disconnect"; - -/* No comment provided by engineer. */ -"Error" = "Ошибка"; - /* No comment provided by engineer. */ "iOS 17 required" = "iOS 17 required"; @@ -49,9 +40,6 @@ /* Title shown to the user when adding the Voice Search lock screen widget */ "lock.screen.widget.voice.title" = "Голосовой поиск"; -/* No comment provided by engineer. */ -"VPN Not Configured" = "VPN Not Configured"; - /* Description of search passwords widget in widget gallery */ "widget.gallery.passwords.description" = "Быстрый поиск среди паролей, сохраненных в DuckDuckGo."; @@ -70,6 +58,12 @@ /* Display name for search only widget in widget gallery */ "widget.gallery.search.display.name" = "Поиск"; +/* Description of VPN widget in widget gallery */ +"widget.gallery.vpn.description" = "View and manage your VPN connection. Requires a Privacy Pro subscription."; + +/* Display name for VPN widget in widget gallery */ +"widget.gallery.vpn.display.name" = "VPN"; + /* CTA shown in the favorites widget empty state. */ "widget.no.favorites.cta" = "Добавить в избранное"; @@ -82,3 +76,18 @@ /* Placeholder text in search field on the search and favorites widget */ "widget.search.duckduckgo" = "Поиск в DuckDuckGo"; +/* VPN connect button text */ +"widget.vpn.button.connect" = "Connect"; + +/* VPN disconnect button text */ +"widget.vpn.button.disconnect" = "Disconnect"; + +/* Message describing VPN connected status */ +"widget.vpn.status.connected" = "VPN is On"; + +/* Message describing VPN disconnected status */ +"widget.vpn.status.disconnected" = "VPN is Off"; + +/* Subtitle describing VPN disconnected status */ +"widget.vpn.subtitle.disconnected" = "Not connected"; + diff --git a/Widgets/sk.lproj/Localizable.strings b/Widgets/sk.lproj/Localizable.strings index b1362fad22..3364e8e245 100644 --- a/Widgets/sk.lproj/Localizable.strings +++ b/Widgets/sk.lproj/Localizable.strings @@ -1,12 +1,3 @@ -/* No comment provided by engineer. */ -"Connect" = "Connect"; - -/* No comment provided by engineer. */ -"Disconnect" = "Disconnect"; - -/* No comment provided by engineer. */ -"Error" = "Chyba"; - /* No comment provided by engineer. */ "iOS 17 required" = "iOS 17 required"; @@ -49,9 +40,6 @@ /* Title shown to the user when adding the Voice Search lock screen widget */ "lock.screen.widget.voice.title" = "Hlasové vyhľadávanie"; -/* No comment provided by engineer. */ -"VPN Not Configured" = "VPN Not Configured"; - /* Description of search passwords widget in widget gallery */ "widget.gallery.passwords.description" = "Rýchlo vyhľadajte svoje uložené DuckDuckGo heslá."; @@ -70,6 +58,12 @@ /* Display name for search only widget in widget gallery */ "widget.gallery.search.display.name" = "Vyhľadávať"; +/* Description of VPN widget in widget gallery */ +"widget.gallery.vpn.description" = "View and manage your VPN connection. Requires a Privacy Pro subscription."; + +/* Display name for VPN widget in widget gallery */ +"widget.gallery.vpn.display.name" = "VPN"; + /* CTA shown in the favorites widget empty state. */ "widget.no.favorites.cta" = "Pridať do obľúbených položiek"; @@ -82,3 +76,18 @@ /* Placeholder text in search field on the search and favorites widget */ "widget.search.duckduckgo" = "Vyhľadávajte cez DuckDuckGo"; +/* VPN connect button text */ +"widget.vpn.button.connect" = "Connect"; + +/* VPN disconnect button text */ +"widget.vpn.button.disconnect" = "Disconnect"; + +/* Message describing VPN connected status */ +"widget.vpn.status.connected" = "VPN is On"; + +/* Message describing VPN disconnected status */ +"widget.vpn.status.disconnected" = "VPN is Off"; + +/* Subtitle describing VPN disconnected status */ +"widget.vpn.subtitle.disconnected" = "Not connected"; + diff --git a/Widgets/sl.lproj/Localizable.strings b/Widgets/sl.lproj/Localizable.strings index 3307da1723..c5849a0715 100644 --- a/Widgets/sl.lproj/Localizable.strings +++ b/Widgets/sl.lproj/Localizable.strings @@ -1,12 +1,3 @@ -/* No comment provided by engineer. */ -"Connect" = "Connect"; - -/* No comment provided by engineer. */ -"Disconnect" = "Disconnect"; - -/* No comment provided by engineer. */ -"Error" = "Napaka"; - /* No comment provided by engineer. */ "iOS 17 required" = "iOS 17 required"; @@ -49,9 +40,6 @@ /* Title shown to the user when adding the Voice Search lock screen widget */ "lock.screen.widget.voice.title" = "Glasovno iskanje"; -/* No comment provided by engineer. */ -"VPN Not Configured" = "VPN Not Configured"; - /* Description of search passwords widget in widget gallery */ "widget.gallery.passwords.description" = "Hitro poiščite shranjena gesla DuckDuckGo."; @@ -70,6 +58,12 @@ /* Display name for search only widget in widget gallery */ "widget.gallery.search.display.name" = "Iskanje"; +/* Description of VPN widget in widget gallery */ +"widget.gallery.vpn.description" = "View and manage your VPN connection. Requires a Privacy Pro subscription."; + +/* Display name for VPN widget in widget gallery */ +"widget.gallery.vpn.display.name" = "VPN"; + /* CTA shown in the favorites widget empty state. */ "widget.no.favorites.cta" = "Dodaj priljubljene"; @@ -82,3 +76,18 @@ /* Placeholder text in search field on the search and favorites widget */ "widget.search.duckduckgo" = "Iščite po DuckDuckGo"; +/* VPN connect button text */ +"widget.vpn.button.connect" = "Connect"; + +/* VPN disconnect button text */ +"widget.vpn.button.disconnect" = "Disconnect"; + +/* Message describing VPN connected status */ +"widget.vpn.status.connected" = "VPN is On"; + +/* Message describing VPN disconnected status */ +"widget.vpn.status.disconnected" = "VPN is Off"; + +/* Subtitle describing VPN disconnected status */ +"widget.vpn.subtitle.disconnected" = "Not connected"; + diff --git a/Widgets/sv.lproj/Localizable.strings b/Widgets/sv.lproj/Localizable.strings index ed6904ff0f..820ebc437e 100644 --- a/Widgets/sv.lproj/Localizable.strings +++ b/Widgets/sv.lproj/Localizable.strings @@ -1,12 +1,3 @@ -/* No comment provided by engineer. */ -"Connect" = "Connect"; - -/* No comment provided by engineer. */ -"Disconnect" = "Disconnect"; - -/* No comment provided by engineer. */ -"Error" = "Fel"; - /* No comment provided by engineer. */ "iOS 17 required" = "iOS 17 required"; @@ -49,9 +40,6 @@ /* Title shown to the user when adding the Voice Search lock screen widget */ "lock.screen.widget.voice.title" = "Röstsökning"; -/* No comment provided by engineer. */ -"VPN Not Configured" = "VPN Not Configured"; - /* Description of search passwords widget in widget gallery */ "widget.gallery.passwords.description" = "Sök snabbt bland dina sparade DuckDuckGo-lösenord."; @@ -70,6 +58,12 @@ /* Display name for search only widget in widget gallery */ "widget.gallery.search.display.name" = "Sök"; +/* Description of VPN widget in widget gallery */ +"widget.gallery.vpn.description" = "View and manage your VPN connection. Requires a Privacy Pro subscription."; + +/* Display name for VPN widget in widget gallery */ +"widget.gallery.vpn.display.name" = "VPN"; + /* CTA shown in the favorites widget empty state. */ "widget.no.favorites.cta" = "Lägg till favoriter"; @@ -82,3 +76,18 @@ /* Placeholder text in search field on the search and favorites widget */ "widget.search.duckduckgo" = "Sök med DuckDuckGo"; +/* VPN connect button text */ +"widget.vpn.button.connect" = "Connect"; + +/* VPN disconnect button text */ +"widget.vpn.button.disconnect" = "Disconnect"; + +/* Message describing VPN connected status */ +"widget.vpn.status.connected" = "VPN is On"; + +/* Message describing VPN disconnected status */ +"widget.vpn.status.disconnected" = "VPN is Off"; + +/* Subtitle describing VPN disconnected status */ +"widget.vpn.subtitle.disconnected" = "Not connected"; + diff --git a/Widgets/tr.lproj/Localizable.strings b/Widgets/tr.lproj/Localizable.strings index 5bca6f7fa7..09573c75bb 100644 --- a/Widgets/tr.lproj/Localizable.strings +++ b/Widgets/tr.lproj/Localizable.strings @@ -1,12 +1,3 @@ -/* No comment provided by engineer. */ -"Connect" = "Connect"; - -/* No comment provided by engineer. */ -"Disconnect" = "Disconnect"; - -/* No comment provided by engineer. */ -"Error" = "Hata"; - /* No comment provided by engineer. */ "iOS 17 required" = "iOS 17 required"; @@ -49,9 +40,6 @@ /* Title shown to the user when adding the Voice Search lock screen widget */ "lock.screen.widget.voice.title" = "Sesli Arama"; -/* No comment provided by engineer. */ -"VPN Not Configured" = "VPN Not Configured"; - /* Description of search passwords widget in widget gallery */ "widget.gallery.passwords.description" = "Kayıtlı DuckDuckGo şifrelerinizi hızlıca arayın."; @@ -70,6 +58,12 @@ /* Display name for search only widget in widget gallery */ "widget.gallery.search.display.name" = "Ara"; +/* Description of VPN widget in widget gallery */ +"widget.gallery.vpn.description" = "View and manage your VPN connection. Requires a Privacy Pro subscription."; + +/* Display name for VPN widget in widget gallery */ +"widget.gallery.vpn.display.name" = "VPN"; + /* CTA shown in the favorites widget empty state. */ "widget.no.favorites.cta" = "Favorilere Ekle"; @@ -82,3 +76,18 @@ /* Placeholder text in search field on the search and favorites widget */ "widget.search.duckduckgo" = "DuckDuckGo'da Ara"; +/* VPN connect button text */ +"widget.vpn.button.connect" = "Connect"; + +/* VPN disconnect button text */ +"widget.vpn.button.disconnect" = "Disconnect"; + +/* Message describing VPN connected status */ +"widget.vpn.status.connected" = "VPN is On"; + +/* Message describing VPN disconnected status */ +"widget.vpn.status.disconnected" = "VPN is Off"; + +/* Subtitle describing VPN disconnected status */ +"widget.vpn.subtitle.disconnected" = "Not connected"; + diff --git a/package-lock.json b/package-lock.json index c0f344c1e2..1cc11dc517 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "ios", "version": "1.0.0", "dependencies": { - "@duckduckgo/autoconsent": "^10.6.1" + "@duckduckgo/autoconsent": "^10.8.0" }, "devDependencies": { "@rollup/plugin-json": "^4.1.0", @@ -135,9 +135,9 @@ } }, "node_modules/@duckduckgo/autoconsent": { - "version": "10.6.1", - "resolved": "https://registry.npmjs.org/@duckduckgo/autoconsent/-/autoconsent-10.6.1.tgz", - "integrity": "sha512-ptgT0sp4zmQTZHAyGR9TN/WJT9W7kTb/yvaF20FwwSIcLKd2xLe2jCDwbGTaLVSqAixWDKqzZ1Dg3l7HE159Sw==" + "version": "10.8.0", + "resolved": "https://registry.npmjs.org/@duckduckgo/autoconsent/-/autoconsent-10.8.0.tgz", + "integrity": "sha512-n4axPmOsDxK9X6UYUb+S7KWqvppG75IemnH+pK1SAp01+US4ez+t7fzq4VQU19dy0EpGyC1bUnsB2BzdhUx65g==" }, "node_modules/@eslint/eslintrc": { "version": "0.4.3", diff --git a/package.json b/package.json index 1c88aff868..f3503bfe65 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,6 @@ "rollup-plugin-terser": "^7.0.2" }, "dependencies": { - "@duckduckgo/autoconsent": "^10.6.1" + "@duckduckgo/autoconsent": "^10.8.0" } }