From eb5a7598a7504567a12052a83191da69ceba5853 Mon Sep 17 00:00:00 2001 From: Sabrina Tardio <44158575+SabrinaTardio@users.noreply.github.com> Date: Thu, 14 Mar 2024 12:32:55 +0100 Subject: [PATCH 01/51] Sabrina/surveys (#2380) Task/Issue URL: https://app.asana.com/0/1199230911884351/1206807075858541/f **Description**: We want to invite app users to research interviews [Invite design](https://app.asana.com/0/72649045549333/1206743222316770) [Invite copy](https://app.asana.com/0/38424471409662/1206743222316766) Survey links: Day0 https://selfserve.decipherinc.com/survey/selfserve/32ab/240300?list=1 Day14 https://selfserve.decipherinc.com/survey/selfserve/32ab/240300?list=2 The invite needs to be shown to a very limited share of users at different points of their usage journey, 10% of day-0 users, and 10% day-14 users Flow: Users receive an invitation to participate in the survey. This invite automatically disappears if it is dismissed, after 24 h, or if the user clicks the participation button Users who choose to participate are then redirected to our designated survey platforms, On the survey platform, committed participants have the option to share their email addresses for further communication or updates Users see 1 invite max --- .../Images/QandA-128.imageset/Contents.json | 12 ++ .../Images/QandA-128.imageset/QandA-128.svg | 9 ++ DuckDuckGo/Common/Localizables/UserText.swift | 12 +- .../Utilities/UserDefaultsWrapper.swift | 4 +- .../Model/HomePageContinueSetUpModel.swift | 116 +++++++++----- DuckDuckGo/Localizable.xcstrings | 102 ++++++++---- DuckDuckGo/Menus/MainMenu.swift | 9 +- DuckDuckGo/Menus/MainMenuActions.swift | 33 ++-- .../Model/AppearancePreferences.swift | 4 +- .../HomePage/ContinueSetUpModelTests.swift | 147 +++++++++++++----- 10 files changed, 319 insertions(+), 129 deletions(-) create mode 100644 DuckDuckGo/Assets.xcassets/Images/QandA-128.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/QandA-128.imageset/QandA-128.svg diff --git a/DuckDuckGo/Assets.xcassets/Images/QandA-128.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/QandA-128.imageset/Contents.json new file mode 100644 index 0000000000..e708cce2ed --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/QandA-128.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "QandA-128.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/QandA-128.imageset/QandA-128.svg b/DuckDuckGo/Assets.xcassets/Images/QandA-128.imageset/QandA-128.svg new file mode 100644 index 0000000000..4e2ca57aaf --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/QandA-128.imageset/QandA-128.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 8fad1dff04..a5149fcee7 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -974,23 +974,23 @@ struct UserText { static let newTabSetUpImportCardTitle = NSLocalizedString("newTab.setup.import.title", value: "Bring Your Stuff", comment: "Title of the Import card of the Set Up section in the home page") static let newTabSetUpDuckPlayerCardTitle = NSLocalizedString("newTab.setup.duck.player.title", value: "Clean Up YouTube", comment: "Title of the Duck Player card of the Set Up section in the home page") static let newTabSetUpEmailProtectionCardTitle = NSLocalizedString("newTab.setup.email.protection.title", value: "Protect Your Inbox", comment: "Title of the Email Protection card of the Set Up section in the home page") - static let newTabSetUpSurveyDay0CardTitle = NSLocalizedString("newTab.setup.survey.day.0.title", value: "Tell Us What Brought You Here", comment: "Title of the Day 0 durvey of the Set Up section in the home page") - static let newTabSetUpSurveyDay7CardTitle = NSLocalizedString("newTab.setup.survey.day.7.title", value: "Help Us Improve", comment: "Title of the Day 7 durvey of the Set Up section in the home page") + static let newTabSetUpSurveyDay0CardTitle = NSLocalizedString("newTab.setup.survey.day.0.title", value: "Share Your Thoughts With Us", comment: "Title of the Day 0 durvey of the Set Up section in the home page") + static let newTabSetUpSurveyDay14CardTitle = NSLocalizedString("newTab.setup.survey.day.14.title", value: "Share Your Thoughts With Us", comment: "Title of the Day 14 durvey of the Set Up section in the home page") static let newTabSetUpDefaultBrowserAction = NSLocalizedString("newTab.setup.default.browser.action", value: "Make Default Browser", comment: "Action title on the action menu of the Default Browser card") static let newTabSetUpImportAction = NSLocalizedString("newTab.setup.Import.action", value: "Import Now", comment: "Action title on the action menu of the Import card of the Set Up section in the home page") static let newTabSetUpDuckPlayerAction = NSLocalizedString("newTab.setup.duck.player.action", value: "Try Duck Player", comment: "Action title on the action menu of the Duck Player card of the Set Up section in the home page") static let newTabSetUpEmailProtectionAction = NSLocalizedString("newTab.setup.email.protection.action", value: "Get a Duck Address", comment: "Action title on the action menu of the Email Protection card of the Set Up section in the home page") static let newTabSetUpRemoveItemAction = NSLocalizedString("newTab.setup.remove.item", value: "Dismiss", comment: "Action title on the action menu of the set up cards card of the SetUp section in the home page to remove the item") - static let newTabSetUpSurveyDay0Action = NSLocalizedString("newTab.setup.survey.day.0.action", value: "Share Your Thoughts", comment: "Action title of the Day 0 durvey of the Set Up section in the home page") - static let newTabSetUpSurveyDay7Action = NSLocalizedString("newTab.setup.survey.day.7.action", value: "Share Your Thoughts", comment: "Action title of the Day 7 durvey of the Set Up section in the home page") + static let newTabSetUpSurveyDay0Action = NSLocalizedString("newTab.setup.survey.day.0.action", value: "Sign Up To Participate", comment: "Action title of the Day 0 survey of the Set Up section in the home page") + static let newTabSetUpSurveyDay14Action = NSLocalizedString("newTab.setup.survey.day.14.action", value: "Sign Up To Participate", comment: "Action title of the Day 14 survey of the Set Up section in the home page") static let newTabSetUpDefaultBrowserSummary = NSLocalizedString("newTab.setup.default.browser.summary", value: "We automatically block trackers as you browse. It's privacy, simplified.", comment: "Summary of the Default Browser card") static let newTabSetUpImportSummary = NSLocalizedString("newTab.setup.import.summary", value: "Import bookmarks, favorites, and passwords from your old browser.", comment: "Summary of the Import card of the Set Up section in the home page") static let newTabSetUpDuckPlayerSummary = NSLocalizedString("newTab.setup.duck.player.summary", value: "Enjoy a clean viewing experience without personalized ads.", comment: "Summary of the Duck Player card of the Set Up section in the home page") static let newTabSetUpEmailProtectionSummary = NSLocalizedString("newTab.setup.email.protection.summary", value: "Generate custom @duck.com addresses that clean trackers from incoming email.", comment: "Summary of the Email Protection card of the Set Up section in the home page") - static let newTabSetUpSurveyDay0Summary = NSLocalizedString("newTab.setup.survey.day.0.summary", value: "Take our short survey and help us build the best browser.", comment: "Summary of the Day 0 durvey of the Set Up section in the home page") - static let newTabSetUpSurveyDay7Summary = NSLocalizedString("newTab.setup.survey.day.7.summary", value: "Take our short survey and help us build the best browser.", comment: "Summary of the Day 7 durvey of the Set Up section in the home page") + static let newTabSetUpSurveyDay0Summary = NSLocalizedString("newTab.setup.survey.day.0.summary", value: "Join an interview with a member of our research team to help us build the best browser.", comment: "Summary of the card on the new tab page that invites users to partecipate to a survey") + static let newTabSetUpSurveyDay14Summary = NSLocalizedString("newTab.setup.survey.day.14.summary", value: "Join an interview with a member of our research team to help us build the best browser.", comment: "Summary of the card on the new tab page that invites users to partecipate to a survey") // Recent Activity static let newTabRecentActivitySectionTitle = NSLocalizedString("newTab.recent.activity.section.title", value: "Recent Activity", comment: "Title of the RecentActivity section in the home page") diff --git a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift index 272d63cca0..66216b4343 100644 --- a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift +++ b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift @@ -111,8 +111,10 @@ public struct UserDefaultsWrapper { case homePageShowDuckPlayer = "home.page.show.duck.player" case homePageShowEmailProtection = "home.page.show.email.protection" case homePageShowSurveyDay0 = "home.page.show.survey.0" + case homePageShowSurveyDay0in10Percent = "home.page.show.survey.0.in.10.pervent" + case homePageShowSurveyDay14in10Percent = "home.page.show.survey.0.in.14.pervent" case homePageUserInteractedWithSurveyDay0 = "home.page.user.interacted.with.survey.0" - case homePageShowSurveyDay7 = "home.page.show.survey.7" + case homePageShowSurveyDay14 = "home.page.show.survey.14" case homePageShowPageTitles = "home.page.show.page.titles" case homePageShowRecentlyVisited = "home.page.show.recently.visited" case homePageContinueSetUpImport = "home.page.continue.set.up.import" diff --git a/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift b/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift index 33e2482333..1e7a137548 100644 --- a/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift +++ b/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift @@ -51,10 +51,10 @@ extension HomePage.Models { } return false } - var isDay7SurveyEnabled: Bool { + var isDay14SurveyEnabled: Bool { let newTabContinueSetUpSettings = privacyConfigurationManager.privacyConfig.settings(for: .newTabContinueSetUp) - if let day7SurveyString = newTabContinueSetUpSettings["surveyCardDay7"] as? String { - if day7SurveyString == "enabled" { + if let day14SurveyString = newTabContinueSetUpSettings["surveyCardDay14"] as? String { + if day14SurveyString == "enabled" { return true } } @@ -64,8 +64,8 @@ extension HomePage.Models { let duckPlayerSettings = privacyConfigurationManager.privacyConfig.settings(for: .duckPlayer) return duckPlayerSettings["tryDuckPlayerLink"] as? String ?? "https://www.youtube.com/watch?v=yKWIA-Pys4c" } - var day0SurveyURL: String = "https://selfserve.decipherinc.com/survey/selfserve/32ab/230701?list=1" - var day7SurveyURL: String = "https://selfserve.decipherinc.com/survey/selfserve/32ab/230702?list=1" + var day0SurveyURL: String = "https://selfserve.decipherinc.com/survey/selfserve/32ab/240300?list=1" + var day14SurveyURL: String = "https://selfserve.decipherinc.com/survey/selfserve/32ab/240300?list=2" private let defaultBrowserProvider: DefaultBrowserProvider private let dataImportProvider: DataImportStatusProviding @@ -73,6 +73,7 @@ extension HomePage.Models { private let emailManager: EmailManager private let privacyPreferences: PrivacySecurityPreferences private let duckPlayerPreferences: DuckPlayerPreferencesPersistor + private let randomNumberGenerator: RandomNumberGenerating @UserDefaultsWrapper(key: .homePageShowAllFeatures, defaultValue: false) var shouldShowAllFeatures: Bool { @@ -102,12 +103,18 @@ extension HomePage.Models { @UserDefaultsWrapper(key: .shouldShowDBPWaitlistInvitedCardUI, defaultValue: false) private var shouldShowDBPWaitlistInvitedCardUI: Bool - @UserDefaultsWrapper(key: .homePageShowSurveyDay7, defaultValue: true) - private var shouldShowSurveyDay7: Bool + @UserDefaultsWrapper(key: .homePageShowSurveyDay14, defaultValue: true) + private var shouldShowSurveyDay14: Bool @UserDefaultsWrapper(key: .homePageIsFirstSession, defaultValue: true) private var isFirstSession: Bool + @UserDefaultsWrapper(key: .homePageShowSurveyDay0in10Percent, defaultValue: nil) + private var isPartOfSurveyDay0On10Percent: Bool? + + @UserDefaultsWrapper(key: .homePageShowSurveyDay14in10Percent, defaultValue: nil) + private var isPartOfSurveyDay14On10Percent: Bool? + @UserDefaultsWrapper(key: .firstLaunchDate, defaultValue: Calendar.current.date(byAdding: .month, value: -1, to: Date())!) private var firstLaunchDate: Date @@ -138,7 +145,8 @@ extension HomePage.Models { privacyPreferences: PrivacySecurityPreferences = PrivacySecurityPreferences.shared, duckPlayerPreferences: DuckPlayerPreferencesPersistor, homePageRemoteMessaging: HomePageRemoteMessaging, - privacyConfigurationManager: PrivacyConfigurationManaging = AppPrivacyFeatures.shared.contentBlocking.privacyConfigurationManager) { + privacyConfigurationManager: PrivacyConfigurationManaging = AppPrivacyFeatures.shared.contentBlocking.privacyConfigurationManager, + randomNumberGenerator: RandomNumberGenerating = RandomNumberGenerator()) { self.defaultBrowserProvider = defaultBrowserProvider self.dataImportProvider = dataImportProvider self.tabCollectionViewModel = tabCollectionViewModel @@ -147,6 +155,7 @@ extension HomePage.Models { self.duckPlayerPreferences = duckPlayerPreferences self.homePageRemoteMessaging = homePageRemoteMessaging self.privacyConfigurationManager = privacyConfigurationManager + self.randomNumberGenerator = randomNumberGenerator refreshFeaturesMatrix() @@ -175,8 +184,8 @@ extension HomePage.Models { tabCollectionViewModel.append(tab: tab) case .surveyDay0: visitSurvey(day: .day0) - case .surveyDay7: - visitSurvey(day: .day7) + case .surveyDay14: + visitSurvey(day: .day14) case .networkProtectionRemoteMessage(let message): handle(remoteMessage: message) case .dataBrokerProtectionRemoteMessage(let message): @@ -200,8 +209,8 @@ extension HomePage.Models { shouldShowEmailProtectionSetting = false case .surveyDay0: shouldShowSurveyDay0 = false - case .surveyDay7: - shouldShowSurveyDay7 = false + case .surveyDay14: + shouldShowSurveyDay14 = false case .networkProtectionRemoteMessage(let message): #if NETWORK_PROTECTION homePageRemoteMessaging.networkProtectionRemoteMessaging.dismiss(message: message) @@ -268,9 +277,10 @@ extension HomePage.Models { case .surveyDay0: if shouldSurveyDay0BeVisible { features.append(feature) + userInteractedWithSurveyDay0 = true } - case .surveyDay7: - if shouldSurveyDay7BeVisible { + case .surveyDay14: + if shouldSurveyDay14BeVisible { features.append(feature) } case .networkProtectionRemoteMessage, .dataBrokerProtectionRemoteMessage, .dataBrokerProtectionWaitlistInvited: @@ -355,22 +365,39 @@ extension HomePage.Models { let oneDayAgo = Calendar.current.date(byAdding: .weekday, value: -1, to: Date())! return isDay0SurveyEnabled && shouldShowSurveyDay0 && - !userInteractedWithSurveyDay0 && - firstLaunchDate > oneDayAgo + firstLaunchDate >= oneDayAgo && + Bundle.main.preferredLocalizations.first == "en" && + isPartOfSurveyDay0On10Percent ?? calculateIfIn10percent(day: .day0) } - private var shouldSurveyDay7BeVisible: Bool { - let oneWeekAgo = Calendar.current.date(byAdding: .weekOfYear, value: -1, to: Date())! - return isDay7SurveyEnabled && + private var shouldSurveyDay14BeVisible: Bool { + let fourteenDaysAgo = Calendar.current.date(byAdding: .weekday, value: -14, to: Date())! + let fifteenDaysAgo = Calendar.current.date(byAdding: .weekday, value: -15, to: Date())! + return isDay14SurveyEnabled && shouldShowSurveyDay0 && - shouldShowSurveyDay7 && + shouldShowSurveyDay14 && !userInteractedWithSurveyDay0 && - firstLaunchDate <= oneWeekAgo + firstLaunchDate >= fifteenDaysAgo && + firstLaunchDate <= fourteenDaysAgo && + Bundle.main.preferredLocalizations.first == "en" && + isPartOfSurveyDay14On10Percent ?? calculateIfIn10percent(day: .day14) + } + + private func calculateIfIn10percent(day: SurveyDay) -> Bool { + let randomNumber0To99 = randomNumberGenerator.random(in: 0..<100) + let isInSurvey10Percent = randomNumber0To99 < 10 + switch day { + case .day0: + isPartOfSurveyDay0On10Percent = isInSurvey10Percent + case .day14: + isPartOfSurveyDay14On10Percent = isInSurvey10Percent + } + return isInSurvey10Percent } private enum SurveyDay { case day0 - case day7 + case day14 } @MainActor private func visitSurvey(day: SurveyDay) { @@ -378,11 +405,8 @@ extension HomePage.Models { switch day { case .day0: surveyURLString = day0SurveyURL - case .day7: - surveyURLString = day7SurveyURL - } - if let atb = statisticsStore.atb { - surveyURLString += "&atb=\(atb)" + case .day14: + surveyURLString = day14SurveyURL } if let url = URL(string: surveyURLString) { @@ -390,9 +414,9 @@ extension HomePage.Models { tabCollectionViewModel.append(tab: tab) switch day { case .day0: - userInteractedWithSurveyDay0 = true - case .day7: - shouldShowSurveyDay7 = false + shouldShowSurveyDay0 = false + case .day14: + shouldShowSurveyDay14 = false } } } @@ -457,7 +481,7 @@ extension HomePage.Models { // We ignore the `networkProtectionRemoteMessage` case here to avoid it getting accidentally included - it has special handling and will get // included elsewhere. static var allCases: [HomePage.Models.FeatureType] { - [.duckplayer, .emailProtection, .defaultBrowser, .importBookmarksAndPasswords, .surveyDay0, .surveyDay7] + [.duckplayer, .emailProtection, .defaultBrowser, .importBookmarksAndPasswords, .surveyDay0, .surveyDay14] } case duckplayer @@ -465,7 +489,7 @@ extension HomePage.Models { case defaultBrowser case importBookmarksAndPasswords case surveyDay0 - case surveyDay7 + case surveyDay14 case networkProtectionRemoteMessage(NetworkProtectionRemoteMessage) case dataBrokerProtectionRemoteMessage(DataBrokerProtectionRemoteMessage) case dataBrokerProtectionWaitlistInvited @@ -482,8 +506,8 @@ extension HomePage.Models { return UserText.newTabSetUpEmailProtectionCardTitle case .surveyDay0: return UserText.newTabSetUpSurveyDay0CardTitle - case .surveyDay7: - return UserText.newTabSetUpSurveyDay7CardTitle + case .surveyDay14: + return UserText.newTabSetUpSurveyDay14CardTitle case .networkProtectionRemoteMessage(let message): return message.cardTitle case .dataBrokerProtectionRemoteMessage(let message): @@ -505,8 +529,8 @@ extension HomePage.Models { return UserText.newTabSetUpEmailProtectionSummary case .surveyDay0: return UserText.newTabSetUpSurveyDay0Summary - case .surveyDay7: - return UserText.newTabSetUpSurveyDay7Summary + case .surveyDay14: + return UserText.newTabSetUpSurveyDay14Summary case .networkProtectionRemoteMessage(let message): return message.cardDescription case .dataBrokerProtectionRemoteMessage(let message): @@ -528,8 +552,8 @@ extension HomePage.Models { return UserText.newTabSetUpEmailProtectionAction case .surveyDay0: return UserText.newTabSetUpSurveyDay0Action - case .surveyDay7: - return UserText.newTabSetUpSurveyDay7Action + case .surveyDay14: + return UserText.newTabSetUpSurveyDay14Action case .networkProtectionRemoteMessage(let message): return message.action.actionTitle case .dataBrokerProtectionRemoteMessage(let message): @@ -552,9 +576,9 @@ extension HomePage.Models { case .emailProtection: return .inbox128.resized(to: iconSize)! case .surveyDay0: - return .survey128.resized(to: iconSize)! - case .surveyDay7: - return .survey128.resized(to: iconSize)! + return .qandA128.resized(to: iconSize)! + case .surveyDay14: + return .qandA128.resized(to: iconSize)! case .networkProtectionRemoteMessage: return .vpnEnded.resized(to: iconSize)! case .dataBrokerProtectionRemoteMessage: @@ -617,3 +641,13 @@ struct HomePageRemoteMessaging { #endif } + +public protocol RandomNumberGenerating { + func random(in range: Range) -> Int +} + +struct RandomNumberGenerator: RandomNumberGenerating { + func random(in range: Range) -> Int { + return Int.random(in: range) + } +} diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 9347c6977d..a00894c192 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -29509,120 +29509,120 @@ } }, "newTab.setup.survey.day.0.action" : { - "comment" : "Action title of the Day 0 durvey of the Set Up section in the home page", + "comment" : "Action title of the Day 0 survey of the Set Up section in the home page", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Teile deine Gedanken" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "Share Your Thoughts" + "value" : "Sign Up To Participate" } }, "es" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Comparte tus ideas" } }, "fr" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Partagez votre avis" } }, "it" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Comunicaci la tua opinione" } }, "nl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Deel je gedachten" } }, "pl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Podziel się przemyśleniami" } }, "pt" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Partilha as tuas opiniões" } }, "ru" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Поделиться соображениями" } } } }, "newTab.setup.survey.day.0.summary" : { - "comment" : "Summary of the Day 0 durvey of the Set Up section in the home page", + "comment" : "Summary of the card on the new tab page that invites users to partecipate to a survey", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Nimm an unserer kurzen Umfrage teil und hilf uns, den besten Browser zu entwickeln." } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "Take our short survey and help us build the best browser." + "value" : "Join an interview with a member of our research team to help us build the best browser." } }, "es" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Responde a nuestra breve encuesta y ayúdanos a crear el mejor navegador." } }, "fr" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Répondez à notre courte enquête et aidez-nous à créer le meilleur navigateur." } }, "it" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Rispondi al nostro breve sondaggio e aiutaci a creare il browser migliore." } }, "nl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Vul onze korte enquête in en help ons de beste browser te bouwen." } }, "pl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Weź udział w krótkiej ankiecie i pomóż nam opracować najlepszą przeglądarkę." } }, "pt" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Responde ao nosso curto inquérito e ajuda-nos a criar o melhor navegador." } }, "ru" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Пройдите короткий опрос и помогите DuckDuckGo стать лучшим из браузеров." } } @@ -29634,55 +29634,55 @@ "localizations" : { "de" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Sag uns, was dich hierher gebracht hat" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "Tell Us What Brought You Here" + "value" : "Share Your Thoughts With Us" } }, "es" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Cuéntanos qué te ha traído hasta aquí" } }, "fr" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Dites-nous ce qui vous amène ici" } }, "it" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Raccontaci cosa ti ha portato qui" } }, "nl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Vertel ons wat je hier heeft gebracht" } }, "pl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Powiedz nam, co Cię tu sprowadziło" } }, "pt" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Diz-nos o que te trouxe aqui" } }, "ru" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Что привело вас к нам" } } @@ -29690,7 +29690,7 @@ }, "newTab.setup.survey.day.7.action" : { "comment" : "Action title of the Day 7 durvey of the Set Up section in the home page", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -29750,7 +29750,7 @@ }, "newTab.setup.survey.day.7.summary" : { "comment" : "Summary of the Day 7 durvey of the Set Up section in the home page", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -29810,7 +29810,7 @@ }, "newTab.setup.survey.day.7.title" : { "comment" : "Title of the Day 7 durvey of the Set Up section in the home page", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -29868,6 +29868,42 @@ } } }, + "newTab.setup.survey.day.14.action" : { + "comment" : "Action title of the Day 14 survey of the Set Up section in the home page", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Sign Up To Participate" + } + } + } + }, + "newTab.setup.survey.day.14.summary" : { + "comment" : "Summary of the card on the new tab page that invites users to partecipate to a survey", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Join an interview with a member of our research team to help us build the best browser." + } + } + } + }, + "newTab.setup.survey.day.14.title" : { + "comment" : "Title of the Day 14 durvey of the Set Up section in the home page", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Share Your Thoughts With Us" + } + } + } + }, "next" : { "comment" : "Next button", "extractionState" : "extracted_with_value", @@ -50524,4 +50560,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/DuckDuckGo/Menus/MainMenu.swift b/DuckDuckGo/Menus/MainMenu.swift index da7c0d9984..6c881a0dad 100644 --- a/DuckDuckGo/Menus/MainMenu.swift +++ b/DuckDuckGo/Menus/MainMenu.swift @@ -575,11 +575,14 @@ import SubscriptionUI NSMenuItem(title: "Reset Pinned Tabs", action: #selector(MainViewController.resetPinnedTabs)) NSMenuItem(title: "Reset YouTube Overlay Interactions", action: #selector(MainViewController.resetDuckPlayerOverlayInteractions)) NSMenuItem(title: "Reset MakeDuckDuckYours user settings", action: #selector(MainViewController.resetMakeDuckDuckGoYoursUserSettings)) + NSMenuItem(title: "Survey 10% on", action: #selector(MainViewController.in10PercentSurveyOn)) + NSMenuItem(title: "Survey 10% off", action: #selector(MainViewController.in10PercentSurveyOff)) NSMenuItem(title: "Change Activation Date") { NSMenuItem(title: "Today", action: #selector(MainViewController.changeInstallDateToToday), keyEquivalent: "N") - NSMenuItem(title: "Less Than a 21 days Ago", action: #selector(MainViewController.changeInstallDateToLessThan21DaysAgo)) - NSMenuItem(title: "More Than 21 Days Ago", action: #selector(MainViewController.changeInstallDateToMoreThan21DaysAgoButLessThan27)) - NSMenuItem(title: "More Than 27 Days Ago", action: #selector(MainViewController.changeInstallDateToMoreThan27DaysAgo)) + NSMenuItem(title: "Less Than a 1 days Ago", action: #selector(MainViewController.changeInstallDateToLessThan1DayAgo(_:))) + NSMenuItem(title: "More Than 1 Days Ago", action: #selector(MainViewController.changeInstallDateToMoreThan1DayAgoButLessThan14(_:))) + NSMenuItem(title: "More Than 14 Days Ago", action: #selector(MainViewController.changeInstallDateToMoreThan14DaysAgoButLessThan15(_:))) + NSMenuItem(title: "More Than 15 Days Ago", action: #selector(MainViewController.changeInstallDateToMoreThan15DaysAgo(_:))) } NSMenuItem(title: "Reset Email Protection InContext Signup Prompt", action: #selector(MainViewController.resetEmailProtectionInContextPrompt)) NSMenuItem(title: "Reset Daily Pixels", action: #selector(MainViewController.resetDailyPixels)) diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index 2f4c286472..f3f9c43c19 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -708,7 +708,7 @@ extension MainViewController { UserDefaults.standard.set(true, forKey: UserDefaultsWrapper.Key.homePageShowDuckPlayer.rawValue) UserDefaults.standard.set(true, forKey: UserDefaultsWrapper.Key.homePageShowEmailProtection.rawValue) UserDefaults.standard.set(true, forKey: UserDefaultsWrapper.Key.homePageShowSurveyDay0.rawValue) - UserDefaults.standard.set(true, forKey: UserDefaultsWrapper.Key.homePageShowSurveyDay7.rawValue) + UserDefaults.standard.set(true, forKey: UserDefaultsWrapper.Key.homePageShowSurveyDay14.rawValue) UserDefaults.standard.set(false, forKey: UserDefaultsWrapper.Key.homePageUserInteractedWithSurveyDay0.rawValue) } @@ -722,22 +722,37 @@ extension MainViewController { UserDefaults.standard.removePersistentDomain(forName: DailyPixel.Constant.dailyPixelStorageIdentifier) } + @objc func in10PercentSurveyOn(_ sender: Any?) { + UserDefaults.standard.set(true, forKey: UserDefaultsWrapper.Key.homePageShowSurveyDay14in10Percent.rawValue) + UserDefaults.standard.set(true, forKey: UserDefaultsWrapper.Key.homePageShowSurveyDay0in10Percent.rawValue) + } + + @objc func in10PercentSurveyOff(_ sender: Any?) { + UserDefaults.standard.set(false, forKey: UserDefaultsWrapper.Key.homePageShowSurveyDay14in10Percent.rawValue) + UserDefaults.standard.set(false, forKey: UserDefaultsWrapper.Key.homePageShowSurveyDay0in10Percent.rawValue) + } + @objc func changeInstallDateToToday(_ sender: Any?) { UserDefaults.standard.set(Date(), forKey: UserDefaultsWrapper.Key.firstLaunchDate.rawValue) } - @objc func changeInstallDateToLessThan21DaysAgo(_ sender: Any?) { - let lessThanTwentyOneDaysAgo = Calendar.current.date(byAdding: .day, value: -20, to: Date()) - UserDefaults.standard.set(lessThanTwentyOneDaysAgo, forKey: UserDefaultsWrapper.Key.firstLaunchDate.rawValue) + @objc func changeInstallDateToLessThan1DayAgo(_ sender: Any?) { + let lessThanOneDaysAgo = Calendar.current.date(byAdding: .hour, value: -23, to: Date()) + UserDefaults.standard.set(lessThanOneDaysAgo, forKey: UserDefaultsWrapper.Key.firstLaunchDate.rawValue) } - @objc func changeInstallDateToMoreThan21DaysAgoButLessThan27(_ sender: Any?) { - let twentyOneDaysAgo = Calendar.current.date(byAdding: .day, value: -21, to: Date()) - UserDefaults.standard.set(twentyOneDaysAgo, forKey: UserDefaultsWrapper.Key.firstLaunchDate.rawValue) + @objc func changeInstallDateToMoreThan1DayAgoButLessThan14(_ sender: Any?) { + let between1And4DaysAgo = Calendar.current.date(byAdding: .day, value: -13, to: Date()) + UserDefaults.standard.set(between1And4DaysAgo, forKey: UserDefaultsWrapper.Key.firstLaunchDate.rawValue) + } + + @objc func changeInstallDateToMoreThan14DaysAgoButLessThan15(_ sender: Any?) { + let twentyEightDaysAgo = Calendar.current.date(byAdding: .day, value: -14, to: Date()) + UserDefaults.standard.set(twentyEightDaysAgo, forKey: UserDefaultsWrapper.Key.firstLaunchDate.rawValue) } - @objc func changeInstallDateToMoreThan27DaysAgo(_ sender: Any?) { - let twentyEightDaysAgo = Calendar.current.date(byAdding: .day, value: -28, to: Date()) + @objc func changeInstallDateToMoreThan15DaysAgo(_ sender: Any?) { + let twentyEightDaysAgo = Calendar.current.date(byAdding: .day, value: -16, to: Date()) UserDefaults.standard.set(twentyEightDaysAgo, forKey: UserDefaultsWrapper.Key.firstLaunchDate.rawValue) } diff --git a/DuckDuckGo/Preferences/Model/AppearancePreferences.swift b/DuckDuckGo/Preferences/Model/AppearancePreferences.swift index 28b4c55576..ab73548d08 100644 --- a/DuckDuckGo/Preferences/Model/AppearancePreferences.swift +++ b/DuckDuckGo/Preferences/Model/AppearancePreferences.swift @@ -248,8 +248,10 @@ final class AppearancePreferences: ObservableObject { } var isContinueSetUpAvailable: Bool { + let osVersion = ProcessInfo.processInfo.operatingSystemVersion + let privacyConfig = AppPrivacyFeatures.shared.contentBlocking.privacyConfigurationManager.privacyConfig - return privacyConfig.isEnabled(featureKey: .newTabContinueSetUp) + return privacyConfig.isEnabled(featureKey: .newTabContinueSetUp) && osVersion.majorVersion >= 12 } func updateUserInterfaceStyle() { diff --git a/UnitTests/HomePage/ContinueSetUpModelTests.swift b/UnitTests/HomePage/ContinueSetUpModelTests.swift index e9411db6e1..817fc5bfb0 100644 --- a/UnitTests/HomePage/ContinueSetUpModelTests.swift +++ b/UnitTests/HomePage/ContinueSetUpModelTests.swift @@ -71,12 +71,16 @@ final class ContinueSetUpModelTests: XCTestCase { var privacyPreferences: PrivacySecurityPreferences! var duckPlayerPreferences: DuckPlayerPreferencesPersistor! var privacyConfigManager: MockPrivacyConfigurationManager! + var randomNumberGenerator: MockRandomNumberGenerator! let userDefaults = UserDefaults(suiteName: "\(Bundle.main.bundleIdentifier!).\(NSApplication.runType)")! @MainActor override func setUp() { UserDefaultsWrapper.clearAll() userDefaults.set(Date(), forKey: UserDefaultsWrapper.Key.firstLaunchDate.rawValue) userDefaults.set(false, forKey: UserDefaultsWrapper.Key.homePageUserInteractedWithSurveyDay0.rawValue) + userDefaults.set(nil, forKey: UserDefaultsWrapper.Key.homePageShowSurveyDay0in10Percent.rawValue) + userDefaults.set(nil, forKey: UserDefaultsWrapper.Key.homePageShowSurveyDay14in10Percent.rawValue) + userDefaults.set(false, forKey: UserDefaultsWrapper.Key.homePageUserInteractedWithSurveyDay0.rawValue) capturingDefaultBrowserProvider = CapturingDefaultBrowserProvider() capturingDataImportProvider = CapturingDataImportProvider() tabCollectionVM = TabCollectionViewModel() @@ -88,9 +92,10 @@ final class ContinueSetUpModelTests: XCTestCase { let config = MockPrivacyConfiguration() config.featureSettings = [ "surveyCardDay0": "enabled", - "surveyCardDay7": "enabled" + "surveyCardDay14": "enabled" ] as! [String: String] privacyConfigManager.privacyConfig = config + randomNumberGenerator = MockRandomNumberGenerator() #if NETWORK_PROTECTION && DBP let messaging = HomePageRemoteMessaging( @@ -121,7 +126,8 @@ final class ContinueSetUpModelTests: XCTestCase { privacyPreferences: privacyPreferences, duckPlayerPreferences: duckPlayerPreferences, homePageRemoteMessaging: messaging, - privacyConfigurationManager: privacyConfigManager + privacyConfigurationManager: privacyConfigManager, + randomNumberGenerator: randomNumberGenerator ) } @@ -178,11 +184,42 @@ final class ContinueSetUpModelTests: XCTestCase { vm.shouldShowAllFeatures = true - expectedMatrix = expectedFeatureMatrixWithout(types: [.surveyDay7]) + expectedMatrix = expectedFeatureMatrixWithout(types: [.surveyDay14]) XCTAssertEqual(vm.visibleFeaturesMatrix, expectedMatrix) } + @MainActor func testWhenInstallDateIsLessThanADayAgoThenRandomGeneratorIsRequestARandomNumberInTheCorrectRange() { + XCTAssertEqual(randomNumberGenerator.capturedRange, 0..<100) + } + + @MainActor func WhenInstallDateIsMoreThan14daysAgoDay14ThenRandomGeneratorIsRequestARandomNumberInTheCorrectRange() { + let twoWeeksAgo = Calendar.current.date(byAdding: .day, value: -16, to: Date())! + userDefaults.set(twoWeeksAgo, forKey: UserDefaultsWrapper.Key.firstLaunchDate.rawValue) + userDefaults.set(false, forKey: UserDefaultsWrapper.Key.homePageUserInteractedWithSurveyDay0.rawValue) + vm = HomePage.Models.ContinueSetUpModel.fixture(appGroupUserDefaults: userDefaults) + + XCTAssertEqual(randomNumberGenerator.capturedRange, 0..<100) + } + + @MainActor func testWhenInstallDateIsLessThanADayAgoButUserNotIn10PercentNoSurveyCardIsShown() { + let aDayAgo = Calendar.current.date(byAdding: .day, value: -1, to: Date())! + userDefaults.set(aDayAgo, forKey: UserDefaultsWrapper.Key.firstLaunchDate.rawValue) + var randomGenerator = MockRandomNumberGenerator() + randomGenerator.numberToReturn = 10 + vm = HomePage.Models.ContinueSetUpModel.fixture(appGroupUserDefaults: userDefaults, randomNumberGenerator: randomGenerator) + vm.shouldShowAllFeatures = true + + XCTAssertFalse(vm.visibleFeaturesMatrix.reduce([], +).contains(HomePage.Models.FeatureType.surveyDay0)) + XCTAssertFalse(vm.visibleFeaturesMatrix.reduce([], +).contains(HomePage.Models.FeatureType.surveyDay14)) + + userDefaults.set(aDayAgo, forKey: UserDefaultsWrapper.Key.firstLaunchDate.rawValue) + vm = HomePage.Models.ContinueSetUpModel.fixture(appGroupUserDefaults: userDefaults) + + XCTAssertFalse(vm.visibleFeaturesMatrix.reduce([], +).contains(HomePage.Models.FeatureType.surveyDay0)) + XCTAssertFalse(vm.visibleFeaturesMatrix.reduce([], +).contains(HomePage.Models.FeatureType.surveyDay14)) + } + @MainActor func testWhenInstallDateIsMoreThanADayAgoButLessThanAWeekAgoNoSurveyCardIsShown() { let aDayAgo = Calendar.current.date(byAdding: .day, value: -1, to: Date())! userDefaults.set(aDayAgo, forKey: UserDefaultsWrapper.Key.firstLaunchDate.rawValue) @@ -190,49 +227,78 @@ final class ContinueSetUpModelTests: XCTestCase { vm.shouldShowAllFeatures = true XCTAssertFalse(vm.visibleFeaturesMatrix.reduce([], +).contains(HomePage.Models.FeatureType.surveyDay0)) - XCTAssertFalse(vm.visibleFeaturesMatrix.reduce([], +).contains(HomePage.Models.FeatureType.surveyDay7)) + XCTAssertFalse(vm.visibleFeaturesMatrix.reduce([], +).contains(HomePage.Models.FeatureType.surveyDay14)) userDefaults.set(aDayAgo, forKey: UserDefaultsWrapper.Key.firstLaunchDate.rawValue) vm = HomePage.Models.ContinueSetUpModel.fixture(appGroupUserDefaults: userDefaults) XCTAssertFalse(vm.visibleFeaturesMatrix.reduce([], +).contains(HomePage.Models.FeatureType.surveyDay0)) - XCTAssertFalse(vm.visibleFeaturesMatrix.reduce([], +).contains(HomePage.Models.FeatureType.surveyDay7)) + XCTAssertFalse(vm.visibleFeaturesMatrix.reduce([], +).contains(HomePage.Models.FeatureType.surveyDay14)) } - @MainActor func testWhenInstallDateIsMoreThanAWeekAgoDay7SurveyCardIsShown() { - let aDayAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date())! - userDefaults.set(aDayAgo, forKey: UserDefaultsWrapper.Key.firstLaunchDate.rawValue) + @MainActor func testWhenInstallDateIsMoreThan14daysAgoDay14SurveyCardIsShown() { + let twoWeeksAgo = Calendar.current.date(byAdding: .day, value: -14, to: Date())! + userDefaults.set(twoWeeksAgo, forKey: UserDefaultsWrapper.Key.firstLaunchDate.rawValue) + userDefaults.set(false, forKey: UserDefaultsWrapper.Key.homePageUserInteractedWithSurveyDay0.rawValue) vm = HomePage.Models.ContinueSetUpModel.fixture(appGroupUserDefaults: userDefaults) vm.shouldShowAllFeatures = true XCTAssertFalse(vm.visibleFeaturesMatrix.reduce([], +).contains(HomePage.Models.FeatureType.surveyDay0)) - XCTAssertTrue(vm.visibleFeaturesMatrix.reduce([], +).contains(HomePage.Models.FeatureType.surveyDay7)) + XCTAssertTrue(vm.visibleFeaturesMatrix.reduce([], +).contains(HomePage.Models.FeatureType.surveyDay14)) } - @MainActor func testWhenInstallDateIsMoreThanAWeekAgoAndUserInteractedWithDay0SurveyDay7SurveyCardIsNotShown() { + @MainActor func testWhenInstallDateIsMoreThan14daysAgoDay14ButUserNotIn10percentSurveyCardIsNotShown() { + let twoWeeksAgo = Calendar.current.date(byAdding: .day, value: -14, to: Date())! + userDefaults.set(twoWeeksAgo, forKey: UserDefaultsWrapper.Key.firstLaunchDate.rawValue) + userDefaults.set(nil, forKey: UserDefaultsWrapper.Key.homePageShowSurveyDay14in10Percent.rawValue) + userDefaults.set(false, forKey: UserDefaultsWrapper.Key.homePageUserInteractedWithSurveyDay0.rawValue) + + let randomGenerator = MockRandomNumberGenerator() + randomGenerator.numberToReturn = 10 + vm = HomePage.Models.ContinueSetUpModel.fixture(appGroupUserDefaults: userDefaults, randomNumberGenerator: randomGenerator) + vm.shouldShowAllFeatures = true + + XCTAssertFalse(vm.visibleFeaturesMatrix.reduce([], +).contains(HomePage.Models.FeatureType.surveyDay0)) + XCTAssertFalse(vm.visibleFeaturesMatrix.reduce([], +).contains(HomePage.Models.FeatureType.surveyDay14)) + } + + @MainActor func testWhenInstallDateIsMoreThan15daysAgoDay14SurveyCardIsShown() { + let twoWeeksAgo = Calendar.current.date(byAdding: .day, value: -16, to: Date())! + userDefaults.set(twoWeeksAgo, forKey: UserDefaultsWrapper.Key.firstLaunchDate.rawValue) + userDefaults.set(false, forKey: UserDefaultsWrapper.Key.homePageUserInteractedWithSurveyDay0.rawValue) + vm = HomePage.Models.ContinueSetUpModel.fixture(appGroupUserDefaults: userDefaults) + vm.shouldShowAllFeatures = true + + XCTAssertFalse(vm.visibleFeaturesMatrix.reduce([], +).contains(HomePage.Models.FeatureType.surveyDay0)) + XCTAssertFalse(vm.visibleFeaturesMatrix.reduce([], +).contains(HomePage.Models.FeatureType.surveyDay14)) + } + + @MainActor func testWhenInstallDateIsMoreThan14daysAgoAndUserInteractedWithDay0SurveyDay14SurveyCardIsNotShown() { let statisticStore = MockStatisticsStore() vm.statisticsStore = statisticStore vm.performAction(for: .surveyDay0) - let aDayAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date())! + let aDayAgo = Calendar.current.date(byAdding: .day, value: -14, to: Date())! userDefaults.set(aDayAgo, forKey: UserDefaultsWrapper.Key.firstLaunchDate.rawValue) + userDefaults.set(false, forKey: UserDefaultsWrapper.Key.homePageUserInteractedWithSurveyDay0.rawValue) vm = HomePage.Models.ContinueSetUpModel.fixture(appGroupUserDefaults: userDefaults) vm.shouldShowAllFeatures = true XCTAssertFalse(vm.visibleFeaturesMatrix.reduce([], +).contains(HomePage.Models.FeatureType.surveyDay0)) - XCTAssertFalse(vm.visibleFeaturesMatrix.reduce([], +).contains(HomePage.Models.FeatureType.surveyDay7)) + XCTAssertFalse(vm.visibleFeaturesMatrix.reduce([], +).contains(HomePage.Models.FeatureType.surveyDay14)) } - @MainActor func testWhenInstallDateIsMoreThanAWeekAgoAndUserDismissedDay0SurveyDay7SurveyCardIsNotShown() { + @MainActor func testWhenInstallDateIsMoreThanAWeekAgoAndUserDismissedDay0SurveyDay14SurveyCardIsNotShown() { let statisticStore = MockStatisticsStore() vm.statisticsStore = statisticStore vm.removeItem(for: .surveyDay0) - let aDayAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date())! + let aDayAgo = Calendar.current.date(byAdding: .day, value: -14, to: Date())! userDefaults.set(aDayAgo, forKey: UserDefaultsWrapper.Key.firstLaunchDate.rawValue) + userDefaults.set(false, forKey: UserDefaultsWrapper.Key.homePageUserInteractedWithSurveyDay0.rawValue) vm = HomePage.Models.ContinueSetUpModel.fixture(appGroupUserDefaults: userDefaults) vm.shouldShowAllFeatures = true XCTAssertFalse(vm.visibleFeaturesMatrix.reduce([], +).contains(HomePage.Models.FeatureType.surveyDay0)) - XCTAssertFalse(vm.visibleFeaturesMatrix.reduce([], +).contains(HomePage.Models.FeatureType.surveyDay7)) + XCTAssertFalse(vm.visibleFeaturesMatrix.reduce([], +).contains(HomePage.Models.FeatureType.surveyDay14)) } @MainActor func testWhenInitializedNotForTheFirstTimeTheMatrixHasAllElementsInTheRightOrder() { @@ -247,7 +313,7 @@ final class ContinueSetUpModelTests: XCTestCase { } func testWhenTogglingShowAllFeatureThenCorrectElementsAreVisible() { - let expectedMatrix = expectedFeatureMatrixWithout(types: [.surveyDay7]) + let expectedMatrix = expectedFeatureMatrixWithout(types: [.surveyDay14]) vm.shouldShowAllFeatures = true @@ -275,7 +341,7 @@ final class ContinueSetUpModelTests: XCTestCase { } @MainActor func testWhenIsDefaultBrowserAndTogglingShowAllFeatureThenCorrectElementsAreVisible() { - let expectedMatrix = expectedFeatureMatrixWithout(types: [.defaultBrowser, .surveyDay7]) + let expectedMatrix = expectedFeatureMatrixWithout(types: [.defaultBrowser, .surveyDay14]) capturingDefaultBrowserProvider.isDefault = true vm = HomePage.Models.ContinueSetUpModel.fixture(defaultBrowserProvider: capturingDefaultBrowserProvider, appGroupUserDefaults: userDefaults) @@ -303,7 +369,7 @@ final class ContinueSetUpModelTests: XCTestCase { } @MainActor func testWhenUserHasUsedImportAndTogglingShowAllFeatureThenCorrectElementsAreVisible() { - let expectedMatrix = expectedFeatureMatrixWithout(types: [.surveyDay7, .importBookmarksAndPasswords]) + let expectedMatrix = expectedFeatureMatrixWithout(types: [.surveyDay14, .importBookmarksAndPasswords]) capturingDataImportProvider.didImport = true vm = HomePage.Models.ContinueSetUpModel.fixture(dataImportProvider: capturingDataImportProvider, appGroupUserDefaults: userDefaults) @@ -325,7 +391,7 @@ final class ContinueSetUpModelTests: XCTestCase { } @MainActor func testWhenUserHasEmailProtectionEnabledThenCorrectElementsAreVisible() { - let expectedMatrix = expectedFeatureMatrixWithout(types: [.surveyDay7, .emailProtection]) + let expectedMatrix = expectedFeatureMatrixWithout(types: [.surveyDay14, .emailProtection]) emailStorage.isEmailProtectionEnabled = true vm = HomePage.Models.ContinueSetUpModel.fixture(emailManager: emailManager, appGroupUserDefaults: userDefaults) @@ -341,7 +407,7 @@ final class ContinueSetUpModelTests: XCTestCase { } @MainActor func testWhenUserHasCookieConsentEnabledThenCorrectElementsAreVisible() { - let expectedMatrix = expectedFeatureMatrixWithout(types: [.surveyDay7]) + let expectedMatrix = expectedFeatureMatrixWithout(types: [.surveyDay14]) privacyPreferences.autoconsentEnabled = true vm = HomePage.Models.ContinueSetUpModel.fixture(privacyPreferences: privacyPreferences, appGroupUserDefaults: userDefaults) @@ -363,7 +429,7 @@ final class ContinueSetUpModelTests: XCTestCase { } @MainActor func testWhenUserHasDuckPlayerEnabledAndOverlayButtonNotPressedThenCorrectElementsAreVisible() { - let expectedMatrix = expectedFeatureMatrixWithout(types: [.surveyDay7, .duckplayer]) + let expectedMatrix = expectedFeatureMatrixWithout(types: [.surveyDay14, .duckplayer]) duckPlayerPreferences.youtubeOverlayAnyButtonPressed = false duckPlayerPreferences.duckPlayerModeBool = true @@ -380,7 +446,7 @@ final class ContinueSetUpModelTests: XCTestCase { } @MainActor func testWhenUserHasDuckPlayerDisabledAndOverlayButtonNotPressedThenCorrectElementsAreVisible() { - let expectedMatrix = expectedFeatureMatrixWithout(types: [.surveyDay7, .duckplayer]) + let expectedMatrix = expectedFeatureMatrixWithout(types: [.surveyDay14, .duckplayer]) duckPlayerPreferences.youtubeOverlayAnyButtonPressed = false duckPlayerPreferences.duckPlayerModeBool = false @@ -397,7 +463,7 @@ final class ContinueSetUpModelTests: XCTestCase { } @MainActor func testWhenUserHasDuckPlayerOnAlwaysAskAndOverlayButtonNotPressedThenCorrectElementsAreVisible() { - let expectedMatrix = expectedFeatureMatrixWithout(types: [.surveyDay7]) + let expectedMatrix = expectedFeatureMatrixWithout(types: [.surveyDay14]) duckPlayerPreferences.youtubeOverlayAnyButtonPressed = false duckPlayerPreferences.duckPlayerModeBool = nil @@ -414,7 +480,7 @@ final class ContinueSetUpModelTests: XCTestCase { } @MainActor func testWhenUserHasDuckPlayerOnAlwaysAskAndOverlayButtonIsPressedThenCorrectElementsAreVisible() { - let expectedMatrix = expectedFeatureMatrixWithout(types: [.surveyDay7, .duckplayer]) + let expectedMatrix = expectedFeatureMatrixWithout(types: [.surveyDay14, .duckplayer]) duckPlayerPreferences.youtubeOverlayAnyButtonPressed = true duckPlayerPreferences.duckPlayerModeBool = nil @@ -437,17 +503,17 @@ final class ContinueSetUpModelTests: XCTestCase { vm.statisticsStore = statisticStore vm.performAction(for: .surveyDay0) - XCTAssertEqual(tabCollectionVM.tabs[1].url, URL(string: vm.day0SurveyURL + "&atb=" + atb)) + XCTAssertEqual(tabCollectionVM.tabs[1].url, URL(string: vm.day0SurveyURL)) } - @MainActor func testWhenAskedToPerformActionForSurveyDay7ShowsTheSurveySite() { + @MainActor func testWhenAskedToPerformActionForSurveyDay14ShowsTheSurveySite() { let atb = "someAtb" let statisticStore = MockStatisticsStore() statisticStore.atb = atb vm.statisticsStore = statisticStore - vm.performAction(for: .surveyDay7) - XCTAssertEqual(tabCollectionVM.tabs[1].url, URL(string: vm.day7SurveyURL + "&atb=" + atb)) + vm.performAction(for: .surveyDay14) + XCTAssertEqual(tabCollectionVM.tabs[1].url, URL(string: vm.day14SurveyURL)) } @MainActor func testThatWhenIfAllFeatureActiveThenVisibleMatrixIsEmpty() { @@ -457,7 +523,7 @@ final class ContinueSetUpModelTests: XCTestCase { duckPlayerPreferences.youtubeOverlayAnyButtonPressed = true capturingDataImportProvider.didImport = true userDefaults.set(false, forKey: UserDefaultsWrapper.Key.homePageShowSurveyDay0.rawValue) - userDefaults.set(false, forKey: UserDefaultsWrapper.Key.homePageShowSurveyDay7.rawValue) + userDefaults.set(false, forKey: UserDefaultsWrapper.Key.homePageShowSurveyDay14.rawValue) vm = HomePage.Models.ContinueSetUpModel( defaultBrowserProvider: capturingDefaultBrowserProvider, @@ -474,7 +540,7 @@ final class ContinueSetUpModelTests: XCTestCase { @MainActor func testDismissedItemsAreRemovedFromVisibleMatrixAndChoicesArePersisted() { vm.shouldShowAllFeatures = true - let expectedMatrix = expectedFeatureMatrixWithout(types: [.surveyDay7]) + let expectedMatrix = expectedFeatureMatrixWithout(types: [.surveyDay14]) XCTAssertEqual(expectedMatrix, vm.visibleFeaturesMatrix) vm.removeItem(for: .surveyDay0) @@ -483,8 +549,8 @@ final class ContinueSetUpModelTests: XCTestCase { userDefaults.set(Calendar.current.date(byAdding: .month, value: -1, to: Date())!, forKey: UserDefaultsWrapper.Key.firstLaunchDate.rawValue) vm = HomePage.Models.ContinueSetUpModel.fixture(appGroupUserDefaults: userDefaults) - vm.removeItem(for: .surveyDay7) - XCTAssertFalse(vm.visibleFeaturesMatrix.flatMap { $0 }.contains(.surveyDay7)) + vm.removeItem(for: .surveyDay14) + XCTAssertFalse(vm.visibleFeaturesMatrix.flatMap { $0 }.contains(.surveyDay14)) vm.removeItem(for: .defaultBrowser) XCTAssertFalse(vm.visibleFeaturesMatrix.flatMap { $0 }.contains(.defaultBrowser)) @@ -564,11 +630,12 @@ extension HomePage.Models.ContinueSetUpModel { privacyPreferences: PrivacySecurityPreferences = PrivacySecurityPreferences.shared, duckPlayerPreferences: DuckPlayerPreferencesPersistor = DuckPlayerPreferencesPersistorMock(), privacyConfig: MockPrivacyConfiguration = MockPrivacyConfiguration(), - appGroupUserDefaults: UserDefaults + appGroupUserDefaults: UserDefaults, + randomNumberGenerator: RandomNumberGenerating = MockRandomNumberGenerator() ) -> HomePage.Models.ContinueSetUpModel { privacyConfig.featureSettings = [ "surveyCardDay0": "enabled", - "surveyCardDay7": "enabled", + "surveyCardDay14": "enabled", "networkProtection": "disabled" ] as! [String: String] let manager = MockPrivacyConfigurationManager() @@ -603,6 +670,16 @@ extension HomePage.Models.ContinueSetUpModel { privacyPreferences: privacyPreferences, duckPlayerPreferences: duckPlayerPreferences, homePageRemoteMessaging: messaging, - privacyConfigurationManager: manager) + privacyConfigurationManager: manager, + randomNumberGenerator: randomNumberGenerator) + } +} + +class MockRandomNumberGenerator: RandomNumberGenerating { + var numberToReturn = 0 + var capturedRange: Range? + func random(in range: Range) -> Int { + capturedRange = range + return numberToReturn } } From b4b968d94d3d3b9f555e959e46e883c84cc11004 Mon Sep 17 00:00:00 2001 From: Anh Do <18567+quanganhdo@users.noreply.github.com> Date: Thu, 14 Mar 2024 10:42:31 -0400 Subject: [PATCH 02/51] NetP x Subscription Clean-up (#2363) Task/Issue URL: https://app.asana.com/0/0/1206470585910126/f Tech Design URL: CC: --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 ++-- .../NetworkProtection+ConvenienceInitializers.swift | 3 ++- .../NetworkExtensionTargets/MacPacketTunnelProvider.swift | 3 ++- DuckDuckGoVPN/NetworkProtectionBouncer.swift | 5 ++++- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 8 files changed, 14 insertions(+), 9 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 90fc764ac7..79f1871b76 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -13749,7 +13749,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 122.2.1; + version = 123.0.0; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index ff344fde70..17ffe51f48 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "1295a22823157c9b7d11793299cd58b189e87629", - "version" : "122.2.1" + "revision" : "d110859c6100daa59bcbc200dd8565975faf2151", + "version" : "123.0.0" } }, { diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift index a32ca7ee85..fcdb50ff75 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift @@ -50,7 +50,8 @@ extension NetworkProtectionKeychainTokenStore { convenience init() { self.init(keychainType: .default, errorEvents: .networkProtectionAppDebugEvents, - isSubscriptionEnabled: false) + isSubscriptionEnabled: false, + accessTokenProvider: { nil }) } } diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift index a0b733539c..d7d6de4627 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift @@ -280,7 +280,8 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { let tokenStore = NetworkProtectionKeychainTokenStore(keychainType: Bundle.keychainType, serviceName: Self.tokenServiceName, errorEvents: debugEvents, - isSubscriptionEnabled: false) + isSubscriptionEnabled: false, + accessTokenProvider: { nil }) let notificationsPresenter = NetworkProtectionNotificationsPresenterFactory().make(settings: settings, defaults: defaults) super.init(notificationsPresenter: notificationsPresenter, diff --git a/DuckDuckGoVPN/NetworkProtectionBouncer.swift b/DuckDuckGoVPN/NetworkProtectionBouncer.swift index b36585dc0c..6b64523c3e 100644 --- a/DuckDuckGoVPN/NetworkProtectionBouncer.swift +++ b/DuckDuckGoVPN/NetworkProtectionBouncer.swift @@ -30,7 +30,10 @@ final class NetworkProtectionBouncer { /// current app. /// func requireAuthTokenOrKillApp() { - let keychainStore = NetworkProtectionKeychainTokenStore(keychainType: .default, errorEvents: nil, isSubscriptionEnabled: false) + let keychainStore = NetworkProtectionKeychainTokenStore(keychainType: .default, + errorEvents: nil, + isSubscriptionEnabled: false, + accessTokenProvider: { nil }) guard keychainStore.isFeatureActivated else { os_log(.error, log: .networkProtection, "🔴 Stopping: DuckDuckGo VPN not authorized.") diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 16230a9a8e..4be565fe0e 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -29,7 +29,7 @@ let package = Package( targets: ["DataBrokerProtection"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "122.2.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "123.0.0"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index edd448f1f8..8899b18238 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "122.2.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "123.0.0"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index e3f0621d70..df6fe2d263 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "122.2.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "123.0.0"), .package(path: "../SwiftUIExtensions") ], targets: [ From ddf8b045556034ee91105af309a4f3016b268bd6 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Thu, 14 Mar 2024 16:49:54 +0100 Subject: [PATCH 03/51] Removing some waitlist code (#2406) Task/Issue URL: https://app.asana.com/0/0/1206800069675135/f ## Description Removing some of our waitlist code so that nothing screams "new" for the VPN. --- .../NavigationBar/View/MoreOptionsMenu.swift | 28 ++----------------- .../NetworkProtectionNavBarButtonModel.swift | 21 -------------- 2 files changed, 3 insertions(+), 46 deletions(-) diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift index 24ecc77e86..6ff8bfea32 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift @@ -339,18 +339,9 @@ final class MoreOptionsMenu: NSMenu { #if NETWORK_PROTECTION if networkProtectionFeatureVisibility.isNetworkProtectionVisible() { - let isWaitlistUser = NetworkProtectionWaitlist().waitlistStorage.isWaitlistUser - let hasAuthToken = NetworkProtectionKeychainTokenStore().isFeatureActivated - let networkProtectionItem: NSMenuItem - // If the user can see the VPN option but they haven't joined the waitlist or don't have an auth token, show the "New" - // badge to bring it to their attention. - if !isWaitlistUser && !hasAuthToken { - networkProtectionItem = makeNetworkProtectionItem(showNewLabel: true) - } else { - networkProtectionItem = makeNetworkProtectionItem(showNewLabel: false) - } + networkProtectionItem = makeNetworkProtectionItem() items.append(networkProtectionItem) @@ -490,25 +481,12 @@ final class MoreOptionsMenu: NSMenu { } #if NETWORK_PROTECTION - private func makeNetworkProtectionItem(showNewLabel: Bool) -> NSMenuItem { + private func makeNetworkProtectionItem() -> NSMenuItem { let networkProtectionItem = NSMenuItem(title: "", action: #selector(showNetworkProtectionStatus(_:)), keyEquivalent: "") .targetting(self) .withImage(.image(for: .vpnIcon)) - if showNewLabel { - let attributedText = NSMutableAttributedString(string: UserText.networkProtection) - attributedText.append(NSAttributedString(string: " ")) - - let imageAttachment = NSTextAttachment() - imageAttachment.image = .newLabel - imageAttachment.setImageHeight(height: 16, offset: .init(x: 0, y: -4)) - - attributedText.append(NSAttributedString(attachment: imageAttachment)) - - networkProtectionItem.attributedTitle = attributedText - } else { - networkProtectionItem.title = UserText.networkProtection - } + networkProtectionItem.title = UserText.networkProtection return networkProtectionItem } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift index c756d18ac6..057f5b622c 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift @@ -113,13 +113,6 @@ final class NetworkProtectionNavBarButtonModel: NSObject, ObservableObject { guard [.normal, .integrationTests].contains(NSApp.runType) else { return NSImage() } #endif - let isWaitlistUser = NetworkProtectionWaitlist().waitlistStorage.isWaitlistUser - let hasAuthToken = NetworkProtectionKeychainTokenStore().isFeatureActivated - - if !isWaitlistUser && !hasAuthToken { - return .networkProtectionAvailableButton - } - if NetworkProtectionWaitlist().readyToAcceptTermsAndConditions { return .networkProtectionAvailableButton } @@ -191,20 +184,6 @@ final class NetworkProtectionNavBarButtonModel: NSObject, ObservableObject { showButton = true return } - - let waitlist = NetworkProtectionWaitlist() - let isWaitlistUser = waitlist.waitlistStorage.isWaitlistUser - let hasAuthToken = NetworkProtectionKeychainTokenStore().isFeatureActivated - -#if !APPSTORE - // If the user hasn't signed up to the waitlist or doesn't have an auth token through some other method, then show them the badged icon - // to get their attention and encourage them to sign up. Also avoid showing the button is the user has opened the waitlist UI but - // dismissed it. - if !isWaitlistUser && !hasAuthToken && !waitlist.waitlistSignUpPromptDismissed { - showButton = true - return - } -#endif } guard !isPinned, From 208d549c63c35f2201fcb4190d46c158e3161222 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Thu, 14 Mar 2024 14:22:25 -0300 Subject: [PATCH 04/51] DBP: Add stage granularity to Pixels (#2365) --- DuckDuckGo/DBP/DBPHomeViewController.swift | 1 + .../DataBrokerRunCustomJSONViewModel.swift | 6 ++ .../Operations/DataBrokerOperation.swift | 21 ++++- .../Operations/OptOutOperation.swift | 1 + .../Pixels/DataBrokerProtectionPixels.swift | 15 +++- ...kerProtectionStageDurationCalculator.swift | 16 +++- .../DataBrokerOperationActionTests.swift | 80 +++++++++++++++++++ ...kerProfileQueryOperationManagerTests.swift | 2 +- .../DataBrokerProtectionTests/Mocks.swift | 71 ++++++++++++++++ 9 files changed, 205 insertions(+), 8 deletions(-) diff --git a/DuckDuckGo/DBP/DBPHomeViewController.swift b/DuckDuckGo/DBP/DBPHomeViewController.swift index e590867c10..b64fc689e8 100644 --- a/DuckDuckGo/DBP/DBPHomeViewController.swift +++ b/DuckDuckGo/DBP/DBPHomeViewController.swift @@ -163,6 +163,7 @@ public class DataBrokerProtectionPixelsHandler: EventMapping Double { + return 0.0 + } + + func durationSinceStartTime() -> Double { + return 0.0 + } + + func fireOptOutStart() { + } + + func fireOptOutEmailGenerate() { + } + + func fireOptOutCaptchaParse() { + } + + func fireOptOutCaptchaSend() { + } + + func fireOptOutCaptchaSolve() { + } + + func fireOptOutSubmit() { + } + + func fireOptOutEmailReceive() { + } + + func fireOptOutEmailConfirm() { + } + + func fireOptOutValidate() { + } + + func fireOptOutSubmitSuccess(tries: Int) { + } + + func fireOptOutFillForm() { + } + + func fireOptOutFailure(tries: Int) { + } + + func fireScanSuccess(matchesFound: Int) { + } + + func fireScanFailed() { + } + + func fireScanError(error: any Error) { + } + + func setStage(_ stage: DataBrokerProtection.Stage) { + self.stage = stage + } + + func setEmailPattern(_ emailPattern: String?) { + } + + func setLastActionId(_ actionID: String) { + } + + func clear() { + self.stage = nil + } +} From adfa4d9cae2640772c4161db8c829443cea91ae5 Mon Sep 17 00:00:00 2001 From: Christopher Brind Date: Thu, 14 Mar 2024 18:14:43 +0000 Subject: [PATCH 05/51] bump bsk to fix retain cycle (#2422) Task/Issue URL: https://app.asana.com/0/414235014887631/1206840485929811/f Tech Design URL: CC: **Description**: Bumps BSK to fix retain cycle **Steps to test this PR**: 1. Launch macOS app --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 373021ff66..e2d8d1c9d2 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -13770,7 +13770,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = "121.0.0-1"; + version = "121.0.0-2"; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3f56380165..550a377509 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "592242549bc2258d5ebeb3224029267915bcca52", - "version" : "121.0.0-1" + "revision" : "d64db9f34a00f54b2a6b518ddd461ad5b28321d9", + "version" : "121.0.0-2" } }, { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index e773b78b41..656e91c4e4 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -29,7 +29,7 @@ let package = Package( targets: ["DataBrokerProtection"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "121.0.0-1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "121.0.0-2"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 8d5e069afb..34c210e4d3 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "121.0.0-1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "121.0.0-2"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 91bad2741b..66efa5c9f6 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "121.0.0-1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "121.0.0-2"), .package(path: "../SwiftUIExtensions") ], targets: [ From c3e59d621c58705a9dee9c236271236c0c6e3ff9 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Thu, 14 Mar 2024 18:29:18 +0000 Subject: [PATCH 06/51] Bump version to 1.79.0 (143) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 73bf1efab5..9a14499d7a 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 142 +CURRENT_PROJECT_VERSION = 143 From 6a7015e221413ad0756908a5f2a303668cb428d8 Mon Sep 17 00:00:00 2001 From: Christopher Brind Date: Thu, 14 Mar 2024 20:04:42 +0000 Subject: [PATCH 07/51] bump bsk to fix retain cycle (#2425) Task/Issue URL: https://app.asana.com/0/414235014887631/1206840485929811/f Tech Design URL: CC: **Description**: Bump BSK to fix retain cycle **Steps to test this PR**: 1. Launch the app --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 79f1871b76..a809af52f6 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -13749,7 +13749,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 123.0.0; + version = 123.0.1; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 17ffe51f48..7f8548488a 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "d110859c6100daa59bcbc200dd8565975faf2151", - "version" : "123.0.0" + "revision" : "838cb53a8f7050d87ae6931b45ce126ece994359", + "version" : "123.0.1" } }, { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 4be565fe0e..8e88a31067 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -29,7 +29,7 @@ let package = Package( targets: ["DataBrokerProtection"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "123.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "123.0.1"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 8899b18238..a5aa5f7c63 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "123.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "123.0.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index df6fe2d263..fb17968848 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "123.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "123.0.1"), .package(path: "../SwiftUIExtensions") ], targets: [ From e1f7ab0ac5ec1380d671663d9d2917a41216fb0c Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Thu, 14 Mar 2024 20:15:43 +0000 Subject: [PATCH 08/51] Update broker json (#2407) Task/Issue URL: https://app.asana.com/0/1199230911884351/1206835317209633/f **Description**: Add unique name for PeopleWhiz child sites --- .../Resources/JSON/people-wizard.com.json | 51 +++++++++++++++++++ .../Resources/JSON/peopleswhizr.com.json | 51 +++++++++++++++++++ .../Resources/JSON/peopleswiz.com.json | 51 +++++++++++++++++++ .../Resources/JSON/peopleswizard.com.json | 51 +++++++++++++++++++ .../Resources/JSON/peoplewhiz.com.json | 32 ++++++------ .../Resources/JSON/peoplewhiz.net.json | 51 +++++++++++++++++++ .../Resources/JSON/peoplewhized.com.json | 51 +++++++++++++++++++ .../Resources/JSON/peoplewhized.net.json | 51 +++++++++++++++++++ .../Resources/JSON/peoplewhizr.com.json | 51 +++++++++++++++++++ .../Resources/JSON/peoplewhizr.net.json | 51 +++++++++++++++++++ .../Resources/JSON/peoplewiz.com.json | 51 +++++++++++++++++++ .../Resources/JSON/peoplewizard.net.json | 51 +++++++++++++++++++ .../Resources/JSON/peoplewizr.com.json | 51 +++++++++++++++++++ 13 files changed, 628 insertions(+), 16 deletions(-) create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/people-wizard.com.json create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peopleswhizr.com.json create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peopleswiz.com.json create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peopleswizard.com.json create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhiz.net.json create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhized.com.json create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhized.net.json create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhizr.com.json create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhizr.net.json create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewiz.com.json create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewizard.net.json create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewizr.com.json diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/people-wizard.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/people-wizard.com.json new file mode 100644 index 0000000000..33b35506fb --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/people-wizard.com.json @@ -0,0 +1,51 @@ +{ + "name": "People-Wizard.com", + "url": "people-wizard.com", + "version": "0.1.6", + "parent": "peoplewhiz.com", + "addedDatetime": 1709445600000, + "steps": [ + { + "stepType": "scan", + "scanType": "templatedUrl", + "actions": [ + { + "actionType": "navigate", + "id": "19d2fe20-8de2-4719-b6af-d6e405544697", + "url": "https://www.people-wizard.com/flow/results/${firstName}/${middleName|defaultIfEmpty:~}/${lastName}/${city}/${state}/${age}" + }, + { + "actionType": "extract", + "id": "1344b079-bb6b-417b-9c4d-eec858914b13", + "selector": "[class^='ResultsTable__Record-sc']", + "profile": { + "name": { + "selector": "[class^='ResultsTable__Name-sc']" + }, + "age": { + "selector": "[class^='ResultsTable__Age-sc']" + }, + "addressCityState": { + "selector": ".MuiGrid-item:nth-child(3) [class^='ResultsTable__AddressAndLocation-sc']", + "findElements": true + }, + "relativesList": { + "selector": ".MuiGrid-item:nth-child(4) [class^='ResultsTable__AddressAndLocation-sc']", + "findElements": true + } + } + } + ] + }, + { + "stepType": "optOut", + "optOutType": "parentSiteOptOut", + "actions": [] + } + ], + "schedulingConfig": { + "retryError": 48, + "confirmOptOutScan": 72, + "maintenanceScan": 240 + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peopleswhizr.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peopleswhizr.com.json new file mode 100644 index 0000000000..c51c669302 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peopleswhizr.com.json @@ -0,0 +1,51 @@ +{ + "name": "PeoplesWhizr", + "url": "peopleswhizr.com", + "version": "0.1.6", + "parent": "peoplewhiz.com", + "addedDatetime": 1709445600000, + "steps": [ + { + "stepType": "scan", + "scanType": "templatedUrl", + "actions": [ + { + "actionType": "navigate", + "id": "e1142da5-5cc5-4038-b23a-c6f13182caf0", + "url": "https://www.peopleswhizr.com/flow/results/${firstName}/${middleName|defaultIfEmpty:~}/${lastName}/${city}/${state}/${age}" + }, + { + "actionType": "extract", + "id": "bc952139-373b-4461-8e24-83661038f657", + "selector": "[class^='ResultsTable__Record-sc']", + "profile": { + "name": { + "selector": "[class^='ResultsTable__Name-sc']" + }, + "age": { + "selector": "[class^='ResultsTable__Age-sc']" + }, + "addressCityState": { + "selector": ".MuiGrid-item:nth-child(3) [class^='ResultsTable__AddressAndLocation-sc']", + "findElements": true + }, + "relativesList": { + "selector": ".MuiGrid-item:nth-child(4) [class^='ResultsTable__AddressAndLocation-sc']", + "findElements": true + } + } + } + ] + }, + { + "stepType": "optOut", + "optOutType": "parentSiteOptOut", + "actions": [] + } + ], + "schedulingConfig": { + "retryError": 48, + "confirmOptOutScan": 72, + "maintenanceScan": 240 + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peopleswiz.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peopleswiz.com.json new file mode 100644 index 0000000000..7c3aa7d2c8 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peopleswiz.com.json @@ -0,0 +1,51 @@ +{ + "name": "PeoplesWiz", + "url": "peopleswiz.com", + "version": "0.1.6", + "parent": "peoplewhiz.com", + "addedDatetime": 1709445600000, + "steps": [ + { + "stepType": "scan", + "scanType": "templatedUrl", + "actions": [ + { + "actionType": "navigate", + "id": "48419647-ba1f-4625-8f9b-6531718c5c12", + "url": "https://www.peopleswiz.com/flow/results/${firstName}/${middleName|defaultIfEmpty:~}/${lastName}/${city}/${state}/${age}" + }, + { + "actionType": "extract", + "id": "9e1e6ff5-cb99-49aa-a37c-b3dc80012f19", + "selector": "[class^='ResultsTable__Record-sc']", + "profile": { + "name": { + "selector": "[class^='ResultsTable__Name-sc']" + }, + "age": { + "selector": "[class^='ResultsTable__Age-sc']" + }, + "addressCityState": { + "selector": ".MuiGrid-item:nth-child(3) [class^='ResultsTable__AddressAndLocation-sc']", + "findElements": true + }, + "relativesList": { + "selector": ".MuiGrid-item:nth-child(4) [class^='ResultsTable__AddressAndLocation-sc']", + "findElements": true + } + } + } + ] + }, + { + "stepType": "optOut", + "optOutType": "parentSiteOptOut", + "actions": [] + } + ], + "schedulingConfig": { + "retryError": 48, + "confirmOptOutScan": 72, + "maintenanceScan": 240 + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peopleswizard.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peopleswizard.com.json new file mode 100644 index 0000000000..9d288252ab --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peopleswizard.com.json @@ -0,0 +1,51 @@ +{ + "name": "PeoplesWizard", + "url": "peopleswizard.com", + "version": "0.1.6", + "parent": "peoplewhiz.com", + "addedDatetime": 1709445600000, + "steps": [ + { + "stepType": "scan", + "scanType": "templatedUrl", + "actions": [ + { + "actionType": "navigate", + "id": "b92d9477-3e4a-4022-be5d-3a5d02a84c90", + "url": "https://www.peopleswizard.com/flow/results/${firstName}/${middleName|defaultIfEmpty:~}/${lastName}/${city}/${state}/${age}" + }, + { + "actionType": "extract", + "id": "c538d04e-85ce-46a5-bbbd-3f9bd0fae078", + "selector": "[class^='ResultsTable__Record-sc']", + "profile": { + "name": { + "selector": "[class^='ResultsTable__Name-sc']" + }, + "age": { + "selector": "[class^='ResultsTable__Age-sc']" + }, + "addressCityState": { + "selector": ".MuiGrid-item:nth-child(3) [class^='ResultsTable__AddressAndLocation-sc']", + "findElements": true + }, + "relativesList": { + "selector": ".MuiGrid-item:nth-child(4) [class^='ResultsTable__AddressAndLocation-sc']", + "findElements": true + } + } + } + ] + }, + { + "stepType": "optOut", + "optOutType": "parentSiteOptOut", + "actions": [] + } + ], + "schedulingConfig": { + "retryError": 48, + "confirmOptOutScan": 72, + "maintenanceScan": 240 + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhiz.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhiz.com.json index 4266d710f9..861ede1a91 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhiz.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhiz.com.json @@ -1,8 +1,8 @@ { - "name": "PeopleWhiz", + "name": "PeopleWhiz.com", "url": "peoplewhiz.com", "version": "0.1.6", - "addedDatetime": 1676181600000, + "addedDatetime": 1676160000000, "steps": [ { "stepType": "scan", @@ -10,12 +10,12 @@ "actions": [ { "actionType": "navigate", - "id": "25f11594-f04a-4d46-a7ef-9fb3ed237c2d", + "id": "581295d7-1fd9-4039-a207-e9b8216b3d0f", "url": "https://www.peoplewhiz.com/flow/results/${firstName}/${middleName|defaultIfEmpty:~}/${lastName}/${city}/${state}/${age}" }, { "actionType": "extract", - "id": "eb93e81d-36d1-4a8a-ad3d-a87ae2018e74", + "id": "efa321f2-f214-411f-a87b-bc42feff7931", "selector": "[class^='ResultsTable__Record-sc']", "profile": { "name": { @@ -42,12 +42,12 @@ "actions": [ { "actionType": "navigate", - "id": "ffb90236-10e7-4969-a305-1eb729a1177d", + "id": "2d9dd816-ec21-4210-91d1-3725837a11b1", "url": "https://www.peoplewhiz.com/optout/results/${firstName}/${middleName|defaultIfEmpty:~}/${lastName}/${city}/${state}/${age}" }, { "actionType": "click", - "id": "4ee5b821-8b26-4a16-928b-5dd20a7292a2", + "id": "327e4f66-a554-4aaa-9a99-f789740112a8", "elements": [ { "selector": "button", @@ -74,7 +74,7 @@ }, { "actionType": "fillForm", - "id": "64f99a6d-f4bb-425e-a430-dded77ed24d0", + "id": "4c088375-cd04-4ce3-ab05-0833b55b82b4", "selector": "form", "elements": [ { @@ -85,7 +85,7 @@ }, { "actionType": "click", - "id": "c39d5b51-794a-4c2f-8acc-ed999b5b2245", + "id": "0d61b830-c740-4d55-a32e-dcc370ff0cdc", "elements": [ { "type": "button", @@ -95,7 +95,7 @@ }, { "actionType": "click", - "id": "9dec9932-00ba-4a7d-85b3-d3d5e5ee7bcc", + "id": "617dd625-36bb-4111-83f2-9e2a94beed7d", "elements": [ { "type": "button", @@ -105,7 +105,7 @@ }, { "actionType": "fillForm", - "id": "00405f21-3fcf-4114-9d56-233abceff54a", + "id": "271cfc3e-deb7-4da5-8e2a-c866d28cde02", "selector": "form", "elements": [ { @@ -116,17 +116,17 @@ }, { "actionType": "getCaptchaInfo", - "id": "4e2b5224-9293-45d0-98fa-2b088079ff83", + "id": "d5316049-1a13-4a51-b5fc-bf1607c0d6e9", "selector": "[class^='Confirm__ReCaptchaContainer-sc']" }, { "actionType": "solveCaptcha", - "id": "006b796c-cf4e-4a7e-a10c-f5c202dfa52e", + "id": "a93d0de2-fc52-4c32-8b7e-74b7a5dc078e", "selector": "[class^='Confirm__ReCaptchaContainer-sc']" }, { "actionType": "click", - "id": "003e51ee-6ccb-49eb-b820-e1af2e6fc407", + "id": "e0b1077d-4a8b-4d30-ba21-550367363d00", "elements": [ { "type": "button", @@ -136,7 +136,7 @@ }, { "actionType": "expectation", - "id": "9866e653-d719-4796-8153-4fa89f673d70", + "id": "5b261491-18ce-476d-9990-4afe2cbadfb3", "expectations": [ { "type": "text", @@ -147,12 +147,12 @@ }, { "actionType": "emailConfirmation", - "id": "efa0d7c1-2081-4bb5-bff9-5d60eef4e8c8", + "id": "86420965-0bd7-49ee-8034-bbea1d34c3d6", "pollingTime": 30 }, { "actionType": "expectation", - "id": "71781ca0-881f-4cc3-b918-0112f952a05e", + "id": "0884ae5d-b57b-4e62-92f5-01f4adf06005", "expectations": [ { "type": "text", diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhiz.net.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhiz.net.json new file mode 100644 index 0000000000..697a7a94e1 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhiz.net.json @@ -0,0 +1,51 @@ +{ + "name": "PeopleWhiz.net", + "url": "peoplewhiz.net", + "version": "0.1.6", + "parent": "peoplewhiz.com", + "addedDatetime": 1709424000000, + "steps": [ + { + "stepType": "scan", + "scanType": "templatedUrl", + "actions": [ + { + "actionType": "navigate", + "id": "72348ba6-cfe1-493e-a6bf-a7b15eefe688", + "url": "https://www.peoplewhiz.net/flow/results/${firstName}/${middleName|defaultIfEmpty:~}/${lastName}/${city}/${state}/${age}" + }, + { + "actionType": "extract", + "id": "9e0d566b-dd31-40a7-9315-ec6fba8d6233", + "selector": "[class^='ResultsTable__Record-sc']", + "profile": { + "name": { + "selector": "[class^='ResultsTable__Name-sc']" + }, + "age": { + "selector": "[class^='ResultsTable__Age-sc']" + }, + "addressCityState": { + "selector": ".MuiGrid-item:nth-child(3) [class^='ResultsTable__AddressAndLocation-sc']", + "findElements": true + }, + "relativesList": { + "selector": ".MuiGrid-item:nth-child(4) [class^='ResultsTable__AddressAndLocation-sc']", + "findElements": true + } + } + } + ] + }, + { + "stepType": "optOut", + "optOutType": "parentSiteOptOut", + "actions": [] + } + ], + "schedulingConfig": { + "retryError": 48, + "confirmOptOutScan": 72, + "maintenanceScan": 240 + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhized.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhized.com.json new file mode 100644 index 0000000000..03a649fb82 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhized.com.json @@ -0,0 +1,51 @@ +{ + "name": "PeopleWhized.com", + "url": "peoplewhized.com", + "version": "0.1.6", + "parent": "peoplewhiz.com", + "addedDatetime": 1709424000000, + "steps": [ + { + "stepType": "scan", + "scanType": "templatedUrl", + "actions": [ + { + "actionType": "navigate", + "id": "d4cae653-4d48-425b-abe1-27c2c36a2503", + "url": "https://www.peoplewhized.com/flow/results/${firstName}/${middleName|defaultIfEmpty:~}/${lastName}/${city}/${state}/${age}" + }, + { + "actionType": "extract", + "id": "77517554-f1e7-4e73-b0d1-92f56a0f247c", + "selector": "[class^='ResultsTable__Record-sc']", + "profile": { + "name": { + "selector": "[class^='ResultsTable__Name-sc']" + }, + "age": { + "selector": "[class^='ResultsTable__Age-sc']" + }, + "addressCityState": { + "selector": ".MuiGrid-item:nth-child(3) [class^='ResultsTable__AddressAndLocation-sc']", + "findElements": true + }, + "relativesList": { + "selector": ".MuiGrid-item:nth-child(4) [class^='ResultsTable__AddressAndLocation-sc']", + "findElements": true + } + } + } + ] + }, + { + "stepType": "optOut", + "optOutType": "parentSiteOptOut", + "actions": [] + } + ], + "schedulingConfig": { + "retryError": 48, + "confirmOptOutScan": 72, + "maintenanceScan": 240 + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhized.net.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhized.net.json new file mode 100644 index 0000000000..7ff6ee6216 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhized.net.json @@ -0,0 +1,51 @@ +{ + "name": "PeopleWhized.net", + "url": "peoplewhized.net", + "version": "0.1.6", + "parent": "peoplewhiz.com", + "addedDatetime": 1709445600000, + "steps": [ + { + "stepType": "scan", + "scanType": "templatedUrl", + "actions": [ + { + "actionType": "navigate", + "id": "3fc4b8b9-b6d0-4700-8855-e3d3a8ee562c", + "url": "https://www.peoplewhized.net/flow/results/${firstName}/${middleName|defaultIfEmpty:~}/${lastName}/${city}/${state}/${age}" + }, + { + "actionType": "extract", + "id": "440406f6-bd8a-4af7-b509-43eb24c79e4c", + "selector": "[class^='ResultsTable__Record-sc']", + "profile": { + "name": { + "selector": "[class^='ResultsTable__Name-sc']" + }, + "age": { + "selector": "[class^='ResultsTable__Age-sc']" + }, + "addressCityState": { + "selector": ".MuiGrid-item:nth-child(3) [class^='ResultsTable__AddressAndLocation-sc']", + "findElements": true + }, + "relativesList": { + "selector": ".MuiGrid-item:nth-child(4) [class^='ResultsTable__AddressAndLocation-sc']", + "findElements": true + } + } + } + ] + }, + { + "stepType": "optOut", + "optOutType": "parentSiteOptOut", + "actions": [] + } + ], + "schedulingConfig": { + "retryError": 48, + "confirmOptOutScan": 72, + "maintenanceScan": 240 + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhizr.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhizr.com.json new file mode 100644 index 0000000000..ff7542026a --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhizr.com.json @@ -0,0 +1,51 @@ +{ + "name": "PeopleWhizr.com", + "url": "peoplewhizr.com", + "version": "0.1.6", + "parent": "peoplewhiz.com", + "addedDatetime": 1709445600000, + "steps": [ + { + "stepType": "scan", + "scanType": "templatedUrl", + "actions": [ + { + "actionType": "navigate", + "id": "846e2915-d3bb-4db2-b212-992063c48254", + "url": "https://www.peoplewhizr.com/flow/results/${firstName}/${middleName|defaultIfEmpty:~}/${lastName}/${city}/${state}/${age}" + }, + { + "actionType": "extract", + "id": "21a783ce-7a7e-4a97-8dc2-ee4c2f3dc63a", + "selector": "[class^='ResultsTable__Record-sc']", + "profile": { + "name": { + "selector": "[class^='ResultsTable__Name-sc']" + }, + "age": { + "selector": "[class^='ResultsTable__Age-sc']" + }, + "addressCityState": { + "selector": ".MuiGrid-item:nth-child(3) [class^='ResultsTable__AddressAndLocation-sc']", + "findElements": true + }, + "relativesList": { + "selector": ".MuiGrid-item:nth-child(4) [class^='ResultsTable__AddressAndLocation-sc']", + "findElements": true + } + } + } + ] + }, + { + "stepType": "optOut", + "optOutType": "parentSiteOptOut", + "actions": [] + } + ], + "schedulingConfig": { + "retryError": 48, + "confirmOptOutScan": 72, + "maintenanceScan": 240 + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhizr.net.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhizr.net.json new file mode 100644 index 0000000000..a4c3be5697 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhizr.net.json @@ -0,0 +1,51 @@ +{ + "name": "PeopleWhizr.net", + "url": "peoplewhizr.net", + "version": "0.1.6", + "parent": "peoplewhiz.com", + "addedDatetime": 1709445600000, + "steps": [ + { + "stepType": "scan", + "scanType": "templatedUrl", + "actions": [ + { + "actionType": "navigate", + "id": "8cbdebad-158e-43ee-90f1-4f555feeef03", + "url": "https://www.peoplewhizr.net/flow/results/${firstName}/${middleName|defaultIfEmpty:~}/${lastName}/${city}/${state}/${age}" + }, + { + "actionType": "extract", + "id": "2955fb45-374e-42f2-a72a-f7a2ffe4ebe5", + "selector": "[class^='ResultsTable__Record-sc']", + "profile": { + "name": { + "selector": "[class^='ResultsTable__Name-sc']" + }, + "age": { + "selector": "[class^='ResultsTable__Age-sc']" + }, + "addressCityState": { + "selector": ".MuiGrid-item:nth-child(3) [class^='ResultsTable__AddressAndLocation-sc']", + "findElements": true + }, + "relativesList": { + "selector": ".MuiGrid-item:nth-child(4) [class^='ResultsTable__AddressAndLocation-sc']", + "findElements": true + } + } + } + ] + }, + { + "stepType": "optOut", + "optOutType": "parentSiteOptOut", + "actions": [] + } + ], + "schedulingConfig": { + "retryError": 48, + "confirmOptOutScan": 72, + "maintenanceScan": 240 + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewiz.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewiz.com.json new file mode 100644 index 0000000000..3c4309d416 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewiz.com.json @@ -0,0 +1,51 @@ +{ + "name": "PeopleWiz", + "url": "peoplewiz.com", + "version": "0.1.6", + "parent": "peoplewhiz.com", + "addedDatetime": 1709445600000, + "steps": [ + { + "stepType": "scan", + "scanType": "templatedUrl", + "actions": [ + { + "actionType": "navigate", + "id": "723b8b9e-6f6d-4180-aaab-639682113875", + "url": "https://www.peoplewiz.com/flow/results/${firstName}/${middleName|defaultIfEmpty:~}/${lastName}/${city}/${state}/${age}" + }, + { + "actionType": "extract", + "id": "36ba376d-03c1-4ecc-92ed-2fe12bd79d6f", + "selector": "[class^='ResultsTable__Record-sc']", + "profile": { + "name": { + "selector": "[class^='ResultsTable__Name-sc']" + }, + "age": { + "selector": "[class^='ResultsTable__Age-sc']" + }, + "addressCityState": { + "selector": ".MuiGrid-item:nth-child(3) [class^='ResultsTable__AddressAndLocation-sc']", + "findElements": true + }, + "relativesList": { + "selector": ".MuiGrid-item:nth-child(4) [class^='ResultsTable__AddressAndLocation-sc']", + "findElements": true + } + } + } + ] + }, + { + "stepType": "optOut", + "optOutType": "parentSiteOptOut", + "actions": [] + } + ], + "schedulingConfig": { + "retryError": 48, + "confirmOptOutScan": 72, + "maintenanceScan": 240 + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewizard.net.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewizard.net.json new file mode 100644 index 0000000000..7715592055 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewizard.net.json @@ -0,0 +1,51 @@ +{ + "name": "PeopleWizard.net", + "url": "peoplewizard.net", + "version": "0.1.6", + "parent": "peoplewhiz.com", + "addedDatetime": 1709445600000, + "steps": [ + { + "stepType": "scan", + "scanType": "templatedUrl", + "actions": [ + { + "actionType": "navigate", + "id": "177e5220-d31b-4405-8331-fe34fdb86266", + "url": "https://www.peoplewizard.net/flow/results/${firstName}/${middleName|defaultIfEmpty:~}/${lastName}/${city}/${state}/${age}" + }, + { + "actionType": "extract", + "id": "f6d41239-9208-419e-b101-c58e52a71d42", + "selector": "[class^='ResultsTable__Record-sc']", + "profile": { + "name": { + "selector": "[class^='ResultsTable__Name-sc']" + }, + "age": { + "selector": "[class^='ResultsTable__Age-sc']" + }, + "addressCityState": { + "selector": ".MuiGrid-item:nth-child(3) [class^='ResultsTable__AddressAndLocation-sc']", + "findElements": true + }, + "relativesList": { + "selector": ".MuiGrid-item:nth-child(4) [class^='ResultsTable__AddressAndLocation-sc']", + "findElements": true + } + } + } + ] + }, + { + "stepType": "optOut", + "optOutType": "parentSiteOptOut", + "actions": [] + } + ], + "schedulingConfig": { + "retryError": 48, + "confirmOptOutScan": 72, + "maintenanceScan": 240 + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewizr.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewizr.com.json new file mode 100644 index 0000000000..db79a59baf --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewizr.com.json @@ -0,0 +1,51 @@ +{ + "name": "PeopleWizr", + "url": "peoplewizr.com", + "version": "0.1.6", + "parent": "peoplewhiz.com", + "addedDatetime": 1709445600000, + "steps": [ + { + "stepType": "scan", + "scanType": "templatedUrl", + "actions": [ + { + "actionType": "navigate", + "id": "20d29fc7-9def-4d28-b457-1784d705ae31", + "url": "https://www.peoplewizr.com/flow/results/${firstName}/${middleName|defaultIfEmpty:~}/${lastName}/${city}/${state}/${age}" + }, + { + "actionType": "extract", + "id": "53c86c5d-3960-44a9-91a3-143b71d1e07f", + "selector": "[class^='ResultsTable__Record-sc']", + "profile": { + "name": { + "selector": "[class^='ResultsTable__Name-sc']" + }, + "age": { + "selector": "[class^='ResultsTable__Age-sc']" + }, + "addressCityState": { + "selector": ".MuiGrid-item:nth-child(3) [class^='ResultsTable__AddressAndLocation-sc']", + "findElements": true + }, + "relativesList": { + "selector": ".MuiGrid-item:nth-child(4) [class^='ResultsTable__AddressAndLocation-sc']", + "findElements": true + } + } + } + ] + }, + { + "stepType": "optOut", + "optOutType": "parentSiteOptOut", + "actions": [] + } + ], + "schedulingConfig": { + "retryError": 48, + "confirmOptOutScan": 72, + "maintenanceScan": 240 + } +} From 664f5d8f84bbfc947fd295d7f57fbb096cbd07e3 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Fri, 15 Mar 2024 03:49:17 +0100 Subject: [PATCH 09/51] Renamed some pixel parameters to be more clear (#2402) Task/Issue URL: https://app.asana.com/0/1199230911884351/1206830283741167/f ## Description I'm standardizing pixel parameters because it seems we were using some parameters incorrectly in PixelKit, or at least not how they're used in the main app or in iOS. - The error localized description is no longer passed as a default error parameter in pixels because it's dangerous for privacy reasons. - `errorDesc` and `underlyingErrorDesc` have been renamed to more accurately describe what they should be used for. --- DuckDuckGo/Statistics/PixelParameters.swift | 4 ++-- .../Pixels/TransparentProxyProviderPixel.swift | 2 +- .../Sources/PixelKit/PixelKit+Parameters.swift | 8 ++------ LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift | 4 +--- .../PixelKit/Sources/PixelKit/PixelKitEvent.swift | 4 ++-- .../PixelKit/Sources/PixelKit/PixelKitEventV2.swift | 3 +-- .../PixelKitTestingUtilities/ValidatePixel.swift | 2 +- .../PixelKitTestingUtilities/XCTestCase+PixelKit.swift | 10 ++-------- 8 files changed, 12 insertions(+), 25 deletions(-) diff --git a/DuckDuckGo/Statistics/PixelParameters.swift b/DuckDuckGo/Statistics/PixelParameters.swift index 12312f60bb..2313656cc7 100644 --- a/DuckDuckGo/Statistics/PixelParameters.swift +++ b/DuckDuckGo/Statistics/PixelParameters.swift @@ -345,11 +345,11 @@ extension Error { let nsError = self as NSError params[PixelKit.Parameters.errorCode] = "\(nsError.code)" - params[PixelKit.Parameters.errorDesc] = nsError.domain + params[PixelKit.Parameters.errorDomain] = nsError.domain if let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] as? NSError { params[PixelKit.Parameters.underlyingErrorCode] = "\(underlyingError.code)" - params[PixelKit.Parameters.underlyingErrorDesc] = underlyingError.domain + params[PixelKit.Parameters.underlyingErrorDomain] = underlyingError.domain } if let sqlErrorCode = nsError.userInfo["SQLiteResultCode"] as? NSNumber { diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyProviderPixel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyProviderPixel.swift index aff7421bea..4e4024d08c 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyProviderPixel.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyProviderPixel.swift @@ -25,7 +25,7 @@ extension TransparentProxyProvider.StartError: ErrorWithPixelParameters { case .failedToUpdateNetworkSettings(let underlyingError): return [ PixelKit.Parameters.underlyingErrorCode: "\((underlyingError as NSError).code)", - PixelKit.Parameters.underlyingErrorDesc: (underlyingError as NSError).domain, + PixelKit.Parameters.underlyingErrorDomain: (underlyingError as NSError).domain, ] default: return [:] diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift index 7a25f133ff..ea08352d29 100644 --- a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift +++ b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift @@ -28,14 +28,12 @@ public extension PixelKit { public static let osMajorVersion = "osMajorVersion" public static let errorCode = "e" - public static let errorDomain = "errorDomain" - public static let errorDesc = "d" + public static let errorDomain = "d" public static let errorCount = "c" public static let errorSource = "error_source" public static let sourceBrowserVersion = "source_browser_version" public static let underlyingErrorCode = "ue" - public static let underlyingErrorDomain = "underlyingErrorDomain" - public static let underlyingErrorDesc = "ud" + public static let underlyingErrorDomain = "ud" public static let underlyingErrorSQLiteCode = "sqlrc" public static let underlyingErrorSQLiteExtendedCode = "sqlerc" @@ -95,12 +93,10 @@ public extension Error { params[PixelKit.Parameters.errorCode] = "\(nsError.code)" params[PixelKit.Parameters.errorDomain] = nsError.domain - params[PixelKit.Parameters.errorDesc] = nsError.localizedDescription if let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] as? NSError { params[PixelKit.Parameters.underlyingErrorCode] = "\(underlyingError.code)" params[PixelKit.Parameters.underlyingErrorDomain] = underlyingError.domain - params[PixelKit.Parameters.underlyingErrorDesc] = underlyingError.localizedDescription } if let sqlErrorCode = nsError.userInfo["SQLiteResultCode"] as? NSNumber { diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift index 9c616d0060..5370432743 100644 --- a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift +++ b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift @@ -204,7 +204,7 @@ public final class PixelKit { private func printDebugInfo(pixelName: String, parameters: [String: String], skipped: Bool = false) { #if DEBUG - let params = parameters.filter { key, _ in !["appVersion", "test"].contains(key) } + let params = parameters.filter { key, _ in !["test"].contains(key) } os_log(.debug, log: log, "👾 [%{public}@] %{public}@ %{public}@", skipped ? "SKIPPED" : "FIRED", pixelName.replacingOccurrences(of: "_", with: "."), params) #endif } @@ -377,7 +377,6 @@ extension Dictionary where Key == String, Value == String { self[PixelKit.Parameters.errorCode] = "\(nsError.code)" self[PixelKit.Parameters.errorDomain] = nsError.domain - self[PixelKit.Parameters.errorDesc] = nsError.localizedDescription if let error = error as? PixelKitEventErrorDetails, let underlyingError = error.underlyingError { @@ -385,7 +384,6 @@ extension Dictionary where Key == String, Value == String { let underlyingNSError = underlyingError as NSError self[PixelKit.Parameters.underlyingErrorCode] = "\(underlyingNSError.code)" self[PixelKit.Parameters.underlyingErrorDomain] = underlyingNSError.domain - self[PixelKit.Parameters.underlyingErrorDesc] = underlyingNSError.localizedDescription } else if let underlyingError = nsError.userInfo["NSUnderlyingError"] as? NSError { self[PixelKit.Parameters.underlyingErrorCode] = "\(underlyingError.code)" self[PixelKit.Parameters.underlyingErrorDomain] = underlyingError.domain diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEvent.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEvent.swift index bc87070df3..ca352f3347 100644 --- a/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEvent.swift +++ b/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEvent.swift @@ -79,11 +79,11 @@ public final class DebugEvent: PixelKitEvent { let nsError = error as NSError params[PixelKit.Parameters.errorCode] = "\(nsError.code)" - params[PixelKit.Parameters.errorDesc] = nsError.domain + params[PixelKit.Parameters.errorDomain] = nsError.domain if let underlyingError = nsError.userInfo["NSUnderlyingError"] as? NSError { params[PixelKit.Parameters.underlyingErrorCode] = "\(underlyingError.code)" - params[PixelKit.Parameters.underlyingErrorDesc] = underlyingError.domain + params[PixelKit.Parameters.underlyingErrorDomain] = underlyingError.domain } if let sqlErrorCode = nsError.userInfo["SQLiteResultCode"] as? NSNumber { diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEventV2.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEventV2.swift index 7048519e32..811bf4643d 100644 --- a/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEventV2.swift +++ b/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEventV2.swift @@ -30,8 +30,7 @@ extension PixelKitEventErrorDetails { return [ PixelKit.Parameters.underlyingErrorCode: "\(nsError.code)", - PixelKit.Parameters.underlyingErrorDomain: nsError.domain, - PixelKit.Parameters.underlyingErrorDesc: nsError.localizedDescription + PixelKit.Parameters.underlyingErrorDomain: nsError.domain ] } } diff --git a/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/ValidatePixel.swift b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/ValidatePixel.swift index a56f128454..00547deb5b 100644 --- a/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/ValidatePixel.swift +++ b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/ValidatePixel.swift @@ -44,7 +44,7 @@ public final class PixelRequestValidator { if let error = expectedError as? NSError { XCTAssertEqual(parameters[PixelKit.Parameters.errorCode], "\(error.code)") - XCTAssertEqual(parameters[PixelKit.Parameters.errorDesc], error.domain) + XCTAssertEqual(parameters[PixelKit.Parameters.errorDomain], error.domain) } } } diff --git a/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift index 5088ba1371..325b7414b3 100644 --- a/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift +++ b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift @@ -38,16 +38,14 @@ public extension XCTestCase { /// private static var errorPixelParameters = [ PixelKit.Parameters.errorCode, - PixelKit.Parameters.errorDomain, - PixelKit.Parameters.errorDesc + PixelKit.Parameters.errorDomain ] /// List of underlying error pixel parameters /// private static var underlyingErrorPixelParameters = [ PixelKit.Parameters.underlyingErrorCode, - PixelKit.Parameters.underlyingErrorDomain, - PixelKit.Parameters.underlyingErrorDesc + PixelKit.Parameters.underlyingErrorDomain ] /// Filter out the standard parameters. @@ -75,13 +73,11 @@ public extension XCTestCase { let nsError = error as NSError expectedParameters[PixelKit.Parameters.errorCode] = "\(nsError.code)" expectedParameters[PixelKit.Parameters.errorDomain] = nsError.domain - expectedParameters[PixelKit.Parameters.errorDesc] = nsError.localizedDescription if let underlyingError = (error as? PixelKitEventErrorDetails)?.underlyingError { let underlyingNSError = underlyingError as NSError expectedParameters[PixelKit.Parameters.underlyingErrorCode] = "\(underlyingNSError.code)" expectedParameters[PixelKit.Parameters.underlyingErrorDomain] = underlyingNSError.domain - expectedParameters[PixelKit.Parameters.underlyingErrorDesc] = underlyingNSError.localizedDescription } } @@ -129,14 +125,12 @@ public extension XCTestCase { let nsError = error as NSError XCTAssertEqual(firedParameters[PixelKit.Parameters.errorCode], String(nsError.code), file: file, line: line) XCTAssertEqual(firedParameters[PixelKit.Parameters.errorDomain], nsError.domain, file: file, line: line) - XCTAssertEqual(firedParameters[PixelKit.Parameters.errorDesc], nsError.localizedDescription, file: file, line: line) } if let underlyingError = expectations.underlyingError { let nsError = underlyingError as NSError XCTAssertEqual(firedParameters[PixelKit.Parameters.underlyingErrorCode], String(nsError.code), file: file, line: line) XCTAssertEqual(firedParameters[PixelKit.Parameters.underlyingErrorDomain], nsError.domain, file: file, line: line) - XCTAssertEqual(firedParameters[PixelKit.Parameters.underlyingErrorDesc], nsError.localizedDescription, file: file, line: line) } completion(true, nil) From e9bd56dca465c05a6cec74c233c9cdc1032771da Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Fri, 15 Mar 2024 06:54:37 +0100 Subject: [PATCH 10/51] Fix determining if internal release should be automatically bumped --- .github/workflows/bump_internal_release.yml | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/.github/workflows/bump_internal_release.yml b/.github/workflows/bump_internal_release.yml index ba06acdcc6..1b9c4b3c4e 100644 --- a/.github/workflows/bump_internal_release.yml +++ b/.github/workflows/bump_internal_release.yml @@ -63,13 +63,21 @@ jobs: echo "skip-release=false" >> $GITHUB_OUTPUT else latest_tag="$(git describe --tags --abbrev=0)" - changed_files="$(git diff --name-only "$latest_tag".."origin/${release_branch}")" + latest_tag_sha="$(git rev-parse "$latest_tag")" + release_branch_sha="$(git rev-parse "origin/${release_branch}")" - if grep -q -v -e '.github' -e 'scripts' <<< "$changed_files"; then - echo "skip-release=false" >> $GITHUB_OUTPUT - else - echo "::warning::No changes to the release branch (or only changes to scripts and workflows). Skipping automatic release." + if [[ "${latest_tag_sha}" == "${release_branch_sha}" ]]; then + echo "::warning::Release branch's HEAD is already tagged. Skipping automatic release." echo "skip-release=true" >> $GITHUB_OUTPUT + else + changed_files="$(git diff --name-only "$latest_tag".."origin/${release_branch}")" + if grep -q -v -e '.github' -e 'scripts' <<< "$changed_files"; then + echo "::warning::New code changes found in the release branch since the last release. Will bump internal release now." + echo "skip-release=false" >> $GITHUB_OUTPUT + else + echo "::warning::No changes to the release branch (or only changes to scripts and workflows). Skipping automatic release." + echo "skip-release=true" >> $GITHUB_OUTPUT + fi fi fi From a40035b73906caff097f53471521d274de2696fe Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Fri, 15 Mar 2024 08:38:32 -0300 Subject: [PATCH 11/51] DBP: Use url instead of name to identify brokers on pixels (#2423) --- .../Operations/DataBrokerProfileQueryOperationManager.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift index ff931a7687..58352c3fc3 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift @@ -117,7 +117,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { notificationCenter.post(name: DataBrokerProtectionNotifications.didFinishScan, object: brokerProfileQueryData.dataBroker.name) } - let stageCalculator = DataBrokerProtectionStageDurationCalculator(dataBroker: brokerProfileQueryData.dataBroker.name, handler: pixelHandler) + let stageCalculator = DataBrokerProtectionStageDurationCalculator(dataBroker: brokerProfileQueryData.dataBroker.url, handler: pixelHandler) do { let event = HistoryEvent(brokerId: brokerId, profileQueryId: profileQueryId, type: .scanStarted) @@ -280,7 +280,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { } let retriesCalculatorUseCase = OperationRetriesCalculatorUseCase() - let stageDurationCalculator = DataBrokerProtectionStageDurationCalculator(dataBroker: brokerProfileQueryData.dataBroker.name, handler: pixelHandler) + let stageDurationCalculator = DataBrokerProtectionStageDurationCalculator(dataBroker: brokerProfileQueryData.dataBroker.url, handler: pixelHandler) stageDurationCalculator.fireOptOutStart() os_log("Running opt-out operation: %{public}@", log: .dataBrokerProtection, String(describing: brokerProfileQueryData.dataBroker.name)) From 919a93baf974e56a9b69b440b2b3918757bc7644 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Fri, 15 Mar 2024 08:39:00 -0300 Subject: [PATCH 12/51] DBP: Treat 404 as failure instead of error (#2426) --- ...kerProtectionStageDurationCalculator.swift | 7 +- ...otectionStageDurationCalculatorTests.swift | 141 ++++++++++++++++++ 2 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionStageDurationCalculatorTests.swift diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionStageDurationCalculator.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionStageDurationCalculator.swift index 7de439e30b..36723e14bf 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionStageDurationCalculator.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionStageDurationCalculator.swift @@ -170,7 +170,12 @@ final class DataBrokerProtectionStageDurationCalculator: StageDurationCalculator switch dataBrokerProtectionError { case .httpError(let httpCode): if httpCode < 500 { - errorCategory = .clientError(httpCode: httpCode) + if httpCode == 404 { + fireScanFailed() + return + } else { + errorCategory = .clientError(httpCode: httpCode) + } } else { errorCategory = .serverError(httpCode: httpCode) } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionStageDurationCalculatorTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionStageDurationCalculatorTests.swift new file mode 100644 index 0000000000..8b899e12a1 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionStageDurationCalculatorTests.swift @@ -0,0 +1,141 @@ +// +// DataBrokerProtectionStageDurationCalculatorTests.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import BrowserServicesKit +import Foundation +import XCTest + +@testable import DataBrokerProtection + +final class DataBrokerProtectionStageDurationCalculatorTests: XCTestCase { + let handler = MockDataBrokerProtectionPixelsHandler() + + override func tearDown() { + handler.clear() + } + + func testWhenErrorIs404_thenWeFireScanFailedPixel() { + let sut = DataBrokerProtectionStageDurationCalculator(dataBroker: "broker", handler: handler) + + sut.fireScanError(error: DataBrokerProtectionError.httpError(code: 404)) + + XCTAssertTrue(MockDataBrokerProtectionPixelsHandler.lastPixelsFired.count == 1) + + if let failurePixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.last{ + switch failurePixel { + case .scanFailed(let broker, _, _): + XCTAssertEqual(broker, "broker") + default: XCTFail("The scan failed pixel should be fired") + } + } else { + XCTFail("A pixel should be fired") + } + } + + func testWhenErrorIs403_thenWeFireScanErrorPixelWithClientErrorCategory() { + let sut = DataBrokerProtectionStageDurationCalculator(dataBroker: "broker", handler: handler) + + sut.fireScanError(error: DataBrokerProtectionError.httpError(code: 403)) + + XCTAssertTrue(MockDataBrokerProtectionPixelsHandler.lastPixelsFired.count == 1) + + if let failurePixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.last{ + switch failurePixel { + case .scanError(_, _, let category, _): + XCTAssertEqual(category, ErrorCategory.clientError(httpCode: 403).toString) + default: XCTFail("The scan error pixel should be fired") + } + } else { + XCTFail("A pixel should be fired") + } + } + + func testWhenErrorIs500_thenWeFireScanErrorPixelWithServerErrorCategory() { + let sut = DataBrokerProtectionStageDurationCalculator(dataBroker: "broker", handler: handler) + + sut.fireScanError(error: DataBrokerProtectionError.httpError(code: 500)) + + XCTAssertTrue(MockDataBrokerProtectionPixelsHandler.lastPixelsFired.count == 1) + + if let failurePixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.last{ + switch failurePixel { + case .scanError(_, _, let category, _): + XCTAssertEqual(category, ErrorCategory.serverError(httpCode: 500).toString) + default: XCTFail("The scan error pixel should be fired") + } + } else { + XCTFail("A pixel should be fired") + } + } + + func testWhenErrorIsNotHttp_thenWeFireScanErrorPixelWithValidationErrorCategory() { + let sut = DataBrokerProtectionStageDurationCalculator(dataBroker: "broker", handler: handler) + + sut.fireScanError(error: DataBrokerProtectionError.actionFailed(actionID: "Action-ID", message: "Some message")) + + XCTAssertTrue(MockDataBrokerProtectionPixelsHandler.lastPixelsFired.count == 1) + + if let failurePixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.last{ + switch failurePixel { + case .scanError(_, _, let category, _): + XCTAssertEqual(category, ErrorCategory.validationError.toString) + default: XCTFail("The scan error pixel should be fired") + } + } else { + XCTFail("A pixel should be fired") + } + } + + func testWhenErrorIsNotDBPErrorButItIsNSURL_thenWeFireScanErrorPixelWithNetworkErrorErrorCategory() { + let sut = DataBrokerProtectionStageDurationCalculator(dataBroker: "broker", handler: handler) + let nsURLError = NSError(domain: NSURLErrorDomain, code: NSURLErrorTimedOut) + + sut.fireScanError(error: nsURLError) + + XCTAssertTrue(MockDataBrokerProtectionPixelsHandler.lastPixelsFired.count == 1) + + if let failurePixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.last{ + switch failurePixel { + case .scanError(_, _, let category, _): + XCTAssertEqual(category, ErrorCategory.networkError.toString) + default: XCTFail("The scan error pixel should be fired") + } + } else { + XCTFail("A pixel should be fired") + } + } + + func testWhenErrorIsNotDBPErrorAndNotURL_thenWeFireScanErrorPixelWithUnclassifiedErrorCategory() { + let sut = DataBrokerProtectionStageDurationCalculator(dataBroker: "broker", handler: handler) + let error = NSError(domain: NSCocoaErrorDomain, code: -1) + + sut.fireScanError(error: error) + + XCTAssertTrue(MockDataBrokerProtectionPixelsHandler.lastPixelsFired.count == 1) + + if let failurePixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.last{ + switch failurePixel { + case .scanError(_, _, let category, _): + XCTAssertEqual(category, ErrorCategory.unclassified.toString) + default: XCTFail("The scan error pixel should be fired") + } + } else { + XCTFail("A pixel should be fired") + } + } +} From aaacc14afca2f61dd36314bb09f6837b344713bb Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 15 Mar 2024 12:49:25 +0100 Subject: [PATCH 13/51] Properly handle edge cases (#2417) Task/Issue URL: https://app.asana.com/0/0/1206841322374471/f **Description**: - Handle edge cases via "Something Went Wrong" alert - Handle scenario when App Store purchase went through but the BE failed - Fix description in the settings detailing subscription's billing period and expiry or renewal date - Handle expired subscription state **Steps to test this PR**: Test triggering "Something Went Wrong" alert, e.g.: 1. Opening purchase page 2. Disable internet connection 3. Try purchasing Handle scenario when App Store purchase went through but the BE failed 1. Modify `SubscriptionPagesUserScript.swift` commenting call to `AppStorePurchaseFlow.completeSubscriptionPurchase` and the whole `switch` - but you need to leave `await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: PurchaseUpdate(type: "completed"))` 2. Switch purchase platform to App Store 3. Try purchasing subscription 5. Due to code modification the confirmation endpoint will not be used leaving you with an account lacking entitlements 6. Go to settings Fix description in the settings detailing subscription's billing period and expiry or renewal date 1. Purchase or activate subscription 2. Open settings, the description should properly reflect if subscription will renew or expire at given date Handle expired subscription state 1. Purchase subscription via App Store 3. Wait until it expires 4. Open settings --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --- .../Images/ITR-Icon.imageset/ITR-Icon.pdf | Bin 4129 -> 3919 bytes .../SubscriptionIcon.pdf | Bin 2646 -> 4808 bytes DuckDuckGo/Common/Localizables/UserText.swift | 1 - .../NavigationBar/View/MoreOptionsMenu.swift | 9 +- .../SubscriptionPagesUserScript.swift | 12 +- .../SubscriptionUI/NSAlert+Subscription.swift | 8 - .../PreferencesSubscriptionModel.swift | 115 ++++-- .../PreferencesSubscriptionView.swift | 352 ++++++++++++------ .../Contents.json | 12 + .../subscription-expired-icon.pdf | Bin 0 -> 2374 bytes .../Contents.json | 12 + .../Info-Color-16.pdf | Bin 0 -> 1745 bytes .../ActivateSubscriptionAccessModel.swift | 5 +- .../Model/ShareSubscriptionAccessModel.swift | 2 +- .../SubscriptionAccessView.swift | 1 + .../Sources/SubscriptionUI/UserText.swift | 55 ++- 16 files changed, 407 insertions(+), 177 deletions(-) create mode 100644 LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/subscription-expired-icon.imageset/Contents.json create mode 100644 LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/subscription-expired-icon.imageset/subscription-expired-icon.pdf create mode 100644 LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/subscription-pending-icon.imageset/Contents.json create mode 100644 LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/subscription-pending-icon.imageset/Info-Color-16.pdf diff --git a/DuckDuckGo/Assets.xcassets/Images/ITR-Icon.imageset/ITR-Icon.pdf b/DuckDuckGo/Assets.xcassets/Images/ITR-Icon.imageset/ITR-Icon.pdf index 1ff6a4f6db7f5eb49a1d732bf7d6ebef1fbc4c10..71e908133d48e1c57639464825a9722d736aefbe 100644 GIT binary patch delta 143 zcmZ3ea9(bL_{6h*o0S;NStlzo>Tm96>tX~l^fvo(D6xP=mhr?hG8>s&Z06uAWb`vI tHc>DD0fjs+Fl}ILY-WKWWNC;lWNLvw@_h%c)h-BMTvWGNn%N=f{l%WzN-nCN={TT(6cZx00IR&J1zxGpzmr#v`S;# zD$P(#)^|0;trVfs&TgWZKO0DKdhz7htOgsWaIgZoMnLZ7*{r)5fox-ll>X*sb|n@d z-vp|3^A+xRMrLCJv(09_g^YeihGq&NPZ}uXae-+AV^ebrbRjbXLv$f?a}#tS3zNxT Q`2smD4a~VzRbBnv02LET*Z=?k diff --git a/DuckDuckGo/Assets.xcassets/Images/SubscriptionIcon.imageset/SubscriptionIcon.pdf b/DuckDuckGo/Assets.xcassets/Images/SubscriptionIcon.imageset/SubscriptionIcon.pdf index 934a29304855a8da122da590fa025cc53f443972..d9be8eca8e50aae5d9b27cf4e2e48715bd2c614e 100644 GIT binary patch literal 4808 zcmd^@OOG2z5`^#lEBYe9UO>%yKLA63^@QOytex3|0b>soMN4DPkXn*j$ouQ}i|S^R zoYBJa*}SMGx-u&(GBT@5k6u52`PdG*>uhiO?VDe_zW?T%e(}%WKdk%3Pd~o-b-g-( z@mu_Ty?J+d-#aE=P0*{|_Tz^$gZB6Mt=Wu**ZA8lH~SCE-Fmb7{o?zRCzGq|k8cib z(U;rZ-Gp7O_uG%V)q1Zb=9y2QZa?)u_hvBdcN*AqgifP>;R?(BVYgo1b&Kcg|J6=5K9Sl_B|<+bmN;@QEcR#0}Y3*{!gfFf-VkGT3cr?OfU$1berxU ze_rttjrA_tFOYm1V$8{-kcetbFb>V9q552M1FQ2PraliL)l^bX&03dSsR+QBg4{$uqjV}7%taH31#34LXANZpYGQ2RlZC{R&7-m2R7M^(krm_MDJjDUKQTt1WgV&eQZupnA$aRR)!S^07R*6sg5`!e<3xp1 zSE@msLNlYQHo#~MH7dwBgz6L|-%>6C3*ur0YkCI7kO7$@DG~)y4n~W?hbrBaL>?`n zlvpfmI~#*7G#DmT{I9x7SAyUm%x8l)5N}vth>fP%nNn=)RuE2Rnw^1JBVVLG#Xxf) z!F1RxOR(VmghhI2!^nWzUs2Y_I7~)64pn6`mgUWmAL!5Q*Ok=#y9^tw2?o0^%a^ao zTYN<}PS;_-V2RH#P)k$>p|%Eu#HZ{S`XghKi5a3R#hZy(XwpgHt@jiCp{Nln8tP|E zQ(|kfM0_Hu9_&jWA$o0qlmr@M6Re@yiegI@gQhL)Ds;QS z;cQH`4OF7|_9#4M*VL+*Ok_94rrt>#~xJ$k!p+d1=5uZ-Y2kTPrgu?T0ZkQ=ET2R1yr#|!U~M(L$s8X za)MiUVP!uCQ))?#D(|gLmiP=F#_BTBBR)(iQ*{8Uf^yC%EpZmZS(d5#EPey*A)Hp2 zZYXpQ8i9x}0Zq4htN2hg4#}mYqGle=Ls_BS(`8y_spG);f^aj(DBUq2+Z<>u$~B%0<-@T+ zj{=Jj$mS*Pkb`Bwq2ste=c(_bVx1#k$#OqXy+`!S zQo^!mov@-FFN}#J7E5V+ie$uT9*sX%+KM_Gjg9UjifyL96!-~-N+e+jA(&+Ik;c{( zX^^DY`H^ipR}*J35y7;SptS~Rqf_co*kqd#gZc`TbCny5HgOE-IFuABtyLO3eo zXz3Ya*2lg>Tjjb2^q?iQOUmEcEu~v;-cHwdMtY)jdV-8^6<2H0ga*?$zw8Oa;q+I2 z!sx)!uN_Bi@W(Hh+2-`Lx@#AV(bEZ~!sch#Y;^mX2htgKvbdcur=Ll?(l({tyE*G| zM)h!=o~-Rn`V*2?rcY1D7oQHVt`Ez@x_y6P+dtPoEcd^)(fI1spBHb`(^K|xw0PA^ za?@k@^4o*I7QBMLz5Vch`Nyu0FWm<|1WZr!c_;tR-IFKdwsCf>k95J*^qlANGcxo= zPvixhz1w#SsC~ZW3GtaM$>Er#a4Jo%U1z2F?Pjw*?E9bfI6rOa_43_%0)AM(y;=U* zXTCh0{>MPFIQ}=qv+d@v-msz=ID)VBzY&L?%VmSkcoH=3K#KbQ<}IQ2+QC(SjQH91 zBewQI{CkiyPrA3AxzY~z^NOdPKU*G_x7&A@wx4$EUpkt*?OKVAb(Xkx#U6b6X#xHE z%>;2pq)hHXnVeignYMc$;zYWH0?(`a5C`Rbh9HGN*d2_qob)xsX z=aBo=etWn6y8r6t?bjp4i>vJxennr)*W&u-ziV*;x%E4l@s5R@VMvC zm!E0gT-dTHJXrtJyt&+ea1rpFiPLU>JFjl0lb7>9SL^xw%?o$>_VfSNyXo)gP`C5}^mWZ!~V~8*Lq^Tq#P#;T(Q4I@GdTF*=ea|HlKd4wL%nZKN5G#p*Nz8$` zB3M-AZSoqWF^|>k6pC2fj6{PjQCN^;Bt?5g+4qLpUgZ#NKD< ziNF>`D>2uRZSSoVM!^SIY~ySgB?}eTeB!l?V>qQmalC^PsI|CSQrKaTG{~SV@;b*L zF8G*R6?jswS&PDhPtpVAbXe*Mj>V@WFd?*hmiBiVwYO0y3FnZ&r{stLX3*R_l!LgV&Dp55|!Wlf+iFqWAEZ z%5ij1dR8yxG))Ur@DKGo+7SB1# zR}pf!$NYpa-zfO?^Z`v#h68O!m8!kc2kt@Ns7LDc+N!h!ZC&BLEuzC3<)QFRZD|fT z2!e#V>wyr2otFZSn5meD2!h%uBd!dmG?C^DvM`STsDjaID3VeeJ{biH$|0svgnC5< zX3_|(r~~b(i;RMT%b6ROcCcbNvHY`)=YAC>J+FELAZWyEpQFM%5b3wmI3bfit z{$B}Tx>O$yx-1eTWVy%CDkRhxMZ^{P=>GRjnN100?Kz}+;BZ|Q|dJg0ZIy$StRE>-V6(`rO=lbr#I_FMs=zbiTVQN zM@^7yEX`QJD#ygb7aYd5fVD=;MB{8~8)R1tvoHg;A4?OB6?V-ul&A$q0b55|oJFUm zJrsh}g8``(E3VE3(Jj5DS4w_QSn>cgfp*zI-Go z#yH;n=?L4m!}pubcE5A)e&<_)=j6A4|C-&&+3IpW06)(cSF0D>Pww4lMi1|LyK=`f zF4bjpx6}E&+kU)V&pT&dE9ZypPPd!=yx~Oq1i1sxR<~e|mOLOnLGE%e=>9|J$EyoM zb(X=oyF2G}`;oQ!Rs1`U&vv3`f3}k)xH}$rpZMu&zq;ODK3w|gc78vJtJ9LJ{dOhi zm>uxpp~Z}m+Z>@voc0_cZbtHX2$|2nfC85O0*bVuJ7|Bqy1JfkCsXf-m(cNex4oI4 rxW`u)PZm;6&bQmWvxEo1CvUI*n~8pW*{yE(BOQt}O^+VE`sMZi [NSMenuItem] { - let dataBrokerProtectionItem = NSMenuItem(title: UserText.dataBrokerProtectionScanOptionsMenuItem, - action: #selector(openSubscriptionPurchasePage(_:)), - keyEquivalent: "") - .targetting(self) - .withImage(.dbpIcon) - let privacyProItem = NSMenuItem(title: UserText.subscriptionOptionsMenuItem, action: #selector(openSubscriptionPurchasePage(_:)), keyEquivalent: "") .targetting(self) .withImage(.subscriptionIcon) - return [dataBrokerProtectionItem, privacyProItem] + return [privacyProItem] } #endif diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index b0c6391e44..a69c43fa24 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -305,8 +305,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { case .accountCreationFailed: report(subscriptionActivationError: .accountCreationFailed) } - - return nil + await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: PurchaseUpdate(type: "canceled")) } } @@ -524,15 +523,10 @@ extension SubscriptionPagesUseSubscriptionFeature { extension MainWindowController { @MainActor - func showSomethingWentWrongAlert(environment: SubscriptionPurchaseEnvironment.Environment = SubscriptionPurchaseEnvironment.current) { + func showSomethingWentWrongAlert() { guard let window else { return } - switch environment { - case .appStore: - window.show(.somethingWentWrongAlert()) - case .stripe: - window.show(.somethingWentWrongStripeAlert()) - } + window.show(.somethingWentWrongAlert()) } @MainActor diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/NSAlert+Subscription.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/NSAlert+Subscription.swift index d79042d247..7080cdd43f 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/NSAlert+Subscription.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/NSAlert+Subscription.swift @@ -29,14 +29,6 @@ public extension NSAlert { return alert } - static func somethingWentWrongStripeAlert() -> NSAlert { - let alert = NSAlert() - alert.messageText = UserText.somethingWentWrongAlertTitle - alert.informativeText = UserText.somethingWentWrongStripeAlertDescription - alert.addButton(withTitle: UserText.okButtonTitle) - return alert - } - static func subscriptionNotFoundAlert() -> NSAlert { let alert = NSAlert() alert.messageText = UserText.subscriptionNotFoundAlertTitle diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift index 8a0392ec37..c2cd52eba6 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift @@ -18,11 +18,14 @@ import AppKit import Subscription +import struct Combine.AnyPublisher +import enum Combine.Publishers public final class PreferencesSubscriptionModel: ObservableObject { @Published var isUserAuthenticated: Bool = false @Published var subscriptionDetails: String? + @Published var subscriptionStatus: Subscription.Status? @Published var hasAccessToVPN: Bool = false @Published var hasAccessToDBP: Bool = false @@ -58,6 +61,28 @@ public final class PreferencesSubscriptionModel: ObservableObject { removeSubscriptionClick } + lazy var statePublisher: AnyPublisher = { + let isSubscriptionActivePublisher = $subscriptionStatus.map { + $0 != .expired && $0 != .inactive + }.eraseToAnyPublisher() + + let hasAnyEntitlementPublisher = Publishers.CombineLatest3($hasAccessToVPN, $hasAccessToDBP, $hasAccessToITR).map { + return $0 || $1 || $2 + }.eraseToAnyPublisher() + + return Publishers.CombineLatest3($isUserAuthenticated, isSubscriptionActivePublisher, hasAnyEntitlementPublisher) + .map { isUserAuthenticated, isSubscriptionActive, hasAnyEntitlement in + switch (isUserAuthenticated, isSubscriptionActive, hasAnyEntitlement) { + case (false, _, _): return PreferencesSubscriptionState.noSubscription + case (true, false, _): return PreferencesSubscriptionState.subscriptionExpired + case (true, true, false): return PreferencesSubscriptionState.subscriptionPendingActivation + case (true, true, true): return PreferencesSubscriptionState.subscriptionActive + } + } + .removeDuplicates() + .eraseToAnyPublisher() + }() + public init(openURLHandler: @escaping (URL) -> Void, userEventHandler: @escaping (UserEvent) -> Void, sheetActionHandler: SubscriptionAccessActionHandlers, @@ -72,9 +97,32 @@ public final class PreferencesSubscriptionModel: ObservableObject { if let token = accountManager.accessToken { Task { - let subscriptionResult = await SubscriptionService.getSubscription(accessToken: token) + let subscriptionResult = await SubscriptionService.getSubscription(accessToken: token, cachePolicy: .returnCacheDataElseLoad) if case .success(let subscription) = subscriptionResult { - self.updateDescription(for: subscription.expiresOrRenewsAt) + self.updateDescription(for: subscription.expiresOrRenewsAt, status: subscription.status, period: subscription.billingPeriod) + self.subscriptionPlatform = subscription.platform + self.subscriptionStatus = subscription.status + } + + switch await self.accountManager.hasEntitlement(for: .networkProtection, cachePolicy: .returnCacheDataElseLoad) { + case let .success(result): + self.hasAccessToVPN = result + case .failure: + self.hasAccessToVPN = false + } + + switch await self.accountManager.hasEntitlement(for: .dataBrokerProtection, cachePolicy: .returnCacheDataElseLoad) { + case let .success(result): + self.hasAccessToDBP = result + case .failure: + self.hasAccessToDBP = false + } + + switch await self.accountManager.hasEntitlement(for: .identityTheftRestoration, cachePolicy: .returnCacheDataElseLoad) { + case let .success(result): + self.hasAccessToITR = result + case .failure: + self.hasAccessToITR = false } } } @@ -123,6 +171,7 @@ public final class PreferencesSubscriptionModel: ObservableObject { @MainActor func changePlanOrBillingAction() async -> ChangePlanOrBillingAction { + switch subscriptionPlatform { case .apple: if await confirmIfSignedInToSameAccount() { @@ -199,7 +248,20 @@ public final class PreferencesSubscriptionModel: ObservableObject { openURLHandler(.subscriptionFAQ) } - // swiftlint:disable cyclomatic_complexity + @MainActor + func refreshSubscriptionPendingState() { + if SubscriptionPurchaseEnvironment.current == .appStore { + if #available(macOS 12.0, *) { + Task { + _ = await AppStoreRestoreFlow.restoreAccountFromPastPurchase(subscriptionAppGroup: subscriptionAppGroup) + fetchAndUpdateSubscriptionDetails() + } + } + } else { + fetchAndUpdateSubscriptionDetails() + } + } + @MainActor func fetchAndUpdateSubscriptionDetails() { guard fetchSubscriptionDetailsTask == nil else { return } @@ -211,22 +273,12 @@ public final class PreferencesSubscriptionModel: ObservableObject { guard let token = self?.accountManager.accessToken else { return } - let subscriptionResult = await SubscriptionService.getSubscription(accessToken: token) + let subscriptionResult = await SubscriptionService.getSubscription(accessToken: token, cachePolicy: .reloadIgnoringLocalCacheData) if case .success(let subscription) = subscriptionResult { - self?.updateDescription(for: subscription.expiresOrRenewsAt) + self?.updateDescription(for: subscription.expiresOrRenewsAt, status: subscription.status, period: subscription.billingPeriod) self?.subscriptionPlatform = subscription.platform - - if subscription.expiresOrRenewsAt.timeIntervalSinceNow < 0 || !subscription.isActive { - self?.hasAccessToVPN = false - self?.hasAccessToDBP = false - self?.hasAccessToITR = false - - if !subscription.isActive { - self?.accountManager.signOut() - return - } - } + self?.subscriptionStatus = subscription.status } else { self?.accountManager.signOut() } @@ -255,16 +307,33 @@ public final class PreferencesSubscriptionModel: ObservableObject { } } } - // swiftlint:enable cyclomatic_complexity - private func updateDescription(for date: Date) { - self.subscriptionDetails = UserText.preferencesSubscriptionActiveCaption(formattedDate: dateFormatter.string(from: date)) + func updateDescription(for date: Date, status: Subscription.Status, period: Subscription.BillingPeriod) { + + let formattedDate = dateFormatter.string(from: date) + + let billingPeriod: String + + switch period { + case .monthly: billingPeriod = UserText.monthlySubscriptionBillingPeriod.lowercased() + case .yearly: billingPeriod = UserText.yearlySubscriptionBillingPeriod.lowercased() + case .unknown: billingPeriod = "" + } + + switch status { + case .autoRenewable: + self.subscriptionDetails = UserText.preferencesSubscriptionActiveRenewCaption(period: billingPeriod, formattedDate: formattedDate) + case .expired, .inactive: + self.subscriptionDetails = UserText.preferencesSubscriptionExpiredCaption(formattedDate: formattedDate) + default: + self.subscriptionDetails = UserText.preferencesSubscriptionActiveExpireCaption(period: billingPeriod, formattedDate: formattedDate) + } } private var dateFormatter = { let dateFormatter = DateFormatter() - dateFormatter.dateStyle = .medium - dateFormatter.timeStyle = .short + dateFormatter.dateStyle = .long + dateFormatter.timeStyle = .none return dateFormatter }() @@ -277,3 +346,7 @@ enum ManageSubscriptionSheet: Identifiable { return self } } + +enum PreferencesSubscriptionState: String { + case noSubscription, subscriptionPendingActivation, subscriptionActive, subscriptionExpired +} diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionView.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionView.swift index f17fcb0ea8..5424f3920b 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionView.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionView.swift @@ -21,6 +21,9 @@ import SwiftUI import SwiftUIExtensions public struct PreferencesSubscriptionView: View { + + @State private var state: PreferencesSubscriptionState = .noSubscription + @ObservedObject var model: PreferencesSubscriptionModel @State private var showingSheet = false @State private var showingRemoveConfirmationDialog = false @@ -39,41 +42,14 @@ public struct PreferencesSubscriptionView: View { SubscriptionAccessView(model: model.sheetModel) } .sheet(isPresented: $showingRemoveConfirmationDialog) { - SubscriptionDialog(imageName: "Privacy-Pro-128", - title: UserText.removeSubscriptionDialogTitle, - description: UserText.removeSubscriptionDialogDescription, - buttons: { - Button(UserText.removeSubscriptionDialogCancel) { showingRemoveConfirmationDialog = false } - Button(action: { - showingRemoveConfirmationDialog = false - model.removeFromThisDeviceAction() - }, label: { - Text(UserText.removeSubscriptionDialogConfirm) - .foregroundColor(.red) - }) - }) - .frame(width: 320) + removeConfirmationDialog } .sheet(item: $manageSubscriptionSheet) { sheet in switch sheet { case .apple: - SubscriptionDialog(imageName: "app-store", - title: UserText.changeSubscriptionDialogTitle, - description: UserText.changeSubscriptionAppleDialogDescription, - buttons: { - Button(UserText.changeSubscriptionDialogDone) { manageSubscriptionSheet = nil } - .buttonStyle(DefaultActionButtonStyle(enabled: true)) - }) - .frame(width: 360) + manageSubscriptionAppStoreDialog case .google: - SubscriptionDialog(imageName: "google-play", - title: UserText.changeSubscriptionDialogTitle, - description: UserText.changeSubscriptionGoogleDialogDescription, - buttons: { - Button(UserText.changeSubscriptionDialogDone) { manageSubscriptionSheet = nil } - .buttonStyle(DefaultActionButtonStyle(enabled: true)) - }) - .frame(width: 360) + manageSubscriptionGooglePlayDialog } } @@ -81,93 +57,26 @@ public struct PreferencesSubscriptionView: View { .frame(height: 20) VStack { - if model.isUserAuthenticated { - UniversalHeaderView { - Image(.subscriptionActiveIcon) - .padding(4) - } content: { - TextMenuItemHeader(UserText.preferencesSubscriptionActiveHeader) - TextMenuItemCaption(model.subscriptionDetails ?? "") - } buttons: { - Button(UserText.addToAnotherDeviceButton) { - model.userEventHandler(.addToAnotherDeviceClick) - showingSheet.toggle() - } - - Menu { - Button(UserText.changePlanOrBillingButton, action: { - model.userEventHandler(.changePlanOrBillingClick) - Task { - switch await model.changePlanOrBillingAction() { - case .presentSheet(let sheet): - manageSubscriptionSheet = sheet - case .navigateToManageSubscription(let navigationAction): - navigationAction() - } - } - }) - Button(UserText.removeFromThisDeviceButton, action: { - model.userEventHandler(.removeSubscriptionClick) - showingRemoveConfirmationDialog.toggle() - }) - } label: { - Text(UserText.manageSubscriptionButton) - } - .fixedSize() - } - .onAppear { - model.fetchAndUpdateSubscriptionDetails() - } - - } else { - UniversalHeaderView { - Image(.privacyPro) - .padding(4) - .background(Color("BadgeBackground", bundle: .module)) - .cornerRadius(4) - } content: { - TextMenuItemHeader(UserText.preferencesSubscriptionInactiveHeader) - TextMenuItemCaption(UserText.preferencesSubscriptionInactiveCaption) - } buttons: { - Button(UserText.purchaseButton) { model.purchaseAction() } - .buttonStyle(DefaultActionButtonStyle(enabled: true)) - Button(UserText.haveSubscriptionButton) { - showingSheet.toggle() - model.userEventHandler(.iHaveASubscriptionClick) - } - } + switch state { + case .noSubscription: + unauthenticatedHeaderView + case .subscriptionPendingActivation: + pendingActivationHeaderView + case .subscriptionActive: + authenticatedHeaderView + case .subscriptionExpired: + expiredHeaderView } Divider() .foregroundColor(Color.secondary) .padding(.horizontal, -10) - SectionView(iconName: "VPN-Icon", - title: UserText.vpnServiceTitle, - description: UserText.vpnServiceDescription, - buttonName: model.isUserAuthenticated ? UserText.vpnServiceButtonTitle : nil, - buttonAction: { model.openVPN() }, - enabled: !model.isUserAuthenticated || model.hasAccessToVPN) - - Divider() - .foregroundColor(Color.secondary) - - SectionView(iconName: "PIR-Icon", - title: UserText.personalInformationRemovalServiceTitle, - description: UserText.personalInformationRemovalServiceDescription, - buttonName: model.isUserAuthenticated ? UserText.personalInformationRemovalServiceButtonTitle : nil, - buttonAction: { model.openPersonalInformationRemoval() }, - enabled: !model.isUserAuthenticated || model.hasAccessToDBP) - - Divider() - .foregroundColor(Color.secondary) - - SectionView(iconName: "ITR-Icon", - title: UserText.identityTheftRestorationServiceTitle, - description: UserText.identityTheftRestorationServiceDescription, - buttonName: model.isUserAuthenticated ? UserText.identityTheftRestorationServiceButtonTitle : nil, - buttonAction: { model.openIdentityTheftRestoration() }, - enabled: !model.isUserAuthenticated || model.hasAccessToITR) + if state == .subscriptionActive { + servicesRowsForActiveSubscriptionView + } else { + servicesRowsForNoSubscriptionView + } } .padding(10) .roundedBorder() @@ -175,19 +84,226 @@ public struct PreferencesSubscriptionView: View { Spacer() .frame(height: 24) - PreferencePaneSection { - TextMenuItemHeader(UserText.preferencesSubscriptionFooterTitle) - HStack(alignment: .top, spacing: 6) { - TextMenuItemCaption(UserText.preferencesSubscriptionFooterCaption) - Button(UserText.viewFaqsButton) { model.openFAQ() } - } - } + footerView } .onAppear(perform: { if model.isUserAuthenticated { model.userEventHandler(.activeSubscriptionSettingsClick) } }) + .onReceive(model.statePublisher, perform: updateState(state:)) + } + + private func updateState(state: PreferencesSubscriptionState) { + self.state = state + } + + @ViewBuilder + private var authenticatedHeaderView: some View { + UniversalHeaderView { + Image(.subscriptionActiveIcon) + .padding(4) + } content: { + TextMenuItemHeader(UserText.preferencesSubscriptionActiveHeader) + TextMenuItemCaption(model.subscriptionDetails ?? "") + } buttons: { + Button(UserText.addToAnotherDeviceButton) { + model.userEventHandler(.addToAnotherDeviceClick) + showingSheet.toggle() + } + + Menu { + Button(UserText.changePlanOrBillingButton, action: { + model.userEventHandler(.changePlanOrBillingClick) + Task { + switch await model.changePlanOrBillingAction() { + case .presentSheet(let sheet): + manageSubscriptionSheet = sheet + case .navigateToManageSubscription(let navigationAction): + navigationAction() + } + } + }) + Button(UserText.removeFromThisDeviceButton, action: { + model.userEventHandler(.removeSubscriptionClick) + showingRemoveConfirmationDialog.toggle() + }) + } label: { + Text(UserText.manageSubscriptionButton) + } + .fixedSize() + } + .onAppear { + model.fetchAndUpdateSubscriptionDetails() + } + } + + @ViewBuilder + private var unauthenticatedHeaderView: some View { + UniversalHeaderView { + Image(.privacyPro) + .padding(4) + .background(Color("BadgeBackground", bundle: .module)) + .cornerRadius(4) + } content: { + TextMenuItemHeader(UserText.preferencesSubscriptionInactiveHeader) + TextMenuItemCaption(UserText.preferencesSubscriptionInactiveCaption) + } buttons: { + Button(UserText.purchaseButton) { model.purchaseAction() } + .buttonStyle(DefaultActionButtonStyle(enabled: true)) + Button(UserText.haveSubscriptionButton) { + showingSheet.toggle() + model.userEventHandler(.iHaveASubscriptionClick) + } + .buttonStyle(DismissActionButtonStyle()) + } + } + + @ViewBuilder + private var pendingActivationHeaderView: some View { + UniversalHeaderView { + Image(.subscriptionPendingIcon) + .padding(4) + } content: { + TextMenuItemHeader(UserText.preferencesSubscriptionPendingHeader) + TextMenuItemCaption(UserText.preferencesSubscriptionPendingCaption) + } buttons: { + Button(UserText.restorePurchaseButton) { model.refreshSubscriptionPendingState() } + .buttonStyle(DefaultActionButtonStyle(enabled: true)) + } + .onAppear { + model.fetchAndUpdateSubscriptionDetails() + } + } + + @ViewBuilder + private var expiredHeaderView: some View { + UniversalHeaderView { + Image(.subscriptionExpiredIcon) + .padding(4) + } content: { + TextMenuItemHeader(model.subscriptionDetails ?? UserText.preferencesSubscriptionInactiveHeader) + TextMenuItemCaption(UserText.preferencesSubscriptionExpiredCaption) + } buttons: { + Button(UserText.viewPlansButtonTitle) { model.purchaseAction() } + .buttonStyle(DefaultActionButtonStyle(enabled: true)) + Menu { + Button(UserText.removeFromThisDeviceButton, action: { + model.userEventHandler(.removeSubscriptionClick) + showingRemoveConfirmationDialog.toggle() + }) + } label: { + Text(UserText.manageDevicesButton) + } + .fixedSize() + } + .onAppear { + model.fetchAndUpdateSubscriptionDetails() + } + } + + @ViewBuilder + private var servicesRowsForNoSubscriptionView: some View { + SectionView(iconName: "VPN-Icon", + title: UserText.vpnServiceTitle, + description: UserText.vpnServiceDescription) + + Divider() + .foregroundColor(Color.secondary) + + SectionView(iconName: "PIR-Icon", + title: UserText.personalInformationRemovalServiceTitle, + description: UserText.personalInformationRemovalServiceDescription) + + Divider() + .foregroundColor(Color.secondary) + + SectionView(iconName: "ITR-Icon", + title: UserText.identityTheftRestorationServiceTitle, + description: UserText.identityTheftRestorationServiceDescription) + } + + @ViewBuilder + private var servicesRowsForActiveSubscriptionView: some View { + SectionView(iconName: "VPN-Icon", + title: UserText.vpnServiceTitle, + description: UserText.vpnServiceDescription, + buttonName: UserText.vpnServiceButtonTitle, + buttonAction: { model.openVPN() }, + enabled: model.hasAccessToVPN) + + Divider() + .foregroundColor(Color.secondary) + + SectionView(iconName: "PIR-Icon", + title: UserText.personalInformationRemovalServiceTitle, + description: UserText.personalInformationRemovalServiceDescription, + buttonName: UserText.personalInformationRemovalServiceButtonTitle, + buttonAction: { model.openPersonalInformationRemoval() }, + enabled: model.hasAccessToDBP) + + Divider() + .foregroundColor(Color.secondary) + + SectionView(iconName: "ITR-Icon", + title: UserText.identityTheftRestorationServiceTitle, + description: UserText.identityTheftRestorationServiceDescription, + buttonName: UserText.identityTheftRestorationServiceButtonTitle, + buttonAction: { model.openIdentityTheftRestoration() }, + enabled: model.hasAccessToITR) + } + + @ViewBuilder + private var footerView: some View { + PreferencePaneSection { + TextMenuItemHeader(UserText.preferencesSubscriptionFooterTitle) + HStack(alignment: .top, spacing: 6) { + TextMenuItemCaption(UserText.preferencesSubscriptionFooterCaption) + Button(UserText.viewFaqsButton) { model.openFAQ() } + } + } + } + + @ViewBuilder + private var removeConfirmationDialog: some View { + SubscriptionDialog(imageName: "Privacy-Pro-128", + title: UserText.removeSubscriptionDialogTitle, + description: UserText.removeSubscriptionDialogDescription, + buttons: { + Button(UserText.removeSubscriptionDialogCancel) { showingRemoveConfirmationDialog = false } + Button(action: { + showingRemoveConfirmationDialog = false + model.removeFromThisDeviceAction() + }, label: { + Text(UserText.removeSubscriptionDialogConfirm) + .foregroundColor(.red) + }) + }) + .frame(width: 320) + } + + @ViewBuilder + private var manageSubscriptionAppStoreDialog: some View { + SubscriptionDialog(imageName: "app-store", + title: UserText.changeSubscriptionDialogTitle, + description: UserText.changeSubscriptionAppleDialogDescription, + buttons: { + Button(UserText.changeSubscriptionDialogDone) { manageSubscriptionSheet = nil } + .buttonStyle(DefaultActionButtonStyle(enabled: true)) + }) + .frame(width: 360) + } + + @ViewBuilder + private var manageSubscriptionGooglePlayDialog: some View { + SubscriptionDialog(imageName: "google-play", + title: UserText.changeSubscriptionDialogTitle, + description: UserText.changeSubscriptionGoogleDialogDescription, + buttons: { + Button(UserText.changeSubscriptionDialogDone) { manageSubscriptionSheet = nil } + .buttonStyle(DefaultActionButtonStyle(enabled: true)) + }) + .frame(width: 360) } } diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/subscription-expired-icon.imageset/Contents.json b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/subscription-expired-icon.imageset/Contents.json new file mode 100644 index 0000000000..0feb43913c --- /dev/null +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/subscription-expired-icon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "subscription-expired-icon.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/subscription-expired-icon.imageset/subscription-expired-icon.pdf b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/subscription-expired-icon.imageset/subscription-expired-icon.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d43daeb9b999065914b2ae55c80a3467fee00e9a GIT binary patch literal 2374 zcmbVOOHbS|5Wf3Y%moQ5tz#!~oJa_%ETyX2s&wfM^^j%_Y{e$+CRDV)zGKH88y2aL z9MJ6NnQtB*&v0;YcD|5YGbTa6@Z~E5I6j7x6K49g{mJqa&)%8-X1av}!Ijk-r=4lH zEITtlHl10$K859F`l}n+cP6=TABZ3LO!dFUC*Qn zgqnM!Z2}g&P`a*4kdjwrE{iHgKn4^^$4H+r684XXpu;GAe$fxM98y6DDj$tKDx+8iAApjb^lsq@`o-d7)Eh|yA6hcFMdGUovURSDVY>oZPCl1$Q^MoP_686;zaiicVYxEr{ zD8|$!fjCbBNtEc6Od_$ZFx86A?c@zJE$5}Ib9~$dSGCmm)MOP7$sj_Q2w6llk)Amv z8Z;*E=#DHXC^8_#PH!I`VI=Gy5rONl7jBH&mFM?aSblcT%}Y`0qM)5v5{$btd)fEH zG{V(q+@1JkpMHKbkX^Jl#slxodefc`4{#Ot55*P}j#6M+nQ^$^b!G&+omZZ2IrNk1 z(W70>0la8;hyk{?gOUYqAzJL&+szt71)ISNf}iDZkG66z{t=Oc3D;-BWD^|8VN874 zPVL=r^VsxZXRet5&E~GMf1y&UP=K)iqyQ&DPf&qz+K(XYM7)zHP>F#(f^s37JqVv` z(3*Db=FaSxy}dVQSdPPS*qS3a+^mo0qGYRKn7}6N3Y=YTei@A3{m0YpCYMewRn86$ IUVS+K4Zq#t=Kufz literal 0 HcmV?d00001 diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/subscription-pending-icon.imageset/Contents.json b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/subscription-pending-icon.imageset/Contents.json new file mode 100644 index 0000000000..d161417381 --- /dev/null +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/subscription-pending-icon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Info-Color-16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/subscription-pending-icon.imageset/Info-Color-16.pdf b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/subscription-pending-icon.imageset/Info-Color-16.pdf new file mode 100644 index 0000000000000000000000000000000000000000..42f3cc015dc1fe46421444dd220b6daad5be740b GIT binary patch literal 1745 zcma)7O^?(#5WV|X)MX?LQjFv{cr>Oq33Q7R z0Sr^wvTOv-MTy|pT0z;tc@7R<;*;@yYIboYr37o@f|L&P!C2hlUH8DN_8hhtGS6G! zR}z3>sk3^a_RJn^jOKtYC_RK%2oQ*~#z0EZ98Ct%5+{Zu?p1WKW1T=iDtoAB@cv+& zYP{;`GJU4eGS8tha_;66E=KBjf)>lzYM$)({c)go?=i-(nz#S|+fj45z3wL9Wp}mP zKJCBJyR`(<`&*v#V$GR0-OztMwB0~uP;RDl=l%ZJ?Qvq6k~4U@Jpdz69HG@LEl{1Y z&ELB#luCuchURn5`>%-QllliBt4z4BRi;94s0S{@&$q|zO@IA!=(j`nQ5lNrj&=E^ z;Jxz%cv@)v(IQNMX*twcR)v`J31k_4_p?`N^IsqdOW^Ua-Q9Etg^y12;sSEq8~WSs qKHb}0-M9Ds>ot^#l>e&xM_ijW0 literal 0 HcmV?d00001 diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ActivateSubscriptionAccessModel.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ActivateSubscriptionAccessModel.swift index 41063cca08..3e14765641 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ActivateSubscriptionAccessModel.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ActivateSubscriptionAccessModel.swift @@ -17,12 +17,13 @@ // import Foundation +import Subscription public final class ActivateSubscriptionAccessModel: SubscriptionAccessModel, PurchaseRestoringSubscriptionAccessModel { private var actionHandlers: SubscriptionAccessActionHandlers public var title = UserText.activateModalTitle - public var description = UserText.activateModalDescription + public var description = UserText.activateModalDescription(platform: SubscriptionPurchaseEnvironment.current) public var email: String? public var emailLabel: String { UserText.email } @@ -31,7 +32,7 @@ public final class ActivateSubscriptionAccessModel: SubscriptionAccessModel, Pur public private(set) var shouldShowRestorePurchase: Bool public var restorePurchaseDescription = UserText.restorePurchaseDescription - public var restorePurchaseButtonTitle = UserText.restorePurchasesButton + public var restorePurchaseButtonTitle = UserText.restorePurchaseButton public init(actionHandlers: SubscriptionAccessActionHandlers, shouldShowRestorePurchase: Bool) { self.actionHandlers = actionHandlers diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ShareSubscriptionAccessModel.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ShareSubscriptionAccessModel.swift index 5ebc6d5433..9e007f4e7e 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ShareSubscriptionAccessModel.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ShareSubscriptionAccessModel.swift @@ -21,7 +21,7 @@ import Subscription public final class ShareSubscriptionAccessModel: SubscriptionAccessModel { public var title = UserText.shareModalTitle - public var description = UserText.shareModalDescription + public var description = UserText.shareModalDescription(platform: SubscriptionPurchaseEnvironment.current) private let subscriptionAppGroup: String private var actionHandlers: SubscriptionAccessActionHandlers diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/SubscriptionAccessView.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/SubscriptionAccessView.swift index 8be8b6f333..bf1931a6fc 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/SubscriptionAccessView.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/SubscriptionAccessView.swift @@ -115,6 +115,7 @@ public struct SubscriptionAccessView: View { Button("Cancel") { dismiss() } + .buttonStyle(DismissActionButtonStyle()) } .padding(.horizontal, 20) .padding(.vertical, 16) diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift index 05c1ff1a5f..f90aba85ad 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift @@ -17,6 +17,7 @@ // import Foundation +import Subscription enum UserText { // MARK: - Subscription preferences @@ -42,11 +43,25 @@ enum UserText { // MARK: Preferences when subscription is active static let preferencesSubscriptionActiveHeader = NSLocalizedString("subscription.preferences.subscription.active.header", value: "Privacy Pro is active on this device", comment: "Header for the subscription preferences pane when the subscription is active") - static func preferencesSubscriptionActiveCaption(formattedDate: String) -> String { - let localized = NSLocalizedString("subscription.preferences.subscription.active.caption", value: "Your monthly Privacy Pro subscription renews on %@", comment: "Caption for the subscription preferences pane when the subscription is active") + + static func preferencesSubscriptionActiveRenewCaption(period: String, formattedDate: String) -> String { + let localized = NSLocalizedString("subscription.preferences.subscription.active.renew.caption", value: "Your %@ Privacy Pro subscription renews on %@.", comment: "Caption for the subscription preferences pane when the subscription is active and will renew. First parameter is renewal period (monthly/yearly). Second parameter is date.") + return String(format: localized, period, formattedDate) + } + + static func preferencesSubscriptionActiveExpireCaption(period: String, formattedDate: String) -> String { + let localized = NSLocalizedString("subscription.preferences.subscription.active.expire.caption", value: "Your %@ Privacy Pro subscription expires on %@.", comment: "Caption for the subscription preferences pane when the subscription is active but will expire. First parameter is renewal period (monthly/yearly). Second parameter is date.") + return String(format: localized, period, formattedDate) + } + + static func preferencesSubscriptionExpiredCaption(formattedDate: String) -> String { + let localized = NSLocalizedString("subscription.preferences.subscription.expired.caption", value: "Your Privacy Pro subscription expired on %@.", comment: "Caption for the subscription preferences pane when the subscription has expired. The parameter is date of expiry.") return String(format: localized, formattedDate) } + static let monthlySubscriptionBillingPeriod = NSLocalizedString("subscription.billing.period.monthly", value: "Monthly", comment: "Type of subscription billing period that lasts a month") + static let yearlySubscriptionBillingPeriod = NSLocalizedString("subscription.billing.period.yearly", value: "Yearly", comment: "Type of subscription billing period that lasts a year") + static let addToAnotherDeviceButton = NSLocalizedString("subscription.preferences.add.to.another.device.button", value: "Add to Another Device…", comment: "Button to add subscription to another device") static let manageSubscriptionButton = NSLocalizedString("subscription.preferences.manage.subscription.button", value: "Manage Subscription", comment: "Button to manage subscription") static let changePlanOrBillingButton = NSLocalizedString("subscription.preferences.change.plan.or.billing.button", value: "Change Plan or Billing...", comment: "Button to add subscription to another device") @@ -59,6 +74,15 @@ enum UserText { static let purchaseButton = NSLocalizedString("subscription.preferences.purchase.button", value: "Get Privacy Pro", comment: "Button to open a page where user can learn more and purchase the subscription") static let haveSubscriptionButton = NSLocalizedString("subscription.preferences.i.have.a.subscription.button", value: "I Have a Subscription", comment: "Button enabling user to activate a subscription user bought earlier or on another device") + // MARK: Preferences when subscription activation is pending + static let preferencesSubscriptionPendingHeader = NSLocalizedString("subscription.preferences.subscription.pending.header", value: "Your Subscription is Being Activated", comment: "Header for the subscription preferences pane when the subscription activation is pending") + static let preferencesSubscriptionPendingCaption = NSLocalizedString("subscription.preferences.subscription.pending.caption", value: "This is taking longer than usual, please check back later.", comment: "Caption for the subscription preferences pane when the subscription activation is pending") + + // MARK: Preferences when subscription is expired + static let preferencesSubscriptionExpiredCaption = NSLocalizedString("subscription.preferences.subscription.expired.caption", value: "Subscribe again to continue using Privacy Pro.", comment: "Caption for the subscription preferences pane when the subscription activation is pending") + + static let manageDevicesButton = NSLocalizedString("subscription.preferences.manage.devices.button", value: "Manage Devices", comment: "Button to manage devices") + // MARK: - Change plan or billing dialogs static let changeSubscriptionDialogTitle = NSLocalizedString("subscription.dialog.change.title", value: "Change Plan or Billing", comment: "Change plan or billing dialog title") static let changeSubscriptionGoogleDialogDescription = NSLocalizedString("subscription.dialog.change.google.description", value: "Your subscription was purchased through the Google Play Store. To change your plan or billing settings, please open Google Play Store subscription settings on a device signed in to the same Google Account used to purchase your subscription.", comment: "Change plan or billing dialog subtitle description for subscription purchased via Google") @@ -76,14 +100,28 @@ enum UserText { // MARK: - Activate subscription modal static let activateModalTitle = NSLocalizedString("subscription.activate.modal.title", value: "Activate your subscription on this device", comment: "Activate subscription modal view title") - static let activateModalDescription = NSLocalizedString("subscription.activate.modal.description", value: "Access your subscription on other devices via Apple ID or an 
email address.", comment: "Activate subscription modal view subtitle description") + static func activateModalDescription(platform: SubscriptionPurchaseEnvironment.Environment) -> String { + switch platform { + case .appStore: + NSLocalizedString("subscription.appstore.activate.modal.description", value: "Access your subscription on other devices via Apple ID or an email address.", comment: "Activate subscription modal view subtitle description") + case .stripe: + NSLocalizedString("subscription.activate.modal.description", value: "Access your subscription on other devices via an email address.", comment: "Activate subscription modal view subtitle description") + } + } - static let activateModalEmailDescription = NSLocalizedString("subscription.activate.modal.email.description", value: "Use your email to access your subscription on this device.", comment: "Activate subscription modal description for email address channel") + static let activateModalEmailDescription = NSLocalizedString("subscription.activate.modal.email.description", value: "Use your email to activate your subscription on this device.", comment: "Activate subscription modal description for email address channel") static let restorePurchaseDescription = NSLocalizedString("subscription.activate.modal.restore.purchase.description", value: "Your subscription is automatically available in DuckDuckGo on any device signed in to your Apple ID.", comment: "Activate subscription modal description via restore purchase from Apple ID") // MARK: - Share subscription modal - static let shareModalTitle = NSLocalizedString("subscription.share.modal.title", value: "Use your subscription on all your devices", comment: "Share subscription modal view title") - static let shareModalDescription = NSLocalizedString("subscription.share.modal.description", value: "Access your Privacy Pro subscription on any of your devices via Sync, Apple ID or by adding an email address.", comment: "Share subscription modal view subtitle description") + static let shareModalTitle = NSLocalizedString("subscription.share.modal.title", value: "Use your subscription on other devices", comment: "Share subscription modal view title") + static func shareModalDescription(platform: SubscriptionPurchaseEnvironment.Environment) -> String { + switch platform { + case .appStore: + NSLocalizedString("subscription.appstore.share.modal.description", value: "Access your subscription via Apple ID or by adding an email address.", comment: "Share subscription modal view subtitle description") + case .stripe: + NSLocalizedString("subscription.share.modal.description", value: "Activate your Privacy Pro subscription via an email address.", comment: "Share subscription modal view subtitle description") + } + } static let shareModalHasEmailDescription = NSLocalizedString("subscription.share.modal.has.email.description", value: "Use this email to activate your subscription on other devices. Open the DuckDuckGo app on another device and find Privacy Pro in browser settings.", comment: "Share subscription modal description for email address channel") static let shareModalNoEmailDescription = NSLocalizedString("subscription.share.modal.no.email.description", value: "Add an email address to access your subscription in DuckDuckGo on other devices. We’ll only use this address to verify your subscription.", comment: "Share subscription modal description for email address channel") @@ -91,7 +129,7 @@ enum UserText { static let restorePurchasesDescription = NSLocalizedString("subscription.share.modal.restore.purchases.description", value: "Your subscription is automatically available in DuckDuckGo on any device signed in to your Apple ID.", comment: "Share subscription modal description for restoring Apple ID purchases") // MARK: - Activate/share modal buttons - static let restorePurchasesButton = NSLocalizedString("subscription.modal.restore.purchases.button", value: "Restore Purchases", comment: "Button for restoring past subscription purchases") + static let restorePurchaseButton = NSLocalizedString("subscription.modal.restore.purchase.button", value: "Restore Purchase", comment: "Button for restoring past subscription purchase") static let manageEmailButton = NSLocalizedString("subscription.modal.manage.email.button", value: "Manage Email", comment: "Button for opening manage email address page") static let enterEmailButton = NSLocalizedString("subscription.modal.enter.email.button", value: "Enter Email", comment: "Button for opening page to enter email address") static let addEmailButton = NSLocalizedString("subscription.modal.add.email.button", value: "Add Email", comment: "Button for opening page to add email address") @@ -103,8 +141,7 @@ enum UserText { static let restoreButtonTitle = NSLocalizedString("subscription.alert.button.restore", value: "Restore", comment: "Alert button for restoring past subscription purchases") static let somethingWentWrongAlertTitle = NSLocalizedString("subscription.alert.something.went.wrong.title", value: "Something Went Wrong", comment: "Alert title when unknown error has occurred") - static let somethingWentWrongAlertDescription = NSLocalizedString("subscription.alert.something.went.wrong.description", value: "The App Store was not able to process your purchase. Please try again later.", comment: "Alert message when unknown error has occurred") - static let somethingWentWrongStripeAlertDescription = NSLocalizedString("subscription.alert.something.went.wrong.stripe.description", value: "We were not able to start your purchase process. Please try again later.", comment: "Alert message when unknown error has occurred") + static let somethingWentWrongAlertDescription = NSLocalizedString("subscription.alert.something.went.wrong.description", value: "We’re having trouble connecting. Please try again later.", comment: "Alert message when unknown error has occurred") static let subscriptionNotFoundAlertTitle = NSLocalizedString("subscription.alert.subscription.not.found.title", value: "Subscription Not Found", comment: "Alert title when subscription was not found") static let subscriptionNotFoundAlertDescription = NSLocalizedString("subscription.alert.subscription.not.found.description", value: "We couldn’t find a subscription associated with this Apple ID.", comment: "Alert message when subscription was not found") From 4167e4d43f9efae33ff11ba39970751e3ba8e761 Mon Sep 17 00:00:00 2001 From: bwaresiak Date: Fri, 15 Mar 2024 13:38:32 +0100 Subject: [PATCH 14/51] Stub objects for Bookmarks DB (#2418) Task/Issue URL: https://app.asana.com/0/414235014887631/1206754257727808/f Description: Provide implementation for stub objects. Steps to test this PR: See BSK PR. --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 6 ++--- DuckDuckGo/Bookmarks/Model/Bookmark.swift | 25 ++++++++++++++----- DuckDuckGo/Sync/SyncDebugMenu.swift | 25 +++++++++++++++++++ .../DataBrokerProtection/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 7 files changed, 51 insertions(+), 13 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index a809af52f6..76a2d243ff 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -13749,7 +13749,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 123.0.1; + version = 124.0.0; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7f8548488a..e8258208b3 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "838cb53a8f7050d87ae6931b45ce126ece994359", - "version" : "123.0.1" + "branch" : "bartek/stub-objects", + "revision" : "968c429c464688c641d100eab584aa95b039e371" } }, { @@ -165,7 +165,7 @@ { "identity" : "trackerradarkit", "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/TrackerRadarKit", + "location" : "https://github.com/duckduckgo/TrackerRadarKit.git", "state" : { "revision" : "a6b7ba151d9dc6684484f3785293875ec01cc1ff", "version" : "1.2.2" diff --git a/DuckDuckGo/Bookmarks/Model/Bookmark.swift b/DuckDuckGo/Bookmarks/Model/Bookmark.swift index 99ca656b3f..c7c5f61076 100644 --- a/DuckDuckGo/Bookmarks/Model/Bookmark.swift +++ b/DuckDuckGo/Bookmarks/Model/Bookmark.swift @@ -23,25 +23,32 @@ internal class BaseBookmarkEntity: Identifiable { static func singleEntity(with uuid: String) -> NSFetchRequest { let request = BookmarkEntity.fetchRequest() - request.predicate = NSPredicate(format: "%K == %@ AND %K == NO", #keyPath(BookmarkEntity.uuid), uuid, #keyPath(BookmarkEntity.isPendingDeletion)) + request.predicate = NSPredicate(format: "%K == %@ AND %K == NO AND (%K == NO OR %K == nil)", + #keyPath(BookmarkEntity.uuid), uuid, + #keyPath(BookmarkEntity.isPendingDeletion), + #keyPath(BookmarkEntity.isStub), #keyPath(BookmarkEntity.isStub)) return request } static func favorite(with uuid: String, favoritesFolder: BookmarkEntity) -> NSFetchRequest { let request = BookmarkEntity.fetchRequest() - request.predicate = NSPredicate(format: "%K == %@ AND %K CONTAINS %@ AND %K == NO AND %K == NO", + request.predicate = NSPredicate(format: "%K == %@ AND %K CONTAINS %@ AND %K == NO AND %K == NO AND (%K == NO OR %K == nil)", #keyPath(BookmarkEntity.uuid), uuid as CVarArg, #keyPath(BookmarkEntity.favoriteFolders), favoritesFolder, #keyPath(BookmarkEntity.isFolder), - #keyPath(BookmarkEntity.isPendingDeletion)) + #keyPath(BookmarkEntity.isPendingDeletion), + #keyPath(BookmarkEntity.isStub), #keyPath(BookmarkEntity.isStub)) return request } static func entities(with identifiers: [String]) -> NSFetchRequest { let request = BookmarkEntity.fetchRequest() - request.predicate = NSPredicate(format: "%K IN %@ AND %K == NO", #keyPath(BookmarkEntity.uuid), identifiers, #keyPath(BookmarkEntity.isPendingDeletion)) + request.predicate = NSPredicate(format: "%K IN %@ AND %K == NO AND (%K == NO OR %K == nil)", + #keyPath(BookmarkEntity.uuid), identifiers, + #keyPath(BookmarkEntity.isPendingDeletion), + #keyPath(BookmarkEntity.isStub), #keyPath(BookmarkEntity.isStub)) return request } @@ -98,7 +105,10 @@ final class BookmarkFolder: BaseBookmarkEntity { static func bookmarkFoldersFetchRequest() -> NSFetchRequest { let request = BookmarkEntity.fetchRequest() - request.predicate = NSPredicate(format: "%K == YES AND %K == NO", #keyPath(BookmarkEntity.isFolder), #keyPath(BookmarkEntity.isPendingDeletion)) + request.predicate = NSPredicate(format: "%K == YES AND %K == NO AND (%K == NO OR %K == nil)", + #keyPath(BookmarkEntity.isFolder), + #keyPath(BookmarkEntity.isPendingDeletion), + #keyPath(BookmarkEntity.isStub), #keyPath(BookmarkEntity.isStub)) return request } @@ -135,7 +145,10 @@ final class Bookmark: BaseBookmarkEntity { static func bookmarksFetchRequest() -> NSFetchRequest { let request = BookmarkEntity.fetchRequest() - request.predicate = NSPredicate(format: "%K == NO AND %K == NO", #keyPath(BookmarkEntity.isFolder), #keyPath(BookmarkEntity.isPendingDeletion)) + request.predicate = NSPredicate(format: "%K == NO AND %K == NO AND (%K == NO OR %K == nil)", + #keyPath(BookmarkEntity.isFolder), + #keyPath(BookmarkEntity.isPendingDeletion), + #keyPath(BookmarkEntity.isStub), #keyPath(BookmarkEntity.isStub)) return request } diff --git a/DuckDuckGo/Sync/SyncDebugMenu.swift b/DuckDuckGo/Sync/SyncDebugMenu.swift index 6d60705595..da1fe3e0be 100644 --- a/DuckDuckGo/Sync/SyncDebugMenu.swift +++ b/DuckDuckGo/Sync/SyncDebugMenu.swift @@ -18,6 +18,7 @@ import Foundation import DDGSync +import Bookmarks @MainActor final class SyncDebugMenu: NSMenu { @@ -32,6 +33,8 @@ final class SyncDebugMenu: NSMenu { .submenu(environmentMenu) NSMenuItem(title: "Reset Favicons Fetcher Onboarding Dialog", action: #selector(resetFaviconsFetcherOnboardingDialog)) .targetting(self) + NSMenuItem(title: "Populate Stub objects", action: #selector(createStubsForDebug)) + .targetting(self) } } @@ -78,6 +81,28 @@ final class SyncDebugMenu: NSMenu { #endif } + @objc func createStubsForDebug() { +#if DEBUG || REVIEW + let db = BookmarkDatabase.shared + + let context = db.db.makeContext(concurrencyType: .privateQueueConcurrencyType) + + context.performAndWait { + let root = BookmarkUtils.fetchRootFolder(context)! + + _ = BookmarkEntity.makeBookmark(title: "Non stub", url: "url", parent: root, context: context) + let stub = BookmarkEntity.makeBookmark(title: "Stub", url: "", parent: root, context: context) + stub.isStub = true + let emptyStub = BookmarkEntity.makeBookmark(title: "", url: "", parent: root, context: context) + emptyStub.isStub = true + emptyStub.title = nil + emptyStub.url = nil + + try? context.save() + } +#endif + } + @objc func resetFaviconsFetcherOnboardingDialog(_ sender: NSMenuItem) { UserDefaults.standard.set(false, forKey: UserDefaultsWrapper.Key.syncDidPresentFaviconsFetcherOnboarding.rawValue) } diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 8e88a31067..dd701eff21 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -29,7 +29,7 @@ let package = Package( targets: ["DataBrokerProtection"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "123.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "124.0.0"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index a5aa5f7c63..000dc4e3f9 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "123.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "124.0.0"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index fb17968848..98ceb2ad67 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "123.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "124.0.0"), .package(path: "../SwiftUIExtensions") ], targets: [ From 409350dcc856c7707adffa55c476a69e1e3a655f Mon Sep 17 00:00:00 2001 From: Pete Smith Date: Fri, 15 Mar 2024 12:56:28 +0000 Subject: [PATCH 15/51] Password Import: Ignore Excluded Sites When Importing from Chrome (#2404) Task/Issue URL: https://app.asana.com/0/0/1206830207129428/f CC: Description: This is a Fix/Improvement as part of macOS: Investigate & Action Top Failed Import Issues for Passwords. --- .../Logins/Chromium/ChromiumLoginReader.swift | 6 ++-- .../DataImport/ChromiumLoginReaderTests.swift | 34 ++++++++++++++++++ .../TestChromeData/Legacy Excluded/Login Data | Bin 0 -> 40960 bytes .../TestChromeData/Legacy/Login Data | Bin 40960 -> 40960 bytes .../TestChromeData/v32 Excluded/Login Data | Bin 0 -> 40960 bytes .../v32 Excluded/Login Data for Account | Bin 0 -> 40960 bytes .../TestChromeData/v32/Login Data | Bin 40960 -> 40960 bytes 7 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 UnitTests/DataImport/DataImportResources/TestChromeData/Legacy Excluded/Login Data create mode 100644 UnitTests/DataImport/DataImportResources/TestChromeData/v32 Excluded/Login Data create mode 100644 UnitTests/DataImport/DataImportResources/TestChromeData/v32 Excluded/Login Data for Account diff --git a/DuckDuckGo/DataImport/Logins/Chromium/ChromiumLoginReader.swift b/DuckDuckGo/DataImport/Logins/Chromium/ChromiumLoginReader.swift index 2c5b36ba8e..c423ec9025 100644 --- a/DuckDuckGo/DataImport/Logins/Chromium/ChromiumLoginReader.swift +++ b/DuckDuckGo/DataImport/Logins/Chromium/ChromiumLoginReader.swift @@ -65,9 +65,9 @@ final class ChromiumLoginReader { private let decryptionKey: String? private let decryptionKeyPrompt: ChromiumKeychainPrompting - private static let sqlSelectWithPasswordTimestamp = "SELECT signon_realm, username_value, password_value, date_password_modified FROM logins;" - private static let sqlSelectWithCreatedTimestamp = "SELECT signon_realm, username_value, password_value, date_created FROM logins;" - private static let sqlSelectWithoutTimestamp = "SELECT signon_realm, username_value, password_value FROM logins;" + private static let sqlSelectWithPasswordTimestamp = "SELECT signon_realm, username_value, password_value, date_password_modified, blacklisted_by_user FROM logins WHERE blacklisted_by_user != 1;" + private static let sqlSelectWithCreatedTimestamp = "SELECT signon_realm, username_value, password_value, date_created, blacklisted_by_user FROM logins WHERE blacklisted_by_user != 1;" + private static let sqlSelectWithoutTimestamp = "SELECT signon_realm, username_value, password_value, blacklisted_by_user FROM logins WHERE blacklisted_by_user != 1;" private let source: DataImport.Source diff --git a/UnitTests/DataImport/ChromiumLoginReaderTests.swift b/UnitTests/DataImport/ChromiumLoginReaderTests.swift index ba59c59bba..4c5d835e7a 100644 --- a/UnitTests/DataImport/ChromiumLoginReaderTests.swift +++ b/UnitTests/DataImport/ChromiumLoginReaderTests.swift @@ -22,7 +22,9 @@ import XCTest private struct ChromiumLoginStore { static let legacy: Self = .init(directory: "Legacy", decryptionKey: "0geUdf5dTuZmIrtd8Omf/Q==") + static let legacyExcluded: Self = .init(directory: "Legacy Excluded", decryptionKey: "0geUdf5dTuZmIrtd8Omf/Q==") static let v32: Self = .init(directory: "v32", decryptionKey: "IcBAbGhvYp70AP+5W5ojcw==") + static let v32Excluded: Self = .init(directory: "v32 Excluded", decryptionKey: "IcBAbGhvYp70AP+5W5ojcw==") let directory: String let decryptionKey: String @@ -64,6 +66,22 @@ class ChromiumLoginReaderTests: XCTestCase { XCTAssertEqual(logins[2].password, "password") } + func testImportFromVersion32_WithOnlyExcludedSites_IgnoresExcludedCredentials() throws { + // Given + let expectedResult: DataImportResult<[ImportedLoginCredential]> = .success([]) + let reader = ChromiumLoginReader( + chromiumDataDirectoryURL: ChromiumLoginStore.v32Excluded.databaseDirectoryURL, + source: .chrome, + decryptionKey: ChromiumLoginStore.v32Excluded.decryptionKey + ) + + // When + let actualResult = reader.readLogins() + + // Then + XCTAssertEqual(expectedResult, actualResult) + } + func testImportFromLegacyVersion() throws { let reader = ChromiumLoginReader( @@ -83,6 +101,22 @@ class ChromiumLoginReaderTests: XCTestCase { XCTAssertEqual(logins[0].password, "password") } + func testImportFromLegacyVersion_WithOnlyExcludedSites_IgnoresExcludedCredentials() { + // Given + let expectedResult: DataImportResult<[ImportedLoginCredential]> = .success([]) + let reader = ChromiumLoginReader( + chromiumDataDirectoryURL: ChromiumLoginStore.legacyExcluded.databaseDirectoryURL, + source: .chrome, + decryptionKey: ChromiumLoginStore.legacyExcluded.decryptionKey + ) + + // When + let actualResult = reader.readLogins() + + // Then + XCTAssertEqual(expectedResult, actualResult) + } + func testWhenImportingChromiumData_AndTheUserCancelsTheKeychainPrompt_ThenAnErrorIsReturned() { let mockPrompt = MockChromiumPrompt(returnValue: .userDeniedKeychainPrompt) let reader = ChromiumLoginReader( diff --git a/UnitTests/DataImport/DataImportResources/TestChromeData/Legacy Excluded/Login Data b/UnitTests/DataImport/DataImportResources/TestChromeData/Legacy Excluded/Login Data new file mode 100644 index 0000000000000000000000000000000000000000..0acb8dc848e3636e8d752fcf7b06458492928ab0 GIT binary patch literal 40960 zcmeI)PjA~~90%~nO%`Vb1BH&NNJS4jK&zT|gBwVUbPJ}CrEN$zda@kH%d9$f=ErGP z4sf8Hc0jNb>Wv$`fSz_5;>Lj!N5pLsmq~jA<@eZenzUWk6>XI1`$UgZKmYtZpXZ6} z+HF2tDY=ouuLWV#iuk)M&lH7y$T?$7#&L?zQ+$!)D-^R7)1ah340-2l?&>F6)@Rww zHp}j23z_?wZN=Yv4XOJKW~sr*+t1*zFr+6TP|4*c-kMZ|>(`x|UeIuTVG6h52Y#}n zUaS-g)goUkUn+ji2MX||ayNlbCsoYQvApKY@~Pa!?5wgDSvAiQH$3|6o5H!#c6@vP zZm!#WwXjes@_l8eeXHr{RC8uFo13^Wsj#@0-BMON3gUgU+pF`*`zd1j?nzJ`yUlOD z%%~HSlgh2Du}IR*WSborf0D+hgV3dkFx#QWuM{d5KQ2^wd8x|FtEH08Ejx09flQsZ zg%e6F)A5|9<40+=zMKuqYdd@CElY?_5Y`XY=;b6e7D`JCIv4F)(~S-m?w|Lx@xH#+ zEt-0pRz>90X$e(}MzMb=HP5owJy%e!S=%(_C=Mwl?CUhBLo#|jjW%1oe4R&b(-E?< z-XZn))MHX_ES`)f?wHu51=&}q?uwRYZJJUt9gNOhd%yNIr|yK-!C4UNZp-|_2?Nvi zTzft3vCbQg->cB_0;_I{$coxxuS6>l!lh43Y!J_6%W^}}?XBDEBwwy9UM^JD_@~7+ zURbRzEtY8oT`rcZI*-TXSt1%3dv7-QrQ%10)l!w8(|I%4aQ%i^^8$OF?CHeZJ<@7< zab>mGzXJP~P#OmFQm=Gc(8jN_BL%0AOL}N{#e@Avhf;P72&5or(SoXIXh_1$G7L_hh|RG^Ga17 ziNaZmX<^^#h`CK_q~`io6oh;29lhKLwI_!ogM374F`HgdZ$ENw#yh*ayI;kL-Hm*N znnu~i{vaQiq#vFC-1~lL6Kwv?zka>T9a~;@9r%*av1Jh@K#Nj>8>cXv@) zL0(>WHck!>j+RsoUS3`f1_lNYW}NBHD=G_QFfjH5ako2A4oEkHXdrOsMc0y1Qc_TC zrLSLJUanV^nv#-PqL-YXtDlpfo|&hQO_CEXJNcm87Yj3XNvPSp9GoB*zyJpWHr;T| zo5kebF>)}mw=l4;Vs8QZ&WwGsle&~&psb)fBiIagAn6QrG9y-VRN2ASVwZ%upMCQx vb#V?>cA!ai6C1528L$9tyve|RlN}VEo7v@+u$m3h&ce*g$iDfWxl#rIC+TQo delta 119 zcmZoTz|?SnX@WGP%tRSyRv8ApaF2~C3&c4X*~A#w#F%R~GjbF$Pp*@GvUvhyHzO01 z=)^|#&0=!z7&#c(S23`!Vz&pXFk_$Wq%Jjag5Ty<>f#)%EDQ_`b`u+|CK<2*Ro!D? WzsG(IsA?}eqvS+Kkgxrd@X73i=_55+D42CUeCtcPvbZ5X;8mcuR^2INqnhwOWgB1QeR z(lyH}d@m9IeD8bT``+(8hUE8<$159o(=+IfQ?KeCT@sRlC<@O~DhR?P{HEX+q{DC! z4^Ch&Mvey^P6}s!_`M0HOuk?`$Nk+Pb(A9kB!C2v01`j~51)WMAwwk+`L#g)N&W^q zOh^C;AOR$R1dzbjg23aFbRlKwuBVlqYEAddZOhQE8Fklm?4`vcF=-|hoSi)`N+(m* zs$SDvPxl(`{Nf>KqeSiqUl1lfg$*VofCL^P0rBp}#);X~S$Q>)gmkMU%W^U~B_yU2S3R%h z!aUkPHsoO%=NA@i1A_OX zf}3*ko6+;OyYiIG-3^@Chc}aL)7Cwweg+N~EN9oWhiV85=imC-Uq1PS!XzVUb;uL2 zA6kf;6@*dsVqlNWak9kbHFcp3f4uPAGhXH=_wV0-g$r97Ht!kyn2l&U6*&oPGo6}n zo#y~Vp{Bd;b*Elof-h^E)z5sk8feGib0yRl5how;+?kPrUiLd;a6%Gq<;HOvc|mmHyXv-+1NEug6nQ zOZFe{9ewBL?EODR9Dz9GmvD#)2_OL^fCP{L5_n(&FV3b`*`2t=_E*`{;|nrBf@gRB zFuY3G6En*TB=D^M;E%`2OJL{NNOp!l)+N~czew&0@D~#jKmter2_OL^FvbK9iPE&l zgJo{gfB(NNkk82NF>W$84GACtB!C2vz}OI&l@16bBoEZ!@Be%fVr-~jLy!OxKmter z2_S)k2(b7+-v1Axg!xDS2_OL^fCR>i0N(${t3Rz z1dsp{Kmtf$d6!?4TUr-|jFnk%Wa zl)IQ$X_s!=)~iMqN@iYrTq4T!wD{&@+@}8@hWu^3!-6lPX{T<&x;|Q?ZqXm)ip!UB zMOs)d(ZXgvpQU=)g9S;WnOWL!jXJANGc2QO*j`X9l5__^j^6WXG`pV$$Ja^i&8T;P{d)$pJLJw4WO88JzE&)BE= z^}=fR1w>noh|tYaf2c>)Gjp+oa^b`vr&tZzBHOa1me|rmLNNbNe)lN;(ZV{B01`j~ zNB{{Sf&ZGocOKkA5dSBCdhoJX0tp}iB!C2v0226rA}~L)GjhBr91&j?$S+7NQJI*I z{~`W@REf_?Q{oq~zd{D4hflyu5oLZ>Y|io65yZ2sie}n74s0N{)IOhT#f#PoE9#GE zTkh-8^+H5O)9jnh8jD%Mm!9ed1QWU4P>^Zr$A~gJE8diOP^xXNee{iv099MwZ9E7Q zhT*rK+6HPwgP@V(e)LsiLD4=%gNWWiP0TDEPbjOigW_TB25F1!rj-P9(M;rGntH>P@Yl-ar7Z9^4&4G zTzF*BJ4}>?b7C{)4`Qcoz);j6zUhxv({_zAj7t`0uR!eI)GhZ&2U_Ud&LWI&zhX39 zgXPn0Pcti-)k6v8$L9t)+OM>>*iU-+#-&N3oI5ApTH>A7Pp6;kP)C>c(};K*^bswp zVVu&iF8t;rN<>gMGYHcPXdyRw{HQCZ_x)VvyzK)75j7WpL3T4AlUmdoe2SPUJ6 z>IzH?_=HVwU;9!QndxfPM!y-5Xq-N{62l(osW+2p&LoMlv?Mlv;*bA*yR1Rr-mo+l zie}RrZm(#v17sfsnjUS^p|hr*3o6r`BSd-fNwI0Ry<_9iGhsTW!@(YDve(BR9h!FG zvlsfV!G?a=;gK+Y_asSBWv{2jL_(RH7F~WpwI&$+ISK!SbX-0i3T*VWMpn9G8Y6uF z|Lo!4=CB4NfCP{L5w^g-VN46&$Ic!btme_wI6+TtFN% z!tme8?ZdUF z1{NRy1b_e#00KbZjvz3aB&VbatSB)KkITzSyj|d!F3u*LPsU3I5{rO zNb35C8%|00_h)5SUM7wX}@PO({vP(4r0!i|Ul|@Bvu3MbYgPut3yqlFgz6ofzNbq#rX~$FbdhK~S=h{E~T3 zEeh{HwfZ6!;BeW-f{=~zLu#W#@c5nI$xzq4moCys^KWaYU_Nm!WT<+c z8VH)aiPu1l|64HXKCR`J&JDsq00;m9AOHk{1Ol@N4Yr~b#p^E^8s13UoK(BlUAE-> z6z=}4!e{sV<9fE{?>n$rzx{flt?mWe192<1Z<_nns*S5h@7-IA9pA;Rocj3NGgB_n zC2LOZ-nsblk+bja`Xsw}o#Tb#j`#K$X06@danYan^4@va`0Ss(^=3)M#MKLS9r$`% zO48D?TgP5GEfMz1nfk5!TD#YOcE!{z?90S@H$Q9tOn>IB*PqKvI`#46&mYjaQYJm7 zIeg_mA8b1Mj^SA5=O?bOeC6XOethMysq*Byx@E$Jd(AtyY-OKrU%ctlpVVLYYx?2E zSv6Nb49voVmc_;8YbpwPW}1ZwDzOhMx_@{DPQ-xn6&>-2Y>Wp z@}6<;J$>m{dt5%tyOm!$mwxg_wf+4kMtrz+L#4FoVurKsnQ`kcKXm$^n>O2eKOH}P z{D~=xr|()da${H9FMf0Gz?vmX*PPwc@_u&ClH=cOcfN>!F!9{^b(*QaG>uq0W?y>! zQK{!r^IxvT?XBXH#yqeYH!ZAW=Fhq3Bze8QId^^G+ShikJ#;AW+=arA7Hy;t9oT*9 z=LL=1T8`~_(z@G!Y}woX-AzxPUi04qV%Z}*Ka#Kixb_bi`hx`s00AHX1c1PuLty#+ z=+eA$N#Yhf{^j#T`mgcyUOb*iwc2b{hvkNL#%3h6&L2@O zr={Xqsa4YxOfR+0FUd?C6;Gr+JSL$+&dEgb!6kjNT%X-JSbc++q_O&MdRJWhUy)bc zq+{8$%WJ&VMn^%7Fl(l{+s5(Jipn2#71W!{%Dvq+U1h$xVpsFR;-=ZvjeK))wPdDjFZ3=_+$H6wUOuwp$vE zT`k>{dW2~e_Li!ul99$@qtRGVQ8uyMSTeDEa%stAWBDZc`j004fdvQv0U!VbfB+D< ziwMZ^f4KhNMGX$70s$ZZ1b_e#Km_3W58VI=00AHX1c1QZMz#0(TOD<~Xgcpa7pEv1tBJaiM^|>cNQILOvYwwURDvib6?D+W|j|JZ1f(q(1gy(5%5)fa};wI-?tHI2^q_;@tG z3*+|*^h}ejrru`e!ed%6S z6uUh>=fD!-9JNHXrK#FLi+;O{mj()pe1{t;w z9BGPOU>%(TFQQz=-owaN#E=pl3y@JUGQyflJ>GD>ftGj|dPxga7Cxmx`78?~zs^Og z_BeUbE3iF`+(se24K(lQ54N3iay~Q_Z2xqzllL-H|Nq9Y?s7x&zZZt2;`^2DPd{}9^+1Ai#vdy8Vo9ED`8Es7sR@7M<&DJ&pt#DN) zsf#o-44$qvPczN1w9&-|+U4ou-5rcw@HjdVX7qiU5CXYv!U)kzZ+tu7hb(4YGpr3Q zGt80wGoph9yIw?!(t!+g_#9!0$X=t@7ihG)saZJ2yPe#l!9m3OC698S;Xtk?Hi0ktRt!~Q5!IYBE zz!>EUHp zaYoQz)L^YO&!+p*RXMuJ8j(il%inZnWSVI8j3-K)b@PoDgtERj)N+sb}gR3k)Y%MG9$hdkjgJ|FxfM9QlS_yeP5)`VN>M`O<@aYvkDUDt^}+qKwwK zzcW;?*C%UrRl31Ast}CICIt-_#j_|4qjW{(cpy#f#2z1qx*~)8n`&?6-6H2e{ZjU` zJLNgbSV2sRLW>$Vx`@m-Rxvu?%lc3i4DZy}C2Do^$_G<4R%ylUXHIU zRT?dpOsurTs3wgi5ut6UjcA*Bn%QQy)|gxAU@!%lI-|K(j?&baT5C+TX4&{!v&9^d zg52e1n@tJgG*}zj8cY_;9NCAC63w+JET9tRr5@%x4Kf~08Rw4`0g3d}hel$!1qPf~ zuMgzJQ@YYpJn*pE{|C-i7h?jugeZ_P1nB&47j%f87jM9T(g*S=N>@>V2lzhi2F#fk zdnUd3fjSN-N#~2`^9O7PCwKjDgGadW`v*y6_q-DF=^p~n+q7C;UKTDY11cC`P-7D6 z0TnfTq6FmL6YN>}QDF>t{vZ0wJDdRs00AHX1c1QpMgZdfw_A&VK0p8n00AHX1csgf pJpT_pV>km400KY&2mpcGjR0K#Z?_f!eSiQE00KY&2n;=e{{!tc{`UX? literal 0 HcmV?d00001 diff --git a/UnitTests/DataImport/DataImportResources/TestChromeData/v32/Login Data b/UnitTests/DataImport/DataImportResources/TestChromeData/v32/Login Data index 76ab8de045152236833e62ee84aebff573ed9e65..6ffc00b624f06022593919113f0210425583e72e 100644 GIT binary patch delta 281 zcmZoTz|?SnX@WE(=R_H2R!#=JaIcLi3&gpYnL`*@3z>VELpC;YGS^oyFff$3^U8_> z=^P-=aOdUaHD=)D;N$@Ez#tSP#ET-tz`()4kx^pCDG8PXN+YR-!)gx0022tIGX?f delta 90 zcmV-g0Hyzczyg540+1U42$38^1qc8xS4pvCpce=N1B?I#aRZF8fq( Date: Fri, 15 Mar 2024 14:03:57 +0100 Subject: [PATCH 16/51] Roll back CPM post-rollout cleanup (#2430) Task/Issue URL: https://app.asana.com/0/414235014887631/1206850469068241/f Tech Design URL: CC: **Description**: Updates the BSK reference as a part of rolling back https://github.com/duckduckgo/macos-browser/pull/2316 **Steps to test this PR**: 1. CI is green --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 6 +++--- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 76a2d243ff..23d4cdecf1 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -13749,7 +13749,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 124.0.0; + version = 124.1.0; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index e8258208b3..5d9e06d99f 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "branch" : "bartek/stub-objects", - "revision" : "968c429c464688c641d100eab584aa95b039e371" + "revision" : "bcafd206465427c560f9f581def57d8eef53748c", + "version" : "124.1.0" } }, { @@ -165,7 +165,7 @@ { "identity" : "trackerradarkit", "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/TrackerRadarKit.git", + "location" : "https://github.com/duckduckgo/TrackerRadarKit", "state" : { "revision" : "a6b7ba151d9dc6684484f3785293875ec01cc1ff", "version" : "1.2.2" diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index dd701eff21..e07c2c873c 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -29,7 +29,7 @@ let package = Package( targets: ["DataBrokerProtection"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "124.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "124.1.0"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 000dc4e3f9..5c07540155 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "124.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "124.1.0"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 98ceb2ad67..c29571c2ce 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "124.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "124.1.0"), .package(path: "../SwiftUIExtensions") ], targets: [ From c299d42a0f292e156efeb392d89195c41de93e13 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Fri, 15 Mar 2024 10:36:10 -0300 Subject: [PATCH 17/51] DBP: Debug scan model implementation (#2421) --- .../CCF/DataBrokerProtectionFeature.swift | 4 +- .../CCF/WebViewHandler.swift | 71 +++ .../DebugUI/DataBrokerRunCustomJSONView.swift | 60 ++- .../DataBrokerRunCustomJSONViewModel.swift | 425 ++++++++++++++++-- .../DebugUI/DebugScanOperation.swift | 181 ++++++++ .../Model/DataBrokerProtectionProfile.swift | 2 +- .../Model/ProfileQuery.swift | 15 + .../Operations/OptOutOperation.swift | 2 +- .../Operations/ScanOperation.swift | 2 +- .../DataBrokerProtectionFeatureTests.swift | 2 +- .../DataBrokerProtectionTests/Mocks.swift | 8 + 11 files changed, 700 insertions(+), 72 deletions(-) create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanOperation.swift diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/CCF/DataBrokerProtectionFeature.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/CCF/DataBrokerProtectionFeature.swift index c596488888..be97096c4e 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/CCF/DataBrokerProtectionFeature.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/CCF/DataBrokerProtectionFeature.swift @@ -24,7 +24,7 @@ import Common protocol CCFCommunicationDelegate: AnyObject { func loadURL(url: URL) async - func extractedProfiles(profiles: [ExtractedProfile]) async + func extractedProfiles(profiles: [ExtractedProfile], meta: [String: Any]?) async func captchaInformation(captchaInfo: GetCaptchaInfoResponse) async func solveCaptcha(with response: SolveCaptchaResponse) async func success(actionId: String, actionType: ActionType) async @@ -101,7 +101,7 @@ struct DataBrokerProtectionFeature: Subfeature { await delegate?.onError(error: DataBrokerProtectionError.malformedURL) } case .extract(let profiles): - await delegate?.extractedProfiles(profiles: profiles) + await delegate?.extractedProfiles(profiles: profiles, meta: success.meta) case .getCaptchaInfo(let captchaInfo): await delegate?.captchaInformation(captchaInfo: captchaInfo) case .solveCaptcha(let response): diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/CCF/WebViewHandler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/CCF/WebViewHandler.swift index c89644eec4..ab98fa98b9 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/CCF/WebViewHandler.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/CCF/WebViewHandler.swift @@ -25,6 +25,8 @@ import Common protocol WebViewHandler: NSObject { func initializeWebView(showWebView: Bool) async func load(url: URL) async throws + func takeSnaphost(path: String, fileName: String) async throws + func saveHTML(path: String, fileName: String) async throws func waitForWebViewLoad(timeoutInSeconds: Int) async throws func finish() async func execute(action: Action, data: CCFRequestData) async @@ -122,6 +124,75 @@ final class DataBrokerProtectionWebViewHandler: NSObject, WebViewHandler { func evaluateJavaScript(_ javaScript: String) async throws { _ = webView?.evaluateJavaScript(javaScript, in: nil, in: WKContentWorld.page) } + + func takeSnaphost(path: String, fileName: String) async throws { + let script = "document.body.scrollHeight" + + let result = try await webView?.evaluateJavaScript(script) + + if let height = result as? CGFloat { + webView?.frame = CGRect(origin: .zero, size: CGSize(width: 1024, height: height)) + let configuration = WKSnapshotConfiguration() + configuration.rect = CGRect(x: 0, y: 0, width: webView?.frame.size.width ?? 0.0, height: height) + if let image = try await webView?.takeSnapshot(configuration: configuration) { + saveToDisk(image: image, path: path, fileName: fileName) + } + } + } + + func saveHTML(path: String, fileName: String) async throws { + let result = try await webView?.evaluateJavaScript("document.documentElement.outerHTML") + let fileManager = FileManager.default + + if let htmlString = result as? String { + do { + if !fileManager.fileExists(atPath: path) { + try fileManager.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil) + } + + let fileURL = URL(fileURLWithPath: "\(path)/\(fileName)") + try htmlString.write(to: fileURL, atomically: true, encoding: .utf8) + print("HTML content saved to file: \(fileURL)") + } catch { + print("Error writing HTML content to file: \(error)") + } + } + } + + private func saveToDisk(image: NSImage, path: String, fileName: String) { + guard let tiffData = image.tiffRepresentation else { + // Handle the case where tiff representation is not available + return + } + + // Create a bitmap representation from the tiff data + guard let bitmapImageRep = NSBitmapImageRep(data: tiffData) else { + // Handle the case where bitmap representation cannot be created + return + } + + let fileManager = FileManager.default + + if !fileManager.fileExists(atPath: path) { + do { + try fileManager.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil) + } catch { + print("Error creating folder: \(error)") + } + } + + if let pngData = bitmapImageRep.representation(using: .png, properties: [:]) { + // Save the PNG data to a file + do { + let fileURL = URL(fileURLWithPath: "\(path)/\(fileName)") + try pngData.write(to: fileURL) + } catch { + print("Error writing PNG: \(error)") + } + } else { + print("Error png data was not respresented") + } + } } extension DataBrokerProtectionWebViewHandler: WKNavigationDelegate { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONView.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONView.swift index 33671421d5..22b8c6b90a 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONView.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONView.swift @@ -28,26 +28,42 @@ struct DataBrokerRunCustomJSONView: View { if viewModel.results.isEmpty { VStack(alignment: .leading) { Text("macOS App version: \(viewModel.appVersion())") - Text("C-S-S version: \(viewModel.contentScopeScriptsVersion())") Divider() - HStack { - TextField("First name", text: $viewModel.firstName) - .padding() - TextField("Last name", text: $viewModel.lastName) - .padding() - TextField("Middle", text: $viewModel.middle) - .padding() + ForEach(viewModel.names.indices, id: \.self) { index in + HStack { + TextField("First name", text: $viewModel.names[index].first) + .padding() + TextField("Middle", text: $viewModel.names[index].middle) + .padding() + TextField("Last name", text: $viewModel.names[index].last) + .padding() + } + } + + Button("Add other name") { + viewModel.names.append(.empty()) } Divider() - HStack { - TextField("City", text: $viewModel.city) - .padding() - TextField("State", text: $viewModel.state) - .padding() + ForEach(viewModel.addresses.indices, id: \.self) { index in + HStack { + TextField("City", text: $viewModel.addresses[index].city) + .padding() + TextField("State (two characters format)", text: $viewModel.addresses[index].state) + .onChange(of: viewModel.addresses[index].state) { newValue in + if newValue.count > 2 { + viewModel.addresses[index].state = String(newValue.prefix(2)) + } + } + .padding() + } + } + + Button("Add other address") { + viewModel.addresses.append(.empty()) } Divider() @@ -76,6 +92,14 @@ struct DataBrokerRunCustomJSONView: View { Button("Run") { viewModel.runJSON(jsonString: jsonText) } + + if viewModel.isRunningOnAllBrokers { + ProgressView("Scanning...") + } else { + Button("Run all brokers") { + viewModel.runAllBrokers() + } + } } .padding() .frame(minWidth: 600, minHeight: 800) @@ -88,19 +112,19 @@ struct DataBrokerRunCustomJSONView: View { } else { VStack { VStack { - List(viewModel.results, id: \.name) { extractedProfile in + List(viewModel.results, id: \.id) { scanResult in HStack { - Text(extractedProfile.name ?? "No name") + Text(scanResult.extractedProfile.name ?? "No name") .padding(.horizontal, 10) Divider() - Text(extractedProfile.addresses?.first?.fullAddress ?? "No address") + Text(scanResult.extractedProfile.addresses?.first?.fullAddress ?? "No address") .padding(.horizontal, 10) Divider() - Text(extractedProfile.relatives?.joined(separator: ",") ?? "No relatives") + Text(scanResult.extractedProfile.relatives?.joined(separator: ",") ?? "No relatives") .padding(.horizontal, 10) Divider() Button("Opt-out") { - viewModel.runOptOut(extractedProfile: extractedProfile) + viewModel.runOptOut(scanResult: scanResult) } } }.navigationTitle("Results") diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift index e0241d5542..0cb75a8f1d 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift @@ -22,6 +22,49 @@ import Common import ContentScopeScripts import Combine +struct ExtractedAddress: Codable { + let state: String + let city: String +} + +struct UserData: Codable { + let firstName: String + let lastName: String + let middleName: String? + let state: String + let email: String? + let city: String + let age: Int + let addresses: [ExtractedAddress] +} + +struct ProfileUrl: Codable { + let profileUrl: String + let identifier: String +} + +struct ScrapedData: Codable { + let name: String? + let alternativeNamesList: [String]? + let age: String? + let addressCityState: String? + let addressCityStateList: [ExtractedAddress]? + let relativesList: [String]? + let profileUrl: ProfileUrl? +} + +struct ExtractResult: Codable { + let scrapedData: ScrapedData + let result: Bool + let score: Int + let matchedFields: [String] +} + +struct Metadata: Codable { + let userData: UserData + let extractResults: [ExtractResult] +} + struct AlertUI { var title: String = "" var description: String = "" @@ -30,28 +73,80 @@ struct AlertUI { AlertUI(title: "No results", description: "No results were found.") } + static func finishedScanningAllBrokers() -> AlertUI { + AlertUI(title: "Finished!", description: "We finished scanning all brokers. You should find the data inside ~/Desktop/PIR-Debug/") + } + static func from(error: DataBrokerProtectionError) -> AlertUI { AlertUI(title: error.title, description: error.description) } } -final class DataBrokerRunCustomJSONViewModel: ObservableObject { +final class NameUI: ObservableObject { + let id = UUID() + @Published var first: String + @Published var middle: String + @Published var last: String + + init(first: String, middle: String = "", last: String) { + self.first = first + self.middle = middle + self.last = last + } - @Published var firstName: String = "" - @Published var lastName: String = "" - @Published var middle: String = "" - @Published var city: String = "" - @Published var state: String = "" + static func empty() -> NameUI { + .init(first: "", middle: "", last: "") + } + + func toModel() -> DataBrokerProtectionProfile.Name { + .init(firstName: first, lastName: last, middleName: middle.isEmpty ? nil : middle) + } +} + +final class AddressUI: ObservableObject { + let id = UUID() + @Published var city: String + @Published var state: String + + init(city: String, state: String) { + self.city = city + self.state = state + } + + static func empty() -> AddressUI { + .init(city: "", state: "") + } + + func toModel() -> DataBrokerProtectionProfile.Address { + .init(city: city, state: state) + } +} + +struct ScanResult { + let id = UUID() + let dataBroker: DataBroker + let profileQuery: ProfileQuery + let extractedProfile: ExtractedProfile +} + +final class DataBrokerRunCustomJSONViewModel: ObservableObject { @Published var birthYear: String = "" - @Published var results = [ExtractedProfile]() + @Published var results = [ScanResult]() @Published var showAlert = false @Published var showNoResults = false + @Published var isRunningOnAllBrokers = false + @Published var names = [NameUI.empty()] + @Published var addresses = [AddressUI.empty()] + var alert: AlertUI? var selectedDataBroker: DataBroker? let brokers: [DataBroker] private let runnerProvider: OperationRunnerProvider + private let privacyConfigManager: PrivacyConfigurationManaging + private let contentScopeProperties: ContentScopeProperties + private let csvColumns = ["name_input", "age_input", "city_input", "state_input", "name_scraped", "age_scraped", "address_scraped", "relatives_scraped", "url", "broker name", "screenshot_id", "error", "matched_fields", "result_match", "expected_match"] init() { let privacyConfigurationManager = PrivacyConfigurationManagingMock() @@ -69,45 +164,202 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject { let contentScopeProperties = ContentScopeProperties(gpcEnabled: false, sessionKey: sessionKey, featureToggles: features) + self.runnerProvider = DataBrokerOperationRunnerProvider( privacyConfigManager: privacyConfigurationManager, contentScopeProperties: contentScopeProperties, emailService: EmailService(), captchaService: CaptchaService()) + self.privacyConfigManager = privacyConfigurationManager + self.contentScopeProperties = contentScopeProperties let fileResources = FileResources() self.brokers = fileResources.fetchBrokerFromResourceFiles() ?? [DataBroker]() } - func runJSON(jsonString: String) { - if firstName.isEmpty || lastName.isEmpty || city.isEmpty || state.isEmpty || birthYear.isEmpty { + func runAllBrokers() { + isRunningOnAllBrokers = true + + let brokerProfileQueryData = createBrokerProfileQueryData() + + Task.detached { + var scanResults = [DebugScanReturnValue]() + let semaphore = DispatchSemaphore(value: 10) + try await withThrowingTaskGroup(of: DebugScanReturnValue.self) { group in + for queryData in brokerProfileQueryData { + semaphore.wait() + let debugScanOperation = DebugScanOperation(privacyConfig: self.privacyConfigManager, prefs: self.contentScopeProperties, query: queryData) { + true + } + + group.addTask { + defer { + semaphore.signal() + } + do { + return try await debugScanOperation.run(inputValue: (), stageCalculator: FakeStageDurationCalculator(), showWebView: false) + } catch { + return DebugScanReturnValue(brokerURL: "ERROR - with broker: \(queryData.dataBroker.name)", extractedProfiles: [ExtractedProfile](), brokerProfileQueryData: queryData) + } + } + } + + for try await result in group { + scanResults.append(result) + } + + self.formCSV(with: scanResults) + + self.finishLoading() + } + } + } + + private func finishLoading() { + DispatchQueue.main.async { + self.alert = AlertUI.finishedScanningAllBrokers() self.showAlert = true - self.alert = AlertUI(title: "Error", description: "Some required fields were not entered.") - return + self.isRunningOnAllBrokers = false + } + } + + private func formCSV(with scanResults: [DebugScanReturnValue]) { + var csvText = csvColumns.map { $0 }.joined(separator: ",") + csvText.append("\n") + + for result in scanResults { + if let error = result.error { + csvText.append(append(error: error, for: result)) + } else { + csvText.append(append(result)) + } + } + + save(csv: csvText) + } + + private func append(error: Error, for result: DebugScanReturnValue) -> String { + if let dbpError = error as? DataBrokerProtectionError { + if dbpError.is404 { + return createRowFor(matched: false, result: result, error: "404 - No results") + } else { + return createRowFor(matched: false, result: result, error: "\(dbpError.title)-\(dbpError.description)") + } + } else { + return createRowFor(matched: false, result: result, error: error.localizedDescription) + } + } + + private func append(_ result: DebugScanReturnValue) -> String { + var resultsText = "" + + if let meta = result.meta{ + do { + let jsonData = try JSONSerialization.data(withJSONObject: meta, options: []) + let decoder = JSONDecoder() + let decodedMeta = try decoder.decode(Metadata.self, from: jsonData) + + for extractedResult in decodedMeta.extractResults { + resultsText.append(createRowFor(matched: extractedResult.result, result: result, extractedResult: extractedResult)) + } + } catch { + print("Error decoding JSON: \(error)") + } + } else { + print("No meta object") } + return resultsText + } + + private func createRowFor(matched: Bool, + result: DebugScanReturnValue, + error: String? = nil, + extractedResult: ExtractResult? = nil) -> String { + let matchedString = matched ? "TRUE" : "FALSE" + let profileQuery = result.brokerProfileQueryData.profileQuery + + var csvRow = "" + + csvRow.append("\(profileQuery.fullName),") // Name (input) + csvRow.append("\(profileQuery.age),") // Age (input) + csvRow.append("\(profileQuery.city),") // City (input) + csvRow.append("\(profileQuery.state),") // State (input) + + if let extractedResult = extractedResult { + csvRow.append("\(extractedResult.scrapedData.nameCSV),") // Name (scraped) + csvRow.append("\(extractedResult.scrapedData.ageCSV),") // Age (scraped) + csvRow.append("\(extractedResult.scrapedData.addressesCSV),") // Address (scraped) + csvRow.append("\(extractedResult.scrapedData.relativesCSV),") // Relatives (matched) + } else { + csvRow.append(",") // Name (scraped) + csvRow.append(",") // Age (scraped) + csvRow.append(",") // Address (scraped) + csvRow.append(",") // Relatives (scraped) + } + + csvRow.append("\(result.brokerURL),") // Broker URL + csvRow.append("\(result.brokerProfileQueryData.dataBroker.name),") // Broker Name + csvRow.append("\(profileQuery.id ?? 0)_\(result.brokerProfileQueryData.dataBroker.name),") // Screenshot name + + if let error = error { + csvRow.append("\(error),") // Error + } else { + csvRow.append(",") // Error empty + } + + if let extractedResult = extractedResult { + csvRow.append("\(extractedResult.matchedFields.joined(separator: "-")),") // matched_fields + } else { + csvRow.append(",") // matched_fields + } + + csvRow.append("\(matchedString),") // result_match + csvRow.append(",") // expected_match + csvRow.append("\n") + + return csvRow + } + + private func save(csv: String) { + do { + if let desktopPath = FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask).first?.relativePath { + let path = desktopPath + "/PIR-Debug" + let fileName = "output.csv" + let fileURL = URL(fileURLWithPath: "\(path)/\(fileName)") + try csv.write(to: fileURL, atomically: true, encoding: .utf8) + } else { + os_log("Error getting path") + } + } catch { + os_log("Error writing to file: \(error)") + } + } + + func runJSON(jsonString: String) { if let data = jsonString.data(using: .utf8) { do { let decoder = JSONDecoder() let dataBroker = try decoder.decode(DataBroker.self, from: data) self.selectedDataBroker = dataBroker let brokerProfileQueryData = createBrokerProfileQueryData(for: dataBroker) - let runner = runnerProvider.getOperationRunner() - Task { - do { - let extractedProfiles = try await runner.scan(brokerProfileQueryData, stageCalculator: FakeStageDurationCalculator(), showWebView: true) { true } - - DispatchQueue.main.async { - if extractedProfiles.isEmpty { - self.showNoResultsAlert() - } else { - self.results = extractedProfiles + for query in brokerProfileQueryData { + Task { + do { + let extractedProfiles = try await runner.scan(query, stageCalculator: FakeStageDurationCalculator(), showWebView: true) { true } + + DispatchQueue.main.async { + for extractedProfile in extractedProfiles { + self.results.append(ScanResult(dataBroker: query.dataBroker, + profileQuery: query.profileQuery, + extractedProfile: extractedProfile)) + } } + } catch { + print("Error when scanning: \(error)") } - } catch { - showAlert(for: error) } } } catch { @@ -116,18 +368,16 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject { } } - func runOptOut(extractedProfile: ExtractedProfile) { + func runOptOut(scanResult: ScanResult) { let runner = runnerProvider.getOperationRunner() - guard let dataBroker = self.selectedDataBroker else { - print("No broker selected") - return - } - - let brokerProfileQueryData = createBrokerProfileQueryData(for: dataBroker) - + let brokerProfileQueryData = BrokerProfileQueryData( + dataBroker: scanResult.dataBroker, + profileQuery: scanResult.profileQuery, + scanOperationData: ScanOperationData(brokerId: 1, profileQueryId: 1, historyEvents: [HistoryEvent]()) + ) Task { do { - try await runner.optOut(profileQuery: brokerProfileQueryData, extractedProfile: extractedProfile, stageCalculator: FakeStageDurationCalculator(), showWebView: true) { + try await runner.optOut(profileQuery: brokerProfileQueryData, extractedProfile: scanResult.extractedProfile, stageCalculator: FakeStageDurationCalculator(), showWebView: true) { true } @@ -142,10 +392,54 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject { } } - private func createBrokerProfileQueryData(for dataBroker: DataBroker) -> BrokerProfileQueryData { - let profile = createProfile() - let fakeScanOperationData = ScanOperationData(brokerId: 0, profileQueryId: 0, historyEvents: [HistoryEvent]()) - return BrokerProfileQueryData(dataBroker: dataBroker, profileQuery: profile.profileQueries.first!, scanOperationData: fakeScanOperationData) + private func createBrokerProfileQueryData(for broker: DataBroker) -> [BrokerProfileQueryData] { + let profile: DataBrokerProtectionProfile = + .init( + names: names.map { $0.toModel() }, + addresses: addresses.map { $0.toModel() }, + phones: [String](), + birthYear: Int(birthYear) ?? 1990 + ) + let profileQueries = profile.profileQueries + var brokerProfileQueryData = [BrokerProfileQueryData]() + + var profileQueryIndex: Int64 = 1 + for profileQuery in profileQueries { + let fakeScanOperationData = ScanOperationData(brokerId: 0, profileQueryId: profileQueryIndex, historyEvents: [HistoryEvent]()) + brokerProfileQueryData.append( + .init(dataBroker: broker, profileQuery: profileQuery.with(id: profileQueryIndex), scanOperationData: fakeScanOperationData) + ) + + profileQueryIndex += 1 + } + + return brokerProfileQueryData + } + + private func createBrokerProfileQueryData() -> [BrokerProfileQueryData] { + let profile: DataBrokerProtectionProfile = + .init( + names: names.map { $0.toModel() }, + addresses: addresses.map { $0.toModel() }, + phones: [String](), + birthYear: Int(birthYear) ?? 1990 + ) + let profileQueries = profile.profileQueries + var brokerProfileQueryData = [BrokerProfileQueryData]() + + var profileQueryIndex: Int64 = 1 + for profileQuery in profileQueries { + let fakeScanOperationData = ScanOperationData(brokerId: 0, profileQueryId: profileQueryIndex, historyEvents: [HistoryEvent]()) + for broker in brokers { + brokerProfileQueryData.append( + .init(dataBroker: broker, profileQuery: profileQuery.with(id: profileQueryIndex), scanOperationData: fakeScanOperationData) + ) + } + + profileQueryIndex += 1 + } + + return brokerProfileQueryData } private func showNoResultsAlert() { @@ -166,21 +460,9 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject { } } - private func createProfile() -> DataBrokerProtectionProfile { - let names = DataBrokerProtectionProfile.Name(firstName: firstName, lastName: lastName, middleName: middle) - let addresses = DataBrokerProtectionProfile.Address(city: city, state: state) - - return DataBrokerProtectionProfile(names: [names], addresses: [addresses], phones: [String](), birthYear: Int(birthYear) ?? 1990) - } - func appVersion() -> String { AppVersion.shared.versionNumber } - - func contentScopeScriptsVersion() -> String { - // How can I return this? - return "4.59.2" - } } final class FakeStageDurationCalculator: StageDurationCalculator { @@ -367,4 +649,51 @@ extension DataBrokerProtectionError { default: return name } } + + var is404: Bool { + switch self { + case .httpError(let code): + return code == 404 + default: return false + } + } +} + +extension ScrapedData { + + var nameCSV: String { + if let name = self.name { + return name.replacingOccurrences(of: ",", with: "-") + } else if let alternativeNamesList = self.alternativeNamesList { + return alternativeNamesList.joined(separator: "/").replacingOccurrences(of: ",", with: "-") + } else { + return "" + } + } + + var ageCSV: String { + if let age = self.age { + return age + } else { + return "" + } + } + + var addressesCSV: String { + if let address = self.addressCityState { + return address + } else if let addressFull = self.addressCityStateList { + return addressFull.map { "\($0.city)-\($0.state)" }.joined(separator: "/") + } else { + return "" + } + } + + var relativesCSV: String { + if let relatives = self.relativesList { + return relatives.joined(separator: "-").replacingOccurrences(of: ",", with: "-") + } else { + return "" + } + } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanOperation.swift new file mode 100644 index 0000000000..94ee1b481c --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanOperation.swift @@ -0,0 +1,181 @@ +// +// DebugScanOperation.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import WebKit +import BrowserServicesKit +import UserScript +import Common + +struct DebugScanReturnValue { + let brokerURL: String + let extractedProfiles: [ExtractedProfile] + let error: Error? + let brokerProfileQueryData: BrokerProfileQueryData + let meta: [String: Any]? + + init(brokerURL: String, + extractedProfiles: [ExtractedProfile] = [ExtractedProfile](), + error: Error? = nil, + brokerProfileQueryData: BrokerProfileQueryData, + meta: [String: Any]? = nil) { + self.brokerURL = brokerURL + self.extractedProfiles = extractedProfiles + self.error = error + self.brokerProfileQueryData = brokerProfileQueryData + self.meta = meta + } +} + +final class DebugScanOperation: DataBrokerOperation { + typealias ReturnValue = DebugScanReturnValue + typealias InputValue = Void + + let privacyConfig: PrivacyConfigurationManaging + let prefs: ContentScopeProperties + let query: BrokerProfileQueryData + let emailService: EmailServiceProtocol + let captchaService: CaptchaServiceProtocol + var webViewHandler: WebViewHandler? + var actionsHandler: ActionsHandler? + var continuation: CheckedContinuation? + var extractedProfile: ExtractedProfile? + var stageCalculator: StageDurationCalculator? + private let operationAwaitTime: TimeInterval + let shouldRunNextStep: () -> Bool + var retriesCountOnError: Int = 0 + var scanURL: String? + + private let fileManager = FileManager.default + private let debugScanContentPath: String? + + init(privacyConfig: PrivacyConfigurationManaging, + prefs: ContentScopeProperties, + query: BrokerProfileQueryData, + emailService: EmailServiceProtocol = EmailService(), + captchaService: CaptchaServiceProtocol = CaptchaService(), + operationAwaitTime: TimeInterval = 3, + shouldRunNextStep: @escaping () -> Bool + ) { + self.privacyConfig = privacyConfig + self.prefs = prefs + self.query = query + self.emailService = emailService + self.captchaService = captchaService + self.operationAwaitTime = operationAwaitTime + self.shouldRunNextStep = shouldRunNextStep + if let desktopPath = fileManager.urls(for: .desktopDirectory, in: .userDomainMask).first?.relativePath { + self.debugScanContentPath = desktopPath + "/PIR-Debug" + } else { + self.debugScanContentPath = nil + } + } + + func run(inputValue: Void, + webViewHandler: WebViewHandler? = nil, + actionsHandler: ActionsHandler? = nil, + stageCalculator: StageDurationCalculator, // We do not need it for scans - for now. + showWebView: Bool) async throws -> DebugScanReturnValue { + try await withCheckedThrowingContinuation { continuation in + self.continuation = continuation + Task { + await initialize(handler: webViewHandler, isFakeBroker: query.dataBroker.isFakeBroker, showWebView: showWebView) + + do { + let scanStep = try query.dataBroker.scanStep() + if let actionsHandler = actionsHandler { + self.actionsHandler = actionsHandler + } else { + self.actionsHandler = ActionsHandler(step: scanStep) + } + if self.shouldRunNextStep() { + await executeNextStep() + } else { + failed(with: DataBrokerProtectionError.cancelled) + } + } catch { + failed(with: DataBrokerProtectionError.unknown(error.localizedDescription)) + } + } + } + } + + func runNextAction(_ action: Action) async { + if action as? ExtractAction != nil { + do { + if let path = self.debugScanContentPath { + let fileName = "\(query.profileQuery.id ?? 0)_\(query.dataBroker.name)" + try await webViewHandler?.takeSnaphost(path: path + "/screenshots/", fileName: "\(fileName).png") + try await webViewHandler?.saveHTML(path: path + "/html/", fileName: "\(fileName).html") + } + } catch { + print("Error: \(error)") + } + } + + await webViewHandler?.execute(action: action, data: .userData(query.profileQuery, self.extractedProfile)) + } + + func extractedProfiles(profiles: [ExtractedProfile], meta: [String: Any]?) async { + if let scanURL = self.scanURL { + let debugScanReturnValue = DebugScanReturnValue( + brokerURL: scanURL, + extractedProfiles: profiles, + brokerProfileQueryData: query, + meta: meta + ) + complete(debugScanReturnValue) + } + + await executeNextStep() + } + + func completeWith(error: Error) async { + if let scanURL = self.scanURL { + let debugScanReturnValue = DebugScanReturnValue(brokerURL: scanURL, error: error, brokerProfileQueryData: query) + complete(debugScanReturnValue) + } + + await executeNextStep() + } + + func executeNextStep() async { + retriesCountOnError = 0 // We reset the retries on error when it is successful + os_log("SCAN Waiting %{public}f seconds...", log: .action, operationAwaitTime) + + try? await Task.sleep(nanoseconds: UInt64(operationAwaitTime) * 1_000_000_000) + + if let action = actionsHandler?.nextAction() { + os_log("Next action: %{public}@", log: .action, String(describing: action.actionType.rawValue)) + await runNextAction(action) + } else { + os_log("Releasing the web view", log: .action) + await webViewHandler?.finish() // If we executed all steps we release the web view + } + } + + func loadURL(url: URL) async { + do { + self.scanURL = url.absoluteString + try await webViewHandler?.load(url: url) + await executeNextStep() + } catch { + await completeWith(error: error) + } + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DataBrokerProtectionProfile.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DataBrokerProtectionProfile.swift index a77770d5f9..c76645a804 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DataBrokerProtectionProfile.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DataBrokerProtectionProfile.swift @@ -69,7 +69,7 @@ public struct DataBrokerProtectionProfile: Codable { } } -internal extension DataBrokerProtectionProfile { +extension DataBrokerProtectionProfile { var profileQueries: [ProfileQuery] { return addresses.flatMap { address in names.map { name in diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/ProfileQuery.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/ProfileQuery.swift index 0850e3027c..c2cbde5fa5 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/ProfileQuery.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/ProfileQuery.swift @@ -117,6 +117,21 @@ extension ProfileQuery { birthYear: birthYear, deprecated: deprecated) } + + func with(id: Int64) -> ProfileQuery { + return ProfileQuery(id: id, + firstName: firstName, + lastName: lastName, + middleName: middleName, + suffix: suffix, + city: city, + state: state, + street: street, + zipCode: zip, + phone: phone, + birthYear: birthYear, + deprecated: deprecated) + } } extension ProfileQuery: Hashable { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutOperation.swift index ba59229aeb..7f12d1112f 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutOperation.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutOperation.swift @@ -99,7 +99,7 @@ final class OptOutOperation: DataBrokerOperation { } } - func extractedProfiles(profiles: [ExtractedProfile]) async { + func extractedProfiles(profiles: [ExtractedProfile], meta: [String: Any]?) async { // No - op } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanOperation.swift index ac26510a64..06dfddd03d 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanOperation.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanOperation.swift @@ -87,7 +87,7 @@ final class ScanOperation: DataBrokerOperation { } } - func extractedProfiles(profiles: [ExtractedProfile]) async { + func extractedProfiles(profiles: [ExtractedProfile], meta: [String: Any]?) async { complete(profiles) await executeNextStep() } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionFeatureTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionFeatureTests.swift index 4fe2f44458..280b30050d 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionFeatureTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionFeatureTests.swift @@ -118,7 +118,7 @@ final class MockCSSCommunicationDelegate: CCFCommunicationDelegate { self.url = url } - func extractedProfiles(profiles: [ExtractedProfile]) { + func extractedProfiles(profiles: [ExtractedProfile], meta: [String: Any]?) async { self.profiles = profiles } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift index 24a55ee92d..cc36c1e9de 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift @@ -214,6 +214,14 @@ final class WebViewHandlerMock: NSObject, WebViewHandler { wasExecuteJavascriptCalled = true } + func takeSnaphost(path: String, fileName: String) async throws { + + } + + func saveHTML(path: String, fileName: String) async throws { + + } + func reset() { wasInitializeWebViewCalled = false wasLoadCalledWithURL = nil From 8f594baec13c6b48634fa0361bf7a7044a5d0ecd Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Fri, 15 Mar 2024 13:41:32 +0000 Subject: [PATCH 18/51] Update embedded files --- .../AppPrivacyConfigurationDataProvider.swift | 4 +- DuckDuckGo/ContentBlocker/macos-config.json | 68 ++++++++++++++----- 2 files changed, 52 insertions(+), 20 deletions(-) diff --git a/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift b/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift index 427b2983c2..1843f8fc9d 100644 --- a/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift +++ b/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift @@ -22,8 +22,8 @@ import BrowserServicesKit final class AppPrivacyConfigurationDataProvider: EmbeddedDataProvider { public struct Constants { - public static let embeddedDataETag = "\"b9487380b1490e67513c650e9497a28c\"" - public static let embeddedDataSHA = "79590d4f2a9713b20eb127a29f862130fdc964145917004031022beaecf80fd0" + public static let embeddedDataETag = "\"11616ebbea5b6d7731bf08d224c6b1a7\"" + public static let embeddedDataSHA = "980b19df068a45b754ed6865295960a914c3d2c3bcc2f20b9d1f8a5f2c1d68c3" } var embeddedDataEtag: String { diff --git a/DuckDuckGo/ContentBlocker/macos-config.json b/DuckDuckGo/ContentBlocker/macos-config.json index 57da7cd02e..bb74d388c6 100644 --- a/DuckDuckGo/ContentBlocker/macos-config.json +++ b/DuckDuckGo/ContentBlocker/macos-config.json @@ -1,6 +1,6 @@ { "readme": "https://github.com/duckduckgo/privacy-configuration", - "version": 1710170291349, + "version": 1710501855617, "features": { "adClickAttribution": { "readme": "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/#3rd-party-tracker-loading-protection", @@ -264,6 +264,18 @@ { "domain": "meneame.net" }, + { + "domain": "espn.com" + }, + { + "domain": "usaa.com" + }, + { + "domain": "publico.es" + }, + { + "domain": "cnbc.com" + }, { "domain": "earth.google.com" }, @@ -287,7 +299,7 @@ ] }, "state": "enabled", - "hash": "8458a9cc012a9613e1497204003be946" + "hash": "ec25d3a0b633fbc4f208f35999f4ab0e" }, "autofill": { "exceptions": [ @@ -968,8 +980,11 @@ }, "clientBrandHint": { "exceptions": [], + "settings": { + "domains": [] + }, "state": "disabled", - "hash": "728493ef7a1488e4781656d3f9db84aa" + "hash": "d35dd75140cdfe166762013e59eb076d" }, "contentBlocking": { "state": "enabled", @@ -3904,6 +3919,23 @@ } ] }, + { + "domain": "uzone.id", + "rules": [ + { + "selector": "[class^='box-ads']", + "type": "hide-empty" + }, + { + "selector": "[class^='section-ads']", + "type": "hide-empty" + }, + { + "selector": ".parallax-container", + "type": "hide-empty" + } + ] + }, { "domain": "washingtontimes.com", "rules": [ @@ -3984,7 +4016,7 @@ ] }, "state": "enabled", - "hash": "3dc2d5b9a38827f46503a3b5882c7e33" + "hash": "3098b766cd343378237605f207f4fd17" }, "exceptionHandler": { "exceptions": [ @@ -4878,6 +4910,16 @@ "state": "enabled", "settings": { "allowlistedTrackers": { + "2mdn.net": { + "rules": [ + { + "rule": "2mdn.net", + "domains": [ + "crunchyroll.com" + ] + } + ] + }, "3lift.com": { "rules": [ { @@ -6013,22 +6055,11 @@ "fwmrm.net": { "rules": [ { - "rule": "2a7e9.v.fwmrm.net/ad/g/1", + "rule": "v.fwmrm.net/ad", "domains": [ + "6play.fr", "channel4.com" ] - }, - { - "rule": "2a7e9.v.fwmrm.net/ad/l/1", - "domains": [ - "channel4.com" - ] - }, - { - "rule": "7cbf2.v.fwmrm.net/ad/g/1", - "domains": [ - "6play.fr" - ] } ] }, @@ -6197,6 +6228,7 @@ "domains": [ "arkadium.com", "bloomberg.com", + "crunchyroll.com", "gamak.tv", "games.washingtonpost.com", "metro.co.uk", @@ -7946,7 +7978,7 @@ "domain": "sundancecatalog.com" } ], - "hash": "4325ed151d126936fbeb9a608d77da86" + "hash": "1bbefc586e08d2c42b5db17ed95cc8e5" }, "trackingCookies1p": { "settings": { From ecdd9ac00618cdf9cb3314be0f86548dc9f37855 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Fri, 15 Mar 2024 13:41:32 +0000 Subject: [PATCH 19/51] Set marketing version to 1.80.0 --- Configuration/Version.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/Version.xcconfig b/Configuration/Version.xcconfig index da3c8e7045..7a0581611b 100644 --- a/Configuration/Version.xcconfig +++ b/Configuration/Version.xcconfig @@ -1 +1 @@ -MARKETING_VERSION = 1.79.0 +MARKETING_VERSION = 1.80.0 From 05d80b76af5b65443a8dcf923a25e659388221b5 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Fri, 15 Mar 2024 13:52:33 +0000 Subject: [PATCH 20/51] Bump version to 1.80.0 (144) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 9a14499d7a..db702d8aab 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 143 +CURRENT_PROJECT_VERSION = 144 From 8fdbc689ac386571c186ff2f69a67c5d1e051b83 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Fri, 15 Mar 2024 15:08:14 +0100 Subject: [PATCH 21/51] Fix syntax in Asana actions --- .github/actions/asana-create-action-item/action.yml | 2 +- .github/actions/asana-log-message/action.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/asana-create-action-item/action.yml b/.github/actions/asana-create-action-item/action.yml index f3c5455006..7f4ee3f3c2 100644 --- a/.github/actions/asana-create-action-item/action.yml +++ b/.github/actions/asana-create-action-item/action.yml @@ -44,7 +44,7 @@ runs: task-url: ${{ inputs.release-task-url }} - id: get-asana-user-id - if: github.event_name != "schedule" + if: github.event_name != 'schedule' uses: duckduckgo/apple-infra/actions/asana-get-user-id-for-github-handle@main with: access-token: ${{ inputs.access-token }} diff --git a/.github/actions/asana-log-message/action.yml b/.github/actions/asana-log-message/action.yml index 86940852bf..288fd832ba 100644 --- a/.github/actions/asana-log-message/action.yml +++ b/.github/actions/asana-log-message/action.yml @@ -30,7 +30,7 @@ runs: task-url: ${{ inputs.task-url }} - id: get-asana-user-id - if: github.event_name != "schedule" + if: github.event_name != 'schedule' uses: duckduckgo/apple-infra/actions/asana-get-user-id-for-github-handle@main with: access-token: ${{ inputs.access-token }} From 90cff6bc14ff3f80997ad1251d37464397b3bcf7 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Fri, 15 Mar 2024 14:28:41 +0000 Subject: [PATCH 22/51] Bump version to 1.80.0 (145) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index db702d8aab..b11c07fc54 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 144 +CURRENT_PROJECT_VERSION = 145 From 793d16e629d10198642340da0ad0530fd0448845 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Sat, 16 Mar 2024 00:35:41 +0100 Subject: [PATCH 23/51] Bump BSK (#2435) Task/Issue URL: https://app.asana.com/0/72649045549333/1206350282230222/f **Description**: Bump BSK **Steps to test this PR**: 1. iOS Changes Only --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 23d4cdecf1..c985be5176 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -13749,7 +13749,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 124.1.0; + version = 125.0.0; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5d9e06d99f..24e42fd976 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "bcafd206465427c560f9f581def57d8eef53748c", - "version" : "124.1.0" + "revision" : "ac2127a26f75b2aa293f6036bcdd2bc241d09819", + "version" : "125.0.0" } }, { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index e07c2c873c..d428da60b2 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -29,7 +29,7 @@ let package = Package( targets: ["DataBrokerProtection"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "124.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "125.0.0"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 5c07540155..39f13d9fe0 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "124.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "125.0.0"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index c29571c2ce..6ffb49191b 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "124.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "125.0.0"), .package(path: "../SwiftUIExtensions") ], targets: [ From 5d7b4bc272c7f06188e129f807d44dc99f5b6894 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Sun, 17 Mar 2024 13:50:36 -0300 Subject: [PATCH 24/51] DBP: Implement event pixels (#2408) --- DuckDuckGo/DBP/DBPHomeViewController.swift | 6 +- .../Model/HistoryEvent.swift | 10 + ...taBrokerProfileQueryOperationManager.swift | 7 +- .../OperationPreferredDateCalculator.swift | 5 +- .../DataBrokerProtectionEventPixels.swift | 142 ++++++++ .../Pixels/DataBrokerProtectionPixels.swift | 29 +- .../DataBrokerProtectionProcessor.swift | 4 + ...kerProfileQueryOperationManagerTests.swift | 17 + ...DataBrokerProtectionEventPixelsTests.swift | 318 ++++++++++++++++++ 9 files changed, 531 insertions(+), 7 deletions(-) create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionEventPixels.swift create mode 100644 LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionEventPixelsTests.swift diff --git a/DuckDuckGo/DBP/DBPHomeViewController.swift b/DuckDuckGo/DBP/DBPHomeViewController.swift index b64fc689e8..9313c0530a 100644 --- a/DuckDuckGo/DBP/DBPHomeViewController.swift +++ b/DuckDuckGo/DBP/DBPHomeViewController.swift @@ -191,7 +191,11 @@ public class DataBrokerProtectionPixelsHandler: EventMapping Int { + switch type { + case .matchesFound(let matchesFound): + return matchesFound + default: + return 0 + } + } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift index 58352c3fc3..f5c29ae8b7 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift @@ -117,7 +117,8 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { notificationCenter.post(name: DataBrokerProtectionNotifications.didFinishScan, object: brokerProfileQueryData.dataBroker.name) } - let stageCalculator = DataBrokerProtectionStageDurationCalculator(dataBroker: brokerProfileQueryData.dataBroker.url, handler: pixelHandler) + let eventPixels = DataBrokerProtectionEventPixels(database: database, handler: pixelHandler) + let stageCalculator = DataBrokerProtectionStageDurationCalculator(dataBroker: brokerProfileQueryData.dataBroker.name, handler: pixelHandler) do { let event = HistoryEvent(brokerId: brokerId, profileQueryId: profileQueryId, type: .scanStarted) @@ -141,6 +142,9 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { if doesProfileExistsInDatabase, let alreadyInDatabaseProfile = extractedProfilesForBroker.first(where: { $0.identifier == extractedProfile.identifier }), let id = alreadyInDatabaseProfile.id { // If it was removed in the past but was found again when scanning, it means it appearead again, so we reset the remove date. if alreadyInDatabaseProfile.removedDate != nil { + let reAppereanceEvent = HistoryEvent(extractedProfileId: extractedProfile.id, brokerId: brokerId, profileQueryId: profileQueryId, type: .reAppearence) + eventPixels.fireReAppereanceEventPixel() + database.add(reAppereanceEvent) database.updateRemovedDate(nil, on: id) } @@ -148,6 +152,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { } else { // If it's a new found profile, we'd like to opt-out ASAP // If this broker has a parent opt out, we set the preferred date to nil, as we will only perform the operation within the parent. + eventPixels.fireNewMatchEventPixel() let broker = brokerProfileQueryData.dataBroker let preferredRunOperation: Date? = broker.performsOptOutWithinParent() ? nil : Date() diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OperationPreferredDateCalculator.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OperationPreferredDateCalculator.swift index f5d2063be0..ca42eaedda 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OperationPreferredDateCalculator.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OperationPreferredDateCalculator.swift @@ -41,14 +41,13 @@ struct OperationPreferredDateCalculator { } switch lastEvent.type { - case .optOutConfirmed: if isDeprecated { return nil } else { return Date().addingTimeInterval(schedulingConfig.maintenanceScan.hoursToSeconds) } - case .noMatchFound, .matchesFound: + case .noMatchFound, .matchesFound, .reAppearence: return Date().addingTimeInterval(schedulingConfig.maintenanceScan.hoursToSeconds) case .error: return Date().addingTimeInterval(schedulingConfig.retryError.hoursToSeconds) @@ -70,7 +69,7 @@ struct OperationPreferredDateCalculator { } switch lastEvent.type { - case .matchesFound: + case .matchesFound, .reAppearence: if let extractedProfileID = extractedProfileID, shouldScheduleNewOptOut(events: historyEvents, extractedProfileId: extractedProfileID, schedulingConfig: schedulingConfig) { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionEventPixels.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionEventPixels.swift new file mode 100644 index 0000000000..fd0a382703 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionEventPixels.swift @@ -0,0 +1,142 @@ +// +// DataBrokerProtectionEventPixels.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Common +import BrowserServicesKit +import PixelKit + +protocol DataBrokerProtectionEventPixelsRepository { + func markWeeklyPixelSent() + + func getLatestWeeklyPixel() -> Date? +} + +final class DataBrokerProtectionEventPixelsUserDefaults: DataBrokerProtectionEventPixelsRepository { + + enum Consts { + static let weeklyPixelKey = "macos.browser.data-broker-protection.eventsWeeklyPixelKey" + } + + private let userDefaults: UserDefaults + + init(userDefaults: UserDefaults = .standard) { + self.userDefaults = userDefaults + } + + func markWeeklyPixelSent() { + userDefaults.set(Date(), forKey: Consts.weeklyPixelKey) + } + + func getLatestWeeklyPixel() -> Date? { + userDefaults.object(forKey: Consts.weeklyPixelKey) as? Date + } +} + +final class DataBrokerProtectionEventPixels { + + private let database: DataBrokerProtectionRepository + private let repository: DataBrokerProtectionEventPixelsRepository + private let handler: EventMapping + private let calendar = Calendar.current + + init(database: DataBrokerProtectionRepository, + repository: DataBrokerProtectionEventPixelsRepository = DataBrokerProtectionEventPixelsUserDefaults(), + handler: EventMapping) { + self.database = database + self.repository = repository + self.handler = handler + } + + func tryToFireWeeklyPixels() { + if shouldWeFireWeeklyPixel() { + fireWeeklyReportPixels() + repository.markWeeklyPixelSent() + } + } + + func fireNewMatchEventPixel() { + handler.fire(.scanningEventNewMatch) + } + + func fireReAppereanceEventPixel() { + handler.fire(.scanningEventReAppearance) + } + + private func shouldWeFireWeeklyPixel() -> Bool { + guard let lastPixelFiredDate = repository.getLatestWeeklyPixel() else { + return true // Last pixel fired date is not present. We should fire it + } + + return didWeekPassedBetweenDates(start: lastPixelFiredDate, end: Date()) + } + + private func fireWeeklyReportPixels() { + let data = database.fetchAllBrokerProfileQueryData() + let dataInThePastWeek = data.filter(hadScanThisWeek(_:)) + + var newMatchesFoundInTheLastWeek = 0 + var reAppereancesInTheLastWeek = 0 + var removalsInTheLastWeek = 0 + + for query in data { + let allHistoryEventsForQuery = query.scanOperationData.historyEvents + query.optOutOperationsData.flatMap { $0.historyEvents } + let historyEventsInThePastWeek = allHistoryEventsForQuery.filter { + !didWeekPassedBetweenDates(start: $0.date, end: Date()) + } + let newMatches = historyEventsInThePastWeek.reduce(0, { result, next in + return result + next.matchesFound() + }) + let reAppereances = historyEventsInThePastWeek.filter { $0.type == .reAppearence }.count + let removals = historyEventsInThePastWeek.filter { $0.type == .optOutConfirmed }.count + + newMatchesFoundInTheLastWeek += newMatches + reAppereancesInTheLastWeek += reAppereances + removalsInTheLastWeek += removals + } + + let totalBrokers = Dictionary(grouping: data, by: { $0.dataBroker.url }).count + let totalBrokersInTheLastWeek = Dictionary(grouping: dataInThePastWeek, by: { $0.dataBroker.url }).count + var percentageOfBrokersScanned: Int + + if totalBrokers == 0 { + percentageOfBrokersScanned = 0 + } else { + percentageOfBrokersScanned = (totalBrokersInTheLastWeek * 100) / totalBrokers + } + + handler.fire(.weeklyReportScanning(hadNewMatch: newMatchesFoundInTheLastWeek > 0, hadReAppereance: reAppereancesInTheLastWeek > 0, scanCoverage: percentageOfBrokersScanned)) + handler.fire(.weeklyReportRemovals(removals: removalsInTheLastWeek)) + } + + private func hadScanThisWeek(_ brokerProfileQuery: BrokerProfileQueryData) -> Bool { + return brokerProfileQuery.scanOperationData.historyEvents.contains { historyEvent in + !didWeekPassedBetweenDates(start: historyEvent.date, end: Date()) + } + } + + private func didWeekPassedBetweenDates(start: Date, end: Date) -> Bool { + let components = calendar.dateComponents([.day], from: start, to: end) + + if let differenceInDays = components.day { + return differenceInDays >= 7 + } else { + return false + } + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift index e7198fe713..9663bbae4d 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift @@ -54,6 +54,10 @@ public enum DataBrokerProtectionPixels { static let pattern = "pattern" static let isParent = "is_parent" static let actionIDKey = "action_id" + static let hadNewMatch = "had_new_match" + static let hadReAppereance = "had_re-appearance" + static let scanCoverage = "scan_coverage" + static let removals = "removals" } case error(error: DataBrokerProtectionError, dataBroker: String) @@ -117,6 +121,12 @@ public enum DataBrokerProtectionPixels { case dailyActiveUser case weeklyActiveUser case monthlyActiveUser + + // KPIs - events + case weeklyReportScanning(hadNewMatch: Bool, hadReAppereance: Bool, scanCoverage: Int) + case weeklyReportRemovals(removals: Int) + case scanningEventNewMatch + case scanningEventReAppearance } extension DataBrokerProtectionPixels: PixelKitEvent { @@ -190,6 +200,11 @@ extension DataBrokerProtectionPixels: PixelKitEvent { case .dailyActiveUser: return "m_mac_dbp_engagement_dau" case .weeklyActiveUser: return "m_mac_dbp_engagement_wau" case .monthlyActiveUser: return "m_mac_dbp_engagement_mau" + + case .weeklyReportScanning: return "m_mac_dbp_event_weekly-report_scanning" + case .weeklyReportRemovals: return "m_mac_dbp_event_weekly-report_removals" + case .scanningEventNewMatch: return "m_mac_dbp_event_scanning-events_new-match" + case .scanningEventReAppearance: return "m_mac_dbp_event_scanning-events_re-appearance" } } @@ -251,6 +266,10 @@ extension DataBrokerProtectionPixels: PixelKitEvent { } return params + case .weeklyReportScanning(let hadNewMatch, let hadReAppereance, let scanCoverage): + return [Consts.hadNewMatch: hadNewMatch.description, Consts.hadReAppereance: hadReAppereance.description, Consts.scanCoverage: scanCoverage.description] + case .weeklyReportRemovals(let removals): + return [Consts.removals: String(removals)] case .backgroundAgentStarted, .backgroundAgentRunOperationsAndStartSchedulerIfPossible, .backgroundAgentRunOperationsAndStartSchedulerIfPossibleNoSavedProfile, @@ -266,7 +285,9 @@ extension DataBrokerProtectionPixels: PixelKitEvent { .dataBrokerProtectionNotificationOpenedAllRecordsRemoved, .dailyActiveUser, .weeklyActiveUser, - .monthlyActiveUser: + .monthlyActiveUser, + .scanningEventNewMatch, + .scanningEventReAppearance: return [:] case .ipcServerRegister, .ipcServerStartScheduler, @@ -341,7 +362,11 @@ public class DataBrokerProtectionPixelsHandler: EventMapping private let userNotificationService: DataBrokerProtectionUserNotificationService private let engagementPixels: DataBrokerProtectionEngagementPixels + private let eventPixels: DataBrokerProtectionEventPixels init(database: DataBrokerProtectionRepository, config: SchedulerConfig, @@ -50,6 +51,7 @@ final class DataBrokerProtectionProcessor { self.operationQueue.maxConcurrentOperationCount = config.concurrentOperationsDifferentBrokers self.userNotificationService = userNotificationService self.engagementPixels = DataBrokerProtectionEngagementPixels(database: database, handler: pixelHandler) + self.eventPixels = DataBrokerProtectionEventPixels(database: database, handler: pixelHandler) } // MARK: - Public functions @@ -115,6 +117,8 @@ final class DataBrokerProtectionProcessor { // This will fire the DAU/WAU/MAU pixels, engagementPixels.fireEngagementPixel() + // This will try to fire the event weekly report pixels + eventPixels.tryToFireWeeklyPixels() let brokersProfileData = database.fetchAllBrokerProfileQueryData() let dataBrokerOperationCollections = createDataBrokerOperationCollections(from: brokersProfileData, diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift index 08796454c7..70619e8736 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift @@ -974,6 +974,10 @@ extension ScanOperationData { historyEvents: [HistoryEvent]() ) } + + static func mockWith(historyEvents: [HistoryEvent]) -> ScanOperationData { + ScanOperationData(brokerId: 1, profileQueryId: 1, historyEvents: historyEvents) + } } extension OptOutOperationData { @@ -1035,6 +1039,19 @@ extension DataBroker { ) ) } + + static func mockWithURL(_ url: String) -> DataBroker { + .init(name: "Test", + url: url, + steps: [Step](), + version: "1.0", + schedulingConfig: DataBrokerScheduleConfig( + retryError: 0, + confirmOptOutScan: 0, + maintenanceScan: 0 + ) + ) + } } extension ProfileQuery { diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionEventPixelsTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionEventPixelsTests.swift new file mode 100644 index 0000000000..4c5a93563b --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionEventPixelsTests.swift @@ -0,0 +1,318 @@ +// +// DataBrokerProtectionEventPixelsTests.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +import Foundation +@testable import DataBrokerProtection + +final class DataBrokerProtectionEventPixelsTests: XCTestCase { + + let database = MockDatabase() + let repository = MockDataBrokerProtectionEventPixelsRepository() + let handler = MockDataBrokerProtectionPixelsHandler() + let calendar = Calendar.current + + override func tearDown() { + handler.clear() + repository.clear() + } + + func testWhenFireNewMatchEventPixelIsCalled_thenCorrectPixelIsFired() { + let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler) + + sut.fireNewMatchEventPixel() + + XCTAssertEqual( + MockDataBrokerProtectionPixelsHandler.lastPixelsFired.last!.name, + DataBrokerProtectionPixels.scanningEventNewMatch.name + ) + } + + func testWhenFireReAppereanceEventPixelIsCalled_thenCorrectPixelIsFired() { + let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler) + + sut.fireReAppereanceEventPixel() + + XCTAssertEqual( + MockDataBrokerProtectionPixelsHandler.lastPixelsFired.last!.name, + DataBrokerProtectionPixels.scanningEventReAppearance.name + ) + } + + func testWhenReportWasFiredInTheLastWeek_thenWeDoNotFireWeeklyPixels() { + repository.customGetLatestWeeklyPixel = Date().yesterday + let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler) + + sut.tryToFireWeeklyPixels() + + XCTAssertFalse(repository.wasMarkWeeklyPixelSentCalled) + } + + func testWhenReportWasNotFiredInTheLastWeek_thenWeFireWeeklyPixels() { + guard let eightDaysSinceToday = Calendar.current.date(byAdding: .day, value: -8, to: Date()) else { + XCTFail("This should no throw") + return + } + + repository.customGetLatestWeeklyPixel = eightDaysSinceToday + let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler) + + sut.tryToFireWeeklyPixels() + + XCTAssertTrue(repository.wasMarkWeeklyPixelSentCalled) + } + + func testWhenLastWeeklyPixelIsNil_thenWeFireWeeklyPixels() { + repository.customGetLatestWeeklyPixel = nil + let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler) + + sut.tryToFireWeeklyPixels() + + XCTAssertTrue(repository.wasMarkWeeklyPixelSentCalled) + } + + func testWhenReAppereanceOcurredInTheLastWeek_thenReAppereanceFlagIsTrue() { + guard let sixDaysSinceToday = Calendar.current.date(byAdding: .day, value: -6, to: Date()) else { + XCTFail("This should no throw") + return + } + + let reAppereanceThisWeekEvent = HistoryEvent(brokerId: 1, profileQueryId: 1, type: .reAppearence, date: sixDaysSinceToday) + let dataBrokerProfileQueryWithReAppereance: [BrokerProfileQueryData] = [ + .init(dataBroker: .mock, + profileQuery: .mock, + scanOperationData: .mockWith(historyEvents: [reAppereanceThisWeekEvent])) + ] + let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler) + database.brokerProfileQueryDataToReturn = dataBrokerProfileQueryWithReAppereance + repository.customGetLatestWeeklyPixel = nil + + sut.tryToFireWeeklyPixels() + + let weeklyReportScanningPixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.first! + let hadReAppereance = weeklyReportScanningPixel.params!["had_re-appearance"]! + + XCTAssertTrue(Bool(hadReAppereance)!) + } + + func testWhenReAppereanceDidNotOcurrInTheLastWeek_thenReAppereanceFlagIsFalse() { + guard let eighDaysSinceToday = Calendar.current.date(byAdding: .day, value: -8, to: Date()) else { + XCTFail("This should no throw") + return + } + + let reAppereanceThisWeekEvent = HistoryEvent(brokerId: 1, profileQueryId: 1, type: .reAppearence, date: eighDaysSinceToday) + let dataBrokerProfileQueryWithReAppereance: [BrokerProfileQueryData] = [ + .init(dataBroker: .mock, + profileQuery: .mock, + scanOperationData: .mockWith(historyEvents: [reAppereanceThisWeekEvent])) + ] + let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler) + database.brokerProfileQueryDataToReturn = dataBrokerProfileQueryWithReAppereance + repository.customGetLatestWeeklyPixel = nil + + sut.tryToFireWeeklyPixels() + + let weeklyReportScanningPixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.first! + let hadReAppereance = weeklyReportScanningPixel.params!["had_re-appearance"]! + + XCTAssertFalse(Bool(hadReAppereance)!) + } + + func testWhenNoMatchesHappendInTheLastWeek_thenHadNewMatchFlagIsFalse() { + guard let eighDaysSinceToday = Calendar.current.date(byAdding: .day, value: -8, to: Date()) else { + XCTFail("This should no throw") + return + } + + let newMatchesPriorToThisWeekEvent = HistoryEvent(brokerId: 1, profileQueryId: 1, type: .matchesFound(count: 2), date: eighDaysSinceToday) + let dataBrokerProfileQueryWithMatches: [BrokerProfileQueryData] = [ + .init(dataBroker: .mock, + profileQuery: .mock, + scanOperationData: .mockWith(historyEvents: [newMatchesPriorToThisWeekEvent])) + ] + let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler) + database.brokerProfileQueryDataToReturn = dataBrokerProfileQueryWithMatches + repository.customGetLatestWeeklyPixel = nil + + sut.tryToFireWeeklyPixels() + + let weeklyReportScanningPixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.first! + let hadNewMatch = weeklyReportScanningPixel.params!["had_new_match"]! + + XCTAssertFalse(Bool(hadNewMatch)!) + } + + func testWhenMatchesHappendInTheLastWeek_thenHadNewMatchFlagIsTrue() { + guard let sixDaysSinceToday = Calendar.current.date(byAdding: .day, value: -6, to: Date()) else { + XCTFail("This should no throw") + return + } + + let newMatchesThisWeekEvent = HistoryEvent(brokerId: 1, profileQueryId: 1, type: .matchesFound(count: 2), date: sixDaysSinceToday) + let dataBrokerProfileQueryWithMatches: [BrokerProfileQueryData] = [ + .init(dataBroker: .mock, + profileQuery: .mock, + scanOperationData: .mockWith(historyEvents: [newMatchesThisWeekEvent])) + ] + let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler) + database.brokerProfileQueryDataToReturn = dataBrokerProfileQueryWithMatches + repository.customGetLatestWeeklyPixel = nil + + sut.tryToFireWeeklyPixels() + + let weeklyReportScanningPixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.first! + let hadNewMatch = weeklyReportScanningPixel.params!["had_new_match"]! + + XCTAssertTrue(Bool(hadNewMatch)!) + } + + func testWhenNoRemovalsHappendInTheLastWeek_thenRemovalsCountIsZero() { + guard let eighDaysSinceToday = Calendar.current.date(byAdding: .day, value: -8, to: Date()) else { + XCTFail("This should no throw") + return + } + + let removalsPriorToThisWeekEvent = HistoryEvent(brokerId: 1, profileQueryId: 1, type: .optOutConfirmed, date: eighDaysSinceToday) + let dataBrokerProfileQueryWithRemovals: [BrokerProfileQueryData] = [ + .init(dataBroker: .mock, + profileQuery: .mock, + scanOperationData: .mockWith(historyEvents: [removalsPriorToThisWeekEvent])) + ] + let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler) + database.brokerProfileQueryDataToReturn = dataBrokerProfileQueryWithRemovals + repository.customGetLatestWeeklyPixel = nil + + sut.tryToFireWeeklyPixels() + + let weeklyReportScanningPixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.last! + let removals = weeklyReportScanningPixel.params!["removals"]! + + XCTAssertEqual("0", removals) + } + + func testWhenRemovalsHappendInTheLastWeek_thenRemovalsCountIsCorrect() { + guard let sixDaysSinceToday = Calendar.current.date(byAdding: .day, value: -6, to: Date()) else { + XCTFail("This should no throw") + return + } + + let removalThisWeekEventOne = HistoryEvent(brokerId: 1, profileQueryId: 1, type: .optOutConfirmed, date: sixDaysSinceToday) + let removalThisWeekEventTwo = HistoryEvent(brokerId: 1, profileQueryId: 1, type: .optOutConfirmed, date: sixDaysSinceToday) + let dataBrokerProfileQueryWithRemovals: [BrokerProfileQueryData] = [ + .init(dataBroker: .mock, + profileQuery: .mock, + scanOperationData: .mockWith(historyEvents: [removalThisWeekEventOne, removalThisWeekEventTwo])) + ] + let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler) + database.brokerProfileQueryDataToReturn = dataBrokerProfileQueryWithRemovals + repository.customGetLatestWeeklyPixel = nil + + sut.tryToFireWeeklyPixels() + + let weeklyReportScanningPixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.last! + let removals = weeklyReportScanningPixel.params!["removals"]! + + XCTAssertEqual("2", removals) + } + + func testWhenNoHistoryEventsHappenedInTheLastWeek_thenBrokersScannedIsZero() { + guard let eighDaysSinceToday = Calendar.current.date(byAdding: .day, value: -8, to: Date()) else { + XCTFail("This should no throw") + return + } + + let eventOne = HistoryEvent(brokerId: 1, profileQueryId: 1, type: .optOutStarted, date: eighDaysSinceToday) + let eventTwo = HistoryEvent(brokerId: 1, profileQueryId: 1, type: .error(error: .cancelled), date: eighDaysSinceToday) + let eventThree = HistoryEvent(brokerId: 1, profileQueryId: 1, type: .scanStarted, date: eighDaysSinceToday) + let eventFour = HistoryEvent(brokerId: 1, profileQueryId: 1, type: .noMatchFound, date: eighDaysSinceToday) + let dataBrokerProfileQueries: [BrokerProfileQueryData] = [ + .init(dataBroker: .mock, + profileQuery: .mock, + scanOperationData: .mockWith(historyEvents: [eventOne, eventTwo, eventThree, eventFour])) + ] + let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler) + database.brokerProfileQueryDataToReturn = dataBrokerProfileQueries + repository.customGetLatestWeeklyPixel = nil + + sut.tryToFireWeeklyPixels() + + let weeklyReportScanningPixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.first! + let scanCoverage = weeklyReportScanningPixel.params!["scan_coverage"]! + + XCTAssertEqual("0", scanCoverage) + } + + func testWhenHistoryEventsHappenedInTheLastWeek_thenBrokersScannedIsMoreThanZero() { + guard let eighDaysSinceToday = Calendar.current.date(byAdding: .day, value: -8, to: Date()) else { + XCTFail("This should no throw") + return + } + + guard let sixDaysSinceToday = Calendar.current.date(byAdding: .day, value: -6, to: Date()) else { + XCTFail("This should no throw") + return + } + + let eventOne = HistoryEvent(brokerId: 1, profileQueryId: 1, type: .scanStarted, date: eighDaysSinceToday) + let eventTwo = HistoryEvent(brokerId: 1, profileQueryId: 1, type: .scanStarted, date: sixDaysSinceToday) + let dataBrokerProfileQueries: [BrokerProfileQueryData] = [ + .init(dataBroker: .mockWithURL("www.brokerone.com"), + profileQuery: .mock, + scanOperationData: .mockWith(historyEvents: [eventOne])), + .init(dataBroker: .mockWithURL("www.brokertwo.com"), + profileQuery: .mock, + scanOperationData: .mockWith(historyEvents: [eventOne])), + .init(dataBroker: .mockWithURL("www.brokerthree.com"), + profileQuery: .mock, + scanOperationData: .mockWith(historyEvents: [eventTwo])), + .init(dataBroker: .mockWithURL("www.brokerfour.com"), + profileQuery: .mock, + scanOperationData: .mockWith(historyEvents: [eventTwo])) + ] + let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler) + database.brokerProfileQueryDataToReturn = dataBrokerProfileQueries + repository.customGetLatestWeeklyPixel = nil + + sut.tryToFireWeeklyPixels() + + let weeklyReportScanningPixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.first! + let scanCoverage = weeklyReportScanningPixel.params!["scan_coverage"]! + + XCTAssertEqual("50", scanCoverage) + } +} + +final class MockDataBrokerProtectionEventPixelsRepository: DataBrokerProtectionEventPixelsRepository { + + var wasMarkWeeklyPixelSentCalled = false + var customGetLatestWeeklyPixel: Date? + + func markWeeklyPixelSent() { + wasMarkWeeklyPixelSentCalled = true + } + + func getLatestWeeklyPixel() -> Date? { + return customGetLatestWeeklyPixel + } + + func clear() { + wasMarkWeeklyPixelSentCalled = false + customGetLatestWeeklyPixel = nil + } +} From 806f6bed8103a2016f7a27495c89d42c27c7cc64 Mon Sep 17 00:00:00 2001 From: Dax Mobile <44842493+daxmobile@users.noreply.github.com> Date: Mon, 18 Mar 2024 00:19:13 +0100 Subject: [PATCH 25/51] Update autoconsent to v10.3.0 (#2433) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1206853951101015/1206853951101015 Autoconsent Release: https://github.com/duckduckgo/autoconsent/releases/tag/v10.3.0 ## Description Updates Autoconsent to version [v10.3.0](https://github.com/duckduckgo/autoconsent/releases/tag/v10.3.0). ### Autoconsent v10.3.0 release notes #### 🚀 Enhancement - DDG release automation [#389](https://github.com/duckduckgo/autoconsent/pull/389) ([@muodov](https://github.com/muodov)) - Bump the dev-dependencies group with 4 updates [#390](https://github.com/duckduckgo/autoconsent/pull/390) ([@dependabot[bot]](https://github.com/dependabot[bot])) #### 🐛 Bug Fix - Fix infinite reload for OneTrust sites [#393](https://github.com/duckduckgo/autoconsent/pull/393) ([@muodov](https://github.com/muodov)) - Script to crawl page text content in multiple languages. [#386](https://github.com/duckduckgo/autoconsent/pull/386) ([@sammacbeth](https://github.com/sammacbeth)) - Update Asana sync action [#388](https://github.com/duckduckgo/autoconsent/pull/388) ([@sammacbeth](https://github.com/sammacbeth)) #### Authors: 3 - [@dependabot[bot]](https://github.com/dependabot[bot]) - Maxim Tsoy ([@muodov](https://github.com/muodov)) - Sam Macbeth ([@sammacbeth](https://github.com/sammacbeth)) ## Steps to test This release has been tested during Autoconsent development. You can check the release notes for more information. Co-authored-by: muodov --- DuckDuckGo/Autoconsent/autoconsent-bundle.js | 2 +- package-lock.json | 8 ++++---- package.json | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo/Autoconsent/autoconsent-bundle.js b/DuckDuckGo/Autoconsent/autoconsent-bundle.js index 6994afdcd0..db02dcc60b 100644 --- a/DuckDuckGo/Autoconsent/autoconsent-bundle.js +++ b/DuckDuckGo/Autoconsent/autoconsent-bundle.js @@ -1 +1 @@ -!function(){"use strict";var e=class e{static setBase(t){e.base=t}static findElement(t,o=null,c=!1){let i=null;return i=null!=o?Array.from(o.querySelectorAll(t.selector)):null!=e.base?Array.from(e.base.querySelectorAll(t.selector)):Array.from(document.querySelectorAll(t.selector)),null!=t.textFilter&&(i=i.filter((e=>{const o=e.textContent.toLowerCase();if(Array.isArray(t.textFilter)){let e=!1;for(const c of t.textFilter)if(-1!==o.indexOf(c.toLowerCase())){e=!0;break}return e}if(null!=t.textFilter)return-1!==o.indexOf(t.textFilter.toLowerCase())}))),null!=t.styleFilters&&(i=i.filter((e=>{const o=window.getComputedStyle(e);let c=!0;for(const e of t.styleFilters){const t=o[e.option];c=e.negated?c&&t!==e.value:c&&t===e.value}return c}))),null!=t.displayFilter&&(i=i.filter((e=>t.displayFilter?0!==e.offsetHeight:0===e.offsetHeight))),null!=t.iframeFilter&&(i=i.filter((()=>t.iframeFilter?window.location!==window.parent.location:window.location===window.parent.location))),null!=t.childFilter&&(i=i.filter((o=>{const c=e.base;e.setBase(o);const i=e.find(t.childFilter);return e.setBase(c),null!=i.target}))),c?i:(i.length>1&&console.warn("Multiple possible targets: ",i,t,o),i[0])}static find(t,o=!1){const c=[];if(null!=t.parent){const i=e.findElement(t.parent,null,o);if(null!=i){if(i instanceof Array)return i.forEach((i=>{const n=e.findElement(t.target,i,o);n instanceof Array?n.forEach((e=>{c.push({parent:i,target:e})})):c.push({parent:i,target:n})})),c;{const n=e.findElement(t.target,i,o);n instanceof Array?n.forEach((e=>{c.push({parent:i,target:e})})):c.push({parent:i,target:n})}}}else{const i=e.findElement(t.target,null,o);i instanceof Array?i.forEach((e=>{c.push({parent:null,target:e})})):c.push({parent:null,target:i})}return 0===c.length&&c.push({parent:null,target:null}),o?c:(1!==c.length&&console.warn("Multiple results found, even though multiple false",c),c[0])}};e.base=null;var t=e;function o(e){const o=t.find(e);return"css"===e.type?!!o.target:"checkbox"===e.type?!!o.target&&o.target.checked:void 0}async function c(e,n){switch(e.type){case"click":return async function(e){const o=t.find(e);null!=o.target&&o.target.click();return i(0)}(e);case"list":return async function(e,t){for(const o of e.actions)await c(o,t)}(e,n);case"consent":return async function(e,t){for(const i of e.consents){const e=-1!==t.indexOf(i.type);if(i.matcher&&i.toggleAction){o(i.matcher)!==e&&await c(i.toggleAction)}else e?await c(i.trueAction):await c(i.falseAction)}}(e,n);case"ifcss":return async function(e,o){const i=t.find(e);i.target?e.falseAction&&await c(e.falseAction,o):e.trueAction&&await c(e.trueAction,o)}(e,n);case"waitcss":return async function(e){await new Promise((o=>{let c=e.retries||10;const i=e.waitTime||250,n=()=>{const a=t.find(e);(e.negated&&a.target||!e.negated&&!a.target)&&c>0?(c-=1,setTimeout(n,i)):o()};n()}))}(e);case"foreach":return async function(e,o){const i=t.find(e,!0),n=t.base;for(const n of i)n.target&&(t.setBase(n.target),await c(e.action,o));t.setBase(n)}(e,n);case"hide":return async function(e){const o=t.find(e);o.target&&o.target.classList.add("Autoconsent-Hidden")}(e);case"slide":return async function(e){const o=t.find(e),c=t.find(e.dragTarget);if(o.target){const e=o.target.getBoundingClientRect(),t=c.target.getBoundingClientRect();let i=t.top-e.top,n=t.left-e.left;"y"===this.config.axis.toLowerCase()&&(n=0),"x"===this.config.axis.toLowerCase()&&(i=0);const a=window.screenX+e.left+e.width/2,s=window.screenY+e.top+e.height/2,r=e.left+e.width/2,l=e.top+e.height/2,p=document.createEvent("MouseEvents");p.initMouseEvent("mousedown",!0,!0,window,0,a,s,r,l,!1,!1,!1,!1,0,o.target);const d=document.createEvent("MouseEvents");d.initMouseEvent("mousemove",!0,!0,window,0,a+n,s+i,r+n,l+i,!1,!1,!1,!1,0,o.target);const u=document.createEvent("MouseEvents");u.initMouseEvent("mouseup",!0,!0,window,0,a+n,s+i,r+n,l+i,!1,!1,!1,!1,0,o.target),o.target.dispatchEvent(p),await this.waitTimeout(10),o.target.dispatchEvent(d),await this.waitTimeout(10),o.target.dispatchEvent(u)}}(e);case"close":return async function(){window.close()}();case"wait":return async function(e){await i(e.waitTime)}(e);case"eval":return async function(e){return console.log("eval!",e.code),new Promise((t=>{try{e.async?(window.eval(e.code),setTimeout((()=>{t(window.eval("window.__consentCheckResult"))}),e.timeout||250)):t(window.eval(e.code))}catch(o){console.warn("eval error",o,e.code),t(!1)}}))}(e);default:throw"Unknown action type: "+e.type}}function i(e){return new Promise((t=>{setTimeout((()=>{t()}),e)}))}function n(){return crypto&&void 0!==crypto.randomUUID?crypto.randomUUID():Math.random().toString()}var a={pending:new Map,sendContentMessage:null};function s(e,t){const o=n();a.sendContentMessage({type:"eval",id:o,code:e,snippetId:t});const c=new class{constructor(e,t=1e3){this.id=e,this.promise=new Promise(((e,t)=>{this.resolve=e,this.reject=t})),this.timer=window.setTimeout((()=>{this.reject(new Error("timeout"))}),t)}}(o);return a.pending.set(c.id,c),c.promise}var r={EVAL_0:()=>console.log(1),EVAL_CONSENTMANAGER_1:()=>window.__cmp&&"object"==typeof __cmp("getCMPData"),EVAL_CONSENTMANAGER_2:()=>!__cmp("consentStatus").userChoiceExists,EVAL_CONSENTMANAGER_3:()=>__cmp("setConsent",0),EVAL_CONSENTMANAGER_4:()=>__cmp("setConsent",1),EVAL_CONSENTMANAGER_5:()=>__cmp("consentStatus").userChoiceExists,EVAL_COOKIEBOT_1:()=>!!window.Cookiebot,EVAL_COOKIEBOT_2:()=>!window.Cookiebot.hasResponse&&!0===window.Cookiebot.dialog?.visible,EVAL_COOKIEBOT_3:()=>window.Cookiebot.withdraw()||!0,EVAL_COOKIEBOT_4:()=>window.Cookiebot.hide()||!0,EVAL_COOKIEBOT_5:()=>!0===window.Cookiebot.declined,EVAL_KLARO_1:()=>{const e=globalThis.klaroConfig||globalThis.klaro?.getManager&&globalThis.klaro.getManager().config;if(!e)return!0;const t=(e.services||e.apps).filter((e=>!e.required)).map((e=>e.name));if(klaro&&klaro.getManager){const e=klaro.getManager();return t.every((t=>!e.consents[t]))}if(klaroConfig&&"cookie"===klaroConfig.storageMethod){const e=klaroConfig.cookieName||klaroConfig.storageName,o=JSON.parse(decodeURIComponent(document.cookie.split(";").find((t=>t.trim().startsWith(e))).split("=")[1]));return Object.keys(o).filter((e=>t.includes(e))).every((e=>!1===o[e]))}},EVAL_ONETRUST_1:()=>window.OnetrustActiveGroups.split(",").filter((e=>e.length>0)).length<=1,EVAL_TRUSTARC_TOP:()=>window&&window.truste&&"0"===window.truste.eu.bindMap.prefCookie,EVAL_ADROLL_0:()=>!document.cookie.includes("__adroll_fpc"),EVAL_ALMACMP_0:()=>document.cookie.includes('"name":"Google","consent":false'),EVAL_AFFINITY_SERIF_COM_0:()=>document.cookie.includes("serif_manage_cookies_viewed")&&!document.cookie.includes("serif_allow_analytics"),EVAL_ARBEITSAGENTUR_TEST:()=>document.cookie.includes("cookie_consent=denied"),EVAL_AXEPTIO_0:()=>document.cookie.includes("axeptio_authorized_vendors=%2C%2C"),EVAL_BAHN_TEST:()=>1===utag.gdpr.getSelectedCategories().length,EVAL_BING_0:()=>document.cookie.includes("AL=0")&&document.cookie.includes("AD=0")&&document.cookie.includes("SM=0"),EVAL_BLOCKSY_0:()=>document.cookie.includes("blocksy_cookies_consent_accepted=no"),EVAL_BORLABS_0:()=>!JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>-1!==e.indexOf("borlabs-cookie"))).split("=",2)[1])).consents.statistics,EVAL_BUNDESREGIERUNG_DE_0:()=>document.cookie.match("cookie-allow-tracking=0"),EVAL_CANVA_0:()=>!document.cookie.includes("gtm_fpc_engagement_event"),EVAL_CC_BANNER2_0:()=>!!document.cookie.match(/sncc=[^;]+D%3Dtrue/),EVAL_CLICKIO_0:()=>document.cookie.includes("__lxG__consent__v2_daisybit="),EVAL_CLINCH_0:()=>document.cookie.includes("ctc_rejected=1"),EVAL_COOKIECONSENT2_TEST:()=>document.cookie.includes("cc_cookie="),EVAL_COOKIECONSENT3_TEST:()=>document.cookie.includes("cc_cookie="),EVAL_COINBASE_0:()=>JSON.parse(decodeURIComponent(document.cookie.match(/cm_(eu|default)_preferences=([0-9a-zA-Z\\{\\}\\[\\]%:]*);?/)[2])).consent.length<=1,EVAL_COMPLIANZ_BANNER_0:()=>document.cookie.includes("cmplz_banner-status=dismissed"),EVAL_COOKIE_LAW_INFO_0:()=>CLI.disableAllCookies()||CLI.reject_close()||!0,EVAL_COOKIE_LAW_INFO_1:()=>-1===document.cookie.indexOf("cookielawinfo-checkbox-non-necessary=yes"),EVAL_COOKIE_LAW_INFO_DETECT:()=>!!window.CLI,EVAL_COOKIE_MANAGER_POPUP_0:()=>!1===JSON.parse(document.cookie.split(";").find((e=>e.trim().startsWith("CookieLevel"))).split("=")[1]).social,EVAL_COOKIEALERT_0:()=>document.querySelector("body").removeAttribute("style")||!0,EVAL_COOKIEALERT_1:()=>document.querySelector("body").removeAttribute("style")||!0,EVAL_COOKIEALERT_2:()=>!0===window.CookieConsent.declined,EVAL_COOKIEFIRST_0:()=>{return!1===(e=JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>-1!==e.indexOf("cookiefirst"))).trim()).split("=")[1])).performance&&!1===e.functional&&!1===e.advertising;var e},EVAL_COOKIEFIRST_1:()=>document.querySelectorAll("button[data-cookiefirst-accent-color=true][role=checkbox]:not([disabled])").forEach((e=>"true"==e.getAttribute("aria-checked")&&e.click()))||!0,EVAL_COOKIEINFORMATION_0:()=>CookieInformation.declineAllCategories()||!0,EVAL_COOKIEINFORMATION_1:()=>CookieInformation.submitAllCategories()||!0,EVAL_COOKIEINFORMATION_2:()=>document.cookie.includes("CookieInformationConsent="),EVAL_COOKIEYES_0:()=>document.cookie.includes("advertisement:no"),EVAL_DAILYMOTION_0:()=>!!document.cookie.match("dm-euconsent-v2"),EVAL_DNDBEYOND_TEST:()=>document.cookie.includes("cookie-consent=denied"),EVAL_DSGVO_0:()=>!document.cookie.includes("sp_dsgvo_cookie_settings"),EVAL_DUNELM_0:()=>document.cookie.includes("cc_functional=0")&&document.cookie.includes("cc_targeting=0"),EVAL_ETSY_0:()=>document.querySelectorAll(".gdpr-overlay-body input").forEach((e=>{e.checked=!1}))||!0,EVAL_ETSY_1:()=>document.querySelector(".gdpr-overlay-view button[data-wt-overlay-close]").click()||!0,EVAL_EU_COOKIE_COMPLIANCE_0:()=>-1===document.cookie.indexOf("cookie-agreed=2"),EVAL_EU_COOKIE_LAW_0:()=>!document.cookie.includes("euCookie"),EVAL_EZOIC_0:()=>ezCMP.handleAcceptAllClick(),EVAL_EZOIC_1:()=>!!document.cookie.match(/ez-consent-tcf/),EVAL_GOOGLE_0:()=>!!document.cookie.match(/SOCS=CAE/),EVAL_HEMA_TEST_0:()=>document.cookie.includes("cookies_rejected=1"),EVAL_IUBENDA_0:()=>document.querySelectorAll(".purposes-item input[type=checkbox]:not([disabled])").forEach((e=>{e.checked&&e.click()}))||!0,EVAL_IUBENDA_1:()=>!!document.cookie.match(/_iub_cs-\d+=/),EVAL_IWINK_TEST:()=>document.cookie.includes("cookie_permission_granted=no"),EVAL_JQUERY_COOKIEBAR_0:()=>!document.cookie.includes("cookies-state=accepted"),EVAL_MEDIAVINE_0:()=>document.querySelectorAll('[data-name="mediavine-gdpr-cmp"] input[type=checkbox]').forEach((e=>e.checked&&e.click()))||!0,EVAL_MICROSOFT_0:()=>Array.from(document.querySelectorAll("div > button")).filter((e=>e.innerText.match("Reject|Ablehnen")))[0].click()||!0,EVAL_MICROSOFT_1:()=>Array.from(document.querySelectorAll("div > button")).filter((e=>e.innerText.match("Accept|Annehmen")))[0].click()||!0,EVAL_MICROSOFT_2:()=>!!document.cookie.match("MSCC|GHCC"),EVAL_MOOVE_0:()=>document.querySelectorAll("#moove_gdpr_cookie_modal input").forEach((e=>{e.disabled||"moove_gdpr_strict_cookies"===e.name||(e.checked=!1)}))||!0,EVAL_ONENINETWO_0:()=>document.cookie.includes("CC_ADVERTISING=NO")&&document.cookie.includes("CC_ANALYTICS=NO"),EVAL_OPERA_0:()=>document.cookie.includes("cookie_consent_essential=true")&&!document.cookie.includes("cookie_consent_marketing=true"),EVAL_PAYPAL_0:()=>!0===document.cookie.includes("cookie_prefs"),EVAL_PRIMEBOX_0:()=>!document.cookie.includes("cb-enabled=accepted"),EVAL_PUBTECH_0:()=>document.cookie.includes("euconsent-v2")&&(document.cookie.match(/.YAAAAAAAAAAA/)||document.cookie.match(/.aAAAAAAAAAAA/)||document.cookie.match(/.YAAACFgAAAAA/)),EVAL_REDDIT_0:()=>document.cookie.includes("eu_cookie={%22opted%22:true%2C%22nonessential%22:false}"),EVAL_SIBBO_0:()=>!!window.localStorage.getItem("euconsent-v2"),EVAL_SIRDATA_UNBLOCK_SCROLL:()=>(document.documentElement.classList.forEach((e=>{e.startsWith("sd-cmp-")&&document.documentElement.classList.remove(e)})),!0),EVAL_SNIGEL_0:()=>!!document.cookie.match("snconsent"),EVAL_STEAMPOWERED_0:()=>2===JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>e.trim().startsWith("cookieSettings"))).split("=")[1])).preference_state,EVAL_SVT_TEST:()=>document.cookie.includes('cookie-consent-1={"optedIn":true,"functionality":false,"statistics":false}'),EVAL_TAKEALOT_0:()=>document.body.classList.remove("freeze")||(document.body.style="")||!0,EVAL_TARTEAUCITRON_0:()=>tarteaucitron.userInterface.respondAll(!1)||!0,EVAL_TARTEAUCITRON_1:()=>tarteaucitron.userInterface.respondAll(!0)||!0,EVAL_TARTEAUCITRON_2:()=>document.cookie.match(/tarteaucitron=[^;]*/)[0].includes("false"),EVAL_TAUNTON_TEST:()=>document.cookie.includes("taunton_user_consent_submitted=true"),EVAL_TEALIUM_0:()=>void 0!==window.utag&&"object"==typeof utag.gdpr,EVAL_TEALIUM_1:()=>utag.gdpr.setConsentValue(!1)||!0,EVAL_TEALIUM_DONOTSELL:()=>utag.gdpr.dns?.setDnsState(!1)||!0,EVAL_TEALIUM_2:()=>utag.gdpr.setConsentValue(!0)||!0,EVAL_TEALIUM_3:()=>1!==utag.gdpr.getConsentState(),EVAL_TEALIUM_DONOTSELL_CHECK:()=>1!==utag.gdpr.dns?.getDnsState(),EVAL_TESTCMP_0:()=>"button_clicked"===window.results.results[0],EVAL_TESTCMP_COSMETIC_0:()=>"banner_hidden"===window.results.results[0],EVAL_THEFREEDICTIONARY_0:()=>cmpUi.showPurposes()||cmpUi.rejectAll()||!0,EVAL_THEFREEDICTIONARY_1:()=>cmpUi.allowAll()||!0,EVAL_THEVERGE_0:()=>document.cookie.includes("_duet_gdpr_acknowledged=1"),EVAL_UBUNTU_COM_0:()=>document.cookie.includes("_cookies_accepted=essential"),EVAL_UK_COOKIE_CONSENT_0:()=>!document.cookie.includes("catAccCookies"),EVAL_USERCENTRICS_API_0:()=>"object"==typeof UC_UI,EVAL_USERCENTRICS_API_1:()=>!!UC_UI.closeCMP(),EVAL_USERCENTRICS_API_2:()=>!!UC_UI.denyAllConsents(),EVAL_USERCENTRICS_API_3:()=>!!UC_UI.acceptAllConsents(),EVAL_USERCENTRICS_API_4:()=>!!UC_UI.closeCMP(),EVAL_USERCENTRICS_API_5:()=>!0===UC_UI.areAllConsentsAccepted(),EVAL_USERCENTRICS_API_6:()=>!1===UC_UI.areAllConsentsAccepted(),EVAL_USERCENTRICS_BUTTON_0:()=>JSON.parse(localStorage.getItem("usercentrics")).consents.every((e=>e.isEssential||!e.consentStatus)),EVAL_WAITROSE_0:()=>Array.from(document.querySelectorAll("label[id$=cookies-deny-label]")).forEach((e=>e.click()))||!0,EVAL_WAITROSE_1:()=>document.cookie.includes("wtr_cookies_advertising=0")&&document.cookie.includes("wtr_cookies_analytics=0"),EVAL_WP_COOKIE_NOTICE_0:()=>document.cookie.includes("wpl_viewed_cookie=no"),EVAL_XE_TEST:()=>document.cookie.includes("xeConsentState={%22performance%22:false%2C%22marketing%22:false%2C%22compliance%22:false}"),EVAL_XING_0:()=>document.cookie.includes("userConsent=%7B%22marketing%22%3Afalse"),EVAL_YOUTUBE_DESKTOP_0:()=>!!document.cookie.match(/SOCS=CAE/),EVAL_YOUTUBE_MOBILE_0:()=>!!document.cookie.match(/SOCS=CAE/)};var l={main:!0,frame:!1,urlPattern:""},p=class{constructor(e){this.runContext=l,this.autoconsent=e}get hasSelfTest(){throw new Error("Not Implemented")}get isIntermediate(){throw new Error("Not Implemented")}get isCosmetic(){throw new Error("Not Implemented")}mainWorldEval(e){const t=r[e];if(!t)return console.warn("Snippet not found",e),Promise.resolve(!1);const o=this.autoconsent.config.logs;if(this.autoconsent.config.isMainWorld){o.evals&&console.log("inline eval:",e,t);let c=!1;try{c=!!t.call(globalThis)}catch(t){o.evals&&console.error("error evaluating rule",e,t)}return Promise.resolve(c)}const c=`(${t.toString()})()`;return o.evals&&console.log("async eval:",e,c),s(c,e).catch((t=>(o.evals&&console.error("error evaluating rule",e,t),!1)))}checkRunContext(){const e={...l,...this.runContext},t=window.top===window;return!(t&&!e.main)&&(!(!t&&!e.frame)&&!(e.urlPattern&&!window.location.href.match(e.urlPattern)))}detectCmp(){throw new Error("Not Implemented")}async detectPopup(){return!1}optOut(){throw new Error("Not Implemented")}optIn(){throw new Error("Not Implemented")}openCmp(){throw new Error("Not Implemented")}async test(){return Promise.resolve(!0)}click(e,t=!1){return this.autoconsent.domActions.click(e,t)}elementExists(e){return this.autoconsent.domActions.elementExists(e)}elementVisible(e,t){return this.autoconsent.domActions.elementVisible(e,t)}waitForElement(e,t){return this.autoconsent.domActions.waitForElement(e,t)}waitForVisible(e,t,o){return this.autoconsent.domActions.waitForVisible(e,t,o)}waitForThenClick(e,t,o){return this.autoconsent.domActions.waitForThenClick(e,t,o)}wait(e){return this.autoconsent.domActions.wait(e)}hide(e,t){return this.autoconsent.domActions.hide(e,t)}prehide(e){return this.autoconsent.domActions.prehide(e)}undoPrehide(){return this.autoconsent.domActions.undoPrehide()}querySingleReplySelector(e,t){return this.autoconsent.domActions.querySingleReplySelector(e,t)}querySelectorChain(e){return this.autoconsent.domActions.querySelectorChain(e)}elementSelector(e){return this.autoconsent.domActions.elementSelector(e)}},d=class extends p{constructor(e,t){super(t),this.rule=e,this.name=e.name,this.runContext=e.runContext||l}get hasSelfTest(){return!!this.rule.test}get isIntermediate(){return!!this.rule.intermediate}get isCosmetic(){return!!this.rule.cosmetic}get prehideSelectors(){return this.rule.prehideSelectors}async detectCmp(){return!!this.rule.detectCmp&&this._runRulesParallel(this.rule.detectCmp)}async detectPopup(){return!!this.rule.detectPopup&&this._runRulesSequentially(this.rule.detectPopup)}async optOut(){const e=this.autoconsent.config.logs;return!!this.rule.optOut&&(e.lifecycle&&console.log("Initiated optOut()",this.rule.optOut),this._runRulesSequentially(this.rule.optOut))}async optIn(){const e=this.autoconsent.config.logs;return!!this.rule.optIn&&(e.lifecycle&&console.log("Initiated optIn()",this.rule.optIn),this._runRulesSequentially(this.rule.optIn))}async openCmp(){return!!this.rule.openCmp&&this._runRulesSequentially(this.rule.openCmp)}async test(){return this.hasSelfTest?this._runRulesSequentially(this.rule.test):super.test()}async evaluateRuleStep(e){const t=[],o=this.autoconsent.config.logs;if(e.exists&&t.push(this.elementExists(e.exists)),e.visible&&t.push(this.elementVisible(e.visible,e.check)),e.eval){const o=this.mainWorldEval(e.eval);t.push(o)}if(e.waitFor&&t.push(this.waitForElement(e.waitFor,e.timeout)),e.waitForVisible&&t.push(this.waitForVisible(e.waitForVisible,e.timeout,e.check)),e.click&&t.push(this.click(e.click,e.all)),e.waitForThenClick&&t.push(this.waitForThenClick(e.waitForThenClick,e.timeout,e.all)),e.wait&&t.push(this.wait(e.wait)),e.hide&&t.push(this.hide(e.hide,e.method)),e.if){if(!e.if.exists&&!e.if.visible)return console.error("invalid conditional rule",e.if),!1;const c=await this.evaluateRuleStep(e.if);o.rulesteps&&console.log("Condition is",c),c?t.push(this._runRulesSequentially(e.then)):e.else?t.push(this._runRulesSequentially(e.else)):t.push(!0)}if(e.any){for(const t of e.any)if(await this.evaluateRuleStep(t))return!0;return!1}if(0===t.length)return o.errors&&console.warn("Unrecognized rule",e),!1;return(await Promise.all(t)).reduce(((e,t)=>e&&t),!0)}async _runRulesParallel(e){const t=e.map((e=>this.evaluateRuleStep(e)));return(await Promise.all(t)).every((e=>!!e))}async _runRulesSequentially(e){const t=this.autoconsent.config.logs;for(const o of e){t.rulesteps&&console.log("Running rule...",o);const e=await this.evaluateRuleStep(o);if(t.rulesteps&&console.log("...rule result",e),!e&&!o.optional)return!1}return!0}};function u(e="autoconsent-css-rules"){const t=`style#${e}`,o=document.querySelector(t);if(o&&o instanceof HTMLStyleElement)return o;{const t=document.head||document.getElementsByTagName("head")[0]||document.documentElement,o=document.createElement("style");return o.id=e,t.appendChild(o),o}}function m(e,t,o="display"){const c=`${t} { ${"opacity"===o?"opacity: 0":"display: none"} !important; z-index: -1 !important; pointer-events: none !important; } `;return e instanceof HTMLStyleElement&&(e.innerText+=c,t.length>0)}async function h(e,t,o){const c=await e();return!c&&t>0?new Promise((c=>{setTimeout((async()=>{c(h(e,t-1,o))}),o)})):Promise.resolve(c)}function k(e){if(!e)return!1;if(null!==e.offsetParent)return!0;{const t=window.getComputedStyle(e);if("fixed"===t.position&&"none"!==t.display)return!0}return!1}function b(e){const t={enabled:!0,autoAction:"optOut",disabledCmps:[],enablePrehide:!0,enableCosmeticRules:!0,detectRetries:20,isMainWorld:!1,prehideTimeout:2e3,logs:{lifecycle:!1,rulesteps:!1,evals:!1,errors:!0,messages:!1}},o=(c=t,globalThis.structuredClone?structuredClone(c):JSON.parse(JSON.stringify(c)));var c;for(const c of Object.keys(t))void 0!==e[c]&&(o[c]=e[c]);return o}var _="#truste-show-consent",g="#truste-consent-track",y=[class extends p{constructor(e){super(e),this.name="TrustArc-top",this.prehideSelectors=[".trustarc-banner-container",`.truste_popframe,.truste_overlay,.truste_box_overlay,${g}`],this.runContext={main:!0,frame:!1},this._shortcutButton=null,this._optInDone=!1}get hasSelfTest(){return!1}get isIntermediate(){return!this._optInDone&&!this._shortcutButton}get isCosmetic(){return!1}async detectCmp(){const e=this.elementExists(`${_},${g}`);return e&&(this._shortcutButton=document.querySelector("#truste-consent-required")),e}async detectPopup(){return this.elementVisible(`#truste-consent-content,#trustarc-banner-overlay,${g}`,"all")}openFrame(){this.click(_)}async optOut(){return this._shortcutButton?(this._shortcutButton.click(),!0):(m(u(),`.truste_popframe, .truste_overlay, .truste_box_overlay, ${g}`),this.click(_),setTimeout((()=>{u().remove()}),1e4),!0)}async optIn(){return this._optInDone=!0,this.click("#truste-consent-button")}async openCmp(){return!0}async test(){return await this.mainWorldEval("EVAL_TRUSTARC_TOP")}},class extends p{constructor(){super(...arguments),this.name="TrustArc-frame",this.runContext={main:!1,frame:!0,urlPattern:"^https://consent-pref\\.trustarc\\.com/\\?"}}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return!0}async detectPopup(){return this.elementVisible("#defaultpreferencemanager","any")&&this.elementVisible(".mainContent","any")}async navigateToSettings(){return await h((async()=>this.elementExists(".shp")||this.elementVisible(".advance","any")||this.elementExists(".switch span:first-child")),10,500),this.elementExists(".shp")&&this.click(".shp"),await this.waitForElement(".prefPanel",5e3),this.elementVisible(".advance","any")&&this.click(".advance"),await h((()=>this.elementVisible(".switch span:first-child","any")),5,1e3)}async optOut(){return await h((()=>"complete"===document.readyState),20,100),await this.waitForElement(".mainContent[aria-hidden=false]",5e3),!!this.click(".rejectAll")||(this.elementExists(".prefPanel")&&await this.waitForElement('.prefPanel[style="visibility: visible;"]',3e3),this.click("#catDetails0")?(this.click(".submit"),this.waitForThenClick("#gwt-debug-close_id",5e3),!0):this.click(".required")?(this.waitForThenClick("#gwt-debug-close_id",5e3),!0):(await this.navigateToSettings(),this.click(".switch span:nth-child(1):not(.active)",!0),this.click(".submit"),this.waitForThenClick("#gwt-debug-close_id",3e5),!0))}async optIn(){return this.click(".call")||(await this.navigateToSettings(),this.click(".switch span:nth-child(2)",!0),this.click(".submit"),this.waitForElement("#gwt-debug-close_id",3e5).then((()=>{this.click("#gwt-debug-close_id")}))),!0}},class extends p{constructor(){super(...arguments),this.name="Cybotcookiebot",this.prehideSelectors=["#CybotCookiebotDialog,#CybotCookiebotDialogBodyUnderlay,#dtcookie-container,#cookiebanner,#cb-cookieoverlay,.modal--cookie-banner,#cookiebanner_outer,#CookieBanner"]}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return await this.mainWorldEval("EVAL_COOKIEBOT_1")}async detectPopup(){return this.mainWorldEval("EVAL_COOKIEBOT_2")}async optOut(){await this.wait(500);let e=await this.mainWorldEval("EVAL_COOKIEBOT_3");return await this.wait(500),e=e&&await this.mainWorldEval("EVAL_COOKIEBOT_4"),e}async optIn(){return this.elementExists("#dtcookie-container")?this.click(".h-dtcookie-accept"):(this.click(".CybotCookiebotDialogBodyLevelButton:not(:checked):enabled",!0),this.click("#CybotCookiebotDialogBodyLevelButtonAccept"),this.click("#CybotCookiebotDialogBodyButtonAccept"),!0)}async test(){return await this.wait(500),await this.mainWorldEval("EVAL_COOKIEBOT_5")}},class extends p{constructor(){super(...arguments),this.name="Sourcepoint-frame",this.prehideSelectors=["div[id^='sp_message_container_'],.message-overlay","#sp_privacy_manager_container"],this.ccpaNotice=!1,this.ccpaPopup=!1,this.runContext={main:!1,frame:!0}}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){const e=new URL(location.href);return e.searchParams.has("message_id")&&"ccpa-notice.sp-prod.net"===e.hostname?(this.ccpaNotice=!0,!0):"ccpa-pm.sp-prod.net"===e.hostname?(this.ccpaPopup=!0,!0):("/index.html"===e.pathname||"/privacy-manager/index.html"===e.pathname||"/ccpa_pm/index.html"===e.pathname)&&(e.searchParams.has("message_id")||e.searchParams.has("requestUUID")||e.searchParams.has("consentUUID"))}async detectPopup(){return!!this.ccpaNotice||(this.ccpaPopup?await this.waitForElement(".priv-save-btn",2e3):(await this.waitForElement(".sp_choice_type_11,.sp_choice_type_12,.sp_choice_type_13,.sp_choice_type_ACCEPT_ALL,.sp_choice_type_SAVE_AND_EXIT",2e3),!this.elementExists(".sp_choice_type_9")))}async optIn(){return await this.waitForElement(".sp_choice_type_11,.sp_choice_type_ACCEPT_ALL",2e3),!!this.click(".sp_choice_type_11")||!!this.click(".sp_choice_type_ACCEPT_ALL")}isManagerOpen(){return"/privacy-manager/index.html"===location.pathname||"/ccpa_pm/index.html"===location.pathname}async optOut(){const e=this.autoconsent.config.logs;if(this.ccpaPopup){const e=document.querySelectorAll(".priv-purpose-container .sp-switch-arrow-block a.neutral.on .right");for(const t of e)t.click();const t=document.querySelectorAll(".priv-purpose-container .sp-switch-arrow-block a.switch-bg.on");for(const e of t)e.click();return this.click(".priv-save-btn")}if(!this.isManagerOpen()){if(!await this.waitForElement(".sp_choice_type_12,.sp_choice_type_13"))return!1;if(!this.elementExists(".sp_choice_type_12"))return this.click(".sp_choice_type_13");this.click(".sp_choice_type_12"),await h((()=>this.isManagerOpen()),200,100)}await this.waitForElement(".type-modal",2e4),this.waitForThenClick(".ccpa-stack .pm-switch[aria-checked=true] .slider",500,!0);try{const e=".sp_choice_type_REJECT_ALL",t=".reject-toggle",o=await Promise.race([this.waitForElement(e,2e3).then((e=>e?0:-1)),this.waitForElement(t,2e3).then((e=>e?1:-1)),this.waitForElement(".pm-features",2e3).then((e=>e?2:-1))]);if(0===o)return await this.wait(1500),this.click(e);1===o?this.click(t):2===o&&(await this.waitForElement(".pm-features",1e4),this.click(".checked > span",!0),this.click(".chevron"))}catch(t){e.errors&&console.warn(t)}return this.click(".sp_choice_type_SAVE_AND_EXIT")}},class extends p{constructor(){super(...arguments),this.name="consentmanager.net",this.prehideSelectors=["#cmpbox,#cmpbox2"],this.apiAvailable=!1}get hasSelfTest(){return this.apiAvailable}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.apiAvailable=await this.mainWorldEval("EVAL_CONSENTMANAGER_1"),!!this.apiAvailable||this.elementExists("#cmpbox")}async detectPopup(){return this.apiAvailable?(await this.wait(500),await this.mainWorldEval("EVAL_CONSENTMANAGER_2")):this.elementVisible("#cmpbox .cmpmore","any")}async optOut(){return await this.wait(500),this.apiAvailable?await this.mainWorldEval("EVAL_CONSENTMANAGER_3"):!!this.click(".cmpboxbtnno")||(this.elementExists(".cmpwelcomeprpsbtn")?(this.click(".cmpwelcomeprpsbtn > a[aria-checked=true]",!0),this.click(".cmpboxbtnsave"),!0):(this.click(".cmpboxbtncustom"),await this.waitForElement(".cmptblbox",2e3),this.click(".cmptdchoice > a[aria-checked=true]",!0),this.click(".cmpboxbtnyescustomchoices"),this.hide("#cmpwrapper,#cmpbox","display"),!0))}async optIn(){return this.apiAvailable?await this.mainWorldEval("EVAL_CONSENTMANAGER_4"):this.click(".cmpboxbtnyes")}async test(){if(this.apiAvailable)return await this.mainWorldEval("EVAL_CONSENTMANAGER_5")}},class extends p{constructor(){super(...arguments),this.name="Evidon"}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("#_evidon_banner")}async detectPopup(){return this.elementVisible("#_evidon_banner","any")}async optOut(){return this.click("#_evidon-decline-button")||(m(u(),"#evidon-prefdiag-overlay,#evidon-prefdiag-background"),this.click("#_evidon-option-button"),await this.waitForElement("#evidon-prefdiag-overlay",5e3),this.click("#evidon-prefdiag-decline")),!0}async optIn(){return this.click("#_evidon-accept-button")}},class extends p{constructor(){super(...arguments),this.name="Onetrust",this.prehideSelectors=["#onetrust-banner-sdk,#onetrust-consent-sdk,.onetrust-pc-dark-filter,.js-consent-banner"],this.runContext={urlPattern:"^(?!.*https://www\\.nba\\.com/)"}}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("#onetrust-banner-sdk,#onetrust-consent-sdk")}async detectPopup(){return this.elementVisible("#onetrust-banner-sdk,#onetrust-consent-sdk","any")}async optOut(){return this.elementVisible("#onetrust-reject-all-handler,.ot-pc-refuse-all-handler,.js-reject-cookies","any")?this.click("#onetrust-reject-all-handler,.ot-pc-refuse-all-handler,.js-reject-cookies"):(this.elementExists("#onetrust-pc-btn-handler")?this.click("#onetrust-pc-btn-handler"):this.click(".ot-sdk-show-settings,button.js-cookie-settings"),await this.waitForElement("#onetrust-consent-sdk",2e3),await this.wait(1e3),this.click("#onetrust-consent-sdk input.category-switch-handler:checked,.js-editor-toggle-state:checked",!0),await this.wait(1e3),await this.waitForElement(".save-preference-btn-handler,.js-consent-save",2e3),this.click(".save-preference-btn-handler,.js-consent-save"),await this.waitForVisible("#onetrust-banner-sdk",5e3,"none"),!0)}async optIn(){return this.click("#onetrust-accept-btn-handler,#accept-recommended-btn-handler,.js-accept-cookies")}async test(){return await h((()=>this.mainWorldEval("EVAL_ONETRUST_1")),10,500)}},class extends p{constructor(){super(...arguments),this.name="Klaro",this.prehideSelectors=[".klaro"],this.settingsOpen=!1}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".klaro > .cookie-modal")?(this.settingsOpen=!0,!0):this.elementExists(".klaro > .cookie-notice")}async detectPopup(){return this.elementVisible(".klaro > .cookie-notice,.klaro > .cookie-modal","any")}async optOut(){return!!this.click(".klaro .cn-decline")||(this.settingsOpen||(this.click(".klaro .cn-learn-more,.klaro .cm-button-manage"),await this.waitForElement(".klaro > .cookie-modal",2e3),this.settingsOpen=!0),!!this.click(".klaro .cn-decline")||(this.click(".cm-purpose:not(.cm-toggle-all) > input:not(.half-checked,.required,.only-required),.cm-purpose:not(.cm-toggle-all) > div > input:not(.half-checked,.required,.only-required)",!0),this.click(".cm-btn-accept,.cm-button")))}async optIn(){return!!this.click(".klaro .cm-btn-accept-all")||(this.settingsOpen?(this.click(".cm-purpose:not(.cm-toggle-all) > input.half-checked",!0),this.click(".cm-btn-accept")):this.click(".klaro .cookie-notice .cm-btn-success"))}async test(){return await this.mainWorldEval("EVAL_KLARO_1")}},class extends p{constructor(){super(...arguments),this.name="Uniconsent"}get prehideSelectors(){return[".unic",".modal:has(.unic)"]}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".unic .unic-box,.unic .unic-bar,.unic .unic-modal")}async detectPopup(){return this.elementVisible(".unic .unic-box,.unic .unic-bar,.unic .unic-modal","any")}async optOut(){if(await this.waitForElement(".unic button",1e3),document.querySelectorAll(".unic button").forEach((e=>{const t=e.textContent;(t.includes("Manage Options")||t.includes("Optionen verwalten"))&&e.click()})),await this.waitForElement(".unic input[type=checkbox]",1e3)){await this.waitForElement(".unic button",1e3),document.querySelectorAll(".unic input[type=checkbox]").forEach((e=>{e.checked&&e.click()}));for(const e of document.querySelectorAll(".unic button")){const t=e.textContent;for(const o of["Confirm Choices","Save Choices","Auswahl speichern"])if(t.includes(o))return e.click(),await this.wait(500),!0}}return!1}async optIn(){return this.waitForThenClick(".unic #unic-agree")}async test(){await this.wait(1e3);return!this.elementExists(".unic .unic-box,.unic .unic-bar")}},class extends p{constructor(){super(...arguments),this.prehideSelectors=[".cmp-root"],this.name="Conversant"}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".cmp-root .cmp-receptacle")}async detectPopup(){return this.elementVisible(".cmp-root .cmp-receptacle","any")}async optOut(){if(!await this.waitForThenClick(".cmp-main-button:not(.cmp-main-button--primary)"))return!1;if(!await this.waitForElement(".cmp-view-tab-tabs"))return!1;await this.waitForThenClick(".cmp-view-tab-tabs > :first-child"),await this.waitForThenClick(".cmp-view-tab-tabs > .cmp-view-tab--active:first-child");for(const e of Array.from(document.querySelectorAll(".cmp-accordion-item"))){e.querySelector(".cmp-accordion-item-title").click(),await h((()=>!!e.querySelector(".cmp-accordion-item-content.cmp-active")),10,50);const t=e.querySelector(".cmp-accordion-item-content.cmp-active");t.querySelectorAll(".cmp-toggle-actions .cmp-toggle-deny:not(.cmp-toggle-deny--active)").forEach((e=>e.click())),t.querySelectorAll(".cmp-toggle-actions .cmp-toggle-checkbox:not(.cmp-toggle-checkbox--active)").forEach((e=>e.click()))}return await this.click(".cmp-main-button:not(.cmp-main-button--primary)"),!0}async optIn(){return this.waitForThenClick(".cmp-main-button.cmp-main-button--primary")}async test(){return document.cookie.includes("cmp-data=0")}},class extends p{constructor(){super(...arguments),this.name="tiktok.com",this.runContext={urlPattern:"tiktok"}}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}getShadowRoot(){const e=document.querySelector("tiktok-cookie-banner");return e?e.shadowRoot:null}async detectCmp(){return this.elementExists("tiktok-cookie-banner")}async detectPopup(){return k(this.getShadowRoot().querySelector(".tiktok-cookie-banner"))}async optOut(){const e=this.autoconsent.config.logs,t=this.getShadowRoot().querySelector(".button-wrapper button:first-child");return t?(e.rulesteps&&console.log("[clicking]",t),t.click(),!0):(e.errors&&console.log("no decline button found"),!1)}async optIn(){const e=this.autoconsent.config.logs,t=this.getShadowRoot().querySelector(".button-wrapper button:last-child");return t?(e.rulesteps&&console.log("[clicking]",t),t.click(),!0):(e.errors&&console.log("no accept button found"),!1)}async test(){const e=document.cookie.match(/cookie-consent=([^;]+)/);if(!e)return!1;const t=JSON.parse(decodeURIComponent(e[1]));return Object.values(t).every((e=>"boolean"!=typeof e||!1===e))}},class extends p{constructor(){super(...arguments),this.runContext={urlPattern:"^https://(www\\.)?airbnb\\.[^/]+/"},this.prehideSelectors=["div[data-testid=main-cookies-banner-container]",'div:has(> div:first-child):has(> div:last-child):has(> section [data-testid="strictly-necessary-cookies"])']}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("div[data-testid=main-cookies-banner-container]")}async detectPopup(){return this.elementVisible("div[data-testid=main-cookies-banner-container","any")}async optOut(){let e;for(await this.waitForThenClick("div[data-testid=main-cookies-banner-container] button._snbhip0");e=document.querySelector("[data-testid=modal-container] button[aria-checked=true]:not([disabled])");)e.click();return this.waitForThenClick("button[data-testid=save-btn]")}async optIn(){return this.waitForThenClick("div[data-testid=main-cookies-banner-container] button._148dgdpk")}async test(){return await h((()=>!!document.cookie.match("OptanonAlertBoxClosed")),20,200)}}];var w=[{name:"192.com",detectCmp:[{exists:".ont-cookies"}],detectPopup:[{visible:".ont-cookies"}],optIn:[{click:".ont-btn-main.ont-cookies-btn.js-ont-btn-ok2"}],optOut:[{click:".ont-cookes-btn-manage"},{click:".ont-btn-main.ont-cookies-btn.js-ont-btn-choose"}],test:[{eval:"EVAL_ONENINETWO_0"}]},{name:"1password-com",cosmetic:!0,prehideSelectors:['footer #footer-root [aria-label="Cookie Consent"]'],detectCmp:[{exists:'footer #footer-root [aria-label="Cookie Consent"]'}],detectPopup:[{visible:'footer #footer-root [aria-label="Cookie Consent"]'}],optIn:[{click:'footer #footer-root [aria-label="Cookie Consent"] button'}],optOut:[{hide:'footer #footer-root [aria-label="Cookie Consent"]'}]},{name:"abconcerts.be",vendorUrl:"https://unknown",intermediate:!1,prehideSelectors:["dialog.cookie-consent"],detectCmp:[{exists:"dialog.cookie-consent form.cookie-consent__form"}],detectPopup:[{visible:"dialog.cookie-consent form.cookie-consent__form"}],optIn:[{waitForThenClick:"dialog.cookie-consent form.cookie-consent__form button[value=yes]"}],optOut:[{if:{exists:"dialog.cookie-consent form.cookie-consent__form button[value=no]"},then:[{click:"dialog.cookie-consent form.cookie-consent__form button[value=no]"}],else:[{click:"dialog.cookie-consent form.cookie-consent__form button.cookie-consent__options-toggle"},{waitForThenClick:'dialog.cookie-consent form.cookie-consent__form button[value="save_options"]'}]}]},{name:"activobank.pt",runContext:{urlPattern:"^https://(www\\.)?activobank\\.pt"},prehideSelectors:["aside#cookies,.overlay-cookies"],detectCmp:[{exists:"#cookies .cookies-btn"}],detectPopup:[{visible:"#cookies #submitCookies"}],optIn:[{waitForThenClick:"#cookies #submitCookies"}],optOut:[{waitForThenClick:"#cookies #rejectCookies"}]},{name:"Adroll",prehideSelectors:["#adroll_consent_container"],detectCmp:[{exists:"#adroll_consent_container"}],detectPopup:[{visible:"#adroll_consent_container"}],optIn:[{waitForThenClick:"#adroll_consent_accept"}],optOut:[{waitForThenClick:"#adroll_consent_reject"}],test:[{eval:"EVAL_ADROLL_0"}]},{name:"affinity.serif.com",detectCmp:[{exists:".c-cookie-banner button[data-qa='allow-all-cookies']"}],detectPopup:[{visible:".c-cookie-banner"}],optIn:[{click:'button[data-qa="allow-all-cookies"]'}],optOut:[{click:'button[data-qa="manage-cookies"]'},{waitFor:'.c-cookie-banner ~ [role="dialog"]'},{waitForThenClick:'.c-cookie-banner ~ [role="dialog"] input[type="checkbox"][value="true"]',all:!0},{click:'.c-cookie-banner ~ [role="dialog"] .c-modal__action button'}],test:[{wait:500},{eval:"EVAL_AFFINITY_SERIF_COM_0"}]},{name:"agolde.com",cosmetic:!0,prehideSelectors:["#modal-1 div[data-micromodal-close]"],detectCmp:[{exists:"#modal-1 div[aria-labelledby=modal-1-title]"}],detectPopup:[{exists:"#modal-1 div[data-micromodal-close]"}],optIn:[{click:'button[aria-label="Close modal"]'}],optOut:[{hide:"#modal-1 div[data-micromodal-close]"}]},{name:"aliexpress",vendorUrl:"https://aliexpress.com/",runContext:{urlPattern:"^https://.*\\.aliexpress\\.com/"},prehideSelectors:["#gdpr-new-container"],detectCmp:[{exists:"#gdpr-new-container"}],detectPopup:[{visible:"#gdpr-new-container"}],optIn:[{waitForThenClick:"#gdpr-new-container .btn-accept"}],optOut:[{waitForThenClick:"#gdpr-new-container .btn-more"},{waitFor:"#gdpr-new-container .gdpr-dialog-switcher"},{click:"#gdpr-new-container .switcher-on",all:!0,optional:!0},{click:"#gdpr-new-container .btn-save"}]},{name:"almacmp",prehideSelectors:["#alma-cmpv2-container"],detectCmp:[{exists:"#alma-cmpv2-container"}],detectPopup:[{visible:"#alma-cmpv2-container #almacmp-modal-layer1"}],optIn:[{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer1 #almacmp-modalConfirmBtn"}],optOut:[{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer1 #almacmp-modalSettingBtn"},{waitFor:"#alma-cmpv2-container #almacmp-modal-layer2"},{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer2 #almacmp-reject-all-layer2"}],test:[{eval:"EVAL_ALMACMP_0"}]},{name:"altium.com",cosmetic:!0,prehideSelectors:[".altium-privacy-bar"],detectCmp:[{exists:".altium-privacy-bar"}],detectPopup:[{exists:".altium-privacy-bar"}],optIn:[{click:"a.altium-privacy-bar__btn"}],optOut:[{hide:".altium-privacy-bar"}]},{name:"amazon.com",prehideSelectors:['span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'],detectCmp:[{exists:'span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'}],detectPopup:[{visible:'span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'}],optIn:[{waitForVisible:"#sp-cc-accept"},{wait:500},{click:"#sp-cc-accept"}],optOut:[{waitForVisible:"#sp-cc-rejectall-link"},{wait:500},{click:"#sp-cc-rejectall-link"}]},{name:"aquasana.com",cosmetic:!0,prehideSelectors:["#consent-tracking"],detectCmp:[{exists:"#consent-tracking"}],detectPopup:[{exists:"#consent-tracking"}],optIn:[{click:"#accept_consent"}],optOut:[{hide:"#consent-tracking"}]},{name:"arbeitsagentur",vendorUrl:"https://www.arbeitsagentur.de/",prehideSelectors:[".modal-open bahf-cookie-disclaimer-dpl3"],detectCmp:[{exists:"bahf-cookie-disclaimer-dpl3"}],detectPopup:[{visible:"bahf-cookie-disclaimer-dpl3"}],optIn:[{waitForThenClick:["bahf-cookie-disclaimer-dpl3","bahf-cd-modal-dpl3 .ba-btn-primary"]}],optOut:[{waitForThenClick:["bahf-cookie-disclaimer-dpl3","bahf-cd-modal-dpl3 .ba-btn-contrast"]}],test:[{eval:"EVAL_ARBEITSAGENTUR_TEST"}]},{name:"asus",vendorUrl:"https://www.asus.com/",runContext:{urlPattern:"^https://www\\.asus\\.com/"},prehideSelectors:["#cookie-policy-info,#cookie-policy-info-bg"],detectCmp:[{exists:"#cookie-policy-info"}],detectPopup:[{visible:"#cookie-policy-info"}],optIn:[{waitForThenClick:'#cookie-policy-info [data-agree="Accept Cookies"]'}],optOut:[{if:{exists:"#cookie-policy-info .btn-reject"},then:[{waitForThenClick:"#cookie-policy-info .btn-reject"}],else:[{waitForThenClick:"#cookie-policy-info .btn-setting"},{waitForThenClick:'#cookie-policy-lightbox-wrapper [data-agree="Save Settings"]'}]}]},{name:"athlinks-com",runContext:{urlPattern:"^https://(www\\.)?athlinks\\.com/"},cosmetic:!0,prehideSelectors:["#footer-container ~ div"],detectCmp:[{exists:"#footer-container ~ div"}],detectPopup:[{visible:"#footer-container > div"}],optIn:[{click:"#footer-container ~ div button"}],optOut:[{hide:"#footer-container ~ div"}]},{name:"ausopen.com",cosmetic:!0,detectCmp:[{exists:".gdpr-popup__message"}],detectPopup:[{visible:".gdpr-popup__message"}],optOut:[{hide:".gdpr-popup__message"}],optIn:[{click:".gdpr-popup__message button"}]},{name:"automattic-cmp-optout",prehideSelectors:['form[class*="cookie-banner"][method="post"]'],detectCmp:[{exists:'form[class*="cookie-banner"][method="post"]'}],detectPopup:[{visible:'form[class*="cookie-banner"][method="post"]'}],optIn:[{click:'a[class*="accept-all-button"]'}],optOut:[{click:'form[class*="cookie-banner"] div[class*="simple-options"] a[class*="customize-button"]'},{waitForThenClick:"input[type=checkbox][checked]:not([disabled])",all:!0},{click:'a[class*="accept-selection-button"]'}]},{name:"aws.amazon.com",prehideSelectors:["#awsccc-cb-content","#awsccc-cs-container","#awsccc-cs-modalOverlay","#awsccc-cs-container-inner"],detectCmp:[{exists:"#awsccc-cb-content"}],detectPopup:[{visible:"#awsccc-cb-content"}],optIn:[{click:"button[data-id=awsccc-cb-btn-accept"}],optOut:[{click:"button[data-id=awsccc-cb-btn-customize]"},{waitFor:"input[aria-checked]"},{click:"input[aria-checked=true]",all:!0,optional:!0},{click:"button[data-id=awsccc-cs-btn-save]"}]},{name:"axeptio",prehideSelectors:[".axeptio_widget"],detectCmp:[{exists:".axeptio_widget"}],detectPopup:[{visible:".axeptio_widget"}],optIn:[{waitFor:".axeptio-widget--open"},{click:"button#axeptio_btn_acceptAll"}],optOut:[{waitFor:".axeptio-widget--open"},{click:"button#axeptio_btn_dismiss"}],test:[{eval:"EVAL_AXEPTIO_0"}]},{name:"baden-wuerttemberg.de",prehideSelectors:[".cookie-alert.t-dark"],cosmetic:!0,detectCmp:[{exists:".cookie-alert.t-dark"}],detectPopup:[{visible:".cookie-alert.t-dark"}],optIn:[{click:".cookie-alert__form input:not([disabled]):not([checked])"},{click:".cookie-alert__button button"}],optOut:[{hide:".cookie-alert.t-dark"}]},{name:"bahn-de",vendorUrl:"https://www.bahn.de/",cosmetic:!1,runContext:{main:!0,frame:!1,urlPattern:"^https://(www\\.)?bahn\\.de/"},intermediate:!1,prehideSelectors:[],detectCmp:[{exists:["body > div:first-child","#consent-layer"]}],detectPopup:[{visible:["body > div:first-child","#consent-layer"]}],optIn:[{waitForThenClick:["body > div:first-child","#consent-layer .js-accept-all-cookies"]}],optOut:[{waitForThenClick:["body > div:first-child","#consent-layer .js-accept-essential-cookies"]}],test:[{eval:"EVAL_BAHN_TEST"}]},{name:"bbb.org",runContext:{urlPattern:"^https://www\\.bbb\\.org/"},cosmetic:!0,prehideSelectors:['div[aria-label="use of cookies on bbb.org"]'],detectCmp:[{exists:'div[aria-label="use of cookies on bbb.org"]'}],detectPopup:[{visible:'div[aria-label="use of cookies on bbb.org"]'}],optIn:[{click:'div[aria-label="use of cookies on bbb.org"] button.bds-button-unstyled span.visually-hidden'}],optOut:[{hide:'div[aria-label="use of cookies on bbb.org"]'}]},{name:"bing.com",prehideSelectors:["#bnp_container"],detectCmp:[{exists:"#bnp_cookie_banner"}],detectPopup:[{visible:"#bnp_cookie_banner"}],optIn:[{click:"#bnp_btn_accept"}],optOut:[{click:"#bnp_btn_preference"},{click:"#mcp_savesettings"}],test:[{eval:"EVAL_BING_0"}]},{name:"blocksy",vendorUrl:"https://creativethemes.com/blocksy/docs/extensions/cookies-consent/",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,prehideSelectors:[".cookie-notification"],detectCmp:[{exists:"#blocksy-ext-cookies-consent-styles-css"}],detectPopup:[{visible:".cookie-notification"}],optIn:[{click:".cookie-notification .ct-cookies-decline-button"}],optOut:[{waitForThenClick:".cookie-notification .ct-cookies-decline-button"}],test:[{eval:"EVAL_BLOCKSY_0"}]},{name:"borlabs",detectCmp:[{exists:"._brlbs-block-content"}],detectPopup:[{visible:"._brlbs-bar-wrap,._brlbs-box-wrap"}],optIn:[{click:"a[data-cookie-accept-all]"}],optOut:[{click:"a[data-cookie-individual]"},{waitForVisible:".cookie-preference"},{click:"input[data-borlabs-cookie-checkbox]:checked",all:!0,optional:!0},{click:"#CookiePrefSave"},{wait:500}],prehideSelectors:["#BorlabsCookieBox"],test:[{eval:"EVAL_BORLABS_0"}]},{name:"bundesregierung.de",prehideSelectors:[".bpa-cookie-banner"],detectCmp:[{exists:".bpa-cookie-banner"}],detectPopup:[{visible:".bpa-cookie-banner .bpa-module-full-hero"}],optIn:[{click:".bpa-accept-all-button"}],optOut:[{wait:500,comment:"click is not immediately recognized"},{waitForThenClick:".bpa-close-button"}],test:[{eval:"EVAL_BUNDESREGIERUNG_DE_0"}]},{name:"burpee.com",cosmetic:!0,prehideSelectors:["#notice-cookie-block"],detectCmp:[{exists:"#notice-cookie-block"}],detectPopup:[{exists:"#html-body #notice-cookie-block"}],optIn:[{click:"#btn-cookie-allow"}],optOut:[{hide:"#html-body #notice-cookie-block, #notice-cookie"}]},{name:"canva.com",prehideSelectors:['div[role="dialog"] a[data-anchor-id="cookie-policy"]'],detectCmp:[{exists:'div[role="dialog"] a[data-anchor-id="cookie-policy"]'}],detectPopup:[{exists:'div[role="dialog"] a[data-anchor-id="cookie-policy"]'}],optIn:[{click:'div[role="dialog"] button:nth-child(1)'}],optOut:[{if:{exists:'div[role="dialog"] button:nth-child(3)'},then:[{click:'div[role="dialog"] button:nth-child(2)'}],else:[{click:'div[role="dialog"] button:nth-child(2)'},{waitFor:'div[role="dialog"] a[data-anchor-id="privacy-policy"]'},{click:'div[role="dialog"] button:nth-child(2)'},{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 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:[{click:"#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:"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"}],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",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)"},{click:'div.b-cookies-informer__switchers > div:nth-child(2) > div[at-attr="checkbox"] > span.b-input-radio__container > input[type="checkbox"]'},{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:!0,prehideSelectors:["#sd-cmp"],detectCmp:[{exists:"#sd-cmp"}],detectPopup:[{visible:"#sd-cmp"}],optIn:[{waitForThenClick:"#sd-cmp .sd-cmp-3cRQ2"}],optOut:[{hide:"#sd-cmp"},{eval:"EVAL_SIRDATA_UNBLOCK_SCROLL"}]},{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:["#cookie_initial_modal",".modal-backdrop"],runContext:{urlPattern:"^https://www\\.strato\\.de/"},detectCmp:[{exists:"#cookie_initial_modal"}],detectPopup:[{visible:"#cookie_initial_modal"}],optIn:[{click:"button#jss_consent_all_initial_modal"}],optOut:[{click:"button#jss_open_settings_modal"},{click:"button#jss_consent_checked"}]},{name:"svt.se",vendorUrl:"https://www.svt.se/",runContext:{urlPattern:"^https://www\\.svt\\.se/"},prehideSelectors:["[class*=CookieConsent__root___]"],detectCmp:[{exists:"[class*=CookieConsent__root___]"}],detectPopup:[{visible:"[class*=CookieConsent__modal___]"}],optIn:[{waitForThenClick:"[class*=CookieConsent__modal___] > div > button[class*=primary]"}],optOut:[{waitForThenClick:"[class*=CookieConsent__modal___] > div > button[class*=secondary]:nth-child(2)"}],test:[{eval:"EVAL_SVT_TEST"}]},{name:"takealot.com",cosmetic:!0,prehideSelectors:['div[class^="cookies-banner-module_"]'],detectCmp:[{exists:'div[class^="cookies-banner-module_cookie-banner_"]'}],detectPopup:[{exists:'div[class^="cookies-banner-module_cookie-banner_"]'}],optIn:[{click:'button[class*="cookies-banner-module_dismiss-button_"]'}],optOut:[{hide:'div[class^="cookies-banner-module_"]'},{if:{exists:'div[class^="cookies-banner-module_small-cookie-banner_"]'},then:[{eval:"EVAL_TAKEALOT_0"}],else:[]}]},{name:"tarteaucitron.js",prehideSelectors:["#tarteaucitronRoot"],detectCmp:[{exists:"#tarteaucitronRoot"}],detectPopup:[{visible:"#tarteaucitronRoot #tarteaucitronAlertSmall,#tarteaucitronRoot #tarteaucitronAlertBig",check:"any"}],optIn:[{eval:"EVAL_TARTEAUCITRON_1"}],optOut:[{eval:"EVAL_TARTEAUCITRON_0"}],test:[{eval:"EVAL_TARTEAUCITRON_2",comment:"sometimes there are required categories, so we check that at least something is false"}]},{name:"taunton",vendorUrl:"https://www.taunton.com/",prehideSelectors:["#taunton-user-consent__overlay"],detectCmp:[{exists:"#taunton-user-consent__overlay"}],detectPopup:[{exists:"#taunton-user-consent__overlay:not([aria-hidden=true])"}],optIn:[{click:"#taunton-user-consent__toolbar input[type=checkbox]:not(:checked)"},{click:"#taunton-user-consent__toolbar button[type=submit]"}],optOut:[{click:"#taunton-user-consent__toolbar input[type=checkbox]:checked",optional:!0,all:!0},{click:"#taunton-user-consent__toolbar button[type=submit]"}],test:[{eval:"EVAL_TAUNTON_TEST"}]},{name:"Tealium",prehideSelectors:["#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs,#__tealiumImplicitmodal,#consent-layer"],detectCmp:[{exists:"#__tealiumGDPRecModal *,#__tealiumGDPRcpPrefs *,#__tealiumImplicitmodal *"},{eval:"EVAL_TEALIUM_0"}],detectPopup:[{visible:"#__tealiumGDPRecModal *,#__tealiumGDPRcpPrefs *,#__tealiumImplicitmodal *",check:"any"}],optOut:[{eval:"EVAL_TEALIUM_1"},{eval:"EVAL_TEALIUM_DONOTSELL"},{hide:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs,#__tealiumImplicitmodal"},{waitForThenClick:"#cm-acceptNone,.js-accept-essential-cookies",timeout:1e3,optional:!0}],optIn:[{hide:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs"},{eval:"EVAL_TEALIUM_2"}],test:[{eval:"EVAL_TEALIUM_3"},{eval:"EVAL_TEALIUM_DONOTSELL_CHECK"},{visible:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs",check:"none"}]},{name:"temu",vendorUrl:"https://temu.com",runContext:{urlPattern:"^https://[^/]*temu\\.com/"},prehideSelectors:["._2d-8vq-W,._1UdBUwni"],detectCmp:[{exists:"._3YCsmIaS"}],detectPopup:[{visible:"._3YCsmIaS"}],optIn:[{waitForThenClick:"._3fKiu5wx._3zN5SumS._3tAK973O.IYOfhWEs.VGNGF1pA"}],optOut:[{waitForThenClick:"._3fKiu5wx._1_XToJBF._3tAK973O.IYOfhWEs.VGNGF1pA"}]},{name:"Termly",prehideSelectors:["#termly-code-snippet-support"],detectCmp:[{exists:"#termly-code-snippet-support"}],detectPopup:[{visible:"#termly-code-snippet-support div"}],optIn:[{waitForThenClick:'[data-tid="banner-accept"]'}],optOut:[{if:{exists:'[data-tid="banner-decline"]'},then:[{click:'[data-tid="banner-decline"]'}],else:[{click:".t-preference-button"},{wait:500},{if:{exists:".t-declineAllButton"},then:[{click:".t-declineAllButton"}],else:[{waitForThenClick:".t-preference-modal input[type=checkbox][checked]:not([disabled])",all:!0},{waitForThenClick:".t-saveButton"}]}]}]},{name:"termsfeed",vendorUrl:"https://termsfeed.com",comment:"v4.x.x",prehideSelectors:[".termsfeed-com---nb"],detectCmp:[{exists:".termsfeed-com---nb"}],detectPopup:[{visible:".termsfeed-com---nb"}],optIn:[{waitForThenClick:".cc-nb-okagree"}],optOut:[{waitForThenClick:".cc-nb-reject"}]},{name:"termsfeed3",vendorUrl:"https://termsfeed.com",comment:"v3.x.x",cosmetic:!0,prehideSelectors:[".cc_dialog.cc_css_reboot"],detectCmp:[{exists:".cc_dialog.cc_css_reboot"}],detectPopup:[{visible:".cc_dialog.cc_css_reboot"}],optIn:[{waitForThenClick:".cc_dialog.cc_css_reboot .cc_b_ok"}],optOut:[{hide:".cc_dialog.cc_css_reboot"}]},{name:"Test page cosmetic CMP",cosmetic:!0,prehideSelectors:["#privacy-test-page-cmp-test-prehide"],detectCmp:[{exists:"#privacy-test-page-cmp-test-banner"}],detectPopup:[{visible:"#privacy-test-page-cmp-test-banner"}],optIn:[{waitFor:"#accept-all"},{click:"#accept-all"}],optOut:[{hide:"#privacy-test-page-cmp-test-banner"}],test:[{wait:500},{eval:"EVAL_TESTCMP_COSMETIC_0"}]},{name:"Test page CMP",prehideSelectors:["#reject-all"],detectCmp:[{exists:"#privacy-test-page-cmp-test"}],detectPopup:[{visible:"#privacy-test-page-cmp-test"}],optIn:[{waitFor:"#accept-all"},{click:"#accept-all"}],optOut:[{waitFor:"#reject-all"},{click:"#reject-all"}],test:[{eval:"EVAL_TESTCMP_0"}]},{name:"thalia.de",prehideSelectors:[".consent-banner-box"],detectCmp:[{exists:"consent-banner[component=consent-banner]"}],detectPopup:[{visible:".consent-banner-box"}],optIn:[{click:".button-zustimmen"}],optOut:[{click:"button[data-consent=disagree]"}]},{name:"thefreedictionary.com",prehideSelectors:["#cmpBanner"],detectCmp:[{exists:"#cmpBanner"}],detectPopup:[{visible:"#cmpBanner"}],optIn:[{eval:"EVAL_THEFREEDICTIONARY_1"}],optOut:[{eval:"EVAL_THEFREEDICTIONARY_0"}]},{name:"theverge",runContext:{frame:!1,main:!0,urlPattern:"^https://(www)?\\.theverge\\.com"},intermediate:!1,prehideSelectors:[".duet--cta--cookie-banner"],detectCmp:[{exists:".duet--cta--cookie-banner"}],detectPopup:[{visible:".duet--cta--cookie-banner"}],optIn:[{click:".duet--cta--cookie-banner button.tracking-12",all:!1}],optOut:[{click:".duet--cta--cookie-banner button.tracking-12 > span"}],test:[{eval:"EVAL_THEVERGE_0"}]},{name:"tidbits-com",cosmetic:!0,prehideSelectors:["#eu_cookie_law_widget-2"],detectCmp:[{exists:"#eu_cookie_law_widget-2"}],detectPopup:[{visible:"#eu_cookie_law_widget-2"}],optIn:[{click:"#eu-cookie-law form > input.accept"}],optOut:[{hide:"#eu_cookie_law_widget-2"}]},{name:"tractor-supply",runContext:{urlPattern:"^https://www\\.tractorsupply\\.com/"},cosmetic:!0,prehideSelectors:[".tsc-cookie-banner"],detectCmp:[{exists:".tsc-cookie-banner"}],detectPopup:[{visible:".tsc-cookie-banner"}],optIn:[{click:"#cookie-banner-cancel"}],optOut:[{hide:".tsc-cookie-banner"}]},{name:"trader-joes-com",cosmetic:!0,prehideSelectors:['div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'],detectCmp:[{exists:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}],detectPopup:[{visible:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}],optIn:[{click:'div[class^="CookiesAlert_cookiesAlert__container__"] button'}],optOut:[{hide:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}]},{name:"transcend",vendorUrl:"https://unknown",cosmetic:!0,prehideSelectors:["#transcend-consent-manager"],detectCmp:[{exists:"#transcend-consent-manager"}],detectPopup:[{visible:"#transcend-consent-manager"}],optIn:[{waitForThenClick:["#transcend-consent-manager","#consentManagerMainDialog .inner-container button"]}],optOut:[{hide:"#transcend-consent-manager"}]},{name:"transip-nl",runContext:{urlPattern:"^https://www\\.transip\\.nl/"},prehideSelectors:["#consent-modal"],detectCmp:[{any:[{exists:"#consent-modal"},{exists:"#privacy-settings-content"}]}],detectPopup:[{any:[{visible:"#consent-modal"},{visible:"#privacy-settings-content"}]}],optIn:[{click:'button[type="submit"]'}],optOut:[{if:{exists:"#privacy-settings-content"},then:[{click:'button[type="submit"]'}],else:[{click:"div.one-modal__action-footer-column--secondary > a"}]}]},{name:"tropicfeel-com",prehideSelectors:["#shopify-section-cookies-controller"],detectCmp:[{exists:"#shopify-section-cookies-controller"}],detectPopup:[{visible:"#shopify-section-cookies-controller #cookies-controller-main-pane",check:"any"}],optIn:[{waitForThenClick:"#cookies-controller-main-pane form[data-form-allow-all] button"}],optOut:[{click:"#cookies-controller-main-pane a[data-tab-target=manage-cookies]"},{waitFor:"#manage-cookies-pane.active"},{click:"#manage-cookies-pane.active input[type=checkbox][checked]:not([disabled])",all:!0},{click:"#manage-cookies-pane.active button[type=submit]"}],test:[]},{name:"true-car",runContext:{urlPattern:"^https://www\\.truecar\\.com/"},cosmetic:!0,prehideSelectors:[['div[aria-labelledby="cookie-banner-heading"]']],detectCmp:[{exists:'div[aria-labelledby="cookie-banner-heading"]'}],detectPopup:[{visible:'div[aria-labelledby="cookie-banner-heading"]'}],optIn:[{click:'div[aria-labelledby="cookie-banner-heading"] > button[aria-label="Close"]'}],optOut:[{hide:'div[aria-labelledby="cookie-banner-heading"]'}]},{name:"truyo",prehideSelectors:["#truyo-consent-module"],detectCmp:[{exists:"#truyo-cookieBarContent"}],detectPopup:[{visible:"#truyo-consent-module"}],optIn:[{click:"button#acceptAllCookieButton"}],optOut:[{click:"button#declineAllCookieButton"}]},{name:"tumblr-com",cosmetic:!0,prehideSelectors:["#cmp-app-container"],detectCmp:[{exists:"#cmp-app-container"}],detectPopup:[{visible:"#cmp-app-container"}],optIn:[{click:"#tumblr #cmp-app-container div.components-modal__frame > iframe > html body > div > div > div.cmp__dialog-footer > div > button.components-button.white-space-normal.is-primary"}],optOut:[{hide:"#cmp-app-container"}]},{name:"twitch-mobile",vendorUrl:"https://m.twitch.tv/",cosmetic:!0,runContext:{urlPattern:"^https?://m\\.twitch\\.tv"},prehideSelectors:[],detectCmp:[{exists:'.ReactModal__Overlay [href="https://www.twitch.tv/p/cookie-policy"]'}],detectPopup:[{visible:'.ReactModal__Overlay [href="https://www.twitch.tv/p/cookie-policy"]'}],optIn:[{waitForThenClick:'.ReactModal__Overlay:has([href="https://www.twitch.tv/p/cookie-policy"]) button'}],optOut:[{hide:'.ReactModal__Overlay:has([href="https://www.twitch.tv/p/cookie-policy"])'}]},{name:"twitch.tv",runContext:{urlPattern:"^https?://(www\\.)?twitch\\.tv"},prehideSelectors:["div:has(> .consent-banner .consent-banner__content--gdpr-v2),.ReactModalPortal:has([data-a-target=consent-modal-save])"],detectCmp:[{exists:".consent-banner .consent-banner__content--gdpr-v2"}],detectPopup:[{visible:".consent-banner .consent-banner__content--gdpr-v2"}],optIn:[{click:'button[data-a-target="consent-banner-accept"]'}],optOut:[{hide:"div:has(> .consent-banner .consent-banner__content--gdpr-v2)"},{click:'button[data-a-target="consent-banner-manage-preferences"]'},{waitFor:"input[type=checkbox][data-a-target=tw-checkbox]"},{click:"input[type=checkbox][data-a-target=tw-checkbox][checked]:not([disabled])",all:!0,optional:!0},{waitForThenClick:"[data-a-target=consent-modal-save]"},{waitForVisible:".ReactModalPortal:has([data-a-target=consent-modal-save])",check:"none"}]},{name:"twitter",runContext:{urlPattern:"^https://([a-z0-9-]+\\.)?twitter\\.com/"},prehideSelectors:['[data-testid="BottomBar"]'],detectCmp:[{exists:'[data-testid="BottomBar"] div'}],detectPopup:[{visible:'[data-testid="BottomBar"] div'}],optIn:[{waitForThenClick:'[data-testid="BottomBar"] > div:has(>div:first-child>div:last-child>span[role=button]) > div:last-child > div[role=button]:first-child'}],optOut:[{waitForThenClick:'[data-testid="BottomBar"] > div:has(>div:first-child>div:last-child>span[role=button]) > div:last-child > div[role=button]:last-child'}],TODOtest:[{eval:"EVAL_document.cookie.includes('d_prefs=MjoxLGNvbnNlbnRfdmVyc2lvbjoy')"}]},{name:"ubuntu.com",prehideSelectors:["dialog.cookie-policy"],detectCmp:[{any:[{exists:"dialog.cookie-policy header"},{exists:'xpath///*[@id="modal"]/div/header'}]}],detectPopup:[{any:[{visible:"dialog header"},{visible:'xpath///*[@id="modal"]/div/header'}]}],optIn:[{any:[{waitForThenClick:"#cookie-policy-button-accept"},{waitForThenClick:'xpath///*[@id="cookie-policy-button-accept"]'}]}],optOut:[{any:[{waitForThenClick:"button.js-manage"},{waitForThenClick:'xpath///*[@id="cookie-policy-content"]/p[4]/button[2]'}]},{waitForThenClick:"dialog.cookie-policy .p-switch__input:checked",optional:!0,all:!0,timeout:500},{any:[{waitForThenClick:"dialog.cookie-policy .js-save-preferences"},{waitForThenClick:'xpath///*[@id="modal"]/div/button'}]}],test:[{eval:"EVAL_UBUNTU_COM_0"}]},{name:"UK Cookie Consent",prehideSelectors:["#catapult-cookie-bar"],cosmetic:!0,detectCmp:[{exists:"#catapult-cookie-bar"}],detectPopup:[{exists:".has-cookie-bar #catapult-cookie-bar"}],optIn:[{click:"#catapultCookie"}],optOut:[{hide:"#catapult-cookie-bar"}],test:[{eval:"EVAL_UK_COOKIE_CONSENT_0"}]},{name:"urbanarmorgear-com",cosmetic:!0,prehideSelectors:['div[class^="Layout__CookieBannerContainer-"]'],detectCmp:[{exists:'div[class^="Layout__CookieBannerContainer-"]'}],detectPopup:[{visible:'div[class^="Layout__CookieBannerContainer-"]'}],optIn:[{click:'button[class^="CookieBanner__AcceptButton"]'}],optOut:[{hide:'div[class^="Layout__CookieBannerContainer-"]'}]},{name:"usercentrics-api",detectCmp:[{exists:"#usercentrics-root"}],detectPopup:[{eval:"EVAL_USERCENTRICS_API_0"},{exists:["#usercentrics-root","[data-testid=uc-container]"]},{waitForVisible:"#usercentrics-root",timeout:2e3}],optIn:[{eval:"EVAL_USERCENTRICS_API_3"},{eval:"EVAL_USERCENTRICS_API_1"},{eval:"EVAL_USERCENTRICS_API_5"}],optOut:[{eval:"EVAL_USERCENTRICS_API_1"},{eval:"EVAL_USERCENTRICS_API_2"}],test:[{eval:"EVAL_USERCENTRICS_API_6"}]},{name:"usercentrics-button",detectCmp:[{exists:"#usercentrics-button"}],detectPopup:[{visible:"#usercentrics-button #uc-btn-accept-banner"}],optIn:[{click:"#usercentrics-button #uc-btn-accept-banner"}],optOut:[{click:"#usercentrics-button #uc-btn-deny-banner"}],test:[{eval:"EVAL_USERCENTRICS_BUTTON_0"}]},{name:"uswitch.com",prehideSelectors:["#cookie-banner-wrapper"],detectCmp:[{exists:"#cookie-banner-wrapper"}],detectPopup:[{visible:"#cookie-banner-wrapper"}],optIn:[{click:"#cookie_banner_accept_mobile"}],optOut:[{click:"#cookie_banner_save"}]},{name:"vodafone.de",runContext:{urlPattern:"^https://www\\.vodafone\\.de/"},prehideSelectors:[".dip-consent,.dip-consent-container"],detectCmp:[{exists:".dip-consent-container"}],detectPopup:[{visible:".dip-consent-content"}],optOut:[{click:'.dip-consent-btn[tabindex="2"]'}],optIn:[{click:'.dip-consent-btn[tabindex="1"]'}]},{name:"waitrose.com",prehideSelectors:["div[aria-labelledby=CookieAlertModalHeading]","section[data-test=initial-waitrose-cookie-consent-banner]","section[data-test=cookie-consent-modal]"],detectCmp:[{exists:"section[data-test=initial-waitrose-cookie-consent-banner]"}],detectPopup:[{visible:"section[data-test=initial-waitrose-cookie-consent-banner]"}],optIn:[{click:"button[data-test=accept-all]"}],optOut:[{click:"button[data-test=manage-cookies]"},{wait:200},{eval:"EVAL_WAITROSE_0"},{click:"button[data-test=submit]"}],test:[{eval:"EVAL_WAITROSE_1"}]},{name:"webflow",vendorUrl:"https://webflow.com/",prehideSelectors:[".fs-cc-components"],detectCmp:[{exists:".fs-cc-components"}],detectPopup:[{visible:".fs-cc-components"},{visible:"[fs-cc=banner]"}],optIn:[{wait:500},{waitForThenClick:"[fs-cc=banner] [fs-cc=allow]"}],optOut:[{wait:500},{waitForThenClick:"[fs-cc=banner] [fs-cc=deny]"}]},{name:"wetransfer.com",detectCmp:[{exists:".welcome__cookie-notice"}],detectPopup:[{visible:".welcome__cookie-notice"}],optIn:[{click:".welcome__button--accept"}],optOut:[{click:".welcome__button--decline"}]},{name:"whitepages.com",runContext:{urlPattern:"^https://www\\.whitepages\\.com/"},cosmetic:!0,prehideSelectors:[".cookie-wrapper, .cookie-overlay"],detectCmp:[{exists:".cookie-wrapper"}],detectPopup:[{visible:".cookie-overlay"}],optIn:[{click:'button[aria-label="Got it"]'}],optOut:[{hide:".cookie-wrapper"}]},{name:"wolframalpha",vendorUrl:"https://www.wolframalpha.com",prehideSelectors:[],cosmetic:!0,runContext:{urlPattern:"^https://www\\.wolframalpha\\.com/"},detectCmp:[{exists:"section._a_yb"}],detectPopup:[{visible:"section._a_yb"}],optIn:[{waitForThenClick:"section._a_yb button"}],optOut:[{hide:"section._a_yb"}]},{name:"woo-commerce-com",prehideSelectors:[".wccom-comp-privacy-banner .wccom-privacy-banner"],detectCmp:[{exists:".wccom-comp-privacy-banner .wccom-privacy-banner"}],detectPopup:[{exists:".wccom-comp-privacy-banner .wccom-privacy-banner"}],optIn:[{click:".wccom-privacy-banner__content-buttons button.is-primary"}],optOut:[{click:".wccom-privacy-banner__content-buttons button.is-secondary"},{waitForThenClick:"input[type=checkbox][checked]:not([disabled])",all:!0},{click:"div.wccom-modal__footer > button"}]},{name:"WP Cookie Notice for GDPR",vendorUrl:"https://wordpress.org/plugins/gdpr-cookie-consent/",prehideSelectors:["#gdpr-cookie-consent-bar"],detectCmp:[{exists:"#gdpr-cookie-consent-bar"}],detectPopup:[{visible:"#gdpr-cookie-consent-bar"}],optIn:[{waitForThenClick:"#gdpr-cookie-consent-bar #cookie_action_accept"}],optOut:[{waitForThenClick:"#gdpr-cookie-consent-bar #cookie_action_reject"}],test:[{eval:"EVAL_WP_COOKIE_NOTICE_0"}]},{name:"wpcc",cosmetic:!0,prehideSelectors:[".wpcc-container"],detectCmp:[{exists:".wpcc-container"}],detectPopup:[{exists:".wpcc-container .wpcc-message"}],optIn:[{click:".wpcc-compliance .wpcc-btn"}],optOut:[{hide:".wpcc-container"}]},{name:"xe.com",vendorUrl:"https://www.xe.com/",runContext:{urlPattern:"^https://www\\.xe\\.com/"},prehideSelectors:["[class*=ConsentBanner]"],detectCmp:[{exists:"[class*=ConsentBanner]"}],detectPopup:[{visible:"[class*=ConsentBanner]"}],optIn:[{waitForThenClick:"[class*=ConsentBanner] .egnScw"}],optOut:[{wait:1e3},{waitForThenClick:"[class*=ConsentBanner] .frDWEu"},{waitForThenClick:"[class*=ConsentBanner] .hXIpFU"}],test:[{eval:"EVAL_XE_TEST"}]},{name:"xhamster-eu",prehideSelectors:[".cookies-modal"],detectCmp:[{exists:".cookies-modal"}],detectPopup:[{exists:".cookies-modal"}],optIn:[{click:"button.cmd-button-accept-all"}],optOut:[{click:"button.cmd-button-reject-all"}]},{name:"xhamster-us",runContext:{urlPattern:"^https://(www\\.)?xhamster\\d?\\.com"},cosmetic:!0,prehideSelectors:[".cookie-announce"],detectCmp:[{exists:".cookie-announce"}],detectPopup:[{visible:".cookie-announce .announce-text"}],optIn:[{click:".cookie-announce button.xh-button"}],optOut:[{hide:".cookie-announce"}]},{name:"xing.com",detectCmp:[{exists:"div[class^=cookie-consent-CookieConsent]"}],detectPopup:[{exists:"div[class^=cookie-consent-CookieConsent]"}],optIn:[{click:"#consent-accept-button"}],optOut:[{click:"#consent-settings-button"},{click:".consent-banner-button-accept-overlay"}],test:[{eval:"EVAL_XING_0"}]},{name:"xnxx-com",cosmetic:!0,prehideSelectors:["#cookies-use-alert"],detectCmp:[{exists:"#cookies-use-alert"}],detectPopup:[{visible:"#cookies-use-alert"}],optIn:[{click:"#cookies-use-alert .close"}],optOut:[{hide:"#cookies-use-alert"}]},{name:"xvideos",vendorUrl:"https://xvideos.com",runContext:{urlPattern:"^https://[^/]*xvideos\\.com/"},prehideSelectors:[],detectCmp:[{exists:".disclaimer-opened #disclaimer-cookies"}],detectPopup:[{visible:".disclaimer-opened #disclaimer-cookies"}],optIn:[{waitForThenClick:"#disclaimer-accept_cookies"}],optOut:[{waitForThenClick:"#disclaimer-reject_cookies"}]},{name:"Yahoo",runContext:{urlPattern:"^https://consent\\.yahoo\\.com/v2/"},prehideSelectors:["#reject-all"],detectCmp:[{exists:"#consent-page"}],detectPopup:[{visible:"#consent-page"}],optIn:[{waitForThenClick:"#consent-page button[value=agree]"}],optOut:[{waitForThenClick:"#consent-page button[value=reject]"}]},{name:"youporn.com",cosmetic:!0,prehideSelectors:[".euCookieModal, #js_euCookieModal"],detectCmp:[{exists:".euCookieModal"}],detectPopup:[{exists:".euCookieModal, #js_euCookieModal"}],optIn:[{click:'button[name="user_acceptCookie"]'}],optOut:[{hide:".euCookieModal"}]},{name:"youtube-desktop",prehideSelectors:["tp-yt-iron-overlay-backdrop.opened","ytd-consent-bump-v2-lightbox"],detectCmp:[{exists:"ytd-consent-bump-v2-lightbox tp-yt-paper-dialog"},{exists:'ytd-consent-bump-v2-lightbox tp-yt-paper-dialog a[href^="https://consent.youtube.com/"]'}],detectPopup:[{visible:"ytd-consent-bump-v2-lightbox tp-yt-paper-dialog"}],optIn:[{waitForThenClick:"ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:last-child #button,ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:last-child button"},{wait:500}],optOut:[{waitForThenClick:"ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:first-child #button,ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:first-child button"},{wait:500}],test:[{wait:500},{eval:"EVAL_YOUTUBE_DESKTOP_0"}]},{name:"youtube-mobile",prehideSelectors:[".consent-bump-v2-lightbox"],detectCmp:[{exists:"ytm-consent-bump-v2-renderer"}],detectPopup:[{visible:"ytm-consent-bump-v2-renderer"}],optIn:[{waitForThenClick:"ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons c3-material-button:first-child button, ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons ytm-button-renderer:first-child button"},{wait:500}],optOut:[{waitForThenClick:"ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons c3-material-button:nth-child(2) button, ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons ytm-button-renderer:nth-child(2) button"},{wait:500}],test:[{wait:500},{eval:"EVAL_YOUTUBE_MOBILE_0"}]},{name:"zdf",prehideSelectors:["#zdf-cmp-banner-sdk"],detectCmp:[{exists:"#zdf-cmp-banner-sdk"}],detectPopup:[{visible:"#zdf-cmp-main.zdf-cmp-show"}],optIn:[{waitForThenClick:"#zdf-cmp-main #zdf-cmp-accept-btn"}],optOut:[{waitForThenClick:"#zdf-cmp-main #zdf-cmp-deny-btn"}],test:[]}],C={"didomi.io":{detectors:[{presentMatcher:{target:{selector:"#didomi-host, #didomi-notice"},type:"css"},showingMatcher:{target:{selector:"body.didomi-popup-open, .didomi-notice-banner"},type:"css"}}],methods:[{action:{target:{selector:".didomi-popup-notice-buttons .didomi-button:not(.didomi-button-highlight), .didomi-notice-banner .didomi-learn-more-button"},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{retries:50,target:{selector:"#didomi-purpose-cookies"},type:"waitcss",waitTime:50},{consents:[{description:"Share (everything) with others",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-share_whith_others]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-share_whith_others]:last-child"},type:"click"},type:"X"},{description:"Information storage and access",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-cookies]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-cookies]:last-child"},type:"click"},type:"D"},{description:"Content selection, offers and marketing",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-CL-T1Rgm7]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-CL-T1Rgm7]:last-child"},type:"click"},type:"E"},{description:"Analytics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-analytics]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-analytics]:last-child"},type:"click"},type:"B"},{description:"Analytics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-M9NRHJe3G]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-M9NRHJe3G]:last-child"},type:"click"},type:"B"},{description:"Ad and content selection",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-advertising_personalization]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-advertising_personalization]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection",falseAction:{parent:{childFilter:{target:{selector:"#didomi-purpose-pub-ciblee"}},selector:".didomi-consent-popup-data-processing, .didomi-components-accordion-label-container"},target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-pub-ciblee]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-pub-ciblee]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - basics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-q4zlJqdcD]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-q4zlJqdcD]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - partners and subsidiaries",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-partenaire-cAsDe8jC]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-partenaire-cAsDe8jC]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-p4em9a8m]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-p4em9a8m]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - others",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-autres-pub]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-autres-pub]:last-child"},type:"click"},type:"F"},{description:"Social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-reseauxsociaux]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-reseauxsociaux]:last-child"},type:"click"},type:"A"},{description:"Social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-social_media]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-social_media]:last-child"},type:"click"},type:"A"},{description:"Content selection",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-content_personalization]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-content_personalization]:last-child"},type:"click"},type:"E"},{description:"Ad delivery",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-ad_delivery]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-ad_delivery]:last-child"},type:"click"},type:"F"}],type:"consent"},{action:{consents:[{matcher:{childFilter:{target:{selector:":not(.didomi-components-radio__option--selected)"}},type:"css"},trueAction:{target:{selector:":nth-child(2)"},type:"click"},falseAction:{target:{selector:":first-child"},type:"click"},type:"X"}],type:"consent"},target:{selector:".didomi-components-radio"},type:"foreach"}],type:"list"},name:"DO_CONSENT"},{action:{parent:{selector:".didomi-consent-popup-footer .didomi-consent-popup-actions"},target:{selector:".didomi-components-button:first-child"},type:"click"},name:"SAVE_CONSENT"}]},oil:{detectors:[{presentMatcher:{target:{selector:".as-oil-content-overlay"},type:"css"},showingMatcher:{target:{selector:".as-oil-content-overlay"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".as-js-advanced-settings"},type:"click"},{retries:"10",target:{selector:".as-oil-cpc__purpose-container"},type:"waitcss",waitTime:"250"}],type:"list"},name:"OPEN_OPTIONS"},{action:{actions:[{consents:[{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Information storage and access","Opbevaring af og adgang til oplysninger på din enhed"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Information storage and access","Opbevaring af og adgang til oplysninger på din enhed"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"D"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personlige annoncer","Personalisation"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personlige annoncer","Personalisation"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"E"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Annoncevalg, levering og rapportering","Ad selection, delivery, reporting"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Annoncevalg, levering og rapportering","Ad selection, delivery, reporting"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"F"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personalisering af indhold","Content selection, delivery, reporting"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personalisering af indhold","Content selection, delivery, reporting"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"E"},{matcher:{parent:{childFilter:{target:{selector:".as-oil-cpc__purpose-header",textFilter:["Måling","Measurement"]}},selector:".as-oil-cpc__purpose-container"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{childFilter:{target:{selector:".as-oil-cpc__purpose-header",textFilter:["Måling","Measurement"]}},selector:".as-oil-cpc__purpose-container"},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"B"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:"Google"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:"Google"},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"F"}],type:"consent"}],type:"list"},name:"DO_CONSENT"},{action:{target:{selector:".as-oil__btn-optin"},type:"click"},name:"SAVE_CONSENT"},{action:{target:{selector:"div.as-oil"},type:"hide"},name:"HIDE_CMP"}]},optanon:{detectors:[{presentMatcher:{target:{selector:"#optanon-menu, .optanon-alert-box-wrapper"},type:"css"},showingMatcher:{target:{displayFilter:!0,selector:".optanon-alert-box-wrapper"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".optanon-alert-box-wrapper .optanon-toggle-display, a[onclick*='OneTrust.ToggleInfoDisplay()'], a[onclick*='Optanon.ToggleInfoDisplay()']"},type:"click"}],type:"list"},name:"OPEN_OPTIONS"},{action:{actions:[{target:{selector:".preference-menu-item #Your-privacy"},type:"click"},{target:{selector:"#optanon-vendor-consent-text"},type:"click"},{action:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},target:{selector:"#optanon-vendor-consent-list .vendor-item"},type:"foreach"},{target:{selector:".vendor-consent-back-link"},type:"click"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-performance"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-performance"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-functional"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-functional"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-advertising"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-advertising"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-social"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-social"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Social Media Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Social Media Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalisation"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalisation"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Site monitoring cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Site monitoring cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Third party privacy-enhanced content"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Third party privacy-enhanced content"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Performance & Advertising Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Performance & Advertising Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Information storage and access"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Information storage and access"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"D"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad selection, delivery, reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad selection, delivery, reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content selection, delivery, reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content selection, delivery, reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Measurement"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Measurement"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Recommended Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Recommended Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Unclassified Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Unclassified Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Analytical Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Analytical Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Marketing Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Marketing Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalization"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalization"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad Selection, Delivery & Reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad Selection, Delivery & Reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content Selection, Delivery & Reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content Selection, Delivery & Reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"}],type:"list"},name:"DO_CONSENT"},{action:{parent:{selector:".optanon-save-settings-button"},target:{selector:".optanon-white-button-middle"},type:"click"},name:"SAVE_CONSENT"},{action:{actions:[{target:{selector:"#optanon-popup-wrapper"},type:"hide"},{target:{selector:"#optanon-popup-bg"},type:"hide"},{target:{selector:".optanon-alert-box-wrapper"},type:"hide"}],type:"list"},name:"HIDE_CMP"}]},quantcast2:{detectors:[{presentMatcher:{target:{selector:"[data-tracking-opt-in-overlay]"},type:"css"},showingMatcher:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-learn-more]"},type:"css"}}],methods:[{action:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-learn-more]"},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{type:"wait",waitTime:500},{action:{actions:[{target:{selector:"div",textFilter:["Information storage and access"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"D"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Personalization"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"F"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Ad selection, delivery, reporting"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"F"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Content selection, delivery, reporting"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"E"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Measurement"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"B"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Other Partners"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},type:"ifcss"}],type:"list"},parent:{childFilter:{target:{selector:"input"}},selector:"[data-tracking-opt-in-overlay] > div > div"},target:{childFilter:{target:{selector:"input"}},selector:":scope > div"},type:"foreach"}],type:"list"},name:"DO_CONSENT"},{action:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-save]"},type:"click"},name:"SAVE_CONSENT"}]},springer:{detectors:[{presentMatcher:{parent:null,target:{selector:".cmp-app_gdpr"},type:"css"},showingMatcher:{parent:null,target:{displayFilter:!0,selector:".cmp-popup_popup"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".cmp-intro_rejectAll"},type:"click"},{type:"wait",waitTime:250},{target:{selector:".cmp-purposes_purposeItem:not(.cmp-purposes_selectedPurpose)"},type:"click"}],type:"list"},name:"OPEN_OPTIONS"},{action:{consents:[{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Przechowywanie informacji na urządzeniu lub dostęp do nich",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Przechowywanie informacji na urządzeniu lub dostęp do nich",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"D"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór podstawowych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór podstawowych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"F"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"F"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"E"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"E"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Stosowanie badań rynkowych w celu generowania opinii odbiorców",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Stosowanie badań rynkowych w celu generowania opinii odbiorców",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"X"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Opracowywanie i ulepszanie produktów",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Opracowywanie i ulepszanie produktów",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"X"}],type:"consent"},name:"DO_CONSENT"},{action:{target:{selector:".cmp-details_save"},type:"click"},name:"SAVE_CONSENT"}]},wordpressgdpr:{detectors:[{presentMatcher:{parent:null,target:{selector:".wpgdprc-consent-bar"},type:"css"},showingMatcher:{parent:null,target:{displayFilter:!0,selector:".wpgdprc-consent-bar"},type:"css"}}],methods:[{action:{parent:null,target:{selector:".wpgdprc-consent-bar .wpgdprc-consent-bar__settings",textFilter:null},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{target:{selector:".wpgdprc-consent-modal .wpgdprc-button",textFilter:"Eyeota"},type:"click"},{consents:[{description:"Eyeota Cookies",matcher:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Eyeota"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Eyeota"},target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},{target:{selector:".wpgdprc-consent-modal .wpgdprc-button",textFilter:"Advertising"},type:"click"},{consents:[{description:"Advertising Cookies",matcher:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Advertising"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Advertising"},target:{selector:"label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},name:"DO_CONSENT"},{action:{parent:null,target:{selector:".wpgdprc-button",textFilter:"Save my settings"},type:"click"},name:"SAVE_CONSENT"}]}},v={autoconsent:w,consentomatic:C},f=Object.freeze({__proto__:null,autoconsent:w,consentomatic:C,default:v});const A=new class{constructor(e,t=null,o=null){if(this.id=n(),this.rules=[],this.foundCmp=null,this.state={lifecycle:"loading",prehideOn:!1,findCmpAttempts:0,detectedCmps:[],detectedPopups:[],selfTest:null},a.sendContentMessage=e,this.sendContentMessage=e,this.rules=[],this.updateState({lifecycle:"loading"}),this.addDynamicRules(),t)this.initialize(t,o);else{o&&this.parseDeclarativeRules(o);e({type:"init",url:window.location.href}),this.updateState({lifecycle:"waitingForInitResponse"})}this.domActions=new class{constructor(e){this.autoconsentInstance=e}click(e,t=!1){const o=this.elementSelector(e);return this.autoconsentInstance.config.logs.rulesteps&&console.log("[click]",e,t,o),o.length>0&&(t?o.forEach((e=>e.click())):o[0].click()),o.length>0}elementExists(e){return this.elementSelector(e).length>0}elementVisible(e,t){const o=this.elementSelector(e),c=new Array(o.length);return o.forEach(((e,t)=>{c[t]=k(e)})),"none"===t?c.every((e=>!e)):0!==c.length&&("any"===t?c.some((e=>e)):c.every((e=>e)))}waitForElement(e,t=1e4){const o=Math.ceil(t/200);return this.autoconsentInstance.config.logs.rulesteps&&console.log("[waitForElement]",e),h((()=>this.elementSelector(e).length>0),o,200)}waitForVisible(e,t=1e4,o="any"){return h((()=>this.elementVisible(e,o)),Math.ceil(t/200),200)}async waitForThenClick(e,t=1e4,o=!1){return await this.waitForElement(e,t),this.click(e,o)}wait(e){return new Promise((t=>{setTimeout((()=>{t(!0)}),e)}))}hide(e,t){return m(u(),e,t)}prehide(e){const t=u("autoconsent-prehide");return this.autoconsentInstance.config.logs.lifecycle&&console.log("[prehide]",t,location.href),m(t,e,"opacity")}undoPrehide(){const e=u("autoconsent-prehide");return this.autoconsentInstance.config.logs.lifecycle&&console.log("[undoprehide]",e,location.href),e&&e.remove(),!!e}querySingleReplySelector(e,t=document){if(e.startsWith("aria/"))return[];if(e.startsWith("xpath/")){const o=e.slice(6),c=document.evaluate(o,t,null,XPathResult.ANY_TYPE,null);let i=null;const n=[];for(;i=c.iterateNext();)n.push(i);return n}return e.startsWith("text/")||e.startsWith("pierce/")?[]:t.shadowRoot?Array.from(t.shadowRoot.querySelectorAll(e)):Array.from(t.querySelectorAll(e))}querySelectorChain(e){let t,o=document;for(const c of e){if(t=this.querySingleReplySelector(c,o),0===t.length)return[];o=t[0]}return t}elementSelector(e){return"string"==typeof e?this.querySingleReplySelector(e):this.querySelectorChain(e)}}(this)}initialize(e,t){const o=b(e);if(o.logs.lifecycle&&console.log("autoconsent init",window.location.href),this.config=o,o.enabled){if(t&&this.parseDeclarativeRules(t),this.rules=function(e,t){return e.filter((e=>(!t.disabledCmps||!t.disabledCmps.includes(e.name))&&(t.enableCosmeticRules||!e.isCosmetic)))}(this.rules,o),e.enablePrehide)if(document.documentElement)this.prehideElements();else{const e=()=>{window.removeEventListener("DOMContentLoaded",e),this.prehideElements()};window.addEventListener("DOMContentLoaded",e)}if("loading"===document.readyState){const e=()=>{window.removeEventListener("DOMContentLoaded",e),this.start()};window.addEventListener("DOMContentLoaded",e)}else this.start();this.updateState({lifecycle:"initialized"})}else o.logs.lifecycle&&console.log("autoconsent is disabled")}addDynamicRules(){y.forEach((e=>{this.rules.push(new e(this))}))}parseDeclarativeRules(e){Object.keys(e.consentomatic).forEach((t=>{this.addConsentomaticCMP(t,e.consentomatic[t])})),e.autoconsent.forEach((e=>{this.addDeclarativeCMP(e)}))}addDeclarativeCMP(e){this.rules.push(new d(e,this))}addConsentomaticCMP(e,t){this.rules.push(new class{constructor(e,t){this.name=e,this.config=t,this.methods=new Map,this.runContext=l,this.isCosmetic=!1,t.methods.forEach((e=>{e.action&&this.methods.set(e.name,e.action)})),this.hasSelfTest=!1}get isIntermediate(){return!1}checkRunContext(){return!0}async detectCmp(){return this.config.detectors.map((e=>o(e.presentMatcher))).some((e=>!!e))}async detectPopup(){return this.config.detectors.map((e=>o(e.showingMatcher))).some((e=>!!e))}async executeAction(e,t){return!this.methods.has(e)||c(this.methods.get(e),t)}async optOut(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),await this.executeAction("HIDE_CMP"),await this.executeAction("DO_CONSENT",[]),await this.executeAction("SAVE_CONSENT"),!0}async optIn(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),await this.executeAction("HIDE_CMP"),await this.executeAction("DO_CONSENT",["D","A","B","E","F","X"]),await this.executeAction("SAVE_CONSENT"),!0}async openCmp(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),!0}async test(){return!0}}(`com_${e}`,t))}start(){window.requestIdleCallback?window.requestIdleCallback((()=>this._start()),{timeout:500}):this._start()}async _start(){const e=this.config.logs;e.lifecycle&&console.log(`Detecting CMPs on ${window.location.href}`),this.updateState({lifecycle:"started"});const t=await this.findCmp(this.config.detectRetries);if(this.updateState({detectedCmps:t.map((e=>e.name))}),0===t.length)return e.lifecycle&&console.log("no CMP found",location.href),this.config.enablePrehide&&this.undoPrehide(),this.updateState({lifecycle:"nothingDetected"}),!1;this.updateState({lifecycle:"cmpDetected"});const o=[],c=[];for(const e of t)e.isCosmetic?c.push(e):o.push(e);let i=!1,n=await this.detectPopups(o,(async e=>{i=await this.handlePopup(e)}));if(0===n.length&&(n=await this.detectPopups(c,(async e=>{i=await this.handlePopup(e)}))),0===n.length)return e.lifecycle&&console.log("no popup found"),this.config.enablePrehide&&this.undoPrehide(),!1;if(n.length>1){const t={msg:"Found multiple CMPs, check the detection rules.",cmps:n.map((e=>e.name))};e.errors&&console.warn(t.msg,t.cmps),this.sendContentMessage({type:"autoconsentError",details:t})}return i}async findCmp(e){const t=this.config.logs;this.updateState({findCmpAttempts:this.state.findCmpAttempts+1});const o=[];for(const e of this.rules)try{if(!e.checkRunContext())continue;await e.detectCmp()&&(t.lifecycle&&console.log(`Found CMP: ${e.name} ${window.location.href}`),this.sendContentMessage({type:"cmpDetected",url:location.href,cmp:e.name}),o.push(e))}catch(o){t.errors&&console.warn(`error detecting ${e.name}`,o)}return 0===o.length&&e>0?(await this.domActions.wait(500),this.findCmp(e-1)):o}async detectPopup(e){if(await this.waitForPopup(e).catch((t=>(this.config.logs.errors&&console.warn(`error waiting for a popup for ${e.name}`,t),!1))))return this.updateState({detectedPopups:this.state.detectedPopups.concat([e.name])}),this.sendContentMessage({type:"popupFound",cmp:e.name,url:location.href}),e;throw new Error("Popup is not shown")}async detectPopups(e,t){const o=e.map((e=>this.detectPopup(e)));await Promise.any(o).then((e=>{t(e)})).catch((()=>null));const c=await Promise.allSettled(o),i=[];for(const e of c)"fulfilled"===e.status&&i.push(e.value);return i}async handlePopup(e){return this.updateState({lifecycle:"openPopupDetected"}),this.config.enablePrehide&&!this.state.prehideOn&&this.prehideElements(),this.foundCmp=e,"optOut"===this.config.autoAction?await this.doOptOut():"optIn"===this.config.autoAction?await this.doOptIn():(this.config.logs.lifecycle&&console.log("waiting for opt-out signal...",location.href),!0)}async doOptOut(){const e=this.config.logs;let t;return this.updateState({lifecycle:"runningOptOut"}),this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: opt out on ${window.location.href}`),t=await this.foundCmp.optOut(),e.lifecycle&&console.log(`${this.foundCmp.name}: opt out result ${t}`)):(e.errors&&console.log("no CMP to opt out"),t=!1),this.config.enablePrehide&&this.undoPrehide(),this.sendContentMessage({type:"optOutResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,scheduleSelfTest:this.foundCmp&&this.foundCmp.hasSelfTest,url:location.href}),t&&!this.foundCmp.isIntermediate?(this.sendContentMessage({type:"autoconsentDone",cmp:this.foundCmp.name,isCosmetic:this.foundCmp.isCosmetic,url:location.href}),this.updateState({lifecycle:"done"})):this.updateState({lifecycle:t?"optOutSucceeded":"optOutFailed"}),t}async doOptIn(){const e=this.config.logs;let t;return this.updateState({lifecycle:"runningOptIn"}),this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: opt in on ${window.location.href}`),t=await this.foundCmp.optIn(),e.lifecycle&&console.log(`${this.foundCmp.name}: opt in result ${t}`)):(e.errors&&console.log("no CMP to opt in"),t=!1),this.config.enablePrehide&&this.undoPrehide(),this.sendContentMessage({type:"optInResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,scheduleSelfTest:!1,url:location.href}),t&&!this.foundCmp.isIntermediate?(this.sendContentMessage({type:"autoconsentDone",cmp:this.foundCmp.name,isCosmetic:this.foundCmp.isCosmetic,url:location.href}),this.updateState({lifecycle:"done"})):this.updateState({lifecycle:t?"optInSucceeded":"optInFailed"}),t}async doSelfTest(){const e=this.config.logs;let t;return this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: self-test on ${window.location.href}`),t=await this.foundCmp.test()):(e.errors&&console.log("no CMP to self test"),t=!1),this.sendContentMessage({type:"selfTestResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,url:location.href}),this.updateState({selfTest:t}),t}async waitForPopup(e,t=5,o=500){const c=this.config.logs;c.lifecycle&&console.log("checking if popup is open...",e.name);const i=await e.detectPopup().catch((t=>(c.errors&&console.warn(`error detecting popup for ${e.name}`,t),!1)));return!i&&t>0?(await this.domActions.wait(o),this.waitForPopup(e,t-1,o)):(c.lifecycle&&console.log(e.name,"popup is "+(i?"open":"not open")),i)}prehideElements(){const e=this.config.logs,t=this.rules.reduce(((e,t)=>t.prehideSelectors?[...e,...t.prehideSelectors]:e),["#didomi-popup,.didomi-popup-container,.didomi-popup-notice,.didomi-consent-popup-preferences,#didomi-notice,.didomi-popup-backdrop,.didomi-screen-medium"]);return this.updateState({prehideOn:!0}),setTimeout((()=>{this.config.enablePrehide&&this.state.prehideOn&&!["runningOptOut","runningOptIn"].includes(this.state.lifecycle)&&(e.lifecycle&&console.log("Process is taking too long, unhiding elements"),this.undoPrehide())}),this.config.prehideTimeout||2e3),this.domActions.prehide(t.join(","))}undoPrehide(){return this.updateState({prehideOn:!1}),this.domActions.undoPrehide()}updateState(e){Object.assign(this.state,e),this.sendContentMessage({type:"report",instanceId:this.id,url:window.location.href,mainFrame:window.top===window.self,state:this.state})}async receiveMessageCallback(e){const t=this.config?.logs;switch(t?.messages&&console.log("received from background",e,window.location.href),e.type){case"initResp":this.initialize(e.config,e.rules);break;case"optIn":await this.doOptIn();break;case"optOut":await this.doOptOut();break;case"selfTest":await this.doSelfTest();break;case"evalResp":!function(e,t){const o=a.pending.get(e);o?(a.pending.delete(e),o.timer&&window.clearTimeout(o.timer),o.resolve(t)):console.warn("no eval #",e)}(e.id,e.result)}}}((e=>{window.webkit.messageHandlers[e.type]&&window.webkit.messageHandlers[e.type].postMessage(e).then((e=>{A.receiveMessageCallback(e)}))}),null,f);window.autoconsentMessageCallback=e=>{A.receiveMessageCallback(e)}}(); +!function(){"use strict";var e=class e{static setBase(t){e.base=t}static findElement(t,o=null,c=!1){let i=null;return i=null!=o?Array.from(o.querySelectorAll(t.selector)):null!=e.base?Array.from(e.base.querySelectorAll(t.selector)):Array.from(document.querySelectorAll(t.selector)),null!=t.textFilter&&(i=i.filter((e=>{const o=e.textContent.toLowerCase();if(Array.isArray(t.textFilter)){let e=!1;for(const c of t.textFilter)if(-1!==o.indexOf(c.toLowerCase())){e=!0;break}return e}if(null!=t.textFilter)return-1!==o.indexOf(t.textFilter.toLowerCase())}))),null!=t.styleFilters&&(i=i.filter((e=>{const o=window.getComputedStyle(e);let c=!0;for(const e of t.styleFilters){const t=o[e.option];c=e.negated?c&&t!==e.value:c&&t===e.value}return c}))),null!=t.displayFilter&&(i=i.filter((e=>t.displayFilter?0!==e.offsetHeight:0===e.offsetHeight))),null!=t.iframeFilter&&(i=i.filter((()=>t.iframeFilter?window.location!==window.parent.location:window.location===window.parent.location))),null!=t.childFilter&&(i=i.filter((o=>{const c=e.base;e.setBase(o);const i=e.find(t.childFilter);return e.setBase(c),null!=i.target}))),c?i:(i.length>1&&console.warn("Multiple possible targets: ",i,t,o),i[0])}static find(t,o=!1){const c=[];if(null!=t.parent){const i=e.findElement(t.parent,null,o);if(null!=i){if(i instanceof Array)return i.forEach((i=>{const n=e.findElement(t.target,i,o);n instanceof Array?n.forEach((e=>{c.push({parent:i,target:e})})):c.push({parent:i,target:n})})),c;{const n=e.findElement(t.target,i,o);n instanceof Array?n.forEach((e=>{c.push({parent:i,target:e})})):c.push({parent:i,target:n})}}}else{const i=e.findElement(t.target,null,o);i instanceof Array?i.forEach((e=>{c.push({parent:null,target:e})})):c.push({parent:null,target:i})}return 0===c.length&&c.push({parent:null,target:null}),o?c:(1!==c.length&&console.warn("Multiple results found, even though multiple false",c),c[0])}};e.base=null;var t=e;function o(e){const o=t.find(e);return"css"===e.type?!!o.target:"checkbox"===e.type?!!o.target&&o.target.checked:void 0}async function c(e,n){switch(e.type){case"click":return async function(e){const o=t.find(e);null!=o.target&&o.target.click();return i(0)}(e);case"list":return async function(e,t){for(const o of e.actions)await c(o,t)}(e,n);case"consent":return async function(e,t){for(const i of e.consents){const e=-1!==t.indexOf(i.type);if(i.matcher&&i.toggleAction){o(i.matcher)!==e&&await c(i.toggleAction)}else e?await c(i.trueAction):await c(i.falseAction)}}(e,n);case"ifcss":return async function(e,o){const i=t.find(e);i.target?e.falseAction&&await c(e.falseAction,o):e.trueAction&&await c(e.trueAction,o)}(e,n);case"waitcss":return async function(e){await new Promise((o=>{let c=e.retries||10;const i=e.waitTime||250,n=()=>{const a=t.find(e);(e.negated&&a.target||!e.negated&&!a.target)&&c>0?(c-=1,setTimeout(n,i)):o()};n()}))}(e);case"foreach":return async function(e,o){const i=t.find(e,!0),n=t.base;for(const n of i)n.target&&(t.setBase(n.target),await c(e.action,o));t.setBase(n)}(e,n);case"hide":return async function(e){const o=t.find(e);o.target&&o.target.classList.add("Autoconsent-Hidden")}(e);case"slide":return async function(e){const o=t.find(e),c=t.find(e.dragTarget);if(o.target){const e=o.target.getBoundingClientRect(),t=c.target.getBoundingClientRect();let i=t.top-e.top,n=t.left-e.left;"y"===this.config.axis.toLowerCase()&&(n=0),"x"===this.config.axis.toLowerCase()&&(i=0);const a=window.screenX+e.left+e.width/2,s=window.screenY+e.top+e.height/2,r=e.left+e.width/2,l=e.top+e.height/2,p=document.createEvent("MouseEvents");p.initMouseEvent("mousedown",!0,!0,window,0,a,s,r,l,!1,!1,!1,!1,0,o.target);const d=document.createEvent("MouseEvents");d.initMouseEvent("mousemove",!0,!0,window,0,a+n,s+i,r+n,l+i,!1,!1,!1,!1,0,o.target);const u=document.createEvent("MouseEvents");u.initMouseEvent("mouseup",!0,!0,window,0,a+n,s+i,r+n,l+i,!1,!1,!1,!1,0,o.target),o.target.dispatchEvent(p),await this.waitTimeout(10),o.target.dispatchEvent(d),await this.waitTimeout(10),o.target.dispatchEvent(u)}}(e);case"close":return async function(){window.close()}();case"wait":return async function(e){await i(e.waitTime)}(e);case"eval":return async function(e){return console.log("eval!",e.code),new Promise((t=>{try{e.async?(window.eval(e.code),setTimeout((()=>{t(window.eval("window.__consentCheckResult"))}),e.timeout||250)):t(window.eval(e.code))}catch(o){console.warn("eval error",o,e.code),t(!1)}}))}(e);default:throw"Unknown action type: "+e.type}}function i(e){return new Promise((t=>{setTimeout((()=>{t()}),e)}))}function n(){return crypto&&void 0!==crypto.randomUUID?crypto.randomUUID():Math.random().toString()}var a={pending:new Map,sendContentMessage:null};function s(e,t){const o=n();a.sendContentMessage({type:"eval",id:o,code:e,snippetId:t});const c=new class{constructor(e,t=1e3){this.id=e,this.promise=new Promise(((e,t)=>{this.resolve=e,this.reject=t})),this.timer=window.setTimeout((()=>{this.reject(new Error("timeout"))}),t)}}(o);return a.pending.set(c.id,c),c.promise}var r={EVAL_0:()=>console.log(1),EVAL_CONSENTMANAGER_1:()=>window.__cmp&&"object"==typeof __cmp("getCMPData"),EVAL_CONSENTMANAGER_2:()=>!__cmp("consentStatus").userChoiceExists,EVAL_CONSENTMANAGER_3:()=>__cmp("setConsent",0),EVAL_CONSENTMANAGER_4:()=>__cmp("setConsent",1),EVAL_CONSENTMANAGER_5:()=>__cmp("consentStatus").userChoiceExists,EVAL_COOKIEBOT_1:()=>!!window.Cookiebot,EVAL_COOKIEBOT_2:()=>!window.Cookiebot.hasResponse&&!0===window.Cookiebot.dialog?.visible,EVAL_COOKIEBOT_3:()=>window.Cookiebot.withdraw()||!0,EVAL_COOKIEBOT_4:()=>window.Cookiebot.hide()||!0,EVAL_COOKIEBOT_5:()=>!0===window.Cookiebot.declined,EVAL_KLARO_1:()=>{const e=globalThis.klaroConfig||globalThis.klaro?.getManager&&globalThis.klaro.getManager().config;if(!e)return!0;const t=(e.services||e.apps).filter((e=>!e.required)).map((e=>e.name));if(klaro&&klaro.getManager){const e=klaro.getManager();return t.every((t=>!e.consents[t]))}if(klaroConfig&&"cookie"===klaroConfig.storageMethod){const e=klaroConfig.cookieName||klaroConfig.storageName,o=JSON.parse(decodeURIComponent(document.cookie.split(";").find((t=>t.trim().startsWith(e))).split("=")[1]));return Object.keys(o).filter((e=>t.includes(e))).every((e=>!1===o[e]))}},EVAL_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||"moove_gdpr_strict_cookies"===e.name||(e.checked=!1)}))||!0,EVAL_ONENINETWO_0:()=>document.cookie.includes("CC_ADVERTISING=NO")&&document.cookie.includes("CC_ANALYTICS=NO"),EVAL_OPERA_0:()=>document.cookie.includes("cookie_consent_essential=true")&&!document.cookie.includes("cookie_consent_marketing=true"),EVAL_PAYPAL_0:()=>!0===document.cookie.includes("cookie_prefs"),EVAL_PRIMEBOX_0:()=>!document.cookie.includes("cb-enabled=accepted"),EVAL_PUBTECH_0:()=>document.cookie.includes("euconsent-v2")&&(document.cookie.match(/.YAAAAAAAAAAA/)||document.cookie.match(/.aAAAAAAAAAAA/)||document.cookie.match(/.YAAACFgAAAAA/)),EVAL_REDDIT_0:()=>document.cookie.includes("eu_cookie={%22opted%22:true%2C%22nonessential%22:false}"),EVAL_SIBBO_0:()=>!!window.localStorage.getItem("euconsent-v2"),EVAL_SIRDATA_UNBLOCK_SCROLL:()=>(document.documentElement.classList.forEach((e=>{e.startsWith("sd-cmp-")&&document.documentElement.classList.remove(e)})),!0),EVAL_SNIGEL_0:()=>!!document.cookie.match("snconsent"),EVAL_STEAMPOWERED_0:()=>2===JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>e.trim().startsWith("cookieSettings"))).split("=")[1])).preference_state,EVAL_SVT_TEST:()=>document.cookie.includes('cookie-consent-1={"optedIn":true,"functionality":false,"statistics":false}'),EVAL_TAKEALOT_0:()=>document.body.classList.remove("freeze")||(document.body.style="")||!0,EVAL_TARTEAUCITRON_0:()=>tarteaucitron.userInterface.respondAll(!1)||!0,EVAL_TARTEAUCITRON_1:()=>tarteaucitron.userInterface.respondAll(!0)||!0,EVAL_TARTEAUCITRON_2:()=>document.cookie.match(/tarteaucitron=[^;]*/)[0].includes("false"),EVAL_TAUNTON_TEST:()=>document.cookie.includes("taunton_user_consent_submitted=true"),EVAL_TEALIUM_0:()=>void 0!==window.utag&&"object"==typeof utag.gdpr,EVAL_TEALIUM_1:()=>utag.gdpr.setConsentValue(!1)||!0,EVAL_TEALIUM_DONOTSELL:()=>utag.gdpr.dns?.setDnsState(!1)||!0,EVAL_TEALIUM_2:()=>utag.gdpr.setConsentValue(!0)||!0,EVAL_TEALIUM_3:()=>1!==utag.gdpr.getConsentState(),EVAL_TEALIUM_DONOTSELL_CHECK:()=>1!==utag.gdpr.dns?.getDnsState(),EVAL_TESTCMP_0:()=>"button_clicked"===window.results.results[0],EVAL_TESTCMP_COSMETIC_0:()=>"banner_hidden"===window.results.results[0],EVAL_THEFREEDICTIONARY_0:()=>cmpUi.showPurposes()||cmpUi.rejectAll()||!0,EVAL_THEFREEDICTIONARY_1:()=>cmpUi.allowAll()||!0,EVAL_THEVERGE_0:()=>document.cookie.includes("_duet_gdpr_acknowledged=1"),EVAL_UBUNTU_COM_0:()=>document.cookie.includes("_cookies_accepted=essential"),EVAL_UK_COOKIE_CONSENT_0:()=>!document.cookie.includes("catAccCookies"),EVAL_USERCENTRICS_API_0:()=>"object"==typeof UC_UI,EVAL_USERCENTRICS_API_1:()=>!!UC_UI.closeCMP(),EVAL_USERCENTRICS_API_2:()=>!!UC_UI.denyAllConsents(),EVAL_USERCENTRICS_API_3:()=>!!UC_UI.acceptAllConsents(),EVAL_USERCENTRICS_API_4:()=>!!UC_UI.closeCMP(),EVAL_USERCENTRICS_API_5:()=>!0===UC_UI.areAllConsentsAccepted(),EVAL_USERCENTRICS_API_6:()=>!1===UC_UI.areAllConsentsAccepted(),EVAL_USERCENTRICS_BUTTON_0:()=>JSON.parse(localStorage.getItem("usercentrics")).consents.every((e=>e.isEssential||!e.consentStatus)),EVAL_WAITROSE_0:()=>Array.from(document.querySelectorAll("label[id$=cookies-deny-label]")).forEach((e=>e.click()))||!0,EVAL_WAITROSE_1:()=>document.cookie.includes("wtr_cookies_advertising=0")&&document.cookie.includes("wtr_cookies_analytics=0"),EVAL_WP_COOKIE_NOTICE_0:()=>document.cookie.includes("wpl_viewed_cookie=no"),EVAL_XE_TEST:()=>document.cookie.includes("xeConsentState={%22performance%22:false%2C%22marketing%22:false%2C%22compliance%22:false}"),EVAL_XING_0:()=>document.cookie.includes("userConsent=%7B%22marketing%22%3Afalse"),EVAL_YOUTUBE_DESKTOP_0:()=>!!document.cookie.match(/SOCS=CAE/),EVAL_YOUTUBE_MOBILE_0:()=>!!document.cookie.match(/SOCS=CAE/)};var l={main:!0,frame:!1,urlPattern:""},p=class{constructor(e){this.runContext=l,this.autoconsent=e}get hasSelfTest(){throw new Error("Not Implemented")}get isIntermediate(){throw new Error("Not Implemented")}get isCosmetic(){throw new Error("Not Implemented")}mainWorldEval(e){const t=r[e];if(!t)return console.warn("Snippet not found",e),Promise.resolve(!1);const o=this.autoconsent.config.logs;if(this.autoconsent.config.isMainWorld){o.evals&&console.log("inline eval:",e,t);let c=!1;try{c=!!t.call(globalThis)}catch(t){o.evals&&console.error("error evaluating rule",e,t)}return Promise.resolve(c)}const c=`(${t.toString()})()`;return o.evals&&console.log("async eval:",e,c),s(c,e).catch((t=>(o.evals&&console.error("error evaluating rule",e,t),!1)))}checkRunContext(){const e={...l,...this.runContext},t=window.top===window;return!(t&&!e.main)&&(!(!t&&!e.frame)&&!(e.urlPattern&&!window.location.href.match(e.urlPattern)))}detectCmp(){throw new Error("Not Implemented")}async detectPopup(){return!1}optOut(){throw new Error("Not Implemented")}optIn(){throw new Error("Not Implemented")}openCmp(){throw new Error("Not Implemented")}async test(){return Promise.resolve(!0)}click(e,t=!1){return this.autoconsent.domActions.click(e,t)}elementExists(e){return this.autoconsent.domActions.elementExists(e)}elementVisible(e,t){return this.autoconsent.domActions.elementVisible(e,t)}waitForElement(e,t){return this.autoconsent.domActions.waitForElement(e,t)}waitForVisible(e,t,o){return this.autoconsent.domActions.waitForVisible(e,t,o)}waitForThenClick(e,t,o){return this.autoconsent.domActions.waitForThenClick(e,t,o)}wait(e){return this.autoconsent.domActions.wait(e)}hide(e,t){return this.autoconsent.domActions.hide(e,t)}prehide(e){return this.autoconsent.domActions.prehide(e)}undoPrehide(){return this.autoconsent.domActions.undoPrehide()}querySingleReplySelector(e,t){return this.autoconsent.domActions.querySingleReplySelector(e,t)}querySelectorChain(e){return this.autoconsent.domActions.querySelectorChain(e)}elementSelector(e){return this.autoconsent.domActions.elementSelector(e)}},d=class extends p{constructor(e,t){super(t),this.rule=e,this.name=e.name,this.runContext=e.runContext||l}get hasSelfTest(){return!!this.rule.test}get isIntermediate(){return!!this.rule.intermediate}get isCosmetic(){return!!this.rule.cosmetic}get prehideSelectors(){return this.rule.prehideSelectors}async detectCmp(){return!!this.rule.detectCmp&&this._runRulesParallel(this.rule.detectCmp)}async detectPopup(){return!!this.rule.detectPopup&&this._runRulesSequentially(this.rule.detectPopup)}async optOut(){const e=this.autoconsent.config.logs;return!!this.rule.optOut&&(e.lifecycle&&console.log("Initiated optOut()",this.rule.optOut),this._runRulesSequentially(this.rule.optOut))}async optIn(){const e=this.autoconsent.config.logs;return!!this.rule.optIn&&(e.lifecycle&&console.log("Initiated optIn()",this.rule.optIn),this._runRulesSequentially(this.rule.optIn))}async openCmp(){return!!this.rule.openCmp&&this._runRulesSequentially(this.rule.openCmp)}async test(){return this.hasSelfTest?this._runRulesSequentially(this.rule.test):super.test()}async evaluateRuleStep(e){const t=[],o=this.autoconsent.config.logs;if(e.exists&&t.push(this.elementExists(e.exists)),e.visible&&t.push(this.elementVisible(e.visible,e.check)),e.eval){const o=this.mainWorldEval(e.eval);t.push(o)}if(e.waitFor&&t.push(this.waitForElement(e.waitFor,e.timeout)),e.waitForVisible&&t.push(this.waitForVisible(e.waitForVisible,e.timeout,e.check)),e.click&&t.push(this.click(e.click,e.all)),e.waitForThenClick&&t.push(this.waitForThenClick(e.waitForThenClick,e.timeout,e.all)),e.wait&&t.push(this.wait(e.wait)),e.hide&&t.push(this.hide(e.hide,e.method)),e.if){if(!e.if.exists&&!e.if.visible)return console.error("invalid conditional rule",e.if),!1;const c=await this.evaluateRuleStep(e.if);o.rulesteps&&console.log("Condition is",c),c?t.push(this._runRulesSequentially(e.then)):e.else?t.push(this._runRulesSequentially(e.else)):t.push(!0)}if(e.any){for(const t of e.any)if(await this.evaluateRuleStep(t))return!0;return!1}if(0===t.length)return o.errors&&console.warn("Unrecognized rule",e),!1;return(await Promise.all(t)).reduce(((e,t)=>e&&t),!0)}async _runRulesParallel(e){const t=e.map((e=>this.evaluateRuleStep(e)));return(await Promise.all(t)).every((e=>!!e))}async _runRulesSequentially(e){const t=this.autoconsent.config.logs;for(const o of e){t.rulesteps&&console.log("Running rule...",o);const e=await this.evaluateRuleStep(o);if(t.rulesteps&&console.log("...rule result",e),!e&&!o.optional)return!1}return!0}};function u(e="autoconsent-css-rules"){const t=`style#${e}`,o=document.querySelector(t);if(o&&o instanceof HTMLStyleElement)return o;{const t=document.head||document.getElementsByTagName("head")[0]||document.documentElement,o=document.createElement("style");return o.id=e,t.appendChild(o),o}}function m(e,t,o="display"){const c=`${t} { ${"opacity"===o?"opacity: 0":"display: none"} !important; z-index: -1 !important; pointer-events: none !important; } `;return e instanceof HTMLStyleElement&&(e.innerText+=c,t.length>0)}async function h(e,t,o){const c=await e();return!c&&t>0?new Promise((c=>{setTimeout((async()=>{c(h(e,t-1,o))}),o)})):Promise.resolve(c)}function k(e){if(!e)return!1;if(null!==e.offsetParent)return!0;{const t=window.getComputedStyle(e);if("fixed"===t.position&&"none"!==t.display)return!0}return!1}function b(e){const t={enabled:!0,autoAction:"optOut",disabledCmps:[],enablePrehide:!0,enableCosmeticRules:!0,detectRetries:20,isMainWorld:!1,prehideTimeout:2e3,logs:{lifecycle:!1,rulesteps:!1,evals:!1,errors:!0,messages:!1}},o=(c=t,globalThis.structuredClone?structuredClone(c):JSON.parse(JSON.stringify(c)));var c;for(const c of Object.keys(t))void 0!==e[c]&&(o[c]=e[c]);return o}var _="#truste-show-consent",g="#truste-consent-track",y=[class extends p{constructor(e){super(e),this.name="TrustArc-top",this.prehideSelectors=[".trustarc-banner-container",`.truste_popframe,.truste_overlay,.truste_box_overlay,${g}`],this.runContext={main:!0,frame:!1},this._shortcutButton=null,this._optInDone=!1}get hasSelfTest(){return!1}get isIntermediate(){return!this._optInDone&&!this._shortcutButton}get isCosmetic(){return!1}async detectCmp(){const e=this.elementExists(`${_},${g}`);return e&&(this._shortcutButton=document.querySelector("#truste-consent-required")),e}async detectPopup(){return this.elementVisible(`#truste-consent-content,#trustarc-banner-overlay,${g}`,"all")}openFrame(){this.click(_)}async optOut(){return this._shortcutButton?(this._shortcutButton.click(),!0):(m(u(),`.truste_popframe, .truste_overlay, .truste_box_overlay, ${g}`),this.click(_),setTimeout((()=>{u().remove()}),1e4),!0)}async optIn(){return this._optInDone=!0,this.click("#truste-consent-button")}async openCmp(){return!0}async test(){return await this.mainWorldEval("EVAL_TRUSTARC_TOP")}},class extends p{constructor(){super(...arguments),this.name="TrustArc-frame",this.runContext={main:!1,frame:!0,urlPattern:"^https://consent-pref\\.trustarc\\.com/\\?"}}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return!0}async detectPopup(){return this.elementVisible("#defaultpreferencemanager","any")&&this.elementVisible(".mainContent","any")}async navigateToSettings(){return await h((async()=>this.elementExists(".shp")||this.elementVisible(".advance","any")||this.elementExists(".switch span:first-child")),10,500),this.elementExists(".shp")&&this.click(".shp"),await this.waitForElement(".prefPanel",5e3),this.elementVisible(".advance","any")&&this.click(".advance"),await h((()=>this.elementVisible(".switch span:first-child","any")),5,1e3)}async optOut(){return await h((()=>"complete"===document.readyState),20,100),await this.waitForElement(".mainContent[aria-hidden=false]",5e3),!!this.click(".rejectAll")||(this.elementExists(".prefPanel")&&await this.waitForElement('.prefPanel[style="visibility: visible;"]',3e3),this.click("#catDetails0")?(this.click(".submit"),this.waitForThenClick("#gwt-debug-close_id",5e3),!0):this.click(".required")?(this.waitForThenClick("#gwt-debug-close_id",5e3),!0):(await this.navigateToSettings(),this.click(".switch span:nth-child(1):not(.active)",!0),this.click(".submit"),this.waitForThenClick("#gwt-debug-close_id",3e5),!0))}async optIn(){return this.click(".call")||(await this.navigateToSettings(),this.click(".switch span:nth-child(2)",!0),this.click(".submit"),this.waitForElement("#gwt-debug-close_id",3e5).then((()=>{this.click("#gwt-debug-close_id")}))),!0}},class extends p{constructor(){super(...arguments),this.name="Cybotcookiebot",this.prehideSelectors=["#CybotCookiebotDialog,#CybotCookiebotDialogBodyUnderlay,#dtcookie-container,#cookiebanner,#cb-cookieoverlay,.modal--cookie-banner,#cookiebanner_outer,#CookieBanner"]}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return await this.mainWorldEval("EVAL_COOKIEBOT_1")}async detectPopup(){return this.mainWorldEval("EVAL_COOKIEBOT_2")}async optOut(){await this.wait(500);let e=await this.mainWorldEval("EVAL_COOKIEBOT_3");return await this.wait(500),e=e&&await this.mainWorldEval("EVAL_COOKIEBOT_4"),e}async optIn(){return this.elementExists("#dtcookie-container")?this.click(".h-dtcookie-accept"):(this.click(".CybotCookiebotDialogBodyLevelButton:not(:checked):enabled",!0),this.click("#CybotCookiebotDialogBodyLevelButtonAccept"),this.click("#CybotCookiebotDialogBodyButtonAccept"),!0)}async test(){return await this.wait(500),await this.mainWorldEval("EVAL_COOKIEBOT_5")}},class extends p{constructor(){super(...arguments),this.name="Sourcepoint-frame",this.prehideSelectors=["div[id^='sp_message_container_'],.message-overlay","#sp_privacy_manager_container"],this.ccpaNotice=!1,this.ccpaPopup=!1,this.runContext={main:!1,frame:!0}}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){const e=new URL(location.href);return e.searchParams.has("message_id")&&"ccpa-notice.sp-prod.net"===e.hostname?(this.ccpaNotice=!0,!0):"ccpa-pm.sp-prod.net"===e.hostname?(this.ccpaPopup=!0,!0):("/index.html"===e.pathname||"/privacy-manager/index.html"===e.pathname||"/ccpa_pm/index.html"===e.pathname)&&(e.searchParams.has("message_id")||e.searchParams.has("requestUUID")||e.searchParams.has("consentUUID"))}async detectPopup(){return!!this.ccpaNotice||(this.ccpaPopup?await this.waitForElement(".priv-save-btn",2e3):(await this.waitForElement(".sp_choice_type_11,.sp_choice_type_12,.sp_choice_type_13,.sp_choice_type_ACCEPT_ALL,.sp_choice_type_SAVE_AND_EXIT",2e3),!this.elementExists(".sp_choice_type_9")))}async optIn(){return await this.waitForElement(".sp_choice_type_11,.sp_choice_type_ACCEPT_ALL",2e3),!!this.click(".sp_choice_type_11")||!!this.click(".sp_choice_type_ACCEPT_ALL")}isManagerOpen(){return"/privacy-manager/index.html"===location.pathname||"/ccpa_pm/index.html"===location.pathname}async optOut(){const e=this.autoconsent.config.logs;if(this.ccpaPopup){const e=document.querySelectorAll(".priv-purpose-container .sp-switch-arrow-block a.neutral.on .right");for(const t of e)t.click();const t=document.querySelectorAll(".priv-purpose-container .sp-switch-arrow-block a.switch-bg.on");for(const e of t)e.click();return this.click(".priv-save-btn")}if(!this.isManagerOpen()){if(!await this.waitForElement(".sp_choice_type_12,.sp_choice_type_13"))return!1;if(!this.elementExists(".sp_choice_type_12"))return this.click(".sp_choice_type_13");this.click(".sp_choice_type_12"),await h((()=>this.isManagerOpen()),200,100)}await this.waitForElement(".type-modal",2e4),this.waitForThenClick(".ccpa-stack .pm-switch[aria-checked=true] .slider",500,!0);try{const e=".sp_choice_type_REJECT_ALL",t=".reject-toggle",o=await Promise.race([this.waitForElement(e,2e3).then((e=>e?0:-1)),this.waitForElement(t,2e3).then((e=>e?1:-1)),this.waitForElement(".pm-features",2e3).then((e=>e?2:-1))]);if(0===o)return await this.wait(1500),this.click(e);1===o?this.click(t):2===o&&(await this.waitForElement(".pm-features",1e4),this.click(".checked > span",!0),this.click(".chevron"))}catch(t){e.errors&&console.warn(t)}return this.click(".sp_choice_type_SAVE_AND_EXIT")}},class extends p{constructor(){super(...arguments),this.name="consentmanager.net",this.prehideSelectors=["#cmpbox,#cmpbox2"],this.apiAvailable=!1}get hasSelfTest(){return this.apiAvailable}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.apiAvailable=await this.mainWorldEval("EVAL_CONSENTMANAGER_1"),!!this.apiAvailable||this.elementExists("#cmpbox")}async detectPopup(){return this.apiAvailable?(await this.wait(500),await this.mainWorldEval("EVAL_CONSENTMANAGER_2")):this.elementVisible("#cmpbox .cmpmore","any")}async optOut(){return await this.wait(500),this.apiAvailable?await this.mainWorldEval("EVAL_CONSENTMANAGER_3"):!!this.click(".cmpboxbtnno")||(this.elementExists(".cmpwelcomeprpsbtn")?(this.click(".cmpwelcomeprpsbtn > a[aria-checked=true]",!0),this.click(".cmpboxbtnsave"),!0):(this.click(".cmpboxbtncustom"),await this.waitForElement(".cmptblbox",2e3),this.click(".cmptdchoice > a[aria-checked=true]",!0),this.click(".cmpboxbtnyescustomchoices"),this.hide("#cmpwrapper,#cmpbox","display"),!0))}async optIn(){return this.apiAvailable?await this.mainWorldEval("EVAL_CONSENTMANAGER_4"):this.click(".cmpboxbtnyes")}async test(){if(this.apiAvailable)return await this.mainWorldEval("EVAL_CONSENTMANAGER_5")}},class extends p{constructor(){super(...arguments),this.name="Evidon"}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("#_evidon_banner")}async detectPopup(){return this.elementVisible("#_evidon_banner","any")}async optOut(){return this.click("#_evidon-decline-button")||(m(u(),"#evidon-prefdiag-overlay,#evidon-prefdiag-background"),this.click("#_evidon-option-button"),await this.waitForElement("#evidon-prefdiag-overlay",5e3),this.click("#evidon-prefdiag-decline")),!0}async optIn(){return this.click("#_evidon-accept-button")}},class extends p{constructor(){super(...arguments),this.name="Onetrust",this.prehideSelectors=["#onetrust-banner-sdk,#onetrust-consent-sdk,.onetrust-pc-dark-filter,.js-consent-banner"],this.runContext={urlPattern:"^(?!.*https://www\\.nba\\.com/)"}}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("#onetrust-banner-sdk,#onetrust-pc-sdk")}async detectPopup(){return this.elementVisible("#onetrust-banner-sdk,#onetrust-pc-sdk","any")}async optOut(){return this.elementVisible("#onetrust-reject-all-handler,.ot-pc-refuse-all-handler,.js-reject-cookies","any")?this.click("#onetrust-reject-all-handler,.ot-pc-refuse-all-handler,.js-reject-cookies"):(this.elementExists("#onetrust-pc-btn-handler")?this.click("#onetrust-pc-btn-handler"):this.click(".ot-sdk-show-settings,button.js-cookie-settings"),await this.waitForElement("#onetrust-consent-sdk",2e3),await this.wait(1e3),this.click("#onetrust-consent-sdk input.category-switch-handler:checked,.js-editor-toggle-state:checked",!0),await this.wait(1e3),await this.waitForElement(".save-preference-btn-handler,.js-consent-save",2e3),this.click(".save-preference-btn-handler,.js-consent-save"),await this.waitForVisible("#onetrust-banner-sdk",5e3,"none"),!0)}async optIn(){return this.click("#onetrust-accept-btn-handler,#accept-recommended-btn-handler,.js-accept-cookies")}async test(){return await h((()=>this.mainWorldEval("EVAL_ONETRUST_1")),10,500)}},class extends p{constructor(){super(...arguments),this.name="Klaro",this.prehideSelectors=[".klaro"],this.settingsOpen=!1}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".klaro > .cookie-modal")?(this.settingsOpen=!0,!0):this.elementExists(".klaro > .cookie-notice")}async detectPopup(){return this.elementVisible(".klaro > .cookie-notice,.klaro > .cookie-modal","any")}async optOut(){return!!this.click(".klaro .cn-decline")||(this.settingsOpen||(this.click(".klaro .cn-learn-more,.klaro .cm-button-manage"),await this.waitForElement(".klaro > .cookie-modal",2e3),this.settingsOpen=!0),!!this.click(".klaro .cn-decline")||(this.click(".cm-purpose:not(.cm-toggle-all) > input:not(.half-checked,.required,.only-required),.cm-purpose:not(.cm-toggle-all) > div > input:not(.half-checked,.required,.only-required)",!0),this.click(".cm-btn-accept,.cm-button")))}async optIn(){return!!this.click(".klaro .cm-btn-accept-all")||(this.settingsOpen?(this.click(".cm-purpose:not(.cm-toggle-all) > input.half-checked",!0),this.click(".cm-btn-accept")):this.click(".klaro .cookie-notice .cm-btn-success"))}async test(){return await this.mainWorldEval("EVAL_KLARO_1")}},class extends p{constructor(){super(...arguments),this.name="Uniconsent"}get prehideSelectors(){return[".unic",".modal:has(.unic)"]}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".unic .unic-box,.unic .unic-bar,.unic .unic-modal")}async detectPopup(){return this.elementVisible(".unic .unic-box,.unic .unic-bar,.unic .unic-modal","any")}async optOut(){if(await this.waitForElement(".unic button",1e3),document.querySelectorAll(".unic button").forEach((e=>{const t=e.textContent;(t.includes("Manage Options")||t.includes("Optionen verwalten"))&&e.click()})),await this.waitForElement(".unic input[type=checkbox]",1e3)){await this.waitForElement(".unic button",1e3),document.querySelectorAll(".unic input[type=checkbox]").forEach((e=>{e.checked&&e.click()}));for(const e of document.querySelectorAll(".unic button")){const t=e.textContent;for(const o of["Confirm Choices","Save Choices","Auswahl speichern"])if(t.includes(o))return e.click(),await this.wait(500),!0}}return!1}async optIn(){return this.waitForThenClick(".unic #unic-agree")}async test(){await this.wait(1e3);return!this.elementExists(".unic .unic-box,.unic .unic-bar")}},class extends p{constructor(){super(...arguments),this.prehideSelectors=[".cmp-root"],this.name="Conversant"}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".cmp-root .cmp-receptacle")}async detectPopup(){return this.elementVisible(".cmp-root .cmp-receptacle","any")}async optOut(){if(!await this.waitForThenClick(".cmp-main-button:not(.cmp-main-button--primary)"))return!1;if(!await this.waitForElement(".cmp-view-tab-tabs"))return!1;await this.waitForThenClick(".cmp-view-tab-tabs > :first-child"),await this.waitForThenClick(".cmp-view-tab-tabs > .cmp-view-tab--active:first-child");for(const e of Array.from(document.querySelectorAll(".cmp-accordion-item"))){e.querySelector(".cmp-accordion-item-title").click(),await h((()=>!!e.querySelector(".cmp-accordion-item-content.cmp-active")),10,50);const t=e.querySelector(".cmp-accordion-item-content.cmp-active");t.querySelectorAll(".cmp-toggle-actions .cmp-toggle-deny:not(.cmp-toggle-deny--active)").forEach((e=>e.click())),t.querySelectorAll(".cmp-toggle-actions .cmp-toggle-checkbox:not(.cmp-toggle-checkbox--active)").forEach((e=>e.click()))}return await this.click(".cmp-main-button:not(.cmp-main-button--primary)"),!0}async optIn(){return this.waitForThenClick(".cmp-main-button.cmp-main-button--primary")}async test(){return document.cookie.includes("cmp-data=0")}},class extends p{constructor(){super(...arguments),this.name="tiktok.com",this.runContext={urlPattern:"tiktok"}}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}getShadowRoot(){const e=document.querySelector("tiktok-cookie-banner");return e?e.shadowRoot:null}async detectCmp(){return this.elementExists("tiktok-cookie-banner")}async detectPopup(){return k(this.getShadowRoot().querySelector(".tiktok-cookie-banner"))}async optOut(){const e=this.autoconsent.config.logs,t=this.getShadowRoot().querySelector(".button-wrapper button:first-child");return t?(e.rulesteps&&console.log("[clicking]",t),t.click(),!0):(e.errors&&console.log("no decline button found"),!1)}async optIn(){const e=this.autoconsent.config.logs,t=this.getShadowRoot().querySelector(".button-wrapper button:last-child");return t?(e.rulesteps&&console.log("[clicking]",t),t.click(),!0):(e.errors&&console.log("no accept button found"),!1)}async test(){const e=document.cookie.match(/cookie-consent=([^;]+)/);if(!e)return!1;const t=JSON.parse(decodeURIComponent(e[1]));return Object.values(t).every((e=>"boolean"!=typeof e||!1===e))}},class extends p{constructor(){super(...arguments),this.runContext={urlPattern:"^https://(www\\.)?airbnb\\.[^/]+/"},this.prehideSelectors=["div[data-testid=main-cookies-banner-container]",'div:has(> div:first-child):has(> div:last-child):has(> section [data-testid="strictly-necessary-cookies"])']}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("div[data-testid=main-cookies-banner-container]")}async detectPopup(){return this.elementVisible("div[data-testid=main-cookies-banner-container","any")}async optOut(){let e;for(await this.waitForThenClick("div[data-testid=main-cookies-banner-container] button._snbhip0");e=document.querySelector("[data-testid=modal-container] button[aria-checked=true]:not([disabled])");)e.click();return this.waitForThenClick("button[data-testid=save-btn]")}async optIn(){return this.waitForThenClick("div[data-testid=main-cookies-banner-container] button._148dgdpk")}async test(){return await h((()=>!!document.cookie.match("OptanonAlertBoxClosed")),20,200)}}];var w=[{name:"192.com",detectCmp:[{exists:".ont-cookies"}],detectPopup:[{visible:".ont-cookies"}],optIn:[{click:".ont-btn-main.ont-cookies-btn.js-ont-btn-ok2"}],optOut:[{click:".ont-cookes-btn-manage"},{click:".ont-btn-main.ont-cookies-btn.js-ont-btn-choose"}],test:[{eval:"EVAL_ONENINETWO_0"}]},{name:"1password-com",cosmetic:!0,prehideSelectors:['footer #footer-root [aria-label="Cookie Consent"]'],detectCmp:[{exists:'footer #footer-root [aria-label="Cookie Consent"]'}],detectPopup:[{visible:'footer #footer-root [aria-label="Cookie Consent"]'}],optIn:[{click:'footer #footer-root [aria-label="Cookie Consent"] button'}],optOut:[{hide:'footer #footer-root [aria-label="Cookie Consent"]'}]},{name:"abconcerts.be",vendorUrl:"https://unknown",intermediate:!1,prehideSelectors:["dialog.cookie-consent"],detectCmp:[{exists:"dialog.cookie-consent form.cookie-consent__form"}],detectPopup:[{visible:"dialog.cookie-consent form.cookie-consent__form"}],optIn:[{waitForThenClick:"dialog.cookie-consent form.cookie-consent__form button[value=yes]"}],optOut:[{if:{exists:"dialog.cookie-consent form.cookie-consent__form button[value=no]"},then:[{click:"dialog.cookie-consent form.cookie-consent__form button[value=no]"}],else:[{click:"dialog.cookie-consent form.cookie-consent__form button.cookie-consent__options-toggle"},{waitForThenClick:'dialog.cookie-consent form.cookie-consent__form button[value="save_options"]'}]}]},{name:"activobank.pt",runContext:{urlPattern:"^https://(www\\.)?activobank\\.pt"},prehideSelectors:["aside#cookies,.overlay-cookies"],detectCmp:[{exists:"#cookies .cookies-btn"}],detectPopup:[{visible:"#cookies #submitCookies"}],optIn:[{waitForThenClick:"#cookies #submitCookies"}],optOut:[{waitForThenClick:"#cookies #rejectCookies"}]},{name:"Adroll",prehideSelectors:["#adroll_consent_container"],detectCmp:[{exists:"#adroll_consent_container"}],detectPopup:[{visible:"#adroll_consent_container"}],optIn:[{waitForThenClick:"#adroll_consent_accept"}],optOut:[{waitForThenClick:"#adroll_consent_reject"}],test:[{eval:"EVAL_ADROLL_0"}]},{name:"affinity.serif.com",detectCmp:[{exists:".c-cookie-banner button[data-qa='allow-all-cookies']"}],detectPopup:[{visible:".c-cookie-banner"}],optIn:[{click:'button[data-qa="allow-all-cookies"]'}],optOut:[{click:'button[data-qa="manage-cookies"]'},{waitFor:'.c-cookie-banner ~ [role="dialog"]'},{waitForThenClick:'.c-cookie-banner ~ [role="dialog"] input[type="checkbox"][value="true"]',all:!0},{click:'.c-cookie-banner ~ [role="dialog"] .c-modal__action button'}],test:[{wait:500},{eval:"EVAL_AFFINITY_SERIF_COM_0"}]},{name:"agolde.com",cosmetic:!0,prehideSelectors:["#modal-1 div[data-micromodal-close]"],detectCmp:[{exists:"#modal-1 div[aria-labelledby=modal-1-title]"}],detectPopup:[{exists:"#modal-1 div[data-micromodal-close]"}],optIn:[{click:'button[aria-label="Close modal"]'}],optOut:[{hide:"#modal-1 div[data-micromodal-close]"}]},{name:"aliexpress",vendorUrl:"https://aliexpress.com/",runContext:{urlPattern:"^https://.*\\.aliexpress\\.com/"},prehideSelectors:["#gdpr-new-container"],detectCmp:[{exists:"#gdpr-new-container"}],detectPopup:[{visible:"#gdpr-new-container"}],optIn:[{waitForThenClick:"#gdpr-new-container .btn-accept"}],optOut:[{waitForThenClick:"#gdpr-new-container .btn-more"},{waitFor:"#gdpr-new-container .gdpr-dialog-switcher"},{click:"#gdpr-new-container .switcher-on",all:!0,optional:!0},{click:"#gdpr-new-container .btn-save"}]},{name:"almacmp",prehideSelectors:["#alma-cmpv2-container"],detectCmp:[{exists:"#alma-cmpv2-container"}],detectPopup:[{visible:"#alma-cmpv2-container #almacmp-modal-layer1"}],optIn:[{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer1 #almacmp-modalConfirmBtn"}],optOut:[{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer1 #almacmp-modalSettingBtn"},{waitFor:"#alma-cmpv2-container #almacmp-modal-layer2"},{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer2 #almacmp-reject-all-layer2"}],test:[{eval:"EVAL_ALMACMP_0"}]},{name:"altium.com",cosmetic:!0,prehideSelectors:[".altium-privacy-bar"],detectCmp:[{exists:".altium-privacy-bar"}],detectPopup:[{exists:".altium-privacy-bar"}],optIn:[{click:"a.altium-privacy-bar__btn"}],optOut:[{hide:".altium-privacy-bar"}]},{name:"amazon.com",prehideSelectors:['span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'],detectCmp:[{exists:'span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'}],detectPopup:[{visible:'span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'}],optIn:[{waitForVisible:"#sp-cc-accept"},{wait:500},{click:"#sp-cc-accept"}],optOut:[{waitForVisible:"#sp-cc-rejectall-link"},{wait:500},{click:"#sp-cc-rejectall-link"}]},{name:"aquasana.com",cosmetic:!0,prehideSelectors:["#consent-tracking"],detectCmp:[{exists:"#consent-tracking"}],detectPopup:[{exists:"#consent-tracking"}],optIn:[{click:"#accept_consent"}],optOut:[{hide:"#consent-tracking"}]},{name:"arbeitsagentur",vendorUrl:"https://www.arbeitsagentur.de/",prehideSelectors:[".modal-open bahf-cookie-disclaimer-dpl3"],detectCmp:[{exists:"bahf-cookie-disclaimer-dpl3"}],detectPopup:[{visible:"bahf-cookie-disclaimer-dpl3"}],optIn:[{waitForThenClick:["bahf-cookie-disclaimer-dpl3","bahf-cd-modal-dpl3 .ba-btn-primary"]}],optOut:[{waitForThenClick:["bahf-cookie-disclaimer-dpl3","bahf-cd-modal-dpl3 .ba-btn-contrast"]}],test:[{eval:"EVAL_ARBEITSAGENTUR_TEST"}]},{name:"asus",vendorUrl:"https://www.asus.com/",runContext:{urlPattern:"^https://www\\.asus\\.com/"},prehideSelectors:["#cookie-policy-info,#cookie-policy-info-bg"],detectCmp:[{exists:"#cookie-policy-info"}],detectPopup:[{visible:"#cookie-policy-info"}],optIn:[{waitForThenClick:'#cookie-policy-info [data-agree="Accept Cookies"]'}],optOut:[{if:{exists:"#cookie-policy-info .btn-reject"},then:[{waitForThenClick:"#cookie-policy-info .btn-reject"}],else:[{waitForThenClick:"#cookie-policy-info .btn-setting"},{waitForThenClick:'#cookie-policy-lightbox-wrapper [data-agree="Save Settings"]'}]}]},{name:"athlinks-com",runContext:{urlPattern:"^https://(www\\.)?athlinks\\.com/"},cosmetic:!0,prehideSelectors:["#footer-container ~ div"],detectCmp:[{exists:"#footer-container ~ div"}],detectPopup:[{visible:"#footer-container > div"}],optIn:[{click:"#footer-container ~ div button"}],optOut:[{hide:"#footer-container ~ div"}]},{name:"ausopen.com",cosmetic:!0,detectCmp:[{exists:".gdpr-popup__message"}],detectPopup:[{visible:".gdpr-popup__message"}],optOut:[{hide:".gdpr-popup__message"}],optIn:[{click:".gdpr-popup__message button"}]},{name:"automattic-cmp-optout",prehideSelectors:['form[class*="cookie-banner"][method="post"]'],detectCmp:[{exists:'form[class*="cookie-banner"][method="post"]'}],detectPopup:[{visible:'form[class*="cookie-banner"][method="post"]'}],optIn:[{click:'a[class*="accept-all-button"]'}],optOut:[{click:'form[class*="cookie-banner"] div[class*="simple-options"] a[class*="customize-button"]'},{waitForThenClick:"input[type=checkbox][checked]:not([disabled])",all:!0},{click:'a[class*="accept-selection-button"]'}]},{name:"aws.amazon.com",prehideSelectors:["#awsccc-cb-content","#awsccc-cs-container","#awsccc-cs-modalOverlay","#awsccc-cs-container-inner"],detectCmp:[{exists:"#awsccc-cb-content"}],detectPopup:[{visible:"#awsccc-cb-content"}],optIn:[{click:"button[data-id=awsccc-cb-btn-accept"}],optOut:[{click:"button[data-id=awsccc-cb-btn-customize]"},{waitFor:"input[aria-checked]"},{click:"input[aria-checked=true]",all:!0,optional:!0},{click:"button[data-id=awsccc-cs-btn-save]"}]},{name:"axeptio",prehideSelectors:[".axeptio_widget"],detectCmp:[{exists:".axeptio_widget"}],detectPopup:[{visible:".axeptio_widget"}],optIn:[{waitFor:".axeptio-widget--open"},{click:"button#axeptio_btn_acceptAll"}],optOut:[{waitFor:".axeptio-widget--open"},{click:"button#axeptio_btn_dismiss"}],test:[{eval:"EVAL_AXEPTIO_0"}]},{name:"baden-wuerttemberg.de",prehideSelectors:[".cookie-alert.t-dark"],cosmetic:!0,detectCmp:[{exists:".cookie-alert.t-dark"}],detectPopup:[{visible:".cookie-alert.t-dark"}],optIn:[{click:".cookie-alert__form input:not([disabled]):not([checked])"},{click:".cookie-alert__button button"}],optOut:[{hide:".cookie-alert.t-dark"}]},{name:"bahn-de",vendorUrl:"https://www.bahn.de/",cosmetic:!1,runContext:{main:!0,frame:!1,urlPattern:"^https://(www\\.)?bahn\\.de/"},intermediate:!1,prehideSelectors:[],detectCmp:[{exists:["body > div:first-child","#consent-layer"]}],detectPopup:[{visible:["body > div:first-child","#consent-layer"]}],optIn:[{waitForThenClick:["body > div:first-child","#consent-layer .js-accept-all-cookies"]}],optOut:[{waitForThenClick:["body > div:first-child","#consent-layer .js-accept-essential-cookies"]}],test:[{eval:"EVAL_BAHN_TEST"}]},{name:"bbb.org",runContext:{urlPattern:"^https://www\\.bbb\\.org/"},cosmetic:!0,prehideSelectors:['div[aria-label="use of cookies on bbb.org"]'],detectCmp:[{exists:'div[aria-label="use of cookies on bbb.org"]'}],detectPopup:[{visible:'div[aria-label="use of cookies on bbb.org"]'}],optIn:[{click:'div[aria-label="use of cookies on bbb.org"] button.bds-button-unstyled span.visually-hidden'}],optOut:[{hide:'div[aria-label="use of cookies on bbb.org"]'}]},{name:"bing.com",prehideSelectors:["#bnp_container"],detectCmp:[{exists:"#bnp_cookie_banner"}],detectPopup:[{visible:"#bnp_cookie_banner"}],optIn:[{click:"#bnp_btn_accept"}],optOut:[{click:"#bnp_btn_preference"},{click:"#mcp_savesettings"}],test:[{eval:"EVAL_BING_0"}]},{name:"blocksy",vendorUrl:"https://creativethemes.com/blocksy/docs/extensions/cookies-consent/",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,prehideSelectors:[".cookie-notification"],detectCmp:[{exists:"#blocksy-ext-cookies-consent-styles-css"}],detectPopup:[{visible:".cookie-notification"}],optIn:[{click:".cookie-notification .ct-cookies-decline-button"}],optOut:[{waitForThenClick:".cookie-notification .ct-cookies-decline-button"}],test:[{eval:"EVAL_BLOCKSY_0"}]},{name:"borlabs",detectCmp:[{exists:"._brlbs-block-content"}],detectPopup:[{visible:"._brlbs-bar-wrap,._brlbs-box-wrap"}],optIn:[{click:"a[data-cookie-accept-all]"}],optOut:[{click:"a[data-cookie-individual]"},{waitForVisible:".cookie-preference"},{click:"input[data-borlabs-cookie-checkbox]:checked",all:!0,optional:!0},{click:"#CookiePrefSave"},{wait:500}],prehideSelectors:["#BorlabsCookieBox"],test:[{eval:"EVAL_BORLABS_0"}]},{name:"bundesregierung.de",prehideSelectors:[".bpa-cookie-banner"],detectCmp:[{exists:".bpa-cookie-banner"}],detectPopup:[{visible:".bpa-cookie-banner .bpa-module-full-hero"}],optIn:[{click:".bpa-accept-all-button"}],optOut:[{wait:500,comment:"click is not immediately recognized"},{waitForThenClick:".bpa-close-button"}],test:[{eval:"EVAL_BUNDESREGIERUNG_DE_0"}]},{name:"burpee.com",cosmetic:!0,prehideSelectors:["#notice-cookie-block"],detectCmp:[{exists:"#notice-cookie-block"}],detectPopup:[{exists:"#html-body #notice-cookie-block"}],optIn:[{click:"#btn-cookie-allow"}],optOut:[{hide:"#html-body #notice-cookie-block, #notice-cookie"}]},{name:"canva.com",prehideSelectors:['div[role="dialog"] a[data-anchor-id="cookie-policy"]'],detectCmp:[{exists:'div[role="dialog"] a[data-anchor-id="cookie-policy"]'}],detectPopup:[{exists:'div[role="dialog"] a[data-anchor-id="cookie-policy"]'}],optIn:[{click:'div[role="dialog"] button:nth-child(1)'}],optOut:[{if:{exists:'div[role="dialog"] button:nth-child(3)'},then:[{click:'div[role="dialog"] button:nth-child(2)'}],else:[{click:'div[role="dialog"] button:nth-child(2)'},{waitFor:'div[role="dialog"] a[data-anchor-id="privacy-policy"]'},{click:'div[role="dialog"] button:nth-child(2)'},{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 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:[{click:"#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:"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"}],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",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)"},{click:'div.b-cookies-informer__switchers > div:nth-child(2) > div[at-attr="checkbox"] > span.b-input-radio__container > input[type="checkbox"]'},{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:["#cookie_initial_modal",".modal-backdrop"],runContext:{urlPattern:"^https://www\\.strato\\.de/"},detectCmp:[{exists:"#cookie_initial_modal"}],detectPopup:[{visible:"#cookie_initial_modal"}],optIn:[{click:"button#jss_consent_all_initial_modal"}],optOut:[{click:"button#jss_open_settings_modal"},{click:"button#jss_consent_checked"}]},{name:"svt.se",vendorUrl:"https://www.svt.se/",runContext:{urlPattern:"^https://www\\.svt\\.se/"},prehideSelectors:["[class*=CookieConsent__root___]"],detectCmp:[{exists:"[class*=CookieConsent__root___]"}],detectPopup:[{visible:"[class*=CookieConsent__modal___]"}],optIn:[{waitForThenClick:"[class*=CookieConsent__modal___] > div > button[class*=primary]"}],optOut:[{waitForThenClick:"[class*=CookieConsent__modal___] > div > button[class*=secondary]:nth-child(2)"}],test:[{eval:"EVAL_SVT_TEST"}]},{name:"takealot.com",cosmetic:!0,prehideSelectors:['div[class^="cookies-banner-module_"]'],detectCmp:[{exists:'div[class^="cookies-banner-module_cookie-banner_"]'}],detectPopup:[{exists:'div[class^="cookies-banner-module_cookie-banner_"]'}],optIn:[{click:'button[class*="cookies-banner-module_dismiss-button_"]'}],optOut:[{hide:'div[class^="cookies-banner-module_"]'},{if:{exists:'div[class^="cookies-banner-module_small-cookie-banner_"]'},then:[{eval:"EVAL_TAKEALOT_0"}],else:[]}]},{name:"tarteaucitron.js",prehideSelectors:["#tarteaucitronRoot"],detectCmp:[{exists:"#tarteaucitronRoot"}],detectPopup:[{visible:"#tarteaucitronRoot #tarteaucitronAlertSmall,#tarteaucitronRoot #tarteaucitronAlertBig",check:"any"}],optIn:[{eval:"EVAL_TARTEAUCITRON_1"}],optOut:[{eval:"EVAL_TARTEAUCITRON_0"}],test:[{eval:"EVAL_TARTEAUCITRON_2",comment:"sometimes there are required categories, so we check that at least something is false"}]},{name:"taunton",vendorUrl:"https://www.taunton.com/",prehideSelectors:["#taunton-user-consent__overlay"],detectCmp:[{exists:"#taunton-user-consent__overlay"}],detectPopup:[{exists:"#taunton-user-consent__overlay:not([aria-hidden=true])"}],optIn:[{click:"#taunton-user-consent__toolbar input[type=checkbox]:not(:checked)"},{click:"#taunton-user-consent__toolbar button[type=submit]"}],optOut:[{click:"#taunton-user-consent__toolbar input[type=checkbox]:checked",optional:!0,all:!0},{click:"#taunton-user-consent__toolbar button[type=submit]"}],test:[{eval:"EVAL_TAUNTON_TEST"}]},{name:"Tealium",prehideSelectors:["#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs,#__tealiumImplicitmodal,#consent-layer"],detectCmp:[{exists:"#__tealiumGDPRecModal *,#__tealiumGDPRcpPrefs *,#__tealiumImplicitmodal *"},{eval:"EVAL_TEALIUM_0"}],detectPopup:[{visible:"#__tealiumGDPRecModal *,#__tealiumGDPRcpPrefs *,#__tealiumImplicitmodal *",check:"any"}],optOut:[{eval:"EVAL_TEALIUM_1"},{eval:"EVAL_TEALIUM_DONOTSELL"},{hide:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs,#__tealiumImplicitmodal"},{waitForThenClick:"#cm-acceptNone,.js-accept-essential-cookies",timeout:1e3,optional:!0}],optIn:[{hide:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs"},{eval:"EVAL_TEALIUM_2"}],test:[{eval:"EVAL_TEALIUM_3"},{eval:"EVAL_TEALIUM_DONOTSELL_CHECK"},{visible:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs",check:"none"}]},{name:"temu",vendorUrl:"https://temu.com",runContext:{urlPattern:"^https://[^/]*temu\\.com/"},prehideSelectors:["._2d-8vq-W,._1UdBUwni"],detectCmp:[{exists:"._3YCsmIaS"}],detectPopup:[{visible:"._3YCsmIaS"}],optIn:[{waitForThenClick:"._3fKiu5wx._3zN5SumS._3tAK973O.IYOfhWEs.VGNGF1pA"}],optOut:[{waitForThenClick:"._3fKiu5wx._1_XToJBF._3tAK973O.IYOfhWEs.VGNGF1pA"}]},{name:"Termly",prehideSelectors:["#termly-code-snippet-support"],detectCmp:[{exists:"#termly-code-snippet-support"}],detectPopup:[{visible:"#termly-code-snippet-support div"}],optIn:[{waitForThenClick:'[data-tid="banner-accept"]'}],optOut:[{if:{exists:'[data-tid="banner-decline"]'},then:[{click:'[data-tid="banner-decline"]'}],else:[{click:".t-preference-button"},{wait:500},{if:{exists:".t-declineAllButton"},then:[{click:".t-declineAllButton"}],else:[{waitForThenClick:".t-preference-modal input[type=checkbox][checked]:not([disabled])",all:!0},{waitForThenClick:".t-saveButton"}]}]}]},{name:"termsfeed",vendorUrl:"https://termsfeed.com",comment:"v4.x.x",prehideSelectors:[".termsfeed-com---nb"],detectCmp:[{exists:".termsfeed-com---nb"}],detectPopup:[{visible:".termsfeed-com---nb"}],optIn:[{waitForThenClick:".cc-nb-okagree"}],optOut:[{waitForThenClick:".cc-nb-reject"}]},{name:"termsfeed3",vendorUrl:"https://termsfeed.com",comment:"v3.x.x",cosmetic:!0,prehideSelectors:[".cc_dialog.cc_css_reboot"],detectCmp:[{exists:".cc_dialog.cc_css_reboot"}],detectPopup:[{visible:".cc_dialog.cc_css_reboot"}],optIn:[{waitForThenClick:".cc_dialog.cc_css_reboot .cc_b_ok"}],optOut:[{hide:".cc_dialog.cc_css_reboot"}]},{name:"Test page cosmetic CMP",cosmetic:!0,prehideSelectors:["#privacy-test-page-cmp-test-prehide"],detectCmp:[{exists:"#privacy-test-page-cmp-test-banner"}],detectPopup:[{visible:"#privacy-test-page-cmp-test-banner"}],optIn:[{waitFor:"#accept-all"},{click:"#accept-all"}],optOut:[{hide:"#privacy-test-page-cmp-test-banner"}],test:[{wait:500},{eval:"EVAL_TESTCMP_COSMETIC_0"}]},{name:"Test page CMP",prehideSelectors:["#reject-all"],detectCmp:[{exists:"#privacy-test-page-cmp-test"}],detectPopup:[{visible:"#privacy-test-page-cmp-test"}],optIn:[{waitFor:"#accept-all"},{click:"#accept-all"}],optOut:[{waitFor:"#reject-all"},{click:"#reject-all"}],test:[{eval:"EVAL_TESTCMP_0"}]},{name:"thalia.de",prehideSelectors:[".consent-banner-box"],detectCmp:[{exists:"consent-banner[component=consent-banner]"}],detectPopup:[{visible:".consent-banner-box"}],optIn:[{click:".button-zustimmen"}],optOut:[{click:"button[data-consent=disagree]"}]},{name:"thefreedictionary.com",prehideSelectors:["#cmpBanner"],detectCmp:[{exists:"#cmpBanner"}],detectPopup:[{visible:"#cmpBanner"}],optIn:[{eval:"EVAL_THEFREEDICTIONARY_1"}],optOut:[{eval:"EVAL_THEFREEDICTIONARY_0"}]},{name:"theverge",runContext:{frame:!1,main:!0,urlPattern:"^https://(www)?\\.theverge\\.com"},intermediate:!1,prehideSelectors:[".duet--cta--cookie-banner"],detectCmp:[{exists:".duet--cta--cookie-banner"}],detectPopup:[{visible:".duet--cta--cookie-banner"}],optIn:[{click:".duet--cta--cookie-banner button.tracking-12",all:!1}],optOut:[{click:".duet--cta--cookie-banner button.tracking-12 > span"}],test:[{eval:"EVAL_THEVERGE_0"}]},{name:"tidbits-com",cosmetic:!0,prehideSelectors:["#eu_cookie_law_widget-2"],detectCmp:[{exists:"#eu_cookie_law_widget-2"}],detectPopup:[{visible:"#eu_cookie_law_widget-2"}],optIn:[{click:"#eu-cookie-law form > input.accept"}],optOut:[{hide:"#eu_cookie_law_widget-2"}]},{name:"tractor-supply",runContext:{urlPattern:"^https://www\\.tractorsupply\\.com/"},cosmetic:!0,prehideSelectors:[".tsc-cookie-banner"],detectCmp:[{exists:".tsc-cookie-banner"}],detectPopup:[{visible:".tsc-cookie-banner"}],optIn:[{click:"#cookie-banner-cancel"}],optOut:[{hide:".tsc-cookie-banner"}]},{name:"trader-joes-com",cosmetic:!0,prehideSelectors:['div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'],detectCmp:[{exists:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}],detectPopup:[{visible:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}],optIn:[{click:'div[class^="CookiesAlert_cookiesAlert__container__"] button'}],optOut:[{hide:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}]},{name:"transcend",vendorUrl:"https://unknown",cosmetic:!0,prehideSelectors:["#transcend-consent-manager"],detectCmp:[{exists:"#transcend-consent-manager"}],detectPopup:[{visible:"#transcend-consent-manager"}],optIn:[{waitForThenClick:["#transcend-consent-manager","#consentManagerMainDialog .inner-container button"]}],optOut:[{hide:"#transcend-consent-manager"}]},{name:"transip-nl",runContext:{urlPattern:"^https://www\\.transip\\.nl/"},prehideSelectors:["#consent-modal"],detectCmp:[{any:[{exists:"#consent-modal"},{exists:"#privacy-settings-content"}]}],detectPopup:[{any:[{visible:"#consent-modal"},{visible:"#privacy-settings-content"}]}],optIn:[{click:'button[type="submit"]'}],optOut:[{if:{exists:"#privacy-settings-content"},then:[{click:'button[type="submit"]'}],else:[{click:"div.one-modal__action-footer-column--secondary > a"}]}]},{name:"tropicfeel-com",prehideSelectors:["#shopify-section-cookies-controller"],detectCmp:[{exists:"#shopify-section-cookies-controller"}],detectPopup:[{visible:"#shopify-section-cookies-controller #cookies-controller-main-pane",check:"any"}],optIn:[{waitForThenClick:"#cookies-controller-main-pane form[data-form-allow-all] button"}],optOut:[{click:"#cookies-controller-main-pane a[data-tab-target=manage-cookies]"},{waitFor:"#manage-cookies-pane.active"},{click:"#manage-cookies-pane.active input[type=checkbox][checked]:not([disabled])",all:!0},{click:"#manage-cookies-pane.active button[type=submit]"}],test:[]},{name:"true-car",runContext:{urlPattern:"^https://www\\.truecar\\.com/"},cosmetic:!0,prehideSelectors:[['div[aria-labelledby="cookie-banner-heading"]']],detectCmp:[{exists:'div[aria-labelledby="cookie-banner-heading"]'}],detectPopup:[{visible:'div[aria-labelledby="cookie-banner-heading"]'}],optIn:[{click:'div[aria-labelledby="cookie-banner-heading"] > button[aria-label="Close"]'}],optOut:[{hide:'div[aria-labelledby="cookie-banner-heading"]'}]},{name:"truyo",prehideSelectors:["#truyo-consent-module"],detectCmp:[{exists:"#truyo-cookieBarContent"}],detectPopup:[{visible:"#truyo-consent-module"}],optIn:[{click:"button#acceptAllCookieButton"}],optOut:[{click:"button#declineAllCookieButton"}]},{name:"tumblr-com",cosmetic:!0,prehideSelectors:["#cmp-app-container"],detectCmp:[{exists:"#cmp-app-container"}],detectPopup:[{visible:"#cmp-app-container"}],optIn:[{click:"#tumblr #cmp-app-container div.components-modal__frame > iframe > html body > div > div > div.cmp__dialog-footer > div > button.components-button.white-space-normal.is-primary"}],optOut:[{hide:"#cmp-app-container"}]},{name:"twitch-mobile",vendorUrl:"https://m.twitch.tv/",cosmetic:!0,runContext:{urlPattern:"^https?://m\\.twitch\\.tv"},prehideSelectors:[],detectCmp:[{exists:'.ReactModal__Overlay [href="https://www.twitch.tv/p/cookie-policy"]'}],detectPopup:[{visible:'.ReactModal__Overlay [href="https://www.twitch.tv/p/cookie-policy"]'}],optIn:[{waitForThenClick:'.ReactModal__Overlay:has([href="https://www.twitch.tv/p/cookie-policy"]) button'}],optOut:[{hide:'.ReactModal__Overlay:has([href="https://www.twitch.tv/p/cookie-policy"])'}]},{name:"twitch.tv",runContext:{urlPattern:"^https?://(www\\.)?twitch\\.tv"},prehideSelectors:["div:has(> .consent-banner .consent-banner__content--gdpr-v2),.ReactModalPortal:has([data-a-target=consent-modal-save])"],detectCmp:[{exists:".consent-banner .consent-banner__content--gdpr-v2"}],detectPopup:[{visible:".consent-banner .consent-banner__content--gdpr-v2"}],optIn:[{click:'button[data-a-target="consent-banner-accept"]'}],optOut:[{hide:"div:has(> .consent-banner .consent-banner__content--gdpr-v2)"},{click:'button[data-a-target="consent-banner-manage-preferences"]'},{waitFor:"input[type=checkbox][data-a-target=tw-checkbox]"},{click:"input[type=checkbox][data-a-target=tw-checkbox][checked]:not([disabled])",all:!0,optional:!0},{waitForThenClick:"[data-a-target=consent-modal-save]"},{waitForVisible:".ReactModalPortal:has([data-a-target=consent-modal-save])",check:"none"}]},{name:"twitter",runContext:{urlPattern:"^https://([a-z0-9-]+\\.)?twitter\\.com/"},prehideSelectors:['[data-testid="BottomBar"]'],detectCmp:[{exists:'[data-testid="BottomBar"] div'}],detectPopup:[{visible:'[data-testid="BottomBar"] div'}],optIn:[{waitForThenClick:'[data-testid="BottomBar"] > div:has(>div:first-child>div:last-child>span[role=button]) > div:last-child > div[role=button]:first-child'}],optOut:[{waitForThenClick:'[data-testid="BottomBar"] > div:has(>div:first-child>div:last-child>span[role=button]) > div:last-child > div[role=button]:last-child'}],TODOtest:[{eval:"EVAL_document.cookie.includes('d_prefs=MjoxLGNvbnNlbnRfdmVyc2lvbjoy')"}]},{name:"ubuntu.com",prehideSelectors:["dialog.cookie-policy"],detectCmp:[{any:[{exists:"dialog.cookie-policy header"},{exists:'xpath///*[@id="modal"]/div/header'}]}],detectPopup:[{any:[{visible:"dialog header"},{visible:'xpath///*[@id="modal"]/div/header'}]}],optIn:[{any:[{waitForThenClick:"#cookie-policy-button-accept"},{waitForThenClick:'xpath///*[@id="cookie-policy-button-accept"]'}]}],optOut:[{any:[{waitForThenClick:"button.js-manage"},{waitForThenClick:'xpath///*[@id="cookie-policy-content"]/p[4]/button[2]'}]},{waitForThenClick:"dialog.cookie-policy .p-switch__input:checked",optional:!0,all:!0,timeout:500},{any:[{waitForThenClick:"dialog.cookie-policy .js-save-preferences"},{waitForThenClick:'xpath///*[@id="modal"]/div/button'}]}],test:[{eval:"EVAL_UBUNTU_COM_0"}]},{name:"UK Cookie Consent",prehideSelectors:["#catapult-cookie-bar"],cosmetic:!0,detectCmp:[{exists:"#catapult-cookie-bar"}],detectPopup:[{exists:".has-cookie-bar #catapult-cookie-bar"}],optIn:[{click:"#catapultCookie"}],optOut:[{hide:"#catapult-cookie-bar"}],test:[{eval:"EVAL_UK_COOKIE_CONSENT_0"}]},{name:"urbanarmorgear-com",cosmetic:!0,prehideSelectors:['div[class^="Layout__CookieBannerContainer-"]'],detectCmp:[{exists:'div[class^="Layout__CookieBannerContainer-"]'}],detectPopup:[{visible:'div[class^="Layout__CookieBannerContainer-"]'}],optIn:[{click:'button[class^="CookieBanner__AcceptButton"]'}],optOut:[{hide:'div[class^="Layout__CookieBannerContainer-"]'}]},{name:"usercentrics-api",detectCmp:[{exists:"#usercentrics-root"}],detectPopup:[{eval:"EVAL_USERCENTRICS_API_0"},{exists:["#usercentrics-root","[data-testid=uc-container]"]},{waitForVisible:"#usercentrics-root",timeout:2e3}],optIn:[{eval:"EVAL_USERCENTRICS_API_3"},{eval:"EVAL_USERCENTRICS_API_1"},{eval:"EVAL_USERCENTRICS_API_5"}],optOut:[{eval:"EVAL_USERCENTRICS_API_1"},{eval:"EVAL_USERCENTRICS_API_2"}],test:[{eval:"EVAL_USERCENTRICS_API_6"}]},{name:"usercentrics-button",detectCmp:[{exists:"#usercentrics-button"}],detectPopup:[{visible:"#usercentrics-button #uc-btn-accept-banner"}],optIn:[{click:"#usercentrics-button #uc-btn-accept-banner"}],optOut:[{click:"#usercentrics-button #uc-btn-deny-banner"}],test:[{eval:"EVAL_USERCENTRICS_BUTTON_0"}]},{name:"uswitch.com",prehideSelectors:["#cookie-banner-wrapper"],detectCmp:[{exists:"#cookie-banner-wrapper"}],detectPopup:[{visible:"#cookie-banner-wrapper"}],optIn:[{click:"#cookie_banner_accept_mobile"}],optOut:[{click:"#cookie_banner_save"}]},{name:"vodafone.de",runContext:{urlPattern:"^https://www\\.vodafone\\.de/"},prehideSelectors:[".dip-consent,.dip-consent-container"],detectCmp:[{exists:".dip-consent-container"}],detectPopup:[{visible:".dip-consent-content"}],optOut:[{click:'.dip-consent-btn[tabindex="2"]'}],optIn:[{click:'.dip-consent-btn[tabindex="1"]'}]},{name:"waitrose.com",prehideSelectors:["div[aria-labelledby=CookieAlertModalHeading]","section[data-test=initial-waitrose-cookie-consent-banner]","section[data-test=cookie-consent-modal]"],detectCmp:[{exists:"section[data-test=initial-waitrose-cookie-consent-banner]"}],detectPopup:[{visible:"section[data-test=initial-waitrose-cookie-consent-banner]"}],optIn:[{click:"button[data-test=accept-all]"}],optOut:[{click:"button[data-test=manage-cookies]"},{wait:200},{eval:"EVAL_WAITROSE_0"},{click:"button[data-test=submit]"}],test:[{eval:"EVAL_WAITROSE_1"}]},{name:"webflow",vendorUrl:"https://webflow.com/",prehideSelectors:[".fs-cc-components"],detectCmp:[{exists:".fs-cc-components"}],detectPopup:[{visible:".fs-cc-components"},{visible:"[fs-cc=banner]"}],optIn:[{wait:500},{waitForThenClick:"[fs-cc=banner] [fs-cc=allow]"}],optOut:[{wait:500},{waitForThenClick:"[fs-cc=banner] [fs-cc=deny]"}]},{name:"wetransfer.com",detectCmp:[{exists:".welcome__cookie-notice"}],detectPopup:[{visible:".welcome__cookie-notice"}],optIn:[{click:".welcome__button--accept"}],optOut:[{click:".welcome__button--decline"}]},{name:"whitepages.com",runContext:{urlPattern:"^https://www\\.whitepages\\.com/"},cosmetic:!0,prehideSelectors:[".cookie-wrapper, .cookie-overlay"],detectCmp:[{exists:".cookie-wrapper"}],detectPopup:[{visible:".cookie-overlay"}],optIn:[{click:'button[aria-label="Got it"]'}],optOut:[{hide:".cookie-wrapper"}]},{name:"wolframalpha",vendorUrl:"https://www.wolframalpha.com",prehideSelectors:[],cosmetic:!0,runContext:{urlPattern:"^https://www\\.wolframalpha\\.com/"},detectCmp:[{exists:"section._a_yb"}],detectPopup:[{visible:"section._a_yb"}],optIn:[{waitForThenClick:"section._a_yb button"}],optOut:[{hide:"section._a_yb"}]},{name:"woo-commerce-com",prehideSelectors:[".wccom-comp-privacy-banner .wccom-privacy-banner"],detectCmp:[{exists:".wccom-comp-privacy-banner .wccom-privacy-banner"}],detectPopup:[{exists:".wccom-comp-privacy-banner .wccom-privacy-banner"}],optIn:[{click:".wccom-privacy-banner__content-buttons button.is-primary"}],optOut:[{click:".wccom-privacy-banner__content-buttons button.is-secondary"},{waitForThenClick:"input[type=checkbox][checked]:not([disabled])",all:!0},{click:"div.wccom-modal__footer > button"}]},{name:"WP Cookie Notice for GDPR",vendorUrl:"https://wordpress.org/plugins/gdpr-cookie-consent/",prehideSelectors:["#gdpr-cookie-consent-bar"],detectCmp:[{exists:"#gdpr-cookie-consent-bar"}],detectPopup:[{visible:"#gdpr-cookie-consent-bar"}],optIn:[{waitForThenClick:"#gdpr-cookie-consent-bar #cookie_action_accept"}],optOut:[{waitForThenClick:"#gdpr-cookie-consent-bar #cookie_action_reject"}],test:[{eval:"EVAL_WP_COOKIE_NOTICE_0"}]},{name:"wpcc",cosmetic:!0,prehideSelectors:[".wpcc-container"],detectCmp:[{exists:".wpcc-container"}],detectPopup:[{exists:".wpcc-container .wpcc-message"}],optIn:[{click:".wpcc-compliance .wpcc-btn"}],optOut:[{hide:".wpcc-container"}]},{name:"xe.com",vendorUrl:"https://www.xe.com/",runContext:{urlPattern:"^https://www\\.xe\\.com/"},prehideSelectors:["[class*=ConsentBanner]"],detectCmp:[{exists:"[class*=ConsentBanner]"}],detectPopup:[{visible:"[class*=ConsentBanner]"}],optIn:[{waitForThenClick:"[class*=ConsentBanner] .egnScw"}],optOut:[{wait:1e3},{waitForThenClick:"[class*=ConsentBanner] .frDWEu"},{waitForThenClick:"[class*=ConsentBanner] .hXIpFU"}],test:[{eval:"EVAL_XE_TEST"}]},{name:"xhamster-eu",prehideSelectors:[".cookies-modal"],detectCmp:[{exists:".cookies-modal"}],detectPopup:[{exists:".cookies-modal"}],optIn:[{click:"button.cmd-button-accept-all"}],optOut:[{click:"button.cmd-button-reject-all"}]},{name:"xhamster-us",runContext:{urlPattern:"^https://(www\\.)?xhamster\\d?\\.com"},cosmetic:!0,prehideSelectors:[".cookie-announce"],detectCmp:[{exists:".cookie-announce"}],detectPopup:[{visible:".cookie-announce .announce-text"}],optIn:[{click:".cookie-announce button.xh-button"}],optOut:[{hide:".cookie-announce"}]},{name:"xing.com",detectCmp:[{exists:"div[class^=cookie-consent-CookieConsent]"}],detectPopup:[{exists:"div[class^=cookie-consent-CookieConsent]"}],optIn:[{click:"#consent-accept-button"}],optOut:[{click:"#consent-settings-button"},{click:".consent-banner-button-accept-overlay"}],test:[{eval:"EVAL_XING_0"}]},{name:"xnxx-com",cosmetic:!0,prehideSelectors:["#cookies-use-alert"],detectCmp:[{exists:"#cookies-use-alert"}],detectPopup:[{visible:"#cookies-use-alert"}],optIn:[{click:"#cookies-use-alert .close"}],optOut:[{hide:"#cookies-use-alert"}]},{name:"xvideos",vendorUrl:"https://xvideos.com",runContext:{urlPattern:"^https://[^/]*xvideos\\.com/"},prehideSelectors:[],detectCmp:[{exists:".disclaimer-opened #disclaimer-cookies"}],detectPopup:[{visible:".disclaimer-opened #disclaimer-cookies"}],optIn:[{waitForThenClick:"#disclaimer-accept_cookies"}],optOut:[{waitForThenClick:"#disclaimer-reject_cookies"}]},{name:"Yahoo",runContext:{urlPattern:"^https://consent\\.yahoo\\.com/v2/"},prehideSelectors:["#reject-all"],detectCmp:[{exists:"#consent-page"}],detectPopup:[{visible:"#consent-page"}],optIn:[{waitForThenClick:"#consent-page button[value=agree]"}],optOut:[{waitForThenClick:"#consent-page button[value=reject]"}]},{name:"youporn.com",cosmetic:!0,prehideSelectors:[".euCookieModal, #js_euCookieModal"],detectCmp:[{exists:".euCookieModal"}],detectPopup:[{exists:".euCookieModal, #js_euCookieModal"}],optIn:[{click:'button[name="user_acceptCookie"]'}],optOut:[{hide:".euCookieModal"}]},{name:"youtube-desktop",prehideSelectors:["tp-yt-iron-overlay-backdrop.opened","ytd-consent-bump-v2-lightbox"],detectCmp:[{exists:"ytd-consent-bump-v2-lightbox tp-yt-paper-dialog"},{exists:'ytd-consent-bump-v2-lightbox tp-yt-paper-dialog a[href^="https://consent.youtube.com/"]'}],detectPopup:[{visible:"ytd-consent-bump-v2-lightbox tp-yt-paper-dialog"}],optIn:[{waitForThenClick:"ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:last-child #button,ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:last-child button"},{wait:500}],optOut:[{waitForThenClick:"ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:first-child #button,ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:first-child button"},{wait:500}],test:[{wait:500},{eval:"EVAL_YOUTUBE_DESKTOP_0"}]},{name:"youtube-mobile",prehideSelectors:[".consent-bump-v2-lightbox"],detectCmp:[{exists:"ytm-consent-bump-v2-renderer"}],detectPopup:[{visible:"ytm-consent-bump-v2-renderer"}],optIn:[{waitForThenClick:"ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons c3-material-button:first-child button, ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons ytm-button-renderer:first-child button"},{wait:500}],optOut:[{waitForThenClick:"ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons c3-material-button:nth-child(2) button, ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons ytm-button-renderer:nth-child(2) button"},{wait:500}],test:[{wait:500},{eval:"EVAL_YOUTUBE_MOBILE_0"}]},{name:"zdf",prehideSelectors:["#zdf-cmp-banner-sdk"],detectCmp:[{exists:"#zdf-cmp-banner-sdk"}],detectPopup:[{visible:"#zdf-cmp-main.zdf-cmp-show"}],optIn:[{waitForThenClick:"#zdf-cmp-main #zdf-cmp-accept-btn"}],optOut:[{waitForThenClick:"#zdf-cmp-main #zdf-cmp-deny-btn"}],test:[]}],C={"didomi.io":{detectors:[{presentMatcher:{target:{selector:"#didomi-host, #didomi-notice"},type:"css"},showingMatcher:{target:{selector:"body.didomi-popup-open, .didomi-notice-banner"},type:"css"}}],methods:[{action:{target:{selector:".didomi-popup-notice-buttons .didomi-button:not(.didomi-button-highlight), .didomi-notice-banner .didomi-learn-more-button"},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{retries:50,target:{selector:"#didomi-purpose-cookies"},type:"waitcss",waitTime:50},{consents:[{description:"Share (everything) with others",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-share_whith_others]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-share_whith_others]:last-child"},type:"click"},type:"X"},{description:"Information storage and access",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-cookies]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-cookies]:last-child"},type:"click"},type:"D"},{description:"Content selection, offers and marketing",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-CL-T1Rgm7]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-CL-T1Rgm7]:last-child"},type:"click"},type:"E"},{description:"Analytics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-analytics]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-analytics]:last-child"},type:"click"},type:"B"},{description:"Analytics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-M9NRHJe3G]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-M9NRHJe3G]:last-child"},type:"click"},type:"B"},{description:"Ad and content selection",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-advertising_personalization]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-advertising_personalization]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection",falseAction:{parent:{childFilter:{target:{selector:"#didomi-purpose-pub-ciblee"}},selector:".didomi-consent-popup-data-processing, .didomi-components-accordion-label-container"},target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-pub-ciblee]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-pub-ciblee]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - basics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-q4zlJqdcD]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-q4zlJqdcD]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - partners and subsidiaries",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-partenaire-cAsDe8jC]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-partenaire-cAsDe8jC]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-p4em9a8m]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-p4em9a8m]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - others",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-autres-pub]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-autres-pub]:last-child"},type:"click"},type:"F"},{description:"Social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-reseauxsociaux]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-reseauxsociaux]:last-child"},type:"click"},type:"A"},{description:"Social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-social_media]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-social_media]:last-child"},type:"click"},type:"A"},{description:"Content selection",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-content_personalization]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-content_personalization]:last-child"},type:"click"},type:"E"},{description:"Ad delivery",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-ad_delivery]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-ad_delivery]:last-child"},type:"click"},type:"F"}],type:"consent"},{action:{consents:[{matcher:{childFilter:{target:{selector:":not(.didomi-components-radio__option--selected)"}},type:"css"},trueAction:{target:{selector:":nth-child(2)"},type:"click"},falseAction:{target:{selector:":first-child"},type:"click"},type:"X"}],type:"consent"},target:{selector:".didomi-components-radio"},type:"foreach"}],type:"list"},name:"DO_CONSENT"},{action:{parent:{selector:".didomi-consent-popup-footer .didomi-consent-popup-actions"},target:{selector:".didomi-components-button:first-child"},type:"click"},name:"SAVE_CONSENT"}]},oil:{detectors:[{presentMatcher:{target:{selector:".as-oil-content-overlay"},type:"css"},showingMatcher:{target:{selector:".as-oil-content-overlay"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".as-js-advanced-settings"},type:"click"},{retries:"10",target:{selector:".as-oil-cpc__purpose-container"},type:"waitcss",waitTime:"250"}],type:"list"},name:"OPEN_OPTIONS"},{action:{actions:[{consents:[{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Information storage and access","Opbevaring af og adgang til oplysninger på din enhed"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Information storage and access","Opbevaring af og adgang til oplysninger på din enhed"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"D"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personlige annoncer","Personalisation"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personlige annoncer","Personalisation"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"E"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Annoncevalg, levering og rapportering","Ad selection, delivery, reporting"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Annoncevalg, levering og rapportering","Ad selection, delivery, reporting"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"F"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personalisering af indhold","Content selection, delivery, reporting"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personalisering af indhold","Content selection, delivery, reporting"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"E"},{matcher:{parent:{childFilter:{target:{selector:".as-oil-cpc__purpose-header",textFilter:["Måling","Measurement"]}},selector:".as-oil-cpc__purpose-container"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{childFilter:{target:{selector:".as-oil-cpc__purpose-header",textFilter:["Måling","Measurement"]}},selector:".as-oil-cpc__purpose-container"},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"B"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:"Google"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:"Google"},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"F"}],type:"consent"}],type:"list"},name:"DO_CONSENT"},{action:{target:{selector:".as-oil__btn-optin"},type:"click"},name:"SAVE_CONSENT"},{action:{target:{selector:"div.as-oil"},type:"hide"},name:"HIDE_CMP"}]},optanon:{detectors:[{presentMatcher:{target:{selector:"#optanon-menu, .optanon-alert-box-wrapper"},type:"css"},showingMatcher:{target:{displayFilter:!0,selector:".optanon-alert-box-wrapper"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".optanon-alert-box-wrapper .optanon-toggle-display, a[onclick*='OneTrust.ToggleInfoDisplay()'], a[onclick*='Optanon.ToggleInfoDisplay()']"},type:"click"}],type:"list"},name:"OPEN_OPTIONS"},{action:{actions:[{target:{selector:".preference-menu-item #Your-privacy"},type:"click"},{target:{selector:"#optanon-vendor-consent-text"},type:"click"},{action:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},target:{selector:"#optanon-vendor-consent-list .vendor-item"},type:"foreach"},{target:{selector:".vendor-consent-back-link"},type:"click"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-performance"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-performance"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-functional"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-functional"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-advertising"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-advertising"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-social"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-social"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Social Media Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Social Media Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalisation"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalisation"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Site monitoring cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Site monitoring cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Third party privacy-enhanced content"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Third party privacy-enhanced content"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Performance & Advertising Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Performance & Advertising Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Information storage and access"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Information storage and access"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"D"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad selection, delivery, reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad selection, delivery, reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content selection, delivery, reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content selection, delivery, reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Measurement"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Measurement"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Recommended Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Recommended Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Unclassified Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Unclassified Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Analytical Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Analytical Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Marketing Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Marketing Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalization"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalization"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad Selection, Delivery & Reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad Selection, Delivery & Reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content Selection, Delivery & Reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content Selection, Delivery & Reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"}],type:"list"},name:"DO_CONSENT"},{action:{parent:{selector:".optanon-save-settings-button"},target:{selector:".optanon-white-button-middle"},type:"click"},name:"SAVE_CONSENT"},{action:{actions:[{target:{selector:"#optanon-popup-wrapper"},type:"hide"},{target:{selector:"#optanon-popup-bg"},type:"hide"},{target:{selector:".optanon-alert-box-wrapper"},type:"hide"}],type:"list"},name:"HIDE_CMP"}]},quantcast2:{detectors:[{presentMatcher:{target:{selector:"[data-tracking-opt-in-overlay]"},type:"css"},showingMatcher:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-learn-more]"},type:"css"}}],methods:[{action:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-learn-more]"},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{type:"wait",waitTime:500},{action:{actions:[{target:{selector:"div",textFilter:["Information storage and access"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"D"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Personalization"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"F"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Ad selection, delivery, reporting"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"F"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Content selection, delivery, reporting"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"E"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Measurement"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"B"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Other Partners"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},type:"ifcss"}],type:"list"},parent:{childFilter:{target:{selector:"input"}},selector:"[data-tracking-opt-in-overlay] > div > div"},target:{childFilter:{target:{selector:"input"}},selector:":scope > div"},type:"foreach"}],type:"list"},name:"DO_CONSENT"},{action:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-save]"},type:"click"},name:"SAVE_CONSENT"}]},springer:{detectors:[{presentMatcher:{parent:null,target:{selector:".cmp-app_gdpr"},type:"css"},showingMatcher:{parent:null,target:{displayFilter:!0,selector:".cmp-popup_popup"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".cmp-intro_rejectAll"},type:"click"},{type:"wait",waitTime:250},{target:{selector:".cmp-purposes_purposeItem:not(.cmp-purposes_selectedPurpose)"},type:"click"}],type:"list"},name:"OPEN_OPTIONS"},{action:{consents:[{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Przechowywanie informacji na urządzeniu lub dostęp do nich",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Przechowywanie informacji na urządzeniu lub dostęp do nich",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"D"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór podstawowych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór podstawowych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"F"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"F"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"E"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"E"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Stosowanie badań rynkowych w celu generowania opinii odbiorców",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Stosowanie badań rynkowych w celu generowania opinii odbiorców",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"X"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Opracowywanie i ulepszanie produktów",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Opracowywanie i ulepszanie produktów",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"X"}],type:"consent"},name:"DO_CONSENT"},{action:{target:{selector:".cmp-details_save"},type:"click"},name:"SAVE_CONSENT"}]},wordpressgdpr:{detectors:[{presentMatcher:{parent:null,target:{selector:".wpgdprc-consent-bar"},type:"css"},showingMatcher:{parent:null,target:{displayFilter:!0,selector:".wpgdprc-consent-bar"},type:"css"}}],methods:[{action:{parent:null,target:{selector:".wpgdprc-consent-bar .wpgdprc-consent-bar__settings",textFilter:null},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{target:{selector:".wpgdprc-consent-modal .wpgdprc-button",textFilter:"Eyeota"},type:"click"},{consents:[{description:"Eyeota Cookies",matcher:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Eyeota"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Eyeota"},target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},{target:{selector:".wpgdprc-consent-modal .wpgdprc-button",textFilter:"Advertising"},type:"click"},{consents:[{description:"Advertising Cookies",matcher:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Advertising"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Advertising"},target:{selector:"label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},name:"DO_CONSENT"},{action:{parent:null,target:{selector:".wpgdprc-button",textFilter:"Save my settings"},type:"click"},name:"SAVE_CONSENT"}]}},v={autoconsent:w,consentomatic:C},f=Object.freeze({__proto__:null,autoconsent:w,consentomatic:C,default:v});const A=new class{constructor(e,t=null,o=null){if(this.id=n(),this.rules=[],this.foundCmp=null,this.state={lifecycle:"loading",prehideOn:!1,findCmpAttempts:0,detectedCmps:[],detectedPopups:[],selfTest:null},a.sendContentMessage=e,this.sendContentMessage=e,this.rules=[],this.updateState({lifecycle:"loading"}),this.addDynamicRules(),t)this.initialize(t,o);else{o&&this.parseDeclarativeRules(o);e({type:"init",url:window.location.href}),this.updateState({lifecycle:"waitingForInitResponse"})}this.domActions=new class{constructor(e){this.autoconsentInstance=e}click(e,t=!1){const o=this.elementSelector(e);return this.autoconsentInstance.config.logs.rulesteps&&console.log("[click]",e,t,o),o.length>0&&(t?o.forEach((e=>e.click())):o[0].click()),o.length>0}elementExists(e){return this.elementSelector(e).length>0}elementVisible(e,t){const o=this.elementSelector(e),c=new Array(o.length);return o.forEach(((e,t)=>{c[t]=k(e)})),"none"===t?c.every((e=>!e)):0!==c.length&&("any"===t?c.some((e=>e)):c.every((e=>e)))}waitForElement(e,t=1e4){const o=Math.ceil(t/200);return this.autoconsentInstance.config.logs.rulesteps&&console.log("[waitForElement]",e),h((()=>this.elementSelector(e).length>0),o,200)}waitForVisible(e,t=1e4,o="any"){return h((()=>this.elementVisible(e,o)),Math.ceil(t/200),200)}async waitForThenClick(e,t=1e4,o=!1){return await this.waitForElement(e,t),this.click(e,o)}wait(e){return new Promise((t=>{setTimeout((()=>{t(!0)}),e)}))}hide(e,t){return m(u(),e,t)}prehide(e){const t=u("autoconsent-prehide");return this.autoconsentInstance.config.logs.lifecycle&&console.log("[prehide]",t,location.href),m(t,e,"opacity")}undoPrehide(){const e=u("autoconsent-prehide");return this.autoconsentInstance.config.logs.lifecycle&&console.log("[undoprehide]",e,location.href),e&&e.remove(),!!e}querySingleReplySelector(e,t=document){if(e.startsWith("aria/"))return[];if(e.startsWith("xpath/")){const o=e.slice(6),c=document.evaluate(o,t,null,XPathResult.ANY_TYPE,null);let i=null;const n=[];for(;i=c.iterateNext();)n.push(i);return n}return e.startsWith("text/")||e.startsWith("pierce/")?[]:t.shadowRoot?Array.from(t.shadowRoot.querySelectorAll(e)):Array.from(t.querySelectorAll(e))}querySelectorChain(e){let t,o=document;for(const c of e){if(t=this.querySingleReplySelector(c,o),0===t.length)return[];o=t[0]}return t}elementSelector(e){return"string"==typeof e?this.querySingleReplySelector(e):this.querySelectorChain(e)}}(this)}initialize(e,t){const o=b(e);if(o.logs.lifecycle&&console.log("autoconsent init",window.location.href),this.config=o,o.enabled){if(t&&this.parseDeclarativeRules(t),this.rules=function(e,t){return e.filter((e=>(!t.disabledCmps||!t.disabledCmps.includes(e.name))&&(t.enableCosmeticRules||!e.isCosmetic)))}(this.rules,o),e.enablePrehide)if(document.documentElement)this.prehideElements();else{const e=()=>{window.removeEventListener("DOMContentLoaded",e),this.prehideElements()};window.addEventListener("DOMContentLoaded",e)}if("loading"===document.readyState){const e=()=>{window.removeEventListener("DOMContentLoaded",e),this.start()};window.addEventListener("DOMContentLoaded",e)}else this.start();this.updateState({lifecycle:"initialized"})}else o.logs.lifecycle&&console.log("autoconsent is disabled")}addDynamicRules(){y.forEach((e=>{this.rules.push(new e(this))}))}parseDeclarativeRules(e){Object.keys(e.consentomatic).forEach((t=>{this.addConsentomaticCMP(t,e.consentomatic[t])})),e.autoconsent.forEach((e=>{this.addDeclarativeCMP(e)}))}addDeclarativeCMP(e){this.rules.push(new d(e,this))}addConsentomaticCMP(e,t){this.rules.push(new class{constructor(e,t){this.name=e,this.config=t,this.methods=new Map,this.runContext=l,this.isCosmetic=!1,t.methods.forEach((e=>{e.action&&this.methods.set(e.name,e.action)})),this.hasSelfTest=!1}get isIntermediate(){return!1}checkRunContext(){return!0}async detectCmp(){return this.config.detectors.map((e=>o(e.presentMatcher))).some((e=>!!e))}async detectPopup(){return this.config.detectors.map((e=>o(e.showingMatcher))).some((e=>!!e))}async executeAction(e,t){return!this.methods.has(e)||c(this.methods.get(e),t)}async optOut(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),await this.executeAction("HIDE_CMP"),await this.executeAction("DO_CONSENT",[]),await this.executeAction("SAVE_CONSENT"),!0}async optIn(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),await this.executeAction("HIDE_CMP"),await this.executeAction("DO_CONSENT",["D","A","B","E","F","X"]),await this.executeAction("SAVE_CONSENT"),!0}async openCmp(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),!0}async test(){return!0}}(`com_${e}`,t))}start(){window.requestIdleCallback?window.requestIdleCallback((()=>this._start()),{timeout:500}):this._start()}async _start(){const e=this.config.logs;e.lifecycle&&console.log(`Detecting CMPs on ${window.location.href}`),this.updateState({lifecycle:"started"});const t=await this.findCmp(this.config.detectRetries);if(this.updateState({detectedCmps:t.map((e=>e.name))}),0===t.length)return e.lifecycle&&console.log("no CMP found",location.href),this.config.enablePrehide&&this.undoPrehide(),this.updateState({lifecycle:"nothingDetected"}),!1;this.updateState({lifecycle:"cmpDetected"});const o=[],c=[];for(const e of t)e.isCosmetic?c.push(e):o.push(e);let i=!1,n=await this.detectPopups(o,(async e=>{i=await this.handlePopup(e)}));if(0===n.length&&(n=await this.detectPopups(c,(async e=>{i=await this.handlePopup(e)}))),0===n.length)return e.lifecycle&&console.log("no popup found"),this.config.enablePrehide&&this.undoPrehide(),!1;if(n.length>1){const t={msg:"Found multiple CMPs, check the detection rules.",cmps:n.map((e=>e.name))};e.errors&&console.warn(t.msg,t.cmps),this.sendContentMessage({type:"autoconsentError",details:t})}return i}async findCmp(e){const t=this.config.logs;this.updateState({findCmpAttempts:this.state.findCmpAttempts+1});const o=[];for(const e of this.rules)try{if(!e.checkRunContext())continue;await e.detectCmp()&&(t.lifecycle&&console.log(`Found CMP: ${e.name} ${window.location.href}`),this.sendContentMessage({type:"cmpDetected",url:location.href,cmp:e.name}),o.push(e))}catch(o){t.errors&&console.warn(`error detecting ${e.name}`,o)}return 0===o.length&&e>0?(await this.domActions.wait(500),this.findCmp(e-1)):o}async detectPopup(e){if(await this.waitForPopup(e).catch((t=>(this.config.logs.errors&&console.warn(`error waiting for a popup for ${e.name}`,t),!1))))return this.updateState({detectedPopups:this.state.detectedPopups.concat([e.name])}),this.sendContentMessage({type:"popupFound",cmp:e.name,url:location.href}),e;throw new Error("Popup is not shown")}async detectPopups(e,t){const o=e.map((e=>this.detectPopup(e)));await Promise.any(o).then((e=>{t(e)})).catch((()=>null));const c=await Promise.allSettled(o),i=[];for(const e of c)"fulfilled"===e.status&&i.push(e.value);return i}async handlePopup(e){return this.updateState({lifecycle:"openPopupDetected"}),this.config.enablePrehide&&!this.state.prehideOn&&this.prehideElements(),this.foundCmp=e,"optOut"===this.config.autoAction?await this.doOptOut():"optIn"===this.config.autoAction?await this.doOptIn():(this.config.logs.lifecycle&&console.log("waiting for opt-out signal...",location.href),!0)}async doOptOut(){const e=this.config.logs;let t;return this.updateState({lifecycle:"runningOptOut"}),this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: opt out on ${window.location.href}`),t=await this.foundCmp.optOut(),e.lifecycle&&console.log(`${this.foundCmp.name}: opt out result ${t}`)):(e.errors&&console.log("no CMP to opt out"),t=!1),this.config.enablePrehide&&this.undoPrehide(),this.sendContentMessage({type:"optOutResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,scheduleSelfTest:this.foundCmp&&this.foundCmp.hasSelfTest,url:location.href}),t&&!this.foundCmp.isIntermediate?(this.sendContentMessage({type:"autoconsentDone",cmp:this.foundCmp.name,isCosmetic:this.foundCmp.isCosmetic,url:location.href}),this.updateState({lifecycle:"done"})):this.updateState({lifecycle:t?"optOutSucceeded":"optOutFailed"}),t}async doOptIn(){const e=this.config.logs;let t;return this.updateState({lifecycle:"runningOptIn"}),this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: opt in on ${window.location.href}`),t=await this.foundCmp.optIn(),e.lifecycle&&console.log(`${this.foundCmp.name}: opt in result ${t}`)):(e.errors&&console.log("no CMP to opt in"),t=!1),this.config.enablePrehide&&this.undoPrehide(),this.sendContentMessage({type:"optInResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,scheduleSelfTest:!1,url:location.href}),t&&!this.foundCmp.isIntermediate?(this.sendContentMessage({type:"autoconsentDone",cmp:this.foundCmp.name,isCosmetic:this.foundCmp.isCosmetic,url:location.href}),this.updateState({lifecycle:"done"})):this.updateState({lifecycle:t?"optInSucceeded":"optInFailed"}),t}async doSelfTest(){const e=this.config.logs;let t;return this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: self-test on ${window.location.href}`),t=await this.foundCmp.test()):(e.errors&&console.log("no CMP to self test"),t=!1),this.sendContentMessage({type:"selfTestResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,url:location.href}),this.updateState({selfTest:t}),t}async waitForPopup(e,t=5,o=500){const c=this.config.logs;c.lifecycle&&console.log("checking if popup is open...",e.name);const i=await e.detectPopup().catch((t=>(c.errors&&console.warn(`error detecting popup for ${e.name}`,t),!1)));return!i&&t>0?(await this.domActions.wait(o),this.waitForPopup(e,t-1,o)):(c.lifecycle&&console.log(e.name,"popup is "+(i?"open":"not open")),i)}prehideElements(){const e=this.config.logs,t=this.rules.reduce(((e,t)=>t.prehideSelectors?[...e,...t.prehideSelectors]:e),["#didomi-popup,.didomi-popup-container,.didomi-popup-notice,.didomi-consent-popup-preferences,#didomi-notice,.didomi-popup-backdrop,.didomi-screen-medium"]);return this.updateState({prehideOn:!0}),setTimeout((()=>{this.config.enablePrehide&&this.state.prehideOn&&!["runningOptOut","runningOptIn"].includes(this.state.lifecycle)&&(e.lifecycle&&console.log("Process is taking too long, unhiding elements"),this.undoPrehide())}),this.config.prehideTimeout||2e3),this.domActions.prehide(t.join(","))}undoPrehide(){return this.updateState({prehideOn:!1}),this.domActions.undoPrehide()}updateState(e){Object.assign(this.state,e),this.sendContentMessage({type:"report",instanceId:this.id,url:window.location.href,mainFrame:window.top===window.self,state:this.state})}async receiveMessageCallback(e){const t=this.config?.logs;switch(t?.messages&&console.log("received from background",e,window.location.href),e.type){case"initResp":this.initialize(e.config,e.rules);break;case"optIn":await this.doOptIn();break;case"optOut":await this.doOptOut();break;case"selfTest":await this.doSelfTest();break;case"evalResp":!function(e,t){const o=a.pending.get(e);o?(a.pending.delete(e),o.timer&&window.clearTimeout(o.timer),o.resolve(t)):console.warn("no eval #",e)}(e.id,e.result)}}}((e=>{window.webkit.messageHandlers[e.type]&&window.webkit.messageHandlers[e.type].postMessage(e).then((e=>{A.receiveMessageCallback(e)}))}),null,f);window.autoconsentMessageCallback=e=>{A.receiveMessageCallback(e)}}(); diff --git a/package-lock.json b/package-lock.json index 597b71e54c..a506b40d5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "macos-browser", "version": "1.0.0", "dependencies": { - "@duckduckgo/autoconsent": "^10.2.0" + "@duckduckgo/autoconsent": "^10.3.0" }, "devDependencies": { "@rollup/plugin-json": "^4.1.0", @@ -53,9 +53,9 @@ } }, "node_modules/@duckduckgo/autoconsent": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/@duckduckgo/autoconsent/-/autoconsent-10.2.0.tgz", - "integrity": "sha512-Q4sSGrvA5nWl5auJzttPQu1t25ff9N8Xj/UYglNKNqcnMAx/KxAIP5KbAFgf7JBru+q9Dq7muaEEB4FPU31fEw==" + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@duckduckgo/autoconsent/-/autoconsent-10.3.0.tgz", + "integrity": "sha512-dUf37qkaYDuXEytU9mNNLGw28S1t1M1dFnvMHZDV9BpINVJeAl1ye7CmlABuGlDs6URrp2ZLZ5IxcKQhQglYcw==" }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.2", diff --git a/package.json b/package.json index 2fb1f7eb4e..ee09d66203 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,6 @@ "rollup-plugin-terser": "^7.0.2" }, "dependencies": { - "@duckduckgo/autoconsent": "^10.2.0" + "@duckduckgo/autoconsent": "^10.3.0" } } From 67c9d66e68e911cb80fcd368af55c5389180af5f Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Mon, 18 Mar 2024 00:29:02 +0100 Subject: [PATCH 26/51] Improves VPN pixel info and adds tests (#2434) Task/Issue URL: https://app.asana.com/0/0/1206857816167860/f iOS: https://github.com/duckduckgo/iOS/pull/2604 BSK: https://github.com/duckduckgo/BrowserServicesKit/pull/732 ## Description 1. Makes `NetworkProtectionPixelEvent` implement `PixelKitEventV2` to make it easier for me to add automated tests. 2. Ensures all our error pixels contain both error and underlying error information. 3. Adds automated to validate that all pixels in `NetworkProtectionPixelEvent` send the right information. --- DuckDuckGo.xcodeproj/project.pbxproj | 8 +- .../xcshareddata/swiftpm/Package.resolved | 8 +- .../DuckDuckGo Privacy Browser.xcscheme | 3 + .../NetworkProtectionPixelEvent.swift | 210 +++++++---- .../EventMapping+NetworkProtectionError.swift | 8 +- .../NetworkProtectionTunnelController.swift | 2 +- .../MacPacketTunnelProvider.swift | 23 +- .../DataBrokerProtection/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- .../PixelKit/PixelKit+Parameters.swift | 2 +- .../PixelFireExpectations.swift | 28 +- .../XCTestCase+PixelKit.swift | 42 +-- LocalPackages/SubscriptionUI/Package.swift | 2 +- .../NetworkProtectionPixelEventTests.swift | 345 ++++++++++++++++++ 14 files changed, 563 insertions(+), 122 deletions(-) create mode 100644 UnitTests/NetworkProtection/NetworkProtectionPixelEventTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index c985be5176..9f4d19a784 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2171,6 +2171,8 @@ 7B00997F2B6508C200FE7C31 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7B00997E2B6508C200FE7C31 /* NetworkProtectionProxy */; }; 7B0099822B65C6B300FE7C31 /* MacTransparentProxyProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0099802B65C6B300FE7C31 /* MacTransparentProxyProvider.swift */; }; 7B0694982B6E980F00FA4DBA /* VPNProxyLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0694972B6E980F00FA4DBA /* VPNProxyLauncher.swift */; }; + 7B09CBA92BA4BE8100CF245B /* NetworkProtectionPixelEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B09CBA72BA4BE7000CF245B /* NetworkProtectionPixelEventTests.swift */; }; + 7B09CBAA2BA4BE8200CF245B /* NetworkProtectionPixelEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B09CBA72BA4BE7000CF245B /* NetworkProtectionPixelEventTests.swift */; }; 7B1459542B7D437200047F2C /* BrowserWindowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0099782B65013800FE7C31 /* BrowserWindowManager.swift */; }; 7B1459552B7D438F00047F2C /* VPNProxyLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0694972B6E980F00FA4DBA /* VPNProxyLauncher.swift */; }; 7B1459572B7D43E500047F2C /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7B1459562B7D43E500047F2C /* NetworkProtectionProxy */; }; @@ -3833,6 +3835,7 @@ 7B0099802B65C6B300FE7C31 /* MacTransparentProxyProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacTransparentProxyProvider.swift; sourceTree = ""; }; 7B05829D2A812AC000AC3F7C /* NetworkProtectionOnboardingMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionOnboardingMenu.swift; sourceTree = ""; }; 7B0694972B6E980F00FA4DBA /* VPNProxyLauncher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNProxyLauncher.swift; sourceTree = ""; }; + 7B09CBA72BA4BE7000CF245B /* NetworkProtectionPixelEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionPixelEventTests.swift; sourceTree = ""; }; 7B1E819B27C8874900FF0E60 /* ContentOverlayPopover.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentOverlayPopover.swift; sourceTree = ""; }; 7B1E819C27C8874900FF0E60 /* ContentOverlay.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = ContentOverlay.storyboard; sourceTree = ""; }; 7B1E819D27C8874900FF0E60 /* ContentOverlayViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentOverlayViewController.swift; sourceTree = ""; }; @@ -6095,6 +6098,7 @@ 4BCF15E62ABB98A20083F6DF /* Resources */, 4BCF15E42ABB98990083F6DF /* NetworkProtectionRemoteMessageTests.swift */, 4BD57C032AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift */, + 7B09CBA72BA4BE7000CF245B /* NetworkProtectionPixelEventTests.swift */, ); path = NetworkProtection; sourceTree = ""; @@ -10702,6 +10706,7 @@ 3706FE78293F661700E42796 /* HistoryCoordinatingMock.swift in Sources */, 3706FE79293F661700E42796 /* AppearancePreferencesTests.swift in Sources */, 3706FE7A293F661700E42796 /* FirePopoverViewModelTests.swift in Sources */, + 7B09CBAA2BA4BE8200CF245B /* NetworkProtectionPixelEventTests.swift in Sources */, 3706FE7B293F661700E42796 /* HistoryStoringMock.swift in Sources */, 562984702AC4610100AC20EB /* SyncPreferencesTests.swift in Sources */, 3706FE7C293F661700E42796 /* LocalBookmarkStoreTests.swift in Sources */, @@ -12509,6 +12514,7 @@ FD23FD2B28816606007F6985 /* AutoconsentMessageProtocolTests.swift in Sources */, 1D77921A28FDC79800BE0210 /* FaviconStoringMock.swift in Sources */, 1D1C36E629FB019C001FA40C /* HistoryTabExtensionTests.swift in Sources */, + 7B09CBA92BA4BE8100CF245B /* NetworkProtectionPixelEventTests.swift in Sources */, B6DA441E2616C84600DD1EC2 /* PixelStoreMock.swift in Sources */, 4B434690285ED7A100177407 /* BookmarksBarViewModelTests.swift in Sources */, B6BBF1702744CDE1004F850E /* CoreDataStoreTests.swift in Sources */, @@ -13749,7 +13755,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 125.0.0; + version = 125.0.1; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 24e42fd976..23b4cce189 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "ac2127a26f75b2aa293f6036bcdd2bc241d09819", - "version" : "125.0.0" + "revision" : "eced8f93c945ff2fa4ff92bdd619514d4eff7131", + "version" : "125.0.1" } }, { @@ -122,8 +122,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-argument-parser", "state" : { - "revision" : "c8ed701b513cf5177118a175d85fbbbcd707ab41", - "version" : "1.3.0" + "revision" : "46989693916f56d1186bd59ac15124caef896560", + "version" : "1.3.1" } }, { diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme index 58557db074..0224eeaec8 100644 --- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme +++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme @@ -120,6 +120,9 @@ + + diff --git a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift index f7e491577b..2f60171c17 100644 --- a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift +++ b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift @@ -22,22 +22,23 @@ import Foundation import PixelKit import NetworkProtection -enum NetworkProtectionPixelEvent: PixelKitEvent { +enum NetworkProtectionPixelEvent: PixelKitEventV2 { + static let vpnErrorDomain = "com.duckduckgo.vpn.errorDomain" case networkProtectionActiveUser case networkProtectionNewUser case networkProtectionControllerStartAttempt case networkProtectionControllerStartSuccess - case networkProtectionControllerStartFailure + case networkProtectionControllerStartFailure(_ error: Error) case networkProtectionTunnelStartAttempt case networkProtectionTunnelStartSuccess - case networkProtectionTunnelStartFailure + case networkProtectionTunnelStartFailure(_ error: Error) case networkProtectionTunnelUpdateAttempt case networkProtectionTunnelUpdateSuccess - case networkProtectionTunnelUpdateFailure + case networkProtectionTunnelUpdateFailure(_ error: Error) case networkProtectionEnableAttemptConnecting case networkProtectionEnableAttemptSuccess @@ -55,23 +56,23 @@ enum NetworkProtectionPixelEvent: PixelKitEvent { case networkProtectionTunnelConfigurationCouldNotGetPeerHostName case networkProtectionTunnelConfigurationCouldNotGetInterfaceAddressRange - case networkProtectionClientFailedToFetchServerList(error: Error?) + case networkProtectionClientFailedToFetchServerList(_ error: Error?) case networkProtectionClientFailedToParseServerListResponse case networkProtectionClientFailedToEncodeRegisterKeyRequest - case networkProtectionClientFailedToFetchRegisteredServers(error: Error?) + case networkProtectionClientFailedToFetchRegisteredServers(_ error: Error?) case networkProtectionClientFailedToParseRegisteredServersResponse case networkProtectionClientFailedToEncodeRedeemRequest case networkProtectionClientInvalidInviteCode - case networkProtectionClientFailedToRedeemInviteCode(error: Error?) - case networkProtectionClientFailedToParseRedeemResponse(error: Error) - case networkProtectionClientFailedToFetchLocations(error: Error?) - case networkProtectionClientFailedToParseLocationsResponse(error: Error?) + case networkProtectionClientFailedToRedeemInviteCode(_ error: Error?) + case networkProtectionClientFailedToParseRedeemResponse(_ error: Error) + case networkProtectionClientFailedToFetchLocations(_ error: Error?) + case networkProtectionClientFailedToParseLocationsResponse(_ error: Error?) case networkProtectionClientInvalidAuthToken case networkProtectionServerListStoreFailedToEncodeServerList case networkProtectionServerListStoreFailedToDecodeServerList - case networkProtectionServerListStoreFailedToWriteServerList(error: Error) - case networkProtectionServerListStoreFailedToReadServerList(error: Error) + case networkProtectionServerListStoreFailedToWriteServerList(_ error: Error) + case networkProtectionServerListStoreFailedToReadServerList(_ error: Error) case networkProtectionKeychainErrorFailedToCastKeychainValueToData(field: String) case networkProtectionKeychainReadError(field: String, status: Int32) @@ -82,14 +83,14 @@ enum NetworkProtectionPixelEvent: PixelKitEvent { case networkProtectionWireguardErrorCannotLocateTunnelFileDescriptor case networkProtectionWireguardErrorInvalidState(reason: String) case networkProtectionWireguardErrorFailedDNSResolution - case networkProtectionWireguardErrorCannotSetNetworkSettings(error: Error) + case networkProtectionWireguardErrorCannotSetNetworkSettings(_ error: Error) case networkProtectionWireguardErrorCannotStartWireguardBackend(code: Int32) case networkProtectionNoAuthTokenFoundError case networkProtectionRekeyAttempt case networkProtectionRekeyCompleted - case networkProtectionRekeyFailure + case networkProtectionRekeyFailure(_ error: Error) case networkProtectionSystemExtensionActivationFailure @@ -102,169 +103,169 @@ enum NetworkProtectionPixelEvent: PixelKitEvent { switch self { case .networkProtectionActiveUser: - return "m_mac_netp_daily_active" + return "netp_daily_active" case .networkProtectionNewUser: - return "m_mac_netp_daily_active_u" + return "netp_daily_active_u" case .networkProtectionControllerStartAttempt: - return "m_mac_netp_controller_start_attempt" + return "netp_controller_start_attempt" case .networkProtectionControllerStartSuccess: - return "m_mac_netp_controller_start_success" + return "netp_controller_start_success" case .networkProtectionControllerStartFailure: - return "m_mac_netp_controller_start_failure" + return "netp_controller_start_failure" case .networkProtectionTunnelStartAttempt: - return "m_mac_netp_tunnel_start_attempt" + return "netp_tunnel_start_attempt" case .networkProtectionTunnelStartSuccess: - return "m_mac_netp_tunnel_start_success" + return "netp_tunnel_start_success" case .networkProtectionTunnelStartFailure: - return "m_mac_netp_tunnel_start_failure" + return "netp_tunnel_start_failure" case .networkProtectionTunnelUpdateAttempt: - return "m_mac_netp_tunnel_update_attempt" + return "netp_tunnel_update_attempt" case .networkProtectionTunnelUpdateSuccess: - return "m_mac_netp_tunnel_update_success" + return "netp_tunnel_update_success" case .networkProtectionTunnelUpdateFailure: - return "m_mac_netp_tunnel_update_failure" + return "netp_tunnel_update_failure" case .networkProtectionEnableAttemptConnecting: - return "m_mac_netp_ev_enable_attempt" + return "netp_ev_enable_attempt" case .networkProtectionEnableAttemptSuccess: - return "m_mac_netp_ev_enable_attempt_success" + return "netp_ev_enable_attempt_success" case .networkProtectionEnableAttemptFailure: - return "m_mac_netp_ev_enable_attempt_failure" + return "netp_ev_enable_attempt_failure" case .networkProtectionTunnelFailureDetected: - return "m_mac_netp_ev_tunnel_failure" + return "netp_ev_tunnel_failure" case .networkProtectionTunnelFailureRecovered: - return "m_mac_netp_ev_tunnel_failure_recovered" + return "netp_ev_tunnel_failure_recovered" case .networkProtectionLatency(let quality): - return "m_mac_netp_ev_\(quality.rawValue)_latency" + return "netp_ev_\(quality.rawValue)_latency" case .networkProtectionLatencyError: - return "m_mac_netp_ev_latency_error" + return "netp_ev_latency_error" case .networkProtectionTunnelConfigurationNoServerRegistrationInfo: - return "m_mac_netp_tunnel_config_error_no_server_registration_info" + return "netp_tunnel_config_error_no_server_registration_info" case .networkProtectionTunnelConfigurationCouldNotSelectClosestServer: - return "m_mac_netp_tunnel_config_error_could_not_select_closest_server" + return "netp_tunnel_config_error_could_not_select_closest_server" case .networkProtectionTunnelConfigurationCouldNotGetPeerPublicKey: - return "m_mac_netp_tunnel_config_error_could_not_get_peer_public_key" + return "netp_tunnel_config_error_could_not_get_peer_public_key" case .networkProtectionTunnelConfigurationCouldNotGetPeerHostName: - return "m_mac_netp_tunnel_config_error_could_not_get_peer_host_name" + return "netp_tunnel_config_error_could_not_get_peer_host_name" case .networkProtectionTunnelConfigurationCouldNotGetInterfaceAddressRange: - return "m_mac_netp_tunnel_config_error_could_not_get_interface_address_range" + return "netp_tunnel_config_error_could_not_get_interface_address_range" case .networkProtectionClientFailedToFetchServerList: - return "m_mac_netp_backend_api_error_failed_to_fetch_server_list" + return "netp_backend_api_error_failed_to_fetch_server_list" case .networkProtectionClientFailedToParseServerListResponse: - return "m_mac_netp_backend_api_error_parsing_server_list_response_failed" + return "netp_backend_api_error_parsing_server_list_response_failed" case .networkProtectionClientFailedToEncodeRegisterKeyRequest: - return "m_mac_netp_backend_api_error_encoding_register_request_body_failed" + return "netp_backend_api_error_encoding_register_request_body_failed" case .networkProtectionClientFailedToFetchRegisteredServers: - return "m_mac_netp_backend_api_error_failed_to_fetch_registered_servers" + return "netp_backend_api_error_failed_to_fetch_registered_servers" case .networkProtectionClientFailedToParseRegisteredServersResponse: - return "m_mac_netp_backend_api_error_parsing_device_registration_response_failed" + return "netp_backend_api_error_parsing_device_registration_response_failed" case .networkProtectionClientFailedToEncodeRedeemRequest: - return "m_mac_netp_backend_api_error_encoding_redeem_request_body_failed" + return "netp_backend_api_error_encoding_redeem_request_body_failed" case .networkProtectionClientInvalidInviteCode: - return "m_mac_netp_backend_api_error_invalid_invite_code" + return "netp_backend_api_error_invalid_invite_code" case .networkProtectionClientFailedToRedeemInviteCode: - return "m_mac_netp_backend_api_error_failed_to_redeem_invite_code" + return "netp_backend_api_error_failed_to_redeem_invite_code" case .networkProtectionClientFailedToParseRedeemResponse: - return "m_mac_netp_backend_api_error_parsing_redeem_response_failed" + return "netp_backend_api_error_parsing_redeem_response_failed" case .networkProtectionClientFailedToFetchLocations: - return "m_mac_netp_backend_api_error_failed_to_fetch_location_list" + return "netp_backend_api_error_failed_to_fetch_location_list" case .networkProtectionClientFailedToParseLocationsResponse: - return "m_mac_netp_backend_api_error_parsing_location_list_response_failed" + return "netp_backend_api_error_parsing_location_list_response_failed" case .networkProtectionClientInvalidAuthToken: - return "m_mac_netp_backend_api_error_invalid_auth_token" + return "netp_backend_api_error_invalid_auth_token" case .networkProtectionServerListStoreFailedToEncodeServerList: - return "m_mac_netp_storage_error_failed_to_encode_server_list" + return "netp_storage_error_failed_to_encode_server_list" case .networkProtectionServerListStoreFailedToDecodeServerList: - return "m_mac_netp_storage_error_failed_to_decode_server_list" + return "netp_storage_error_failed_to_decode_server_list" case .networkProtectionServerListStoreFailedToWriteServerList: - return "m_mac_netp_storage_error_server_list_file_system_write_failed" + return "netp_storage_error_server_list_file_system_write_failed" case .networkProtectionServerListStoreFailedToReadServerList: - return "m_mac_netp_storage_error_server_list_file_system_read_failed" + return "netp_storage_error_server_list_file_system_read_failed" case .networkProtectionKeychainErrorFailedToCastKeychainValueToData: - return "m_mac_netp_keychain_error_failed_to_cast_keychain_value_to_data" + return "netp_keychain_error_failed_to_cast_keychain_value_to_data" case .networkProtectionKeychainReadError: - return "m_mac_netp_keychain_error_read_failed" + return "netp_keychain_error_read_failed" case .networkProtectionKeychainWriteError: - return "m_mac_netp_keychain_error_write_failed" + return "netp_keychain_error_write_failed" case .networkProtectionKeychainUpdateError: - return "m_mac_netp_keychain_error_update_failed" + return "netp_keychain_error_update_failed" case .networkProtectionKeychainDeleteError: - return "m_mac_netp_keychain_error_delete_failed" + return "netp_keychain_error_delete_failed" case .networkProtectionWireguardErrorCannotLocateTunnelFileDescriptor: - return "m_mac_netp_wireguard_error_cannot_locate_tunnel_file_descriptor" + return "netp_wireguard_error_cannot_locate_tunnel_file_descriptor" case .networkProtectionWireguardErrorInvalidState: - return "m_mac_netp_wireguard_error_invalid_state" + return "netp_wireguard_error_invalid_state" case .networkProtectionWireguardErrorFailedDNSResolution: - return "m_mac_netp_wireguard_error_failed_dns_resolution" + return "netp_wireguard_error_failed_dns_resolution" case .networkProtectionWireguardErrorCannotSetNetworkSettings: - return "m_mac_netp_wireguard_error_cannot_set_network_settings" + return "netp_wireguard_error_cannot_set_network_settings" case .networkProtectionWireguardErrorCannotStartWireguardBackend: - return "m_mac_netp_wireguard_error_cannot_start_wireguard_backend" + return "netp_wireguard_error_cannot_start_wireguard_backend" case .networkProtectionNoAuthTokenFoundError: - return "m_mac_netp_no_auth_token_found_error" + return "netp_no_auth_token_found_error" case .networkProtectionRekeyAttempt: - return "m_mac_netp_rekey_attempt" + return "netp_rekey_attempt" case .networkProtectionRekeyCompleted: - return "m_mac_netp_rekey_completed" + return "netp_rekey_completed" case .networkProtectionRekeyFailure: - return "m_mac_netp_rekey_failure" + return "netp_rekey_failure" case .networkProtectionSystemExtensionActivationFailure: - return "m_mac_netp_system_extension_activation_failure" + return "netp_system_extension_activation_failure" case .networkProtectionUnhandledError: - return "m_mac_netp_unhandled_error" + return "netp_unhandled_error" } } @@ -308,13 +309,13 @@ enum NetworkProtectionPixelEvent: PixelKitEvent { case .networkProtectionClientFailedToFetchRegisteredServers(let error): return error?.pixelParameters - case .networkProtectionClientFailedToRedeemInviteCode(error: let error): + case .networkProtectionClientFailedToRedeemInviteCode(let error): return error?.pixelParameters - case .networkProtectionClientFailedToFetchLocations(error: let error): + case .networkProtectionClientFailedToFetchLocations(let error): return error?.pixelParameters - case .networkProtectionClientFailedToParseLocationsResponse(error: let error): + case .networkProtectionClientFailedToParseLocationsResponse(let error): return error?.pixelParameters case .networkProtectionUnhandledError(let function, let line, let error): @@ -323,7 +324,7 @@ enum NetworkProtectionPixelEvent: PixelKitEvent { parameters[PixelKit.Parameters.line] = String(line) return parameters - case .networkProtectionWireguardErrorCannotSetNetworkSettings(error: let error): + case .networkProtectionWireguardErrorCannotSetNetworkSettings(let error): return error.pixelParameters case .networkProtectionWireguardErrorCannotStartWireguardBackend(code: let code): @@ -379,6 +380,69 @@ enum NetworkProtectionPixelEvent: PixelKitEvent { return nil } } + + var error: (any Error)? { + switch self { + case .networkProtectionActiveUser, + .networkProtectionNewUser, + .networkProtectionControllerStartAttempt, + .networkProtectionControllerStartSuccess, + .networkProtectionTunnelStartAttempt, + .networkProtectionTunnelStartSuccess, + .networkProtectionTunnelUpdateAttempt, + .networkProtectionTunnelUpdateSuccess, + .networkProtectionEnableAttemptConnecting, + .networkProtectionEnableAttemptSuccess, + .networkProtectionEnableAttemptFailure, + .networkProtectionTunnelFailureDetected, + .networkProtectionTunnelFailureRecovered, + .networkProtectionLatencyError, + .networkProtectionLatency, + .networkProtectionTunnelConfigurationNoServerRegistrationInfo, + .networkProtectionTunnelConfigurationCouldNotGetPeerPublicKey, + .networkProtectionTunnelConfigurationCouldNotGetPeerHostName, + .networkProtectionTunnelConfigurationCouldNotGetInterfaceAddressRange, + .networkProtectionClientFailedToParseServerListResponse, + .networkProtectionClientFailedToEncodeRegisterKeyRequest, + .networkProtectionClientFailedToParseRegisteredServersResponse, + .networkProtectionTunnelConfigurationCouldNotSelectClosestServer, + .networkProtectionClientFailedToEncodeRedeemRequest, + .networkProtectionClientInvalidInviteCode, + .networkProtectionClientInvalidAuthToken, + .networkProtectionServerListStoreFailedToEncodeServerList, + .networkProtectionServerListStoreFailedToDecodeServerList, + .networkProtectionKeychainErrorFailedToCastKeychainValueToData, + .networkProtectionKeychainReadError, + .networkProtectionKeychainWriteError, + .networkProtectionKeychainUpdateError, + .networkProtectionKeychainDeleteError, + .networkProtectionWireguardErrorCannotLocateTunnelFileDescriptor, + .networkProtectionWireguardErrorInvalidState, + .networkProtectionWireguardErrorFailedDNSResolution, + .networkProtectionWireguardErrorCannotStartWireguardBackend, + .networkProtectionNoAuthTokenFoundError, + .networkProtectionRekeyAttempt, + .networkProtectionRekeyCompleted, + .networkProtectionSystemExtensionActivationFailure: + return nil + case .networkProtectionClientFailedToRedeemInviteCode(let error), + .networkProtectionClientFailedToFetchLocations(let error), + .networkProtectionClientFailedToParseLocationsResponse(let error), + .networkProtectionClientFailedToFetchServerList(let error), + .networkProtectionClientFailedToFetchRegisteredServers(let error): + return error + case .networkProtectionControllerStartFailure(let error), + .networkProtectionTunnelStartFailure(let error), + .networkProtectionTunnelUpdateFailure(let error), + .networkProtectionClientFailedToParseRedeemResponse(let error), + .networkProtectionServerListStoreFailedToWriteServerList(let error), + .networkProtectionServerListStoreFailedToReadServerList(let error), + .networkProtectionWireguardErrorCannotSetNetworkSettings(let error), + .networkProtectionRekeyFailure(let error), + .networkProtectionUnhandledError(_, _, let error): + return error + } + } } #endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/EventMapping+NetworkProtectionError.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/EventMapping+NetworkProtectionError.swift index b363c3d843..3a42455d0f 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/EventMapping+NetworkProtectionError.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/EventMapping+NetworkProtectionError.swift @@ -36,10 +36,10 @@ extension EventMapping where Event == NetworkProtectionError { domainEvent = .networkProtectionClientInvalidInviteCode frequency = .standard case .failedToRedeemInviteCode(let error): - domainEvent = .networkProtectionClientFailedToRedeemInviteCode(error: error) + domainEvent = .networkProtectionClientFailedToRedeemInviteCode(error) frequency = .standard case .failedToParseRedeemResponse(let error): - domainEvent = .networkProtectionClientFailedToParseRedeemResponse(error: error) + domainEvent = .networkProtectionClientFailedToParseRedeemResponse(error) frequency = .standard case .invalidAuthToken: domainEvent = .networkProtectionClientInvalidAuthToken @@ -63,10 +63,10 @@ extension EventMapping where Event == NetworkProtectionError { domainEvent = .networkProtectionNoAuthTokenFoundError frequency = .standard case .failedToFetchLocationList(let error): - domainEvent = .networkProtectionClientFailedToFetchLocations(error: error) + domainEvent = .networkProtectionClientFailedToFetchLocations(error) frequency = .dailyAndContinuous case .failedToParseLocationListResponse(let error): - domainEvent = .networkProtectionClientFailedToParseLocationsResponse(error: error) + domainEvent = .networkProtectionClientFailedToParseLocationsResponse(error) frequency = .dailyAndContinuous case .noServerRegistrationInfo, .couldNotSelectClosestServer, diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift index 0ea6d48740..4132457ca2 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift @@ -519,7 +519,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr } } catch { PixelKit.fire( - NetworkProtectionPixelEvent.networkProtectionControllerStartFailure, frequency: .dailyAndContinuous, withError: error, includeAppVersionParameter: true + NetworkProtectionPixelEvent.networkProtectionControllerStartFailure(error), frequency: .dailyAndContinuous, includeAppVersionParameter: true ) await stop() diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift index d7d6de4627..da8d81fc24 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift @@ -59,13 +59,13 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { case .couldNotGetInterfaceAddressRange: domainEvent = .networkProtectionTunnelConfigurationCouldNotGetInterfaceAddressRange case .failedToFetchServerList(let eventError): - domainEvent = .networkProtectionClientFailedToFetchServerList(error: eventError) + domainEvent = .networkProtectionClientFailedToFetchServerList(eventError) case .failedToParseServerListResponse: domainEvent = .networkProtectionClientFailedToParseServerListResponse case .failedToEncodeRegisterKeyRequest: domainEvent = .networkProtectionClientFailedToEncodeRegisterKeyRequest case .failedToFetchRegisteredServers(let eventError): - domainEvent = .networkProtectionClientFailedToFetchRegisteredServers(error: eventError) + domainEvent = .networkProtectionClientFailedToFetchRegisteredServers(eventError) case .failedToParseRegisteredServersResponse: domainEvent = .networkProtectionClientFailedToParseRegisteredServersResponse case .failedToEncodeRedeemRequest: @@ -73,9 +73,9 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { case .invalidInviteCode: domainEvent = .networkProtectionClientInvalidInviteCode case .failedToRedeemInviteCode(let error): - domainEvent = .networkProtectionClientFailedToRedeemInviteCode(error: error) + domainEvent = .networkProtectionClientFailedToRedeemInviteCode(error) case .failedToParseRedeemResponse(let error): - domainEvent = .networkProtectionClientFailedToParseRedeemResponse(error: error) + domainEvent = .networkProtectionClientFailedToParseRedeemResponse(error) case .invalidAuthToken: domainEvent = .networkProtectionClientInvalidAuthToken case .serverListInconsistency: @@ -85,13 +85,13 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { case .failedToDecodeServerList: domainEvent = .networkProtectionServerListStoreFailedToDecodeServerList case .failedToWriteServerList(let eventError): - domainEvent = .networkProtectionServerListStoreFailedToWriteServerList(error: eventError) + domainEvent = .networkProtectionServerListStoreFailedToWriteServerList(eventError) case .noServerListFound: return case .couldNotCreateServerListDirectory: return case .failedToReadServerList(let eventError): - domainEvent = .networkProtectionServerListStoreFailedToReadServerList(error: eventError) + domainEvent = .networkProtectionServerListStoreFailedToReadServerList(eventError) case .failedToCastKeychainValueToData(let field): domainEvent = .networkProtectionKeychainErrorFailedToCastKeychainValueToData(field: field) case .keychainReadError(let field, let status): @@ -109,7 +109,7 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { case .wireGuardDnsResolution: domainEvent = .networkProtectionWireguardErrorFailedDNSResolution case .wireGuardSetNetworkSettings(let error): - domainEvent = .networkProtectionWireguardErrorCannotSetNetworkSettings(error: error) + domainEvent = .networkProtectionWireguardErrorCannotSetNetworkSettings(error) case .startWireGuardBackend(let code): domainEvent = .networkProtectionWireguardErrorCannotStartWireguardBackend(code: code) case .noAuthTokenFound: @@ -206,9 +206,8 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { includeAppVersionParameter: true) case .failure(let error): PixelKit.fire( - NetworkProtectionPixelEvent.networkProtectionRekeyFailure, + NetworkProtectionPixelEvent.networkProtectionRekeyFailure(error), frequency: .dailyAndContinuous, - withError: error, includeAppVersionParameter: true) case .success: PixelKit.fire( @@ -225,9 +224,8 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { includeAppVersionParameter: true) case .failure(let error): PixelKit.fire( - NetworkProtectionPixelEvent.networkProtectionTunnelStartFailure, + NetworkProtectionPixelEvent.networkProtectionTunnelStartFailure(error), frequency: .dailyAndContinuous, - withError: error, includeAppVersionParameter: true) case .success: PixelKit.fire( @@ -244,9 +242,8 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { includeAppVersionParameter: true) case .failure(let error): PixelKit.fire( - NetworkProtectionPixelEvent.networkProtectionTunnelUpdateFailure, + NetworkProtectionPixelEvent.networkProtectionTunnelUpdateFailure(error), frequency: .dailyAndContinuous, - withError: error, includeAppVersionParameter: true) case .success: PixelKit.fire( diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index d428da60b2..20e8b1f4b2 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -29,7 +29,7 @@ let package = Package( targets: ["DataBrokerProtection"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "125.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "125.0.1"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 39f13d9fe0..baddd89369 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "125.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "125.0.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems"), diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift index ea08352d29..e368d38db5 100644 --- a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift +++ b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift @@ -20,7 +20,7 @@ import Foundation public extension PixelKit { - enum Parameters { + enum Parameters: Hashable { public static let duration = "duration" public static let test = "test" public static let appVersion = "appVersion" diff --git a/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/PixelFireExpectations.swift b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/PixelFireExpectations.swift index 067eee091e..db56d6664b 100644 --- a/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/PixelFireExpectations.swift +++ b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/PixelFireExpectations.swift @@ -17,6 +17,7 @@ // import Foundation +import PixelKit /// Structure containing information about a pixel fire event. /// @@ -27,10 +28,35 @@ public struct PixelFireExpectations { let pixelName: String var error: Error? var underlyingError: Error? + var customFields: [String: String]? - public init(pixelName: String, error: Error? = nil, underlyingError: Error? = nil) { + /// Convenience initializer for cleaner semantics + /// + public static func expect(pixelName: String, error: Error? = nil, underlyingError: Error? = nil, customFields: [String: String]? = nil) -> PixelFireExpectations { + + .init(pixelName: pixelName, error: error, underlyingError: underlyingError, customFields: customFields) + } + + public init(pixelName: String, error: Error? = nil, underlyingError: Error? = nil, customFields: [String: String]? = nil) { self.pixelName = pixelName self.error = error self.underlyingError = underlyingError + self.customFields = customFields + } + + public var parameters: [String: String] { + var parameters = customFields ?? [String: String]() + + if let nsError = error as? NSError { + parameters[PixelKit.Parameters.errorCode] = String(nsError.code) + parameters[PixelKit.Parameters.errorDomain] = nsError.domain + } + + if let nsUnderlyingError = underlyingError as? NSError { + parameters[PixelKit.Parameters.underlyingErrorCode] = String(nsUnderlyingError.code) + parameters[PixelKit.Parameters.underlyingErrorDomain] = nsUnderlyingError.domain + } + + return parameters } } diff --git a/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift index 325b7414b3..6d742bd532 100644 --- a/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift +++ b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift @@ -66,7 +66,13 @@ public extension XCTestCase { #endif } - func expectedParameters(for event: PixelKitEventV2) -> [String: String] { + /// These parameters are known to be expected just based on the event definition. + /// + /// They're not a complete list of parameters for the event, as the fire call may contain extra information + /// that results in additional parameters. Ideally we want most (if not all) that information to eventually + /// make part of the pixel definition. + /// + func knownExpectedParameters(for event: PixelKitEventV2) -> [String: String] { var expectedParameters = [String: String]() if let error = event.error { @@ -74,10 +80,9 @@ public extension XCTestCase { expectedParameters[PixelKit.Parameters.errorCode] = "\(nsError.code)" expectedParameters[PixelKit.Parameters.errorDomain] = nsError.domain - if let underlyingError = (error as? PixelKitEventErrorDetails)?.underlyingError { - let underlyingNSError = underlyingError as NSError - expectedParameters[PixelKit.Parameters.underlyingErrorCode] = "\(underlyingNSError.code)" - expectedParameters[PixelKit.Parameters.underlyingErrorDomain] = underlyingNSError.domain + if let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] as? NSError { + expectedParameters[PixelKit.Parameters.underlyingErrorCode] = "\(underlyingError.code)" + expectedParameters[PixelKit.Parameters.underlyingErrorDomain] = underlyingError.domain } } @@ -92,14 +97,18 @@ public extension XCTestCase { // MARK: - Pixel Firing Expectations + func fire(_ event: PixelKitEventV2, and expectations: PixelFireExpectations, file: StaticString, line: UInt) { + verifyThat(event, meets: expectations, file: file, line: line) + } + /// Provides some snapshot of a fired pixel so that external libraries can validate all the expected info is included. /// /// This method also checks that there is internal consistency in the expected fields. /// func verifyThat(_ event: PixelKitEventV2, meets expectations: PixelFireExpectations, file: StaticString, line: UInt) { - let expectedPixelName = Self.pixelPlatformPrefix + event.name - let expectedParameters = expectedParameters(for: event) + let expectedPixelName = event.name.hasPrefix(Self.pixelPlatformPrefix) ? event.name : Self.pixelPlatformPrefix + event.name + let knownExpectedParameters = knownExpectedParameters(for: event) let callbackExecutedExpectation = expectation(description: "The PixelKit callback has been executed") PixelKit.setUp(dryRun: false, @@ -115,23 +124,14 @@ public extension XCTestCase { // Internal validations XCTAssertEqual(firedPixelName, expectedPixelName, file: file, line: line) - XCTAssertEqual(firedParameters, expectedParameters, file: file, line: line) - // Expectations + XCTAssertTrue(knownExpectedParameters.allSatisfy { (key, value) in + firedParameters[key] == value + }) + // Expectations XCTAssertEqual(firedPixelName, expectations.pixelName) - - if let error = expectations.error { - let nsError = error as NSError - XCTAssertEqual(firedParameters[PixelKit.Parameters.errorCode], String(nsError.code), file: file, line: line) - XCTAssertEqual(firedParameters[PixelKit.Parameters.errorDomain], nsError.domain, file: file, line: line) - } - - if let underlyingError = expectations.underlyingError { - let nsError = underlyingError as NSError - XCTAssertEqual(firedParameters[PixelKit.Parameters.underlyingErrorCode], String(nsError.code), file: file, line: line) - XCTAssertEqual(firedParameters[PixelKit.Parameters.underlyingErrorDomain], nsError.domain, file: file, line: line) - } + XCTAssertEqual(firedParameters, expectations.parameters) completion(true, nil) } diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 6ffb49191b..ef458f05c7 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "125.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "125.0.1"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/UnitTests/NetworkProtection/NetworkProtectionPixelEventTests.swift b/UnitTests/NetworkProtection/NetworkProtectionPixelEventTests.swift new file mode 100644 index 0000000000..01a2cc72b1 --- /dev/null +++ b/UnitTests/NetworkProtection/NetworkProtectionPixelEventTests.swift @@ -0,0 +1,345 @@ +// +// NetworkProtectionPixelEventTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#if NETWORK_PROTECTION + +import NetworkProtection +import PixelKit +import PixelKitTestingUtilities +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +final class NetworkProtectionPixelEventTests: XCTestCase { + + private enum TestError: CustomNSError { + case testError + case underlyingError + + /// The domain of the error. + static var errorDomain: String { + "testDomain" + } + + /// The error code within the given domain. + var errorCode: Int { + switch self { + case .testError: return 1 + case .underlyingError: return 2 + } + } + + /// The user-info dictionary. + var errorUserInfo: [String: Any] { + switch self { + case .testError: + return [NSUnderlyingErrorKey: TestError.underlyingError] + case .underlyingError: + return [:] + } + } + } + + // MARK: - Test Firing Pixels + + /// This test verifies validates expectations when firing `NetworkProtectionPixelEvent`. + /// + /// This test verifies a few different things: + /// - That the pixel name is not changed by mistake. + /// - That when the pixel is fired its name and parameters are exactly what's expected. + /// + func testVPNPixelFireExpectations() { + fire(NetworkProtectionPixelEvent.networkProtectionActiveUser, + and: .expect(pixelName: "m_mac_netp_daily_active"), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionNewUser, + and: .expect(pixelName: "m_mac_netp_daily_active_u"), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionControllerStartAttempt, + and: .expect(pixelName: "m_mac_netp_controller_start_attempt"), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionControllerStartFailure(TestError.testError), + and: .expect(pixelName: "m_mac_netp_controller_start_failure", + error: TestError.testError, + underlyingError: TestError.underlyingError), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionControllerStartSuccess, + and: .expect(pixelName: "m_mac_netp_controller_start_success"), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionTunnelStartAttempt, + and: .expect(pixelName: "m_mac_netp_tunnel_start_attempt"), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionTunnelStartFailure(TestError.testError), + and: .expect(pixelName: "m_mac_netp_tunnel_start_failure", + error: TestError.testError, + underlyingError: TestError.underlyingError), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionTunnelStartSuccess, + and: .expect(pixelName: "m_mac_netp_tunnel_start_success"), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionTunnelUpdateAttempt, + and: .expect(pixelName: "m_mac_netp_tunnel_update_attempt"), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionTunnelUpdateFailure(TestError.testError), + and: .expect(pixelName: "m_mac_netp_tunnel_update_failure", + error: TestError.testError, + underlyingError: TestError.underlyingError), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionTunnelUpdateSuccess, + and: .expect(pixelName: "m_mac_netp_tunnel_update_success"), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionEnableAttemptConnecting, + and: .expect(pixelName: "m_mac_netp_ev_enable_attempt"), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionEnableAttemptSuccess, + and: .expect(pixelName: "m_mac_netp_ev_enable_attempt_success"), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionEnableAttemptFailure, + and: .expect(pixelName: "m_mac_netp_ev_enable_attempt_failure"), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionTunnelFailureDetected, + and: .expect(pixelName: "m_mac_netp_ev_tunnel_failure"), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionTunnelFailureRecovered, + and: .expect(pixelName: "m_mac_netp_ev_tunnel_failure_recovered"), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionLatency(quality: .excellent), + and: .expect(pixelName: "m_mac_netp_ev_\(NetworkProtectionLatencyMonitor.ConnectionQuality.excellent.rawValue)_latency"), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionLatencyError, + and: .expect(pixelName: "m_mac_netp_ev_latency_error"), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionTunnelConfigurationNoServerRegistrationInfo, + and: .expect(pixelName: "m_mac_netp_tunnel_config_error_no_server_registration_info"), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionTunnelConfigurationCouldNotSelectClosestServer, + and: .expect(pixelName: "m_mac_netp_tunnel_config_error_could_not_select_closest_server"), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionTunnelConfigurationCouldNotGetPeerPublicKey, + and: .expect(pixelName: "m_mac_netp_tunnel_config_error_could_not_get_peer_public_key"), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionTunnelConfigurationCouldNotGetPeerHostName, + and: .expect(pixelName: "m_mac_netp_tunnel_config_error_could_not_get_peer_host_name"), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionTunnelConfigurationCouldNotGetInterfaceAddressRange, + and: .expect(pixelName: "m_mac_netp_tunnel_config_error_could_not_get_interface_address_range"), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionClientFailedToFetchServerList(TestError.testError), + and: .expect(pixelName: "m_mac_netp_backend_api_error_failed_to_fetch_server_list", + error: TestError.testError, + underlyingError: TestError.underlyingError), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionClientFailedToParseServerListResponse, + and: .expect(pixelName: "m_mac_netp_backend_api_error_parsing_server_list_response_failed"), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionClientFailedToEncodeRegisterKeyRequest, + and: .expect(pixelName: "m_mac_netp_backend_api_error_encoding_register_request_body_failed"), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionClientFailedToFetchRegisteredServers(TestError.testError), + and: .expect(pixelName: "m_mac_netp_backend_api_error_failed_to_fetch_registered_servers", + error: TestError.testError, + underlyingError: TestError.underlyingError), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionClientFailedToParseRegisteredServersResponse, + and: .expect(pixelName: "m_mac_netp_backend_api_error_parsing_device_registration_response_failed"), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionClientFailedToEncodeRedeemRequest, + and: .expect(pixelName: "m_mac_netp_backend_api_error_encoding_redeem_request_body_failed"), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionClientInvalidInviteCode, + and: .expect(pixelName: "m_mac_netp_backend_api_error_invalid_invite_code"), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionClientFailedToRedeemInviteCode(TestError.testError), + and: .expect(pixelName: "m_mac_netp_backend_api_error_failed_to_redeem_invite_code", + error: TestError.testError, + underlyingError: TestError.underlyingError), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionClientFailedToParseRedeemResponse(TestError.testError), + and: .expect(pixelName: "m_mac_netp_backend_api_error_parsing_redeem_response_failed", + error: TestError.testError, + underlyingError: TestError.underlyingError), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionClientFailedToFetchLocations(TestError.testError), + and: .expect(pixelName: "m_mac_netp_backend_api_error_failed_to_fetch_location_list", + error: TestError.testError, + underlyingError: TestError.underlyingError), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionClientFailedToParseLocationsResponse(TestError.testError), + and: .expect(pixelName: "m_mac_netp_backend_api_error_parsing_location_list_response_failed", + error: TestError.testError, + underlyingError: TestError.underlyingError), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionClientInvalidAuthToken, + and: .expect(pixelName: "m_mac_netp_backend_api_error_invalid_auth_token"), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionServerListStoreFailedToEncodeServerList, + and: .expect(pixelName: "m_mac_netp_storage_error_failed_to_encode_server_list"), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionServerListStoreFailedToDecodeServerList, + and: .expect(pixelName: "m_mac_netp_storage_error_failed_to_decode_server_list"), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionServerListStoreFailedToWriteServerList(TestError.testError), + and: .expect(pixelName: "m_mac_netp_storage_error_server_list_file_system_write_failed", + error: TestError.testError, + underlyingError: TestError.underlyingError), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionServerListStoreFailedToReadServerList(TestError.testError), + and: .expect(pixelName: "m_mac_netp_storage_error_server_list_file_system_read_failed", + error: TestError.testError, + underlyingError: TestError.underlyingError), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionKeychainErrorFailedToCastKeychainValueToData(field: "field"), + and: .expect(pixelName: "m_mac_netp_keychain_error_failed_to_cast_keychain_value_to_data", + customFields: [ + PixelKit.Parameters.keychainFieldName: "field", + ]), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionKeychainReadError(field: "field", status: 1), + and: .expect(pixelName: "m_mac_netp_keychain_error_read_failed", + customFields: [ + PixelKit.Parameters.keychainFieldName: "field", + PixelKit.Parameters.errorCode: "1", + ]), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionKeychainWriteError(field: "field", status: 1), + and: .expect(pixelName: "m_mac_netp_keychain_error_write_failed", + customFields: [ + PixelKit.Parameters.keychainFieldName: "field", + PixelKit.Parameters.errorCode: "1", + ]), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionKeychainUpdateError(field: "field", status: 1), + and: .expect(pixelName: "m_mac_netp_keychain_error_update_failed", + customFields: [ + PixelKit.Parameters.keychainFieldName: "field", + PixelKit.Parameters.errorCode: "1", + ]), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionKeychainDeleteError(status: 1), + and: .expect(pixelName: "m_mac_netp_keychain_error_delete_failed", + customFields: [ + PixelKit.Parameters.errorCode: "1" + ]), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionWireguardErrorCannotLocateTunnelFileDescriptor, + and: .expect(pixelName: "m_mac_netp_wireguard_error_cannot_locate_tunnel_file_descriptor"), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionWireguardErrorInvalidState(reason: "reason"), + and: .expect(pixelName: "m_mac_netp_wireguard_error_invalid_state", + customFields: [ + PixelKit.Parameters.reason: "reason" + ]), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionWireguardErrorFailedDNSResolution, + and: .expect(pixelName: "m_mac_netp_wireguard_error_failed_dns_resolution"), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionWireguardErrorCannotSetNetworkSettings(TestError.testError), + and: .expect(pixelName: "m_mac_netp_wireguard_error_cannot_set_network_settings", + error: TestError.testError, + underlyingError: TestError.underlyingError), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionWireguardErrorCannotStartWireguardBackend(code: 1), + and: .expect(pixelName: "m_mac_netp_wireguard_error_cannot_start_wireguard_backend", + customFields: [ + PixelKit.Parameters.errorCode: "1" + ]), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionNoAuthTokenFoundError, + and: .expect(pixelName: "m_mac_netp_no_auth_token_found_error"), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionRekeyAttempt, + and: .expect(pixelName: "m_mac_netp_rekey_attempt"), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionRekeyCompleted, + and: .expect(pixelName: "m_mac_netp_rekey_completed"), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionRekeyFailure(TestError.testError), + and: .expect(pixelName: "m_mac_netp_rekey_failure", + error: TestError.testError, + underlyingError: TestError.underlyingError), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionSystemExtensionActivationFailure, + and: .expect(pixelName: "m_mac_netp_system_extension_activation_failure"), + file: #filePath, + line: #line) + fire(NetworkProtectionPixelEvent.networkProtectionUnhandledError(function: "function", line: 1, error: TestError.testError), + and: .expect(pixelName: "m_mac_netp_unhandled_error", + error: TestError.testError, + underlyingError: TestError.underlyingError, + customFields: [ + PixelKit.Parameters.function: "function", + PixelKit.Parameters.line: "1", + ]), + file: #filePath, + line: #line) + } +} + +#endif From db0c6e63f0ef4f8234cbc81d7b341e88f335c709 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Sun, 17 Mar 2024 18:44:31 -0700 Subject: [PATCH 27/51] Update wireguard-apple to 1.1.3 (#2428) Task/Issue URL: https://app.asana.com/0/414235014887631/1206848206360819/f Tech Design URL: CC: **Description**: Client PR for https://github.com/duckduckgo/BrowserServicesKit/pull/729. **Steps to test this PR**: 1. Check that the app builds and NetP works --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 10 +++++----- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 9f4d19a784..7cb377c93b 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -13755,7 +13755,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 125.0.1; + version = 125.0.2; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 23b4cce189..329893ae73 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "eced8f93c945ff2fa4ff92bdd619514d4eff7131", - "version" : "125.0.1" + "revision" : "810bf41347ff437b5d0154405a238553537240a4", + "version" : "125.0.2" } }, { @@ -165,7 +165,7 @@ { "identity" : "trackerradarkit", "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/TrackerRadarKit", + "location" : "https://github.com/duckduckgo/TrackerRadarKit.git", "state" : { "revision" : "a6b7ba151d9dc6684484f3785293875ec01cc1ff", "version" : "1.2.2" @@ -176,8 +176,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/wireguard-apple", "state" : { - "revision" : "2d8172c11478ab11b0f5ad49bdb4f93f4b3d5e0d", - "version" : "1.1.1" + "revision" : "13fd026384b1af11048451061cc1b21434990668", + "version" : "1.1.3" } } ], diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 20e8b1f4b2..8e19bc15eb 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -29,7 +29,7 @@ let package = Package( targets: ["DataBrokerProtection"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "125.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "125.0.2"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index baddd89369..9d711dfba5 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "125.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "125.0.2"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index ef458f05c7..cfe19e07ac 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "125.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "125.0.2"), .package(path: "../SwiftUIExtensions") ], targets: [ From 9f666aab5bcfd4f5b1ff3495013edd04baca072a Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Mon, 18 Mar 2024 08:50:29 +0000 Subject: [PATCH 28/51] Bump version to 1.80.0 (146) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index b11c07fc54..7d20d1f3e4 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 145 +CURRENT_PROJECT_VERSION = 146 From 61ea0c6f8d4a6f2b0cf6befea8bbbac05f13a194 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Mon, 18 Mar 2024 10:39:27 +0100 Subject: [PATCH 29/51] When publishing a DMG, only check out the branch if it exists, otherwise stay on main --- .github/workflows/publish_dmg_release.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish_dmg_release.yml b/.github/workflows/publish_dmg_release.yml index fa4d06d4ce..8daecea7be 100644 --- a/.github/workflows/publish_dmg_release.yml +++ b/.github/workflows/publish_dmg_release.yml @@ -110,12 +110,24 @@ jobs: fi echo "release-version=${TAG//-/.}" >> $GITHUB_OUTPUT + # Always check out main first, because the release branch might have been deleted (for public releases) - name: Check out the code uses: actions/checkout@v4 with: fetch-depth: 0 # Fetch all history and tags in order to extract Asana task URLs from git log submodules: recursive - ref: ${{ inputs.branch || github.ref_name }} + ref: main + + - name: Check out the branch if it exists + env: + branch: ${{ inputs.branch || github.ref_name }} + run: | + if [[ -z "${branch}" ]] || git ls-remote --exit-code --heads origin "${branch}"; then + echo "::notice::Checking out ${branch} branch." + git checkout "${branch}" + else + echo "::notice::Branch ${branch} doesn't exist on the remote repository, staying on main." + fi - name: Select Xcode run: sudo xcode-select -s /Applications/Xcode_$(<.xcode-version).app/Contents/Developer From 655a70c9d1067a53c91147664946e4bc6d31472c Mon Sep 17 00:00:00 2001 From: Christopher Brind Date: Mon, 18 Mar 2024 12:59:22 +0000 Subject: [PATCH 30/51] Use History in Suggestions on iOS (#2339) Task/Issue URL: https://app.asana.com/0/0/1206524433066958/f Tech Design URL: CC: **Description**: BSK updated with API used by iOS, with minor changes reflected here. **Steps to test this PR**: 1. Launch the app 2. Smoke test history feature in particular 3. General smoke testing --- DuckDuckGo.xcodeproj/project.pbxproj | 26 ++++++++++++++++++- .../xcshareddata/swiftpm/Package.resolved | 4 +-- DuckDuckGo/Bookmarks/Model/BookmarkList.swift | 3 ++- .../View/AddressBarTextField.swift | 2 +- .../Model/SuggestionContainer.swift | 6 ++--- .../ViewModel/SuggestionViewModel.swift | 2 +- .../DataBrokerProtection/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- .../Model/SuggestionContainerTests.swift | 2 +- .../Model/SuggestionLoadingMock.swift | 2 +- .../SuggestionContainerViewModelTests.swift | 2 +- .../ViewModel/SuggestionViewModelTests.swift | 2 +- 13 files changed, 41 insertions(+), 16 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 7cb377c93b..605a9fee5a 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2358,6 +2358,9 @@ 85D0327D2B8E3D090041D1FB /* HistoryCoordinatorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85D0327A2B8E3D090041D1FB /* HistoryCoordinatorExtension.swift */; }; 85D33F1225C82EB3002B91A6 /* ConfigurationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85D33F1125C82EB3002B91A6 /* ConfigurationManager.swift */; }; 85D438B6256E7C9E00F3BAF8 /* ContextMenuUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85D438B5256E7C9E00F3BAF8 /* ContextMenuUserScript.swift */; }; + 85D44B862BA08D29001B4AB5 /* Suggestions in Frameworks */ = {isa = PBXBuildFile; productRef = 85D44B852BA08D29001B4AB5 /* Suggestions */; }; + 85D44B882BA08D30001B4AB5 /* Suggestions in Frameworks */ = {isa = PBXBuildFile; productRef = 85D44B872BA08D30001B4AB5 /* Suggestions */; }; + 85D44B8A2BA08D3B001B4AB5 /* Suggestions in Frameworks */ = {isa = PBXBuildFile; productRef = 85D44B892BA08D3B001B4AB5 /* Suggestions */; }; 85D885B026A590A90077C374 /* NSNotificationName+PasswordManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85D885AF26A590A90077C374 /* NSNotificationName+PasswordManager.swift */; }; 85D885B326A5A9DE0077C374 /* NSAlert+PasswordManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85D885B226A5A9DE0077C374 /* NSAlert+PasswordManager.swift */; }; 85E2BBCE2B8F534000DBEC7A /* History in Frameworks */ = {isa = PBXBuildFile; productRef = 85E2BBCD2B8F534000DBEC7A /* History */; }; @@ -4536,6 +4539,7 @@ 3706FCA7293F65D500E42796 /* BrowserServicesKit in Frameworks */, 3129788A2B64131200B67619 /* DataBrokerProtection in Frameworks */, 3706FCA9293F65D500E42796 /* ContentBlocking in Frameworks */, + 85D44B882BA08D30001B4AB5 /* Suggestions in Frameworks */, 4BF97AD12B43C43F00EB4240 /* NetworkProtectionIPC in Frameworks */, 37F44A5F298C17830025E7FE /* Navigation in Frameworks */, B6EC37FF29B8D915001ACE79 /* Configuration in Frameworks */, @@ -4682,6 +4686,7 @@ 85E2BBD22B8F536F00DBEC7A /* History in Frameworks */, 4B957BE62AC7AE700062CA31 /* PrivacyDashboard in Frameworks */, 7B8C083C2AE1268E00F4C67F /* PixelKit in Frameworks */, + 85D44B8A2BA08D3B001B4AB5 /* Suggestions in Frameworks */, 4B957BE72AC7AE700062CA31 /* SyncDataProviders in Frameworks */, 37269F032B332FD8005E8E46 /* Common in Frameworks */, 4B957BE82AC7AE700062CA31 /* SyncUI in Frameworks */, @@ -4767,6 +4772,7 @@ 7BA59C9B2AE18B49009A97B1 /* SystemExtensionManager in Frameworks */, 371D00E129D8509400EC8598 /* OpenSSL in Frameworks */, 1E950E412912A10D0051A99B /* PrivacyDashboard in Frameworks */, + 85D44B862BA08D29001B4AB5 /* Suggestions in Frameworks */, 37DF000529F9C056002B7D3E /* SyncDataProviders in Frameworks */, 37BA812D29B3CD690053F1A3 /* SyncUI in Frameworks */, 372217802B3337FE00B8E9C2 /* TestUtils in Frameworks */, @@ -8466,6 +8472,7 @@ 7B1459562B7D43E500047F2C /* NetworkProtectionProxy */, 85E2BBCF2B8F534A00DBEC7A /* History */, F1D43AF42B98E48900BAB743 /* BareBonesBrowserKit */, + 85D44B872BA08D30001B4AB5 /* Suggestions */, ); productName = DuckDuckGo; productReference = 3706FD05293F65D500E42796 /* DuckDuckGo App Store.app */; @@ -8753,6 +8760,7 @@ 7B94E1642B7ED95100E32B96 /* NetworkProtectionProxy */, 85E2BBD12B8F536F00DBEC7A /* History */, F1D43AF62B98E48F00BAB743 /* BareBonesBrowserKit */, + 85D44B892BA08D3B001B4AB5 /* Suggestions */, ); productName = DuckDuckGo; productReference = 4B957C412AC7AE700062CA31 /* DuckDuckGo Privacy Pro.app */; @@ -8922,6 +8930,7 @@ 1EA7B8D22B7E078C000330A4 /* SubscriptionUI */, 1EA7B8D42B7E078C000330A4 /* Subscription */, F1D43AF22B98E47800BAB743 /* BareBonesBrowserKit */, + 85D44B852BA08D29001B4AB5 /* Suggestions */, ); productName = DuckDuckGo; productReference = AA585D7E248FD31100E9A3E2 /* DuckDuckGo.app */; @@ -13755,7 +13764,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 125.0.2; + version = 126.0.0; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { @@ -14281,6 +14290,21 @@ isa = XCSwiftPackageProductDependency; productName = PixelKit; }; + 85D44B852BA08D29001B4AB5 /* Suggestions */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Suggestions; + }; + 85D44B872BA08D30001B4AB5 /* Suggestions */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Suggestions; + }; + 85D44B892BA08D3B001B4AB5 /* Suggestions */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Suggestions; + }; 85E2BBCD2B8F534000DBEC7A /* History */ = { isa = XCSwiftPackageProductDependency; package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 329893ae73..8827764be0 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "810bf41347ff437b5d0154405a238553537240a4", - "version" : "125.0.2" + "revision" : "7656e94efcf4eedf1c16152c63f57fb52b6ad079", + "version" : "126.0.0" } }, { diff --git a/DuckDuckGo/Bookmarks/Model/BookmarkList.swift b/DuckDuckGo/Bookmarks/Model/BookmarkList.swift index 2fe7281adb..994557e2ba 100644 --- a/DuckDuckGo/Bookmarks/Model/BookmarkList.swift +++ b/DuckDuckGo/Bookmarks/Model/BookmarkList.swift @@ -19,10 +19,11 @@ import Foundation import BrowserServicesKit import Common +import Suggestions struct BookmarkList { - struct IdentifiableBookmark: Equatable, BrowserServicesKit.Bookmark { + struct IdentifiableBookmark: Equatable, Suggestions.Bookmark { let id: String let url: String let urlObject: URL? diff --git a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift index c834cdd58e..f75a983cca 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift @@ -20,7 +20,7 @@ import AppKit import Carbon.HIToolbox import Combine import Common -import BrowserServicesKit +import Suggestions final class AddressBarTextField: NSTextField { diff --git a/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift b/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift index d1be46c375..9ca0f3aaa9 100644 --- a/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift +++ b/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift @@ -17,7 +17,7 @@ // import Foundation -import BrowserServicesKit +import Suggestions import Common import History @@ -86,11 +86,11 @@ final class SuggestionContainer { extension SuggestionContainer: SuggestionLoadingDataSource { - func history(for suggestionLoading: SuggestionLoading) -> [BrowserServicesKit.HistorySuggestion] { + func history(for suggestionLoading: SuggestionLoading) -> [HistorySuggestion] { return historyCoordinating.history ?? [] } - func bookmarks(for suggestionLoading: SuggestionLoading) -> [BrowserServicesKit.Bookmark] { + func bookmarks(for suggestionLoading: SuggestionLoading) -> [Suggestions.Bookmark] { bookmarkManager.list?.bookmarks() ?? [] } diff --git a/DuckDuckGo/Suggestions/ViewModel/SuggestionViewModel.swift b/DuckDuckGo/Suggestions/ViewModel/SuggestionViewModel.swift index e3aac3ea0c..d7b8c5910c 100644 --- a/DuckDuckGo/Suggestions/ViewModel/SuggestionViewModel.swift +++ b/DuckDuckGo/Suggestions/ViewModel/SuggestionViewModel.swift @@ -17,7 +17,7 @@ // import Cocoa -import BrowserServicesKit +import Suggestions struct SuggestionViewModel: Equatable { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 8e19bc15eb..19a07937ac 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -29,7 +29,7 @@ let package = Package( targets: ["DataBrokerProtection"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "125.0.2"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.0.0"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 9d711dfba5..380e8a7897 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "125.0.2"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.0.0"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index cfe19e07ac..2211902822 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "125.0.2"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.0.0"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/UnitTests/Suggestions/Model/SuggestionContainerTests.swift b/UnitTests/Suggestions/Model/SuggestionContainerTests.swift index 3db48611ba..db15131a1c 100644 --- a/UnitTests/Suggestions/Model/SuggestionContainerTests.swift +++ b/UnitTests/Suggestions/Model/SuggestionContainerTests.swift @@ -17,7 +17,7 @@ // import XCTest -import BrowserServicesKit +import Suggestions @testable import DuckDuckGo_Privacy_Browser final class SuggestionContainerTests: XCTestCase { diff --git a/UnitTests/Suggestions/Model/SuggestionLoadingMock.swift b/UnitTests/Suggestions/Model/SuggestionLoadingMock.swift index 717d0c05b2..30248315c7 100644 --- a/UnitTests/Suggestions/Model/SuggestionLoadingMock.swift +++ b/UnitTests/Suggestions/Model/SuggestionLoadingMock.swift @@ -17,7 +17,7 @@ // import XCTest -import BrowserServicesKit +import Suggestions @testable import DuckDuckGo_Privacy_Browser final class SuggestionLoadingMock: SuggestionLoading { diff --git a/UnitTests/Suggestions/ViewModel/SuggestionContainerViewModelTests.swift b/UnitTests/Suggestions/ViewModel/SuggestionContainerViewModelTests.swift index 802bc2cf1f..cee0f7bec3 100644 --- a/UnitTests/Suggestions/ViewModel/SuggestionContainerViewModelTests.swift +++ b/UnitTests/Suggestions/ViewModel/SuggestionContainerViewModelTests.swift @@ -18,7 +18,7 @@ import XCTest import Combine -import BrowserServicesKit +import Suggestions @testable import DuckDuckGo_Privacy_Browser final class SuggestionContainerViewModelTests: XCTestCase { diff --git a/UnitTests/Suggestions/ViewModel/SuggestionViewModelTests.swift b/UnitTests/Suggestions/ViewModel/SuggestionViewModelTests.swift index af77dca2d5..ae95463fd3 100644 --- a/UnitTests/Suggestions/ViewModel/SuggestionViewModelTests.swift +++ b/UnitTests/Suggestions/ViewModel/SuggestionViewModelTests.swift @@ -16,7 +16,7 @@ // limitations under the License. // -import BrowserServicesKit +import Suggestions import XCTest @testable import DuckDuckGo_Privacy_Browser From 9b754f7238aa1759cf96f3b8842d30244128566b Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Mon, 18 Mar 2024 16:13:35 +0100 Subject: [PATCH 31/51] Prevents the tunnel from starting without an auth token (#2438) Task/Issue URL: https://app.asana.com/0/1199230911884351/1206859302875830/f ## Description Does not let the menu app try to start the tunnel without an auth token. Also turns off on-demand if the menu app has no auth token on startup. --- .../NetworkProtectionTunnelController.swift | 8 ++- DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift | 68 ++++++++++--------- DuckDuckGoVPN/NetworkProtectionBouncer.swift | 4 +- 3 files changed, 46 insertions(+), 34 deletions(-) diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift index 4132457ca2..619da111ff 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift @@ -445,12 +445,15 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr // MARK: - Starting & Stopping the VPN enum StartError: LocalizedError { + case noAuthToken case connectionStatusInvalid case connectionAlreadyStarted case simulateControllerFailureError var errorDescription: String? { switch self { + case .noAuthToken: + return "You need a subscription to start the VPN" case .connectionAlreadyStarted: #if DEBUG return "[Debug] Connection already started" @@ -535,7 +538,10 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr var options = [String: NSObject]() options[NetworkProtectionOptionKey.activationAttemptId] = UUID().uuidString as NSString - options[NetworkProtectionOptionKey.authToken] = try tokenStore.fetchToken() as NSString? + guard let authToken = try tokenStore.fetchToken() as NSString? else { + throw StartError.noAuthToken + } + options[NetworkProtectionOptionKey.authToken] = authToken options[NetworkProtectionOptionKey.selectedEnvironment] = settings.selectedEnvironment.rawValue as NSString options[NetworkProtectionOptionKey.selectedServer] = settings.selectedServer.stringValue as? NSString diff --git a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift index f5643c3243..4ca0fe2aa5 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift +++ b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift @@ -260,57 +260,61 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { setupMenuVisibility() - bouncer.requireAuthTokenOrKillApp() + Task { @MainActor in + // The reason we want to await for this is that nothing else should be executed + // if the app should quit. + await bouncer.requireAuthTokenOrKillApp(controller: tunnelController) - // Initialize lazy properties - _ = tunnelControllerIPCService - _ = vpnProxyLauncher + // Initialize lazy properties + _ = tunnelControllerIPCService + _ = vpnProxyLauncher - let dryRun: Bool + let dryRun: Bool #if DEBUG - dryRun = true + dryRun = true #else - dryRun = false + dryRun = false #endif - let pixelSource: String + let pixelSource: String #if NETP_SYSTEM_EXTENSION - pixelSource = "vpnAgent" + pixelSource = "vpnAgent" #else - pixelSource = "vpnAgentAppStore" + pixelSource = "vpnAgentAppStore" #endif - PixelKit.setUp(dryRun: dryRun, - appVersion: AppVersion.shared.versionNumber, - source: pixelSource, - defaultHeaders: [:], - log: .networkProtectionPixel, - defaults: .netP) { (pixelName: String, headers: [String: String], parameters: [String: String], _, _, onComplete: @escaping PixelKit.CompletionBlock) in + PixelKit.setUp(dryRun: dryRun, + appVersion: AppVersion.shared.versionNumber, + source: pixelSource, + defaultHeaders: [:], + log: .networkProtectionPixel, + defaults: .netP) { (pixelName: String, headers: [String: String], parameters: [String: String], _, _, onComplete: @escaping PixelKit.CompletionBlock) in - let url = URL.pixelUrl(forPixelNamed: pixelName) - let apiHeaders = APIRequest.Headers(additionalHeaders: headers) // workaround - Pixel class should really handle APIRequest.Headers by itself - let configuration = APIRequest.Configuration(url: url, method: .get, queryParameters: parameters, headers: apiHeaders) - let request = APIRequest(configuration: configuration) + let url = URL.pixelUrl(forPixelNamed: pixelName) + let apiHeaders = APIRequest.Headers(additionalHeaders: headers) // workaround - Pixel class should really handle APIRequest.Headers by itself + let configuration = APIRequest.Configuration(url: url, method: .get, queryParameters: parameters, headers: apiHeaders) + let request = APIRequest(configuration: configuration) - request.fetch { _, error in - onComplete(error == nil, error) + request.fetch { _, error in + onComplete(error == nil, error) + } } - } - vpnAppEventsHandler.appDidFinishLaunching() + vpnAppEventsHandler.appDidFinishLaunching() - let launchInformation = LoginItemLaunchInformation(agentBundleID: Bundle.main.bundleIdentifier!, defaults: .netP) - let launchedOnStartup = launchInformation.wasLaunchedByStartup - launchInformation.update() + let launchInformation = LoginItemLaunchInformation(agentBundleID: Bundle.main.bundleIdentifier!, defaults: .netP) + let launchedOnStartup = launchInformation.wasLaunchedByStartup + launchInformation.update() - if launchedOnStartup { - Task { - let isConnected = await tunnelController.isConnected + if launchedOnStartup { + Task { + let isConnected = await tunnelController.isConnected - if !isConnected && tunnelSettings.connectOnLogin { - await tunnelController.start() + if !isConnected && tunnelSettings.connectOnLogin { + await tunnelController.start() + } } } } diff --git a/DuckDuckGoVPN/NetworkProtectionBouncer.swift b/DuckDuckGoVPN/NetworkProtectionBouncer.swift index 6b64523c3e..52963e7b61 100644 --- a/DuckDuckGoVPN/NetworkProtectionBouncer.swift +++ b/DuckDuckGoVPN/NetworkProtectionBouncer.swift @@ -29,7 +29,7 @@ final class NetworkProtectionBouncer { /// Simply verifies that the VPN feature is enabled and if not, takes care of killing the /// current app. /// - func requireAuthTokenOrKillApp() { + func requireAuthTokenOrKillApp(controller: TunnelController) async { let keychainStore = NetworkProtectionKeychainTokenStore(keychainType: .default, errorEvents: nil, isSubscriptionEnabled: false, @@ -38,6 +38,8 @@ final class NetworkProtectionBouncer { guard keychainStore.isFeatureActivated else { os_log(.error, log: .networkProtection, "🔴 Stopping: DuckDuckGo VPN not authorized.") + await controller.stop() + // EXIT_SUCCESS ensures the login item won't relaunch // Ref: https://developer.apple.com/documentation/servicemanagement/smappservice/register() // See where it mentions: From 2e80fbfbc8500e64f2f1c9e38714a0bf098ea53e Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Mon, 18 Mar 2024 17:51:06 -0300 Subject: [PATCH 32/51] DBP: Make webview non-persistent and delete any old cache data (#2445) --- .../Sources/DataBrokerProtection/CCF/WebViewHandler.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/CCF/WebViewHandler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/CCF/WebViewHandler.swift index ab98fa98b9..f25a39ff74 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/CCF/WebViewHandler.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/CCF/WebViewHandler.swift @@ -48,6 +48,7 @@ final class DataBrokerProtectionWebViewHandler: NSObject, WebViewHandler { let configuration = WKWebViewConfiguration() configuration.applyDataBrokerConfiguration(privacyConfig: privacyConfig, prefs: prefs, delegate: delegate) configuration.preferences.setValue(true, forKey: "developerExtrasEnabled") + configuration.websiteDataStore = WKWebsiteDataStore.nonPersistent() self.webViewConfiguration = configuration self.isFakeBroker = isFakeBroker @@ -85,6 +86,9 @@ final class DataBrokerProtectionWebViewHandler: NSObject, WebViewHandler { webView?.stopLoading() userContentController?.cleanUpBeforeClosing() + WKWebsiteDataStore.default().removeData(ofTypes: [WKWebsiteDataTypeDiskCache, WKWebsiteDataTypeMemoryCache], modifiedSince: Date(timeIntervalSince1970: 0)) { + os_log("WKWebView data store deleted correctly", log: .action) + } userContentController = nil webView?.navigationDelegate = nil From ba291c1ba3f253f54cd8223c3e48acf8cd3415e3 Mon Sep 17 00:00:00 2001 From: Anh Do <18567+quanganhdo@users.noreply.github.com> Date: Mon, 18 Mar 2024 17:03:08 -0400 Subject: [PATCH 33/51] Remove hardcoded NetP staging endpoint (#2446) --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 6 +++--- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 605a9fee5a..a5457be403 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -13764,7 +13764,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 126.0.0; + version = 126.0.1; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8827764be0..7e9e6baf3f 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "7656e94efcf4eedf1c16152c63f57fb52b6ad079", - "version" : "126.0.0" + "revision" : "d01c760dadbc2e987e7577e2476f95983dc6d38c", + "version" : "126.0.1" } }, { @@ -165,7 +165,7 @@ { "identity" : "trackerradarkit", "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/TrackerRadarKit.git", + "location" : "https://github.com/duckduckgo/TrackerRadarKit", "state" : { "revision" : "a6b7ba151d9dc6684484f3785293875ec01cc1ff", "version" : "1.2.2" diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 19a07937ac..be59cf0606 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -29,7 +29,7 @@ let package = Package( targets: ["DataBrokerProtection"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.0.1"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 380e8a7897..082551ae04 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.0.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 2211902822..a25a90e310 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.0.1"), .package(path: "../SwiftUIExtensions") ], targets: [ From 3353ef13dea81c55eea7d528de4174b0f016bf8b Mon Sep 17 00:00:00 2001 From: Anh Do <18567+quanganhdo@users.noreply.github.com> Date: Tue, 19 Mar 2024 01:18:27 -0400 Subject: [PATCH 34/51] Handle subscription-related iOS use cases (#2427) Task/Issue URL: https://app.asana.com/0/414235014887631/1206844393131400/f --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index a5457be403..1f07e8bad7 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -13764,7 +13764,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 126.0.1; + version = 126.1.0; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7e9e6baf3f..7a11e2163c 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "d01c760dadbc2e987e7577e2476f95983dc6d38c", - "version" : "126.0.1" + "revision" : "f4894b9c00dd7514c66d6b929c12315e0cd9c151", + "version" : "126.1.0" } }, { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index be59cf0606..51a9a0276c 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -29,7 +29,7 @@ let package = Package( targets: ["DataBrokerProtection"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.1.0"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 082551ae04..85d865a47b 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.1.0"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index a25a90e310..90909e471e 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.1.0"), .package(path: "../SwiftUIExtensions") ], targets: [ From 7856a44f666de4db3d5e444253aa986d5ec0bacd Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Tue, 19 Mar 2024 19:13:31 +1100 Subject: [PATCH 35/51] Address bookmarks feedback (#2411) Task/Issue URL: https://app.asana.com/0/72649045549333/1206383230169466/f **Description**: This PR addresses feedback around bookmark editing and deleting. --- DuckDuckGo.xcodeproj/project.pbxproj | 234 +++++- .../AddBookmark.imageset/AddBookmark.svg | 8 + .../Images/AddBookmark.imageset/Contents.json | 2 +- .../icon-16-bookmark-add.pdf | Bin 3871 -> 0 bytes .../Images/AddFolder.imageset/AddFolder.svg | 8 + .../Images/AddFolder.imageset/Contents.json | 2 +- .../AddFolder.imageset/icon-16-folder-add.pdf | Bin 3057 -> 0 bytes .../BookmarksFolder.svg | 10 + .../BookmarksFolder.imageset/Contents.json | 12 + .../Chevron-Medium-Right-16.pdf} | Bin 1160 -> 1153 bytes .../Contents.json | 15 + .../Contents.json | 2 +- .../Images/Trash.imageset/Trash.svg | 9 + .../Bookmarks/Extensions/Bookmarks+Tab.swift | 41 + DuckDuckGo/Bookmarks/Model/Bookmark.swift | 80 +- .../Bookmarks/Model/BookmarkFolderInfo.swift | 32 + DuckDuckGo/Bookmarks/Model/BookmarkList.swift | 29 +- .../Bookmarks/Model/BookmarkManager.swift | 25 + DuckDuckGo/Bookmarks/Model/BookmarkNode.swift | 21 +- .../Model/BookmarkOutlineViewDataSource.swift | 20 +- .../Model/BookmarkSidebarTreeController.swift | 10 +- .../Bookmarks/Model/PasteboardFolder.swift | 25 +- DuckDuckGo/Bookmarks/Model/PseudoFolder.swift | 2 +- .../Bookmarks/Services/BookmarkStore.swift | 1 + .../Services/BookmarkStoreMock.swift | 34 + .../Bookmarks/Services/ContextualMenu.swift | 225 ++++-- .../Services/LocalBookmarkStore.swift | 100 ++- .../Services/MenuItemSelectors.swift | 14 +- .../View/AddBookmarkFolderModalView.swift | 67 -- .../View/AddBookmarkFolderPopoverView.swift | 76 +- .../Bookmarks/View/AddBookmarkModalView.swift | 77 -- .../View/AddBookmarkPopoverView.swift | 103 +-- .../Bookmarks/View/BookmarkFolderPicker.swift | 2 +- .../View/BookmarkListViewController.swift | 110 ++- ...okmarkManagementDetailViewController.swift | 426 +++++----- ...kmarkManagementSidebarViewController.swift | 59 +- .../View/BookmarkOutlineCellView.swift | 95 ++- .../View/BookmarkTableCellView.swift | 252 +----- .../Bookmarks/View/BookmarkTableRowView.swift | 8 +- .../Dialog/AddEditBookmarkDialogView.swift | 123 +++ .../AddEditBookmarkFolderDialogView.swift | 107 +++ .../Dialog/AddEditBookmarkFolderView.swift | 132 ++++ .../View/Dialog/AddEditBookmarkView.swift | 113 +++ .../Dialog/BookmarkDialogButtonsView.swift | 186 +++++ .../Dialog/BookmarkDialogContainerView.swift | 50 ++ .../BookmarkDialogFolderManagementView.swift | 76 ++ .../BookmarkDialogStackedContentView.swift | 111 +++ .../View/Dialog/BookmarkFavoriteView.swift | 47 ++ .../Dialog/BookmarksDialogViewFactory.swift | 93 +++ .../AddBookmarkFolderModalViewModel.swift | 79 -- .../AddBookmarkFolderPopoverViewModel.swift | 23 +- .../ViewModel/AddBookmarkModalViewModel.swift | 127 --- .../AddBookmarkPopoverViewModel.swift | 31 +- ...itBookmarkDialogCoordinatorViewModel.swift | 74 ++ .../AddEditBookmarkDialogViewModel.swift | 216 +++++ ...AddEditBookmarkFolderDialogViewModel.swift | 181 +++++ .../ViewModel/BookmarksDialogViewModel.swift | 35 + .../View/BookmarksBarCollectionViewItem.swift | 108 +-- .../View/BookmarksBarMenuFactory.swift | 7 + .../View/BookmarksBarViewController.swift | 99 ++- .../View/BookmarksBarViewModel.swift | 16 +- .../Common/Extensions/NSMenuExtension.swift | 10 + DuckDuckGo/Common/Localizables/UserText.swift | 6 +- .../Model/HomePageFavoritesModel.swift | 15 +- DuckDuckGo/HomePage/View/FavoritesView.swift | 3 + .../View/HomePageViewController.swift | 19 +- DuckDuckGo/Localizable.xcstrings | 230 +----- .../VPNLocation/VPNLocationView.swift | 15 - .../{ => Dialogs}/Dialog.swift | 0 .../Dialogs/TieredDialogView.swift | 70 ++ .../TwoColumnsListView.swift | 62 ++ .../View+ConditionalModifiers.swift | 47 ++ .../Extensions/Bookmarks+TabTests.swift | 61 ++ .../BookmarksBarMenuFactoryTests.swift | 59 ++ .../Model/BaseBookmarkEntityTests.swift | 247 ++++++ .../Bookmarks/Model/BookmarkListTests.swift | 59 +- .../Bookmarks/Model/BookmarkNodeTests.swift | 170 ++++ .../BookmarkOutlineViewDataSourceTests.swift | 58 +- .../BookmarkSidebarTreeControllerTests.swift | 34 +- UnitTests/Bookmarks/Model/BookmarkTests.swift | 2 +- .../Bookmarks/Model/ContextualMenuTests.swift | 267 +++++++ .../Model/LocalBookmarkManagerTests.swift | 21 + .../Services/LocalBookmarkStoreTests.swift | 158 +++- ...kmarkDialogCoordinatorViewModelTests.swift | 170 ++++ .../AddEditBookmarkDialogViewModelTests.swift | 745 ++++++++++++++++++ ...itBookmarkFolderDialogViewModelTests.swift | 425 ++++++++++ .../BookmarksBarViewModelTests.swift | 214 +++++ .../HomePage/Mocks/MockBookmarkManager.swift | 4 + 88 files changed, 5840 insertions(+), 1601 deletions(-) create mode 100644 DuckDuckGo/Assets.xcassets/Images/AddBookmark.imageset/AddBookmark.svg delete mode 100644 DuckDuckGo/Assets.xcassets/Images/AddBookmark.imageset/icon-16-bookmark-add.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/AddFolder.imageset/AddFolder.svg delete mode 100644 DuckDuckGo/Assets.xcassets/Images/AddFolder.imageset/icon-16-folder-add.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/BookmarksFolder.imageset/BookmarksFolder.svg create mode 100644 DuckDuckGo/Assets.xcassets/Images/BookmarksFolder.imageset/Contents.json rename DuckDuckGo/Assets.xcassets/Images/{Chevron-Next-16.imageset/Chevron-Next-16.pdf => Chevron-Medium-Right-16.imageset/Chevron-Medium-Right-16.pdf} (53%) create mode 100644 DuckDuckGo/Assets.xcassets/Images/Chevron-Medium-Right-16.imageset/Contents.json rename DuckDuckGo/Assets.xcassets/Images/{Chevron-Next-16.imageset => Trash.imageset}/Contents.json (82%) create mode 100644 DuckDuckGo/Assets.xcassets/Images/Trash.imageset/Trash.svg create mode 100644 DuckDuckGo/Bookmarks/Extensions/Bookmarks+Tab.swift create mode 100644 DuckDuckGo/Bookmarks/Model/BookmarkFolderInfo.swift delete mode 100644 DuckDuckGo/Bookmarks/View/AddBookmarkFolderModalView.swift delete mode 100644 DuckDuckGo/Bookmarks/View/AddBookmarkModalView.swift create mode 100644 DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift create mode 100644 DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderDialogView.swift create mode 100644 DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderView.swift create mode 100644 DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkView.swift create mode 100644 DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogButtonsView.swift create mode 100644 DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogContainerView.swift create mode 100644 DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogFolderManagementView.swift create mode 100644 DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogStackedContentView.swift create mode 100644 DuckDuckGo/Bookmarks/View/Dialog/BookmarkFavoriteView.swift create mode 100644 DuckDuckGo/Bookmarks/View/Dialog/BookmarksDialogViewFactory.swift delete mode 100644 DuckDuckGo/Bookmarks/ViewModel/AddBookmarkFolderModalViewModel.swift delete mode 100644 DuckDuckGo/Bookmarks/ViewModel/AddBookmarkModalViewModel.swift create mode 100644 DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogCoordinatorViewModel.swift create mode 100644 DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogViewModel.swift create mode 100644 DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkFolderDialogViewModel.swift create mode 100644 DuckDuckGo/Bookmarks/ViewModel/BookmarksDialogViewModel.swift rename LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/{ => Dialogs}/Dialog.swift (100%) create mode 100644 LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/Dialogs/TieredDialogView.swift create mode 100644 LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/TwoColumnsListView.swift create mode 100644 LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/View+ConditionalModifiers.swift create mode 100644 UnitTests/Bookmarks/Extensions/Bookmarks+TabTests.swift create mode 100644 UnitTests/Bookmarks/Factory/BookmarksBarMenuFactoryTests.swift create mode 100644 UnitTests/Bookmarks/Model/BaseBookmarkEntityTests.swift create mode 100644 UnitTests/Bookmarks/Model/ContextualMenuTests.swift create mode 100644 UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogCoordinatorViewModelTests.swift create mode 100644 UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogViewModelTests.swift create mode 100644 UnitTests/Bookmarks/ViewModels/AddEditBookmarkFolderDialogViewModelTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 1f07e8bad7..71f46f9bd0 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -268,7 +268,6 @@ 3706FADC293F65D500E42796 /* FirefoxLoginReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8AC93826B48A5100879451 /* FirefoxLoginReader.swift */; }; 3706FADD293F65D500E42796 /* AtbParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69B50382726A12400758A2B /* AtbParser.swift */; }; 3706FADE293F65D500E42796 /* PreferencesDuckPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F19A6428E1B3FB00740DC6 /* PreferencesDuckPlayerView.swift */; }; - 3706FADF293F65D500E42796 /* AddBookmarkFolderModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292CA2667123700AD2C21 /* AddBookmarkFolderModalView.swift */; }; 3706FAE0293F65D500E42796 /* BookmarkSidebarTreeController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B92929426670D2A00AD2C21 /* BookmarkSidebarTreeController.swift */; }; 3706FAE1293F65D500E42796 /* HomePageFavoritesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85589E8627BBB8F20038AD11 /* HomePageFavoritesModel.swift */; }; 3706FAE2293F65D500E42796 /* SequenceExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB88B4925B7B690006F6B06 /* SequenceExtensions.swift */; }; @@ -387,7 +386,6 @@ 3706FB6D293F65D500E42796 /* SuggestionListCharacteristics.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAB8203B26B2DE0D00788AC3 /* SuggestionListCharacteristics.swift */; }; 3706FB6F293F65D500E42796 /* BookmarkListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292CC2667123700AD2C21 /* BookmarkListViewController.swift */; }; 3706FB70293F65D500E42796 /* SecureVaultLoginImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B723DF326B0002B00E14D75 /* SecureVaultLoginImporter.swift */; }; - 3706FB71293F65D500E42796 /* AddBookmarkModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292CB2667123700AD2C21 /* AddBookmarkModalView.swift */; }; 3706FB72293F65D500E42796 /* RecentlyClosedCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5C1DD4285C780C0089850C /* RecentlyClosedCoordinator.swift */; }; 3706FB74293F65D500E42796 /* FaviconHostReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA6197C5276B3168008396F0 /* FaviconHostReference.swift */; }; 3706FB76293F65D500E42796 /* ASN1Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8AC93A26B48ADF00879451 /* ASN1Parser.swift */; }; @@ -1279,8 +1277,6 @@ 4B9292CF2667123700AD2C21 /* BookmarkManagementSidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292C72667123700AD2C21 /* BookmarkManagementSidebarViewController.swift */; }; 4B9292D02667123700AD2C21 /* BookmarkManagementSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292C82667123700AD2C21 /* BookmarkManagementSplitViewController.swift */; }; 4B9292D12667123700AD2C21 /* BookmarkTableRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292C92667123700AD2C21 /* BookmarkTableRowView.swift */; }; - 4B9292D22667123700AD2C21 /* AddBookmarkFolderModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292CA2667123700AD2C21 /* AddBookmarkFolderModalView.swift */; }; - 4B9292D32667123700AD2C21 /* AddBookmarkModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292CB2667123700AD2C21 /* AddBookmarkModalView.swift */; }; 4B9292D42667123700AD2C21 /* BookmarkListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292CC2667123700AD2C21 /* BookmarkListViewController.swift */; }; 4B9292D52667123700AD2C21 /* BookmarkManagementDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292CD2667123700AD2C21 /* BookmarkManagementDetailViewController.swift */; }; 4B9292D92667124B00AD2C21 /* BookmarkListTreeControllerDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292D82667124B00AD2C21 /* BookmarkListTreeControllerDataSource.swift */; }; @@ -1391,7 +1387,6 @@ 4B9579B52AC7AE700062CA31 /* FirefoxLoginReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8AC93826B48A5100879451 /* FirefoxLoginReader.swift */; }; 4B9579B62AC7AE700062CA31 /* AtbParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69B50382726A12400758A2B /* AtbParser.swift */; }; 4B9579B72AC7AE700062CA31 /* PreferencesDuckPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F19A6428E1B3FB00740DC6 /* PreferencesDuckPlayerView.swift */; }; - 4B9579B82AC7AE700062CA31 /* AddBookmarkFolderModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292CA2667123700AD2C21 /* AddBookmarkFolderModalView.swift */; }; 4B9579B92AC7AE700062CA31 /* BookmarkSidebarTreeController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B92929426670D2A00AD2C21 /* BookmarkSidebarTreeController.swift */; }; 4B9579BA2AC7AE700062CA31 /* HomePageFavoritesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85589E8627BBB8F20038AD11 /* HomePageFavoritesModel.swift */; }; 4B9579BB2AC7AE700062CA31 /* SequenceExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB88B4925B7B690006F6B06 /* SequenceExtensions.swift */; }; @@ -1561,7 +1556,6 @@ 4B957A692AC7AE700062CA31 /* BookmarkListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292CC2667123700AD2C21 /* BookmarkListViewController.swift */; }; 4B957A6A2AC7AE700062CA31 /* SecureVaultLoginImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B723DF326B0002B00E14D75 /* SecureVaultLoginImporter.swift */; }; 4B957A6B2AC7AE700062CA31 /* WKProcessPoolExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B645D8F529FA95440024461F /* WKProcessPoolExtension.swift */; }; - 4B957A6C2AC7AE700062CA31 /* AddBookmarkModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292CB2667123700AD2C21 /* AddBookmarkModalView.swift */; }; 4B957A6D2AC7AE700062CA31 /* LoginItemsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AE86A2AA76CF90026E7DC /* LoginItemsManager.swift */; }; 4B957A6E2AC7AE700062CA31 /* PixelExperiment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 857E5AF42A79045800FC0FB4 /* PixelExperiment.swift */; }; 4B957A6F2AC7AE700062CA31 /* DuckPlayerTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C416A6294A4AE500C4F2E7 /* DuckPlayerTabExtension.swift */; }; @@ -2417,14 +2411,76 @@ 9DB6E7242AA0DC5800A17F3C /* LoginItems in Frameworks */ = {isa = PBXBuildFile; productRef = 9DB6E7232AA0DC5800A17F3C /* LoginItems */; }; 9DC70B1A2AA1FA5B005A844B /* LoginItems in Frameworks */ = {isa = PBXBuildFile; productRef = 9DC70B192AA1FA5B005A844B /* LoginItems */; }; 9DEF97E12B06C4EE00764F03 /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = 9DEF97E02B06C4EE00764F03 /* Networking */; }; + 9F0A2CF82B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0A2CF72B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift */; }; + 9F0A2CF92B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0A2CF72B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift */; }; 9F180D0F2B69C553000D695F /* Tab+WKUIDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F180D0E2B69C553000D695F /* Tab+WKUIDelegateTests.swift */; }; 9F180D102B69C553000D695F /* Tab+WKUIDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F180D0E2B69C553000D695F /* Tab+WKUIDelegateTests.swift */; }; 9F180D122B69C665000D695F /* DownloadsTabExtensionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F180D112B69C665000D695F /* DownloadsTabExtensionMock.swift */; }; 9F180D132B69C665000D695F /* DownloadsTabExtensionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F180D112B69C665000D695F /* DownloadsTabExtensionMock.swift */; }; + 9F26060B2B85C20A00819292 /* AddEditBookmarkDialogViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2606092B85C20400819292 /* AddEditBookmarkDialogViewModelTests.swift */; }; + 9F26060C2B85C20B00819292 /* AddEditBookmarkDialogViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2606092B85C20400819292 /* AddEditBookmarkDialogViewModelTests.swift */; }; + 9F26060E2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F26060D2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift */; }; + 9F26060F2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F26060D2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift */; }; 9F3910622B68C35600CB5112 /* DownloadsTabExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3910612B68C35600CB5112 /* DownloadsTabExtensionTests.swift */; }; 9F3910632B68C35600CB5112 /* DownloadsTabExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3910612B68C35600CB5112 /* DownloadsTabExtensionTests.swift */; }; 9F3910692B68D87B00CB5112 /* ProgressExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3910682B68D87B00CB5112 /* ProgressExtensionTests.swift */; }; 9F39106A2B68D87B00CB5112 /* ProgressExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3910682B68D87B00CB5112 /* ProgressExtensionTests.swift */; }; + 9F514F912B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F514F902B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift */; }; + 9F514F922B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F514F902B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift */; }; + 9F514F932B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F514F902B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift */; }; + 9F56CFA92B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFA82B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift */; }; + 9F56CFAA2B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFA82B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift */; }; + 9F56CFAB2B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFA82B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift */; }; + 9F56CFAD2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFAC2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift */; }; + 9F56CFAE2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFAC2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift */; }; + 9F56CFAF2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFAC2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift */; }; + 9F56CFB12B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFB02B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift */; }; + 9F56CFB22B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFB02B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift */; }; + 9F56CFB32B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFB02B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift */; }; + 9F872D982B8DA9F800138637 /* Bookmarks+Tab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872D972B8DA9F800138637 /* Bookmarks+Tab.swift */; }; + 9F872D992B8DA9F800138637 /* Bookmarks+Tab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872D972B8DA9F800138637 /* Bookmarks+Tab.swift */; }; + 9F872D9A2B8DA9F800138637 /* Bookmarks+Tab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872D972B8DA9F800138637 /* Bookmarks+Tab.swift */; }; + 9F872D9D2B9058D000138637 /* Bookmarks+TabTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872D9C2B9058D000138637 /* Bookmarks+TabTests.swift */; }; + 9F872D9E2B9058D000138637 /* Bookmarks+TabTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872D9C2B9058D000138637 /* Bookmarks+TabTests.swift */; }; + 9F872DA02B90644800138637 /* ContextualMenuTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872D9F2B90644800138637 /* ContextualMenuTests.swift */; }; + 9F872DA12B90644800138637 /* ContextualMenuTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872D9F2B90644800138637 /* ContextualMenuTests.swift */; }; + 9F872DA32B90920F00138637 /* BookmarkFolderInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872DA22B90920F00138637 /* BookmarkFolderInfo.swift */; }; + 9F872DA42B90920F00138637 /* BookmarkFolderInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872DA22B90920F00138637 /* BookmarkFolderInfo.swift */; }; + 9F872DA52B90920F00138637 /* BookmarkFolderInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872DA22B90920F00138637 /* BookmarkFolderInfo.swift */; }; + 9F982F0D2B8224BF00231028 /* AddEditBookmarkFolderDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */; }; + 9F982F0E2B8224BF00231028 /* AddEditBookmarkFolderDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */; }; + 9F982F0F2B8224BF00231028 /* AddEditBookmarkFolderDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */; }; + 9F982F132B822B7B00231028 /* AddEditBookmarkFolderDialogViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F982F112B82268F00231028 /* AddEditBookmarkFolderDialogViewModelTests.swift */; }; + 9F982F142B822C7400231028 /* AddEditBookmarkFolderDialogViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F982F112B82268F00231028 /* AddEditBookmarkFolderDialogViewModelTests.swift */; }; + 9FA173DA2B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173D92B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift */; }; + 9FA173DB2B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173D92B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift */; }; + 9FA173DC2B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173D92B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift */; }; + 9FA173DF2B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173DE2B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift */; }; + 9FA173E02B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173DE2B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift */; }; + 9FA173E12B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173DE2B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift */; }; + 9FA173E32B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173E22B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift */; }; + 9FA173E42B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173E22B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift */; }; + 9FA173E52B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173E22B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift */; }; + 9FA173E72B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173E62B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift */; }; + 9FA173E82B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173E62B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift */; }; + 9FA173E92B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173E62B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift */; }; + 9FA173EB2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173EA2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift */; }; + 9FA173EC2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173EA2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift */; }; + 9FA173ED2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173EA2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift */; }; + 9FA75A3E2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA75A3D2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift */; }; + 9FA75A3F2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA75A3D2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift */; }; + 9FDA6C212B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDA6C202B79A59D00E099A9 /* BookmarkFavoriteView.swift */; }; + 9FDA6C222B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDA6C202B79A59D00E099A9 /* BookmarkFavoriteView.swift */; }; + 9FDA6C232B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDA6C202B79A59D00E099A9 /* BookmarkFavoriteView.swift */; }; + 9FEE98652B846870002E44E8 /* AddEditBookmarkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE98642B846870002E44E8 /* AddEditBookmarkView.swift */; }; + 9FEE98662B846870002E44E8 /* AddEditBookmarkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE98642B846870002E44E8 /* AddEditBookmarkView.swift */; }; + 9FEE98672B846870002E44E8 /* AddEditBookmarkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE98642B846870002E44E8 /* AddEditBookmarkView.swift */; }; + 9FEE98692B85B869002E44E8 /* BookmarksDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE98682B85B869002E44E8 /* BookmarksDialogViewModel.swift */; }; + 9FEE986A2B85B869002E44E8 /* BookmarksDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE98682B85B869002E44E8 /* BookmarksDialogViewModel.swift */; }; + 9FEE986B2B85B869002E44E8 /* BookmarksDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE98682B85B869002E44E8 /* BookmarksDialogViewModel.swift */; }; + 9FEE986D2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE986C2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift */; }; + 9FEE986E2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE986C2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift */; }; + 9FEE986F2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE986C2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift */; }; AA06B6B72672AF8100F541C5 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = AA06B6B62672AF8100F541C5 /* Sparkle */; }; AA0877B826D5160D00B05660 /* SafariVersionReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0877B726D5160D00B05660 /* SafariVersionReaderTests.swift */; }; AA0877BA26D5161D00B05660 /* WebKitVersionProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0877B926D5161D00B05660 /* WebKitVersionProviderTests.swift */; }; @@ -3052,15 +3108,9 @@ B6F92BA32A691583002ABA6B /* UserDefaultsWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85C6A29525CC1FFD00EEB5F1 /* UserDefaultsWrapper.swift */; }; B6F92BAC2A6937B3002ABA6B /* OptionalExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B637273C26CCF0C200C8CB02 /* OptionalExtension.swift */; }; B6F92BAD2A6937B5002ABA6B /* OptionalExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B637273C26CCF0C200C8CB02 /* OptionalExtension.swift */; }; - B6F9BDD82B45B7D900677B33 /* AddBookmarkModalViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9BDD72B45B7D900677B33 /* AddBookmarkModalViewModel.swift */; }; - B6F9BDD92B45B7D900677B33 /* AddBookmarkModalViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9BDD72B45B7D900677B33 /* AddBookmarkModalViewModel.swift */; }; - B6F9BDDA2B45B7D900677B33 /* AddBookmarkModalViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9BDD72B45B7D900677B33 /* AddBookmarkModalViewModel.swift */; }; B6F9BDDC2B45B7EE00677B33 /* WebsiteInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9BDDB2B45B7EE00677B33 /* WebsiteInfo.swift */; }; B6F9BDDD2B45B7EE00677B33 /* WebsiteInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9BDDB2B45B7EE00677B33 /* WebsiteInfo.swift */; }; B6F9BDDE2B45B7EE00677B33 /* WebsiteInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9BDDB2B45B7EE00677B33 /* WebsiteInfo.swift */; }; - B6F9BDE02B45C1A800677B33 /* AddBookmarkFolderModalViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9BDDF2B45C1A800677B33 /* AddBookmarkFolderModalViewModel.swift */; }; - B6F9BDE12B45C1A800677B33 /* AddBookmarkFolderModalViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9BDDF2B45C1A800677B33 /* AddBookmarkFolderModalViewModel.swift */; }; - B6F9BDE22B45C1A800677B33 /* AddBookmarkFolderModalViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9BDDF2B45C1A800677B33 /* AddBookmarkFolderModalViewModel.swift */; }; B6F9BDE42B45CD1900677B33 /* ModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9BDE32B45CD1900677B33 /* ModalView.swift */; }; B6F9BDE52B45CD1900677B33 /* ModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9BDE32B45CD1900677B33 /* ModalView.swift */; }; B6F9BDE62B45CD1900677B33 /* ModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9BDE32B45CD1900677B33 /* ModalView.swift */; }; @@ -3703,8 +3753,6 @@ 4B9292C72667123700AD2C21 /* BookmarkManagementSidebarViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkManagementSidebarViewController.swift; sourceTree = ""; }; 4B9292C82667123700AD2C21 /* BookmarkManagementSplitViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkManagementSplitViewController.swift; sourceTree = ""; }; 4B9292C92667123700AD2C21 /* BookmarkTableRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkTableRowView.swift; sourceTree = ""; }; - 4B9292CA2667123700AD2C21 /* AddBookmarkFolderModalView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddBookmarkFolderModalView.swift; sourceTree = ""; }; - 4B9292CB2667123700AD2C21 /* AddBookmarkModalView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddBookmarkModalView.swift; sourceTree = ""; }; 4B9292CC2667123700AD2C21 /* BookmarkListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkListViewController.swift; sourceTree = ""; }; 4B9292CD2667123700AD2C21 /* BookmarkManagementDetailViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkManagementDetailViewController.swift; sourceTree = ""; }; 4B9292D82667124B00AD2C21 /* BookmarkListTreeControllerDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkListTreeControllerDataSource.swift; sourceTree = ""; }; @@ -4004,10 +4052,33 @@ 9D9AE9282AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionBackgroundManager.swift; sourceTree = ""; }; 9D9AE92B2AAB84FF0026E7DC /* DBPMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBPMocks.swift; sourceTree = ""; }; 9DB6E7222AA0DA7A00A17F3C /* LoginItems */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = LoginItems; sourceTree = ""; }; + 9F0A2CF72B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseBookmarkEntityTests.swift; sourceTree = ""; }; 9F180D0E2B69C553000D695F /* Tab+WKUIDelegateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Tab+WKUIDelegateTests.swift"; sourceTree = ""; }; 9F180D112B69C665000D695F /* DownloadsTabExtensionMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsTabExtensionMock.swift; sourceTree = ""; }; + 9F2606092B85C20400819292 /* AddEditBookmarkDialogViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogViewModelTests.swift; sourceTree = ""; }; + 9F26060D2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogCoordinatorViewModelTests.swift; sourceTree = ""; }; 9F3910612B68C35600CB5112 /* DownloadsTabExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsTabExtensionTests.swift; sourceTree = ""; }; 9F3910682B68D87B00CB5112 /* ProgressExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressExtensionTests.swift; sourceTree = ""; }; + 9F514F902B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkFolderDialogView.swift; sourceTree = ""; }; + 9F56CFA82B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkFolderView.swift; sourceTree = ""; }; + 9F56CFAC2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogViewModel.swift; sourceTree = ""; }; + 9F56CFB02B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksDialogViewFactory.swift; sourceTree = ""; }; + 9F872D972B8DA9F800138637 /* Bookmarks+Tab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bookmarks+Tab.swift"; sourceTree = ""; }; + 9F872D9C2B9058D000138637 /* Bookmarks+TabTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bookmarks+TabTests.swift"; sourceTree = ""; }; + 9F872D9F2B90644800138637 /* ContextualMenuTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextualMenuTests.swift; sourceTree = ""; }; + 9F872DA22B90920F00138637 /* BookmarkFolderInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkFolderInfo.swift; sourceTree = ""; }; + 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkFolderDialogViewModel.swift; sourceTree = ""; }; + 9F982F112B82268F00231028 /* AddEditBookmarkFolderDialogViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkFolderDialogViewModelTests.swift; sourceTree = ""; }; + 9FA173D92B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkDialogContainerView.swift; sourceTree = ""; }; + 9FA173DE2B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkDialogButtonsView.swift; sourceTree = ""; }; + 9FA173E22B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkDialogFolderManagementView.swift; sourceTree = ""; }; + 9FA173E62B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkDialogStackedContentView.swift; sourceTree = ""; }; + 9FA173EA2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogView.swift; sourceTree = ""; }; + 9FA75A3D2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksBarMenuFactoryTests.swift; sourceTree = ""; }; + 9FDA6C202B79A59D00E099A9 /* BookmarkFavoriteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkFavoriteView.swift; sourceTree = ""; }; + 9FEE98642B846870002E44E8 /* AddEditBookmarkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkView.swift; sourceTree = ""; }; + 9FEE98682B85B869002E44E8 /* BookmarksDialogViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksDialogViewModel.swift; sourceTree = ""; }; + 9FEE986C2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogCoordinatorViewModel.swift; sourceTree = ""; }; AA0877B726D5160D00B05660 /* SafariVersionReaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariVersionReaderTests.swift; sourceTree = ""; }; AA0877B926D5161D00B05660 /* WebKitVersionProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebKitVersionProviderTests.swift; sourceTree = ""; }; AA0F3DB6261A566C0077F2D9 /* SuggestionLoadingMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionLoadingMock.swift; sourceTree = ""; }; @@ -4464,9 +4535,7 @@ B6F56566299A414300A04298 /* WKWebViewMockingExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WKWebViewMockingExtension.swift; sourceTree = ""; }; B6F7127D29F6779000594A45 /* QRSharingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRSharingService.swift; sourceTree = ""; }; B6F7128029F681EB00594A45 /* QuickLookUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickLookUI.framework; path = System/Library/Frameworks/QuickLookUI.framework; sourceTree = SDKROOT; }; - B6F9BDD72B45B7D900677B33 /* AddBookmarkModalViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddBookmarkModalViewModel.swift; sourceTree = ""; }; B6F9BDDB2B45B7EE00677B33 /* WebsiteInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteInfo.swift; sourceTree = ""; }; - B6F9BDDF2B45C1A800677B33 /* AddBookmarkFolderModalViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddBookmarkFolderModalViewModel.swift; sourceTree = ""; }; B6F9BDE32B45CD1900677B33 /* ModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalView.swift; sourceTree = ""; }; B6FA893C269C423100588ECD /* PrivacyDashboard.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = PrivacyDashboard.storyboard; sourceTree = ""; }; B6FA893E269C424500588ECD /* PrivacyDashboardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardViewController.swift; sourceTree = ""; }; @@ -5851,6 +5920,7 @@ children = ( 4B9292AE26670F5300AD2C21 /* NSOutlineViewExtensions.swift */, B6C0BB6629AEFF8100AE8E3C /* BookmarkExtension.swift */, + 9F872D972B8DA9F800138637 /* Bookmarks+Tab.swift */, ); path = Extensions; sourceTree = ""; @@ -6674,6 +6744,49 @@ path = DuckDuckGoDBPBackgroundAgent; sourceTree = ""; }; + 9F872D9B2B9058B000138637 /* Extensions */ = { + isa = PBXGroup; + children = ( + 9F872D9C2B9058D000138637 /* Bookmarks+TabTests.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + 9F982F102B82264400231028 /* ViewModels */ = { + isa = PBXGroup; + children = ( + 9F982F112B82268F00231028 /* AddEditBookmarkFolderDialogViewModelTests.swift */, + 9F2606092B85C20400819292 /* AddEditBookmarkDialogViewModelTests.swift */, + 9F26060D2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift */, + ); + path = ViewModels; + sourceTree = ""; + }; + 9FA173DD2B7A0ECE00EE4E6E /* Dialog */ = { + isa = PBXGroup; + children = ( + 9FA173D92B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift */, + 9FA173E62B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift */, + 9FA173DE2B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift */, + 9FDA6C202B79A59D00E099A9 /* BookmarkFavoriteView.swift */, + 9FA173E22B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift */, + 9FA173EA2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift */, + 9F514F902B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift */, + 9F56CFA82B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift */, + 9FEE98642B846870002E44E8 /* AddEditBookmarkView.swift */, + 9F56CFB02B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift */, + ); + path = Dialog; + sourceTree = ""; + }; + 9FA75A3C2BA00DF500DA5FA6 /* Factory */ = { + isa = PBXGroup; + children = ( + 9FA75A3D2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift */, + ); + path = Factory; + sourceTree = ""; + }; AA0877B626D515EE00B05660 /* UserAgent */ = { isa = PBXGroup; children = ( @@ -7021,6 +7134,9 @@ AA652CAB25DD820D009059CC /* Bookmarks */ = { isa = PBXGroup; children = ( + 9F872D9B2B9058B000138637 /* Extensions */, + 9FA75A3C2BA00DF500DA5FA6 /* Factory */, + 9F982F102B82264400231028 /* ViewModels */, AA652CAE25DD8228009059CC /* Model */, AA652CAF25DD822C009059CC /* Services */, ); @@ -7042,6 +7158,8 @@ AA652CCD25DD9071009059CC /* BookmarkListTests.swift */, AA652CD225DDA6E9009059CC /* LocalBookmarkManagerTests.swift */, 98A95D87299A2DF900B9B81A /* BookmarkMigrationTests.swift */, + 9F872D9F2B90644800138637 /* ContextualMenuTests.swift */, + 9F0A2CF72B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift */, ); path = Model; sourceTree = ""; @@ -7430,8 +7548,10 @@ B69A14F12B4D6FE800B9417D /* AddBookmarkFolderPopoverViewModel.swift */, B69A14F52B4D701F00B9417D /* AddBookmarkPopoverViewModel.swift */, AAB549DE25DAB8F80058460B /* BookmarkViewModel.swift */, - B6F9BDD72B45B7D900677B33 /* AddBookmarkModalViewModel.swift */, - B6F9BDDF2B45C1A800677B33 /* AddBookmarkFolderModalViewModel.swift */, + 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */, + 9F56CFAC2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift */, + 9FEE98682B85B869002E44E8 /* BookmarksDialogViewModel.swift */, + 9FEE986C2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift */, ); path = ViewModel; sourceTree = ""; @@ -7502,9 +7622,8 @@ AAC5E4C125D6A6C3007F5990 /* View */ = { isa = PBXGroup; children = ( - 4B9292CA2667123700AD2C21 /* AddBookmarkFolderModalView.swift */, + 9FA173DD2B7A0ECE00EE4E6E /* Dialog */, 7BEC20412B0F505F00243D3E /* AddBookmarkFolderPopoverView.swift */, - 4B9292CB2667123700AD2C21 /* AddBookmarkModalView.swift */, AAC5E4C425D6A6E8007F5990 /* AddBookmarkPopover.swift */, 7BEC20402B0F505F00243D3E /* AddBookmarkPopoverView.swift */, B69A14F92B4D705D00B9417D /* BookmarkFolderPicker.swift */, @@ -7542,6 +7661,7 @@ AAC5E4CE25D6A709007F5990 /* BookmarkManager.swift */, 379E877529E98729001C8BB0 /* BookmarksCleanupErrorHandling.swift */, B6F9BDDB2B45B7EE00677B33 /* WebsiteInfo.swift */, + 9F872DA22B90920F00138637 /* BookmarkFolderInfo.swift */, ); path = Model; sourceTree = ""; @@ -9778,6 +9898,7 @@ 4B41EDA82B1543C9001EEDF4 /* PreferencesVPNView.swift in Sources */, 3706FA7B293F65D500E42796 /* FaviconUserScript.swift in Sources */, 3706FA7E293F65D500E42796 /* LottieAnimationCache.swift in Sources */, + 9F982F0E2B8224BF00231028 /* AddEditBookmarkFolderDialogViewModel.swift in Sources */, 3706FA7F293F65D500E42796 /* TabIndex.swift in Sources */, 3706FA80293F65D500E42796 /* TabLazyLoaderDataSource.swift in Sources */, 3706FA81293F65D500E42796 /* LoginImport.swift in Sources */, @@ -9793,6 +9914,7 @@ 3706FA89293F65D500E42796 /* CrashReportPromptPresenter.swift in Sources */, 3706FA8B293F65D500E42796 /* PreferencesRootView.swift in Sources */, 3706FA8C293F65D500E42796 /* AppStateChangedPublisher.swift in Sources */, + 9FEE986E2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift in Sources */, 3706FA8D293F65D500E42796 /* BookmarkTableCellView.swift in Sources */, 3706FA8E293F65D500E42796 /* BookmarkManagementSidebarViewController.swift in Sources */, 3706FA8F293F65D500E42796 /* NSStackViewExtension.swift in Sources */, @@ -9843,6 +9965,7 @@ 4B9DB0332A983B24000927DB /* EnableWaitlistFeatureView.swift in Sources */, 3706FABA293F65D500E42796 /* BookmarkOutlineViewDataSource.swift in Sources */, 3706FABB293F65D500E42796 /* PasswordManagementBitwardenItemView.swift in Sources */, + 9FA173E42B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift in Sources */, 3706FABD293F65D500E42796 /* NSNotificationName+PasswordManager.swift in Sources */, 3706FABE293F65D500E42796 /* RulesCompilationMonitor.swift in Sources */, 3706FABF293F65D500E42796 /* CrashReportReader.swift in Sources */, @@ -9884,7 +10007,6 @@ 3706FADE293F65D500E42796 /* PreferencesDuckPlayerView.swift in Sources */, EEC4A65E2B277E8D00F7C0AA /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */, B66260E729ACAE4B00E9E3EE /* NavigationHotkeyHandler.swift in Sources */, - 3706FADF293F65D500E42796 /* AddBookmarkFolderModalView.swift in Sources */, 3706FAE0293F65D500E42796 /* BookmarkSidebarTreeController.swift in Sources */, 3706FAE1293F65D500E42796 /* HomePageFavoritesModel.swift in Sources */, 3706FAE2293F65D500E42796 /* SequenceExtensions.swift in Sources */, @@ -9904,6 +10026,7 @@ 3706FAF1293F65D500E42796 /* PreferencesAboutView.swift in Sources */, 3706FAF2293F65D500E42796 /* ContentBlocking.swift in Sources */, 31F2D2002AF026D800BF0144 /* WaitlistTermsAndConditionsActionHandler.swift in Sources */, + 9FA173E02B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift in Sources */, 3706FAF3293F65D500E42796 /* LocalAuthenticationService.swift in Sources */, 1D36E659298AA3BA00AA485D /* InternalUserDeciderStore.swift in Sources */, B6BCC5242AFCDABB002C5499 /* DataImportSourceViewModel.swift in Sources */, @@ -10045,7 +10168,6 @@ 3706FB5E293F65D500E42796 /* EncryptionKeyStore.swift in Sources */, 3706FB60293F65D500E42796 /* PasswordManagementIdentityItemView.swift in Sources */, 3706FB61293F65D500E42796 /* ProgressExtension.swift in Sources */, - B6F9BDD92B45B7D900677B33 /* AddBookmarkModalViewModel.swift in Sources */, 3706FB62293F65D500E42796 /* CSVParser.swift in Sources */, 3706FB64293F65D500E42796 /* PixelDataModel.xcdatamodeld in Sources */, B626A75B29921FAA00053070 /* NavigationActionPolicyExtension.swift in Sources */, @@ -10058,6 +10180,7 @@ 3706FB69293F65D500E42796 /* NavigationBarBadgeAnimationView.swift in Sources */, 1D1A334A2A6FEB170080ACED /* BurnerMode.swift in Sources */, B603971B29BA084C00902A34 /* JSAlertController.swift in Sources */, + 9FEE986A2B85B869002E44E8 /* BookmarksDialogViewModel.swift in Sources */, 3706FB6A293F65D500E42796 /* AddressBarButton.swift in Sources */, 4B41EDA42B1543B9001EEDF4 /* VPNPreferencesModel.swift in Sources */, 3706FB6C293F65D500E42796 /* FaviconStore.swift in Sources */, @@ -10066,7 +10189,6 @@ 4B4D60C62A0C849600BCD287 /* NetworkProtectionInviteCodeViewModel.swift in Sources */, 3706FB6F293F65D500E42796 /* BookmarkListViewController.swift in Sources */, 3706FB70293F65D500E42796 /* SecureVaultLoginImporter.swift in Sources */, - 3706FB71293F65D500E42796 /* AddBookmarkModalView.swift in Sources */, 3706FB72293F65D500E42796 /* RecentlyClosedCoordinator.swift in Sources */, 3706FB74293F65D500E42796 /* FaviconHostReference.swift in Sources */, B69A14F32B4D6FE800B9417D /* AddBookmarkFolderPopoverViewModel.swift in Sources */, @@ -10075,6 +10197,7 @@ 37CBCA9B2A8966E60050218F /* SyncSettingsAdapter.swift in Sources */, 3707C72A294B5D2900682A9F /* URLExtension.swift in Sources */, 3706FB76293F65D500E42796 /* ASN1Parser.swift in Sources */, + 9F56CFAA2B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift in Sources */, 37FD78122A29EBD100B36DB1 /* SyncErrorHandler.swift in Sources */, 987799F42999993C005D8EB6 /* LegacyBookmarksStoreMigration.swift in Sources */, 3706FB7A293F65D500E42796 /* FileDownloadManager.swift in Sources */, @@ -10134,7 +10257,6 @@ 3706FB97293F65D500E42796 /* ActionSpeech.swift in Sources */, 3706FB99293F65D500E42796 /* PrivacySecurityPreferences.swift in Sources */, B6AFE6BD29A5D621002FF962 /* HTTPSUpgradeTabExtension.swift in Sources */, - B6F9BDE12B45C1A800677B33 /* AddBookmarkFolderModalViewModel.swift in Sources */, 3706FB9A293F65D500E42796 /* FireproofDomainsStore.swift in Sources */, 3706FB9B293F65D500E42796 /* PrivacyDashboardPermissionHandler.swift in Sources */, 3706FB9C293F65D500E42796 /* TabCollectionViewModel.swift in Sources */, @@ -10147,6 +10269,7 @@ 3706FB9E293F65D500E42796 /* AboutModel.swift in Sources */, 3706FB9F293F65D500E42796 /* PasswordManagementCreditCardItemView.swift in Sources */, 3706FBA0293F65D500E42796 /* NSTextFieldExtension.swift in Sources */, + 9FA173E82B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift in Sources */, 3706FBA1293F65D500E42796 /* FireproofDomainsContainer.swift in Sources */, 3706FBA2293F65D500E42796 /* GeolocationService.swift in Sources */, 4B4D60C42A0C849600BCD287 /* NetworkProtectionInvitePresenter.swift in Sources */, @@ -10222,6 +10345,7 @@ 3706FBD9293F65D500E42796 /* NSAppearanceExtension.swift in Sources */, 3706FBDA293F65D500E42796 /* PermissionManager.swift in Sources */, 3706FBDB293F65D500E42796 /* DefaultBrowserPreferences.swift in Sources */, + 9FEE98662B846870002E44E8 /* AddEditBookmarkView.swift in Sources */, 3706FBDC293F65D500E42796 /* Permissions.xcdatamodeld in Sources */, 4B41EDB52B168C55001EEDF4 /* VPNFeedbackFormViewModel.swift in Sources */, 3706FBDD293F65D500E42796 /* PaddedImageButton.swift in Sources */, @@ -10236,6 +10360,7 @@ EEC4A6722B2C90AB00F7C0AA /* VPNLocationPreferenceItem.swift in Sources */, 3706FBE0293F65D500E42796 /* NSException+Catch.m in Sources */, 3706FBE1293F65D500E42796 /* AppStateRestorationManager.swift in Sources */, + 9FA173EC2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift in Sources */, 3706FBE2293F65D500E42796 /* ClickToLoadUserScript.swift in Sources */, 3706FBE3293F65D500E42796 /* WindowControllersManager.swift in Sources */, 37197EAA2942443D00394917 /* ModalSheetCancellable.swift in Sources */, @@ -10257,6 +10382,7 @@ 3706FBF0293F65D500E42796 /* PasswordManagementItemModel.swift in Sources */, 3706FBF2293F65D500E42796 /* FindInPageModel.swift in Sources */, 1D9A4E5B2B43213B00F449E2 /* TabSnapshotExtension.swift in Sources */, + 9F56CFAE2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift in Sources */, 3706FBF3293F65D500E42796 /* PseudoFolder.swift in Sources */, 1D26EBAD2B74BECB0002A93F /* NSImageSendable.swift in Sources */, 3706FBF5293F65D500E42796 /* PixelDataStore.swift in Sources */, @@ -10278,6 +10404,7 @@ 3706FC01293F65D500E42796 /* ChromiumBookmarksReader.swift in Sources */, 3706FC02293F65D500E42796 /* Downloads.xcdatamodeld in Sources */, B60C6F7829B0E286007BFAA8 /* SearchNonexistentDomainNavigationResponder.swift in Sources */, + 9F56CFB22B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift in Sources */, 3707C720294B5D2900682A9F /* WKWebsiteDataStoreExtension.swift in Sources */, 3706FC03293F65D500E42796 /* TabPreviewViewController.swift in Sources */, 4B9754EC2984300100D7B834 /* EmailManagerExtension.swift in Sources */, @@ -10301,6 +10428,7 @@ 3706FC0C293F65D500E42796 /* NSAttributedStringExtension.swift in Sources */, C1DAF3B62B9A44860059244F /* AutofillPopoverPresenter.swift in Sources */, 3706FC0D293F65D500E42796 /* AnimationView.swift in Sources */, + 9FA173DB2B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift in Sources */, 3706FC0E293F65D500E42796 /* NSRectExtension.swift in Sources */, 3706FC0F293F65D500E42796 /* YoutubeOverlayUserScript.swift in Sources */, 3775913729AB9A1C00E26367 /* SyncManagementDialogViewController.swift in Sources */, @@ -10385,12 +10513,14 @@ 3706FC50293F65D500E42796 /* FeedbackWindow.swift in Sources */, 3706FC51293F65D500E42796 /* RecentlyVisitedView.swift in Sources */, B645D8F729FA95440024461F /* WKProcessPoolExtension.swift in Sources */, + 9F514F922B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift in Sources */, 3706FC52293F65D500E42796 /* MouseOverAnimationButton.swift in Sources */, B60293E72BA19ECD0033186B /* NetPPopoverManagerMock.swift in Sources */, 3706FC53293F65D500E42796 /* TabBarScrollView.swift in Sources */, 3706FC54293F65D500E42796 /* BookmarkListTreeControllerDataSource.swift in Sources */, 3706FC55293F65D500E42796 /* AddressBarViewController.swift in Sources */, 3706FC56293F65D500E42796 /* Permissions.swift in Sources */, + 9F872D992B8DA9F800138637 /* Bookmarks+Tab.swift in Sources */, B6B4D1D02B0E0DD000C26286 /* DataImportNoDataView.swift in Sources */, 3706FC57293F65D500E42796 /* TabPreviewWindowController.swift in Sources */, 3706FC58293F65D500E42796 /* NSSizeExtension.swift in Sources */, @@ -10415,6 +10545,7 @@ 3706FC65293F65D500E42796 /* HomePageViewController.swift in Sources */, 3706FC67293F65D500E42796 /* OperatingSystemVersionExtension.swift in Sources */, B6F9BDE52B45CD1900677B33 /* ModalView.swift in Sources */, + 9F872DA42B90920F00138637 /* BookmarkFolderInfo.swift in Sources */, B677FC502B06376B0099EB04 /* ReportFeedbackView.swift in Sources */, 3706FC68293F65D500E42796 /* ToggleableScrollView.swift in Sources */, 3706FC69293F65D500E42796 /* UserScripts.swift in Sources */, @@ -10432,6 +10563,7 @@ 3706FC73293F65D500E42796 /* AddressBarButtonsViewController.swift in Sources */, 3706FC76293F65D500E42796 /* PixelDataRecord.swift in Sources */, 7BFE955A2A9DF4550081ABE9 /* NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift in Sources */, + 9FDA6C222B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */, 3706FC77293F65D500E42796 /* PageObserverUserScript.swift in Sources */, 4BF0E5132AD25A2600FFEC9E /* DuckDuckGoUserAgent.swift in Sources */, 3706FC78293F65D500E42796 /* SecureVaultErrorReporter.swift in Sources */, @@ -10508,6 +10640,7 @@ 3706FDDE293F661700E42796 /* SuggestionViewModelTests.swift in Sources */, 3706FDDF293F661700E42796 /* BookmarkSidebarTreeControllerTests.swift in Sources */, 3706FDE0293F661700E42796 /* TabIndexTests.swift in Sources */, + 9F26060F2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift in Sources */, 3706FDE1293F661700E42796 /* AdjacentItemEnumeratorTests.swift in Sources */, 3706FDE2293F661700E42796 /* PixelArgumentsTests.swift in Sources */, 4B9DB0572A983B55000927DB /* MockNotificationService.swift in Sources */, @@ -10536,6 +10669,7 @@ 3706FDF8293F661700E42796 /* FileStoreTests.swift in Sources */, 5603D90729B7B746007F9F01 /* MockTabViewItemDelegate.swift in Sources */, 3706FDF9293F661700E42796 /* TabViewModelTests.swift in Sources */, + 9F872DA12B90644800138637 /* ContextualMenuTests.swift in Sources */, 3706FDFA293F661700E42796 /* DefaultBrowserPreferencesTests.swift in Sources */, 3706FDFB293F661700E42796 /* DispatchQueueExtensionsTests.swift in Sources */, 9F180D102B69C553000D695F /* Tab+WKUIDelegateTests.swift in Sources */, @@ -10599,6 +10733,7 @@ 3706FE26293F661700E42796 /* TemporaryFileCreator.swift in Sources */, 3706FE27293F661700E42796 /* AppPrivacyConfigurationTests.swift in Sources */, B626A7652992506A00053070 /* SerpHeadersNavigationResponderTests.swift in Sources */, + 9F26060C2B85C20B00819292 /* AddEditBookmarkDialogViewModelTests.swift in Sources */, 3706FE28293F661700E42796 /* BookmarkTests.swift in Sources */, 3706FE29293F661700E42796 /* SuggestionContainerViewModelTests.swift in Sources */, 1D8C2FEB2B70F5A7005E4BBD /* MockWebViewSnapshotRenderer.swift in Sources */, @@ -10652,6 +10787,7 @@ 3706FE4A293F661700E42796 /* BookmarkManagedObjectTests.swift in Sources */, EEC8EB402982CD550065AA39 /* JSAlertViewModelTests.swift in Sources */, 3706FE4B293F661700E42796 /* BookmarksHTMLImporterTests.swift in Sources */, + 9FA75A3F2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift in Sources */, 56D145E929E6BB6300E3488A /* CapturingDataImportProvider.swift in Sources */, 3706FE4C293F661700E42796 /* CSVParserTests.swift in Sources */, 3706FE4D293F661700E42796 /* OnboardingTests.swift in Sources */, @@ -10665,6 +10801,7 @@ 3706FE54293F661700E42796 /* PasteboardBookmarkTests.swift in Sources */, 3706FE55293F661700E42796 /* CBRCompileTimeReporterTests.swift in Sources */, 566B196429CDB824007E38F4 /* MoreOptionsMenuTests.swift in Sources */, + 9F0A2CF92B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift in Sources */, 3706FE56293F661700E42796 /* FaviconManagerMock.swift in Sources */, 3706FE57293F661700E42796 /* LocalPinningManagerTests.swift in Sources */, 3706FE58293F661700E42796 /* HistoryStoreTests.swift in Sources */, @@ -10688,6 +10825,7 @@ 3706FE64293F661700E42796 /* DownloadListStoreTests.swift in Sources */, 3706FE65293F661700E42796 /* ContentBlockingUpdatingTests.swift in Sources */, 3706FE67293F661700E42796 /* EncryptionMocks.swift in Sources */, + 9F872D9E2B9058D000138637 /* Bookmarks+TabTests.swift in Sources */, 3706FE68293F661700E42796 /* DuckPlayerURLExtensionTests.swift in Sources */, 3706FE6A293F661700E42796 /* FirefoxKeyReaderTests.swift in Sources */, 3706FE6B293F661700E42796 /* AppKitPrivateMethodsAvailabilityTests.swift in Sources */, @@ -10719,6 +10857,7 @@ 3706FE7B293F661700E42796 /* HistoryStoringMock.swift in Sources */, 562984702AC4610100AC20EB /* SyncPreferencesTests.swift in Sources */, 3706FE7C293F661700E42796 /* LocalBookmarkStoreTests.swift in Sources */, + 9F982F142B822C7400231028 /* AddEditBookmarkFolderDialogViewModelTests.swift in Sources */, B6CA4825298CE4B70067ECCE /* AdClickAttributionTabExtensionTests.swift in Sources */, 3707C72D294B5D4100682A9F /* EmptyAttributionRulesProver.swift in Sources */, 376E2D2629428353001CD31B /* PrivacyReferenceTestHelper.swift in Sources */, @@ -10968,6 +11107,7 @@ 4B9579552AC7AE700062CA31 /* Logging.swift in Sources */, 4B9579562AC7AE700062CA31 /* CrashReportPromptPresenter.swift in Sources */, B6B4D1CD2B0C8C9200C26286 /* FirefoxCompatibilityPreferences.swift in Sources */, + 9FA173ED2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift in Sources */, 4B9579572AC7AE700062CA31 /* BWCredential.swift in Sources */, 4B9579582AC7AE700062CA31 /* PreferencesRootView.swift in Sources */, 4B9579592AC7AE700062CA31 /* AppStateChangedPublisher.swift in Sources */, @@ -10977,6 +11117,7 @@ 4B95795D2AC7AE700062CA31 /* OptionalExtension.swift in Sources */, 4B95795E2AC7AE700062CA31 /* PasswordManagementLoginItemView.swift in Sources */, 4B95795F2AC7AE700062CA31 /* UserText.swift in Sources */, + 9F872D9A2B8DA9F800138637 /* Bookmarks+Tab.swift in Sources */, 4B9579602AC7AE700062CA31 /* WKWebView+Download.swift in Sources */, 4B9579612AC7AE700062CA31 /* TabShadowConfig.swift in Sources */, 4B9579622AC7AE700062CA31 /* URLSessionExtension.swift in Sources */, @@ -10997,6 +11138,8 @@ 4B95796E2AC7AE700062CA31 /* LegacyBookmarkStore.swift in Sources */, 4B95796F2AC7AE700062CA31 /* NSAlert+DataImport.swift in Sources */, 4B9579702AC7AE700062CA31 /* MainWindow.swift in Sources */, + 9F872DA52B90920F00138637 /* BookmarkFolderInfo.swift in Sources */, + 9FEE986B2B85B869002E44E8 /* BookmarksDialogViewModel.swift in Sources */, 4B9579712AC7AE700062CA31 /* CrashReportPromptViewController.swift in Sources */, 4B9579722AC7AE700062CA31 /* BookmarksCleanupErrorHandling.swift in Sources */, 4B9579732AC7AE700062CA31 /* ContextMenuManager.swift in Sources */, @@ -11009,6 +11152,7 @@ 4B9579792AC7AE700062CA31 /* BWRequest.swift in Sources */, 4B95797A2AC7AE700062CA31 /* WKWebViewConfigurationExtensions.swift in Sources */, 4B95797B2AC7AE700062CA31 /* HomePageDefaultBrowserModel.swift in Sources */, + 9F514F932B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift in Sources */, 4B95797C2AC7AE700062CA31 /* CrashReporter.swift in Sources */, 4B95797D2AC7AE700062CA31 /* AddressBarTextSelectionNavigation.swift in Sources */, 4B37EE7D2B4CFF8300A89A61 /* SurveyURLBuilder.swift in Sources */, @@ -11069,7 +11213,6 @@ 4B9579B52AC7AE700062CA31 /* FirefoxLoginReader.swift in Sources */, 4B9579B62AC7AE700062CA31 /* AtbParser.swift in Sources */, 4B9579B72AC7AE700062CA31 /* PreferencesDuckPlayerView.swift in Sources */, - 4B9579B82AC7AE700062CA31 /* AddBookmarkFolderModalView.swift in Sources */, 4B41EDB62B169883001EEDF4 /* VPNFeedbackFormViewController.swift in Sources */, 4B9579B92AC7AE700062CA31 /* BookmarkSidebarTreeController.swift in Sources */, 4B9579BA2AC7AE700062CA31 /* HomePageFavoritesModel.swift in Sources */, @@ -11123,6 +11266,7 @@ 4B9579E42AC7AE700062CA31 /* PopUpWindow.swift in Sources */, 4B9579E52AC7AE700062CA31 /* Favicons.xcdatamodeld in Sources */, 4B9579E62AC7AE700062CA31 /* Publisher.asVoid.swift in Sources */, + 9FEE986F2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift in Sources */, 4B9579E72AC7AE700062CA31 /* Waitlist.swift in Sources */, 3158B1582B0BF76000AF130C /* DataBrokerProtectionFeatureVisibility.swift in Sources */, 4B9579E82AC7AE700062CA31 /* NavigationButtonMenuDelegate.swift in Sources */, @@ -11186,6 +11330,7 @@ 4B957A202AC7AE700062CA31 /* CancellableExtension.swift in Sources */, 4B957A212AC7AE700062CA31 /* PinnedTabsHostingView.swift in Sources */, 4B957A222AC7AE700062CA31 /* FirefoxBookmarksReader.swift in Sources */, + 9F982F0F2B8224BF00231028 /* AddEditBookmarkFolderDialogViewModel.swift in Sources */, 4B0526622B1D55320054955A /* VPNFeedbackSender.swift in Sources */, 4B957A232AC7AE700062CA31 /* DeviceIdleStateDetector.swift in Sources */, 85D0327D2B8E3D090041D1FB /* HistoryCoordinatorExtension.swift in Sources */, @@ -11209,6 +11354,7 @@ 4B957A332AC7AE700062CA31 /* Favicon.swift in Sources */, 1E2AE4CA2ACB21A000684E0A /* NetworkProtectionRemoteMessage.swift in Sources */, 4B957A342AC7AE700062CA31 /* SuggestionContainerViewModel.swift in Sources */, + 9F56CFAF2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift in Sources */, 4B957A352AC7AE700062CA31 /* FirePopoverWrapperViewController.swift in Sources */, 4B957A362AC7AE700062CA31 /* NSPasteboardItemExtension.swift in Sources */, 4B957A372AC7AE700062CA31 /* AutofillPreferencesModel.swift in Sources */, @@ -11246,6 +11392,7 @@ 4B957A542AC7AE700062CA31 /* VisitMenuItem.swift in Sources */, 4B957A552AC7AE700062CA31 /* EncryptionKeyStore.swift in Sources */, 4B957A562AC7AE700062CA31 /* TabExtensionsBuilder.swift in Sources */, + 9F56CFB32B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift in Sources */, 1E2AE4C82ACB216B00684E0A /* HoverTrackingArea.swift in Sources */, 4B957A582AC7AE700062CA31 /* PasswordManagementIdentityItemView.swift in Sources */, 4B957A592AC7AE700062CA31 /* ProgressExtension.swift in Sources */, @@ -11270,7 +11417,6 @@ 4B957A692AC7AE700062CA31 /* BookmarkListViewController.swift in Sources */, 4B957A6A2AC7AE700062CA31 /* SecureVaultLoginImporter.swift in Sources */, 4B957A6B2AC7AE700062CA31 /* WKProcessPoolExtension.swift in Sources */, - 4B957A6C2AC7AE700062CA31 /* AddBookmarkModalView.swift in Sources */, 4B957A6D2AC7AE700062CA31 /* LoginItemsManager.swift in Sources */, 4B957A6E2AC7AE700062CA31 /* PixelExperiment.swift in Sources */, 4B957A6F2AC7AE700062CA31 /* DuckPlayerTabExtension.swift in Sources */, @@ -11359,6 +11505,7 @@ 4B957AB62AC7AE700062CA31 /* FireproofDomains.xcdatamodeld in Sources */, 3158B14F2B0BF74F00AF130C /* DataBrokerProtectionManager.swift in Sources */, 4B957AB82AC7AE700062CA31 /* HomePageView.swift in Sources */, + 9FEE98672B846870002E44E8 /* AddEditBookmarkView.swift in Sources */, 4B957AB92AC7AE700062CA31 /* SerpHeadersNavigationResponder.swift in Sources */, 4B957ABA2AC7AE700062CA31 /* HomePageContinueSetUpModel.swift in Sources */, 4B957ABB2AC7AE700062CA31 /* WebKitDownloadTask.swift in Sources */, @@ -11375,12 +11522,12 @@ 4B957AC42AC7AE700062CA31 /* BWVault.swift in Sources */, 4B957AC52AC7AE700062CA31 /* NSViewExtension.swift in Sources */, BBDFDC5C2B2B8D7000F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift in Sources */, + 9FA173E52B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift in Sources */, 4B957AC72AC7AE700062CA31 /* DownloadListViewModel.swift in Sources */, 4B957AC82AC7AE700062CA31 /* BookmarkManagementDetailViewController.swift in Sources */, 4B957AC92AC7AE700062CA31 /* CSVImporter.swift in Sources */, 4B957ACA2AC7AE700062CA31 /* StartupPreferences.swift in Sources */, 4B957ACB2AC7AE700062CA31 /* UserDefaults+NetworkProtectionWaitlist.swift in Sources */, - B6F9BDE22B45C1A800677B33 /* AddBookmarkFolderModalViewModel.swift in Sources */, 4B957ACC2AC7AE700062CA31 /* MainMenu.swift in Sources */, 4B957ACE2AC7AE700062CA31 /* BrowserTabViewController.swift in Sources */, 4B957ACF2AC7AE700062CA31 /* CallToAction.swift in Sources */, @@ -11395,6 +11542,7 @@ 4B957AD72AC7AE700062CA31 /* CustomRoundedCornersShape.swift in Sources */, 4B957AD82AC7AE700062CA31 /* LocaleExtension.swift in Sources */, 4B957AD92AC7AE700062CA31 /* SavePaymentMethodViewController.swift in Sources */, + 9FA173E92B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift in Sources */, 4B957ADA2AC7AE700062CA31 /* BWStatus.swift in Sources */, 4B957ADB2AC7AE700062CA31 /* WebKitVersionProvider.swift in Sources */, B6BCC54D2AFDF24B002C5499 /* TaskWithProgress.swift in Sources */, @@ -11422,6 +11570,7 @@ 4B957AF02AC7AE700062CA31 /* NSException+Catch.m in Sources */, 4B957AF12AC7AE700062CA31 /* AppStateRestorationManager.swift in Sources */, 4B957AF22AC7AE700062CA31 /* DailyPixel.swift in Sources */, + 9FDA6C232B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */, 4B957AF32AC7AE700062CA31 /* NavigationHotkeyHandler.swift in Sources */, 1EA7B8DA2B7E1283000330A4 /* SubscriptionFeatureAvailability.swift in Sources */, 4B957AF42AC7AE700062CA31 /* ClickToLoadUserScript.swift in Sources */, @@ -11462,7 +11611,6 @@ B66CA4212AD910B300447CF0 /* DataImportView.swift in Sources */, 4B957B162AC7AE700062CA31 /* Downloads.xcdatamodeld in Sources */, 4B957B172AC7AE700062CA31 /* TabPreviewViewController.swift in Sources */, - B6F9BDDA2B45B7D900677B33 /* AddBookmarkModalViewModel.swift in Sources */, 4B957B182AC7AE700062CA31 /* PreferencesPrivacyView.swift in Sources */, 4B957B192AC7AE700062CA31 /* NSPasteboardExtension.swift in Sources */, 4B957B1A2AC7AE700062CA31 /* OnboardingViewModel.swift in Sources */, @@ -11548,6 +11696,7 @@ 4B957B612AC7AE700062CA31 /* HomePage.swift in Sources */, 4B957B622AC7AE700062CA31 /* RoundedSelectionRowView.swift in Sources */, B6A22B652B1E29D000ECD2BA /* DataImportSummaryViewModel.swift in Sources */, + 9FA173E12B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift in Sources */, 4B957B632AC7AE700062CA31 /* LocalStatisticsStore.swift in Sources */, 4B957B642AC7AE700062CA31 /* BackForwardListItem.swift in Sources */, 4B957B672AC7AE700062CA31 /* AtbAndVariantCleanup.swift in Sources */, @@ -11623,6 +11772,8 @@ 4B957BA12AC7AE700062CA31 /* UserDefaults+NetworkProtectionShared.swift in Sources */, 4B957BA22AC7AE700062CA31 /* NavigationActionPolicyExtension.swift in Sources */, 4B957BA32AC7AE700062CA31 /* CIImageExtension.swift in Sources */, + 9F56CFAB2B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift in Sources */, + 9FA173DC2B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift in Sources */, 4B957BA42AC7AE700062CA31 /* NSMenuExtension.swift in Sources */, 4B957BA52AC7AE700062CA31 /* MainWindowController.swift in Sources */, 4B957BA62AC7AE700062CA31 /* Tab.swift in Sources */, @@ -11783,7 +11934,6 @@ 1D1A33492A6FEB170080ACED /* BurnerMode.swift in Sources */, 14505A08256084EF00272CC6 /* UserAgent.swift in Sources */, 987799F12999993C005D8EB6 /* LegacyBookmarkStore.swift in Sources */, - B6F9BDE02B45C1A800677B33 /* AddBookmarkFolderModalViewModel.swift in Sources */, 4B8AC93526B3B2FD00879451 /* NSAlert+DataImport.swift in Sources */, AA7412BD24D2BEEE00D22FE0 /* MainWindow.swift in Sources */, AAD6D8882696DF6D002393B3 /* CrashReportPromptViewController.swift in Sources */, @@ -11862,7 +12012,6 @@ 4B8AC93926B48A5100879451 /* FirefoxLoginReader.swift in Sources */, B69B503E2726A12500758A2B /* AtbParser.swift in Sources */, 37F19A6528E1B3FB00740DC6 /* PreferencesDuckPlayerView.swift in Sources */, - 4B9292D22667123700AD2C21 /* AddBookmarkFolderModalView.swift in Sources */, 4B92929E26670D2A00AD2C21 /* BookmarkSidebarTreeController.swift in Sources */, EEC4A6712B2C90AB00F7C0AA /* VPNLocationPreferenceItem.swift in Sources */, 85589E8727BBB8F20038AD11 /* HomePageFavoritesModel.swift in Sources */, @@ -11871,6 +12020,7 @@ 4B59024026B35F3600489384 /* ChromiumDataImporter.swift in Sources */, B62B48562ADE730D000DECE5 /* FileImportView.swift in Sources */, AAA0CC3C25337FAB0079BC96 /* BackForwardListItemViewModel.swift in Sources */, + 9F982F0D2B8224BF00231028 /* AddEditBookmarkFolderDialogViewModel.swift in Sources */, 1D43EB3429297D760065E5D6 /* BWNotRespondingAlert.swift in Sources */, 4BB88B4525B7B55C006F6B06 /* DebugUserScript.swift in Sources */, AAC6881928626BF800D54247 /* RecentlyClosedTab.swift in Sources */, @@ -11897,6 +12047,7 @@ AA92127725ADA07900600CD4 /* WKWebViewExtension.swift in Sources */, AAAB9114288EB1D600A057A9 /* CleanThisHistoryMenuItem.swift in Sources */, B6C0B23626E732000031CB7F /* DownloadListItem.swift in Sources */, + 9F872DA32B90920F00138637 /* BookmarkFolderInfo.swift in Sources */, 4B9DB0232A983B24000927DB /* WaitlistRequest.swift in Sources */, B6B1E87E26D5DA0E0062C350 /* DownloadsPopover.swift in Sources */, 85774AFF2A713D3B00DE0561 /* BookmarksBarMenuFactory.swift in Sources */, @@ -11968,6 +12119,7 @@ 85C6A29625CC1FFD00EEB5F1 /* UserDefaultsWrapper.swift in Sources */, 85625998269C9C5F00EE44BC /* PasswordManagementPopover.swift in Sources */, 1DDF076328F815AD00EDFBE3 /* BWCommunicator.swift in Sources */, + 9FEE98652B846870002E44E8 /* AddEditBookmarkView.swift in Sources */, 85589E9127BFB9810038AD11 /* HomePageRecentlyVisitedModel.swift in Sources */, 85012B0229133F9F003D0DCC /* NavigationBarPopovers.swift in Sources */, B626A7602992407D00053070 /* CancellableExtension.swift in Sources */, @@ -12053,9 +12205,9 @@ 4B6785472AA8DE68008A5004 /* NetworkProtectionFeatureDisabler.swift in Sources */, 4B0526642B1D55D80054955A /* VPNFeedbackCategory.swift in Sources */, 4B9292D42667123700AD2C21 /* BookmarkListViewController.swift in Sources */, + 9F56CFB12B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift in Sources */, 4B723E0D26B0006100E14D75 /* SecureVaultLoginImporter.swift in Sources */, B645D8F629FA95440024461F /* WKProcessPoolExtension.swift in Sources */, - 4B9292D32667123700AD2C21 /* AddBookmarkModalView.swift in Sources */, 9D9AE86B2AA76CF90026E7DC /* LoginItemsManager.swift in Sources */, 857E5AF52A79045800FC0FB4 /* PixelExperiment.swift in Sources */, B6C416A7294A4AE500C4F2E7 /* DuckPlayerTabExtension.swift in Sources */, @@ -12082,6 +12234,7 @@ AA9E9A5625A3AE8400D1959D /* NSWindowExtension.swift in Sources */, 7BD3AF5D2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift in Sources */, 370A34B12AB24E3700C77F7C /* SyncDebugMenu.swift in Sources */, + 9FDA6C212B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */, AAC5E4C725D6A6E8007F5990 /* AddBookmarkPopover.swift in Sources */, 37CC53F027E8D1440028713D /* PreferencesDownloadsView.swift in Sources */, B6F7127E29F6779000594A45 /* QRSharingService.swift in Sources */, @@ -12090,6 +12243,7 @@ 3171D6BA288984D00068632A /* BadgeAnimationView.swift in Sources */, 1DB67F292B6FE4A6003DF243 /* WebViewSnapshotRenderer.swift in Sources */, 4B9292CE2667123700AD2C21 /* BrowserTabSelectionDelegate.swift in Sources */, + 9FEE986D2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift in Sources */, B6BCC53B2AFD15DF002C5499 /* DataImportProfilePicker.swift in Sources */, 3158B1562B0BF75D00AF130C /* DataBrokerProtectionFeatureVisibility.swift in Sources */, 56D6A3D629DB2BAB0055215A /* ContinueSetUpView.swift in Sources */, @@ -12142,7 +12296,6 @@ AA6EF9B525081B4C004754E6 /* MainMenuActions.swift in Sources */, B63D466925BEB6C200874977 /* WKWebView+SessionState.swift in Sources */, 4B4D60C02A0C848D00BCD287 /* NetworkProtectionControllerErrorStore.swift in Sources */, - B6F9BDD82B45B7D900677B33 /* AddBookmarkModalViewModel.swift in Sources */, 4B723E1226B0006E00E14D75 /* DataImport.swift in Sources */, 7BE146072A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift in Sources */, B6085D092743AAB600A9C456 /* FireproofDomains.xcdatamodeld in Sources */, @@ -12155,6 +12308,7 @@ 1DB67F2D2B6FEFDB003DF243 /* ViewSnapshotRenderer.swift in Sources */, 4B44FEF32B1FEF5A000619D8 /* FocusableTextEditor.swift in Sources */, 4B59023E26B35F3600489384 /* ChromiumLoginReader.swift in Sources */, + 9F872D982B8DA9F800138637 /* Bookmarks+Tab.swift in Sources */, 85D885B326A5A9DE0077C374 /* NSAlert+PasswordManager.swift in Sources */, 983DFB2528B67036006B7E34 /* UserContentUpdating.swift in Sources */, 1D9A4E5A2B43213B00F449E2 /* TabSnapshotExtension.swift in Sources */, @@ -12188,6 +12342,7 @@ 4BE4005527CF3F19007D3161 /* SavePaymentMethodViewController.swift in Sources */, 1D2DC009290167A0008083A1 /* BWStatus.swift in Sources */, AAFE068326C7082D005434CC /* WebKitVersionProvider.swift in Sources */, + 9FEE98692B85B869002E44E8 /* BookmarksDialogViewModel.swift in Sources */, B63D467A25BFC3E100874977 /* NSCoderExtensions.swift in Sources */, 1D2DC00B290167EC008083A1 /* RunningApplicationCheck.swift in Sources */, B6A5A27125B9377300AA7ADA /* StatePersistenceService.swift in Sources */, @@ -12197,6 +12352,7 @@ 4BB88B5B25B7BA50006F6B06 /* Instruments.swift in Sources */, 9812D895276CEDA5004B6181 /* ContentBlockerRulesLists.swift in Sources */, 4B0511E2262CAA8600F6079C /* NSViewControllerExtension.swift in Sources */, + 9FA173DF2B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift in Sources */, F44C130225C2DA0400426E3E /* NSAppearanceExtension.swift in Sources */, 4B3B8490297A0E1000A384BD /* EmailManagerExtension.swift in Sources */, B64C84F1269310120048FEBE /* PermissionManager.swift in Sources */, @@ -12263,6 +12419,7 @@ 4BB99D0026FE191E001E4761 /* CoreDataBookmarkImporter.swift in Sources */, 4BCF15D72ABB8A110083F6DF /* NetworkProtectionRemoteMessaging.swift in Sources */, C168B9AC2B31DC7E001AFAD9 /* AutofillNeverPromptWebsitesManager.swift in Sources */, + 9FA173E72B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift in Sources */, D64A5FF82AEA5C2B00B6D6E7 /* HomeButtonMenuFactory.swift in Sources */, 37A6A8F62AFCCA59008580A3 /* FaviconsFetcherOnboardingViewController.swift in Sources */, AA3F895324C18AD500628DDE /* SuggestionViewModel.swift in Sources */, @@ -12311,6 +12468,7 @@ 37F19A6728E1B43200740DC6 /* DuckPlayerPreferences.swift in Sources */, B6C0B22E26E61CE70031CB7F /* DownloadViewModel.swift in Sources */, 4B41EDA72B1543C9001EEDF4 /* PreferencesVPNView.swift in Sources */, + 9FA173DA2B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift in Sources */, 373A1AA8283ED1B900586521 /* BookmarkHTMLReader.swift in Sources */, B68458B825C7E8B200DC17B6 /* Tab+NSSecureCoding.swift in Sources */, 4B37EE612B4CFC3C00A89A61 /* SurveyURLBuilder.swift in Sources */, @@ -12358,10 +12516,12 @@ AAC5E4E425D6BA9C007F5990 /* NSSizeExtension.swift in Sources */, AA6820EB25503D6A005ED0D5 /* Fire.swift in Sources */, 3158B1492B0BF73000AF130C /* DBPHomeViewController.swift in Sources */, + 9F56CFA92B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift in Sources */, 37445F9C2A1569F00029F789 /* SyncBookmarksAdapter.swift in Sources */, C1E961EF2B87AA29001760E1 /* AutofillActionBuilder.swift in Sources */, B6AAAC3E26048F690029438D /* RandomAccessCollectionExtension.swift in Sources */, 4B9292AF26670F5300AD2C21 /* NSOutlineViewExtensions.swift in Sources */, + 9F56CFAD2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift in Sources */, AA585D82248FD31100E9A3E2 /* AppDelegate.swift in Sources */, 7B1E81A027C8874900FF0E60 /* ContentOverlayViewController.swift in Sources */, C1DAF3B52B9A44860059244F /* AutofillPopoverPresenter.swift in Sources */, @@ -12373,6 +12533,7 @@ 4B4D60BF2A0C848A00BCD287 /* NetworkProtection+ConvenienceInitializers.swift in Sources */, 3158B1592B0BF76400AF130C /* DataBrokerProtectionFeatureDisabler.swift in Sources */, B655124829A79465009BFE1C /* NavigationActionExtension.swift in Sources */, + 9FA173EB2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift in Sources */, 85308E25267FC9F2001ABD76 /* NSAlertExtension.swift in Sources */, B69A14F62B4D701F00B9417D /* AddBookmarkPopoverViewModel.swift in Sources */, 4B59024826B3673600489384 /* ThirdPartyBrowser.swift in Sources */, @@ -12454,6 +12615,8 @@ AA72D5FE25FFF94E00C77619 /* NSMenuItemExtension.swift in Sources */, 4BA1A6C2258B0A1300F6F690 /* ContiguousBytesExtension.swift in Sources */, B6A22B622B1E29D000ECD2BA /* DataImportSummaryViewModel.swift in Sources */, + 9F514F912B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift in Sources */, + 9FA173E32B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift in Sources */, 37534CA8281198CD002621E7 /* AdjacentItemEnumerator.swift in Sources */, 1EA7B8D82B7E1283000330A4 /* SubscriptionFeatureAvailability.swift in Sources */, 987799F62999996B005D8EB6 /* BookmarkDatabase.swift in Sources */, @@ -12492,6 +12655,7 @@ B6619F062B17138D00CD9186 /* DataImportSourceViewModelTests.swift in Sources */, 4BBF0917282DD6EF00EE1418 /* TemporaryFileHandlerTests.swift in Sources */, B6A5A27925B93FFF00AA7ADA /* StateRestorationManagerTests.swift in Sources */, + 9F982F132B822B7B00231028 /* AddEditBookmarkFolderDialogViewModelTests.swift in Sources */, B630E7FE29C887ED00363609 /* NSErrorAdditionalInfo.swift in Sources */, 4B9292BB2667103100AD2C21 /* BookmarkNodeTests.swift in Sources */, 4B0219A825E0646500ED7DEA /* WebsiteDataStoreTests.swift in Sources */, @@ -12502,6 +12666,7 @@ B6656E122B29E3BE008798A1 /* DownloadListStoreMock.swift in Sources */, 37D23780287EFEE200BCE03B /* PinnedTabsManagerTests.swift in Sources */, AA0877BA26D5161D00B05660 /* WebKitVersionProviderTests.swift in Sources */, + 9FA75A3E2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift in Sources */, B69B50462726C5C200758A2B /* AtbAndVariantCleanupTests.swift in Sources */, 567DA94529E95C3F008AC5EE /* YoutubeOverlayUserScriptTests.swift in Sources */, 4B59024C26B38BB800489384 /* ChromiumLoginReaderTests.swift in Sources */, @@ -12535,6 +12700,7 @@ 4B8AD0B127A86D9200AE44D6 /* WKWebsiteDataStoreExtensionTests.swift in Sources */, B69B50472726C5C200758A2B /* VariantManagerTests.swift in Sources */, 8546DE6225C03056000CA5E1 /* UserAgentTests.swift in Sources */, + 9F26060B2B85C20A00819292 /* AddEditBookmarkDialogViewModelTests.swift in Sources */, B63ED0DE26AFD9A300A9DAD1 /* AVCaptureDeviceMock.swift in Sources */, 98A95D88299A2DF900B9B81A /* BookmarkMigrationTests.swift in Sources */, B63ED0E026AFE32F00A9DAD1 /* GeolocationProviderMock.swift in Sources */, @@ -12562,12 +12728,14 @@ B63ED0E326B3E7FA00A9DAD1 /* CLLocationManagerMock.swift in Sources */, 37CD54BB27F25A4000F1F7B9 /* DownloadsPreferencesTests.swift in Sources */, 4BE344EE2B2376DF003FC223 /* VPNFeedbackFormViewModelTests.swift in Sources */, + 9F872D9D2B9058D000138637 /* Bookmarks+TabTests.swift in Sources */, 9F3910622B68C35600CB5112 /* DownloadsTabExtensionTests.swift in Sources */, 4B9DB0562A983B55000927DB /* MockNotificationService.swift in Sources */, 4B02199C25E063DE00ED7DEA /* FireproofDomainsTests.swift in Sources */, AA0F3DB7261A566C0077F2D9 /* SuggestionLoadingMock.swift in Sources */, B60C6F8129B1B4AD007BFAA8 /* TestRunHelper.swift in Sources */, 4B9DB0582A983B55000927DB /* MockNetworkProtectionCodeRedeemer.swift in Sources */, + 9F872DA02B90644800138637 /* ContextualMenuTests.swift in Sources */, 4B9292BE2667103100AD2C21 /* PasteboardFolderTests.swift in Sources */, 4B9292C52667104B00AD2C21 /* CoreDataTestUtilities.swift in Sources */, 4B723E1926B000DC00E14D75 /* TemporaryFileCreator.swift in Sources */, @@ -12589,6 +12757,7 @@ B63ED0DC26AE7B1E00A9DAD1 /* WebViewMock.swift in Sources */, 4B4F72EC266B2ED300814C60 /* CollectionExtension.swift in Sources */, AAE39D1B24F44885008EF28B /* TabCollectionViewModelDelegateMock.swift in Sources */, + 9F0A2CF82B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift in Sources */, 373A1AAA283ED86C00586521 /* BookmarksHTMLReaderTests.swift in Sources */, 317295D42AF058D3002C3206 /* MockWaitlistFeatureSetupHandler.swift in Sources */, AA9C363025518CA9004B1BA3 /* FireTests.swift in Sources */, @@ -12638,6 +12807,7 @@ 4BBF0925283083EC00EE1418 /* FileSystemDSLTests.swift in Sources */, 4B11060A25903EAC0039B979 /* CoreDataEncryptionTests.swift in Sources */, B603971029B9D67E00902A34 /* PublishersExtensions.swift in Sources */, + 9F26060E2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift in Sources */, 4B9292C32667103100AD2C21 /* PasteboardBookmarkTests.swift in Sources */, B610F2E427A8F37A00FCEBE9 /* CBRCompileTimeReporterTests.swift in Sources */, AABAF59C260A7D130085060C /* FaviconManagerMock.swift in Sources */, diff --git a/DuckDuckGo/Assets.xcassets/Images/AddBookmark.imageset/AddBookmark.svg b/DuckDuckGo/Assets.xcassets/Images/AddBookmark.imageset/AddBookmark.svg new file mode 100644 index 0000000000..624b357638 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/AddBookmark.imageset/AddBookmark.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/DuckDuckGo/Assets.xcassets/Images/AddBookmark.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/AddBookmark.imageset/Contents.json index fd82163d37..b3519a0b69 100644 --- a/DuckDuckGo/Assets.xcassets/Images/AddBookmark.imageset/Contents.json +++ b/DuckDuckGo/Assets.xcassets/Images/AddBookmark.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "icon-16-bookmark-add.pdf", + "filename" : "AddBookmark.svg", "idiom" : "universal" } ], diff --git a/DuckDuckGo/Assets.xcassets/Images/AddBookmark.imageset/icon-16-bookmark-add.pdf b/DuckDuckGo/Assets.xcassets/Images/AddBookmark.imageset/icon-16-bookmark-add.pdf deleted file mode 100644 index 27e0c7b892a5446cbfb1350213f2e4b25779bb98..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3871 zcmdT{TW{1x6n^)wm=`3d#O&NJ5{d)~L{*ER2@go94~t_$=w>(Fb*j)`-|vja$KGsO zklL48h>Sm<%Xcm_bN12OlUGl*j7_735}S`-8X=xN6N?uwHh1EKP*Rm=TIeuB)9P0! z;aQw@yUp#k?RLe)%e(&cykGX+@TSY3b_r8yuz7SO%D<|8e*3Zidee!;tIhVhVn285 z%l@-4;C5D_)9vQ=rr3H{q|5v8t*{uVoQuV~?Q*@lS#G;^`*rX8(NQ%u7rdXL=R(5=2*xX8BMk!z?hddy(Lob=AEHIya(h*WhJMAKn?2I>Az+Z}x7gk!O zvLRl;067KB1s#KeRLW5~k_sBh(LiEwCRrfx;J|?Nf-W&ybY!%~m3WNF8g!gg5v2%N zM5B^6BjT~JHo!Y4V2bWu}MpyG7&$d{8Tl%h|4z);&n znQC(|xM~vekkDu|<%AIFA*i?rVuF}T3)L!@r1s864bF^3B-K&FBuU{IBjDa!<)g?_ zc^7O1k`?kyxy;f;6Otz^p{FdQTu$0q7*J?`Yk;6_d`yJIsEjIl$vae!upAToWpsOC zpCno(qm5>WMX=~G@>iv%ZePtn-W5qw1EP%6hMLmId#rtE#kM}B+XTUlaOlg>5T6G_WCPFNf*Yv}`OXOZpdiw#hlRE!Pqfx+8ZV?~osLu1A;0vX(aBv4?> z@P9KI@%-@*6Oa2JPCUG~dGYOok42|x^17MU(+Zu=yNZ?ehMC$_Gi=hB&hiJ!PPwjS zw}z|k8KtV%A+gMz?{QO8d1zi7rp*UO-SgMW-EXr&m0tJe&}m%VUoB1rwOk!|_rD#y zEimamU)_9Oe%tg&_rL}Li(we%NdC^;II+XnxIe;Prs~L!Yir2RcTLpweqA87CmYKcXu3v8Zk{#=#Y92j$`R3JMbJ8YC diff --git a/DuckDuckGo/Assets.xcassets/Images/AddFolder.imageset/AddFolder.svg b/DuckDuckGo/Assets.xcassets/Images/AddFolder.imageset/AddFolder.svg new file mode 100644 index 0000000000..b9b806e64d --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/AddFolder.imageset/AddFolder.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/DuckDuckGo/Assets.xcassets/Images/AddFolder.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/AddFolder.imageset/Contents.json index b75b5d095f..55db1eefb9 100644 --- a/DuckDuckGo/Assets.xcassets/Images/AddFolder.imageset/Contents.json +++ b/DuckDuckGo/Assets.xcassets/Images/AddFolder.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "icon-16-folder-add.pdf", + "filename" : "AddFolder.svg", "idiom" : "universal" } ], diff --git a/DuckDuckGo/Assets.xcassets/Images/AddFolder.imageset/icon-16-folder-add.pdf b/DuckDuckGo/Assets.xcassets/Images/AddFolder.imageset/icon-16-folder-add.pdf deleted file mode 100644 index ba53443ad58c36c58052d6ce25d057f1946ee063..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3057 zcmai0+in{-5Pj!Y@WntTAJp~6b|Fixq!ezyzGt}HS=tWT zg<;cYhlex6GqZDcb@AbiQAr3RwcP*qyO8qjTe-S?Jbk=AZBFC#pQ(R_jnpb@dgQxz z(~lT;H?&RIC;NXGcekfI=>V=_D2}JYxOot(i}A1fVO)QFFVC<0|AwRZQy5jbHBM=Z zH@xcKSzol3Ky1)C$l*b-*~`Uo*e$P(Q4X7om)#y?RZ2cZqBJSk;G}V?1g(vgRz++; zzzZ}8uc85*I&dq3ia9!yJ20vkoy&$G=S|L9CS_d634B)JP0CIVA}O0fDA;P1E-8lq zyAXZwUc%*KvNjG72ucAPLxUjZ<{+b6VSEkAgOO1aYD^T0R>|8Gv0|(!TO6GNN(O^M zI-HIYj2#$_38ijMF=uOKQ1H|ttFVf(BohEd@2$=gpEogt3^14CQXo6yoc2aWWvq7z zj(U|5h;x7(Uiw5^5r@yX8v>(tCX!vM>HvfjQbayFC?FePo3>7NNtNUad_oke=8CT2 zB{p5PF}Up=WqC0!EMFelKwoW|Dj&-bWaRU z`kg@5We-BYI#{n!s9x#pT|vf?lvwMeD+$KvYUb);a0?K5v98}uLW3Y{l^K(RFAy3! zhO^KfRA5W?7S~g2!ytuJ$kYrXL!id5GJA%uwRP(N!36VNR+j0t$buh^UsO2KndK9~s@VS-+$GXXQbG-qUhmeKR3 zc--tVMV-)(AP>eMo!JV-m{H*!(g+68VU2*9pzn!*&trtXL+vFmwhP7^o?w;V!NGP= zcXV@ykwG}>AQ|0=F%FC!>=2HJvCc1}e4z&=(VQmDH-5&nszzmTutS)?!&+9EiI3*< z7@>ID(Y)9$7%qOw^b@Q(*fLQY^kl^%;<_UKmQ}_$u;yT1!sqk4d+hw;>+3KsYMQ#* zNHx!2G-^%r8@?Hl$1?QiZmj!ux7(kN^3$&v^YE;G`TLJiuC6w>V*`F1Z|*nm_mA?^ zQn@U9!!lS_Gt+uJ?!O#{@hEAKTsOM&{q8jGa3W2IGkCQ*0F$9Xm=5O{2wG;)@9%Hm zs-y_kaz5vL{{?%^3;&NmIwpK0JEkej^UZ0q-QTvY6!h_HPCOpQ&jJ&In6yH_C8ie_ zB;fWKG&_1EX3O>@_(=~#_}IgXXOP8b`6-kz%07qiEt#RyVROG74}ymB?=FyzSI7Os s_*%ZYzj-|+WwqY#Pm%(z30_^_|1-k-I&^Oiry7pUE{L + + + + + + + + + diff --git a/DuckDuckGo/Assets.xcassets/Images/BookmarksFolder.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/BookmarksFolder.imageset/Contents.json new file mode 100644 index 0000000000..aee9d2b2fb --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/BookmarksFolder.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "BookmarksFolder.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Chevron-Next-16.imageset/Chevron-Next-16.pdf b/DuckDuckGo/Assets.xcassets/Images/Chevron-Medium-Right-16.imageset/Chevron-Medium-Right-16.pdf similarity index 53% rename from DuckDuckGo/Assets.xcassets/Images/Chevron-Next-16.imageset/Chevron-Next-16.pdf rename to DuckDuckGo/Assets.xcassets/Images/Chevron-Medium-Right-16.imageset/Chevron-Medium-Right-16.pdf index 463b781e2007af1affac63754707c0f9f2c7df0e..7d7e84fbe4807dd5ba892d914bf166abb2b50894 100644 GIT binary patch delta 511 zcmZ9Iu};H442DI7gp!TD!%3(M5|-_=@6KWcR>U))ZUyxXq7F!mRNZ+AUJzdzrAco| z@+Hpq|Nd_G?a!tqz~O|$Zgr~=v-)FTC^=ii!|z>>KqaDYR@qSof;due#|;Yi+wi~k5A2gL-Mz4zOEk8ThtRiyWxtM*sCs4HoHV6$0J_9 M-Eg;FKfk{G14id%#{d8T literal 1160 zcmZXUUvHZ*6vf}~r?@YX+DSDw1{qrgG^Kyp8*l zXD<18i|hOOQ&z)oAOYD9ypqe=xTy57D20(aR0gvnj60T-IxD~m8%eDtsz3z3iBy0` zh=jD-_ccSMb?o~%AuUW8%fRBaRSRv53Pa35iAYIfB(3V$1W|y8nZ_V-m`N-Y$o-js zoX)o*h-{!OQaR+&C=!y&4-K_+AVydf8!L&7zNGLwW<0U55G zu65XHMa&K2Do2H>Yg}e;gdiEPl3Ucfoufs=GkAgLcTm80;Mnw;7y`su>4FvdYK&Dd zQi_}Rz9`D3#)n6W9}Vxv-(L>BZMt_o`093fdRxBX1GlLQ(>PIPK+|kEDRFiAT4khkC=@gm<{c1`h;vTTqGZX4{~=l>kJZ$BDo)wJcrI)>Hi H!?(Nt>Sg|1 diff --git a/DuckDuckGo/Assets.xcassets/Images/Chevron-Medium-Right-16.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Chevron-Medium-Right-16.imageset/Contents.json new file mode 100644 index 0000000000..fa088142ba --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Chevron-Medium-Right-16.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Chevron-Medium-Right-16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Chevron-Next-16.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Trash.imageset/Contents.json similarity index 82% rename from DuckDuckGo/Assets.xcassets/Images/Chevron-Next-16.imageset/Contents.json rename to DuckDuckGo/Assets.xcassets/Images/Trash.imageset/Contents.json index 715e36be33..ee8f9e1708 100644 --- a/DuckDuckGo/Assets.xcassets/Images/Chevron-Next-16.imageset/Contents.json +++ b/DuckDuckGo/Assets.xcassets/Images/Trash.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "Chevron-Next-16.pdf", + "filename" : "Trash.svg", "idiom" : "universal" } ], diff --git a/DuckDuckGo/Assets.xcassets/Images/Trash.imageset/Trash.svg b/DuckDuckGo/Assets.xcassets/Images/Trash.imageset/Trash.svg new file mode 100644 index 0000000000..385873ffbf --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Trash.imageset/Trash.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/DuckDuckGo/Bookmarks/Extensions/Bookmarks+Tab.swift b/DuckDuckGo/Bookmarks/Extensions/Bookmarks+Tab.swift new file mode 100644 index 0000000000..82e5748c72 --- /dev/null +++ b/DuckDuckGo/Bookmarks/Extensions/Bookmarks+Tab.swift @@ -0,0 +1,41 @@ +// +// Bookmarks+Tab.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +extension Tab { + + @MainActor + static func withContentOfBookmark(folder: BookmarkFolder, burnerMode: BurnerMode) -> [Tab] { + folder.children.compactMap { entity in + guard let url = (entity as? Bookmark)?.urlObject else { return nil } + return Tab(content: .url(url, source: .bookmark), shouldLoadInBackground: true, burnerMode: burnerMode) + } + } + +} + +extension TabCollection { + + @MainActor + static func withContentOfBookmark(folder: BookmarkFolder, burnerMode: BurnerMode) -> TabCollection { + let tabs = Tab.withContentOfBookmark(folder: folder, burnerMode: burnerMode) + return TabCollection(tabs: tabs) + } + +} diff --git a/DuckDuckGo/Bookmarks/Model/Bookmark.swift b/DuckDuckGo/Bookmarks/Model/Bookmark.swift index c7c5f61076..4d3cf399d8 100644 --- a/DuckDuckGo/Bookmarks/Model/Bookmark.swift +++ b/DuckDuckGo/Bookmarks/Model/Bookmark.swift @@ -19,7 +19,7 @@ import Cocoa import Bookmarks -internal class BaseBookmarkEntity: Identifiable { +internal class BaseBookmarkEntity: Identifiable, Equatable, Hashable { static func singleEntity(with uuid: String) -> NSFetchRequest { let request = BookmarkEntity.fetchRequest() @@ -99,6 +99,23 @@ internal class BaseBookmarkEntity: Identifiable { } } + // Subclasses needs to override to check equality on their properties + func isEqual(to instance: BaseBookmarkEntity) -> Bool { + id == instance.id && + title == instance.title && + isFolder == instance.isFolder + } + + static func == (lhs: BaseBookmarkEntity, rhs: BaseBookmarkEntity) -> Bool { + return type(of: lhs) == type(of: rhs) && lhs.isEqual(to: rhs) + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + hasher.combine(title) + hasher.combine(isFolder) + } + } final class BookmarkFolder: BaseBookmarkEntity { @@ -139,6 +156,38 @@ final class BookmarkFolder: BaseBookmarkEntity { super.init(id: id, title: title, isFolder: true) } + + override func isEqual(to instance: BaseBookmarkEntity) -> Bool { + guard let folder = instance as? BookmarkFolder else { + return false + } + return id == folder.id && + title == folder.title && + isFolder == folder.isFolder && + isParentFolderEqual(lhs: parentFolderUUID, rhs: folder.parentFolderUUID) && + children == folder.children + } + + override func hash(into hasher: inout Hasher) { + hasher.combine(id) + hasher.combine(title) + hasher.combine(isFolder) + hasher.combine(parentFolderUUID) + hasher.combine(children) + } + + // In some cases a bookmark folder that is child of the root folder has its `parentFolderUUID` set to `bookmarks_root`. In some other cases is nil. Making sure that comparing a `nil` and a `bookmarks_root` does not return false. Probably would be good idea to remove the optionality of `parentFolderUUID` in the future and set it to `bookmarks_root` when needed. + private func isParentFolderEqual(lhs: String?, rhs: String?) -> Bool { + switch (lhs, rhs) { + case (.none, .none): + return true + case (.some(let lhsValue), .some(let rhsValue)): + return lhsValue == rhsValue + case (.some(let value), .none), (.none, .some(let value)): + return value == "bookmarks_root" + } + } + } final class Bookmark: BaseBookmarkEntity { @@ -196,12 +245,33 @@ final class Bookmark: BaseBookmarkEntity { parentFolderUUID: bookmark.parentFolderUUID) } -} + convenience init(from bookmark: Bookmark, withNewUrl url: String, title: String, isFavorite: Bool) { + self.init(id: bookmark.id, + url: url, + title: title, + isFavorite: isFavorite, + parentFolderUUID: bookmark.parentFolderUUID) + } -extension BaseBookmarkEntity: Equatable { + override func isEqual(to instance: BaseBookmarkEntity) -> Bool { + guard let bookmark = instance as? Bookmark else { + return false + } + return id == bookmark.id && + title == bookmark.title && + isFolder == bookmark.isFolder && + url == bookmark.url && + isFavorite == bookmark.isFavorite && + parentFolderUUID == bookmark.parentFolderUUID + } - static func == (lhs: BaseBookmarkEntity, rhs: BaseBookmarkEntity) -> Bool { - return lhs.id == rhs.id + override func hash(into hasher: inout Hasher) { + hasher.combine(id) + hasher.combine(title) + hasher.combine(isFolder) + hasher.combine(url) + hasher.combine(isFavorite) + hasher.combine(parentFolderUUID) } } diff --git a/DuckDuckGo/Bookmarks/Model/BookmarkFolderInfo.swift b/DuckDuckGo/Bookmarks/Model/BookmarkFolderInfo.swift new file mode 100644 index 0000000000..a4f8004f71 --- /dev/null +++ b/DuckDuckGo/Bookmarks/Model/BookmarkFolderInfo.swift @@ -0,0 +1,32 @@ +// +// BookmarkFolderInfo.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +protocol BookmarksEntityIdentifiable { + var entityId: String { get } + var parentId: String? { get } +} + +struct BookmarkEntityInfo: Equatable, BookmarksEntityIdentifiable { + let entity: BaseBookmarkEntity + let parent: BookmarkFolder? + + var entityId: String { entity.id } + var parentId: String? { parent?.id } +} diff --git a/DuckDuckGo/Bookmarks/Model/BookmarkList.swift b/DuckDuckGo/Bookmarks/Model/BookmarkList.swift index 994557e2ba..dcae056327 100644 --- a/DuckDuckGo/Bookmarks/Model/BookmarkList.swift +++ b/DuckDuckGo/Bookmarks/Model/BookmarkList.swift @@ -125,6 +125,13 @@ struct BookmarkList { } } + mutating func update(bookmark: Bookmark, newURL: String, newTitle: String, newIsFavorite: Bool) -> Bookmark? { + guard !bookmark.isFolder else { return nil } + + let newBookmark = Bookmark(from: bookmark, withNewUrl: newURL, title: newTitle, isFavorite: newIsFavorite) + return updateBookmarkList(newBookmark: newBookmark, oldBookmark: bookmark) + } + mutating func updateUrl(of bookmark: Bookmark, to newURL: String) -> Bookmark? { guard !bookmark.isFolder else { return nil } @@ -132,12 +139,25 @@ struct BookmarkList { os_log("BookmarkList: Update failed, new url already in bookmark list") return nil } + + let newBookmark = Bookmark(from: bookmark, with: newURL) + return updateBookmarkList(newBookmark: newBookmark, oldBookmark: bookmark) + } + + func bookmarks() -> [IdentifiableBookmark] { + return allBookmarkURLsOrdered + } + +} + +private extension BookmarkList { + + mutating private func updateBookmarkList(newBookmark: Bookmark, oldBookmark bookmark: Bookmark) -> Bookmark? { guard itemsDict[bookmark.url] != nil, let index = allBookmarkURLsOrdered.firstIndex(of: IdentifiableBookmark(from: bookmark)) else { os_log("BookmarkList: Update failed, no such item in bookmark list") return nil } - let newBookmark = Bookmark(from: bookmark, with: newURL) let newIdentifiableBookmark = IdentifiableBookmark(from: newBookmark) allBookmarkURLsOrdered.remove(at: index) @@ -147,13 +167,8 @@ struct BookmarkList { let updatedBookmarks = existingBookmarks.filter { $0.id != bookmark.id } itemsDict[bookmark.url] = updatedBookmarks - itemsDict[newURL] = (itemsDict[newURL] ?? []) + [bookmark] + itemsDict[newBookmark.url] = (itemsDict[newBookmark.url] ?? []) + [newBookmark] return newBookmark } - - func bookmarks() -> [IdentifiableBookmark] { - return allBookmarkURLsOrdered - } - } diff --git a/DuckDuckGo/Bookmarks/Model/BookmarkManager.swift b/DuckDuckGo/Bookmarks/Model/BookmarkManager.swift index ee19960da1..b3172da78e 100644 --- a/DuckDuckGo/Bookmarks/Model/BookmarkManager.swift +++ b/DuckDuckGo/Bookmarks/Model/BookmarkManager.swift @@ -35,7 +35,9 @@ protocol BookmarkManager: AnyObject { func remove(folder: BookmarkFolder) func remove(objectsWithUUIDs uuids: [String]) func update(bookmark: Bookmark) + func update(bookmark: Bookmark, withURL url: URL, title: String, isFavorite: Bool) func update(folder: BookmarkFolder) + func update(folder: BookmarkFolder, andMoveToParent parent: ParentFolderType) @discardableResult func updateUrl(of bookmark: Bookmark, to newUrl: URL) -> Bookmark? func add(bookmark: Bookmark, to parent: BookmarkFolder?, completion: @escaping (Error?) -> Void) func add(objectsWithUUIDs uuids: [String], to parent: BookmarkFolder?, completion: @escaping (Error?) -> Void) @@ -211,12 +213,35 @@ final class LocalBookmarkManager: BookmarkManager { } + func update(bookmark: Bookmark, withURL url: URL, title: String, isFavorite: Bool) { + guard list != nil else { return } + guard getBookmark(forUrl: bookmark.url) != nil else { + os_log("LocalBookmarkManager: Failed to update bookmark url - not in the list.", type: .error) + return + } + + guard let newBookmark = list?.update(bookmark: bookmark, newURL: url.absoluteString, newTitle: title, newIsFavorite: isFavorite) else { + os_log("LocalBookmarkManager: Failed to update URL of bookmark.", type: .error) + return + } + + bookmarkStore.update(bookmark: newBookmark) + loadBookmarks() + requestSync() + } + func update(folder: BookmarkFolder) { bookmarkStore.update(folder: folder) loadBookmarks() requestSync() } + func update(folder: BookmarkFolder, andMoveToParent parent: ParentFolderType) { + bookmarkStore.update(folder: folder, andMoveToParent: parent) + loadBookmarks() + requestSync() + } + func updateUrl(of bookmark: Bookmark, to newUrl: URL) -> Bookmark? { guard list != nil else { return nil } guard getBookmark(forUrl: bookmark.url) != nil else { diff --git a/DuckDuckGo/Bookmarks/Model/BookmarkNode.swift b/DuckDuckGo/Bookmarks/Model/BookmarkNode.swift index 86f24bdc28..4f6b58c10f 100644 --- a/DuckDuckGo/Bookmarks/Model/BookmarkNode.swift +++ b/DuckDuckGo/Bookmarks/Model/BookmarkNode.swift @@ -67,11 +67,24 @@ final class BookmarkNode: Hashable { return 0 } - init(representedObject: AnyObject, parent: BookmarkNode?) { + /// Creates an instance of a bookmark node. + /// - Parameters: + /// - representedObject: The represented object contained in the node. + /// - parent: An optional parent node. + /// - uniqueId: A unique identifier for the node. This should be used only in unit tests. + /// - Attention: Use this initializer only in tests. + init(representedObject: AnyObject, parent: BookmarkNode?, uniqueId: Int) { self.representedObject = representedObject self.parent = parent - self.uniqueID = BookmarkNode.incrementingID + self.uniqueID = uniqueId + } + /// Creates an instance of a bookmark node. + /// - Parameters: + /// - representedObject: The represented object contained in the node. + /// - parent: An optional parent node. + convenience init(representedObject: AnyObject, parent: BookmarkNode?) { + self.init(representedObject: representedObject, parent: parent, uniqueId: BookmarkNode.incrementingID) BookmarkNode.incrementingID += 1 } @@ -165,7 +178,7 @@ final class BookmarkNode: Hashable { // The Node class will most frequently represent Bookmark entities and PseudoFolders. Because of this, their unique properties are // used to derive the hash for the node so that equality can be handled based on the represented object. if let entity = self.representedObject as? BaseBookmarkEntity { - hasher.combine(entity.id) + hasher.combine(entity.hashValue) } else if let folder = self.representedObject as? PseudoFolder { hasher.combine(folder.name) } else { @@ -176,7 +189,7 @@ final class BookmarkNode: Hashable { // MARK: - Equatable class func == (lhs: BookmarkNode, rhs: BookmarkNode) -> Bool { - return lhs === rhs + return lhs.uniqueID == rhs.uniqueID && lhs.representedObjectEquals(rhs.representedObject) } } diff --git a/DuckDuckGo/Bookmarks/Model/BookmarkOutlineViewDataSource.swift b/DuckDuckGo/Bookmarks/Model/BookmarkOutlineViewDataSource.swift index edfcfa6e2e..93a404fa95 100644 --- a/DuckDuckGo/Bookmarks/Model/BookmarkOutlineViewDataSource.swift +++ b/DuckDuckGo/Bookmarks/Model/BookmarkOutlineViewDataSource.swift @@ -30,10 +30,12 @@ final class BookmarkOutlineViewDataSource: NSObject, NSOutlineViewDataSource, NS @Published var selectedFolders: [BookmarkFolder] = [] let treeController: BookmarkTreeController - var expandedNodesIDs = Set() + private(set) var expandedNodesIDs = Set() private let contentMode: ContentMode private let bookmarkManager: BookmarkManager + private let showMenuButtonOnHover: Bool + private let onMenuRequestedAction: ((BookmarkOutlineCellView) -> Void)? private let presentFaviconsFetcherOnboarding: (() -> Void)? private var favoritesPseudoFolder = PseudoFolder.favorites @@ -43,11 +45,15 @@ final class BookmarkOutlineViewDataSource: NSObject, NSOutlineViewDataSource, NS contentMode: ContentMode, bookmarkManager: BookmarkManager, treeController: BookmarkTreeController, + showMenuButtonOnHover: Bool = true, + onMenuRequestedAction: ((BookmarkOutlineCellView) -> Void)? = nil, presentFaviconsFetcherOnboarding: (() -> Void)? = nil ) { self.contentMode = contentMode self.bookmarkManager = bookmarkManager self.treeController = treeController + self.showMenuButtonOnHover = showMenuButtonOnHover + self.onMenuRequestedAction = onMenuRequestedAction self.presentFaviconsFetcherOnboarding = presentFaviconsFetcherOnboarding super.init() @@ -123,6 +129,8 @@ final class BookmarkOutlineViewDataSource: NSObject, NSOutlineViewDataSource, NS } let cell = outlineView.makeView(withIdentifier: .init(BookmarkOutlineCellView.className()), owner: self) as? BookmarkOutlineCellView ?? BookmarkOutlineCellView(identifier: .init(BookmarkOutlineCellView.className())) + cell.shouldShowMenuButton = showMenuButtonOnHover + cell.delegate = self if let bookmark = node.representedObject as? Bookmark { cell.update(from: bookmark) @@ -233,7 +241,7 @@ final class BookmarkOutlineViewDataSource: NSObject, NSOutlineViewDataSource, NS // Folders cannot be dragged onto any of their descendants: let containsDescendantOfDestination = draggedFolders.contains { draggedFolder in - let folder = BookmarkFolder(id: draggedFolder.id, title: draggedFolder.name) + let folder = BookmarkFolder(id: draggedFolder.id, title: draggedFolder.name, parentFolderUUID: draggedFolder.parentFolderUUID, children: draggedFolder.children) guard let draggedNode = treeController.node(representing: folder) else { return false @@ -329,3 +337,11 @@ final class BookmarkOutlineViewDataSource: NSObject, NSOutlineViewDataSource, NS } } + +// MARK: - BookmarkOutlineCellViewDelegate + +extension BookmarkOutlineViewDataSource: BookmarkOutlineCellViewDelegate { + func outlineCellViewRequestedMenu(_ cell: BookmarkOutlineCellView) { + onMenuRequestedAction?(cell) + } +} diff --git a/DuckDuckGo/Bookmarks/Model/BookmarkSidebarTreeController.swift b/DuckDuckGo/Bookmarks/Model/BookmarkSidebarTreeController.swift index 06167f2356..b8549cad89 100644 --- a/DuckDuckGo/Bookmarks/Model/BookmarkSidebarTreeController.swift +++ b/DuckDuckGo/Bookmarks/Model/BookmarkSidebarTreeController.swift @@ -33,19 +33,11 @@ final class BookmarkSidebarTreeController: BookmarkTreeControllerDataSource { // MARK: - Private private func childNodesForRootNode(_ node: BookmarkNode) -> [BookmarkNode] { - let favorites = PseudoFolder.favorites - let favoritesNode = BookmarkNode(representedObject: favorites, parent: node) - favoritesNode.canHaveChildNodes = false - - let blankSpacer = SpacerNode.blank - let spacerNode = BookmarkNode(representedObject: blankSpacer, parent: node) - spacerNode.canHaveChildNodes = false - let bookmarks = PseudoFolder.bookmarks let bookmarksNode = BookmarkNode(representedObject: bookmarks, parent: node) bookmarksNode.canHaveChildNodes = true - return [favoritesNode, spacerNode, bookmarksNode] + return [bookmarksNode] } private func childNodes(for parentNode: BookmarkNode) -> [BookmarkNode] { diff --git a/DuckDuckGo/Bookmarks/Model/PasteboardFolder.swift b/DuckDuckGo/Bookmarks/Model/PasteboardFolder.swift index ffc3eb9a41..f99f1e040d 100644 --- a/DuckDuckGo/Bookmarks/Model/PasteboardFolder.swift +++ b/DuckDuckGo/Bookmarks/Model/PasteboardFolder.swift @@ -26,12 +26,15 @@ struct PasteboardFolder: Hashable { static let name = "name" } - let id: String - let name: String + var id: String { folder.id } + var name: String { folder.title } + var parentFolderUUID: String? { folder.parentFolderUUID } + var children: [BaseBookmarkEntity] { folder.children } - init(id: String, name: String) { - self.id = id - self.name = name + private let folder: BookmarkFolder + + init(folder: BookmarkFolder) { + self.folder = folder } // MARK: - Pasteboard Restoration @@ -41,7 +44,7 @@ struct PasteboardFolder: Hashable { return nil } - self.init(id: id, name: name) + self.init(folder: .init(id: id, title: name)) } init?(pasteboardItem: NSPasteboardItem) { @@ -78,19 +81,17 @@ struct PasteboardFolder: Hashable { static let folderUTIInternalType = NSPasteboard.PasteboardType(rawValue: folderUTIInternal) var pasteboardFolder: PasteboardFolder { - return PasteboardFolder(id: folderID, name: folderName) + return PasteboardFolder(folder: folder) } var internalDictionary: PasteboardAttributes { return pasteboardFolder.internalDictionaryRepresentation } - private let folderID: String - private let folderName: String + private let folder: BookmarkFolder init(folder: BookmarkFolder) { - self.folderID = folder.id - self.folderName = folder.title + self.folder = folder } // MARK: - NSPasteboardWriting @@ -102,7 +103,7 @@ struct PasteboardFolder: Hashable { func pasteboardPropertyList(forType type: NSPasteboard.PasteboardType) -> Any? { switch type { case .string: - return folderName + return folder.title case FolderPasteboardWriter.folderUTIInternalType: return internalDictionary default: diff --git a/DuckDuckGo/Bookmarks/Model/PseudoFolder.swift b/DuckDuckGo/Bookmarks/Model/PseudoFolder.swift index c3aed14be7..85cef1888a 100644 --- a/DuckDuckGo/Bookmarks/Model/PseudoFolder.swift +++ b/DuckDuckGo/Bookmarks/Model/PseudoFolder.swift @@ -22,7 +22,7 @@ import Foundation final class PseudoFolder: Equatable { static let favorites = PseudoFolder(id: UUID().uuidString, name: UserText.favorites, icon: .favoriteFilledBorder) - static let bookmarks = PseudoFolder(id: UUID().uuidString, name: UserText.bookmarks, icon: .folder) + static let bookmarks = PseudoFolder(id: UUID().uuidString, name: UserText.bookmarks, icon: .bookmarksFolder) let id: String let name: String diff --git a/DuckDuckGo/Bookmarks/Services/BookmarkStore.swift b/DuckDuckGo/Bookmarks/Services/BookmarkStore.swift index 6b7981c96a..3466d1a7fa 100644 --- a/DuckDuckGo/Bookmarks/Services/BookmarkStore.swift +++ b/DuckDuckGo/Bookmarks/Services/BookmarkStore.swift @@ -52,6 +52,7 @@ protocol BookmarkStore { func remove(objectsWithUUIDs: [String], completion: @escaping (Bool, Error?) -> Void) func update(bookmark: Bookmark) func update(folder: BookmarkFolder) + func update(folder: BookmarkFolder, andMoveToParent parent: ParentFolderType) func add(objectsWithUUIDs: [String], to parent: BookmarkFolder?, completion: @escaping (Error?) -> Void) func update(objectsWithUUIDs uuids: [String], update: @escaping (BaseBookmarkEntity) -> Void, completion: @escaping (Error?) -> Void) func canMoveObjectWithUUID(objectUUID uuid: String, to parent: BookmarkFolder) -> Bool diff --git a/DuckDuckGo/Bookmarks/Services/BookmarkStoreMock.swift b/DuckDuckGo/Bookmarks/Services/BookmarkStoreMock.swift index f505891891..2ab57158a9 100644 --- a/DuckDuckGo/Bookmarks/Services/BookmarkStoreMock.swift +++ b/DuckDuckGo/Bookmarks/Services/BookmarkStoreMock.swift @@ -46,6 +46,11 @@ public final class BookmarkStoreMock: BookmarkStore { self.updateFavoriteIndexCalled = updateFavoriteIndexCalled } + var capturedFolder: BookmarkFolder? + var capturedParentFolder: BookmarkFolder? + var capturedParentFolderType: ParentFolderType? + var capturedBookmark: Bookmark? + var loadAllCalled = false var bookmarks: [BaseBookmarkEntity]? var loadError: Error? @@ -60,6 +65,8 @@ public final class BookmarkStoreMock: BookmarkStore { func save(bookmark: Bookmark, parent: BookmarkFolder?, index: Int?, completion: @escaping (Bool, Error?) -> Void) { saveBookmarkCalled = true bookmarks?.append(bookmark) + capturedParentFolder = parent + capturedBookmark = bookmark completion(saveBookmarkSuccess, saveBookmarkError) } @@ -68,6 +75,8 @@ public final class BookmarkStoreMock: BookmarkStore { var saveFolderError: Error? func save(folder: BookmarkFolder, parent: BookmarkFolder?, completion: @escaping (Bool, Error?) -> Void) { saveFolderCalled = true + capturedFolder = folder + capturedParentFolder = parent completion(saveFolderSuccess, saveFolderError) } @@ -92,11 +101,20 @@ public final class BookmarkStoreMock: BookmarkStore { } updateBookmarkCalled = true + capturedBookmark = bookmark } var updateFolderCalled = false func update(folder: BookmarkFolder) { updateFolderCalled = true + capturedFolder = folder + } + + var updateFolderAndMoveToParentCalled = false + func update(folder: BookmarkFolder, andMoveToParent parent: ParentFolderType) { + updateFolderAndMoveToParentCalled = true + capturedFolder = folder + capturedParentFolderType = parent } var addChildCalled = false @@ -122,8 +140,11 @@ public final class BookmarkStoreMock: BookmarkStore { } var moveObjectUUIDCalled = false + var capturedObjectUUIDs: [String]? func move(objectUUIDs: [String], toIndex: Int?, withinParentFolder: ParentFolderType, completion: @escaping (Error?) -> Void) { moveObjectUUIDCalled = true + capturedObjectUUIDs = objectUUIDs + capturedParentFolderType = withinParentFolder } var updateFavoriteIndexCalled = false @@ -135,4 +156,17 @@ public final class BookmarkStoreMock: BookmarkStore { func handleFavoritesAfterDisablingSync() {} } +extension ParentFolderType: Equatable { + public static func == (lhs: Self, rhs: Self) -> Bool { + switch (lhs, rhs) { + case (.root, .root): + return true + case (.parent(let lhsValue), .parent(let rhsValue)): + return lhsValue == rhsValue + default: + return false + } + } +} + #endif diff --git a/DuckDuckGo/Bookmarks/Services/ContextualMenu.swift b/DuckDuckGo/Bookmarks/Services/ContextualMenu.swift index 49f694be2b..f79e265d96 100644 --- a/DuckDuckGo/Bookmarks/Services/ContextualMenu.swift +++ b/DuckDuckGo/Bookmarks/Services/ContextualMenu.swift @@ -18,10 +18,19 @@ import AppKit -struct ContextualMenu { +enum ContextualMenu { + + static func menu(for objects: [Any]?) -> NSMenu? { + menu(for: objects, target: nil) + } + + /// Creates an instance of NSMenu for the specified Objects and target. + /// - Parameters: + /// - objects: The objects to create the menu for. + /// - target: The target to associate to the `NSMenuItem` + /// - Returns: An instance of NSMenu or nil if `objects` is not a `Bookmark` or a `Folder`. + static func menu(for objects: [Any]?, target: AnyObject?) -> NSMenu? { - // Not all contexts support an editing option for bookmarks. The option is displayed by default, but `includeBookmarkEditMenu` can disable it. - static func menu(for objects: [Any]?, includeBookmarkEditMenu: Bool = true) -> NSMenu? { guard let objects = objects, objects.count > 0 else { return menuForNoSelection() } @@ -31,140 +40,183 @@ struct ContextualMenu { } let node = objects.first as? BookmarkNode - let object = node?.representedObject ?? objects.first as? BaseBookmarkEntity + let object = node?.representedObject as? BaseBookmarkEntity ?? objects.first as? BaseBookmarkEntity + let parentFolder = node?.parent?.representedObject as? BookmarkFolder - if let bookmark = object as? Bookmark { - return menu(for: bookmark, includeBookmarkEditMenu: includeBookmarkEditMenu) - } else if let folder = object as? BookmarkFolder { - return menu(for: folder) - } else { - return nil - } - } + guard let object else { return nil } - // MARK: - Single Item Menu Creation + let menu = menu(for: object, parentFolder: parentFolder) - private static func menuForNoSelection() -> NSMenu { - let menu = NSMenu(title: "") - menu.addItem(newFolderMenuItem()) + menu?.items.forEach { item in + item.target = target + } return menu } - private static func menu(for bookmark: Bookmark, includeBookmarkEditMenu: Bool) -> NSMenu { - let menu = NSMenu(title: "") + /// Creates an instance of NSMenu for the specified `BaseBookmarkEntity`and parent `BookmarkFolder`. + /// + /// - Parameters: + /// - entity: The bookmark entity to create the menu for. + /// - parentFolder: An optional `BookmarkFolder`. + /// - Returns: An instance of NSMenu or nil if `entity` is not a `Bookmark` or a `Folder`. + static func menu(for entity: BaseBookmarkEntity, parentFolder: BookmarkFolder?) -> NSMenu? { + let menu: NSMenu? + if let bookmark = entity as? Bookmark { + menu = self.menu(for: bookmark, parent: parentFolder, isFavorite: bookmark.isFavorite) + } else if let folder = entity as? BookmarkFolder { + // When the user edits a folder we need to show the parent in the folder picker. Folders directly child of PseudoFolder `Bookmarks` have nil parent because their parent is not an instance of `BookmarkFolder` + menu = self.menu(for: folder, parent: parentFolder) + } else { + menu = nil + } - menu.addItem(openBookmarkInNewTabMenuItem(bookmark: bookmark)) - menu.addItem(openBookmarkInNewWindowMenuItem(bookmark: bookmark)) - menu.addItem(NSMenuItem.separator()) + return menu + } - menu.addItem(addBookmarkToFavoritesMenuItem(bookmark: bookmark)) + /// Returns an array of `NSMenuItem` to show for a bookmark. + /// + /// - Important: The `representedObject` for the `NSMenuItem` returned is `nil`. This function is meant to be used for scenarios where the model is not available at the time of creating the `NSMenu` such as from the BookmarkBarCollectionViewItem. + /// + /// - Parameter isFavorite: True if the menu item should contain a menu item to add to favorites. False to contain a menu item to remove from favorites. + /// - Returns: An array of `NSMenuItem` + static func bookmarkMenuItems(isFavorite: Bool) -> [NSMenuItem] { + menuItems(for: nil, parent: nil, isFavorite: isFavorite) + } - if includeBookmarkEditMenu { - menu.addItem(editBookmarkMenuItem(bookmark: bookmark)) - } + /// Returns an array of `NSMenuItem` to show for a bookmark folder. + /// + /// - Important: The `representedObject` for the `NSMenuItem` returned is `nil`. This function is meant to be used for scenarios where the model is not available at the time of creating the `NSMenu` such as from the BookmarkBarCollectionViewItem. + /// + /// - Returns: An array of `NSMenuItem` + static func folderMenuItems() -> [NSMenuItem] { + menuItems(for: nil, parent: nil) + } - menu.addItem(NSMenuItem.separator()) +} - menu.addItem(copyBookmarkMenuItem(bookmark: bookmark)) - menu.addItem(deleteBookmarkMenuItem(bookmark: bookmark)) - menu.addItem(NSMenuItem.separator()) +private extension ContextualMenu { - menu.addItem(newFolderMenuItem()) + static func menuForNoSelection() -> NSMenu { + NSMenu(items: [addFolderMenuItem(folder: nil)]) + } - return menu + static func menu(for bookmark: Bookmark?, parent: BookmarkFolder?, isFavorite: Bool) -> NSMenu { + NSMenu(items: menuItems(for: bookmark, parent: parent, isFavorite: isFavorite)) } - private static func menu(for folder: BookmarkFolder) -> NSMenu { - let menu = NSMenu(title: "") + static func menu(for folder: BookmarkFolder?, parent: BookmarkFolder?) -> NSMenu { + NSMenu(items: menuItems(for: folder, parent: parent)) + } - menu.addItem(renameFolderMenuItem(folder: folder)) - menu.addItem(deleteFolderMenuItem(folder: folder)) - menu.addItem(NSMenuItem.separator()) + static func menuItems(for bookmark: Bookmark?, parent: BookmarkFolder?, isFavorite: Bool) -> [NSMenuItem] { + [ + openBookmarkInNewTabMenuItem(bookmark: bookmark), + openBookmarkInNewWindowMenuItem(bookmark: bookmark), + NSMenuItem.separator(), + addBookmarkToFavoritesMenuItem(isFavorite: isFavorite, bookmark: bookmark), + NSMenuItem.separator(), + editBookmarkMenuItem(bookmark: bookmark), + copyBookmarkMenuItem(bookmark: bookmark), + deleteBookmarkMenuItem(bookmark: bookmark), + moveToEndMenuItem(entity: bookmark, parent: parent), + NSMenuItem.separator(), + addFolderMenuItem(folder: parent), + manageBookmarksMenuItem(), + ] + } - menu.addItem(openInNewTabsMenuItem(folder: folder)) + static func menuItems(for folder: BookmarkFolder?, parent: BookmarkFolder?) -> [NSMenuItem] { + [ + openInNewTabsMenuItem(folder: folder), + openAllInNewWindowMenuItem(folder: folder), + NSMenuItem.separator(), + editFolderMenuItem(folder: folder, parent: parent), + deleteFolderMenuItem(folder: folder), + moveToEndMenuItem(entity: folder, parent: parent), + NSMenuItem.separator(), + addFolderMenuItem(folder: folder), + manageBookmarksMenuItem(), + ] + } - return menu + static func menuItem(_ title: String, _ action: Selector, _ representedObject: Any? = nil) -> NSMenuItem { + let item = NSMenuItem(title: title, action: action, keyEquivalent: "") + item.representedObject = representedObject + return item } - // MARK: - Menu Items + // MARK: - Single Bookmark Menu Items - static func newFolderMenuItem() -> NSMenuItem { - return menuItem(UserText.newFolder, #selector(FolderMenuItemSelectors.newFolder(_:))) + static func openBookmarkInNewTabMenuItem(bookmark: Bookmark?) -> NSMenuItem { + menuItem(UserText.openInNewTab, #selector(BookmarkMenuItemSelectors.openBookmarkInNewTab(_:)), bookmark) } - static func renameFolderMenuItem(folder: BookmarkFolder) -> NSMenuItem { - return menuItem(UserText.renameFolder, #selector(FolderMenuItemSelectors.renameFolder(_:)), folder) + static func openBookmarkInNewWindowMenuItem(bookmark: Bookmark?) -> NSMenuItem { + menuItem(UserText.openInNewWindow, #selector(BookmarkMenuItemSelectors.openBookmarkInNewWindow(_:)), bookmark) } - static func deleteFolderMenuItem(folder: BookmarkFolder) -> NSMenuItem { - return menuItem(UserText.deleteFolder, #selector(FolderMenuItemSelectors.deleteFolder(_:)), folder) + static func manageBookmarksMenuItem() -> NSMenuItem { + menuItem(UserText.bookmarksManageBookmarks, #selector(BookmarkMenuItemSelectors.manageBookmarks(_:))) } - static func openInNewTabsMenuItem(folder: BookmarkFolder) -> NSMenuItem { - return menuItem(UserText.bookmarksOpenInNewTabs, #selector(FolderMenuItemSelectors.openInNewTabs(_:)), folder) + static func addBookmarkToFavoritesMenuItem(isFavorite: Bool, bookmark: Bookmark?) -> NSMenuItem { + let title = isFavorite ? UserText.removeFromFavorites : UserText.addToFavorites + return menuItem(title, #selector(BookmarkMenuItemSelectors.toggleBookmarkAsFavorite(_:)), bookmark) } - static func openBookmarksInNewTabsMenuItem(bookmarks: [Bookmark]) -> NSMenuItem { - return menuItem(UserText.bookmarksOpenInNewTabs, #selector(FolderMenuItemSelectors.openInNewTabs(_:)), bookmarks) + static func addBookmarksToFavoritesMenuItem(bookmarks: [Bookmark], allFavorites: Bool) -> NSMenuItem { + let title = allFavorites ? UserText.removeFromFavorites : UserText.addToFavorites + return menuItem(title, #selector(BookmarkMenuItemSelectors.toggleBookmarkAsFavorite(_:)), bookmarks) } - static func openBookmarkInNewTabMenuItem(bookmark: Bookmark) -> NSMenuItem { - return menuItem(UserText.openInNewTab, #selector(BookmarkMenuItemSelectors.openBookmarkInNewTab(_:)), bookmark) + static func editBookmarkMenuItem(bookmark: Bookmark?) -> NSMenuItem { + menuItem(UserText.editBookmark, #selector(BookmarkMenuItemSelectors.editBookmark(_:)), bookmark) } - static func openBookmarkInNewWindowMenuItem(bookmark: Bookmark) -> NSMenuItem { - return menuItem(UserText.openInNewWindow, #selector(BookmarkMenuItemSelectors.openBookmarkInNewWindow(_:)), bookmark) + static func copyBookmarkMenuItem(bookmark: Bookmark?) -> NSMenuItem { + menuItem(UserText.copy, #selector(BookmarkMenuItemSelectors.copyBookmark(_:)), bookmark) } - static func addBookmarkToFavoritesMenuItem(bookmark: Bookmark) -> NSMenuItem { - let title: String - - if bookmark.isFavorite { - title = UserText.removeFromFavorites - } else { - title = UserText.addToFavorites - } - - return menuItem(title, #selector(BookmarkMenuItemSelectors.toggleBookmarkAsFavorite(_:)), bookmark) + static func deleteBookmarkMenuItem(bookmark: Bookmark?) -> NSMenuItem { + menuItem(UserText.bookmarksBarContextMenuDelete, #selector(BookmarkMenuItemSelectors.deleteBookmark(_:)), bookmark) } - static func addBookmarksToFavoritesMenuItem(bookmarks: [Bookmark], allFavorites: Bool) -> NSMenuItem { - let title: String + static func moveToEndMenuItem(entity: BaseBookmarkEntity?, parent: BookmarkFolder?) -> NSMenuItem { + let bookmarkEntityInfo = entity.flatMap { BookmarkEntityInfo(entity: $0, parent: parent) } + return menuItem(UserText.bookmarksBarContextMenuMoveToEnd, #selector(BookmarkMenuItemSelectors.moveToEnd(_:)), bookmarkEntityInfo) + } - if allFavorites { - title = UserText.removeFromFavorites - } else { - title = UserText.addToFavorites - } + // MARK: - Bookmark Folder Menu Items - return menuItem(title, #selector(BookmarkMenuItemSelectors.toggleBookmarkAsFavorite(_:)), bookmarks) + static func openInNewTabsMenuItem(folder: BookmarkFolder?) -> NSMenuItem { + menuItem(UserText.openAllInNewTabs, #selector(FolderMenuItemSelectors.openInNewTabs(_:)), folder) } - static func editBookmarkMenuItem(bookmark: Bookmark) -> NSMenuItem { - let title = NSLocalizedString("Edit…", comment: "Command") - return menuItem(title, #selector(BookmarkMenuItemSelectors.editBookmark(_:)), bookmark) + static func openAllInNewWindowMenuItem(folder: BookmarkFolder?) -> NSMenuItem { + menuItem(UserText.openAllTabsInNewWindow, #selector(FolderMenuItemSelectors.openAllInNewWindow(_:)), folder) } - static func copyBookmarkMenuItem(bookmark: Bookmark) -> NSMenuItem { - let title = NSLocalizedString("Copy", comment: "Command") - return menuItem(title, #selector(BookmarkMenuItemSelectors.copyBookmark(_:)), bookmark) + static func addFolderMenuItem(folder: BookmarkFolder?) -> NSMenuItem { + menuItem(UserText.addFolder, #selector(FolderMenuItemSelectors.newFolder(_:)), folder) } - static func deleteBookmarkMenuItem(bookmark: Bookmark) -> NSMenuItem { - let title = NSLocalizedString("Delete", comment: "Command") - return menuItem(title, #selector(BookmarkMenuItemSelectors.deleteBookmark(_:)), bookmark) + static func editFolderMenuItem(folder: BookmarkFolder?, parent: BookmarkFolder?) -> NSMenuItem { + let folderEntityInfo = folder.flatMap { BookmarkEntityInfo(entity: $0, parent: parent) } + return menuItem(UserText.editBookmark, #selector(FolderMenuItemSelectors.editFolder(_:)), folderEntityInfo) } - static func menuItem(_ title: String, _ action: Selector, _ representedObject: Any? = nil) -> NSMenuItem { - let item = NSMenuItem(title: title, action: action, keyEquivalent: "") - item.representedObject = representedObject - return item + static func deleteFolderMenuItem(folder: BookmarkFolder?) -> NSMenuItem { + menuItem(UserText.bookmarksBarContextMenuDelete, #selector(FolderMenuItemSelectors.deleteFolder(_:)), folder) } // MARK: - Multi-Item Menu Creation - private static func menu(for entities: [BaseBookmarkEntity]) -> NSMenu { + static func openBookmarksInNewTabsMenuItem(bookmarks: [Bookmark]) -> NSMenuItem { + menuItem(UserText.bookmarksOpenInNewTabs, #selector(FolderMenuItemSelectors.openInNewTabs(_:)), bookmarks) + } + + static func menu(for entities: [BaseBookmarkEntity]) -> NSMenu { let menu = NSMenu(title: "") var menuItems: [NSMenuItem] = [] @@ -185,8 +237,7 @@ struct ContextualMenu { menuItems.append(NSMenuItem.separator()) } - let title = NSLocalizedString("Delete", comment: "Command") - let deleteItem = NSMenuItem(title: title, action: #selector(BookmarkMenuItemSelectors.deleteEntities(_:)), keyEquivalent: "") + let deleteItem = NSMenuItem(title: UserText.bookmarksBarContextMenuDelete, action: #selector(BookmarkMenuItemSelectors.deleteEntities(_:)), keyEquivalent: "") deleteItem.representedObject = entities menuItems.append(deleteItem) diff --git a/DuckDuckGo/Bookmarks/Services/LocalBookmarkStore.swift b/DuckDuckGo/Bookmarks/Services/LocalBookmarkStore.swift index 307d16cc5d..e2be507a44 100644 --- a/DuckDuckGo/Bookmarks/Services/LocalBookmarkStore.swift +++ b/DuckDuckGo/Bookmarks/Services/LocalBookmarkStore.swift @@ -391,18 +391,27 @@ final class LocalBookmarkStore: BookmarkStore { } func update(folder: BookmarkFolder) { - do { - _ = try applyChangesAndSave(changes: { context in - let folderFetchRequest = BaseBookmarkEntity.singleEntity(with: folder.id) - let folderFetchRequestResults = try? context.fetch(folderFetchRequest) - - guard let bookmarkFolderMO = folderFetchRequestResults?.first else { - assertionFailure("LocalBookmarkStore: Failed to get BookmarkEntity from the context") - throw BookmarkStoreError.missingEntity + _ = try applyChangesAndSave(changes: { [weak self] context in + guard let self = self else { + throw BookmarkStoreError.storeDeallocated } + try update(folder: folder, in: context) + }) + } catch { + let error = error as NSError + commonOnSaveErrorHandler(error) + } + } - bookmarkFolderMO.update(with: folder) + func update(folder: BookmarkFolder, andMoveToParent parent: ParentFolderType) { + do { + _ = try applyChangesAndSave(changes: { [weak self] context in + guard let self = self else { + throw BookmarkStoreError.storeDeallocated + } + let folderEntity = try update(folder: folder, in: context) + try move(entities: [folderEntity], toIndex: nil, withinParentFolderType: parent, in: context) }) } catch { let error = error as NSError @@ -566,10 +575,6 @@ final class LocalBookmarkStore: BookmarkStore { throw BookmarkStoreError.storeDeallocated } - guard let rootFolder = self.bookmarksRoot(in: context) else { - throw BookmarkStoreError.missingRoot - } - // Guarantee that bookmarks are fetched in the same order as the UUIDs. In the future, this should fetch all objects at once with a // batch fetch request and have them sorted in the correct order. let bookmarkManagedObjects: [BookmarkEntity] = objectUUIDs.compactMap { uuid in @@ -577,28 +582,8 @@ final class LocalBookmarkStore: BookmarkStore { return (try? context.fetch(entityFetchRequest))?.first } - let newParentFolder: BookmarkEntity - - switch type { - case .root: newParentFolder = rootFolder - case .parent(let newParentUUID): - let bookmarksFetchRequest = BaseBookmarkEntity.singleEntity(with: newParentUUID) + try move(entities: bookmarkManagedObjects, toIndex: index, withinParentFolderType: type, in: context) - if let fetchedParent = try context.fetch(bookmarksFetchRequest).first, fetchedParent.isFolder { - newParentFolder = fetchedParent - } else { - throw BookmarkStoreError.missingEntity - } - } - - if let index = index, index < newParentFolder.childrenArray.count { - self.move(entities: bookmarkManagedObjects, to: index, within: newParentFolder) - } else { - for bookmarkManagedObject in bookmarkManagedObjects { - bookmarkManagedObject.parent = nil - newParentFolder.addToChildren(bookmarkManagedObject) - } - } }, onError: { [weak self] error in self?.commonOnSaveErrorHandler(error) DispatchQueue.main.async { completion(error) } @@ -996,6 +981,53 @@ final class LocalBookmarkStore: BookmarkStore { } +private extension LocalBookmarkStore { + + @discardableResult + func update(folder: BookmarkFolder, in context: NSManagedObjectContext) throws -> BookmarkEntity { + let folderFetchRequest = BaseBookmarkEntity.singleEntity(with: folder.id) + let folderFetchRequestResults = try? context.fetch(folderFetchRequest) + + guard let bookmarkFolderMO = folderFetchRequestResults?.first else { + assertionFailure("LocalBookmarkStore: Failed to get BookmarkEntity from the context") + throw BookmarkStoreError.missingEntity + } + + bookmarkFolderMO.update(with: folder) + return bookmarkFolderMO + } + + func move(entities: [BookmarkEntity], toIndex index: Int?, withinParentFolderType type: ParentFolderType, in context: NSManagedObjectContext) throws { + guard let rootFolder = bookmarksRoot(in: context) else { + throw BookmarkStoreError.missingRoot + } + + let newParentFolder: BookmarkEntity + + switch type { + case .root: newParentFolder = rootFolder + case .parent(let newParentUUID): + let bookmarksFetchRequest = BaseBookmarkEntity.singleEntity(with: newParentUUID) + + if let fetchedParent = try context.fetch(bookmarksFetchRequest).first, fetchedParent.isFolder { + newParentFolder = fetchedParent + } else { + throw BookmarkStoreError.missingEntity + } + } + + if let index = index, index < newParentFolder.childrenArray.count { + self.move(entities: entities, to: index, within: newParentFolder) + } else { + for bookmarkManagedObject in entities { + bookmarkManagedObject.parent = nil + newParentFolder.addToChildren(bookmarkManagedObject) + } + } + } + +} + extension LocalBookmarkStore.BookmarkStoreError: CustomNSError { var errorCode: Int { diff --git a/DuckDuckGo/Bookmarks/Services/MenuItemSelectors.swift b/DuckDuckGo/Bookmarks/Services/MenuItemSelectors.swift index 44114fc9d8..393b22de8e 100644 --- a/DuckDuckGo/Bookmarks/Services/MenuItemSelectors.swift +++ b/DuckDuckGo/Bookmarks/Services/MenuItemSelectors.swift @@ -18,7 +18,13 @@ import AppKit -@objc protocol BookmarkMenuItemSelectors { +@objc protocol BookmarksMenuItemSelectors { + func newFolder(_ sender: NSMenuItem) + func moveToEnd(_ sender: NSMenuItem) + @objc optional func manageBookmarks(_ sender: NSMenuItem) +} + +@objc protocol BookmarkMenuItemSelectors: BookmarksMenuItemSelectors { func openBookmarkInNewTab(_ sender: NSMenuItem) func openBookmarkInNewWindow(_ sender: NSMenuItem) @@ -30,11 +36,11 @@ import AppKit } -@objc protocol FolderMenuItemSelectors { +@objc protocol FolderMenuItemSelectors: BookmarksMenuItemSelectors { - func newFolder(_ sender: NSMenuItem) - func renameFolder(_ sender: NSMenuItem) + func editFolder(_ sender: NSMenuItem) func deleteFolder(_ sender: NSMenuItem) func openInNewTabs(_ sender: NSMenuItem) + func openAllInNewWindow(_ sender: NSMenuItem) } diff --git a/DuckDuckGo/Bookmarks/View/AddBookmarkFolderModalView.swift b/DuckDuckGo/Bookmarks/View/AddBookmarkFolderModalView.swift deleted file mode 100644 index 31cc06dc04..0000000000 --- a/DuckDuckGo/Bookmarks/View/AddBookmarkFolderModalView.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// AddBookmarkFolderModalView.swift -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import SwiftUI - -struct AddBookmarkFolderModalView: ModalView { - - @State var model: AddBookmarkFolderModalViewModel = .init() - @Environment(\.dismiss) private var dismiss - - var body: some View { - VStack(alignment: .leading, spacing: 16) { - Text(model.title) - .fontWeight(.semibold) - - HStack(spacing: 16) { - Text(UserText.newBookmarkDialogBookmarkNameTitle) - .frame(height: 22) - - TextField("", text: $model.folderName) - .accessibilityIdentifier("Title Text Field") - .textFieldStyle(.roundedBorder) - .disableAutocorrection(true) - } - .padding(.bottom, 4) - - HStack { - Spacer() - - Button(UserText.cancel) { - model.cancel(dismiss: dismiss.callAsFunction) - } - .keyboardShortcut(.cancelAction) - - Button(model.addButtonTitle) { - model.addFolder(dismiss: dismiss.callAsFunction) - } - .keyboardShortcut(.defaultAction) - .disabled(model.isAddButtonDisabled) - - } - } - .font(.system(size: 13)) - .padding() - .frame(width: 450, height: 131) - } - -} - -#Preview { - AddBookmarkFolderModalView() -} diff --git a/DuckDuckGo/Bookmarks/View/AddBookmarkFolderPopoverView.swift b/DuckDuckGo/Bookmarks/View/AddBookmarkFolderPopoverView.swift index a0ab50c89a..f15465df36 100644 --- a/DuckDuckGo/Bookmarks/View/AddBookmarkFolderPopoverView.swift +++ b/DuckDuckGo/Bookmarks/View/AddBookmarkFolderPopoverView.swift @@ -23,59 +23,27 @@ struct AddBookmarkFolderPopoverView: ModalView { @ObservedObject var model: AddBookmarkFolderPopoverViewModel var body: some View { - VStack(alignment: .leading, spacing: 16) { - Text(UserText.newFolder) - .bold() - - VStack(alignment: .leading, spacing: 7) { - Text("Location:", comment: "Add Folder popover: parent folder picker title") - - BookmarkFolderPicker(folders: model.folders, selectedFolder: $model.parent) - .accessibilityIdentifier("bookmark.folder.folder.dropdown") - .disabled(model.isDisabled) - } - - VStack(alignment: .leading, spacing: 7) { - Text(UserText.newFolderDialogFolderNameTitle) - - TextField("", text: $model.folderName) - .focusedOnAppear() - .accessibilityIdentifier("bookmark.folder.name.textfield") - .textFieldStyle(RoundedBorderTextFieldStyle()) - .disabled(model.isDisabled) - } - .padding(.bottom, 16) - - HStack { - Spacer() - - Button(action: { - model.cancel() - }) { - Text(UserText.cancel) - } - .accessibilityIdentifier("bookmark.add.cancel.button") - .disabled(model.isDisabled) - - Button(action: { - model.addFolder() - }) { - Text("Add Folder", comment: "Add Folder popover: Create folder button") - } - .keyboardShortcut(.defaultAction) - .accessibilityIdentifier("bookmark.add.add.folder.button") - .disabled(model.isAddFolderButtonDisabled || model.isDisabled) - } - } + AddEditBookmarkFolderView( + title: model.title, + buttonsState: .expanded, + folders: model.folders, + folderName: $model.folderName, + selectedFolder: $model.parent, + cancelActionTitle: model.cancelActionTitle, + isCancelActionDisabled: model.isCancelActionDisabled, + cancelAction: { _ in model.cancel() }, + defaultActionTitle: model.defaultActionTitle, + isDefaultActionDisabled: model.isDefaultActionButtonDisabled, + defaultAction: { _ in model.addFolder() } + ) + .padding(.vertical, 16.0) .font(.system(size: 13)) - .padding() - .frame(width: 300, height: 229) - .background(Color(.popoverBackground)) + .frame(width: 320) } } #if DEBUG -#Preview { +#Preview("Add Folder - Light") { let bkman = LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(bookmarks: [ BookmarkFolder(id: "1", title: "Folder 1", children: [ BookmarkFolder(id: "2", title: "Nested Folder with a name that in theory won‘t fit into the picker", children: [ @@ -94,5 +62,17 @@ struct AddBookmarkFolderPopoverView: ModalView { return AddBookmarkFolderPopoverView(model: AddBookmarkFolderPopoverViewModel(bookmarkManager: bkman) { print("CompletionHandler:", $0?.title ?? "") }) + .preferredColorScheme(.light) +} + +#Preview("Add Folder - Dark") { + let bkman = LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(bookmarks: [])) + bkman.loadBookmarks() + customAssertionFailure = { _, _, _ in } + + return AddBookmarkFolderPopoverView(model: AddBookmarkFolderPopoverViewModel(bookmarkManager: bkman) { + print("CompletionHandler:", $0?.title ?? "") + }) + .preferredColorScheme(.dark) } #endif diff --git a/DuckDuckGo/Bookmarks/View/AddBookmarkModalView.swift b/DuckDuckGo/Bookmarks/View/AddBookmarkModalView.swift deleted file mode 100644 index 56941737fd..0000000000 --- a/DuckDuckGo/Bookmarks/View/AddBookmarkModalView.swift +++ /dev/null @@ -1,77 +0,0 @@ -// -// AddBookmarkModalView.swift -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import AppKit -import SwiftUI - -struct AddBookmarkModalView: ModalView { - - @State private(set) var model: AddBookmarkModalViewModel - @Environment(\.dismiss) private var dismiss - - var body: some View { - VStack(alignment: .leading, spacing: 16) { - Text(model.title) - .fontWeight(.semibold) - - HStack(spacing: 16) { - VStack(alignment: .leading) { - Text("Title:", comment: "Add Bookmark dialog bookmark title field heading") - .frame(height: 22) - Text("Address:", comment: "Add Bookmark dialog bookmark url field heading") - .frame(height: 22) - } - - VStack { - TextField("", text: $model.bookmarkTitle) - .accessibilityIdentifier("Title Text Field") - .textFieldStyle(.roundedBorder) - TextField("", text: $model.bookmarkAddress) - .accessibilityIdentifier("URL Text Field") - .textFieldStyle(.roundedBorder) - .disableAutocorrection(true) - } - } - .padding(.bottom, 4) - - HStack { - Spacer() - - Button(UserText.cancel) { - model.cancel(dismiss: dismiss.callAsFunction) - } - .keyboardShortcut(.cancelAction) - - Button(model.addButtonTitle) { - model.addOrSave(dismiss: dismiss.callAsFunction) - } - .keyboardShortcut(.defaultAction) - .disabled(model.isAddButtonDisabled) - - } - } - .font(.system(size: 13)) - .padding() - .frame(width: 437, height: 164) - } - -} - -#Preview { - AddBookmarkModalView(model: AddBookmarkModalViewModel()) -} diff --git a/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift b/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift index c8d9cadcbd..fba84b44bc 100644 --- a/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift +++ b/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift @@ -38,82 +38,32 @@ struct AddBookmarkPopoverView: View { @MainActor private var addBookmarkView: some View { - VStack(alignment: .leading, spacing: 19) { - Text("Bookmark Added", comment: "Bookmark Added popover title") - .fontWeight(.bold) - .padding(.bottom, 4) - - VStack(alignment: .leading, spacing: 10) { - TextField("", text: $model.bookmarkTitle) - .focusedOnAppear() - .accessibilityIdentifier("bookmark.add.name.textfield") - .textFieldStyle(RoundedBorderTextFieldStyle()) - .font(.system(size: 14)) - - HStack { - BookmarkFolderPicker(folders: model.folders, - selectedFolder: $model.selectedFolder) - .accessibilityIdentifier("bookmark.add.folder.dropdown") - - Button { - model.addFolderButtonAction() - } label: { - Image(.addFolder) - } - .accessibilityIdentifier("bookmark.add.new.folder.button") - .buttonStyle(StandardButtonStyle()) - } - } - - Divider() - - Button { - model.favoritesButtonAction() - } label: { - HStack(spacing: 8) { - if model.bookmark.isFavorite { - Image(.favoriteFilled) - Text(UserText.removeFromFavorites) - } else { - Image(.favorite) - Text(UserText.addToFavorites) - } - } - } - .accessibilityIdentifier("bookmark.add.add.to.favorites.button") - .buttonStyle(.borderless) - .foregroundColor(Color.button) - - HStack { - Spacer() - - Button { - model.removeButtonAction(dismiss: dismiss.callAsFunction) - } label: { - Text("Remove", comment: "Remove bookmark button title") - } - .accessibilityIdentifier("bookmark.add.remove.button") - - Button { - model.doneButtonAction(dismiss: dismiss.callAsFunction) - } label: { - Text(UserText.done) - } - .keyboardShortcut(.defaultAction) - .accessibilityIdentifier("bookmark.add.done.button") - } - - } + AddEditBookmarkView( + title: UserText.Bookmarks.Dialog.Title.addedBookmark, + buttonsState: .expanded, + bookmarkName: $model.bookmarkTitle, + bookmarkURLPath: nil, + isBookmarkFavorite: $model.isBookmarkFavorite, + folders: model.folders, + selectedFolder: $model.selectedFolder, + isURLFieldHidden: true, + addFolderAction: model.addFolderButtonAction, + otherActionTitle: UserText.remove, + isOtherActionDisabled: false, + otherAction: model.removeButtonAction, + defaultActionTitle: UserText.done, + isDefaultActionDisabled: model.isDefaultActionButtonDisabled, + defaultAction: model.doneButtonAction + ) + .padding(.vertical, 16.0) .font(.system(size: 13)) - .padding(EdgeInsets(top: 19, leading: 19, bottom: 19, trailing: 19)) - .frame(width: 300, height: 229) - .background(Color(.popoverBackground)) + .frame(width: 320) } } #if DEBUG -#Preview { { +#Preview("Bookmark Added - Light") { let bkm = Bookmark(id: "n", url: URL.duckDuckGo.absoluteString, title: "DuckDuckGo", isFavorite: false, parentFolderUUID: "1") let bkman = LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(bookmarks: [ BookmarkFolder(id: "1", title: "Folder with a name that shouldn‘t fit into the picker", children: [ @@ -133,5 +83,16 @@ struct AddBookmarkPopoverView: View { customAssertionFailure = { _, _, _ in } return AddBookmarkPopoverView(model: AddBookmarkPopoverViewModel(bookmark: bkm, bookmarkManager: bkman)) -}() } + .preferredColorScheme(.light) +} + +#Preview("Bookmark Added - Dark") { + let bkm = Bookmark(id: "n", url: URL.duckDuckGo.absoluteString, title: "DuckDuckGo", isFavorite: false, parentFolderUUID: "1") + let bkman = LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(bookmarks: [ + BookmarkFolder(id: "1", title: "Folder with a name that shouldn‘t fit into the picker", children: [])])) + bkman.loadBookmarks() + + return AddBookmarkPopoverView(model: AddBookmarkPopoverViewModel(bookmark: bkm, bookmarkManager: bkman)) + .preferredColorScheme(.dark) +} #endif diff --git a/DuckDuckGo/Bookmarks/View/BookmarkFolderPicker.swift b/DuckDuckGo/Bookmarks/View/BookmarkFolderPicker.swift index c0aeab9bec..e7de655e8c 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkFolderPicker.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkFolderPicker.swift @@ -32,7 +32,7 @@ struct BookmarkFolderPicker: View { return popUpButton } content: { - PopupButtonItem(icon: .folder, title: UserText.bookmarks) + PopupButtonItem(icon: .bookmarksFolder, title: UserText.bookmarks) PopupButtonItem.separator() diff --git a/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift b/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift index 541ad955b6..c405c23f61 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift @@ -63,6 +63,9 @@ final class BookmarkListViewController: NSViewController { contentMode: .bookmarksAndFolders, bookmarkManager: bookmarkManager, treeController: treeController, + onMenuRequestedAction: { [weak self] cell in + self?.showContextMenu(for: cell) + }, presentFaviconsFetcherOnboarding: { [weak self] in guard let self, let window = self.view.window else { return @@ -334,23 +337,18 @@ final class BookmarkListViewController: NSViewController { } @objc func newBookmarkButtonClicked(_ sender: AnyObject) { - delegate?.popover(shouldPreventClosure: true) - AddBookmarkModalView(model: AddBookmarkModalViewModel(currentTabWebsite: currentTabWebsite) { [weak delegate] _ in - delegate?.popover(shouldPreventClosure: false) - }).show(in: parent?.view.window) + let view = BookmarksDialogViewFactory.makeAddBookmarkView(currentTab: currentTabWebsite) + showDialog(view: view) } @objc func newFolderButtonClicked(_ sender: AnyObject) { - delegate?.popover(shouldPreventClosure: true) - AddBookmarkFolderModalView() - .show(in: parent?.view.window) { [weak delegate] in - delegate?.popover(shouldPreventClosure: false) - } + let parentFolder = sender.representedObject as? BookmarkFolder + let view = BookmarksDialogViewFactory.makeAddBookmarkFolderView(parentFolder: parentFolder) + showDialog(view: view) } @objc func openManagementInterface(_ sender: NSButton) { - WindowControllersManager.shared.showBookmarksTab() - delegate?.popoverShouldClose(self) + showManageBookmarks() } @objc func handleClick(_ sender: NSOutlineView) { @@ -425,6 +423,35 @@ final class BookmarkListViewController: NSViewController { outlineView.selectRowIndexes(indexes, byExtendingSelection: false) } + private func showContextMenu(for cell: BookmarkOutlineCellView) { + let row = outlineView.row(for: cell) + guard + let item = outlineView.item(atRow: row), + let contextMenu = ContextualMenu.menu(for: [item], target: self) + else { + return + } + + contextMenu.popUpAtMouseLocation(in: view) + } + +} + +private extension BookmarkListViewController { + + func showDialog(view: any ModalView) { + delegate?.popover(shouldPreventClosure: true) + + view.show(in: parent?.view.window) { [weak delegate] in + delegate?.popover(shouldPreventClosure: false) + } + } + + func showManageBookmarks() { + WindowControllersManager.shared.showBookmarksTab() + delegate?.popoverShouldClose(self) + } + } // MARK: - Menu Item Selectors @@ -439,11 +466,11 @@ extension BookmarkListViewController: NSMenuDelegate { } if outlineView.selectedRowIndexes.contains(row) { - return ContextualMenu.menu(for: outlineView.selectedItems, includeBookmarkEditMenu: false) + return ContextualMenu.menu(for: outlineView.selectedItems) } if let item = outlineView.item(atRow: row) { - return ContextualMenu.menu(for: [item], includeBookmarkEditMenu: false) + return ContextualMenu.menu(for: [item]) } else { return nil } @@ -498,7 +525,13 @@ extension BookmarkListViewController: BookmarkMenuItemSelectors { } func editBookmark(_ sender: NSMenuItem) { - // Unsupported in the list view for the initial release. + guard let bookmark = sender.representedObject as? Bookmark else { + assertionFailure("Failed to retrieve Bookmark from Edit Bookmark context menu item") + return + } + + let view = BookmarksDialogViewFactory.makeEditBookmarkView(bookmark: bookmark) + showDialog(view: view) } func copyBookmark(_ sender: NSMenuItem) { @@ -527,6 +560,20 @@ extension BookmarkListViewController: BookmarkMenuItemSelectors { bookmarkManager.remove(objectsWithUUIDs: uuids) } + func manageBookmarks(_ sender: NSMenuItem) { + showManageBookmarks() + } + + func moveToEnd(_ sender: NSMenuItem) { + guard let bookmarkEntity = sender.representedObject as? BookmarksEntityIdentifiable else { + assertionFailure("Failed to cast menu item's represented object to BookmarkEntity") + return + } + + let parentFolderType: ParentFolderType = bookmarkEntity.parentId.flatMap { .parent(uuid: $0) } ?? .root + bookmarkManager.move(objectUUIDs: [bookmarkEntity.entityId], toIndex: nil, withinParentFolder: parentFolderType) { _ in } + } + } extension BookmarkListViewController: FolderMenuItemSelectors { @@ -535,18 +582,16 @@ extension BookmarkListViewController: FolderMenuItemSelectors { newFolderButtonClicked(sender) } - func renameFolder(_ sender: NSMenuItem) { - guard let folder = sender.representedObject as? BookmarkFolder else { - assertionFailure("Failed to retrieve Bookmark from Rename Folder context menu item") + func editFolder(_ sender: NSMenuItem) { + guard let bookmarkEntityInfo = sender.representedObject as? BookmarkEntityInfo, + let folder = bookmarkEntityInfo.entity as? BookmarkFolder + else { + assertionFailure("Failed to retrieve Bookmark from Edit Folder context menu item") return } - delegate?.popover(shouldPreventClosure: true) - - AddBookmarkFolderModalView(model: AddBookmarkFolderModalViewModel(folder: folder)) - .show(in: parent?.view.window) { [weak delegate] in - delegate?.popover(shouldPreventClosure: false) - } + let view = BookmarksDialogViewFactory.makeEditBookmarkFolderView(folder: folder, parentFolder: bookmarkEntityInfo.parent) + showDialog(view: view) } func deleteFolder(_ sender: NSMenuItem) { @@ -560,15 +605,28 @@ extension BookmarkListViewController: FolderMenuItemSelectors { func openInNewTabs(_ sender: NSMenuItem) { guard let tabCollection = WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.tabCollectionViewModel, - let children = (sender.representedObject as? BookmarkFolder)?.children else { - assertionFailure("Cannot open in new tabs") + let folder = sender.representedObject as? BookmarkFolder + else { + assertionFailure("Cannot open all in new tabs") return } - let tabs = children.compactMap { ($0 as? Bookmark)?.urlObject }.map { Tab(content: .url($0, source: .bookmark), shouldLoadInBackground: true, burnerMode: tabCollection.burnerMode) } + let tabs = Tab.withContentOfBookmark(folder: folder, burnerMode: tabCollection.burnerMode) tabCollection.append(tabs: tabs) } + func openAllInNewWindow(_ sender: NSMenuItem) { + guard let tabCollection = WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.tabCollectionViewModel, + let folder = sender.representedObject as? BookmarkFolder + else { + assertionFailure("Cannot open all in new window") + return + } + + let newTabCollection = TabCollection.withContentOfBookmark(folder: folder, burnerMode: tabCollection.burnerMode) + WindowsManager.openNewWindow(with: newTabCollection, isBurner: tabCollection.isBurner) + } + } // MARK: - BookmarkListPopover diff --git a/DuckDuckGo/Bookmarks/View/BookmarkManagementDetailViewController.swift b/DuckDuckGo/Bookmarks/View/BookmarkManagementDetailViewController.swift index a9b03ede97..ecc643b33d 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkManagementDetailViewController.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkManagementDetailViewController.swift @@ -32,12 +32,10 @@ private struct EditedBookmarkMetadata { final class BookmarkManagementDetailViewController: NSViewController, NSMenuItemValidation { - fileprivate enum Constants { - static let animationSpeed: TimeInterval = 0.3 - } - + private let toolbarButtonsStackView = NSStackView() private lazy var newBookmarkButton = MouseOverButton(title: " " + UserText.newBookmark, target: self, action: #selector(presentAddBookmarkModal)) private lazy var newFolderButton = MouseOverButton(title: " " + UserText.newFolder, target: self, action: #selector(presentAddFolderModal)) + private lazy var deleteItemsButton = MouseOverButton(title: " " + UserText.bookmarksBarContextMenuDelete, target: self, action: #selector(delete)) private lazy var separator = NSBox() private lazy var scrollView = NSScrollView() @@ -54,32 +52,10 @@ final class BookmarkManagementDetailViewController: NSViewController, NSMenuItem private let bookmarkManager: BookmarkManager private var selectionState: BookmarkManagementSidebarViewController.SelectionState = .empty { didSet { - editingBookmarkIndex = nil reloadData() } } - private var isEditing: Bool { - return editingBookmarkIndex != nil - } - - private var editingBookmarkIndex: EditedBookmarkMetadata? { - didSet { - NSAnimationContext.runAnimationGroup { context in - context.allowsImplicitAnimation = true - context.duration = Constants.animationSpeed - - NSAppearance.withAppAppearance { - if editingBookmarkIndex != nil { - view.animator().layer?.backgroundColor = NSColor.backgroundSecondary.cgColor - } else { - view.animator().layer?.backgroundColor = NSColor.bookmarkPageBackground.cgColor - } - } - } - } - } - func update(selectionState: BookmarkManagementSidebarViewController.SelectionState) { self.selectionState = selectionState } @@ -101,34 +77,16 @@ final class BookmarkManagementDetailViewController: NSViewController, NSMenuItem view.addSubview(separator) view.addSubview(scrollView) view.addSubview(emptyState) - view.addSubview(newBookmarkButton) - view.addSubview(newFolderButton) - - newBookmarkButton.bezelStyle = .shadowlessSquare - newBookmarkButton.cornerRadius = 4 - newBookmarkButton.normalTintColor = .button - newBookmarkButton.mouseDownColor = .buttonMouseDown - newBookmarkButton.mouseOverColor = .buttonMouseOver - newBookmarkButton.imageHugsTitle = true - newBookmarkButton.setContentHuggingPriority(.defaultHigh, for: .vertical) - newBookmarkButton.translatesAutoresizingMaskIntoConstraints = false - newBookmarkButton.alignment = .center - newBookmarkButton.font = .systemFont(ofSize: 13) - newBookmarkButton.image = .addBookmark - newBookmarkButton.imagePosition = .imageLeading - - newFolderButton.bezelStyle = .shadowlessSquare - newFolderButton.cornerRadius = 4 - newFolderButton.normalTintColor = .button - newFolderButton.mouseDownColor = .buttonMouseDown - newFolderButton.mouseOverColor = .buttonMouseOver - newFolderButton.imageHugsTitle = true - newFolderButton.setContentHuggingPriority(.defaultHigh, for: .vertical) - newFolderButton.translatesAutoresizingMaskIntoConstraints = false - newFolderButton.alignment = .center - newFolderButton.font = .systemFont(ofSize: 13) - newFolderButton.image = .addFolder - newFolderButton.imagePosition = .imageLeading + view.addSubview(toolbarButtonsStackView) + toolbarButtonsStackView.addArrangedSubview(newBookmarkButton) + toolbarButtonsStackView.addArrangedSubview(newFolderButton) + toolbarButtonsStackView.addArrangedSubview(deleteItemsButton) + toolbarButtonsStackView.translatesAutoresizingMaskIntoConstraints = false + toolbarButtonsStackView.distribution = .fill + + configureToolbar(button: newBookmarkButton, image: .addBookmark, isHidden: false) + configureToolbar(button: newFolderButton, image: .addFolder, isHidden: false) + configureToolbar(button: deleteItemsButton, image: .trash, isHidden: true) emptyState.addSubview(emptyStateImageView) emptyState.addSubview(emptyStateTitle) @@ -137,32 +95,27 @@ final class BookmarkManagementDetailViewController: NSViewController, NSMenuItem emptyState.isHidden = true emptyState.translatesAutoresizingMaskIntoConstraints = false + importButton.translatesAutoresizingMaskIntoConstraints = false - emptyStateTitle.isEditable = false - emptyStateTitle.setContentHuggingPriority(.defaultHigh, for: .vertical) - emptyStateTitle.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal) - emptyStateTitle.translatesAutoresizingMaskIntoConstraints = false - emptyStateTitle.alignment = .center - emptyStateTitle.drawsBackground = false - emptyStateTitle.isBordered = false - emptyStateTitle.font = .systemFont(ofSize: 15, weight: .semibold) - emptyStateTitle.textColor = .labelColor - emptyStateTitle.attributedStringValue = NSAttributedString.make(UserText.bookmarksEmptyStateTitle, - lineHeight: 1.14, - kern: -0.23) - - emptyStateMessage.isEditable = false - emptyStateMessage.setContentHuggingPriority(.defaultHigh, for: .vertical) - emptyStateMessage.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal) - emptyStateMessage.translatesAutoresizingMaskIntoConstraints = false - emptyStateMessage.alignment = .center - emptyStateMessage.drawsBackground = false - emptyStateMessage.isBordered = false - emptyStateMessage.font = .systemFont(ofSize: 13) - emptyStateMessage.textColor = .labelColor - emptyStateMessage.attributedStringValue = NSAttributedString.make(UserText.bookmarksEmptyStateMessage, - lineHeight: 1.05, - kern: -0.08) + configureEmptyState( + label: emptyStateTitle, + font: .systemFont(ofSize: 15, weight: .semibold), + attributedTitle: .make( + UserText.bookmarksEmptyStateTitle, + lineHeight: 1.14, + kern: -0.23 + ) + ) + + configureEmptyState( + label: emptyStateMessage, + font: .systemFont(ofSize: 13), + attributedTitle: .make( + UserText.bookmarksEmptyStateMessage, + lineHeight: 1.05, + kern: -0.08 + ) + ) emptyStateImageView.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal) emptyStateImageView.setContentHuggingPriority(.init(rawValue: 251), for: .vertical) @@ -195,7 +148,6 @@ final class BookmarkManagementDetailViewController: NSViewController, NSMenuItem tableView.selectionHighlightStyle = .none tableView.allowsMultipleSelection = true tableView.usesAutomaticRowHeights = true - tableView.action = #selector(handleClick) tableView.doubleAction = #selector(handleDoubleClick) tableView.delegate = self tableView.dataSource = self @@ -209,47 +161,47 @@ final class BookmarkManagementDetailViewController: NSViewController, NSMenuItem } private func setupLayout() { - newBookmarkButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 48).isActive = true - view.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: 48).isActive = true - separator.topAnchor.constraint(equalTo: newBookmarkButton.bottomAnchor, constant: 24).isActive = true - emptyState.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 20).isActive = true - scrollView.topAnchor.constraint(equalTo: separator.bottomAnchor).isActive = true - - view.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true - view.trailingAnchor.constraint(greaterThanOrEqualTo: newFolderButton.trailingAnchor, constant: 20).isActive = true - view.trailingAnchor.constraint(equalTo: separator.trailingAnchor, constant: 58).isActive = true - newFolderButton.leadingAnchor.constraint(equalTo: newBookmarkButton.trailingAnchor, constant: 16).isActive = true - emptyState.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true - newFolderButton.centerYAnchor.constraint(equalTo: newBookmarkButton.centerYAnchor).isActive = true - separator.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 58).isActive = true - newBookmarkButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 32).isActive = true - emptyState.topAnchor.constraint(greaterThanOrEqualTo: separator.bottomAnchor, constant: 8).isActive = true - scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 48).isActive = true - emptyState.centerXAnchor.constraint(equalTo: separator.centerXAnchor).isActive = true - - newBookmarkButton.heightAnchor.constraint(equalToConstant: 24).isActive = true - - newFolderButton.heightAnchor.constraint(equalToConstant: 24).isActive = true - - emptyStateMessage.centerXAnchor.constraint(equalTo: emptyState.centerXAnchor).isActive = true + NSLayoutConstraint.activate([ + toolbarButtonsStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 48), + view.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: 48), + separator.topAnchor.constraint(equalTo: toolbarButtonsStackView.bottomAnchor, constant: 24), + emptyState.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 20), + scrollView.topAnchor.constraint(equalTo: separator.bottomAnchor), - importButton.translatesAutoresizingMaskIntoConstraints = false - importButton.topAnchor.constraint(equalTo: emptyStateMessage.bottomAnchor, constant: 8).isActive = true - emptyState.heightAnchor.constraint(equalToConstant: 218).isActive = true - emptyStateMessage.topAnchor.constraint(equalTo: emptyStateTitle.bottomAnchor, constant: 8).isActive = true - importButton.centerXAnchor.constraint(equalTo: emptyState.centerXAnchor).isActive = true - emptyStateImageView.centerXAnchor.constraint(equalTo: emptyState.centerXAnchor).isActive = true - emptyState.widthAnchor.constraint(equalToConstant: 224).isActive = true - emptyStateImageView.topAnchor.constraint(equalTo: emptyState.topAnchor).isActive = true - emptyStateTitle.centerXAnchor.constraint(equalTo: emptyState.centerXAnchor).isActive = true - emptyStateTitle.topAnchor.constraint(equalTo: emptyStateImageView.bottomAnchor, constant: 8).isActive = true + view.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), + view.trailingAnchor.constraint(greaterThanOrEqualTo: toolbarButtonsStackView.trailingAnchor, constant: 20), + view.trailingAnchor.constraint(equalTo: separator.trailingAnchor, constant: 58), + emptyState.centerXAnchor.constraint(equalTo: view.centerXAnchor), + separator.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 58), + toolbarButtonsStackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 32), + emptyState.topAnchor.constraint(greaterThanOrEqualTo: separator.bottomAnchor, constant: 8), + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 48), + emptyState.centerXAnchor.constraint(equalTo: separator.centerXAnchor), + + newBookmarkButton.heightAnchor.constraint(equalToConstant: 24), + newFolderButton.heightAnchor.constraint(equalToConstant: 24), + deleteItemsButton.heightAnchor.constraint(equalToConstant: 24), - emptyStateMessage.widthAnchor.constraint(equalToConstant: 192).isActive = true + emptyStateMessage.centerXAnchor.constraint(equalTo: emptyState.centerXAnchor), - emptyStateTitle.widthAnchor.constraint(equalToConstant: 192).isActive = true + importButton.topAnchor.constraint(equalTo: emptyStateMessage.bottomAnchor, constant: 8), + emptyState.heightAnchor.constraint(equalToConstant: 218), + emptyStateMessage.topAnchor.constraint(equalTo: emptyStateTitle.bottomAnchor, constant: 8), + importButton.centerXAnchor.constraint(equalTo: emptyState.centerXAnchor), + emptyStateImageView.centerXAnchor.constraint(equalTo: emptyState.centerXAnchor), + emptyState.widthAnchor.constraint(equalToConstant: 224), + emptyStateImageView.topAnchor.constraint(equalTo: emptyState.topAnchor), + emptyStateTitle.centerXAnchor.constraint(equalTo: emptyState.centerXAnchor), + emptyStateTitle.topAnchor.constraint(equalTo: emptyStateImageView.bottomAnchor, constant: 8), + + emptyStateMessage.widthAnchor.constraint(equalToConstant: 192), + + emptyStateTitle.widthAnchor.constraint(equalToConstant: 192), + + emptyStateImageView.widthAnchor.constraint(equalToConstant: 128), + emptyStateImageView.heightAnchor.constraint(equalToConstant: 96), + ]) - emptyStateImageView.widthAnchor.constraint(equalToConstant: 128).isActive = true - emptyStateImageView.heightAnchor.constraint(equalToConstant: 96).isActive = true } override func viewDidLoad() { @@ -264,15 +216,9 @@ final class BookmarkManagementDetailViewController: NSViewController, NSMenuItem override func viewDidDisappear() { super.viewDidDisappear() - editingBookmarkIndex = nil reloadData() } - override func mouseUp(with event: NSEvent) { - // Clicking anywhere outside of the table view should end editing mode for a given cell. - updateEditingState(forRowAt: -1) - } - override func keyDown(with event: NSEvent) { if event.charactersIgnoringModifiers == String(UnicodeScalar(NSDeleteCharacter)!) { deleteSelectedItems() @@ -280,15 +226,13 @@ final class BookmarkManagementDetailViewController: NSViewController, NSMenuItem } fileprivate func reloadData() { - guard editingBookmarkIndex == nil else { - // If the table view is editing, the reload will be deferred until after the cell animation has completed. - return - } emptyState.isHidden = !(bookmarkManager.list?.topLevelEntities.isEmpty ?? true) let scrollPosition = tableView.visibleRect.origin tableView.reloadData() tableView.scroll(scrollPosition) + + updateToolbarButtons() } @objc func onImportClicked(_ sender: NSButton) { @@ -306,7 +250,7 @@ final class BookmarkManagementDetailViewController: NSViewController, NSMenuItem let index = sender.clickedRow - guard index != -1, editingBookmarkIndex?.index != index, let entity = fetchEntity(at: index) else { + guard index != -1, let entity = fetchEntity(at: index) else { return } @@ -324,21 +268,13 @@ final class BookmarkManagementDetailViewController: NSViewController, NSMenuItem } } - @objc func handleClick(_ sender: NSTableView) { - let index = sender.clickedRow - - if index != editingBookmarkIndex?.index { - endEditing() - } - } - @objc func presentAddBookmarkModal(_ sender: Any) { - AddBookmarkModalView(model: AddBookmarkModalViewModel(parent: selectionState.folder)) + BookmarksDialogViewFactory.makeAddBookmarkView(parent: selectionState.folder) .show(in: view.window) } @objc func presentAddFolderModal(_ sender: Any) { - AddBookmarkFolderModalView(model: AddBookmarkFolderModalViewModel(parent: selectionState.folder)) + BookmarksDialogViewFactory.makeAddBookmarkFolderView(parentFolder: selectionState.folder) .show(in: view.window) } @@ -354,53 +290,6 @@ final class BookmarkManagementDetailViewController: NSViewController, NSMenuItem return true } - private func endEditing() { - if let editingIndex = editingBookmarkIndex?.index { - self.editingBookmarkIndex = nil - animateEditingState(forRowAt: editingIndex, editing: false) - } - } - - private func updateEditingState(forRowAt index: Int) { - guard index != -1 else { - endEditing() - return - } - - if editingBookmarkIndex?.index == nil || editingBookmarkIndex?.index != index { - endEditing() - } - - if let entity = fetchEntity(at: index) { - editingBookmarkIndex = EditedBookmarkMetadata(uuid: entity.id, index: index) - animateEditingState(forRowAt: index, editing: true) - } else { - assertionFailure("\(#file): Failed to find entity when updating editing state") - } - } - - private func animateEditingState(forRowAt index: Int, editing: Bool, completion: (() -> Void)? = nil) { - if let cell = tableView.view(atColumn: 0, row: index, makeIfNecessary: false) as? BookmarkTableCellView, - let row = tableView.rowView(atRow: index, makeIfNecessary: false) as? BookmarkTableRowView { - - tableView.beginUpdates() - NSAnimationContext.runAnimationGroup { context in - context.allowsImplicitAnimation = true - context.duration = Constants.animationSpeed - context.completionHandler = completion - - cell.editing = editing - row.editing = editing - - row.layoutSubtreeIfNeeded() - cell.layoutSubtreeIfNeeded() - tableView.noteHeightOfRows(withIndexesChanged: IndexSet(arrayLiteral: 0, index)) - } - - tableView.endUpdates() - } - } - private func totalRows() -> Int { switch selectionState { case .empty: @@ -444,12 +333,6 @@ extension BookmarkManagementDetailViewController: NSTableViewDelegate, NSTableVi let rowView = BookmarkTableRowView() rowView.onSelectionChanged = onSelectionChanged - let entity = fetchEntity(at: row) - - if let uuid = editingBookmarkIndex?.uuid, uuid == entity?.id { - rowView.editing = true - } - return rowView } @@ -463,14 +346,12 @@ extension BookmarkManagementDetailViewController: NSTableViewDelegate, NSTableVi if let bookmark = entity as? Bookmark { cell.update(from: bookmark) - cell.editing = bookmark.id == editingBookmarkIndex?.uuid if bookmark.favicon(.small) == nil { faviconsFetcherOnboarding?.presentOnboardingIfNeeded() } } else if let folder = entity as? BookmarkFolder { cell.update(from: folder) - cell.editing = folder.id == editingBookmarkIndex?.uuid } else { assertionFailure("Failed to cast bookmark") } @@ -573,6 +454,17 @@ extension BookmarkManagementDetailViewController: NSTableViewDelegate, NSTableVi } } + private func fetchEntityAndParent(at row: Int) -> (entity: BaseBookmarkEntity?, parentFolder: BookmarkFolder?) { + switch selectionState { + case .empty: + return (bookmarkManager.list?.topLevelEntities[safe: row], nil) + case .folder(let folder): + return (folder.children[safe: row], folder) + case .favorites: + return (bookmarkManager.list?.favoriteBookmarks[safe: row], nil) + } + } + private func index(for entity: Bookmark) -> Int? { switch selectionState { case .empty: @@ -610,11 +502,25 @@ extension BookmarkManagementDetailViewController: NSTableViewDelegate, NSTableVi } func onSelectionChanged() { - resetSelections() - let indexes = tableView.selectedRowIndexes - indexes.forEach { - let cell = self.tableView.view(atColumn: 0, row: $0, makeIfNecessary: false) as? BookmarkTableCellView - cell?.isSelected = true + func updateCellSelections() { + resetSelections() + tableView.selectedRowIndexes.forEach { + let cell = self.tableView.view(atColumn: 0, row: $0, makeIfNecessary: false) as? BookmarkTableCellView + cell?.isSelected = true + } + } + + updateCellSelections() + updateToolbarButtons() + } + + private func updateToolbarButtons() { + let shouldShowDeleteButton = tableView.selectedRowIndexes.count > 1 + NSAnimationContext.runAnimationGroup { context in + context.duration = 0.25 + deleteItemsButton.animator().isHidden = !shouldShowDeleteButton + newBookmarkButton.animator().isHidden = shouldShowDeleteButton + newFolderButton.animator().isHidden = shouldShowDeleteButton } } @@ -633,13 +539,45 @@ extension BookmarkManagementDetailViewController: NSTableViewDelegate, NSTableVi } } +// MARK: - Private + +private extension BookmarkManagementDetailViewController { + + func configureToolbar(button: MouseOverButton, image: NSImage, isHidden: Bool) { + button.bezelStyle = .shadowlessSquare + button.cornerRadius = 4 + button.normalTintColor = .button + button.mouseDownColor = .buttonMouseDown + button.mouseOverColor = .buttonMouseOver + button.imageHugsTitle = true + button.setContentHuggingPriority(.defaultHigh, for: .vertical) + button.alignment = .center + button.font = .systemFont(ofSize: 13) + button.image = image + button.imagePosition = .imageLeading + button.isHidden = isHidden + } + + func configureEmptyState(label: NSTextField, font: NSFont, attributedTitle: NSAttributedString) { + label.isEditable = false + label.setContentHuggingPriority(.defaultHigh, for: .vertical) + label.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal) + label.translatesAutoresizingMaskIntoConstraints = false + label.alignment = .center + label.drawsBackground = false + label.isBordered = false + label.font = font + label.textColor = .labelColor + label.attributedStringValue = attributedTitle + } + +} + // MARK: - BookmarkTableCellViewDelegate extension BookmarkManagementDetailViewController: BookmarkTableCellViewDelegate { func bookmarkTableCellViewRequestedMenu(_ sender: NSButton, cell: BookmarkTableCellView) { - guard !isEditing else { return } - let row = tableView.row(for: cell) guard let bookmark = fetchEntity(at: row) as? Bookmark else { @@ -647,45 +585,8 @@ extension BookmarkManagementDetailViewController: BookmarkTableCellViewDelegate return } - if let contextMenu = ContextualMenu.menu(for: [bookmark]), let cursorLocation = self.view.window?.mouseLocationOutsideOfEventStream { - let convertedLocation = self.view.convert(cursorLocation, from: nil) - contextMenu.items.forEach { item in - item.target = self - } - - contextMenu.popUp(positioning: nil, at: convertedLocation, in: self.view) - } - } - - func bookmarkTableCellViewToggledFavorite(cell: BookmarkTableCellView) { - let row = tableView.row(for: cell) - - guard let bookmark = fetchEntity(at: row) as? Bookmark else { - assertionFailure("BookmarkManagementDetailViewController: Tried to favorite object which is not bookmark") - return - } - - bookmark.isFavorite.toggle() - bookmarkManager.update(bookmark: bookmark) - } - - func bookmarkTableCellView(_ cell: BookmarkTableCellView, updatedBookmarkWithUUID uuid: String, newTitle: String, newUrl: String) { - let row = tableView.row(for: cell) - defer { - endEditing() - } - guard var bookmark = fetchEntity(at: row) as? Bookmark, bookmark.id == uuid else { - return - } - - if let url = newUrl.url, url.absoluteString != bookmark.url { - bookmark = bookmarkManager.updateUrl(of: bookmark, to: url) ?? bookmark - } - let bookmarkTitle = (newTitle.isEmpty ? bookmark.title : newTitle).trimmingWhitespace() - if bookmark.title != bookmarkTitle { - bookmark.title = bookmarkTitle - bookmarkManager.update(bookmark: bookmark) - } + guard let contextMenu = ContextualMenu.menu(for: [bookmark], target: self) else { return } + contextMenu.popUpAtMouseLocation(in: view) } } @@ -695,20 +596,21 @@ extension BookmarkManagementDetailViewController: BookmarkTableCellViewDelegate extension BookmarkManagementDetailViewController: NSMenuDelegate { func contextualMenuForClickedRows() -> NSMenu? { - guard !isEditing else { return nil } - let row = tableView.clickedRow guard row != -1 else { return ContextualMenu.menu(for: nil) } - if tableView.selectedRowIndexes.contains(row) { + // If only one item is selected try to get the item and its parent folder otherwise show the menu for multiple items. + if tableView.selectedRowIndexes.contains(row), tableView.selectedRowIndexes.count > 1 { return ContextualMenu.menu(for: self.selectedItems()) } - if let item = fetchEntity(at: row) { - return ContextualMenu.menu(for: [item]) + let (item, parent) = fetchEntityAndParent(at: row) + + if let item { + return ContextualMenu.menu(for: item, parentFolder: parent) } else { return nil } @@ -738,13 +640,15 @@ extension BookmarkManagementDetailViewController: FolderMenuItemSelectors { presentAddFolderModal(sender) } - func renameFolder(_ sender: NSMenuItem) { - guard let folder = sender.representedObject as? BookmarkFolder else { + func editFolder(_ sender: NSMenuItem) { + guard let bookmarkEntityInfo = sender.representedObject as? BookmarkEntityInfo, + let folder = bookmarkEntityInfo.entity as? BookmarkFolder + else { assertionFailure("Failed to cast menu represented object to BookmarkFolder") return } - AddBookmarkFolderModalView(model: AddBookmarkFolderModalViewModel(folder: folder)) + BookmarksDialogViewFactory.makeEditBookmarkFolderView(folder: folder, parentFolder: bookmarkEntityInfo.parent) .show(in: view.window) } @@ -757,6 +661,16 @@ extension BookmarkManagementDetailViewController: FolderMenuItemSelectors { bookmarkManager.remove(folder: folder) } + func moveToEnd(_ sender: NSMenuItem) { + guard let bookmarkEntity = sender.representedObject as? BookmarksEntityIdentifiable else { + assertionFailure("Failed to cast menu item's represented object to BookmarkEntity") + return + } + + let parentFolderType: ParentFolderType = bookmarkEntity.parentId.flatMap { .parent(uuid: $0) } ?? .root + bookmarkManager.move(objectUUIDs: [bookmarkEntity.entityId], toIndex: nil, withinParentFolder: parentFolderType) { _ in } + } + func openInNewTabs(_ sender: NSMenuItem) { if let children = (sender.representedObject as? BookmarkFolder)?.children { let bookmarks = children.compactMap { $0 as? Bookmark } @@ -768,6 +682,18 @@ extension BookmarkManagementDetailViewController: FolderMenuItemSelectors { } } + func openAllInNewWindow(_ sender: NSMenuItem) { + guard let tabCollection = WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.tabCollectionViewModel, + let folder = sender.representedObject as? BookmarkFolder + else { + assertionFailure("Cannot open all in new window") + return + } + + let newTabCollection = TabCollection.withContentOfBookmark(folder: folder, burnerMode: tabCollection.burnerMode) + WindowsManager.openNewWindow(with: newTabCollection, isBurner: tabCollection.isBurner) + } + } extension BookmarkManagementDetailViewController: BookmarkMenuItemSelectors { @@ -811,8 +737,10 @@ extension BookmarkManagementDetailViewController: BookmarkMenuItemSelectors { } func editBookmark(_ sender: NSMenuItem) { - guard let bookmark = sender.representedObject as? Bookmark, let bookmarkIndex = index(for: bookmark) else { return } - updateEditingState(forRowAt: bookmarkIndex) + guard let bookmark = sender.representedObject as? Bookmark else { return } + + BookmarksDialogViewFactory.makeEditBookmarkView(bookmark: bookmark) + .show(in: view.window) } func copyBookmark(_ sender: NSMenuItem) { diff --git a/DuckDuckGo/Bookmarks/View/BookmarkManagementSidebarViewController.swift b/DuckDuckGo/Bookmarks/View/BookmarkManagementSidebarViewController.swift index 6d0fdd6860..53e502383b 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkManagementSidebarViewController.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkManagementSidebarViewController.swift @@ -51,7 +51,7 @@ final class BookmarkManagementSidebarViewController: NSViewController { private lazy var outlineView = BookmarksOutlineView(frame: scrollView.frame) private lazy var treeController = BookmarkTreeController(dataSource: treeControllerDataSource) - private lazy var dataSource = BookmarkOutlineViewDataSource(contentMode: .foldersOnly, bookmarkManager: bookmarkManager, treeController: treeController) + private lazy var dataSource = BookmarkOutlineViewDataSource(contentMode: .foldersOnly, bookmarkManager: bookmarkManager, treeController: treeController, showMenuButtonOnHover: false) private var cancellables = Set() @@ -211,6 +211,13 @@ final class BookmarkManagementSidebarViewController: NSViewController { // MARK: NSOutlineView Configuration private func expandAndRestore(selectedNodes: [BookmarkNode]) { + // OutlineView doesn't allow multiple selections so there should be only one selected node at time. + let selectedNode = selectedNodes.first + // As the data source reloaded we need to refresh the previously selected nodes. + // Lets consider the scenario where we add a folder to a subfolder. + // When the folder is added we need to "refresh" the node because the previously selected node folder has changed (it has a child folder now). + var refreshedSelectedNodes: [BookmarkNode] = [] + treeController.visitNodes { node in if let objectID = (node.representedObject as? BaseBookmarkEntity)?.id { if dataSource.expandedNodesIDs.contains(objectID) { @@ -218,6 +225,11 @@ final class BookmarkManagementSidebarViewController: NSViewController { } else { outlineView.collapseItem(node) } + + // Add the node if it contains previously selected folder + if let folder = selectedNode?.representedObject as? BookmarkFolder, folder.id == objectID { + refreshedSelectedNodes.append(node) + } } // Expand the Bookmarks pseudo folder automatically. @@ -226,7 +238,7 @@ final class BookmarkManagementSidebarViewController: NSViewController { } } - restoreSelection(to: selectedNodes) + restoreSelection(to: refreshedSelectedNodes) } private func restoreSelection(to nodes: [BookmarkNode]) { @@ -292,16 +304,20 @@ extension BookmarkManagementSidebarViewController: NSMenuDelegate { extension BookmarkManagementSidebarViewController: FolderMenuItemSelectors { func newFolder(_ sender: NSMenuItem) { - AddBookmarkFolderModalView().show(in: view.window) + let parent = sender.representedObject as? BookmarkFolder + BookmarksDialogViewFactory.makeAddBookmarkFolderView(parentFolder: parent) + .show(in: view.window) } - func renameFolder(_ sender: NSMenuItem) { - guard let folder = sender.representedObject as? BookmarkFolder else { - assertionFailure("Failed to retrieve Bookmark from Rename Folder context menu item") + func editFolder(_ sender: NSMenuItem) { + guard let bookmarkEntityInfo = sender.representedObject as? BookmarkEntityInfo, + let folder = bookmarkEntityInfo.entity as? BookmarkFolder + else { + assertionFailure("Failed to cast menu represented object to BookmarkFolder") return } - AddBookmarkFolderModalView(model: AddBookmarkFolderModalViewModel(folder: folder)) + BookmarksDialogViewFactory.makeEditBookmarkFolderView(folder: folder, parentFolder: bookmarkEntityInfo.parent) .show(in: view.window) } @@ -314,17 +330,40 @@ extension BookmarkManagementSidebarViewController: FolderMenuItemSelectors { bookmarkManager.remove(folder: folder) } + func moveToEnd(_ sender: NSMenuItem) { + guard let bookmarkEntity = sender.representedObject as? BookmarksEntityIdentifiable else { + assertionFailure("Failed to cast menu item's represented object to BookmarkEntity") + return + } + + let parentFolderType: ParentFolderType = bookmarkEntity.parentId.flatMap { .parent(uuid: $0) } ?? .root + bookmarkManager.move(objectUUIDs: [bookmarkEntity.entityId], toIndex: nil, withinParentFolder: parentFolderType) { _ in } + } + func openInNewTabs(_ sender: NSMenuItem) { guard let tabCollection = WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.tabCollectionViewModel, - let children = (sender.representedObject as? BookmarkFolder)?.children else { - assertionFailure("Cannot open in new tabs") + let folder = sender.representedObject as? BookmarkFolder + else { + assertionFailure("Cannot open all in new tabs") return } - let tabs = children.compactMap { ($0 as? Bookmark)?.urlObject }.map { Tab(content: .url($0, source: .bookmark), shouldLoadInBackground: true, burnerMode: tabCollection.burnerMode) } + let tabs = Tab.withContentOfBookmark(folder: folder, burnerMode: tabCollection.burnerMode) tabCollection.append(tabs: tabs) } + func openAllInNewWindow(_ sender: NSMenuItem) { + guard let tabCollection = WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.tabCollectionViewModel, + let folder = sender.representedObject as? BookmarkFolder + else { + assertionFailure("Cannot open all in new window") + return + } + + let newTabCollection = TabCollection.withContentOfBookmark(folder: folder, burnerMode: tabCollection.burnerMode) + WindowsManager.openNewWindow(with: newTabCollection, isBurner: tabCollection.isBurner) + } + } #if DEBUG diff --git a/DuckDuckGo/Bookmarks/View/BookmarkOutlineCellView.swift b/DuckDuckGo/Bookmarks/View/BookmarkOutlineCellView.swift index 08e6c56953..603849bbbf 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkOutlineCellView.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkOutlineCellView.swift @@ -19,11 +19,24 @@ import AppKit import Foundation +protocol BookmarkOutlineCellViewDelegate: AnyObject { + func outlineCellViewRequestedMenu(_ cell: BookmarkOutlineCellView) +} + final class BookmarkOutlineCellView: NSTableCellView { private lazy var faviconImageView = NSImageView() private lazy var titleLabel = NSTextField(string: "Bookmark/Folder") private lazy var countLabel = NSTextField(string: "42") + private lazy var menuButton = NSButton(title: "", image: .settings, target: self, action: #selector(cellMenuButtonClicked)) + private lazy var favoriteImageView = NSImageView() + private lazy var trackingArea: NSTrackingArea = { + NSTrackingArea(rect: .zero, options: [.inVisibleRect, .activeAlways, .mouseEnteredAndExited], owner: self, userInfo: nil) + }() + + var shouldShowMenuButton = false + + weak var delegate: BookmarkOutlineCellViewDelegate? init(identifier: NSUserInterfaceItemIdentifier) { super.init(frame: .zero) @@ -34,10 +47,35 @@ final class BookmarkOutlineCellView: NSTableCellView { fatalError("\(type(of: self)): Bad initializer") } + override func updateTrackingAreas() { + super.updateTrackingAreas() + + guard !trackingAreas.contains(trackingArea), shouldShowMenuButton else { return } + addTrackingArea(trackingArea) + } + + override func mouseEntered(with event: NSEvent) { + guard shouldShowMenuButton else { return } + countLabel.isHidden = true + favoriteImageView.isHidden = true + menuButton.isHidden = false + } + + override func mouseExited(with event: NSEvent) { + guard shouldShowMenuButton else { return } + menuButton.isHidden = true + countLabel.isHidden = false + favoriteImageView.isHidden = false + } + + // MARK: - Private + private func setupUI() { addSubview(faviconImageView) addSubview(titleLabel) addSubview(countLabel) + addSubview(menuButton) + addSubview(favoriteImageView) faviconImageView.translatesAutoresizingMaskIntoConstraints = false faviconImageView.image = .bookmarkDefaultFavicon @@ -64,40 +102,74 @@ final class BookmarkOutlineCellView: NSTableCellView { countLabel.textColor = .blackWhite60 countLabel.lineBreakMode = .byClipping + menuButton.translatesAutoresizingMaskIntoConstraints = false + menuButton.contentTintColor = .button + menuButton.imagePosition = .imageTrailing + menuButton.isBordered = false + menuButton.isHidden = true + + favoriteImageView.translatesAutoresizingMaskIntoConstraints = false + favoriteImageView.imageScaling = .scaleProportionallyDown setupLayout() } private func setupLayout() { - faviconImageView.heightAnchor.constraint(equalToConstant: 16).isActive = true - faviconImageView.widthAnchor.constraint(equalToConstant: 16).isActive = true - faviconImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 5).isActive = true - faviconImageView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true + NSLayoutConstraint.activate([ + faviconImageView.heightAnchor.constraint(equalToConstant: 16), + faviconImageView.widthAnchor.constraint(equalToConstant: 16), + faviconImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 5), + faviconImageView.centerYAnchor.constraint(equalTo: centerYAnchor), + + titleLabel.leadingAnchor.constraint(equalTo: faviconImageView.trailingAnchor, constant: 10), + bottomAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 6), + titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 6), + + countLabel.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), + countLabel.leadingAnchor.constraint(equalTo: titleLabel.trailingAnchor, constant: 5), + trailingAnchor.constraint(equalTo: countLabel.trailingAnchor), + + menuButton.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), + menuButton.leadingAnchor.constraint(greaterThanOrEqualTo: titleLabel.trailingAnchor, constant: 5), + menuButton.trailingAnchor.constraint(equalTo: trailingAnchor), + menuButton.topAnchor.constraint(equalTo: topAnchor), + menuButton.bottomAnchor.constraint(equalTo: bottomAnchor), + menuButton.widthAnchor.constraint(equalToConstant: 28), + + favoriteImageView.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), + favoriteImageView.leadingAnchor.constraint(greaterThanOrEqualTo: titleLabel.trailingAnchor, constant: 5), + favoriteImageView.trailingAnchor.constraint(equalTo: menuButton.trailingAnchor), + favoriteImageView.heightAnchor.constraint(equalToConstant: 15), + favoriteImageView.widthAnchor.constraint(equalToConstant: 15), + ]) + faviconImageView.setContentHuggingPriority(NSLayoutConstraint.Priority(rawValue: 251), for: .horizontal) faviconImageView.setContentHuggingPriority(NSLayoutConstraint.Priority(rawValue: 251), for: .vertical) - titleLabel.leadingAnchor.constraint(equalTo: faviconImageView.trailingAnchor, constant: 10).isActive = true - bottomAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 6).isActive = true - titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 6).isActive = true titleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) titleLabel.setContentHuggingPriority(.defaultHigh, for: .vertical) titleLabel.setContentHuggingPriority(.init(rawValue: 200), for: .horizontal) - countLabel.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor).isActive = true - countLabel.leadingAnchor.constraint(equalTo: titleLabel.trailingAnchor, constant: 5).isActive = true - trailingAnchor.constraint(equalTo: countLabel.trailingAnchor).isActive = true countLabel.setContentHuggingPriority(.defaultHigh, for: .vertical) countLabel.setContentHuggingPriority(.required, for: .horizontal) } + @objc private func cellMenuButtonClicked() { + delegate?.outlineCellViewRequestedMenu(self) + } + + // MARK: - Public + func update(from bookmark: Bookmark) { faviconImageView.image = bookmark.favicon(.small) ?? .bookmarkDefaultFavicon titleLabel.stringValue = bookmark.title countLabel.stringValue = "" + favoriteImageView.image = bookmark.isFavorite ? .favoriteFilledBorder : nil } func update(from folder: BookmarkFolder) { faviconImageView.image = .folder titleLabel.stringValue = folder.title + favoriteImageView.image = nil let totalChildBookmarks = folder.totalChildBookmarks if totalChildBookmarks > 0 { @@ -111,10 +183,13 @@ final class BookmarkOutlineCellView: NSTableCellView { faviconImageView.image = pseudoFolder.icon titleLabel.stringValue = pseudoFolder.name countLabel.stringValue = pseudoFolder.count > 0 ? String(pseudoFolder.count) : "" + favoriteImageView.image = nil } } +// MARK: - Preview + #if DEBUG @available(macOS 14.0, *) #Preview { diff --git a/DuckDuckGo/Bookmarks/View/BookmarkTableCellView.swift b/DuckDuckGo/Bookmarks/View/BookmarkTableCellView.swift index 195ad48845..8bfdc3c4fb 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkTableCellView.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkTableCellView.swift @@ -22,8 +22,6 @@ import Foundation @objc protocol BookmarkTableCellViewDelegate: AnyObject { func bookmarkTableCellViewRequestedMenu(_ sender: NSButton, cell: BookmarkTableCellView) - func bookmarkTableCellViewToggledFavorite(cell: BookmarkTableCellView) - func bookmarkTableCellView(_ cellView: BookmarkTableCellView, updatedBookmarkWithUUID uuid: String, newTitle: String, newUrl: String) } @@ -33,53 +31,18 @@ final class BookmarkTableCellView: NSTableCellView { private lazy var titleLabel = NSTextField(string: "Bookmark") private lazy var bookmarkURLLabel = NSTextField(string: "URL") - private lazy var favoriteButton = NSButton(title: "", image: .favoriteFilledBorder, target: self, action: #selector(favoriteButtonClicked)) private lazy var accessoryImageView = NSImageView(image: .forward) - private var favoriteButtonBottomConstraint: NSLayoutConstraint! - private var favoriteButtonTrailingConstraint: NSLayoutConstraint! - private lazy var containerView = NSView() - private lazy var shadowView = NSBox() private lazy var menuButton = NSButton(title: "", image: .settings, target: self, action: #selector(cellMenuButtonClicked)) - // Shadow view constraints: - - private var shadowViewTopConstraint: NSLayoutConstraint! - private var shadowViewBottomConstraint: NSLayoutConstraint! - - // Container view constraints: - - private var titleLabelTopConstraint: NSLayoutConstraint! - private var titleLabelBottomConstraint: NSLayoutConstraint! - @objc func cellMenuButtonClicked(_ sender: NSButton) { delegate?.bookmarkTableCellViewRequestedMenu(sender, cell: self) } - @objc func favoriteButtonClicked(_ sender: NSButton) { - guard entity is Bookmark else { - assertionFailure("\(#file): Tried to favorite non-Bookmark object") - return - } - - delegate?.bookmarkTableCellViewToggledFavorite(cell: self) - } - weak var delegate: BookmarkTableCellViewDelegate? - var editing: Bool = false { - didSet { - if editing { - enterEditingMode() - } else { - exitEditingMode() - } - updateColors() - } - } - var isSelected = false { didSet { updateColors() @@ -96,16 +59,14 @@ final class BookmarkTableCellView: NSTableCellView { return } - accessoryImageView.isHidden = mouseInside || editing - menuButton.isHidden = !mouseInside || editing + accessoryImageView.isHidden = mouseInside + menuButton.isHidden = !mouseInside - if !mouseInside && !editing { + if !mouseInside { resetAppearanceFromBookmark() } - if !editing { - updateTitleLabelValue() - } + updateTitleLabelValue() } } @@ -130,36 +91,16 @@ final class BookmarkTableCellView: NSTableCellView { fatalError("\(type(of: self)): Bad initializer") } - // swiftlint:disable:next function_body_length private func setupUI() { autoresizingMask = [.width, .height] - addSubview(shadowView) addSubview(containerView) - shadowView.boxType = .custom - shadowView.borderColor = .clear - shadowView.borderWidth = 1 - shadowView.cornerRadius = 4 - shadowView.fillColor = .tableCellEditing - shadowView.translatesAutoresizingMaskIntoConstraints = false - shadowView.wantsLayer = true - shadowView.layer?.backgroundColor = NSColor.tableCellEditing.cgColor - shadowView.layer?.cornerRadius = 6 - - let shadow = NSShadow() - shadow.shadowOffset = NSSize(width: 0, height: -1) - shadow.shadowColor = NSColor.black.withAlphaComponent(0.2) - shadow.shadowBlurRadius = 2.0 - shadowView.shadow = shadow - containerView.translatesAutoresizingMaskIntoConstraints = false containerView.addSubview(faviconImageView) containerView.addSubview(titleLabel) containerView.addSubview(menuButton) containerView.addSubview(accessoryImageView) - containerView.addSubview(bookmarkURLLabel) - containerView.addSubview(favoriteButton) faviconImageView.contentTintColor = .suggestionIcon faviconImageView.wantsLayer = true @@ -176,92 +117,50 @@ final class BookmarkTableCellView: NSTableCellView { titleLabel.font = .systemFont(ofSize: 13) titleLabel.textColor = .labelColor titleLabel.lineBreakMode = .byTruncatingTail - titleLabel.cell?.sendsActionOnEndEditing = true titleLabel.cell?.usesSingleLineMode = true titleLabel.translatesAutoresizingMaskIntoConstraints = false titleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) titleLabel.setContentHuggingPriority(.defaultHigh, for: .vertical) titleLabel.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal) - titleLabel.delegate = self - - bookmarkURLLabel.focusRingType = .none - bookmarkURLLabel.isEditable = false - bookmarkURLLabel.isSelectable = false - bookmarkURLLabel.isBordered = false - bookmarkURLLabel.drawsBackground = false - bookmarkURLLabel.font = .systemFont(ofSize: 13) - bookmarkURLLabel.textColor = .secondaryLabelColor - bookmarkURLLabel.lineBreakMode = .byClipping - bookmarkURLLabel.translatesAutoresizingMaskIntoConstraints = false - bookmarkURLLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - bookmarkURLLabel.setContentHuggingPriority(.required, for: .vertical) - bookmarkURLLabel.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal) - bookmarkURLLabel.delegate = self accessoryImageView.translatesAutoresizingMaskIntoConstraints = false - accessoryImageView.widthAnchor.constraint(equalToConstant: 22).isActive = true - accessoryImageView.heightAnchor.constraint(equalToConstant: 32).isActive = true menuButton.contentTintColor = .button menuButton.translatesAutoresizingMaskIntoConstraints = false menuButton.isBordered = false menuButton.isHidden = true - - favoriteButton.translatesAutoresizingMaskIntoConstraints = false - favoriteButton.isBordered = false } private func setupLayout() { + NSLayoutConstraint.activate([ + trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: 3), + containerView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 3), + bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 3), + containerView.topAnchor.constraint(equalTo: topAnchor, constant: 3), - trailingAnchor.constraint(equalTo: shadowView.trailingAnchor, constant: 3).isActive = true - shadowView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 3).isActive = true - containerView.leadingAnchor.constraint(equalTo: shadowView.leadingAnchor).isActive = true - containerView.bottomAnchor.constraint(equalTo: shadowView.bottomAnchor).isActive = true - containerView.topAnchor.constraint(equalTo: shadowView.topAnchor).isActive = true - containerView.trailingAnchor.constraint(equalTo: shadowView.trailingAnchor).isActive = true + menuButton.leadingAnchor.constraint(equalTo: titleLabel.trailingAnchor, constant: 8), + faviconImageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 6), - bookmarkURLLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 10).isActive = true - favoriteButtonTrailingConstraint = trailingAnchor.constraint(equalTo: favoriteButton.trailingAnchor, constant: 3) - favoriteButtonTrailingConstraint.isActive = true + accessoryImageView.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), + titleLabel.leadingAnchor.constraint(equalTo: faviconImageView.trailingAnchor, constant: 8), + trailingAnchor.constraint(equalTo: accessoryImageView.trailingAnchor, constant: 3), + faviconImageView.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), + trailingAnchor.constraint(equalTo: menuButton.trailingAnchor, constant: 2), - menuButton.leadingAnchor.constraint(equalTo: titleLabel.trailingAnchor, constant: 8).isActive = true - faviconImageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 6).isActive = true - favoriteButton.topAnchor.constraint(equalTo: bookmarkURLLabel.bottomAnchor).isActive = true + menuButton.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), - accessoryImageView.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor).isActive = true - titleLabel.leadingAnchor.constraint(equalTo: faviconImageView.trailingAnchor, constant: 8).isActive = true - trailingAnchor.constraint(equalTo: accessoryImageView.trailingAnchor, constant: 3).isActive = true - faviconImageView.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor).isActive = true - trailingAnchor.constraint(equalTo: menuButton.trailingAnchor, constant: 2).isActive = true - bookmarkURLLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor).isActive = true - trailingAnchor.constraint(equalTo: bookmarkURLLabel.trailingAnchor, constant: 16).isActive = true - menuButton.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor).isActive = true + menuButton.heightAnchor.constraint(equalToConstant: 32), + menuButton.widthAnchor.constraint(equalToConstant: 28), - favoriteButton.widthAnchor.constraint(equalToConstant: 24).isActive = true - favoriteButton.heightAnchor.constraint(equalToConstant: 24).isActive = true + faviconImageView.heightAnchor.constraint(equalToConstant: 16), + faviconImageView.widthAnchor.constraint(equalToConstant: 16), - menuButton.heightAnchor.constraint(equalToConstant: 32).isActive = true - menuButton.widthAnchor.constraint(equalToConstant: 28).isActive = true + bottomAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8), + titleLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 5), - faviconImageView.heightAnchor.constraint(equalToConstant: 16).isActive = true - faviconImageView.widthAnchor.constraint(equalToConstant: 16).isActive = true - - shadowViewTopConstraint = shadowView.topAnchor.constraint(equalTo: topAnchor, constant: 3) - shadowViewTopConstraint.isActive = true - - shadowViewBottomConstraint = bottomAnchor.constraint(equalTo: shadowView.bottomAnchor, constant: 3) - shadowViewBottomConstraint.isActive = true - - titleLabelBottomConstraint = bottomAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8) - titleLabelBottomConstraint.priority = .init(rawValue: 250) - titleLabelBottomConstraint.isActive = true - - favoriteButtonBottomConstraint = bottomAnchor.constraint(equalTo: favoriteButton.bottomAnchor, constant: 8) - favoriteButtonBottomConstraint.priority = .init(rawValue: 750) - favoriteButtonBottomConstraint.isActive = true - - titleLabelTopConstraint = titleLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 5) - titleLabelTopConstraint.isActive = true + accessoryImageView.widthAnchor.constraint(equalToConstant: 22), + accessoryImageView.heightAnchor.constraint(equalToConstant: 32), + ]) } override var backgroundStyle: NSView.BackgroundStyle { @@ -314,84 +213,28 @@ final class BookmarkTableCellView: NSTableCellView { accessoryImageView.isHidden = false } - accessoryImageView.image = bookmark.isFavorite ? .favorite : nil - favoriteButton.image = bookmark.isFavorite ? .favoriteFilledBorder : .favorite + accessoryImageView.image = bookmark.isFavorite ? .favoriteFilledBorder : nil titleLabel.stringValue = bookmark.title primaryTitleLabelValue = bookmark.title tertiaryTitleLabelValue = bookmark.url - bookmarkURLLabel.stringValue = bookmark.url } func update(from folder: BookmarkFolder) { self.entity = folder faviconImageView.image = .folder - accessoryImageView.image = .chevronNext16 + accessoryImageView.image = .chevronMediumRight16 primaryTitleLabelValue = folder.title tertiaryTitleLabelValue = nil } private func resetCellState() { self.entity = nil - editing = false mouseInside = false - bookmarkURLLabel.isHidden = true - favoriteButton.isHidden = true - titleLabelBottomConstraint.priority = .required - } - - private func enterEditingMode() { - titleLabel.isEditable = true - bookmarkURLLabel.isEditable = true - - shadowViewTopConstraint.constant = 10 - shadowViewBottomConstraint.constant = 10 - titleLabelTopConstraint.constant = 12 - favoriteButtonTrailingConstraint.constant = 11 - favoriteButtonBottomConstraint.constant = 18 - shadowView.isHidden = false - faviconImageView.isHidden = true - - bookmarkURLLabel.isHidden = false - favoriteButton.isHidden = false - titleLabelBottomConstraint.priority = .defaultLow - - hideTertiaryValueInTitleLabel() - - // Reluctantly use GCD as a workaround for a rare label layout issue, in which the text field shows no text upon becoming first responder. - DispatchQueue.main.async { - self.titleLabel.becomeFirstResponder() - } - } - - private func exitEditingMode() { - window?.makeFirstResponder(nil) - - titleLabel.isEditable = false - bookmarkURLLabel.isEditable = false - - titleLabelTopConstraint.constant = 5 - shadowViewTopConstraint.constant = 3 - shadowViewBottomConstraint.constant = 3 - favoriteButtonTrailingConstraint.constant = 3 - favoriteButtonBottomConstraint.constant = 8 - shadowView.isHidden = true - faviconImageView.isHidden = false - - bookmarkURLLabel.isHidden = true - favoriteButton.isHidden = true - titleLabelBottomConstraint.priority = .required - - if let editedBookmark = self.entity as? Bookmark { - delegate?.bookmarkTableCellView(self, - updatedBookmarkWithUUID: editedBookmark.id, - newTitle: titleLabel.stringValue, - newUrl: bookmarkURLLabel.stringValue) - } } private func updateColors() { - titleLabel.textColor = isSelected && !editing ? .white : .controlTextColor + titleLabel.textColor = isSelected ? .white : .controlTextColor menuButton.contentTintColor = isSelected ? .white : .button faviconImageView.contentTintColor = isSelected ? .white : .suggestionIcon accessoryImageView.contentTintColor = isSelected ? .white : .suggestionIcon @@ -428,11 +271,7 @@ final class BookmarkTableCellView: NSTableCellView { } private func updateTitleLabelValue() { - guard !editing else { - return - } - - if let tertiaryValue = tertiaryTitleLabelValue, mouseInside, !editing { + if let tertiaryValue = tertiaryTitleLabelValue, mouseInside { showTertiaryValueInTitleLabel(tertiaryValue) } else { hideTertiaryValueInTitleLabel() @@ -467,26 +306,6 @@ final class BookmarkTableCellView: NSTableCellView { } -extension BookmarkTableCellView: NSTextFieldDelegate { - - func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { - switch commandSelector { - case #selector(cancelOperation) where self.editing: - self.resetAppearanceFromBookmark() - self.editing = false - return true - - case #selector(insertNewline) where self.editing: - self.editing = false - return true - - default: break - } - return false - } - -} - #if DEBUG @available(macOS 14.0, *) #Preview { @@ -517,19 +336,10 @@ extension BookmarkTableCellView { fatalError("init(coder:) has not been implemented") } - func bookmarkTableCellViewRequestedMenu(_ sender: NSButton, cell: BookmarkTableCellView) { - cell.editing.toggle() - } + func bookmarkTableCellViewRequestedMenu(_ sender: NSButton, cell: BookmarkTableCellView) {} func bookmarkTableCellViewToggledFavorite(cell: BookmarkTableCellView) { (cell.entity as? Bookmark)?.isFavorite.toggle() - cell.editing = false - } - - func bookmarkTableCellView(_ cellView: BookmarkTableCellView, updatedBookmarkWithUUID uuid: String, newTitle: String, newUrl: String) { - if cell.editing { - cell.editing = false - } } } } diff --git a/DuckDuckGo/Bookmarks/View/BookmarkTableRowView.swift b/DuckDuckGo/Bookmarks/View/BookmarkTableRowView.swift index ebd3a9f318..0f06f84788 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkTableRowView.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkTableRowView.swift @@ -16,14 +16,12 @@ // limitations under the License. // -import Foundation +import AppKit final class BookmarkTableRowView: NSTableRowView { var onSelectionChanged: (() -> Void)? - var editing = false - var hasPrevious = false { didSet { needsDisplay = true @@ -56,7 +54,7 @@ final class BookmarkTableRowView: NSTableRowView { backgroundColor.setFill() bounds.fill() - if mouseInside && !editing { + if mouseInside { let path = NSBezierPath(roundedRect: bounds, xRadius: 6, yRadius: 6) NSColor.rowHover.setFill() path.fill() @@ -68,8 +66,6 @@ final class BookmarkTableRowView: NSTableRowView { } override func drawSelection(in dirtyRect: NSRect) { - guard !editing else { return } - var roundedCorners = [NSBezierPath.Corners]() if !hasPrevious { diff --git a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift new file mode 100644 index 0000000000..2c0256bba4 --- /dev/null +++ b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift @@ -0,0 +1,123 @@ +// +// AddEditBookmarkDialogView.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct AddEditBookmarkDialogView: ModalView { + @ObservedObject private var viewModel: AddEditBookmarkDialogCoordinatorViewModel + + init(viewModel: AddEditBookmarkDialogCoordinatorViewModel) { + self.viewModel = viewModel + } + + var body: some View { + Group { + switch viewModel.viewState { + case .bookmark: + addEditBookmarkView + case .folder: + addFolderView + } + } + .font(.system(size: 13)) + } + + private var addEditBookmarkView: some View { + AddEditBookmarkView( + title: viewModel.bookmarkModel.title, + buttonsState: .compressed, + bookmarkName: $viewModel.bookmarkModel.bookmarkName, + bookmarkURLPath: $viewModel.bookmarkModel.bookmarkURLPath, + isBookmarkFavorite: $viewModel.bookmarkModel.isBookmarkFavorite, + folders: viewModel.bookmarkModel.folders, + selectedFolder: $viewModel.bookmarkModel.selectedFolder, + isURLFieldHidden: false, + addFolderAction: viewModel.addFolderAction, + otherActionTitle: viewModel.bookmarkModel.cancelActionTitle, + isOtherActionDisabled: viewModel.bookmarkModel.isOtherActionDisabled, + otherAction: viewModel.bookmarkModel.cancel, + defaultActionTitle: viewModel.bookmarkModel.defaultActionTitle, + isDefaultActionDisabled: viewModel.bookmarkModel.isDefaultActionDisabled, + defaultAction: viewModel.bookmarkModel.addOrSave + ) + .frame(width: 448, height: 288) + } + + private var addFolderView: some View { + AddEditBookmarkFolderView( + title: viewModel.folderModel.title, + buttonsState: .compressed, + folders: viewModel.folderModel.folders, + folderName: $viewModel.folderModel.folderName, + selectedFolder: $viewModel.folderModel.selectedFolder, + cancelActionTitle: viewModel.folderModel.cancelActionTitle, + isCancelActionDisabled: viewModel.folderModel.isOtherActionDisabled, + cancelAction: { _ in + viewModel.dismissAction() + }, + defaultActionTitle: viewModel.folderModel.defaultActionTitle, + isDefaultActionDisabled: viewModel.folderModel.isDefaultActionDisabled, + defaultAction: { _ in + viewModel.folderModel.addOrSave { + viewModel.dismissAction() + } + } + ) + .frame(width: 448, height: 210) + } +} + +// MARK: - Previews + +#if DEBUG +#Preview("Add Bookmark - Light Mode") { + let bookmarkManager = LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(bookmarks: [])) + bookmarkManager.loadBookmarks() + + return BookmarksDialogViewFactory.makeAddBookmarkView(parent: nil, bookmarkManager: bookmarkManager) + .preferredColorScheme(.light) +} + +#Preview("Add Bookmark - Dark Mode") { + let bookmarkManager = LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(bookmarks: [])) + bookmarkManager.loadBookmarks() + + return BookmarksDialogViewFactory.makeAddBookmarkView(parent: nil, bookmarkManager: bookmarkManager) + .preferredColorScheme(.dark) +} + +#Preview("Edit Bookmark - Light Mode") { + let parentFolder = BookmarkFolder(id: "7", title: "DuckDuckGo") + let bookmark = Bookmark(id: "1", url: "www.duckduckgo.com", title: "DuckDuckGo", isFavorite: true, parentFolderUUID: "7") + let bookmarkManager = LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(bookmarks: [bookmark, parentFolder])) + bookmarkManager.loadBookmarks() + + return BookmarksDialogViewFactory.makeEditBookmarkView(bookmark: bookmark, bookmarkManager: bookmarkManager) + .preferredColorScheme(.light) +} + +#Preview("Edit Bookmark - Dark Mode") { + let parentFolder = BookmarkFolder(id: "7", title: "DuckDuckGo") + let bookmark = Bookmark(id: "1", url: "www.duckduckgo.com", title: "DuckDuckGo", isFavorite: true, parentFolderUUID: "7") + let bookmarkManager = LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(bookmarks: [bookmark, parentFolder])) + bookmarkManager.loadBookmarks() + + return BookmarksDialogViewFactory.makeEditBookmarkView(bookmark: bookmark, bookmarkManager: bookmarkManager) + .preferredColorScheme(.dark) +} +#endif diff --git a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderDialogView.swift b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderDialogView.swift new file mode 100644 index 0000000000..f859311335 --- /dev/null +++ b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderDialogView.swift @@ -0,0 +1,107 @@ +// +// AddEditBookmarkFolderDialogView.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct AddEditBookmarkFolderDialogView: ModalView { + @ObservedObject private var viewModel: AddEditBookmarkFolderDialogViewModel + + init(viewModel: AddEditBookmarkFolderDialogViewModel) { + self.viewModel = viewModel + } + + var body: some View { + AddEditBookmarkFolderView( + title: viewModel.title, + buttonsState: .compressed, + folders: viewModel.folders, + folderName: $viewModel.folderName, + selectedFolder: $viewModel.selectedFolder, + cancelActionTitle: viewModel.cancelActionTitle, + isCancelActionDisabled: viewModel.isOtherActionDisabled, + cancelAction: viewModel.cancel, + defaultActionTitle: viewModel.defaultActionTitle, + isDefaultActionDisabled: viewModel.isDefaultActionDisabled, + defaultAction: viewModel.addOrSave + ) + .font(.system(size: 13)) + .frame(width: 448, height: 210) + } +} + +// MARK: - Previews +#if DEBUG +#Preview("Add Folder To Bookmarks - Light") { + let bookmarkFolder = BookmarkFolder(id: "1", title: "DuckDuckGo", children: []) + let store = BookmarkStoreMock(bookmarks: [bookmarkFolder]) + let bookmarkManager = LocalBookmarkManager(bookmarkStore: store) + bookmarkManager.loadBookmarks() + + return BookmarksDialogViewFactory.makeAddBookmarkFolderView(parentFolder: nil, bookmarkManager: bookmarkManager) + .preferredColorScheme(.light) +} + +#Preview("Add Folder To Bookmarks Subfolder - Light") { + let bookmarkFolder = BookmarkFolder(id: "1", title: "DuckDuckGo", children: []) + let store = BookmarkStoreMock(bookmarks: [bookmarkFolder]) + let bookmarkManager = LocalBookmarkManager(bookmarkStore: store) + bookmarkManager.loadBookmarks() + + return BookmarksDialogViewFactory.makeAddBookmarkFolderView(parentFolder: bookmarkFolder, bookmarkManager: bookmarkManager) + .preferredColorScheme(.light) +} + +#Preview("Edit Folder - Light") { + let bookmarkFolder = BookmarkFolder(id: "1", title: "DuckDuckGo", children: []) + let store = BookmarkStoreMock(bookmarks: [bookmarkFolder]) + let bookmarkManager = LocalBookmarkManager(bookmarkStore: store) + bookmarkManager.loadBookmarks() + + return BookmarksDialogViewFactory.makeEditBookmarkFolderView(folder: bookmarkFolder, parentFolder: nil, bookmarkManager: bookmarkManager) + .preferredColorScheme(.light) +} + +#Preview("Add Folder To Bookmarks - Dark") { + let store = BookmarkStoreMock(bookmarks: []) + let bookmarkManager = LocalBookmarkManager(bookmarkStore: store) + bookmarkManager.loadBookmarks() + + return BookmarksDialogViewFactory.makeAddBookmarkFolderView(parentFolder: nil, bookmarkManager: bookmarkManager) + .preferredColorScheme(.dark) +} + +#Preview("Add Folder To Bookmarks Subfolder - Dark") { + let bookmarkFolder = BookmarkFolder(id: "1", title: "DuckDuckGo", children: []) + let store = BookmarkStoreMock(bookmarks: [bookmarkFolder]) + let bookmarkManager = LocalBookmarkManager(bookmarkStore: store) + bookmarkManager.loadBookmarks() + + return BookmarksDialogViewFactory.makeAddBookmarkFolderView(parentFolder: bookmarkFolder, bookmarkManager: bookmarkManager) + .preferredColorScheme(.dark) +} + +#Preview("Edit Folder in Subfolder - Dark") { + let bookmarkFolder = BookmarkFolder(id: "1", title: "DuckDuckGo", children: []) + let store = BookmarkStoreMock(bookmarks: [bookmarkFolder]) + let bookmarkManager = LocalBookmarkManager(bookmarkStore: store) + bookmarkManager.loadBookmarks() + + return BookmarksDialogViewFactory.makeEditBookmarkFolderView(folder: bookmarkFolder, parentFolder: bookmarkFolder, bookmarkManager: bookmarkManager) + .preferredColorScheme(.dark) +} +#endif diff --git a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderView.swift b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderView.swift new file mode 100644 index 0000000000..d89cfc7c93 --- /dev/null +++ b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderView.swift @@ -0,0 +1,132 @@ +// +// AddEditBookmarkFolderView.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct AddEditBookmarkFolderView: View { + enum ButtonsState { + case compressed + case expanded + } + + let title: String + let buttonsState: ButtonsState + let folders: [FolderViewModel] + @Binding var folderName: String + @Binding var selectedFolder: BookmarkFolder? + + let cancelActionTitle: String + let isCancelActionDisabled: Bool + let cancelAction: @MainActor (_ dismiss: () -> Void) -> Void + + let defaultActionTitle: String + let isDefaultActionDisabled: Bool + let defaultAction: @MainActor (_ dismiss: () -> Void) -> Void + + var body: some View { + BookmarkDialogContainerView( + title: title, + middleSection: { + BookmarkDialogStackedContentView( + .init( + title: UserText.Bookmarks.Dialog.Field.name, + content: TextField("", text: $folderName) + .focusedOnAppear() + .accessibilityIdentifier("bookmark.add.name.textfield") + .textFieldStyle(RoundedBorderTextFieldStyle()) + .font(.system(size: 14)) + ), + .init( + title: UserText.Bookmarks.Dialog.Field.location, + content: BookmarkFolderPicker( + folders: folders, + selectedFolder: $selectedFolder + ) + .accessibilityIdentifier("bookmark.folder.folder.dropdown") + ) + ) + }, + bottomSection: { + BookmarkDialogButtonsView( + viewState: .init(buttonsState), + otherButtonAction: .init( + title: cancelActionTitle, + keyboardShortCut: .cancelAction, + isDisabled: isCancelActionDisabled, + action: cancelAction + ), defaultButtonAction: .init( + title: defaultActionTitle, + keyboardShortCut: .defaultAction, + isDisabled: isDefaultActionDisabled, + action: defaultAction + ) + ) + } + ) + } +} + +private extension BookmarkDialogButtonsView.ViewState { + + init(_ state: AddEditBookmarkFolderView.ButtonsState) { + switch state { + case .compressed: + self = .compressed + case .expanded: + self = .expanded + } + } +} + +#Preview("Compressed") { + @State var folderName = "" + @State var selectedFolder: BookmarkFolder? + + return AddEditBookmarkFolderView( + title: "Test Title", + buttonsState: .compressed, + folders: [], + folderName: $folderName, + selectedFolder: $selectedFolder, + cancelActionTitle: UserText.cancel, + isCancelActionDisabled: false, + cancelAction: { _ in }, + defaultActionTitle: UserText.save, + isDefaultActionDisabled: false, + defaultAction: { _ in } + ) +} + +#Preview("Expanded") { + @State var folderName = "" + @State var selectedFolder: BookmarkFolder? + + return AddEditBookmarkFolderView( + title: "Test Title", + buttonsState: .expanded, + folders: [], + folderName: $folderName, + selectedFolder: $selectedFolder, + cancelActionTitle: UserText.cancel, + isCancelActionDisabled: false, + cancelAction: { _ in }, + defaultActionTitle: UserText.save, + isDefaultActionDisabled: false, + defaultAction: { _ in } + ) +} diff --git a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkView.swift b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkView.swift new file mode 100644 index 0000000000..8d34889432 --- /dev/null +++ b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkView.swift @@ -0,0 +1,113 @@ +// +// AddEditBookmarkView.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct AddEditBookmarkView: View { + let title: String + let buttonsState: BookmarksDialogButtonsState + + @Binding var bookmarkName: String + var bookmarkURLPath: Binding? + @Binding var isBookmarkFavorite: Bool + + let folders: [FolderViewModel] + @Binding var selectedFolder: BookmarkFolder? + + let isURLFieldHidden: Bool + + let addFolderAction: () -> Void + + let otherActionTitle: String + let isOtherActionDisabled: Bool + let otherAction: @MainActor (_ dismiss: () -> Void) -> Void + + let defaultActionTitle: String + let isDefaultActionDisabled: Bool + let defaultAction: @MainActor (_ dismiss: () -> Void) -> Void + + var body: some View { + BookmarkDialogContainerView( + title: title, + middleSection: { + BookmarkDialogStackedContentView( + .init( + title: UserText.Bookmarks.Dialog.Field.name, + content: TextField("", text: $bookmarkName) + .focusedOnAppear() + .accessibilityIdentifier("bookmark.add.name.textfield") + .textFieldStyle(RoundedBorderTextFieldStyle()) + .font(.system(size: 14)) + ), + .init( + title: UserText.Bookmarks.Dialog.Field.url, + content: TextField("", text: bookmarkURLPath ?? .constant("")) + .accessibilityIdentifier("bookmark.add.url.textfield") + .textFieldStyle(RoundedBorderTextFieldStyle()) + .font(.system(size: 14)), + isContentViewHidden: isURLFieldHidden + ), + .init( + title: UserText.Bookmarks.Dialog.Field.location, + content: BookmarkDialogFolderManagementView( + folders: folders, + selectedFolder: $selectedFolder, + onActionButton: addFolderAction + ) + ) + ) + BookmarkFavoriteView(isFavorite: $isBookmarkFavorite) + }, + bottomSection: { + BookmarkDialogButtonsView( + viewState: .init(buttonsState), + otherButtonAction: .init( + title: otherActionTitle, + isDisabled: isOtherActionDisabled, + action: otherAction + ), + defaultButtonAction: .init( + title: defaultActionTitle, + keyboardShortCut: .defaultAction, + isDisabled: isDefaultActionDisabled, + action: defaultAction + ) + ) + } + ) + } + +} + +// MARK: - BookmarksDialogButtonsState + +enum BookmarksDialogButtonsState { + case compressed + case expanded +} + +extension BookmarkDialogButtonsView.ViewState { + init(_ state: BookmarksDialogButtonsState) { + switch state { + case .compressed: + self = .compressed + case .expanded: + self = .expanded + } + } +} diff --git a/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogButtonsView.swift b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogButtonsView.swift new file mode 100644 index 0000000000..7726516704 --- /dev/null +++ b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogButtonsView.swift @@ -0,0 +1,186 @@ +// +// BookmarkDialogButtonsView.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct BookmarkDialogButtonsView: View { + private let viewState: ViewState + private let otherButtonAction: Action + private let defaultButtonAction: Action + @Environment(\.dismiss) private var dismiss + + init( + viewState: ViewState, + otherButtonAction: Action, + defaultButtonAction: Action + ) { + self.viewState = viewState + self.otherButtonAction = otherButtonAction + self.defaultButtonAction = defaultButtonAction + } + + var body: some View { + HStack { + if viewState == .compressed { + Spacer() + } + + actionButton(action: otherButtonAction, viewState: viewState) + + actionButton(action: defaultButtonAction, viewState: viewState) + } + } + + @MainActor + private func actionButton(action: Action, viewState: ViewState) -> some View { + Button { + action.action(dismiss.callAsFunction) + } label: { + Text(action.title) + .frame(height: viewState.height) + .frame(maxWidth: viewState.maxWidth) + } + .keyboardShortcut(action.keyboardShortCut) + .disabled(action.isDisabled) + .ifLet(action.accessibilityIdentifier) { view, value in + view.accessibilityIdentifier(value) + } + } +} + +// MARK: - BookmarkDialogButtonsView + Types + +extension BookmarkDialogButtonsView { + + enum ViewState: Equatable { + case compressed + case expanded + } + + struct Action { + let title: String + let keyboardShortCut: KeyboardShortcut? + let accessibilityIdentifier: String? + let isDisabled: Bool + let action: @MainActor (_ dismiss: () -> Void) -> Void + + init( + title: String, + accessibilityIdentifier: String? = nil, + keyboardShortCut: KeyboardShortcut? = nil, + isDisabled: Bool = false, + action: @MainActor @escaping (_ dismiss: () -> Void) -> Void + ) { + self.title = title + self.keyboardShortCut = keyboardShortCut + self.accessibilityIdentifier = accessibilityIdentifier + self.isDisabled = isDisabled + self.action = action + } + } +} + +// MARK: - BookmarkDialogButtonsView.ViewState + +private extension BookmarkDialogButtonsView.ViewState { + + var maxWidth: CGFloat? { + switch self { + case .compressed: + return nil + case .expanded: + return .infinity + } + } + + var height: CGFloat? { + switch self { + case .compressed: + return nil + case .expanded: + return 28.0 + } + } + +} + +// MARK: - Preview + +#Preview("Compressed - Disable Default Button") { + BookmarkDialogButtonsView( + viewState: .compressed, + otherButtonAction: .init( + title: "Left", + action: { _ in } + ), + defaultButtonAction: .init( + title: "Right", + isDisabled: true, + action: {_ in } + ) + ) + .frame(width: 320, height: 50) +} + +#Preview("Compressed - Enabled Default Button") { + BookmarkDialogButtonsView( + viewState: .compressed, + otherButtonAction: .init( + title: "Left", + action: { _ in } + ), + defaultButtonAction: .init( + title: "Right", + isDisabled: false, + action: {_ in } + ) + ) + .frame(width: 320, height: 50) +} + +#Preview("Expanded - Disable Default Button") { + BookmarkDialogButtonsView( + viewState: .expanded, + otherButtonAction: .init( + title: "Left", + action: { _ in } + ), + defaultButtonAction: .init( + title: "Right", + isDisabled: true, + action: {_ in } + ) + ) + .frame(width: 320, height: 50) +} + +#Preview("Expanded - Enable Default Button") { + BookmarkDialogButtonsView( + viewState: .expanded, + otherButtonAction: .init( + title: "Left", + action: { _ in } + ), + defaultButtonAction: .init( + title: "Right", + isDisabled: false, + action: {_ in } + ) + ) + .frame(width: 320, height: 50) +} diff --git a/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogContainerView.swift b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogContainerView.swift new file mode 100644 index 0000000000..ea49712abb --- /dev/null +++ b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogContainerView.swift @@ -0,0 +1,50 @@ +// +// BookmarkDialogContainerView.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import SwiftUIExtensions + +struct BookmarkDialogContainerView: View { + private let title: String + @ViewBuilder private let middleSection: () -> Content + @ViewBuilder private let bottomSection: () -> Buttons + + init( + title: String, + @ViewBuilder middleSection: @escaping () -> Content, + @ViewBuilder bottomSection: @escaping () -> Buttons + ) { + self.title = title + self.middleSection = middleSection + self.bottomSection = bottomSection + } + + var body: some View { + TieredDialogView( + verticalSpacing: 16.0, + horizontalPadding: 20.0, + top: { + Text(title) + .foregroundColor(.primary) + .fontWeight(.semibold) + }, + center: middleSection, + bottom: bottomSection + ) + } +} diff --git a/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogFolderManagementView.swift b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogFolderManagementView.swift new file mode 100644 index 0000000000..8081abc14d --- /dev/null +++ b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogFolderManagementView.swift @@ -0,0 +1,76 @@ +// +// BookmarkDialogFolderManagementView.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import SwiftUIExtensions + +struct BookmarkDialogFolderManagementView: View { + private let folders: [FolderViewModel] + private var selectedFolder: Binding + private let onActionButton: @MainActor () -> Void + + init( + folders: [FolderViewModel], + selectedFolder: Binding, + onActionButton: @escaping @MainActor () -> Void + ) { + self.folders = folders + self.selectedFolder = selectedFolder + self.onActionButton = onActionButton + } + + var body: some View { + HStack { + BookmarkFolderPicker( + folders: folders, + selectedFolder: selectedFolder + ) + .accessibilityIdentifier("bookmark.add.folder.dropdown") + + Button { + onActionButton() + } label: { + Image(.addFolder) + } + .accessibilityIdentifier("bookmark.add.new.folder.button") + .buttonStyle(StandardButtonStyle()) + } + } +} + +#Preview { + @State var selectedFolder: BookmarkFolder? = BookmarkFolder(id: "1", title: "Nested Folder", children: []) + let folderViewModels: [FolderViewModel] = [ + .init( + entity: .init( + id: "1", + title: "Nested Folder", + parentFolderUUID: nil, + children: [] + ), + level: 1 + ) + ] + + return BookmarkDialogFolderManagementView( + folders: folderViewModels, + selectedFolder: $selectedFolder, + onActionButton: {} + ) + .frame(width: 400) +} diff --git a/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogStackedContentView.swift b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogStackedContentView.swift new file mode 100644 index 0000000000..864b0cdb13 --- /dev/null +++ b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogStackedContentView.swift @@ -0,0 +1,111 @@ +// +// BookmarkDialogStackedContentView.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import SwiftUIExtensions + +struct BookmarkDialogStackedContentView: View { + private let items: [Item] + + init(_ items: Item...) { + self.items = items + } + + init(_ items: [Item]) { + self.items = items + } + + var body: some View { + TwoColumnsListView( + horizontalSpacing: 16.0, + verticalSpacing: 20.0, + rowHeight: 22.0, + leftColumn: { + ForEach(items, id: \.title) { item in + if !item.isContentViewHidden { + Text(item.title) + .foregroundColor(.primary) + .fontWeight(.medium) + } + } + }, + rightColumn: { + ForEach(items, id: \.title) { item in + if !item.isContentViewHidden { + item.content + } + } + } + ) + } +} + +// MARK: - BookmarkModalStackedContentView + Item + +extension BookmarkDialogStackedContentView { + struct Item { + fileprivate let title: String + fileprivate let content: AnyView + fileprivate let isContentViewHidden: Bool + + init(title: String, content: any View, isContentViewHidden: Bool = false) { + self.title = title + self.content = AnyView(content) + self.isContentViewHidden = isContentViewHidden + } + } +} + +// MARK: - Preview + +#Preview { + @State var name: String = "DuckDuckGo" + @State var url: String = "https://www.duckduckgo.com" + @State var selectedFolder: BookmarkFolder? + + return BookmarkDialogStackedContentView( + .init( + title: "Name", + content: + TextField("", text: $name) + .textFieldStyle(.roundedBorder) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .font(.system(size: 14)) + + ), + .init( + title: "URL", + content: + TextField("", text: $url) + .textFieldStyle(.roundedBorder) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .font(.system(size: 14)) + ), + .init( + title: "Location", + content: + BookmarkDialogFolderManagementView( + folders: [], + selectedFolder: $selectedFolder, + onActionButton: { } + ) + ) + ) + .padding([.horizontal, .vertical]) + .frame(width: 400) +} diff --git a/DuckDuckGo/Bookmarks/View/Dialog/BookmarkFavoriteView.swift b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkFavoriteView.swift new file mode 100644 index 0000000000..9778ab0d5c --- /dev/null +++ b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkFavoriteView.swift @@ -0,0 +1,47 @@ +// +// BookmarkFavoriteView.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import SwiftUIExtensions +import PreferencesViews + +struct BookmarkFavoriteView: View { + @Binding var isFavorite: Bool + + var body: some View { + Toggle(isOn: $isFavorite) { + HStack(spacing: 6) { + Image(.favoriteFilledBorder) + Text(UserText.addToFavorites) + .foregroundColor(.primary) + } + } + .toggleStyle(.checkbox) + .accessibilityIdentifier("bookmark.add.add.to.favorites.button") + } +} + +#Preview("Favorite") { + BookmarkFavoriteView(isFavorite: .constant(true)) + .frame(width: 300) +} + +#Preview("Not Favorite") { + BookmarkFavoriteView(isFavorite: .constant(false)) + .frame(width: 300) +} diff --git a/DuckDuckGo/Bookmarks/View/Dialog/BookmarksDialogViewFactory.swift b/DuckDuckGo/Bookmarks/View/Dialog/BookmarksDialogViewFactory.swift new file mode 100644 index 0000000000..b29b50bbbb --- /dev/null +++ b/DuckDuckGo/Bookmarks/View/Dialog/BookmarksDialogViewFactory.swift @@ -0,0 +1,93 @@ +// +// BookmarksDialogViewFactory.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +@MainActor +enum BookmarksDialogViewFactory { + + /// Creates an instance of AddEditBookmarkFolderDialogView for adding a Bookmark Folder. + /// - Parameters: + /// - parentFolder: An optional `BookmarkFolder`. When adding a folder to the root bookmark folder pass `nil`. For any other folder pass the `BookmarkFolder` the new folder should be within. + /// - bookmarkManager: An instance of `BookmarkManager`. This should be used for `#previews` only. + /// - Returns: An instance of AddEditBookmarkFolderDialogView. + static func makeAddBookmarkFolderView(parentFolder: BookmarkFolder?, bookmarkManager: LocalBookmarkManager = .shared) -> AddEditBookmarkFolderDialogView { + let viewModel = AddEditBookmarkFolderDialogViewModel(mode: .add(parentFolder: parentFolder), bookmarkManager: bookmarkManager) + return AddEditBookmarkFolderDialogView(viewModel: viewModel) + } + + /// Creates an instance of AddEditBookmarkFolderDialogView for editing a Bookmark Folder. + /// - Parameters: + /// - folder: The `BookmarkFolder` to edit. + /// - parentFolder: An optional `BookmarkFolder`. When editing a folder within the root bookmark folder pass `nil`. For any other folder pass the `BookmarkFolder` the folder belongs to. + /// - bookmarkManager: An instance of `BookmarkManager`. This should be used for `#previews` only. + /// - Returns: An instance of AddEditBookmarkFolderDialogView. + static func makeEditBookmarkFolderView(folder: BookmarkFolder, parentFolder: BookmarkFolder?, bookmarkManager: LocalBookmarkManager = .shared) -> AddEditBookmarkFolderDialogView { + let viewModel = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: folder, parentFolder: parentFolder), bookmarkManager: bookmarkManager) + return AddEditBookmarkFolderDialogView(viewModel: viewModel) + } + + /// Creates an instance of AddEditBookmarkDialogView for adding a Bookmark with the specified web page. + /// - Parameters: + /// - currentTab: An optional `WebsiteInfo`. When adding a bookmark from the bookmark shortcut panel, if the `Tab` has loaded a web page pass the information via the `currentTab`. If the `Tab` has not loaded a tab pass `nil`. If adding a `Bookmark` from the `Manage Bookmark` settings page, pass `nil`. + /// - bookmarkManager: An instance of `BookmarkManager`. This should be used for `#previews` only. + /// - Returns: An instance of AddEditBookmarkDialogView. + static func makeAddBookmarkView(currentTab: WebsiteInfo?, bookmarkManager: LocalBookmarkManager = .shared) -> AddEditBookmarkDialogView { + let viewModel = AddEditBookmarkDialogViewModel(mode: .add(tabWebsite: currentTab), bookmarkManager: bookmarkManager) + return makeAddEditBookmarkDialogView(viewModel: viewModel, bookmarkManager: bookmarkManager) + } + + /// Creates an instance of AddEditBookmarkDialogView for adding a Bookmark with the specified parent folder. + /// - Parameters: + /// - parentFolder: An optional `BookmarkFolder`. When adding a bookmark from the bookmark management view, if the user select a parent folder pass this value won't be `nil`. Otherwise, if no folder is selected this value will be `nil`. + /// - bookmarkManager: An instance of `BookmarkManager`. This should be used for `#previews` only. + /// - Returns: An instance of AddEditBookmarkDialogView. + static func makeAddBookmarkView(parent: BookmarkFolder?, bookmarkManager: LocalBookmarkManager = .shared) -> AddEditBookmarkDialogView { + let viewModel = AddEditBookmarkDialogViewModel(mode: .add(parentFolder: parent), bookmarkManager: bookmarkManager) + return makeAddEditBookmarkDialogView(viewModel: viewModel, bookmarkManager: bookmarkManager) + } + + /// Creates an instance of AddEditBookmarkDialogView for adding a Bookmark from the Favorites view in the empty Tab. + /// - Parameter bookmarkManager: An instance of `BookmarkManager`. This should be used for `#previews` only. + /// - Returns: An instance of AddEditBookmarkDialogView, + static func makeAddFavoriteView(bookmarkManager: LocalBookmarkManager = .shared) -> AddEditBookmarkDialogView { + let viewModel = AddEditBookmarkDialogViewModel(mode: .add(shouldPresetFavorite: true), bookmarkManager: bookmarkManager) + return makeAddEditBookmarkDialogView(viewModel: viewModel, bookmarkManager: bookmarkManager) + } + + /// Creates an instance of AddEditBookmarkDialogView for editing a Bookmark. + /// - Parameters: + /// - bookmark: The `Bookmark` to edit. + /// - bookmarkManager: An instance of `BookmarkManager`. This should be used for `#previews` only. + /// - Returns: An instance of AddEditBookmarkDialogView. + static func makeEditBookmarkView(bookmark: Bookmark, bookmarkManager: LocalBookmarkManager = .shared) -> AddEditBookmarkDialogView { + let viewModel = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + return makeAddEditBookmarkDialogView(viewModel: viewModel, bookmarkManager: bookmarkManager) + } + +} + +private extension BookmarksDialogViewFactory { + + private static func makeAddEditBookmarkDialogView(viewModel: AddEditBookmarkDialogViewModel, bookmarkManager: BookmarkManager) -> AddEditBookmarkDialogView { + let addFolderViewModel = AddEditBookmarkFolderDialogViewModel(mode: .add(parentFolder: nil), bookmarkManager: bookmarkManager) + let viewModel = AddEditBookmarkDialogCoordinatorViewModel(bookmarkModel: viewModel, folderModel: addFolderViewModel) + return AddEditBookmarkDialogView(viewModel: viewModel) + } + +} diff --git a/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkFolderModalViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkFolderModalViewModel.swift deleted file mode 100644 index ac2701e3ca..0000000000 --- a/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkFolderModalViewModel.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// AddBookmarkFolderModalViewModel.swift -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -struct AddBookmarkFolderModalViewModel { - - let bookmarkManager: BookmarkManager - - let title: String - let addButtonTitle: String - let originalFolder: BookmarkFolder? - let parent: BookmarkFolder? - - var folderName: String = "" - - var isAddButtonDisabled: Bool { - folderName.trimmingWhitespace().isEmpty - } - - init(folder: BookmarkFolder, - bookmarkManager: BookmarkManager = LocalBookmarkManager.shared, - completionHandler: @escaping (BookmarkFolder?) -> Void = { _ in }) { - self.bookmarkManager = bookmarkManager - self.folderName = folder.title - self.originalFolder = folder - self.parent = nil - self.title = UserText.renameFolder - self.addButtonTitle = UserText.save - } - - init(parent: BookmarkFolder? = nil, - bookmarkManager: BookmarkManager = LocalBookmarkManager.shared, - completionHandler: @escaping (BookmarkFolder?) -> Void = { _ in }) { - self.bookmarkManager = bookmarkManager - self.originalFolder = nil - self.parent = parent - self.title = UserText.newFolder - self.addButtonTitle = UserText.newFolderDialogAdd - } - - func cancel(dismiss: () -> Void) { - dismiss() - } - - func addFolder(dismiss: () -> Void) { - guard !folderName.isEmpty else { - assertionFailure("folderName is empty, button should be disabled") - return - } - - let folderName = folderName.trimmingWhitespace() - if let folder = originalFolder { - folder.title = folderName - bookmarkManager.update(folder: folder) - - } else { - bookmarkManager.makeFolder(for: folderName, parent: parent, completion: { _ in }) - } - - dismiss() - } - -} diff --git a/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkFolderPopoverViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkFolderPopoverViewModel.swift index c16625a362..a1359fa61b 100644 --- a/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkFolderPopoverViewModel.swift +++ b/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkFolderPopoverViewModel.swift @@ -22,15 +22,30 @@ import Foundation final class AddBookmarkFolderPopoverViewModel: ObservableObject { private let bookmarkManager: BookmarkManager - let folders: [FolderViewModel] - @Published var parent: BookmarkFolder? + private let completionHandler: (BookmarkFolder?) -> Void + @Published var parent: BookmarkFolder? @Published var folderName: String = "" - @Published private(set) var isDisabled = false - private let completionHandler: (BookmarkFolder?) -> Void + var title: String { + UserText.Bookmarks.Dialog.Title.addFolder + } + + var cancelActionTitle: String { + UserText.cancel + } + + var defaultActionTitle: String { + UserText.Bookmarks.Dialog.Action.addFolder + } + + let isCancelActionDisabled = false + + var isDefaultActionButtonDisabled: Bool { + folderName.trimmingWhitespace().isEmpty || isDisabled + } init(bookmark: Bookmark? = nil, folderName: String = "", diff --git a/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkModalViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkModalViewModel.swift deleted file mode 100644 index 15554afcbc..0000000000 --- a/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkModalViewModel.swift +++ /dev/null @@ -1,127 +0,0 @@ -// -// AddBookmarkModalViewModel.swift -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -struct AddBookmarkModalViewModel { - - let bookmarkManager: BookmarkManager - - let title: String - let addButtonTitle: String - var isFavorite: Bool - - private let originalBookmark: Bookmark? - private let parent: BookmarkFolder? - - private let completionHandler: (Bookmark?) -> Void - - var bookmarkTitle: String = "" - var bookmarkAddress: String = "" - - private var hasValidInput: Bool { - guard let url = bookmarkAddress.url else { return false } - - return !bookmarkTitle.trimmingWhitespace().isEmpty && url.isValid - } - - var isAddButtonDisabled: Bool { !hasValidInput } - - func cancel(dismiss: () -> Void) { - completionHandler(nil) - dismiss() - } - - func addOrSave(dismiss: () -> Void) { - guard let url = bookmarkAddress.url else { - assertionFailure("invalid URL, button should be disabled") - return - } - - var result: Bookmark? - let bookmarkTitle = bookmarkTitle.trimmingWhitespace() - if var bookmark = originalBookmark ?? bookmarkManager.getBookmark(for: url) { - - if url.absoluteString != bookmark.url { - bookmark = bookmarkManager.updateUrl(of: bookmark, to: url) ?? bookmark - } - if bookmark.title != bookmarkTitle || bookmark.isFavorite != isFavorite { - bookmark.title = bookmarkTitle - bookmark.isFavorite = isFavorite - bookmarkManager.update(bookmark: bookmark) - } - - result = bookmark - - } else if !bookmarkManager.isUrlBookmarked(url: url) { - result = bookmarkManager.makeBookmark(for: url, title: bookmarkTitle, isFavorite: isFavorite, index: nil, parent: parent) - } - - completionHandler(result) - dismiss() - } - - init(isFavorite: Bool = false, - bookmarkManager: BookmarkManager = LocalBookmarkManager.shared, - currentTabWebsite website: WebsiteInfo? = nil, - parent: BookmarkFolder? = nil, - completionHandler: @escaping (Bookmark?) -> Void = { _ in }) { - - self.bookmarkManager = bookmarkManager - - self.isFavorite = isFavorite - self.title = isFavorite ? UserText.addFavorite : UserText.newBookmark - self.addButtonTitle = UserText.bookmarkDialogAdd - - if let website, - !LocalBookmarkManager.shared.isUrlBookmarked(url: website.url) { - bookmarkTitle = website.title ?? "" - bookmarkAddress = website.url.absoluteString - } - self.parent = parent - self.originalBookmark = nil - - self.completionHandler = completionHandler - } - - init(bookmarkManager: BookmarkManager = LocalBookmarkManager.shared, - originalBookmark: Bookmark?, - isFavorite: Bool = false, - completionHandler: @escaping (Bookmark?) -> Void = { _ in }) { - - self.bookmarkManager = bookmarkManager - - self.isFavorite = isFavorite - if originalBookmark != nil { - self.title = isFavorite ? UserText.editFavorite : UserText.updateBookmark - self.addButtonTitle = UserText.save - } else { - self.title = isFavorite ? UserText.addFavorite : UserText.newBookmark - self.addButtonTitle = UserText.bookmarkDialogAdd - } - - self.parent = nil - self.originalBookmark = originalBookmark - - bookmarkTitle = originalBookmark?.title ?? "" - bookmarkAddress = originalBookmark?.url ?? "" - - self.completionHandler = completionHandler - } - -} diff --git a/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkPopoverViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkPopoverViewModel.swift index b474e148b3..88168901ed 100644 --- a/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkPopoverViewModel.swift +++ b/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkPopoverViewModel.swift @@ -38,8 +38,24 @@ final class AddBookmarkPopoverViewModel: ObservableObject { } } + @Published var isBookmarkFavorite: Bool { + didSet { + bookmark.isFavorite = isBookmarkFavorite + bookmarkManager.update(bookmark: bookmark) + } + } + + @Published var bookmarkTitle: String { + didSet { + bookmark.title = bookmarkTitle.trimmingWhitespace() + bookmarkManager.update(bookmark: bookmark) + } + } + @Published var addFolderViewModel: AddBookmarkFolderPopoverViewModel? + let isDefaultActionButtonDisabled: Bool = false + private var bookmarkListCancellable: AnyCancellable? init(bookmark: Bookmark, @@ -47,6 +63,7 @@ final class AddBookmarkPopoverViewModel: ObservableObject { self.bookmarkManager = bookmarkManager self.bookmark = bookmark self.bookmarkTitle = bookmark.title + self.isBookmarkFavorite = bookmark.isFavorite bookmarkListCancellable = bookmarkManager.listPublisher .receive(on: DispatchQueue.main) @@ -74,12 +91,6 @@ final class AddBookmarkPopoverViewModel: ObservableObject { dismiss() } - func favoritesButtonAction() { - bookmark.isFavorite.toggle() - - bookmarkManager.update(bookmark: bookmark) - } - func addFolderButtonAction() { addFolderViewModel = .init(bookmark: bookmark, bookmarkManager: bookmarkManager) { [bookmark, bookmarkManager, weak self] newFolder in if let newFolder { @@ -98,14 +109,6 @@ final class AddBookmarkPopoverViewModel: ObservableObject { addFolderViewModel = nil } - @Published var bookmarkTitle: String { - didSet { - bookmark.title = bookmarkTitle.trimmingWhitespace() - - bookmarkManager.update(bookmark: bookmark) - } - } - } struct FolderViewModel: Identifiable, Equatable { diff --git a/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogCoordinatorViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogCoordinatorViewModel.swift new file mode 100644 index 0000000000..27fc0c64e5 --- /dev/null +++ b/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogCoordinatorViewModel.swift @@ -0,0 +1,74 @@ +// +// AddEditBookmarkDialogCoordinatorViewModel.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import Combine + +final class AddEditBookmarkDialogCoordinatorViewModel: ObservableObject { + @ObservedObject var bookmarkModel: BookmarkViewModel + @ObservedObject var folderModel: AddFolderViewModel + @Published var viewState: ViewState + + private var cancellables: Set = [] + + init(bookmarkModel: BookmarkViewModel, folderModel: AddFolderViewModel) { + self.bookmarkModel = bookmarkModel + self.folderModel = folderModel + viewState = .bookmark + bind() + } + + func dismissAction() { + viewState = .bookmark + } + + func addFolderAction() { + folderModel.selectedFolder = bookmarkModel.selectedFolder + viewState = .folder + } + + private func bind() { + bookmarkModel.objectWillChange + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.objectWillChange.send() + } + .store(in: &cancellables) + + folderModel.objectWillChange + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.objectWillChange.send() + } + .store(in: &cancellables) + + folderModel.addFolderPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] bookmarkFolder in + self?.bookmarkModel.selectedFolder = bookmarkFolder + } + .store(in: &cancellables) + } +} + +extension AddEditBookmarkDialogCoordinatorViewModel { + enum ViewState { + case bookmark + case folder + } +} diff --git a/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogViewModel.swift new file mode 100644 index 0000000000..0aa03eade1 --- /dev/null +++ b/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogViewModel.swift @@ -0,0 +1,216 @@ +// +// AddEditBookmarkDialogViewModel.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Combine + +@MainActor +protocol BookmarkDialogEditing: BookmarksDialogViewModel { + var bookmarkName: String { get set } + var bookmarkURLPath: String { get set } + var isBookmarkFavorite: Bool { get set } + + var isURLFieldHidden: Bool { get } +} + +@MainActor +final class AddEditBookmarkDialogViewModel: BookmarkDialogEditing { + + /// The type of operation to perform on a bookmark. + enum Mode { + /// Add a new bookmark. Bookmarks can have a parent folder but not necessarily. + /// If the users add a bookmark to the root `Bookmarks` folder, then the parent folder is `nil`. + /// If the users add a bookmark to a different folder then the parent folder is not `nil`. + /// If the users add a bookmark from the bookmark shortcut and `Tab` has a page loaded, then the `tabWebsite` is not `nil`. + /// When adding a bookmark from favorite screen the `shouldPresetFavorite` flag should be set to `true`. + case add(tabWebsite: WebsiteInfo? = nil, parentFolder: BookmarkFolder? = nil, shouldPresetFavorite: Bool = false) + /// Edit an existing bookmark. + case edit(bookmark: Bookmark) + } + + @Published var bookmarkName: String + @Published var bookmarkURLPath: String + @Published var isBookmarkFavorite: Bool + + @Published private(set) var folders: [FolderViewModel] + @Published var selectedFolder: BookmarkFolder? + + private var folderCancellable: AnyCancellable? + + var title: String { + mode.title + } + + let isURLFieldHidden: Bool = false + + var cancelActionTitle: String { + mode.cancelActionTitle + } + + var defaultActionTitle: String { + mode.defaultActionTitle + } + + private var hasValidInput: Bool { + guard let url = bookmarkURLPath.url else { return false } + return !bookmarkName.trimmingWhitespace().isEmpty && url.isValid + } + + let isOtherActionDisabled: Bool = false + + var isDefaultActionDisabled: Bool { !hasValidInput } + + private let mode: Mode + private let bookmarkManager: BookmarkManager + + init(mode: Mode, bookmarkManager: LocalBookmarkManager = .shared) { + let isFavorite = mode.bookmarkURL.flatMap(bookmarkManager.isUrlFavorited) ?? false + self.mode = mode + self.bookmarkManager = bookmarkManager + folders = .init(bookmarkManager.list) + switch mode { + case let .add(websiteInfo, parentFolder, shouldPresetFavorite): + // When adding a new bookmark with website info we need to show the bookmark name and URL only if the bookmark is not bookmarked already. + // Scenario we click on the "Add Bookmark" button from Bookmarks shortcut Panel. If Tab has a Bookmark loaded we present the dialog with prepopulated name and URL from the tab. + // If we save and click again on the "Add Bookmark" button we don't want to try re-add the same bookmark. Hence we present a dialog that is not pre-populated. + let isAlreadyBookmarked = websiteInfo.flatMap { bookmarkManager.isUrlBookmarked(url: $0.url) } ?? false + let websiteName = isAlreadyBookmarked ? "" : websiteInfo?.title ?? "" + let websiteURLPath = isAlreadyBookmarked ? "" : websiteInfo?.url.absoluteString ?? "" + bookmarkName = websiteName + bookmarkURLPath = websiteURLPath + isBookmarkFavorite = shouldPresetFavorite ? true : isFavorite + selectedFolder = parentFolder + case let .edit(bookmark): + bookmarkName = bookmark.title + bookmarkURLPath = bookmark.urlObject?.absoluteString ?? "" + isBookmarkFavorite = isFavorite + selectedFolder = folders.first(where: { $0.id == bookmark.parentFolderUUID })?.entity + } + + bind() + } + + func cancel(dismiss: () -> Void) { + dismiss() + } + + func addOrSave(dismiss: () -> Void) { + guard let url = bookmarkURLPath.url else { + assertionFailure("Invalid URL, default action button should be disabled.") + return + } + + let trimmedBookmarkName = bookmarkName.trimmingWhitespace() + + switch mode { + case .add: + addBookmark(withURL: url, name: trimmedBookmarkName, isFavorite: isBookmarkFavorite, to: selectedFolder) + case let .edit(bookmark): + updateBookmark(bookmark, url: url, name: trimmedBookmarkName, isFavorite: isBookmarkFavorite, location: selectedFolder) + } + dismiss() + } +} + +private extension AddEditBookmarkDialogViewModel { + + func bind() { + folderCancellable = bookmarkManager.listPublisher + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] bookmarkList in + self?.folders = .init(bookmarkList) + }) + } + + func updateBookmark(_ bookmark: Bookmark, url: URL, name: String, isFavorite: Bool, location: BookmarkFolder?) { + // If the URL or Title or Favorite is changed update bookmark + if bookmark.url != url.absoluteString || bookmark.title != name || bookmark.isFavorite != isBookmarkFavorite { + bookmarkManager.update(bookmark: bookmark, withURL: url, title: name, isFavorite: isFavorite) + } + + // If the bookmark changed parent location, move it. + if shouldMove(bookmark: bookmark) { + let parentFolder: ParentFolderType = selectedFolder.flatMap { .parent(uuid: $0.id) } ?? .root + bookmarkManager.move(objectUUIDs: [bookmark.id], toIndex: nil, withinParentFolder: parentFolder, completion: { _ in }) + } + } + + func addBookmark(withURL url: URL, name: String, isFavorite: Bool, to parent: BookmarkFolder?) { + // If a bookmark already exist with the new URL, update it + if let existingBookmark = bookmarkManager.getBookmark(for: url) { + updateBookmark(existingBookmark, url: url, name: name, isFavorite: isFavorite, location: parent) + } else { + bookmarkManager.makeBookmark(for: url, title: name, isFavorite: isFavorite, index: nil, parent: parent) + } + } + + func shouldMove(bookmark: Bookmark) -> Bool { + // There's a discrepancy in representing the root folder. A bookmark belonging to the root folder has `parentFolderUUID` equal to `bookmarks_root`. + // There's no `BookmarkFolder` to represent the root folder, so the root folder is represented by a nil selectedFolder. + // Move Bookmarks if its parent folder is != from the selected folder but ONLY if: + // - The selected folder is not nil. This ensure we're comparing a subfolder with any bookmark parent folder. + // - The selected folder is nil and the bookmark parent folder is not the root folder. This ensure we're not unnecessarily moving the items within the same root folder. + bookmark.parentFolderUUID != selectedFolder?.id && (selectedFolder != nil || selectedFolder == nil && !bookmark.isParentFolderRoot) + } +} + +private extension AddEditBookmarkDialogViewModel.Mode { + + var title: String { + switch self { + case .add: + return UserText.Bookmarks.Dialog.Title.addBookmark + case .edit: + return UserText.Bookmarks.Dialog.Title.editBookmark + } + } + + var cancelActionTitle: String { + switch self { + case .add, .edit: + return UserText.cancel + } + } + + var defaultActionTitle: String { + switch self { + case .add: + return UserText.Bookmarks.Dialog.Action.addBookmark + case .edit: + return UserText.save + } + } + + var bookmarkURL: URL? { + switch self { + case let .add(tabInfo, _, _): + return tabInfo?.url + case let .edit(bookmark): + return bookmark.urlObject + } + } + +} + +private extension Bookmark { + + var isParentFolderRoot: Bool { + parentFolderUUID == "bookmarks_root" + } + +} diff --git a/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkFolderDialogViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkFolderDialogViewModel.swift new file mode 100644 index 0000000000..62c1e0356c --- /dev/null +++ b/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkFolderDialogViewModel.swift @@ -0,0 +1,181 @@ +// +// AddEditBookmarkFolderDialogViewModel.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Combine + +@MainActor +protocol BookmarkFolderDialogEditing: BookmarksDialogViewModel { + var addFolderPublisher: AnyPublisher { get } + var folderName: String { get set } +} + +@MainActor +final class AddEditBookmarkFolderDialogViewModel: BookmarkFolderDialogEditing { + + /// The type of operation to perform on a folder + enum Mode { + /// Add a new folder. Folders can have a parent folder but not necessarily. + /// If the users add a folder to a folder whose parent is the root `Bookmarks` folder, then the parent folder is `nil`. + /// If the users add a folder to a folder whose parent is not the root `Bookmarks` folder, then the parent folder is not `nil`. + case add(parentFolder: BookmarkFolder? = nil) + /// Edit an existing folder. Existing folder can have a parent folder but not necessarily. + /// If the users edit a folder whose parent is the root `Bookmarks` folder, then the parent folder is `nil` + /// If the users edit a folder whose parent is not the root `Bookmarks` folder, then the parent folder is not `nil`. + case edit(folder: BookmarkFolder, parentFolder: BookmarkFolder?) + } + + @Published var folderName: String + @Published var selectedFolder: BookmarkFolder? + + let folders: [FolderViewModel] + + var title: String { + mode.title + } + + var cancelActionTitle: String { + mode.cancelActionTitle + } + + var defaultActionTitle: String { + mode.defaultActionTitle + } + + let isOtherActionDisabled = false + + var isDefaultActionDisabled: Bool { + folderName.trimmingWhitespace().isEmpty + } + + var addFolderPublisher: AnyPublisher { + addFolderSubject.eraseToAnyPublisher() + } + + private let mode: Mode + private let bookmarkManager: BookmarkManager + private let addFolderSubject: PassthroughSubject = .init() + + init(mode: Mode, bookmarkManager: BookmarkManager = LocalBookmarkManager.shared) { + self.mode = mode + self.bookmarkManager = bookmarkManager + folderName = mode.folderName + folders = .init(bookmarkManager.list) + selectedFolder = mode.parentFolder + } + + func cancel(dismiss: () -> Void) { + dismiss() + } + + func addOrSave(dismiss: () -> Void) { + defer { dismiss() } + + guard !folderName.isEmpty else { + assertionFailure("folderName is empty, button should be disabled") + return + } + + let folderName = folderName.trimmingWhitespace() + + switch mode { + case let .edit(folder, originalParent): + // If there are no pending changes dismiss + guard folder.title != folderName || selectedFolder?.id != originalParent?.id else { return } + // Otherwise update Folder. + update(folder: folder, originalParent: originalParent, newParent: selectedFolder) + case .add: + add(folderWithName: folderName, to: selectedFolder) + } + } + +} + +// MARK: - Private + +private extension AddEditBookmarkFolderDialogViewModel { + + func update(folder: BookmarkFolder, originalParent: BookmarkFolder?, newParent: BookmarkFolder?) { + // If the original location of the folder changed move it to the new folder. + if selectedFolder?.id != originalParent?.id { + // Update the title anyway. + folder.title = folderName + let parentFolderType: ParentFolderType = newParent.flatMap { ParentFolderType.parent(uuid: $0.id) } ?? .root + bookmarkManager.update(folder: folder, andMoveToParent: parentFolderType) + } else if folder.title != folderName { // If only title changed just update the folder title without updating its parent. + folder.title = folderName + bookmarkManager.update(folder: folder) + } + } + + func add(folderWithName name: String, to parent: BookmarkFolder?) { + bookmarkManager.makeFolder(for: name, parent: parent) { [weak self] bookmarkFolder in + self?.addFolderSubject.send(bookmarkFolder) + } + } + +} + +// MARK: - AddEditBookmarkFolderDialogViewModel.Mode + +private extension AddEditBookmarkFolderDialogViewModel.Mode { + + var title: String { + switch self { + case .add: + return UserText.Bookmarks.Dialog.Title.addFolder + case .edit: + return UserText.Bookmarks.Dialog.Title.editFolder + } + } + + var cancelActionTitle: String { + switch self { + case .add, .edit: + return UserText.cancel + } + } + + var defaultActionTitle: String { + switch self { + case .add: + return UserText.Bookmarks.Dialog.Action.addFolder + case .edit: + return UserText.save + } + } + + var folderName: String { + switch self { + case .add: + return "" + case let .edit(folder, _): + return folder.title + } + } + + var parentFolder: BookmarkFolder? { + switch self { + case let .add(parentFolder): + return parentFolder + case let .edit(_, parentFolder): + return parentFolder + } + } + +} diff --git a/DuckDuckGo/Bookmarks/ViewModel/BookmarksDialogViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/BookmarksDialogViewModel.swift new file mode 100644 index 0000000000..08ef2cc47d --- /dev/null +++ b/DuckDuckGo/Bookmarks/ViewModel/BookmarksDialogViewModel.swift @@ -0,0 +1,35 @@ +// +// BookmarksDialogViewModel.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +@MainActor +protocol BookmarksDialogViewModel: ObservableObject { + var title: String { get } + + var folders: [FolderViewModel] { get } + var selectedFolder: BookmarkFolder? { get set } + + var cancelActionTitle: String { get } + var isOtherActionDisabled: Bool { get } + var defaultActionTitle: String { get } + var isDefaultActionDisabled: Bool { get } + + func cancel(dismiss: () -> Void) + func addOrSave(dismiss: () -> Void) +} diff --git a/DuckDuckGo/BookmarksBar/View/BookmarksBarCollectionViewItem.swift b/DuckDuckGo/BookmarksBar/View/BookmarksBarCollectionViewItem.swift index 61c0ec7630..dd9d2e617f 100644 --- a/DuckDuckGo/BookmarksBar/View/BookmarksBarCollectionViewItem.swift +++ b/DuckDuckGo/BookmarksBar/View/BookmarksBarCollectionViewItem.swift @@ -24,11 +24,13 @@ protocol BookmarksBarCollectionViewItemDelegate: AnyObject { func bookmarksBarCollectionViewItemOpenInNewTabAction(_ item: BookmarksBarCollectionViewItem) func bookmarksBarCollectionViewItemOpenInNewWindowAction(_ item: BookmarksBarCollectionViewItem) - func bookmarksBarCollectionViewItemAddToFavoritesAction(_ item: BookmarksBarCollectionViewItem) + func bookmarksBarCollectionViewItemToggleFavoritesAction(_ item: BookmarksBarCollectionViewItem) func bookmarksBarCollectionViewEditAction(_ item: BookmarksBarCollectionViewItem) func bookmarksBarCollectionViewItemMoveToEndAction(_ item: BookmarksBarCollectionViewItem) func bookmarksBarCollectionViewItemCopyBookmarkURLAction(_ item: BookmarksBarCollectionViewItem) func bookmarksBarCollectionViewItemDeleteEntityAction(_ item: BookmarksBarCollectionViewItem) + func bookmarksBarCollectionViewItemAddEntityAction(_ item: BookmarksBarCollectionViewItem) + func bookmarksBarCollectionViewItemManageBookmarksAction(_ item: BookmarksBarCollectionViewItem) } @@ -128,114 +130,72 @@ extension BookmarksBarCollectionViewItem: NSMenuDelegate { switch entityType { case .bookmark(_, _, _, let isFavorite): - menu.items = createBookmarkMenuItems(isFavorite: isFavorite) + menu.items = ContextualMenu.bookmarkMenuItems(isFavorite: isFavorite) case .folder: - menu.items = createFolderMenuItems() + menu.items = ContextualMenu.folderMenuItems() } } } -extension BookmarksBarCollectionViewItem { +extension BookmarksBarCollectionViewItem: BookmarkMenuItemSelectors { - // MARK: Bookmark Menu Items - - func createBookmarkMenuItems(isFavorite: Bool) -> [NSMenuItem] { - let items = [ - openBookmarkInNewTabMenuItem(), - openBookmarkInNewWindowMenuItem(), - NSMenuItem.separator(), - addToFavoritesMenuItem(isFavorite: isFavorite), - editItem(), - moveToEndMenuItem(), - NSMenuItem.separator(), - copyBookmarkURLMenuItem(), - deleteEntityMenuItem() - ].compactMap { $0 } - - return items - } - - func openBookmarkInNewTabMenuItem() -> NSMenuItem { - return menuItem(UserText.openInNewTab, #selector(openBookmarkInNewTabMenuItemSelected(_:))) - } - - @objc - func openBookmarkInNewTabMenuItemSelected(_ sender: NSMenuItem) { + func openBookmarkInNewTab(_ sender: NSMenuItem) { delegate?.bookmarksBarCollectionViewItemOpenInNewTabAction(self) } - func openBookmarkInNewWindowMenuItem() -> NSMenuItem { - return menuItem(UserText.openInNewWindow, #selector(openBookmarkInNewWindowMenuItemSelected(_:))) - } - - @objc - func openBookmarkInNewWindowMenuItemSelected(_ sender: NSMenuItem) { + func openBookmarkInNewWindow(_ sender: NSMenuItem) { delegate?.bookmarksBarCollectionViewItemOpenInNewWindowAction(self) } - func addToFavoritesMenuItem(isFavorite: Bool) -> NSMenuItem? { - guard !isFavorite else { - return nil - } - - return menuItem(UserText.addToFavorites, #selector(addToFavoritesMenuItemSelected(_:))) - } - - @objc - func addToFavoritesMenuItemSelected(_ sender: NSMenuItem) { - delegate?.bookmarksBarCollectionViewItemAddToFavoritesAction(self) + func toggleBookmarkAsFavorite(_ sender: NSMenuItem) { + delegate?.bookmarksBarCollectionViewItemToggleFavoritesAction(self) } - func editItem() -> NSMenuItem { - return menuItem("Edit…", #selector(editItemSelected(_:))) + func editBookmark(_ sender: NSMenuItem) { + delegate?.bookmarksBarCollectionViewEditAction(self) } - @objc - func editItemSelected(_ sender: NSMenuItem) { - delegate?.bookmarksBarCollectionViewEditAction(self) + func copyBookmark(_ sender: NSMenuItem) { + delegate?.bookmarksBarCollectionViewItemCopyBookmarkURLAction(self) } - func moveToEndMenuItem() -> NSMenuItem { - return menuItem(UserText.bookmarksBarContextMenuMoveToEnd, #selector(moveToEndMenuItemSelected(_:))) + func deleteBookmark(_ sender: NSMenuItem) { + delegate?.bookmarksBarCollectionViewItemDeleteEntityAction(self) } - @objc - func moveToEndMenuItemSelected(_ sender: NSMenuItem) { + func moveToEnd(_ sender: NSMenuItem) { delegate?.bookmarksBarCollectionViewItemMoveToEndAction(self) } - func copyBookmarkURLMenuItem() -> NSMenuItem { - return menuItem(UserText.bookmarksBarContextMenuCopy, #selector(copyBookmarkURLMenuItemSelected(_:))) + func deleteEntities(_ sender: NSMenuItem) {} + + func manageBookmarks(_ sender: NSMenuItem) { + delegate?.bookmarksBarCollectionViewItemManageBookmarksAction(self) } - @objc - func copyBookmarkURLMenuItemSelected(_ sender: NSMenuItem) { - delegate?.bookmarksBarCollectionViewItemCopyBookmarkURLAction(self) +} + +extension BookmarksBarCollectionViewItem: FolderMenuItemSelectors { + + func newFolder(_ sender: NSMenuItem) { + delegate?.bookmarksBarCollectionViewItemAddEntityAction(self) } - func deleteEntityMenuItem() -> NSMenuItem { - return menuItem(UserText.bookmarksBarContextMenuDelete, #selector(deleteMenuItemSelected(_:))) + func editFolder(_ sender: NSMenuItem) { + delegate?.bookmarksBarCollectionViewEditAction(self) } - @objc - func deleteMenuItemSelected(_ sender: NSMenuItem) { + func deleteFolder(_ sender: NSMenuItem) { delegate?.bookmarksBarCollectionViewItemDeleteEntityAction(self) } - // MARK: Folder Menu Items - - func createFolderMenuItems() -> [NSMenuItem] { - return [ - editItem(), - moveToEndMenuItem(), - NSMenuItem.separator(), - deleteEntityMenuItem() - ] + func openInNewTabs(_ sender: NSMenuItem) { + delegate?.bookmarksBarCollectionViewItemOpenInNewTabAction(self) } - func menuItem(_ title: String, _ action: Selector) -> NSMenuItem { - return NSMenuItem(title: title, action: action, keyEquivalent: "") + func openAllInNewWindow(_ sender: NSMenuItem) { + delegate?.bookmarksBarCollectionViewItemOpenInNewWindowAction(self) } } diff --git a/DuckDuckGo/BookmarksBar/View/BookmarksBarMenuFactory.swift b/DuckDuckGo/BookmarksBar/View/BookmarksBarMenuFactory.swift index 56eff5f57a..c8d9f6c016 100644 --- a/DuckDuckGo/BookmarksBar/View/BookmarksBarMenuFactory.swift +++ b/DuckDuckGo/BookmarksBar/View/BookmarksBarMenuFactory.swift @@ -34,6 +34,13 @@ struct BookmarksBarMenuFactory { menu.addItem(makeMenuItem(prefs)) } + static func addToMenuWithManageBookmarksSection(_ menu: NSMenu, target: AnyObject, addFolderSelector: Selector, manageBookmarksSelector: Selector, prefs: AppearancePreferences = .shared) { + addToMenu(menu, prefs) + menu.addItem(.separator()) + menu.addItem(NSMenuItem(title: UserText.addFolder, action: addFolderSelector, target: target)) + menu.addItem(NSMenuItem(title: UserText.bookmarksManageBookmarks, action: manageBookmarksSelector, target: target)) + } + private static func makeMenuItem( _ prefs: AppearancePreferences) -> NSMenuItem { let item = NSMenuItem(title: UserText.showBookmarksBar, action: nil, keyEquivalent: "B") item.submenu = NSMenu(items: [ diff --git a/DuckDuckGo/BookmarksBar/View/BookmarksBarViewController.swift b/DuckDuckGo/BookmarksBar/View/BookmarksBarViewController.swift index ea9b61e496..e298a6335e 100644 --- a/DuckDuckGo/BookmarksBar/View/BookmarksBarViewController.swift +++ b/DuckDuckGo/BookmarksBar/View/BookmarksBarViewController.swift @@ -223,59 +223,113 @@ extension BookmarksBarViewController: BookmarksBarViewModelDelegate { bookmarksBarCollectionView.reloadData() } - private func handle(_ action: BookmarksBarViewModel.BookmarksBarItemAction, for bookmark: Bookmark) { +} + +// MARK: - Private + +private extension BookmarksBarViewController { + + func handle(_ action: BookmarksBarViewModel.BookmarksBarItemAction, for bookmark: Bookmark) { switch action { case .openInNewTab: - guard let url = bookmark.urlObject else { return } - tabCollectionViewModel.appendNewTab(with: .url(url, source: .bookmark), selected: true) + openInNewTab(bookmark: bookmark) case .openInNewWindow: - guard let url = bookmark.urlObject else { return } - WindowsManager.openNewWindow(with: url, source: .bookmark, isBurner: false) + openInNewWindow(bookmark: bookmark) case .clickItem: WindowControllersManager.shared.open(bookmark: bookmark) - case .addToFavorites: - bookmark.isFavorite = true + case .toggleFavorites: + bookmark.isFavorite.toggle() bookmarkManager.update(bookmark: bookmark) case .edit: - AddBookmarkModalView(model: AddBookmarkModalViewModel(originalBookmark: bookmark)) - .show(in: view.window) + showDialog(view: BookmarksDialogViewFactory.makeEditBookmarkView(bookmark: bookmark)) case .moveToEnd: bookmarkManager.move(objectUUIDs: [bookmark.id], toIndex: nil, withinParentFolder: .root) { _ in } case .copyURL: bookmark.copyUrlToPasteboard() case .deleteEntity: bookmarkManager.remove(bookmark: bookmark) + case .addFolder: + addFolder(inParent: nil) + case .manageBookmarks: + manageBookmarks() } } - private func handle(_ action: BookmarksBarViewModel.BookmarksBarItemAction, for folder: BookmarkFolder, item: BookmarksBarCollectionViewItem) { + func handle(_ action: BookmarksBarViewModel.BookmarksBarItemAction, for folder: BookmarkFolder, item: BookmarksBarCollectionViewItem) { switch action { case .clickItem: - let childEntities = folder.children - let viewModels = childEntities.map { BookmarkViewModel(entity: $0) } - let menuItems = viewModel.bookmarksTreeMenuItems(from: viewModels, topLevel: true) - let menu = bookmarkFolderMenu(items: menuItems) - - menu.popUp(positioning: nil, at: CGPoint(x: 0, y: item.view.frame.minY - 7), in: item.view) + showSubmenuFor(folder: folder, fromView: item.view) case .edit: - AddBookmarkFolderModalView(model: AddBookmarkFolderModalViewModel(folder: folder)) - .show(in: view.window) + showDialog(view: BookmarksDialogViewFactory.makeEditBookmarkFolderView(folder: folder, parentFolder: nil)) case .moveToEnd: bookmarkManager.move(objectUUIDs: [folder.id], toIndex: nil, withinParentFolder: .root) { _ in } case .deleteEntity: bookmarkManager.remove(folder: folder) + case .addFolder: + addFolder(inParent: folder) + case .openInNewTab: + openAllInNewTabs(folder: folder) + case .openInNewWindow: + openAllInNewWindow(folder: folder) + case .manageBookmarks: + manageBookmarks() default: assertionFailure("Received unexpected action for bookmark folder") } } - private func bookmarkFolderMenu(items: [NSMenuItem]) -> NSMenu { + func bookmarkFolderMenu(items: [NSMenuItem]) -> NSMenu { let menu = NSMenu() menu.items = items.isEmpty ? [NSMenuItem.empty] : items menu.autoenablesItems = false return menu } + func openInNewTab(bookmark: Bookmark) { + guard let url = bookmark.urlObject else { return } + tabCollectionViewModel.appendNewTab(with: .url(url, source: .bookmark), selected: true) + } + + func openInNewWindow(bookmark: Bookmark) { + guard let url = bookmark.urlObject else { return } + WindowsManager.openNewWindow(with: url, source: .bookmark, isBurner: false) + } + + func openAllInNewTabs(folder: BookmarkFolder) { + let tabs = Tab.withContentOfBookmark(folder: folder, burnerMode: tabCollectionViewModel.burnerMode) + tabCollectionViewModel.append(tabs: tabs) + } + + func openAllInNewWindow(folder: BookmarkFolder) { + let tabCollection = TabCollection.withContentOfBookmark(folder: folder, burnerMode: tabCollectionViewModel.burnerMode) + WindowsManager.openNewWindow(with: tabCollection, isBurner: tabCollectionViewModel.isBurner) + } + + func showSubmenuFor(folder: BookmarkFolder, fromView view: NSView) { + let childEntities = folder.children + let viewModels = childEntities.map { BookmarkViewModel(entity: $0) } + let menuItems = viewModel.bookmarksTreeMenuItems(from: viewModels, topLevel: true) + let menu = bookmarkFolderMenu(items: menuItems) + + menu.popUp(positioning: nil, at: CGPoint(x: 0, y: view.frame.minY - 7), in: view) + } + + func addFolder(inParent parent: BookmarkFolder?) { + showDialog(view: BookmarksDialogViewFactory.makeAddBookmarkFolderView(parentFolder: parent)) + } + + func showDialog(view: any ModalView) { + view.show(in: self.view.window) + } + + @objc func manageBookmarks() { + WindowControllersManager.shared.showBookmarksTab() + } + + @objc func addFolder(sender: NSMenuItem) { + addFolder(inParent: nil) + } + } // MARK: - Menu @@ -284,7 +338,12 @@ extension BookmarksBarViewController: NSMenuDelegate { public func menuNeedsUpdate(_ menu: NSMenu) { menu.removeAllItems() - BookmarksBarMenuFactory.addToMenu(menu) + BookmarksBarMenuFactory.addToMenuWithManageBookmarksSection( + menu, + target: self, + addFolderSelector: #selector(addFolder(sender:)), + manageBookmarksSelector: #selector(manageBookmarks) + ) } } diff --git a/DuckDuckGo/BookmarksBar/View/BookmarksBarViewModel.swift b/DuckDuckGo/BookmarksBar/View/BookmarksBarViewModel.swift index 2dc81a1596..1c82b4c576 100644 --- a/DuckDuckGo/BookmarksBar/View/BookmarksBarViewModel.swift +++ b/DuckDuckGo/BookmarksBar/View/BookmarksBarViewModel.swift @@ -47,11 +47,13 @@ final class BookmarksBarViewModel: NSObject { case clickItem case openInNewTab case openInNewWindow - case addToFavorites + case toggleFavorites case edit case moveToEnd case copyURL case deleteEntity + case addFolder + case manageBookmarks } struct BookmarksBarItem { @@ -482,8 +484,8 @@ extension BookmarksBarViewModel: BookmarksBarCollectionViewItemDelegate { delegate?.bookmarksBarViewModelReceived(action: .openInNewWindow, for: item) } - func bookmarksBarCollectionViewItemAddToFavoritesAction(_ item: BookmarksBarCollectionViewItem) { - delegate?.bookmarksBarViewModelReceived(action: .addToFavorites, for: item) + func bookmarksBarCollectionViewItemToggleFavoritesAction(_ item: BookmarksBarCollectionViewItem) { + delegate?.bookmarksBarViewModelReceived(action: .toggleFavorites, for: item) } func bookmarksBarCollectionViewEditAction(_ item: BookmarksBarCollectionViewItem) { @@ -502,4 +504,12 @@ extension BookmarksBarViewModel: BookmarksBarCollectionViewItemDelegate { delegate?.bookmarksBarViewModelReceived(action: .deleteEntity, for: item) } + func bookmarksBarCollectionViewItemAddEntityAction(_ item: BookmarksBarCollectionViewItem) { + delegate?.bookmarksBarViewModelReceived(action: .addFolder, for: item) + } + + func bookmarksBarCollectionViewItemManageBookmarksAction(_ item: BookmarksBarCollectionViewItem) { + delegate?.bookmarksBarViewModelReceived(action: .manageBookmarks, for: item) + } + } diff --git a/DuckDuckGo/Common/Extensions/NSMenuExtension.swift b/DuckDuckGo/Common/Extensions/NSMenuExtension.swift index 69043e9a3b..6790d5c4f7 100644 --- a/DuckDuckGo/Common/Extensions/NSMenuExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSMenuExtension.swift @@ -58,4 +58,14 @@ extension NSMenu { insertItem(newItem, at: index) } + /// Pops up the menu at the current mouse location. + /// + /// - Parameter view: The view to display the menu item over. + /// - Attention: If the view is not currently installed in a window, this function does not show any pop up menu. + func popUpAtMouseLocation(in view: NSView) { + guard let cursorLocation = view.window?.mouseLocationOutsideOfEventStream else { return } + let convertedLocation = view.convert(cursorLocation, from: nil) + popUp(positioning: nil, at: convertedLocation, in: view) + } + } diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 555dece7ae..3c142fd364 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -426,7 +426,6 @@ struct UserText { static let addToFavorites = NSLocalizedString("add.to.favorites", value: "Add to Favorites", comment: "Button for adding bookmarks to favorites") static let addFavorite = NSLocalizedString("add.favorite", value: "Add Favorite", comment: "Button for adding a favorite bookmark") static let editFavorite = NSLocalizedString("edit.favorite", value: "Edit Favorite", comment: "Header of the view that edits a favorite bookmark") - static let editFolder = NSLocalizedString("edit.folder", value: "Edit Folder", comment: "Header of the view that edits a bookmark folder") static let removeFromFavorites = NSLocalizedString("remove.from.favorites", value: "Remove from Favorites", comment: "Button for removing bookmarks from favorites") static let bookmarkThisPage = NSLocalizedString("bookmark.this.page", value: "Bookmark This Page", comment: "Menu item for bookmarking current page") static let bookmarksShowToolbarPanel = NSLocalizedString("bookmarks.show-toolbar-panel", value: "Open Bookmarks Panel", comment: "Menu item for opening the bookmarks panel") @@ -459,7 +458,6 @@ struct UserText { static let newFolder = NSLocalizedString("folder.optionsMenu.newFolder", value: "New Folder", comment: "Option for creating a new folder") static let renameFolder = NSLocalizedString("folder.optionsMenu.renameFolder", value: "Rename Folder", comment: "Option for renaming a folder") static let deleteFolder = NSLocalizedString("folder.optionsMenu.deleteFolder", value: "Delete Folder", comment: "Option for deleting a folder") - static let newFolderDialogFolderNameTitle = NSLocalizedString("add.folder.name", value: "Name:", comment: "Add Folder popover: folder name text field title") static let newBookmarkDialogBookmarkNameTitle = NSLocalizedString("add.bookmark.name", value: "Name:", comment: "New bookmark folder dialog folder name field heading") static let updateBookmark = NSLocalizedString("bookmark.update", value: "Update Bookmark", comment: "Option for updating a bookmark") @@ -1048,9 +1046,9 @@ struct UserText { enum Bookmarks { enum Dialog { enum Title { - static let addBookmark = NSLocalizedString("bookmarks.dialog.title.add", value: "Add bookmark", comment: "Bookmark creation dialog title") + static let addBookmark = NSLocalizedString("bookmarks.dialog.title.add", value: "Add Bookmark", comment: "Bookmark creation dialog title") static let addedBookmark = NSLocalizedString("bookmarks.dialog.title.added", value: "Bookmark Added", comment: "Bookmark added popover title") - static let editBookmark = NSLocalizedString("bookmarks.dialog.title.edit", value: "Edit bookmark", comment: "Bookmark edit dialog title") + static let editBookmark = NSLocalizedString("bookmarks.dialog.title.edit", value: "Edit Bookmark", comment: "Bookmark edit dialog title") static let addFolder = NSLocalizedString("bookmarks.dialog.folder.title.add", value: "Add Folder", comment: "Bookmark folder creation dialog title") static let editFolder = NSLocalizedString("bookmarks.dialog.folder.title.edit", value: "Edit Folder", comment: "Bookmark folder edit dialog title") } diff --git a/DuckDuckGo/HomePage/Model/HomePageFavoritesModel.swift b/DuckDuckGo/HomePage/Model/HomePageFavoritesModel.swift index 0860f02cfc..e88a48b1b0 100644 --- a/DuckDuckGo/HomePage/Model/HomePageFavoritesModel.swift +++ b/DuckDuckGo/HomePage/Model/HomePageFavoritesModel.swift @@ -86,14 +86,16 @@ extension HomePage.Models { let open: (Bookmark, OpenTarget) -> Void let removeFavorite: (Bookmark) -> Void let deleteBookmark: (Bookmark) -> Void - let addEdit: (Bookmark?) -> Void + let add: () -> Void + let edit: (Bookmark) -> Void let moveFavorite: (Bookmark, Int) -> Void let onFaviconMissing: () -> Void init(open: @escaping (Bookmark, OpenTarget) -> Void, removeFavorite: @escaping (Bookmark) -> Void, deleteBookmark: @escaping (Bookmark) -> Void, - addEdit: @escaping (Bookmark?) -> Void, + add: @escaping () -> Void, + edit: @escaping (Bookmark) -> Void, moveFavorite: @escaping (Bookmark, Int) -> Void, onFaviconMissing: @escaping () -> Void ) { @@ -102,7 +104,8 @@ extension HomePage.Models { self.open = open self.removeFavorite = removeFavorite self.deleteBookmark = deleteBookmark - self.addEdit = addEdit + self.add = add + self.edit = edit self.moveFavorite = moveFavorite self.onFaviconMissing = onFaviconMissing } @@ -119,12 +122,12 @@ extension HomePage.Models { open(bookmark, .current) } - func edit(_ bookmark: Bookmark) { - addEdit(bookmark) + func editBookmark(_ bookmark: Bookmark) { + edit(bookmark) } func addNew() { - addEdit(nil) + add() } private func updateVisibleModels() { diff --git a/DuckDuckGo/HomePage/View/FavoritesView.swift b/DuckDuckGo/HomePage/View/FavoritesView.swift index 90956a4a66..52e7f585d1 100644 --- a/DuckDuckGo/HomePage/View/FavoritesView.swift +++ b/DuckDuckGo/HomePage/View/FavoritesView.swift @@ -305,14 +305,17 @@ struct Favorite: View { let bookmark: Bookmark // Maintain separate copies of bookmark metadata required by the view, in order to ensure that SwiftUI re-renders correctly. + // Do not remove these properties even if some are not used in the `FavoriteTemplate` view as the view will not re-render correctly. private let bookmarkTitle: String private let bookmarkURL: URL + private let bookmarkParentFolder: String? init?(bookmark: Bookmark) { guard let urlObject = bookmark.urlObject else { return nil } self.bookmark = bookmark self.bookmarkTitle = bookmark.title self.bookmarkURL = urlObject + self.bookmarkParentFolder = bookmark.parentFolderUUID } var body: some View { diff --git a/DuckDuckGo/HomePage/View/HomePageViewController.swift b/DuckDuckGo/HomePage/View/HomePageViewController.swift index ae40aff5df..03f2cb4e08 100644 --- a/DuckDuckGo/HomePage/View/HomePageViewController.swift +++ b/DuckDuckGo/HomePage/View/HomePageViewController.swift @@ -172,8 +172,10 @@ final class HomePageViewController: NSViewController { self?.bookmarkManager.update(bookmark: bookmark) }, deleteBookmark: { [weak self] bookmark in self?.bookmarkManager.remove(bookmark: bookmark) - }, addEdit: { [weak self] bookmark in - self?.showAddEditController(for: bookmark) + }, add: { [weak self] in + self?.showAddController() + }, edit: { [weak self] bookmark in + self?.showEditController(for: bookmark) }, moveFavorite: { [weak self] (bookmark, index) in self?.bookmarkManager.moveFavorites(with: [bookmark.id], toIndex: index) { _ in } }, onFaviconMissing: { [weak self] in @@ -204,7 +206,7 @@ final class HomePageViewController: NSViewController { } func subscribeToBookmarks() { - bookmarkManager.listPublisher.receive(on: RunLoop.main).sink { [weak self] _ in + bookmarkManager.listPublisher.receive(on: DispatchQueue.main).sink { [weak self] _ in withAnimation { self?.refreshFavoritesModel() } @@ -230,9 +232,14 @@ final class HomePageViewController: NSViewController { tabCollectionViewModel.selectedTabViewModel?.tab.setContent(.contentFromURL(url, source: .bookmark)) } - private func showAddEditController(for bookmark: Bookmark? = nil) { - AddBookmarkModalView(model: AddBookmarkModalViewModel(originalBookmark: bookmark, isFavorite: true)) - .show(in: self.view.window) + private func showAddController() { + BookmarksDialogViewFactory.makeAddFavoriteView() + .show(in: view.window) + } + + private func showEditController(for bookmark: Bookmark) { + BookmarksDialogViewFactory.makeEditBookmarkView(bookmark: bookmark) + .show(in: view.window) } private var burningDataCancellable: AnyCancellable? diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index a00894c192..94ea695d1b 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -908,66 +908,6 @@ } } }, - "add.folder.name" : { - "comment" : "Add Folder popover: folder name text field title", - "extractionState" : "extracted_with_value", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Name:" - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Name:" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nombre:" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nom :" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nome:" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Naam:" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nazwa:" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nome:" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Имя:" - } - } - } - }, "add.link.to.bookmarks" : { "comment" : "Context menu item", "extractionState" : "extracted_with_value", @@ -8959,7 +8899,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "Add bookmark" + "value" : "Add Bookmark" } }, "es" : { @@ -9079,7 +9019,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "Edit bookmark" + "value" : "Edit Bookmark" } }, "es" : { @@ -15626,66 +15566,6 @@ } } }, - "edit.folder" : { - "comment" : "Header of the view that edits a bookmark folder", - "extractionState" : "extracted_with_value", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ordner bearbeiten" - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Edit Folder" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Editar carpeta" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Modifier le dossier" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Modifica cartella" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Map bewerken" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Edytuj folder" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Editar pasta" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Изменить папку" - } - } - } - }, "email.copied" : { "comment" : "Notification that the Private email address was copied to clipboard after the user generated a new address", "extractionState" : "extracted_with_value", @@ -24584,59 +24464,6 @@ } } }, - "Location:" : { - "comment" : "Add Folder popover: parent folder picker title", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Standort:" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ubicación:" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Emplacement :" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Posizione:" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Locatie:" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lokalizacja:" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Localização:" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Папка:" - } - } - } - }, "looking.for.bitwarden" : { "comment" : "Setup of the integration with Bitwarden app", "extractionState" : "extracted_with_value", @@ -47706,59 +47533,6 @@ } } }, - "Title:" : { - "comment" : "Add Bookmark dialog bookmark title field heading", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Titel:" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Título:" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Titre :" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Titolo:" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Titel:" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tytuł:" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Título:" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Название:" - } - } - } - }, "tooltip.addToFavorites" : { "comment" : "Tooltip for add to favorites button", "extractionState" : "extracted_with_value", diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationView.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationView.swift index 01b7b3f4ec..15e6bcdc66 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationView.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationView.swift @@ -286,19 +286,4 @@ private struct VPNLocationViewButtons: View { } -extension View { - /// Applies the given transform if the given condition evaluates to `true`. - /// - Parameters: - /// - condition: The condition to evaluate. - /// - transform: The transform to apply to the source `View`. - /// - Returns: Either the original `View` or the modified `View` if the condition is `true`. - @ViewBuilder func `if`(_ condition: Bool, transform: (Self) -> Content) -> some View { - if condition { - transform(self) - } else { - self - } - } -} - #endif diff --git a/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/Dialog.swift b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/Dialogs/Dialog.swift similarity index 100% rename from LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/Dialog.swift rename to LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/Dialogs/Dialog.swift diff --git a/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/Dialogs/TieredDialogView.swift b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/Dialogs/TieredDialogView.swift new file mode 100644 index 0000000000..541941304b --- /dev/null +++ b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/Dialogs/TieredDialogView.swift @@ -0,0 +1,70 @@ +// +// TieredDialogView.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +/// A view to arrange its subviews in a three vertical sections separated by dividers. +public struct TieredDialogView: View { + private let verticalSpacing: CGFloat + private let horizontalAlignment: HorizontalAlignment + private let horizontalPadding: CGFloat? + @ViewBuilder private let top: () -> Top + @ViewBuilder private let center: () -> Center + @ViewBuilder private let bottom: () -> Bottom + + /// Creates an instance with the given vertical spacing, horizontal alignment, horizontal padding and views created by the specified view builders. + /// - Parameters: + /// - verticalSpacing: The distance between adjacent sections. + /// - horizontalAlignment: The guide for aligning the sections in the vertical stack. This guide has the same vertical screen coordinate for every subview. + /// - horizontalPadding: The padding amount to add to the horizontal edges of the sections. + /// - top: A view builder that creates the content of the top section of the dialog. + /// - center: A view builder that creates the content of the central section of the dialog. + /// - bottom: A view builder that creates the content of the bottom section of the dialog. + public init( + verticalSpacing: CGFloat = 10.0, + horizontalAlignment: HorizontalAlignment = .leading, + horizontalPadding: CGFloat? = nil, + @ViewBuilder top: @escaping () -> Top, + @ViewBuilder center: @escaping () -> Center, + @ViewBuilder bottom: @escaping () -> Bottom + ) { + self.horizontalAlignment = horizontalAlignment + self.verticalSpacing = verticalSpacing + self.horizontalPadding = horizontalPadding + self.top = top + self.center = center + self.bottom = bottom + } + + public var body: some View { + VStack(alignment: horizontalAlignment, spacing: verticalSpacing) { + top() + .padding(.horizontal, horizontalPadding) + + Divider() + + center() + .padding(.horizontal, horizontalPadding) + + Divider() + + bottom() + .padding(.horizontal, horizontalPadding) + } + } +} diff --git a/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/TwoColumnsListView.swift b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/TwoColumnsListView.swift new file mode 100644 index 0000000000..55bb57bca0 --- /dev/null +++ b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/TwoColumnsListView.swift @@ -0,0 +1,62 @@ +// +// TwoColumnsListView.swift +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +/// A view to arrange its subviews in two-column equally spaced rows. +public struct TwoColumnsListView: View { + private let rowHeight: CGFloat? + private let horizontalSpacing: CGFloat? + private let verticalSpacing: CGFloat? + @ViewBuilder private let leftColumn: () -> Left + @ViewBuilder private let rightColumn: () -> Right + + /// Creates an instance with the given horizontal and vertical spacing, row height and + /// - Parameters: + /// - horizontalSpacing: The horizontal distance between adjacent subviews. + /// - verticalSpacing: The vertical distance between adjacent subviews. + /// - rowHeight: The height of the rows in the stack. + /// - leftColumn: A view builder that creates the content of the left section of the view. + /// - rightColumn: A view builder that creates the content of the right section of the view. + public init( + horizontalSpacing: CGFloat? = nil, + verticalSpacing: CGFloat? = nil, + rowHeight: CGFloat? = nil, + @ViewBuilder leftColumn: @escaping () -> Left, + @ViewBuilder rightColumn: @escaping () -> Right + ) { + self.horizontalSpacing = horizontalSpacing + self.verticalSpacing = verticalSpacing + self.rowHeight = rowHeight + self.leftColumn = leftColumn + self.rightColumn = rightColumn + } + + public var body: some View { + HStack(alignment: .center, spacing: horizontalSpacing) { + VStack(alignment: .leading, spacing: verticalSpacing) { + leftColumn() + .frame(height: rowHeight) + } + VStack(alignment: .leading, spacing: verticalSpacing) { + rightColumn() + .frame(height: rowHeight) + } + } + } +} diff --git a/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/View+ConditionalModifiers.swift b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/View+ConditionalModifiers.swift new file mode 100644 index 0000000000..af223c4439 --- /dev/null +++ b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/View+ConditionalModifiers.swift @@ -0,0 +1,47 @@ +// +// View+ConditionalModifiers.swift +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +public extension View { + /// Applies the given transform if the given condition evaluates to `true`. + /// - Parameters: + /// - condition: The condition to evaluate. + /// - transform: The transform to apply to the source `View`. + /// - Returns: Either the original `View` or the modified `View` if the condition is `true`. + @ViewBuilder func `if`(_ condition: Bool, transform: (Self) -> Content) -> some View { + if condition { + transform(self) + } else { + self + } + } + + /// Applies the given transform if the given optional value is not `nil`. + /// - Parameters: + /// - value: The optional value to evaluate. + /// - transform: The transform to apply to the source `View`. + /// - Returns: Either the original `View` or the modified `View` if the optional value is not `nil`. + @ViewBuilder func `ifLet`(_ value: Value?, transform: (Self, Value) -> Content) -> some View { + if let value = value { + transform(self, value) + } else { + self + } + } +} diff --git a/UnitTests/Bookmarks/Extensions/Bookmarks+TabTests.swift b/UnitTests/Bookmarks/Extensions/Bookmarks+TabTests.swift new file mode 100644 index 0000000000..5226ead0c6 --- /dev/null +++ b/UnitTests/Bookmarks/Extensions/Bookmarks+TabTests.swift @@ -0,0 +1,61 @@ +// +// Bookmarks+TabTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +@MainActor +final class Bookmarks_TabTests: XCTestCase { + + func testWhenBuildTabsWithContentOfFolderThenItShouldReturnAsManyTabsAsBookmarksWithinTheFolder() { + // GIVEN + let bookmark1 = Bookmark(id: "A", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: false) + let bookmark2 = Bookmark(id: "B", url: URL.atb, title: "ATB", isFavorite: false) + let subFolder = BookmarkFolder(id: "C", title: "Child") + let folder = BookmarkFolder(id: "1", title: "Test", children: [bookmark1, subFolder, bookmark2]) + + // WHEN + let tab = Tab.withContentOfBookmark(folder: folder, burnerMode: .regular) + + // THEN + XCTAssertEqual(tab.count, 2) + XCTAssertEqual(tab.first?.url?.absoluteString, bookmark1.url) + XCTAssertEqual(tab.first?.burnerMode, .regular) + XCTAssertEqual(tab.last?.url?.absoluteString, bookmark2.url) + XCTAssertEqual(tab.last?.burnerMode, .regular) + } + + func testWhenBuildTabCollectionWithContentOfFolderThenItShouldReturnACollectionWithAsManyTabsAsBookmarksWithinTheFolder() { + // GIVEN + let bookmark1 = Bookmark(id: "A", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: false) + let bookmark2 = Bookmark(id: "B", url: URL.atb, title: "ATB", isFavorite: false) + let subFolder = BookmarkFolder(id: "C", title: "Child") + let folder = BookmarkFolder(id: "1", title: "Test", children: [bookmark1, subFolder, bookmark2]) + + // WHEN + let tabCollection = TabCollection.withContentOfBookmark(folder: folder, burnerMode: .regular) + + // THEN + XCTAssertEqual(tabCollection.tabs.count, 2) + XCTAssertEqual(tabCollection.tabs.first?.url?.absoluteString, bookmark1.url) + XCTAssertEqual(tabCollection.tabs.first?.burnerMode, .regular) + XCTAssertEqual(tabCollection.tabs.last?.url?.absoluteString, bookmark2.url) + XCTAssertEqual(tabCollection.tabs.last?.burnerMode, .regular) + } + +} diff --git a/UnitTests/Bookmarks/Factory/BookmarksBarMenuFactoryTests.swift b/UnitTests/Bookmarks/Factory/BookmarksBarMenuFactoryTests.swift new file mode 100644 index 0000000000..df9f76ec4e --- /dev/null +++ b/UnitTests/Bookmarks/Factory/BookmarksBarMenuFactoryTests.swift @@ -0,0 +1,59 @@ +// +// BookmarksBarMenuFactoryTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +final class BookmarksBarMenuFactoryTests: XCTestCase { + + func testReturnAddFolderAndManageBookmarksWhenAddToMenuWithManageBookmarksSectionIsCalled() { + // GIVEN + let menu = NSMenu(title: "") + let targetMock = BookmarksBarTargetMock() + XCTAssertTrue(menu.items.isEmpty) + + // WHEN + BookmarksBarMenuFactory.addToMenuWithManageBookmarksSection(menu, target: targetMock, addFolderSelector: #selector(targetMock.addFolder(_:)), manageBookmarksSelector: #selector(targetMock.manageBookmarks)) + + // THEN + XCTAssertEqual(menu.items.count, 4) + XCTAssertEqual(menu.items[1].title, "") + XCTAssertNil(menu.items[1].action) + XCTAssertEqual(menu.items[2].title, UserText.addFolder) + XCTAssertEqual(menu.items[2].action, #selector(targetMock.addFolder(_:))) + XCTAssertEqual(menu.items[3].title, UserText.bookmarksManageBookmarks) + XCTAssertEqual(menu.items[3].action, #selector(targetMock.manageBookmarks)) + } + + func testShouldNotReturnAddFolderAndManageBookmarksWhenAddToMenuIsCalled() { + // GIVEN + let menu = NSMenu(title: "") + XCTAssertTrue(menu.items.isEmpty) + + // WHEN + BookmarksBarMenuFactory.addToMenu(menu) + + // THEN + XCTAssertEqual(menu.items.count, 1) + } +} + +private class BookmarksBarTargetMock: NSObject { + @objc func addFolder(_ sender: NSMenuItem) {} + @objc func manageBookmarks() {} +} diff --git a/UnitTests/Bookmarks/Model/BaseBookmarkEntityTests.swift b/UnitTests/Bookmarks/Model/BaseBookmarkEntityTests.swift new file mode 100644 index 0000000000..11ffefab31 --- /dev/null +++ b/UnitTests/Bookmarks/Model/BaseBookmarkEntityTests.swift @@ -0,0 +1,247 @@ +// +// BaseBookmarkEntityTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +final class BaseBookmarkEntityTests: XCTestCase { + + // MARK: - Folders + + func testTwoBookmarkFolderWithSamePropertiesReturnTrueWhenIsEqualCalled() { + // GIVEN + let parentFolder = BookmarkFolder(id: "Parent", title: "Parent") + let lhs = BookmarkFolder(id: "1", title: "Child", parentFolderUUID: parentFolder.id, children: []) + let rhs = BookmarkFolder(id: "1", title: "Child", parentFolderUUID: parentFolder.id, children: []) + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertTrue(result) + } + + func testTwoBookmarkFolderWithDifferentIdReturnFalseWhenIsEqualCalled() { + // GIVEN + let parentFolder = BookmarkFolder(id: "Parent", title: "Parent") + let lhs = BookmarkFolder(id: "1", title: "Child", parentFolderUUID: parentFolder.id, children: []) + let rhs = BookmarkFolder(id: "2", title: "Child", parentFolderUUID: parentFolder.id, children: []) + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertFalse(result) + } + + func testTwoBookmarkFolderWithDifferentTitleReturnFalseWhenIsEqualCalled() { + // GIVEN + let parentFolder = BookmarkFolder(id: "Parent", title: "Parent") + let lhs = BookmarkFolder(id: "1", title: "Child", parentFolderUUID: parentFolder.id, children: []) + let rhs = BookmarkFolder(id: "1", title: "Child 1", parentFolderUUID: parentFolder.id, children: []) + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertFalse(result) + } + + func testTwoBookmarkFolderWithDifferentParentReturnFalseWhenIsEqualCalled() { + // GIVEN + let parentFolder = BookmarkFolder(id: "Parent", title: "Parent") + let lhs = BookmarkFolder(id: "1", title: "Child", parentFolderUUID: parentFolder.id, children: []) + let rhs = BookmarkFolder(id: "1", title: "Child", parentFolderUUID: #function, children: []) + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertFalse(result) + } + + func testTwoBookmarkParentFolderWithSameSubfoldersReturnTrueWhenIsEqualCalled() { + // GIVEN + let folder1 = BookmarkFolder(id: "1", title: "Child", parentFolderUUID: "Parent", children: []) + let folder2 = BookmarkFolder(id: "2", title: "Child", parentFolderUUID: "Parent", children: []) + let lhs = BookmarkFolder(id: "1-Parent", title: "Parent", children: [folder1, folder2]) + let rhs = BookmarkFolder(id: "1-Parent", title: "Parent", children: [folder1, folder2]) + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertTrue(result) + } + + func testTwoBookmarkParentFolderWithDifferentSubfoldersReturnFalseWhenIsEqualCalled() { + // GIVEN + let folder1 = BookmarkFolder(id: "1", title: "Child", parentFolderUUID: "Parent", children: []) + let folder2 = BookmarkFolder(id: "2", title: "Child", parentFolderUUID: "Parent", children: []) + let lhs = BookmarkFolder(id: "1-Parent", title: "Parent", children: [folder1, folder2]) + let rhs = BookmarkFolder(id: "1-Parent", title: "Parent", children: [folder1, folder2, BookmarkFolder(id: "3", title: "")]) + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertFalse(result) + } + + func testTwoBookmarkParentFolderWithSameBookmarksReturnTrueWhenIsEqualCalled() { + // GIVEN + let bookmark1 = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true, parentFolderUUID: "1-Parent") + let bookmark2 = Bookmark(id: "2", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true, parentFolderUUID: "1-Parent") + let lhs = BookmarkFolder(id: "1-Parent", title: "Parent", children: [bookmark1, bookmark2]) + let rhs = BookmarkFolder(id: "1-Parent", title: "Parent", children: [bookmark1, bookmark2]) + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertTrue(result) + } + + func testTwoBookmarkParentFolderWithDifferentBookmarksReturnFalseWhenIsEqualCalled() { + // GIVEN + let bookmark1 = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true, parentFolderUUID: "1-Parent") + let bookmark2 = Bookmark(id: "2", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true, parentFolderUUID: "1-Parent") + let lhs = BookmarkFolder(id: "1-Parent", title: "Parent", children: [bookmark1, bookmark2]) + let rhs = BookmarkFolder(id: "1-Parent", title: "Parent", children: [bookmark1, bookmark2, BookmarkFolder(id: "4", title: "New")]) + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertFalse(result) + } + + // MARK: - Bookmarks + + func testTwoBookmarkWithSamePropertiesReturnTrueWhenIsEqualCalled() { + // GIVEN + let parentFolder = BookmarkFolder(id: "Parent", title: "Parent") + let lhs = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true, parentFolderUUID: parentFolder.id) + let rhs = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true, parentFolderUUID: parentFolder.id) + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertTrue(result) + } + + func testTwoBookmarkWithDifferentIdReturnFalseWhenIsEqualCalled() { + // GIVEN + let lhs = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true, parentFolderUUID: "z") + let rhs = Bookmark(id: "2", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true, parentFolderUUID: "z") + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertFalse(result) + } + + func testTwoBookmarkWithDifferentURLReturnFalseWhenIsEqualCalled() { + // GIVEN + let lhs = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true, parentFolderUUID: "z") + let rhs = Bookmark(id: "1", url: URL.devMode, title: "DDG", isFavorite: true, parentFolderUUID: "z") + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertFalse(result) + } + + func testTwoBookmarkWithDifferentTitleReturnFalseWhenIsEqualCalled() { + // GIVEN + let lhs = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true, parentFolderUUID: "z") + let rhs = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG 2", isFavorite: true, parentFolderUUID: "z") + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertFalse(result) + } + + func testTwoBookmarkWithDifferentIsFavoriteReturnFalseWhenIsEqualCalled() { + // GIVEN + let lhs = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true, parentFolderUUID: "z") + let rhs = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: false, parentFolderUUID: "z") + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertFalse(result) + } + + func testTwoBookmarkWithDifferentParentFolderReturnFalseWhenIsEqualCalled() { + // GIVEN + let lhs = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true, parentFolderUUID: "z") + let rhs = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: false, parentFolderUUID: "z-a") + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertFalse(result) + } + + func testTwoBookmarkFoldersAddedToRootFolderReturnTrueWhenLeftParentIsBookmarksRootAndRightIsNil() { + // GIVEN + let lhs = BookmarkFolder(id: "1", title: "A", parentFolderUUID: "bookmarks_root", children: []) + let rhs = BookmarkFolder(id: "1", title: "A", parentFolderUUID: nil, children: []) + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertTrue(result) + } + + func testTwoBookmarkFoldersAddedToRootFolderReturnTrueWhenLeftParentIsNilAndRightParentIsRootBookmarks() { + // GIVEN + let lhs = BookmarkFolder(id: "1", title: "A", parentFolderUUID: nil, children: []) + let rhs = BookmarkFolder(id: "1", title: "A", parentFolderUUID: "bookmarks_root", children: []) + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertTrue(result) + } + + // MARK: - Base Entity + + func testDifferentBookmarkEntitiesReturnFalseWhenIsEqualCalled() { + // GIVEN + let lhs = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true) + let rhs = BookmarkFolder(id: "1", title: "DDG") + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertFalse(result) + } + +} diff --git a/UnitTests/Bookmarks/Model/BookmarkListTests.swift b/UnitTests/Bookmarks/Model/BookmarkListTests.swift index e959ff9878..f0f8cda903 100644 --- a/UnitTests/Bookmarks/Model/BookmarkListTests.swift +++ b/UnitTests/Bookmarks/Model/BookmarkListTests.swift @@ -23,15 +23,20 @@ import XCTest final class BookmarkListTests: XCTestCase { - func testWhenBookmarkIsInserted_ThenItIsPartOfTheList() { + func testWhenBookmarkIsInserted_ThenItIsPartOfTheList() throws { var bookmarkList = BookmarkList() let bookmark = Bookmark.aBookmark bookmarkList.insert(bookmark) + let result = try XCTUnwrap(bookmarkList[bookmark.url]) XCTAssert(bookmarkList.bookmarks().count == 1) XCTAssert((bookmarkList.bookmarks()).first == bookmark.identifiableBookmark) - XCTAssertNotNil(bookmarkList[bookmark.url]) + XCTAssertEqual(result.id, bookmark.id) + XCTAssertEqual(result.title, bookmark.title) + XCTAssertEqual(result.url, bookmark.url) + XCTAssertEqual(result.isFavorite, bookmark.isFavorite) + XCTAssertEqual(result.parentFolderUUID, bookmark.parentFolderUUID) } func testWhenBookmarkIsAlreadyPartOfTheListInserted_ThenItCantBeInserted() { @@ -93,7 +98,7 @@ final class BookmarkListTests: XCTestCase { XCTAssertNil(updateUrlResult) } - func testWhenBookmarkUrlIsUpdated_ThenJustTheBookmarkUrlIsUpdated() { + func testWhenBookmarkUrlIsUpdated_ThenJustTheBookmarkUrlIsUpdated() throws { var bookmarkList = BookmarkList() let bookmarks = [ @@ -104,11 +109,14 @@ final class BookmarkListTests: XCTestCase { bookmarks.forEach { bookmarkList.insert($0) } let bookmarkToReplace = bookmarks[2] - let newBookmark = bookmarkList.updateUrl(of: bookmarkToReplace, to: URL.duckDuckGoAutocomplete.absoluteString) + let newBookmark = try XCTUnwrap(bookmarkList.updateUrl(of: bookmarkToReplace, to: URL.duckDuckGoAutocomplete.absoluteString)) + let result = try XCTUnwrap(bookmarkList[newBookmark.url]) XCTAssert(bookmarkList.bookmarks().count == bookmarks.count) XCTAssertNil(bookmarkList[bookmarkToReplace.url]) - XCTAssertNotNil(bookmarkList[newBookmark!.url]) + XCTAssertEqual(result.title, "Title") + XCTAssertEqual(result.url, URL.duckDuckGoAutocomplete.absoluteString) + XCTAssertTrue(result.isFavorite) } func testWhenBookmarkUrlIsUpdatedToAlreadyBookmarkedUrl_ThenUpdatingMustFail() { @@ -127,10 +135,51 @@ final class BookmarkListTests: XCTestCase { XCTAssert(bookmarkList.bookmarks().count == bookmarks.count) XCTAssertNotNil(bookmarkList[firstUrl.absoluteString]) + XCTAssertEqual(bookmarkList[firstUrl.absoluteString]?.url, firstUrl.absoluteString) XCTAssertNotNil(bookmarkList[bookmarkToReplace.url]) + XCTAssertEqual(bookmarkList[bookmarkToReplace.url]?.url, URL.duckDuckGo.absoluteString) XCTAssertNil(newBookmark) } + func testWhenBookmarkURLTitleAndIsFavoriteIsUpdated_ThenURLTitleAndIsFavoriteIsUpdated() throws { + // GIVEN + var bookmarkList = BookmarkList() + let bookmarks = [ + Bookmark(id: UUID().uuidString, url: "wikipedia.org", title: "Wikipedia", isFavorite: true), + Bookmark(id: UUID().uuidString, url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true), + Bookmark(id: UUID().uuidString, url: "apple.com", title: "Apple", isFavorite: true) + ] + bookmarks.forEach { bookmarkList.insert($0) } + let bookmarkToReplace = bookmarks[2] + XCTAssertEqual(bookmarkList.bookmarks().count, bookmarks.count) + XCTAssertEqual(bookmarkList["wikipedia.org"]?.url, "wikipedia.org") + XCTAssertEqual(bookmarkList["wikipedia.org"]?.title, "Wikipedia") + XCTAssertEqual(bookmarkList["wikipedia.org"]?.isFavorite, true) + XCTAssertEqual(bookmarkList[URL.duckDuckGo.absoluteString]?.url, URL.duckDuckGo.absoluteString) + XCTAssertEqual(bookmarkList[URL.duckDuckGo.absoluteString]?.title, "DDG") + XCTAssertEqual(bookmarkList[URL.duckDuckGo.absoluteString]?.isFavorite, true) + XCTAssertEqual(bookmarkList["apple.com"]?.url, "apple.com") + XCTAssertEqual(bookmarkList["apple.com"]?.title, "Apple") + XCTAssertEqual(bookmarkList["apple.com"]?.isFavorite, true) + + // WHEN + let newBookmark = try XCTUnwrap(bookmarkList.update(bookmark: bookmarkToReplace, newURL: "www.example.com", newTitle: "Example", newIsFavorite: false)) + + // THEN + let result = try XCTUnwrap(bookmarkList[newBookmark.url]) + XCTAssertEqual(bookmarkList.bookmarks().count, bookmarks.count) + XCTAssertNil(bookmarkList[bookmarkToReplace.url]) + XCTAssertEqual(bookmarkList["wikipedia.org"]?.url, "wikipedia.org") + XCTAssertEqual(bookmarkList["wikipedia.org"]?.title, "Wikipedia") + XCTAssertEqual(bookmarkList["wikipedia.org"]?.isFavorite, true) + XCTAssertEqual(bookmarkList[URL.duckDuckGo.absoluteString]?.url, URL.duckDuckGo.absoluteString) + XCTAssertEqual(bookmarkList[URL.duckDuckGo.absoluteString]?.title, "DDG") + XCTAssertEqual(bookmarkList[URL.duckDuckGo.absoluteString]?.isFavorite, true) + XCTAssertEqual(result.url, "www.example.com") + XCTAssertEqual(result.title, "Example") + XCTAssertEqual(result.isFavorite, false) + } + } fileprivate extension Bookmark { diff --git a/UnitTests/Bookmarks/Model/BookmarkNodeTests.swift b/UnitTests/Bookmarks/Model/BookmarkNodeTests.swift index 8a9060c2f9..fd795b2ebb 100644 --- a/UnitTests/Bookmarks/Model/BookmarkNodeTests.swift +++ b/UnitTests/Bookmarks/Model/BookmarkNodeTests.swift @@ -217,4 +217,174 @@ class BookmarkNodeTests: XCTestCase { XCTAssertNotEqual(rootNode.findOrCreateChildNode(with: TestObject()), childNode) } + // MARK: - Equality Bookmarks + + func testWhenTwoNodesWithSameIdAndSameBookmarkAsRepresentedObject_ThenIsEqualShouldBeTrue() { + // GIVEN + let lhsBookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true) + let rhsBookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true) + let lhsNode = BookmarkNode(representedObject: lhsBookmark, parent: nil, uniqueId: 1) + let rhsNode = BookmarkNode(representedObject: rhsBookmark, parent: nil, uniqueId: 1) + + // WHEN + let result = lhsNode == rhsNode + + // THEN + XCTAssertTrue(result) + } + + func testWhenTwoNodesWithDifferentIdAndSameBookmarkAsRepresentedObject_ThenIsEqualShouldBeFalse() { + // GIVEN + let lhsBookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true) + let rhsBookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true) + let lhsNode = BookmarkNode(representedObject: lhsBookmark, parent: nil, uniqueId: 1) + let rhsNode = BookmarkNode(representedObject: rhsBookmark, parent: nil, uniqueId: 2) + + // WHEN + let result = lhsNode == rhsNode + + // THEN + XCTAssertFalse(result) + } + + func testWhenTwoNodesContainBookmarkAsRepresentedObjectWithDifferentId_ThenIsEqualShouldBeFalse() { + // GIVEN + let lhsBookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true) + let rhsBookmark = Bookmark(id: "2", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true) + let lhsNode = BookmarkNode(representedObject: lhsBookmark, parent: nil, uniqueId: 1) + let rhsNode = BookmarkNode(representedObject: rhsBookmark, parent: nil, uniqueId: 1) + + // WHEN + let result = lhsNode == rhsNode + + // THEN + XCTAssertFalse(result) + } + + func testWhenTwoNodesContainBookmarkAsRepresentedObjectWithDifferentURL_ThenIsEqualShouldBeFalse() { + // GIVEN + let lhsBookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true) + let rhsBookmark = Bookmark(id: "2", url: URL.ddgLearnMore.absoluteString, title: "DDG", isFavorite: true) + let lhsNode = BookmarkNode(representedObject: lhsBookmark, parent: nil, uniqueId: 1) + let rhsNode = BookmarkNode(representedObject: rhsBookmark, parent: nil, uniqueId: 1) + + // WHEN + let result = lhsNode == rhsNode + + // THEN + XCTAssertFalse(result) + } + + func testWhenTwoNodesContainBookmarkAsRepresentedObjectWithDifferentTitle_ThenIsEqualShouldBeFalse() { + // GIVEN + let lhsBookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true) + let rhsBookmark = Bookmark(id: "2", url: URL.duckDuckGo.absoluteString, title: "DDG 2", isFavorite: true) + let lhsNode = BookmarkNode(representedObject: lhsBookmark, parent: nil, uniqueId: 1) + let rhsNode = BookmarkNode(representedObject: rhsBookmark, parent: nil, uniqueId: 1) + + // WHEN + let result = lhsNode == rhsNode + + // THEN + XCTAssertFalse(result) + } + + func testWhenTwoNodesContainBookmarkAsRepresentedObjectWithDifferentIsFavorite_ThenIsEqualShouldBeFalse() { + // GIVEN + let lhsBookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true) + let rhsBookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: false) + let lhsNode = BookmarkNode(representedObject: lhsBookmark, parent: nil, uniqueId: 1) + let rhsNode = BookmarkNode(representedObject: rhsBookmark, parent: nil, uniqueId: 1) + + // WHEN + let result = lhsNode == rhsNode + + // THEN + XCTAssertFalse(result) + } + + func testWhenTwoNodesWithSameIdAndSameFolderAsRepresentedObject_ThenIsEqualShouldBeTrue() { + // GIVEN + let lhsFolder = BookmarkFolder(id: "1", title: "Folder", parentFolderUUID: "1", children: []) + let rhsFolder = BookmarkFolder(id: "1", title: "Folder", parentFolderUUID: "1", children: []) + let lhsNode = BookmarkNode(representedObject: lhsFolder, parent: nil, uniqueId: 1) + let rhsNode = BookmarkNode(representedObject: rhsFolder, parent: nil, uniqueId: 1) + + // WHEN + let result = lhsNode == rhsNode + + // THEN + XCTAssertTrue(result) + } + + func testWhenTwoNodesWithDifferentIdAndSameFolderAsRepresentedObject_ThenIsEqualShouldBeFalse() { + // GIVEN + let lhsFolder = BookmarkFolder(id: "1", title: "Folder", parentFolderUUID: "1", children: []) + let rhsFolder = BookmarkFolder(id: "1", title: "Folder", parentFolderUUID: "1", children: []) + let lhsNode = BookmarkNode(representedObject: lhsFolder, parent: nil, uniqueId: 1) + let rhsNode = BookmarkNode(representedObject: rhsFolder, parent: nil, uniqueId: 2) + + // WHEN + let result = lhsNode == rhsNode + + // THEN + XCTAssertFalse(result) + } + + func testWhenTwoNodesContainFolderAsRepresentedObjectWithDifferentId_ThenIsEqualShouldBeFalse() { + // GIVEN + let lhsFolder = BookmarkFolder(id: "1", title: "Folder", parentFolderUUID: "1", children: []) + let rhsFolder = BookmarkFolder(id: "2", title: "Folder", parentFolderUUID: "1", children: []) + let lhsNode = BookmarkNode(representedObject: lhsFolder, parent: nil, uniqueId: 1) + let rhsNode = BookmarkNode(representedObject: rhsFolder, parent: nil, uniqueId: 1) + + // WHEN + let result = lhsNode == rhsNode + + // THEN + XCTAssertFalse(result) + } + + func testWhenTwoNodesContainFolderAsRepresentedObjectWithDifferentName_ThenIsEqualShouldBeFalse() { + // GIVEN + let lhsFolder = BookmarkFolder(id: "1", title: "Folder", parentFolderUUID: "1", children: []) + let rhsFolder = BookmarkFolder(id: "1", title: "Folder 1", parentFolderUUID: "1", children: []) + let lhsNode = BookmarkNode(representedObject: lhsFolder, parent: nil, uniqueId: 1) + let rhsNode = BookmarkNode(representedObject: rhsFolder, parent: nil, uniqueId: 1) + + // WHEN + let result = lhsNode == rhsNode + + // THEN + XCTAssertFalse(result) + } + + func testWhenTwoNodesContainBookmarkAsRepresentedObjectWithDifferentParentFolder_ThenIsEqualShouldBeFalse() { + // GIVEN + let lhsFolder = BookmarkFolder(id: "1", title: "Folder", parentFolderUUID: "1", children: []) + let rhsFolder = BookmarkFolder(id: "1", title: "Folder", parentFolderUUID: "2", children: []) + let lhsNode = BookmarkNode(representedObject: lhsFolder, parent: nil, uniqueId: 1) + let rhsNode = BookmarkNode(representedObject: rhsFolder, parent: nil, uniqueId: 1) + + // WHEN + let result = lhsNode == rhsNode + + // THEN + XCTAssertFalse(result) + } + + func testWhenTwoNodesContainFolderAsRepresentedObjectWithDifferentChildren_ThenIsEqualShouldBeFalse() { + // GIVEN + let lhsFolder = BookmarkFolder(id: "1", title: "Folder", parentFolderUUID: "1", children: [Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true)]) + let rhsFolder = BookmarkFolder(id: "1", title: "Folder", parentFolderUUID: "2", children: []) + let lhsNode = BookmarkNode(representedObject: lhsFolder, parent: nil, uniqueId: 1) + let rhsNode = BookmarkNode(representedObject: rhsFolder, parent: nil, uniqueId: 1) + + // WHEN + let result = lhsNode == rhsNode + + // THEN + XCTAssertFalse(result) + } + } diff --git a/UnitTests/Bookmarks/Model/BookmarkOutlineViewDataSourceTests.swift b/UnitTests/Bookmarks/Model/BookmarkOutlineViewDataSourceTests.swift index 8595ebdcc7..d1cc5fa4d8 100644 --- a/UnitTests/Bookmarks/Model/BookmarkOutlineViewDataSourceTests.swift +++ b/UnitTests/Bookmarks/Model/BookmarkOutlineViewDataSourceTests.swift @@ -110,7 +110,7 @@ class BookmarkOutlineViewDataSourceTests: XCTestCase { let mockDestinationNode = treeController.node(representing: mockDestinationFolder)! let dataSource = BookmarkOutlineViewDataSource(contentMode: .foldersOnly, bookmarkManager: bookmarkManager, treeController: treeController) - let pasteboardFolder = PasteboardFolder(id: UUID().uuidString, name: "Pasteboard Folder") + let pasteboardFolder = PasteboardFolder(folder: .init(id: UUID().uuidString, title: "Pasteboard Folder")) let result = dataSource.validateDrop(for: [pasteboardFolder], destination: mockDestinationNode) XCTAssertEqual(result, .move) @@ -130,7 +130,7 @@ class BookmarkOutlineViewDataSourceTests: XCTestCase { let dataSource = BookmarkOutlineViewDataSource(contentMode: .foldersOnly, bookmarkManager: bookmarkManager, treeController: treeController) let mockDestinationNode = treeController.node(representing: mockDestinationFolder)! - let pasteboardFolder = PasteboardFolder(id: mockDestinationFolder.id, name: "Pasteboard Folder") + let pasteboardFolder = PasteboardFolder(folder: mockDestinationFolder) let result = dataSource.validateDrop(for: [pasteboardFolder], destination: mockDestinationNode) XCTAssertEqual(result, .none) @@ -153,12 +153,64 @@ class BookmarkOutlineViewDataSourceTests: XCTestCase { let mockDestinationNode = treeController.node(representing: childFolder)! // Simulate dragging the root folder onto the child folder: - let draggedFolder = PasteboardFolder(id: rootFolder.id, name: "Root") + let draggedFolder = PasteboardFolder(folder: rootFolder) let result = dataSource.validateDrop(for: [draggedFolder], destination: mockDestinationNode) XCTAssertEqual(result, .none) } + func testWhenCellFiresDelegate_ThenOnMenuRequestedActionShouldFire() throws { + // GIVEN + let mockFolder = BookmarkFolder.mock + let mockOutlineView = NSOutlineView(frame: .zero) + let treeController = createTreeController(with: [mockFolder]) + let mockFolderNode = treeController.node(representing: mockFolder)! + var didFireClosure = false + var capturedCell: BookmarkOutlineCellView? + let dataSource = BookmarkOutlineViewDataSource(contentMode: .foldersOnly, bookmarkManager: LocalBookmarkManager(), treeController: treeController) { cell in + didFireClosure = true + capturedCell = cell + } + let cell = try XCTUnwrap(dataSource.outlineView(mockOutlineView, viewFor: nil, item: mockFolderNode) as? BookmarkOutlineCellView) + + // WHEN + cell.delegate?.outlineCellViewRequestedMenu(cell) + + // THEN + XCTAssertTrue(didFireClosure) + XCTAssertEqual(cell, capturedCell) + } + + func testWhenShowMenuButtonOnHoverIsTrue_ThenCellShouldHaveShouldMenuButtonFlagTrue() throws { + // GIVEN + let mockFolder = BookmarkFolder.mock + let mockOutlineView = NSOutlineView(frame: .zero) + let treeController = createTreeController(with: [mockFolder]) + let mockFolderNode = treeController.node(representing: mockFolder)! + let dataSource = BookmarkOutlineViewDataSource(contentMode: .bookmarksAndFolders, bookmarkManager: LocalBookmarkManager(), treeController: treeController, showMenuButtonOnHover: true) + + // WHEN + let cell = try XCTUnwrap(dataSource.outlineView(mockOutlineView, viewFor: nil, item: mockFolderNode) as? BookmarkOutlineCellView) + + // THEN + XCTAssertTrue(cell.shouldShowMenuButton) + } + + func testWhenShowMenuButtonOnHoverIsFalse_ThenCellShouldHaveShouldMenuButtonFlagFalse() throws { + // GIVEN + let mockFolder = BookmarkFolder.mock + let mockOutlineView = NSOutlineView(frame: .zero) + let treeController = createTreeController(with: [mockFolder]) + let mockFolderNode = treeController.node(representing: mockFolder)! + let dataSource = BookmarkOutlineViewDataSource(contentMode: .foldersOnly, bookmarkManager: LocalBookmarkManager(), treeController: treeController, showMenuButtonOnHover: false) + + // WHEN + let cell = try XCTUnwrap(dataSource.outlineView(mockOutlineView, viewFor: nil, item: mockFolderNode) as? BookmarkOutlineCellView) + + // THEN + XCTAssertFalse(cell.shouldShowMenuButton) + } + // MARK: - Private private func createTreeController(with bookmarks: [BaseBookmarkEntity]) -> BookmarkTreeController { diff --git a/UnitTests/Bookmarks/Model/BookmarkSidebarTreeControllerTests.swift b/UnitTests/Bookmarks/Model/BookmarkSidebarTreeControllerTests.swift index 6c9e8c59da..4b95d7ca90 100644 --- a/UnitTests/Bookmarks/Model/BookmarkSidebarTreeControllerTests.swift +++ b/UnitTests/Bookmarks/Model/BookmarkSidebarTreeControllerTests.swift @@ -28,24 +28,18 @@ class BookmarkSidebarTreeControllerTests: XCTestCase { let defaultNodes = treeController.rootNode.childNodes let representedObjects = defaultNodes.representedObjects() - // The sidebar defines three hardcoded nodes: + // The sidebar defines one hardcoded nodes: // - // 1. Favorites node - // 2. Spacer node - // 3. Bookmarks node + // 1. Bookmarks node - XCTAssertEqual(defaultNodes.count, 3) + XCTAssertEqual(defaultNodes.count, 1) - XCTAssertFalse(defaultNodes[0].canHaveChildNodes) - XCTAssertFalse(defaultNodes[1].canHaveChildNodes) - XCTAssertTrue(defaultNodes[2].canHaveChildNodes) + XCTAssertTrue(defaultNodes[0].canHaveChildNodes) - XCTAssert(representedObjects[0] === PseudoFolder.favorites) - XCTAssert(representedObjects[1] === SpacerNode.blank) - XCTAssert(representedObjects[2] === PseudoFolder.bookmarks) + XCTAssert(representedObjects.first === PseudoFolder.bookmarks) } - func testWhenBookmarkStoreHasNoTopLevelFolders_ThenTheDefaultBookmarksNodeHasNoChildren() { + func testWhenBookmarkStoreHasNoTopLevelFolders_ThenTheDefaultBookmarksNodeHasNoChildren() throws { let bookmarkStoreMock = BookmarkStoreMock() let faviconManagerMock = FaviconManagerMock() let bookmarkManager = LocalBookmarkManager(bookmarkStore: bookmarkStoreMock, faviconManagement: faviconManagerMock) @@ -56,11 +50,13 @@ class BookmarkSidebarTreeControllerTests: XCTestCase { let dataSource = BookmarkSidebarTreeController(bookmarkManager: bookmarkManager) let treeController = BookmarkTreeController(dataSource: dataSource) let defaultNodes = treeController.rootNode.childNodes - XCTAssertEqual(defaultNodes.count, 3) + XCTAssertEqual(defaultNodes.count, 1) // The sidebar tree controller only shows folders, so if there are only bookmarks then the bookmarks default folder will be empty. - let bookmarksNode = defaultNodes[2] - XCTAssert(bookmarksNode.childNodes.isEmpty) + let bookmarksNode = defaultNodes[0] + let pseudoFolder = try XCTUnwrap(bookmarksNode.representedObject as? PseudoFolder) + XCTAssertTrue(bookmarksNode.childNodes.isEmpty) + XCTAssertEqual(pseudoFolder.name, "Bookmarks") } func testWhenBookmarkStoreHasTopLevelFolders_ThenTheDefaultBookmarksNodeHasThemAsChildren() { @@ -75,9 +71,9 @@ class BookmarkSidebarTreeControllerTests: XCTestCase { let dataSource = BookmarkSidebarTreeController(bookmarkManager: bookmarkManager) let treeController = BookmarkTreeController(dataSource: dataSource) let defaultNodes = treeController.rootNode.childNodes - XCTAssertEqual(defaultNodes.count, 3) + XCTAssertEqual(defaultNodes.count, 1) - let bookmarksNode = defaultNodes[2] + let bookmarksNode = defaultNodes[0] XCTAssertEqual(bookmarksNode.childNodes.count, 1) let childNode = bookmarksNode.childNodes[0] @@ -98,9 +94,9 @@ class BookmarkSidebarTreeControllerTests: XCTestCase { let dataSource = BookmarkSidebarTreeController(bookmarkManager: bookmarkManager) let treeController = BookmarkTreeController(dataSource: dataSource) let defaultNodes = treeController.rootNode.childNodes - XCTAssertEqual(defaultNodes.count, 3) + XCTAssertEqual(defaultNodes.count, 1) - let bookmarksNode = defaultNodes[2] + let bookmarksNode = defaultNodes[0] XCTAssertTrue(bookmarksNode.canHaveChildNodes) XCTAssertEqual(bookmarksNode.childNodes.count, 1) diff --git a/UnitTests/Bookmarks/Model/BookmarkTests.swift b/UnitTests/Bookmarks/Model/BookmarkTests.swift index cd21752a06..e6cf1df214 100644 --- a/UnitTests/Bookmarks/Model/BookmarkTests.swift +++ b/UnitTests/Bookmarks/Model/BookmarkTests.swift @@ -90,7 +90,7 @@ class BookmarkTests: XCTestCase { XCTAssertEqual(folder.childFolders.count, 0) XCTAssertEqual(folder.childBookmarks.count, 1) XCTAssertEqual(folder.children, [ - BaseBookmarkEntity.from(managedObject: bookmarkManagedObject, favoritesDisplayMode: .displayNative(.desktop)) + BaseBookmarkEntity.from(managedObject: bookmarkManagedObject, parentFolderUUID: folder.id, favoritesDisplayMode: .displayNative(.desktop)) ]) XCTAssertNil(folder.parentFolderUUID) diff --git a/UnitTests/Bookmarks/Model/ContextualMenuTests.swift b/UnitTests/Bookmarks/Model/ContextualMenuTests.swift new file mode 100644 index 0000000000..89da2c186a --- /dev/null +++ b/UnitTests/Bookmarks/Model/ContextualMenuTests.swift @@ -0,0 +1,267 @@ +// +// ContextualMenuTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +final class ContextualMenuTests: XCTestCase { + + func testWhenAskingBookmarkMenuItemsAndIsNotFavoriteThenItShouldReturnTheItemsInTheCorrectOrder() { + // GIVEN + let isFavorite = false + + // WHEN + let items = ContextualMenu.bookmarkMenuItems(isFavorite: isFavorite) + + // THEN + XCTAssertEqual(items.count, 12) + assertMenu(item: items[0], withTitle: UserText.openInNewTab, selector: #selector(BookmarkMenuItemSelectors.openBookmarkInNewTab(_:))) + assertMenu(item: items[1], withTitle: UserText.openInNewWindow, selector: #selector(BookmarkMenuItemSelectors.openBookmarkInNewWindow(_:))) + assertMenu(item: items[2], withTitle: "", selector: nil) // Separator + assertMenu(item: items[3], withTitle: UserText.addToFavorites, selector: #selector(BookmarkMenuItemSelectors.toggleBookmarkAsFavorite(_:))) + assertMenu(item: items[4], withTitle: "", selector: nil) // Separator + assertMenu(item: items[5], withTitle: UserText.editBookmark, selector: #selector(BookmarkMenuItemSelectors.editBookmark(_:))) + assertMenu(item: items[6], withTitle: UserText.bookmarksBarContextMenuCopy, selector: #selector(BookmarkMenuItemSelectors.copyBookmark(_:))) + assertMenu(item: items[7], withTitle: UserText.bookmarksBarContextMenuDelete, selector: #selector(BookmarkMenuItemSelectors.deleteBookmark(_:))) + assertMenu(item: items[8], withTitle: UserText.bookmarksBarContextMenuMoveToEnd, selector: #selector(BookmarkMenuItemSelectors.moveToEnd(_:))) + assertMenu(item: items[9], withTitle: "", selector: nil) // Separator + assertMenu(item: items[10], withTitle: UserText.addFolder, selector: #selector(BookmarkMenuItemSelectors.newFolder(_:))) + assertMenu(item: items[11], withTitle: UserText.bookmarksManageBookmarks, selector: #selector(BookmarkMenuItemSelectors.manageBookmarks(_:))) + + } + + func testWhenAskingBookmarkMenuItemsAndIsFavoriteThenItShouldReturnTheItemsInTheCorrectOrder() { + // GIVEN + let isFavorite = true + + // WHEN + let items = ContextualMenu.bookmarkMenuItems(isFavorite: isFavorite) + + // THEN + assertMenu(item: items[0], withTitle: UserText.openInNewTab, selector: #selector(BookmarkMenuItemSelectors.openBookmarkInNewTab(_:))) + assertMenu(item: items[1], withTitle: UserText.openInNewWindow, selector: #selector(BookmarkMenuItemSelectors.openBookmarkInNewWindow(_:))) + assertMenu(item: items[2], withTitle: "", selector: nil) // Separator + assertMenu(item: items[3], withTitle: UserText.removeFromFavorites, selector: #selector(BookmarkMenuItemSelectors.toggleBookmarkAsFavorite(_:))) + assertMenu(item: items[4], withTitle: "", selector: nil) // Separator + assertMenu(item: items[5], withTitle: UserText.editBookmark, selector: #selector(BookmarkMenuItemSelectors.editBookmark(_:))) + assertMenu(item: items[6], withTitle: UserText.bookmarksBarContextMenuCopy, selector: #selector(BookmarkMenuItemSelectors.copyBookmark(_:))) + assertMenu(item: items[7], withTitle: UserText.bookmarksBarContextMenuDelete, selector: #selector(BookmarkMenuItemSelectors.deleteBookmark(_:))) + assertMenu(item: items[8], withTitle: UserText.bookmarksBarContextMenuMoveToEnd, selector: #selector(BookmarkMenuItemSelectors.moveToEnd(_:))) + assertMenu(item: items[9], withTitle: "", selector: nil) // Separator + assertMenu(item: items[10], withTitle: UserText.addFolder, selector: #selector(BookmarkMenuItemSelectors.newFolder(_:))) + assertMenu(item: items[11], withTitle: UserText.bookmarksManageBookmarks, selector: #selector(BookmarkMenuItemSelectors.manageBookmarks(_:))) + } + + func testWhenAskingFolderItemThenItShouldReturnTheItemsInTheCorrectOrders() { + // WHEN + let items = ContextualMenu.folderMenuItems() + + // THEN + XCTAssertEqual(items.count, 9) + assertMenu(item: items[0], withTitle: UserText.openAllInNewTabs, selector: #selector(FolderMenuItemSelectors.openInNewTabs(_:))) + assertMenu(item: items[1], withTitle: UserText.openAllTabsInNewWindow, selector: #selector(FolderMenuItemSelectors.openAllInNewWindow(_:))) + assertMenu(item: items[2], withTitle: "", selector: nil) // Separator + assertMenu(item: items[3], withTitle: UserText.editBookmark, selector: #selector(FolderMenuItemSelectors.editFolder(_:))) + assertMenu(item: items[4], withTitle: UserText.bookmarksBarContextMenuDelete, selector: #selector(FolderMenuItemSelectors.deleteFolder(_:))) + assertMenu(item: items[5], withTitle: UserText.bookmarksBarContextMenuMoveToEnd, selector: #selector(FolderMenuItemSelectors.moveToEnd(_:))) + assertMenu(item: items[6], withTitle: "", selector: nil) // Separator + assertMenu(item: items[7], withTitle: UserText.addFolder, selector: #selector(FolderMenuItemSelectors.newFolder(_:))) + assertMenu(item: items[8], withTitle: UserText.bookmarksManageBookmarks, selector: #selector(FolderMenuItemSelectors.manageBookmarks(_:))) + } + + func testWhenCreateMenuForEmptySelectionThenItReturnsAMenuWithAddFolderOnly() throws { + // WHEN + let menu = ContextualMenu.menu(for: []) + + // THEN + XCTAssertEqual(menu?.items.count, 1) + let menuItem = try XCTUnwrap(menu?.items.first) + assertMenu(item: menuItem, withTitle: UserText.addFolder, selector: #selector(FolderMenuItemSelectors.newFolder(_:))) + } + + func testWhenCreateMenuForBookmarkWithoutParentThenReturnsAMenuWithTheBookmarkMenuItems() throws { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: false) + + // WHEN + let menu = ContextualMenu.menu(for: [bookmark]) + + // THEN + let items = try XCTUnwrap(menu?.items) + XCTAssertEqual(items.count, 12) + assertMenu(item: items[0], withTitle: UserText.openInNewTab, selector: #selector(BookmarkMenuItemSelectors.openBookmarkInNewTab(_:)), representedObject: bookmark) + assertMenu(item: items[1], withTitle: UserText.openInNewWindow, selector: #selector(BookmarkMenuItemSelectors.openBookmarkInNewWindow(_:)), representedObject: bookmark) + assertMenu(item: items[2], withTitle: "", selector: nil) // Separator + assertMenu(item: items[3], withTitle: UserText.addToFavorites, selector: #selector(BookmarkMenuItemSelectors.toggleBookmarkAsFavorite(_:)), representedObject: bookmark) + assertMenu(item: items[4], withTitle: "", selector: nil) // Separator + assertMenu(item: items[5], withTitle: UserText.editBookmark, selector: #selector(BookmarkMenuItemSelectors.editBookmark(_:)), representedObject: bookmark) + assertMenu(item: items[6], withTitle: UserText.bookmarksBarContextMenuCopy, selector: #selector(BookmarkMenuItemSelectors.copyBookmark(_:)), representedObject: bookmark) + assertMenu(item: items[7], withTitle: UserText.bookmarksBarContextMenuDelete, selector: #selector(BookmarkMenuItemSelectors.deleteBookmark(_:)), representedObject: bookmark) + assertMenu(item: items[8], withTitle: UserText.bookmarksBarContextMenuMoveToEnd, selector: #selector(BookmarkMenuItemSelectors.moveToEnd(_:)), representedObject: BookmarkEntityInfo(entity: bookmark, parent: nil)) + assertMenu(item: items[9], withTitle: "", selector: nil) // Separator + assertMenu(item: items[10], withTitle: UserText.addFolder, selector: #selector(BookmarkMenuItemSelectors.newFolder(_:))) + assertMenu(item: items[11], withTitle: UserText.bookmarksManageBookmarks, selector: #selector(BookmarkMenuItemSelectors.manageBookmarks(_:))) + } + + func testWhenCreateMenuForBookmarkWithParentThenReturnsAMenuWithTheBookmarkMenuItems() throws { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: false, parentFolderUUID: "A") + let parent = BookmarkFolder(id: "A", title: "Folder", children: [bookmark]) + let parentNode = BookmarkNode(representedObject: parent, parent: nil) + let node = BookmarkNode(representedObject: bookmark, parent: parentNode) + + // WHEN + let menu = ContextualMenu.menu(for: [node]) + + // THEN + let items = try XCTUnwrap(menu?.items) + XCTAssertEqual(items.count, 12) + assertMenu(item: items[0], withTitle: UserText.openInNewTab, selector: #selector(BookmarkMenuItemSelectors.openBookmarkInNewTab(_:)), representedObject: bookmark) + assertMenu(item: items[1], withTitle: UserText.openInNewWindow, selector: #selector(BookmarkMenuItemSelectors.openBookmarkInNewWindow(_:)), representedObject: bookmark) + assertMenu(item: items[2], withTitle: "", selector: nil) // Separator + assertMenu(item: items[3], withTitle: UserText.addToFavorites, selector: #selector(BookmarkMenuItemSelectors.toggleBookmarkAsFavorite(_:)), representedObject: bookmark) + assertMenu(item: items[4], withTitle: "", selector: nil) // Separator + assertMenu(item: items[5], withTitle: UserText.editBookmark, selector: #selector(BookmarkMenuItemSelectors.editBookmark(_:)), representedObject: bookmark) + assertMenu(item: items[6], withTitle: UserText.bookmarksBarContextMenuCopy, selector: #selector(BookmarkMenuItemSelectors.copyBookmark(_:)), representedObject: bookmark) + assertMenu(item: items[7], withTitle: UserText.bookmarksBarContextMenuDelete, selector: #selector(BookmarkMenuItemSelectors.deleteBookmark(_:)), representedObject: bookmark) + assertMenu(item: items[8], withTitle: UserText.bookmarksBarContextMenuMoveToEnd, selector: #selector(BookmarkMenuItemSelectors.moveToEnd(_:)), representedObject: BookmarkEntityInfo(entity: bookmark, parent: parent)) + assertMenu(item: items[9], withTitle: "", selector: nil) // Separator + assertMenu(item: items[10], withTitle: UserText.addFolder, selector: #selector(BookmarkMenuItemSelectors.newFolder(_:)), representedObject: parent) + assertMenu(item: items[11], withTitle: UserText.bookmarksManageBookmarks, selector: #selector(BookmarkMenuItemSelectors.manageBookmarks(_:))) + } + + func testWhenCreateMenuForFolderNodeThenReturnsAMenuWithTheFolderMenuItems() throws { + // GIVEN + let folder = BookmarkFolder(id: "1", title: "Child") + let parent = BookmarkFolder(id: "1", title: "Parent", children: [folder]) + let parentNode = BookmarkNode(representedObject: parent, parent: nil) + let node = BookmarkNode(representedObject: folder, parent: parentNode) + + // WHEN + let menu = ContextualMenu.menu(for: [node]) + + // THEN + let items = try XCTUnwrap(menu?.items) + XCTAssertEqual(items.count, 9) + assertMenu(item: items[0], withTitle: UserText.openAllInNewTabs, selector: #selector(FolderMenuItemSelectors.openInNewTabs(_:)), representedObject: folder) + assertMenu(item: items[1], withTitle: UserText.openAllTabsInNewWindow, selector: #selector(FolderMenuItemSelectors.openAllInNewWindow(_:)), representedObject: folder) + assertMenu(item: items[2], withTitle: "", selector: nil) // Separator + assertMenu(item: items[3], withTitle: UserText.editBookmark, selector: #selector(FolderMenuItemSelectors.editFolder(_:)), representedObject: BookmarkEntityInfo(entity: folder, parent: parent)) + assertMenu(item: items[4], withTitle: UserText.bookmarksBarContextMenuDelete, selector: #selector(FolderMenuItemSelectors.deleteFolder(_:)), representedObject: folder) + assertMenu(item: items[5], withTitle: UserText.bookmarksBarContextMenuMoveToEnd, selector: #selector(FolderMenuItemSelectors.moveToEnd(_:)), representedObject: BookmarkEntityInfo(entity: folder, parent: parent)) + assertMenu(item: items[6], withTitle: "", selector: nil) // Separator + assertMenu(item: items[7], withTitle: UserText.addFolder, selector: #selector(FolderMenuItemSelectors.newFolder(_:)), representedObject: folder) + assertMenu(item: items[8], withTitle: UserText.bookmarksManageBookmarks, selector: #selector(FolderMenuItemSelectors.manageBookmarks(_:))) + } + + func testWhenCreateMenuForMultipleUnfavoriteBookmarksThenReturnsMenuWithOpenInNewTabsAddToFavoritesAndDelete() throws { + // GIVEN + let bookmark1 = Bookmark(id: "1", url: "", title: "", isFavorite: false) + let bookmark2 = Bookmark(id: "2", url: "", title: "", isFavorite: false) + + // WHEN + let menu = ContextualMenu.menu(for: [bookmark1, bookmark2]) + + // THEN + let items = try XCTUnwrap(menu?.items) + XCTAssertEqual(items.count, 4) + assertMenu(item: items[0], withTitle: UserText.bookmarksOpenInNewTabs, selector: #selector(FolderMenuItemSelectors.openInNewTabs(_:)), representedObject: [bookmark1, bookmark2]) + assertMenu(item: items[1], withTitle: UserText.addToFavorites, selector: #selector(BookmarkMenuItemSelectors.toggleBookmarkAsFavorite(_:)), representedObject: [bookmark1, bookmark2]) + assertMenu(item: items[2], withTitle: "", selector: nil) // Separator + assertMenu(item: items[3], withTitle: UserText.bookmarksBarContextMenuDelete, selector: #selector(BookmarkMenuItemSelectors.deleteEntities(_:)), representedObject: [bookmark1, bookmark2]) + } + + func testWhenCreateMenuForMultipleFavoriteBookmarksThenReturnsMenuWithOpenInNewTabsRemoveFromFavoritesAndDelete() throws { + // GIVEN + let bookmark1 = Bookmark(id: "1", url: "", title: "", isFavorite: true) + let bookmark2 = Bookmark(id: "2", url: "", title: "", isFavorite: true) + + // WHEN + let menu = ContextualMenu.menu(for: [bookmark1, bookmark2]) + + // THEN + let items = try XCTUnwrap(menu?.items) + XCTAssertEqual(items.count, 4) + assertMenu(item: items[0], withTitle: UserText.bookmarksOpenInNewTabs, selector: #selector(FolderMenuItemSelectors.openInNewTabs(_:)), representedObject: [bookmark1, bookmark2]) + assertMenu(item: items[1], withTitle: UserText.removeFromFavorites, selector: #selector(BookmarkMenuItemSelectors.toggleBookmarkAsFavorite(_:)), representedObject: [bookmark1, bookmark2]) + assertMenu(item: items[2], withTitle: "", selector: nil) // Separator + assertMenu(item: items[3], withTitle: UserText.bookmarksBarContextMenuDelete, selector: #selector(BookmarkMenuItemSelectors.deleteEntities(_:)), representedObject: [bookmark1, bookmark2]) + } + + func testWhenCreateMenuForMultipleMixedFavoriteBookmarksThenReturnsMenuWithOpenInNewTabsAndDelete() throws { + // GIVEN + let bookmark1 = Bookmark(id: "1", url: "", title: "", isFavorite: true) + let bookmark2 = Bookmark(id: "2", url: "", title: "", isFavorite: false) + + // WHEN + let menu = ContextualMenu.menu(for: [bookmark1, bookmark2]) + + // THEN + let items = try XCTUnwrap(menu?.items) + XCTAssertEqual(items.count, 3) + assertMenu(item: items[0], withTitle: UserText.bookmarksOpenInNewTabs, selector: #selector(FolderMenuItemSelectors.openInNewTabs(_:)), representedObject: [bookmark1, bookmark2]) + assertMenu(item: items[1], withTitle: "", selector: nil) // Separator + assertMenu(item: items[2], withTitle: UserText.bookmarksBarContextMenuDelete, selector: #selector(BookmarkMenuItemSelectors.deleteEntities(_:)), representedObject: [bookmark1, bookmark2]) + } + + func testWhenCreateMenuForBookmarkAndFolderThenReturnsMenuWithOpenInNewTabsOnlyForBookmarkAndDelete() throws { + // GIVEN + let bookmark = Bookmark(id: "1", url: "", title: "Bookmark", isFavorite: true) + let folder = BookmarkFolder(id: "1", title: "Folder") + + // WHEN + let menu = ContextualMenu.menu(for: [bookmark, folder]) + + // THEN + let items = try XCTUnwrap(menu?.items) + XCTAssertEqual(items.count, 3) + assertMenu(item: items[0], withTitle: UserText.bookmarksOpenInNewTabs, selector: #selector(FolderMenuItemSelectors.openInNewTabs(_:)), representedObject: [bookmark]) + assertMenu(item: items[1], withTitle: "", selector: nil) // Separator + assertMenu(item: items[2], withTitle: UserText.bookmarksBarContextMenuDelete, selector: #selector(BookmarkMenuItemSelectors.deleteEntities(_:)), representedObject: [bookmark, folder]) + } + +} + +private extension ContextualMenuTests { + + func assertMenu(item: NSMenuItem, withTitle title: String, selector: Selector?, representedObject: T = Empty() ) { + XCTAssertEqual(item.title, title) + XCTAssertEqual(item.action, selector) + if representedObject is Empty { + XCTAssertNil(item.representedObject) + } else { + XCTAssertEqualValue(item.representedObject, representedObject) + } + } + +} + +private struct Empty: Equatable {} + +private func XCTAssertEqualValue(_ expression1: @autoclosure () throws -> Any?, _ expression2: @autoclosure () throws -> T, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) where T: Equatable { + do { + guard let firstValue = try expression1() as? T else { + XCTFail("Type of expression1 \(type(of: try? expression1())) and expression2 \(type(of: try? expression2())) are different.") + return + } + let secondValue = try expression2() + XCTAssertEqual(firstValue, secondValue, message(), file: file, line: line) + } catch { + XCTFail("Failed evaluating expression.") + } +} diff --git a/UnitTests/Bookmarks/Model/LocalBookmarkManagerTests.swift b/UnitTests/Bookmarks/Model/LocalBookmarkManagerTests.swift index b82636a7c5..97ce23202d 100644 --- a/UnitTests/Bookmarks/Model/LocalBookmarkManagerTests.swift +++ b/UnitTests/Bookmarks/Model/LocalBookmarkManagerTests.swift @@ -16,6 +16,7 @@ // limitations under the License. // +import Combine import Foundation import XCTest @@ -157,6 +158,26 @@ final class LocalBookmarkManagerTests: XCTestCase { XCTAssert(bookmarkStoreMock.updateBookmarkCalled) } + func testWhenBookmarkFolderIsUpdatedAndMoved_ThenManagerUpdatesItAlsoInStore() throws { + let (bookmarkManager, bookmarkStoreMock) = LocalBookmarkManager.aManager + let parent = BookmarkFolder(id: "1", title: "Parent") + let folder = BookmarkFolder(id: "2", title: "Child") + var bookmarkList: BookmarkList? + let cancellable = bookmarkManager.listPublisher + .dropFirst() + .sink { list in + bookmarkList = list + } + + bookmarkManager.update(folder: folder, andMoveToParent: .parent(uuid: parent.id)) + + withExtendedLifetime(cancellable) {} + XCTAssertTrue(bookmarkStoreMock.updateFolderAndMoveToParentCalled) + XCTAssertEqual(bookmarkStoreMock.capturedFolder, folder) + XCTAssertEqual(bookmarkStoreMock.capturedParentFolderType, .parent(uuid: parent.id)) + XCTAssertNotNil(bookmarkList) + } + } fileprivate extension LocalBookmarkManager { diff --git a/UnitTests/Bookmarks/Services/LocalBookmarkStoreTests.swift b/UnitTests/Bookmarks/Services/LocalBookmarkStoreTests.swift index 0dff288b23..2ebff62e7d 100644 --- a/UnitTests/Bookmarks/Services/LocalBookmarkStoreTests.swift +++ b/UnitTests/Bookmarks/Services/LocalBookmarkStoreTests.swift @@ -56,7 +56,7 @@ final class LocalBookmarkStoreTests: XCTestCase { let savingExpectation = self.expectation(description: "Saving") let loadingExpectation = self.expectation(description: "Loading") - let bookmark = Bookmark(id: UUID().uuidString, url: URL.duckDuckGo.absoluteString, title: "DuckDuckGo", isFavorite: true) + let bookmark = Bookmark(id: UUID().uuidString, url: URL.duckDuckGo.absoluteString, title: "DuckDuckGo", isFavorite: true, parentFolderUUID: "bookmarks_root") bookmarkStore.save(bookmark: bookmark, parent: nil, index: nil) { (success, error) in XCTAssert(success) @@ -128,7 +128,7 @@ final class LocalBookmarkStoreTests: XCTestCase { savingExpectation.fulfill() - let modifiedBookmark = Bookmark(id: bookmark.id, url: URL.duckDuckGo.absoluteString, title: "New Title", isFavorite: false) + let modifiedBookmark = Bookmark(id: bookmark.id, url: URL.duckDuckGo.absoluteString, title: "New Title", isFavorite: false, parentFolderUUID: "bookmarks_root") bookmarkStore.update(bookmark: modifiedBookmark) bookmarkStore.loadAll(type: .bookmarks) { bookmarks, error in @@ -152,7 +152,7 @@ final class LocalBookmarkStoreTests: XCTestCase { let savingExpectation = self.expectation(description: "Saving") let loadingExpectation = self.expectation(description: "Loading") - let folder = BookmarkFolder(id: UUID().uuidString, title: "Folder") + let folder = BookmarkFolder(id: UUID().uuidString, title: "Folder", parentFolderUUID: "bookmarks_root") bookmarkStore.save(folder: folder, parent: nil) { (success, error) in XCTAssert(success) @@ -181,8 +181,9 @@ final class LocalBookmarkStoreTests: XCTestCase { let saveChildExpectation = self.expectation(description: "Save Child Folder") let loadingExpectation = self.expectation(description: "Loading") - let parentFolder = BookmarkFolder(id: UUID().uuidString, title: "Parent") - let childFolder = BookmarkFolder(id: UUID().uuidString, title: "Child") + let parentId = UUID().uuidString + let childFolder = BookmarkFolder(id: UUID().uuidString, title: "Child", parentFolderUUID: parentId) + let parentFolder = BookmarkFolder(id: parentId, title: "Parent", parentFolderUUID: "bookmarks_root", children: [childFolder]) bookmarkStore.save(folder: parentFolder, parent: nil) { (success, error) in XCTAssert(success) @@ -224,8 +225,9 @@ final class LocalBookmarkStoreTests: XCTestCase { let saveBookmarkExpectation = self.expectation(description: "Save Bookmark") let loadingExpectation = self.expectation(description: "Loading") - let folder = BookmarkFolder(id: UUID().uuidString, title: "Parent") - let bookmark = Bookmark(id: UUID().uuidString, url: "https://example.com", title: "Example", isFavorite: false) + let parentId = UUID().uuidString + let bookmark = Bookmark(id: UUID().uuidString, url: "https://example.com", title: "Example", isFavorite: false, parentFolderUUID: parentId) + let folder = BookmarkFolder(id: parentId, title: "Parent", parentFolderUUID: "bookmarks_root", children: [bookmark]) bookmarkStore.save(folder: folder, parent: nil) { (success, error) in XCTAssert(success) @@ -468,6 +470,148 @@ final class LocalBookmarkStoreTests: XCTestCase { XCTAssertEqual(topLevelEntityIDs, [testState.initialParentFolder.id, testState.bookmark3.id]) } + func testWhenUpdatingBookmarkFolder_ThenBookmarkFolderTitleIsUpdated() async throws { + let context = container.viewContext + let bookmarkStore = LocalBookmarkStore(context: context) + let folder1 = BookmarkFolder(id: UUID().uuidString, title: "Folder 1", parentFolderUUID: "bookmarks_root") + + // Save the initial bookmarks state: + + _ = await bookmarkStore.save(folder: folder1, parent: nil) + + // Fetch persisted bookmark folders back from the store: + + let folders = try await bookmarkStore.loadAll(type: .topLevelEntities).get().compactMap { $0 as? BookmarkFolder } + + XCTAssertEqual(folders.count, 1) + XCTAssertEqual(folders.first, folder1) + + // Update the folder title and parent: + + let folderToMove = folder1 + folderToMove.title = #function + bookmarkStore.update(folder: folder1) + + // Check the new bookmark folders order: + + let newFolders = try await bookmarkStore.loadAll(type: .topLevelEntities).get().compactMap { $0 as? BookmarkFolder } + + XCTAssertEqual(newFolders.count, 1) + XCTAssertEqual(newFolders.first, folderToMove) + } + + func testWhenUpdatingAndMovingBookmarkFolder_ThenBookmarkFolderIsMovedAndTitleUpdated() async throws { + let context = container.viewContext + let bookmarkStore = LocalBookmarkStore(context: context) + + let folder1 = BookmarkFolder(id: UUID().uuidString, title: "Folder 1", parentFolderUUID: "bookmarks_root") + let folder2 = BookmarkFolder(id: UUID().uuidString, title: "Folder 2", parentFolderUUID: "bookmarks_root") + let folder3 = BookmarkFolder(id: UUID().uuidString, title: "Folder 3", parentFolderUUID: "bookmarks_root") + + // Save the initial bookmarks state: + + _ = await bookmarkStore.save(folder: folder1, parent: nil) + _ = await bookmarkStore.save(folder: folder2, parent: nil) + _ = await bookmarkStore.save(folder: folder3, parent: nil) + + // Fetch persisted bookmark folders back from the store: + + let folders = try await bookmarkStore.loadAll(type: .topLevelEntities).get().compactMap { $0 as? BookmarkFolder } + + XCTAssertEqual(folders.count, 3) + XCTAssertEqual(folders[0], folder1) + XCTAssertEqual(folders[1], folder2) + XCTAssertEqual(folders[2], folder3) + + // Update the folder title and parent: + + let folderToMove = folder1 + folderToMove.title = #function + bookmarkStore.update(folder: folder1, andMoveToParent: .parent(uuid: folder2.id)) + let expectedFolderAfterMove = BookmarkFolder(id: folder1.id, title: folder1.title, parentFolderUUID: folder2.id, children: folder1.children) + + // Check the new bookmark folders order: + + let newFolders = try await bookmarkStore.loadAll(type: .topLevelEntities).get().compactMap { $0 as? BookmarkFolder } + + XCTAssertEqual(newFolders.count, 2) + XCTAssertEqual(newFolders[0].id, folder2.id) + XCTAssertEqual(newFolders[0].children, [expectedFolderAfterMove]) + XCTAssertEqual(newFolders[1], folder3) + } + + func testWhenMovingBookmarkFolderToSubfolder_ThenBookmarkFolderLocationIsUpdated() async throws { + let context = container.viewContext + let bookmarkStore = LocalBookmarkStore(context: context) + + let folder1 = BookmarkFolder(id: UUID().uuidString, title: "Folder 1", parentFolderUUID: "bookmarks_root") + let folder2 = BookmarkFolder(id: UUID().uuidString, title: "Folder 2", parentFolderUUID: "bookmarks_root") + + // Save the initial bookmarks state: + + _ = await bookmarkStore.save(folder: folder1, parent: nil) + _ = await bookmarkStore.save(folder: folder2, parent: nil) + + // Fetch persisted bookmark folders back from the store: + + let folders = try await bookmarkStore.loadAll(type: .topLevelEntities).get().compactMap { $0 as? BookmarkFolder } + + XCTAssertEqual(folders.count, 2) + XCTAssertEqual(folders.first, folder1) + XCTAssertEqual(folders.last, folder2) + + // Update the folder parent: + + _ = await bookmarkStore.move(objectUUIDs: [folder2.id], toIndex: nil, withinParentFolder: .parent(uuid: folder1.id)) + let expectedChildFolderAfterMove = BookmarkFolder(id: folder2.id, title: folder2.title, parentFolderUUID: folder1.id, children: folder2.children) + let expectedParentFolderAfterMove = BookmarkFolder(id: folder1.id, title: folder1.title, parentFolderUUID: folder1.parentFolderUUID, children: [expectedChildFolderAfterMove]) + + // Check the new bookmark folders order: + + let newFolders = try await bookmarkStore.loadAll(type: .topLevelEntities).get().compactMap { $0 as? BookmarkFolder } + + XCTAssertEqual(newFolders.count, 1) + XCTAssertEqual(newFolders.first, expectedParentFolderAfterMove) + XCTAssertEqual(newFolders.first?.children, [expectedChildFolderAfterMove]) + } + + func testWhenMovingBookmarkFolderToRootFolder_ThenBookmarkFolderLocationIsUpdated() async throws { + let context = container.viewContext + let bookmarkStore = LocalBookmarkStore(context: context) + + let folder2Id = UUID().uuidString + let folder1 = BookmarkFolder(id: UUID().uuidString, title: "Folder 1", parentFolderUUID: folder2Id) + let folder2 = BookmarkFolder(id: folder2Id, title: "Folder 2", parentFolderUUID: "bookmarks_root", children: [folder1]) + + // Save the initial bookmarks state: + + _ = await bookmarkStore.save(folder: folder2, parent: nil) + _ = await bookmarkStore.save(folder: folder1, parent: folder2) + + // Fetch persisted bookmark folders back from the store: + + let folders = try await bookmarkStore.loadAll(type: .topLevelEntities).get().compactMap { $0 as? BookmarkFolder } + + XCTAssertEqual(folders.count, 1) + XCTAssertEqual(folders.first, folder2) + XCTAssertEqual(folders.first?.children, [folder1]) + + // Update the folder parent: + + _ = await bookmarkStore.move(objectUUIDs: [folder1.id], toIndex: 0, withinParentFolder: .root) + + // Check the new bookmark folders order: + + let newFolders = try await bookmarkStore.loadAll(type: .topLevelEntities).get().compactMap { $0 as? BookmarkFolder } + let expectedFolder1AfterMove = BookmarkFolder(id: folder1.id, title: folder1.title, parentFolderUUID: "bookmarks_root", children: folder1.children) + let expectedFolder2AfterMove = BookmarkFolder(id: folder2.id, title: folder2.title, parentFolderUUID: "bookmarks_root", children: []) + + XCTAssertEqual(newFolders.count, 2) + XCTAssertEqual(newFolders.first, expectedFolder1AfterMove) + XCTAssertEqual(newFolders.last, expectedFolder2AfterMove) + XCTAssertEqual(newFolders.last?.children, []) + } + // MARK: Favorites func testThatTopLevelEntitiesDoNotContainFavoritesFolder() async { diff --git a/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogCoordinatorViewModelTests.swift b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogCoordinatorViewModelTests.swift new file mode 100644 index 0000000000..53072513c4 --- /dev/null +++ b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogCoordinatorViewModelTests.swift @@ -0,0 +1,170 @@ +// +// AddEditBookmarkDialogCoordinatorViewModelTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +import Combine +@testable import DuckDuckGo_Privacy_Browser + +@MainActor +final class AddEditBookmarkDialogCoordinatorViewModelTests: XCTestCase { + private var sut: AddEditBookmarkDialogCoordinatorViewModel! + private var bookmarkViewModelMock: AddEditBookmarkDialogViewModelMock! + private var bookmarkFolderViewModelMock: AddEditBookmarkFolderDialogViewModelMock! + private var cancellables: Set! + + override func setUpWithError() throws { + try super.setUpWithError() + + cancellables = [] + bookmarkViewModelMock = .init() + bookmarkFolderViewModelMock = .init() + sut = .init(bookmarkModel: bookmarkViewModelMock, folderModel: bookmarkFolderViewModelMock) + } + + override func tearDownWithError() throws { + cancellables = nil + bookmarkViewModelMock = nil + bookmarkFolderViewModelMock = nil + sut = nil + try super.tearDownWithError() + } + + func testShouldReturnViewStateBookmarkWhenInit() { + XCTAssertEqual(sut.viewState, .bookmark) + } + + func testShouldReturnViewStateBookmarkWhenDismissActionIsCalled() { + // GIVEN + sut.addFolderAction() + XCTAssertEqual(sut.viewState, .folder) + + // WHEN + sut.dismissAction() + + // THEN + XCTAssertEqual(sut.viewState, .bookmark) + + } + + func testShouldSetSelectedFolderOnFolderViewModelAndReturnFolderViewStateWhenAddFolderActionIsCalled() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: "Folder") + bookmarkViewModelMock.selectedFolder = folder + XCTAssertNil(bookmarkFolderViewModelMock.selectedFolder) + + // WHEN + sut.addFolderAction() + + // THEN + XCTAssertEqual(bookmarkFolderViewModelMock.selectedFolder, folder) + } + + func testShouldReceiveEventsWhenBookmarkModelChanges() { + // GIVEN + let expectation = self.expectation(description: #function) + var didCallChangeValue = false + sut.objectWillChange.sink { _ in + didCallChangeValue = true + expectation.fulfill() + } + .store(in: &cancellables) + + // WHEN + sut.bookmarkModel.objectWillChange.send() + + // THEN + waitForExpectations(timeout: 1.0) + XCTAssertTrue(didCallChangeValue) + } + + func testShouldReceiveEventsWhenBookmarkFolderModelChanges() { + // GIVEN + let expectation = self.expectation(description: #function) + var didCallChangeValue = false + sut.objectWillChange.sink { _ in + didCallChangeValue = true + expectation.fulfill() + } + .store(in: &cancellables) + + // WHEN + sut.folderModel.objectWillChange.send() + + // THEN + waitForExpectations(timeout: 1.0) + XCTAssertTrue(didCallChangeValue) + } + + func testShouldSetSelectedFolderOnBookmarkViewModelWhenAddFolderPublisherSendsEvent() { + // GIVEN + let expectation = self.expectation(description: #function) + bookmarkViewModelMock.selectedFolderExpectation = expectation + let folder = BookmarkFolder(id: "ABCDE", title: #function) + XCTAssertNil(bookmarkViewModelMock.selectedFolder) + + // WHEN + sut.folderModel.subject.send(folder) + + // THEN + waitForExpectations(timeout: 1.0) + XCTAssertEqual(bookmarkViewModelMock.selectedFolder, folder) + } + +} + +final class AddEditBookmarkDialogViewModelMock: BookmarkDialogEditing { + var bookmarkName: String = "" + var bookmarkURLPath: String = "" + var isBookmarkFavorite: Bool = false + var isURLFieldHidden: Bool = false + var title: String = "" + var folders: [DuckDuckGo_Privacy_Browser.FolderViewModel] = [] + var selectedFolder: DuckDuckGo_Privacy_Browser.BookmarkFolder? { + didSet { + selectedFolderExpectation?.fulfill() + } + } + var cancelActionTitle: String = "" + var isOtherActionDisabled: Bool = false + var defaultActionTitle: String = "" + var isDefaultActionDisabled: Bool = false + + func cancel(dismiss: () -> Void) {} + func addOrSave(dismiss: () -> Void) {} + + var selectedFolderExpectation: XCTestExpectation? +} + +final class AddEditBookmarkFolderDialogViewModelMock: BookmarkFolderDialogEditing { + let subject = PassthroughSubject() + + var addFolderPublisher: AnyPublisher { + subject.eraseToAnyPublisher() + } + var folderName: String = "" + var title: String = "" + var folders: [DuckDuckGo_Privacy_Browser.FolderViewModel] = [] + var selectedFolder: DuckDuckGo_Privacy_Browser.BookmarkFolder? + var cancelActionTitle: String = "" + var isOtherActionDisabled: Bool = false + var defaultActionTitle: String = "" + var isDefaultActionDisabled: Bool = false + + func cancel(dismiss: () -> Void) {} + func addOrSave(dismiss: () -> Void) {} +} diff --git a/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogViewModelTests.swift b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogViewModelTests.swift new file mode 100644 index 0000000000..b409df5b9c --- /dev/null +++ b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogViewModelTests.swift @@ -0,0 +1,745 @@ +// +// AddEditBookmarkDialogViewModelTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +@MainActor +final class AddEditBookmarkDialogViewModelTests: XCTestCase { + private var bookmarkManager: LocalBookmarkManager! + private var bookmarkStoreMock: BookmarkStoreMock! + + override func setUpWithError() throws { + try super.setUpWithError() + bookmarkStoreMock = BookmarkStoreMock() + bookmarkStoreMock.bookmarks = [BookmarkFolder.mock] + bookmarkManager = .init(bookmarkStore: bookmarkStoreMock, faviconManagement: FaviconManagerMock()) + bookmarkManager.loadBookmarks() + } + + override func tearDownWithError() throws { + bookmarkStoreMock = nil + bookmarkManager = nil + try super.tearDownWithError() + } + + // MARK: - Copy + + func testReturnAddBookmarkTitleWhenModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.title + + // THEN + XCTAssertEqual(title, UserText.Bookmarks.Dialog.Title.addBookmark) + } + + func testReturnEditBookmarkTitleWhenModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: .mock), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.title + + // THEN + XCTAssertEqual(title, UserText.Bookmarks.Dialog.Title.editBookmark) + } + + func testReturnCancelActionTitleWhenModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.cancelActionTitle + + // THEN + XCTAssertEqual(title, UserText.cancel) + } + + func testReturnCancelActionTitleWhenModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: .mock), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.cancelActionTitle + + // THEN + XCTAssertEqual(title, UserText.cancel) + } + + func testReturnAddBookmarkActionTitleWhenModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.defaultActionTitle + + // THEN + XCTAssertEqual(title, UserText.Bookmarks.Dialog.Action.addBookmark) + } + + func testReturnSaveActionTitleWhenModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: .mock), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.defaultActionTitle + + // THEN + XCTAssertEqual(title, UserText.save) + } + + // MARK: State + + func testShouldSetBookmarkNameToEmptyWhenInitModeIsAddAndTabInfoIsNil() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.bookmarkName + + // THEN + XCTAssertTrue(result.isEmpty) + } + + func testShouldSetNameAndURLToValueWhenInitModeIsAddTabInfoIsNotNilAndURLIsNotAlreadyBookmarked() { + // GIVEN + let tab = Tab(content: .url(URL.duckDuckGo, source: .link), title: "Test") + let sut = AddEditBookmarkDialogViewModel(mode: .add(tabWebsite: WebsiteInfo(tab)), bookmarkManager: bookmarkManager) + + // WHEN + let name = sut.bookmarkName + let url = sut.bookmarkURLPath + + // THEN + XCTAssertEqual(name, "Test") + XCTAssertEqual(url, URL.duckDuckGo.absoluteString) + } + + func testShouldSetNameAndURLToEmptyWhenInitModeIsAddTabInfoIsNotNilAndURLIsAlreadyBookmarked() throws { + // GIVEN + let tab = Tab(content: .url(URL.duckDuckGo, source: .link), title: "Test") + let websiteInfo = try XCTUnwrap(WebsiteInfo(tab)) + let bookmark = Bookmark(id: "1", url: websiteInfo.url.absoluteString, title: websiteInfo.title ?? "", isFavorite: false) + bookmarkStoreMock.bookmarks = [bookmark] + bookmarkManager.loadBookmarks() + let sut = AddEditBookmarkDialogViewModel(mode: .add(tabWebsite: WebsiteInfo(tab)), bookmarkManager: bookmarkManager) + + // WHEN + let name = sut.bookmarkName + let url = sut.bookmarkURLPath + + // THEN + XCTAssertEqual(name, "") + XCTAssertEqual(url, "") + } + + func testShouldSetBookmarkNameToValueWhenInitAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.bookmarkName + + // THEN + XCTAssertEqual(result, #function) + } + + func testShouldSetFoldersFromBookmarkListWhenInitAndModeIsAdd() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + bookmarkStoreMock.bookmarks = [folder] + bookmarkManager.loadBookmarks() + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.folders + + // THEN + XCTAssertEqual(result.count, 1) + XCTAssertEqual(result.first?.entity, folder) + } + + func testShouldSetFoldersFromBookmarkListWhenInitAndModeIsEdit() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + bookmarkStoreMock.bookmarks = [folder] + bookmarkManager.loadBookmarks() + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: .mock), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.folders + + // THEN + XCTAssertEqual(result.count, 1) + XCTAssertEqual(result.first?.entity, folder) + } + + func testShouldSetSelectedFolderToNilWhenBookmarkParentFolderIsNilAndModeIsAdd() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + bookmarkStoreMock.bookmarks = [folder] + bookmarkManager.loadBookmarks() + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.selectedFolder + + // THEN + XCTAssertNil(result) + } + + func testShouldSetSelectedFolderToValueWhenParentFolderIsNotNilAndModeIsAdd() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + bookmarkStoreMock.bookmarks = [folder] + bookmarkManager.loadBookmarks() + let sut = AddEditBookmarkDialogViewModel(mode: .add(parentFolder: folder), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.selectedFolder + + // THEN + XCTAssertEqual(result, folder) + } + + func testShouldSetSelectedFolderToNilWhenParentFolderIsNilAndModeIsEdit() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false, parentFolderUUID: "2") + bookmarkStoreMock.bookmarks = [folder] + bookmarkManager.loadBookmarks() + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.selectedFolder + + // THEN + XCTAssertNil(result) + } + + func testShouldSetSelectedFolderToValueWhenParentFolderIsNotNilAndModeIsEdit() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false, parentFolderUUID: "1") + bookmarkStoreMock.bookmarks = [folder] + bookmarkManager.loadBookmarks() + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.selectedFolder + + // THEN + XCTAssertEqual(result, folder) + } + + func testShouldSetIsBookmarkFavoriteToTrueWhenModeIsAddAndShouldPresetFavoriteIsTrue() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(shouldPresetFavorite: true), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.isBookmarkFavorite + + // THEN + XCTAssertTrue(result) + } + + func testShouldNotSetIsBookmarkFavoriteToTrueWhenModeIsAddAndShouldPresetFavoriteIsFalse() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(shouldPresetFavorite: false), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.isBookmarkFavorite + + // THEN + XCTAssertFalse(result) + } + + // MARK: - Actions + + func testReturnIsCancelActionDisabledFalseWhenModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.isOtherActionDisabled + + // THEN + XCTAssertFalse(result) + } + + func testReturnIsCancelActionDisabledFalseWhenModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: .mock), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.isOtherActionDisabled + + // THEN + XCTAssertFalse(result) + } + + func testReturnIsDefaultActionButtonDisabledTrueWhenBookmarkNameIsEmptyAndModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + sut.bookmarkName = "" + sut.bookmarkURLPath = URL.duckDuckGo.absoluteString + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertTrue(result) + } + + func testReturnIsDefaultActionButtonDisabledTrueWhenBookmarkNameIsEmptyAndModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: .mock), bookmarkManager: bookmarkManager) + sut.bookmarkName = "" + sut.bookmarkURLPath = URL.duckDuckGo.absoluteString + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertTrue(result) + } + + func testReturnIsDefaultActionButtonDisabledFalseWhenBookmarkNameIsNotEmptyAndModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + sut.bookmarkName = " DuckDuckGo " + sut.bookmarkURLPath = URL.duckDuckGo.absoluteString + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertFalse(result) + } + + func testReturnIsDefaultActionButtonDisabledFalseWhenBookmarkNameIsNotEmptyAndModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: .mock), bookmarkManager: bookmarkManager) + sut.bookmarkName = " DuckDuckGo " + sut.bookmarkURLPath = URL.duckDuckGo.absoluteString + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertFalse(result) + } + + func testReturnIsDefaultActionButtonDisabledTrueWhenBookmarURLIsEmptyAndModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + sut.bookmarkName = "DuckDuckGo" + sut.bookmarkURLPath = "" + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertTrue(result) + } + + func testReturnIsDefaultActionButtonDisabledTrueWhenBookmarkURLIsEmptyAndModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: .mock), bookmarkManager: bookmarkManager) + sut.bookmarkName = "DuckDuckGo" + sut.bookmarkURLPath = "" + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertTrue(result) + } + + func testReturnIsDefaultActionButtonDisabledFalseWhenBookmarkURLIsNotEmptyAndModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + sut.bookmarkName = " DuckDuckGo " + sut.bookmarkURLPath = URL.duckDuckGo.absoluteString + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertFalse(result) + } + + func testReturnIsDefaultActionButtonDisabledFalseWhenBookmarkURLIsNotEmptyAndModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: .mock), bookmarkManager: bookmarkManager) + sut.bookmarkName = " DuckDuckGo " + sut.bookmarkURLPath = URL.duckDuckGo.absoluteString + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertFalse(result) + } + + func testShouldCallDismissWhenCancelIsCalled() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + var didCallDismiss = false + + // WHEN + sut.cancel { + didCallDismiss = true + } + + // THEN + XCTAssertTrue(didCallDismiss) + } + + func testShouldCallDismissWhenAddOrSaveIsCalled() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + sut.bookmarkName = "DuckDuckGo" + sut.bookmarkURLPath = URL.duckDuckGo.absoluteString + var didCallDismiss = false + + // WHEN + sut.addOrSave { + didCallDismiss = true + } + + // THEN + XCTAssertTrue(didCallDismiss) + } + + func testShouldAskBookmarkStoreToSaveBookmarkWhenModeIsAddAndURLIsNotAnExistingBookmark() { + // GIVEN + let folder = BookmarkFolder(id: #file, title: #function) + let existingBookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DuckDuckGo", isFavorite: true) + bookmarkStoreMock.bookmarks = [existingBookmark] + bookmarkManager.loadBookmarks() + let sut = AddEditBookmarkDialogViewModel(mode: .add(parentFolder: folder), bookmarkManager: bookmarkManager) + sut.bookmarkName = "DDG" + sut.bookmarkURLPath = URL.duckDuckGo.absoluteString + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + XCTAssertNil(bookmarkStoreMock.capturedParentFolder) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertTrue(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertTrue(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertEqual(bookmarkStoreMock.capturedObjectUUIDs, [existingBookmark.id]) + XCTAssertEqual(bookmarkStoreMock.capturedParentFolderType, .parent(uuid: folder.id)) + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertEqual(bookmarkStoreMock.capturedBookmark?.title, "DDG") + XCTAssertEqual(bookmarkStoreMock.capturedBookmark?.url, URL.duckDuckGo.absoluteString) + XCTAssertNil(bookmarkStoreMock.capturedParentFolder) + } + + func testShouldAskBookmarkStoreToUpdateBookmarkWhenModeIsAddAndURLIsAnExistingBookmark() { + // GIVEN + let folder = BookmarkFolder(id: #file, title: #function) + let sut = AddEditBookmarkDialogViewModel(mode: .add(parentFolder: folder), bookmarkManager: bookmarkManager) + sut.bookmarkName = #function + sut.bookmarkURLPath = URL.duckDuckGo.absoluteString + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + XCTAssertNil(bookmarkStoreMock.capturedParentFolder) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertTrue(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertEqual(bookmarkStoreMock.capturedBookmark?.title, #function) + XCTAssertEqual(bookmarkStoreMock.capturedBookmark?.url, URL.duckDuckGo.absoluteString) + XCTAssertEqual(bookmarkStoreMock.capturedParentFolder, folder) + } + + func testShouldAskBookmarkStoreToUpdateBookmarkWhenURLIsUpdatedAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false) + let expectedBookmark = Bookmark(id: "1", url: URL.exti, title: #function, isFavorite: false) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + sut.bookmarkURLPath = expectedBookmark.url + bookmarkStoreMock.bookmarks = [bookmark] + bookmarkManager.loadBookmarks() + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertTrue(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertEqual(bookmarkStoreMock.capturedBookmark, expectedBookmark) + } + + func testShouldNotAskBookmarkStoreToUpdateBookmarkWhenURLIsNotUpdatedAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + sut.bookmarkURLPath = URL.duckDuckGo.absoluteString + bookmarkStoreMock.bookmarks = [bookmark] + bookmarkManager.loadBookmarks() + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + } + + func testShouldAskBookmarkStoreToUpdateBookmarkWhenNameIsUpdatedAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false) + let expectedBookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DuckDuckGo", isFavorite: false) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + bookmarkStoreMock.bookmarks = [bookmark] + bookmarkManager.loadBookmarks() + sut.bookmarkName = expectedBookmark.title + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertTrue(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertEqual(bookmarkStoreMock.capturedBookmark, expectedBookmark) + } + + func testShouldNotAskBookmarkStoreToUpdateBookmarkWhenNameIsNotUpdatedAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + bookmarkStoreMock.bookmarks = [bookmark] + bookmarkManager.loadBookmarks() + sut.bookmarkName = #function + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + } + + func testShouldAskBookmarkStoreToUpdateBookmarkWhenIsFavoriteIsUpdatedAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false) + let expectedBookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: true) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + bookmarkStoreMock.bookmarks = [bookmark] + bookmarkManager.loadBookmarks() + sut.isBookmarkFavorite = expectedBookmark.isFavorite + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertTrue(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertEqual(bookmarkStoreMock.capturedBookmark, expectedBookmark) + } + + func testShouldNotAskBookmarkStoreToUpdateBookmarkWhenIsFavoriteIsNotUpdatedAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + bookmarkStoreMock.bookmarks = [bookmark] + bookmarkManager.loadBookmarks() + sut.isBookmarkFavorite = false + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + } + + func testShouldAskBookmarkStoreToUpdateBookmarkWhenURLAndTitleAndIsFavoriteIsUpdatedAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false) + let expectedBookmark = Bookmark(id: "1", url: URL.exti, title: "DDG", isFavorite: true) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + bookmarkStoreMock.bookmarks = [bookmark] + bookmarkManager.loadBookmarks() + sut.bookmarkURLPath = expectedBookmark.url + sut.bookmarkName = expectedBookmark.title + sut.isBookmarkFavorite = expectedBookmark.isFavorite + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertTrue(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertEqual(bookmarkStoreMock.capturedBookmark, expectedBookmark) + } + + func testShouldAskBookmarkStoreToMoveBookmarkWhenSelectedFolderIsDifferentFromOriginalFolderAndModeIsEdit() { + // GIVEN + let folder = BookmarkFolder(id: "ABCDE", title: "Test Folder") + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + bookmarkStoreMock.bookmarks = [folder, bookmark] + bookmarkManager.loadBookmarks() + sut.selectedFolder = folder + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertNil(bookmarkStoreMock.capturedObjectUUIDs) + XCTAssertNil(bookmarkStoreMock.capturedParentFolderType) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertTrue(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertEqual(bookmarkStoreMock.capturedObjectUUIDs, [bookmark.id]) + XCTAssertEqual(bookmarkStoreMock.capturedParentFolderType, .parent(uuid: folder.id)) + } + + func testShouldAskBookmarkStoreToMoveBookmarkWhenSelectedFolderIsNilAndOriginalFolderIsNotRootFolderAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false, parentFolderUUID: "ABCDE") + let folder = BookmarkFolder(id: "ABCDE", title: "Test Folder", children: [bookmark]) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + bookmarkStoreMock.bookmarks = [folder, bookmark] + bookmarkManager.loadBookmarks() + sut.selectedFolder = nil + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertNil(bookmarkStoreMock.capturedObjectUUIDs) + XCTAssertNil(bookmarkStoreMock.capturedParentFolderType) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertTrue(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertEqual(bookmarkStoreMock.capturedObjectUUIDs, [bookmark.id]) + XCTAssertEqual(bookmarkStoreMock.capturedParentFolderType, .root) + } + + func testShouldNotAskBookmarkStoreToMoveBookmarkWhenSelectedFolderIsNotDifferentFromOriginalFolderAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false, parentFolderUUID: "ABCDE") + let folder = BookmarkFolder(id: "ABCDE", title: "Test Folder", children: [bookmark]) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + bookmarkStoreMock.bookmarks = [bookmark] + bookmarkManager.loadBookmarks() + sut.selectedFolder = folder + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertNil(bookmarkStoreMock.capturedObjectUUIDs) + XCTAssertNil(bookmarkStoreMock.capturedParentFolderType) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertNil(bookmarkStoreMock.capturedObjectUUIDs) + XCTAssertNil(bookmarkStoreMock.capturedParentFolderType) + } + + func testShouldNotAskBookmarkStoreToMoveBookmarkWhenSelectedFolderIsNilAndOriginalFolderIsRootAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false, parentFolderUUID: "bookmarks_root") + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + bookmarkStoreMock.bookmarks = [bookmark] + bookmarkManager.loadBookmarks() + sut.selectedFolder = nil + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertNil(bookmarkStoreMock.capturedObjectUUIDs) + XCTAssertNil(bookmarkStoreMock.capturedParentFolderType) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertNil(bookmarkStoreMock.capturedObjectUUIDs) + XCTAssertNil(bookmarkStoreMock.capturedParentFolderType) + } +} diff --git a/UnitTests/Bookmarks/ViewModels/AddEditBookmarkFolderDialogViewModelTests.swift b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkFolderDialogViewModelTests.swift new file mode 100644 index 0000000000..a48f18d2ab --- /dev/null +++ b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkFolderDialogViewModelTests.swift @@ -0,0 +1,425 @@ +// +// AddEditBookmarkFolderDialogViewModelTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +@MainActor +final class AddEditBookmarkFolderDialogViewModelTests: XCTestCase { + private var bookmarkManager: LocalBookmarkManager! + private var bookmarkStoreMock: BookmarkStoreMock! + + override func setUpWithError() throws { + try super.setUpWithError() + bookmarkStoreMock = BookmarkStoreMock() + bookmarkStoreMock.bookmarks = [BookmarkFolder.mock] + bookmarkManager = .init(bookmarkStore: bookmarkStoreMock, faviconManagement: FaviconManagerMock()) + bookmarkManager.loadBookmarks() + } + + override func tearDownWithError() throws { + bookmarkStoreMock = nil + bookmarkManager = nil + try super.tearDownWithError() + } + + // MARK: - Copy + + func testReturnAddBookmarkFolderTitleWhenModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.title + + // THEN + XCTAssertEqual(title, UserText.Bookmarks.Dialog.Title.addFolder) + } + + func testReturnEditBookmarkFolderTitleWhenModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: .mock, parentFolder: nil), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.title + + // THEN + XCTAssertEqual(title, UserText.Bookmarks.Dialog.Title.editFolder) + } + + func testReturnCancelActionTitleWhenModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.cancelActionTitle + + // THEN + XCTAssertEqual(title, UserText.cancel) + } + + func testReturnCancelActionTitleWhenModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: .mock, parentFolder: nil), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.cancelActionTitle + + // THEN + XCTAssertEqual(title, UserText.cancel) + } + + func testReturnAddBookmarkFolderActionTitleWhenModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.defaultActionTitle + + // THEN + XCTAssertEqual(title, UserText.Bookmarks.Dialog.Action.addFolder) + } + + func testReturnSaveActionTitleWhenModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: .mock, parentFolder: nil), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.defaultActionTitle + + // THEN + XCTAssertEqual(title, UserText.save) + } + + // MARK: State + + func testShouldSetFolderNameToEmptyWhenInitAndModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(parentFolder: .mock), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.folderName + + // THEN + XCTAssertTrue(result.isEmpty) + } + + func testShouldSetFolderNameToValueWhenInitAndModeIsEdit() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: folder, parentFolder: .mock), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.folderName + + // THEN + XCTAssertEqual(result, #function) + } + + func testShouldSetFoldersFromBookmarkListWhenInitAndModeIsAdd() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + bookmarkStoreMock.bookmarks = [folder] + bookmarkManager.loadBookmarks() + let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.folders + + // THEN + XCTAssertEqual(result.count, 1) + XCTAssertEqual(result.first?.entity, folder) + } + + func testShouldSetFoldersFromBookmarkListWhenInitAndModeIsEdit() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + bookmarkStoreMock.bookmarks = [folder] + bookmarkManager.loadBookmarks() + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: .mock, parentFolder: nil), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.folders + + // THEN + XCTAssertEqual(result.count, 1) + XCTAssertEqual(result.first?.entity, folder) + } + + func testShouldSetSelectedFolderToNilWhenParentFolderIsNilAndModeIsAdd() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + bookmarkStoreMock.bookmarks = [folder] + let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.selectedFolder + + // THEN + XCTAssertNil(result) + } + + func testShouldSetSelectedFolderToValueWhenParentFolderIsNotNilAndModeIsAdd() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + bookmarkStoreMock.bookmarks = [folder] + let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(parentFolder: .mock), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.selectedFolder + + // THEN + XCTAssertEqual(result, .mock) + } + + func testShouldSetSelectedFolderToNilWhenParentFolderIsNilAndModeIsEdit() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + bookmarkStoreMock.bookmarks = [folder] + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: folder, parentFolder: nil), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.selectedFolder + + // THEN + XCTAssertNil(result) + } + + func testShouldSetSelectedFolderToValueWhenParentFolderIsNotNilAndModeIsEdit() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + bookmarkStoreMock.bookmarks = [folder] + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: folder, parentFolder: .mock), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.selectedFolder + + // THEN + XCTAssertEqual(result, .mock) + } + + // MARK: - Actions + + func testReturnIsCancelActionDisabledFalseWhenModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.isOtherActionDisabled + + // THEN + XCTAssertFalse(result) + } + + func testReturnIsCancelActionDisabledFalseWhenModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: .mock, parentFolder: nil), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.isOtherActionDisabled + + // THEN + XCTAssertFalse(result) + } + + func testReturnIsDefaultActionButtonDisabledTrueWhenFolderNameIsEmptyAndModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + sut.folderName = "" + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertTrue(result) + } + + func testReturnIsDefaultActionButtonDisabledTrueWhenFolderNameIsEmptyAndModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: .mock, parentFolder: nil), bookmarkManager: bookmarkManager) + sut.folderName = "" + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertTrue(result) + } + + func testReturnIsDefaultActionButtonDisabledFalseWhenFolderNameIsNotEmptyAndModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + sut.folderName = " Test " + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertFalse(result) + } + + func testReturnIsDefaultActionButtonDisabledFalseWhenFolderNameIsNotEmptyAndModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: .mock, parentFolder: nil), bookmarkManager: bookmarkManager) + sut.folderName = " Test " + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertFalse(result) + } + + func testShouldCallDismissWhenCancelIsCalled() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + var didCallDismiss = false + + // WHEN + sut.cancel { + didCallDismiss = true + } + + // THEN + XCTAssertTrue(didCallDismiss) + } + + func testShouldCallDismissWhenAddOrSaveIsCalled() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + var didCallDismiss = false + sut.folderName = "DuckDuckGo" + + // WHEN + sut.addOrSave { + didCallDismiss = true + } + + // THEN + XCTAssertTrue(didCallDismiss) + } + + func testShouldAskBookmarkStoreToSaveFolderWhenAddOrSaveIsCalledAndModeIsAdd() { + // GIVEN + let folder = BookmarkFolder(id: #file, title: #function) + let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(parentFolder: folder), bookmarkManager: bookmarkManager) + sut.folderName = #function + XCTAssertFalse(bookmarkStoreMock.saveFolderCalled) + XCTAssertNil(bookmarkStoreMock.capturedFolder) + XCTAssertNil(bookmarkStoreMock.capturedParentFolder) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.updateFolderCalled) + XCTAssertTrue(bookmarkStoreMock.saveFolderCalled) + XCTAssertEqual(bookmarkStoreMock.capturedFolder?.title, sut.folderName) + XCTAssertEqual(bookmarkStoreMock.capturedParentFolder, folder) + } + + func testShouldAskBookmarkStoreToUpdateFolderWhenNameIsChanged() { + // GIVEN + let folder = BookmarkFolder(id: #file, title: #function) + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: folder, parentFolder: nil), bookmarkManager: bookmarkManager) + sut.folderName = "TEST" + XCTAssertFalse(bookmarkStoreMock.updateFolderCalled) + XCTAssertNil(bookmarkStoreMock.capturedFolder) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertTrue(bookmarkStoreMock.updateFolderCalled) + XCTAssertEqual(bookmarkStoreMock.capturedFolder?.title, sut.folderName) + } + + func testShouldNotAskBookmarkStoreToUpdateFolderWhenNameIsNotChanged() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: .mock, parentFolder: nil), bookmarkManager: bookmarkManager) + XCTAssertFalse(bookmarkStoreMock.updateFolderCalled) + XCTAssertNil(bookmarkStoreMock.capturedFolder) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveFolderCalled) + XCTAssertFalse(bookmarkStoreMock.updateFolderCalled) + XCTAssertNil(bookmarkStoreMock.capturedFolder?.title) + } + + func testShouldAskBookmarkStoreToMoveFolderToSubfolderWhenSelectedFolderIsDifferentFromOriginalFolder() { + // GIVEN + let location = BookmarkFolder(id: #file, title: #function) + let folder = BookmarkFolder.mock + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: folder, parentFolder: nil), bookmarkManager: bookmarkManager) + sut.selectedFolder = location + XCTAssertFalse(bookmarkStoreMock.updateFolderAndMoveToParentCalled) + XCTAssertNil(bookmarkStoreMock.capturedFolder) + XCTAssertNil(bookmarkStoreMock.capturedParentFolderType) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveFolderCalled) + XCTAssertTrue(bookmarkStoreMock.updateFolderAndMoveToParentCalled) + XCTAssertEqual(bookmarkStoreMock.capturedFolder, folder) + XCTAssertEqual(bookmarkStoreMock.capturedParentFolderType, .parent(uuid: #file)) + } + + func testShouldAskBookmarkStoreToMoveFolderToRootFolderWhenSelectedFolderIsDifferentFromOriginalFolder() { + // GIVEN + let folder = BookmarkFolder.mock + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: folder, parentFolder: .mock), bookmarkManager: bookmarkManager) + sut.selectedFolder = nil + XCTAssertFalse(bookmarkStoreMock.updateFolderAndMoveToParentCalled) + XCTAssertNil(bookmarkStoreMock.capturedFolder) + XCTAssertNil(bookmarkStoreMock.capturedParentFolderType) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveFolderCalled) + XCTAssertTrue(bookmarkStoreMock.updateFolderAndMoveToParentCalled) + XCTAssertEqual(bookmarkStoreMock.capturedFolder, folder) + XCTAssertEqual(bookmarkStoreMock.capturedParentFolderType, .root) + } + + func testShouldNotAskBookmarkStoreToMoveFolderWhenSelectedFolderIsNotDifferentFromOriginalFolder() { + // GIVEN + let folder = BookmarkFolder.mock + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: folder, parentFolder: nil), bookmarkManager: bookmarkManager) + sut.selectedFolder = nil + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertNil(bookmarkStoreMock.capturedObjectUUIDs) + XCTAssertNil(bookmarkStoreMock.capturedParentFolderType) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveFolderCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertNil(bookmarkStoreMock.capturedObjectUUIDs) + XCTAssertNil(bookmarkStoreMock.capturedParentFolderType) + } + +} diff --git a/UnitTests/BookmarksBar/ViewModel/BookmarksBarViewModelTests.swift b/UnitTests/BookmarksBar/ViewModel/BookmarksBarViewModelTests.swift index 32d827186b..269980c361 100644 --- a/UnitTests/BookmarksBar/ViewModel/BookmarksBarViewModelTests.swift +++ b/UnitTests/BookmarksBar/ViewModel/BookmarksBarViewModelTests.swift @@ -91,6 +91,199 @@ class BookmarksBarViewModelTests: XCTestCase { XCTAssert(bookmarksBarViewModel.clippedItems.isEmpty) } + // MARK: - Bookmarks Delegate + + func testWhenItemFiresClickedActionThenDelegateReceivesClickItemActionAndPreventClickIsFalse() { + // GIVEN + let sut = BookmarksBarViewModel(bookmarkManager: createMockBookmarksManager(), tabCollectionViewModel: .mock()) + let collectionViewItem = BookmarksBarCollectionViewItem() + let delegateMock = BookmarksBarViewModelDelegateMock() + sut.delegate = delegateMock + XCTAssertFalse(delegateMock.didCallViewModelReceivedAction) + XCTAssertNil(delegateMock.capturedAction) + XCTAssertNil(delegateMock.capturedItem) + + // WHEN + sut.bookmarksBarCollectionViewItemClicked(collectionViewItem) + + // THEN + XCTAssertTrue(delegateMock.didCallViewModelReceivedAction) + XCTAssertEqual(delegateMock.capturedAction, .clickItem) + XCTAssertEqual(delegateMock.capturedItem, collectionViewItem) + + } + + func testWhenItemFiresOpenInNewTabActionThenDelegateReceivesOpenInNewTabAction() { + // GIVEN + let sut = BookmarksBarViewModel(bookmarkManager: createMockBookmarksManager(), tabCollectionViewModel: .mock()) + let collectionViewItem = BookmarksBarCollectionViewItem() + let delegateMock = BookmarksBarViewModelDelegateMock() + sut.delegate = delegateMock + XCTAssertFalse(delegateMock.didCallViewModelReceivedAction) + XCTAssertNil(delegateMock.capturedAction) + XCTAssertNil(delegateMock.capturedItem) + + // WHEN + sut.bookmarksBarCollectionViewItemOpenInNewTabAction(collectionViewItem) + + // THEN + XCTAssertTrue(delegateMock.didCallViewModelReceivedAction) + XCTAssertEqual(delegateMock.capturedAction, .openInNewTab) + XCTAssertEqual(delegateMock.capturedItem, collectionViewItem) + } + + func testWhenItemFiresOpenInNewWindowActionThenDelegateReceivesOpenInNewWindowAction() { + // GIVEN + let sut = BookmarksBarViewModel(bookmarkManager: createMockBookmarksManager(), tabCollectionViewModel: .mock()) + let collectionViewItem = BookmarksBarCollectionViewItem() + let delegateMock = BookmarksBarViewModelDelegateMock() + sut.delegate = delegateMock + XCTAssertFalse(delegateMock.didCallViewModelReceivedAction) + XCTAssertNil(delegateMock.capturedAction) + XCTAssertNil(delegateMock.capturedItem) + + // WHEN + sut.bookmarksBarCollectionViewItemOpenInNewWindowAction(collectionViewItem) + + // THEN + XCTAssertTrue(delegateMock.didCallViewModelReceivedAction) + XCTAssertEqual(delegateMock.capturedAction, .openInNewWindow) + XCTAssertEqual(delegateMock.capturedItem, collectionViewItem) + } + + func testWhenItemFiresToggleFavoritesActionThenDelegateReceivesToggleFavoritesAction() { + // GIVEN + let sut = BookmarksBarViewModel(bookmarkManager: createMockBookmarksManager(), tabCollectionViewModel: .mock()) + let collectionViewItem = BookmarksBarCollectionViewItem() + let delegateMock = BookmarksBarViewModelDelegateMock() + sut.delegate = delegateMock + XCTAssertFalse(delegateMock.didCallViewModelReceivedAction) + XCTAssertNil(delegateMock.capturedAction) + XCTAssertNil(delegateMock.capturedItem) + + // WHEN + sut.bookmarksBarCollectionViewItemToggleFavoritesAction(collectionViewItem) + + // THEN + XCTAssertTrue(delegateMock.didCallViewModelReceivedAction) + XCTAssertEqual(delegateMock.capturedAction, .toggleFavorites) + XCTAssertEqual(delegateMock.capturedItem, collectionViewItem) + } + + func testWhenItemFiresEditActionThenDelegateReceivesEditAction() { + // GIVEN + let sut = BookmarksBarViewModel(bookmarkManager: createMockBookmarksManager(), tabCollectionViewModel: .mock()) + let collectionViewItem = BookmarksBarCollectionViewItem() + let delegateMock = BookmarksBarViewModelDelegateMock() + sut.delegate = delegateMock + XCTAssertFalse(delegateMock.didCallViewModelReceivedAction) + XCTAssertNil(delegateMock.capturedAction) + XCTAssertNil(delegateMock.capturedItem) + + // WHEN + sut.bookmarksBarCollectionViewEditAction(collectionViewItem) + + // THEN + XCTAssertTrue(delegateMock.didCallViewModelReceivedAction) + XCTAssertEqual(delegateMock.capturedAction, .edit) + XCTAssertEqual(delegateMock.capturedItem, collectionViewItem) + } + + func testWhenItemFiresMoveToEndActionThenDelegateReceivesMoveToEndAction() { + // GIVEN + let sut = BookmarksBarViewModel(bookmarkManager: createMockBookmarksManager(), tabCollectionViewModel: .mock()) + let collectionViewItem = BookmarksBarCollectionViewItem() + let delegateMock = BookmarksBarViewModelDelegateMock() + sut.delegate = delegateMock + XCTAssertFalse(delegateMock.didCallViewModelReceivedAction) + XCTAssertNil(delegateMock.capturedAction) + XCTAssertNil(delegateMock.capturedItem) + + // WHEN + sut.bookmarksBarCollectionViewItemMoveToEndAction(collectionViewItem) + + // THEN + XCTAssertTrue(delegateMock.didCallViewModelReceivedAction) + XCTAssertEqual(delegateMock.capturedAction, .moveToEnd) + XCTAssertEqual(delegateMock.capturedItem, collectionViewItem) + } + + func testWhenItemFiresCopyBookmarkURLActionThenDelegateReceivesCopyBookmarkURLAction() { + // GIVEN + let sut = BookmarksBarViewModel(bookmarkManager: createMockBookmarksManager(), tabCollectionViewModel: .mock()) + let collectionViewItem = BookmarksBarCollectionViewItem() + let delegateMock = BookmarksBarViewModelDelegateMock() + sut.delegate = delegateMock + XCTAssertFalse(delegateMock.didCallViewModelReceivedAction) + XCTAssertNil(delegateMock.capturedAction) + XCTAssertNil(delegateMock.capturedItem) + + // WHEN + sut.bookmarksBarCollectionViewItemCopyBookmarkURLAction(collectionViewItem) + + // THEN + XCTAssertTrue(delegateMock.didCallViewModelReceivedAction) + XCTAssertEqual(delegateMock.capturedAction, .copyURL) + XCTAssertEqual(delegateMock.capturedItem, collectionViewItem) + } + + func testWhenItemFiresDeleteEntityActionThenDelegateReceivesDeleteEntityAction() { + // GIVEN + let sut = BookmarksBarViewModel(bookmarkManager: createMockBookmarksManager(), tabCollectionViewModel: .mock()) + let collectionViewItem = BookmarksBarCollectionViewItem() + let delegateMock = BookmarksBarViewModelDelegateMock() + sut.delegate = delegateMock + XCTAssertFalse(delegateMock.didCallViewModelReceivedAction) + XCTAssertNil(delegateMock.capturedAction) + XCTAssertNil(delegateMock.capturedItem) + + // WHEN + sut.bookmarksBarCollectionViewItemDeleteEntityAction(collectionViewItem) + + // THEN + XCTAssertTrue(delegateMock.didCallViewModelReceivedAction) + XCTAssertEqual(delegateMock.capturedAction, .deleteEntity) + XCTAssertEqual(delegateMock.capturedItem, collectionViewItem) + } + + func testWhenItemFiresAddEntityActionThenDelegateReceivesAddEntityAction() { + // GIVEN + let sut = BookmarksBarViewModel(bookmarkManager: createMockBookmarksManager(), tabCollectionViewModel: .mock()) + let collectionViewItem = BookmarksBarCollectionViewItem() + let delegateMock = BookmarksBarViewModelDelegateMock() + sut.delegate = delegateMock + XCTAssertFalse(delegateMock.didCallViewModelReceivedAction) + XCTAssertNil(delegateMock.capturedAction) + XCTAssertNil(delegateMock.capturedItem) + + // WHEN + sut.bookmarksBarCollectionViewItemAddEntityAction(collectionViewItem) + + // THEN + XCTAssertTrue(delegateMock.didCallViewModelReceivedAction) + XCTAssertEqual(delegateMock.capturedAction, .addFolder) + XCTAssertEqual(delegateMock.capturedItem, collectionViewItem) + } + + func testWhenItemFiresManageBookmarksActionThenDelegateReceivesManageBookmarksAction() { + // GIVEN + let sut = BookmarksBarViewModel(bookmarkManager: createMockBookmarksManager(), tabCollectionViewModel: .mock()) + let collectionViewItem = BookmarksBarCollectionViewItem() + let delegateMock = BookmarksBarViewModelDelegateMock() + sut.delegate = delegateMock + XCTAssertFalse(delegateMock.didCallViewModelReceivedAction) + XCTAssertNil(delegateMock.capturedAction) + XCTAssertNil(delegateMock.capturedItem) + + // WHEN + sut.bookmarksBarCollectionViewItemManageBookmarksAction(collectionViewItem) + + // THEN + XCTAssertTrue(delegateMock.didCallViewModelReceivedAction) + XCTAssertEqual(delegateMock.capturedAction, .manageBookmarks) + XCTAssertEqual(delegateMock.capturedItem, collectionViewItem) + } + private func createMockBookmarksManager(mockBookmarkStore: BookmarkStoreMock = BookmarkStoreMock()) -> BookmarkManager { let mockFaviconManager = FaviconManagerMock() return LocalBookmarkManager(bookmarkStore: mockBookmarkStore, faviconManagement: mockFaviconManager) @@ -107,3 +300,24 @@ fileprivate extension TabCollectionViewModel { } } + +// MARK: - BookmarksBarViewModelDelegateMock + +final class BookmarksBarViewModelDelegateMock: BookmarksBarViewModelDelegate { + private(set) var didCallViewModelReceivedAction = false + private(set) var capturedAction: BookmarksBarViewModel.BookmarksBarItemAction? + private(set) var capturedItem: BookmarksBarCollectionViewItem? + + func bookmarksBarViewModelReceived(action: BookmarksBarViewModel.BookmarksBarItemAction, for item: BookmarksBarCollectionViewItem) { + didCallViewModelReceivedAction = true + capturedAction = action + capturedItem = item + } + + func bookmarksBarViewModelWidthForContainer() -> CGFloat { + 0 + } + + func bookmarksBarViewModelReloadedData() {} + +} diff --git a/UnitTests/HomePage/Mocks/MockBookmarkManager.swift b/UnitTests/HomePage/Mocks/MockBookmarkManager.swift index c6a0840629..28f65fdf59 100644 --- a/UnitTests/HomePage/Mocks/MockBookmarkManager.swift +++ b/UnitTests/HomePage/Mocks/MockBookmarkManager.swift @@ -59,8 +59,12 @@ class MockBookmarkManager: BookmarkManager { func update(bookmark: DuckDuckGo_Privacy_Browser.Bookmark) {} + func update(bookmark: DuckDuckGo_Privacy_Browser.Bookmark, withURL url: URL, title: String, isFavorite: Bool) {} + func update(folder: DuckDuckGo_Privacy_Browser.BookmarkFolder) {} + func update(folder: DuckDuckGo_Privacy_Browser.BookmarkFolder, andMoveToParent parent: DuckDuckGo_Privacy_Browser.ParentFolderType) {} + func updateUrl(of bookmark: DuckDuckGo_Privacy_Browser.Bookmark, to newUrl: URL) -> DuckDuckGo_Privacy_Browser.Bookmark? { return nil } From 36dbe605f4291da51fbe5e1b0214b54c548fc1e0 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Tue, 19 Mar 2024 11:46:32 +0100 Subject: [PATCH 36/51] Subscription design review further fixes (#2448) Task/Issue URL: https://app.asana.com/0/1199230911884351/1206874000818381/f **Description**: Minor updates after the design review. **Steps to test this PR**: Please see comments in the description of https://app.asana.com/0/1200019156869587/1206659068942464/f --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --- .../Images/Privacy.imageset/Contents.json | 2 +- .../Images/Privacy.imageset/Privacy (Color).pdf | Bin 2879 -> 0 bytes .../Images/Privacy.imageset/Privacy.pdf | Bin 0 -> 4997 bytes .../Sources/SubscriptionUI/UserText.swift | 4 ++-- 4 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 DuckDuckGo/Assets.xcassets/Images/Privacy.imageset/Privacy (Color).pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/Privacy.imageset/Privacy.pdf diff --git a/DuckDuckGo/Assets.xcassets/Images/Privacy.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Privacy.imageset/Contents.json index 7b9656ec27..ec1dea3aba 100644 --- a/DuckDuckGo/Assets.xcassets/Images/Privacy.imageset/Contents.json +++ b/DuckDuckGo/Assets.xcassets/Images/Privacy.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "Privacy (Color).pdf", + "filename" : "Privacy.pdf", "idiom" : "universal" } ], diff --git a/DuckDuckGo/Assets.xcassets/Images/Privacy.imageset/Privacy (Color).pdf b/DuckDuckGo/Assets.xcassets/Images/Privacy.imageset/Privacy (Color).pdf deleted file mode 100644 index 5d44d31ebdea8ebefd11731ae29afc05d60054dc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2879 zcmbVOO^@3)5WVwP@Dd;?5Slam27&<1Zc`L((XG>4(1Yt$HZFFp)^>_CzrJrMiIUer zzjQF@CvwDj@6C`;ZmwTHCzF&SZFT?4uS)9|FZ9cos@vbnU&Tv&^Y0{`!@^db9kuJ*wXo8CxH1r@`|!YVY#&&YH-jkki`-WlbndAz`I2 zmP*7R4};@&kJn@1DPGjFhHOmH=^3W?TpPp zoxWGWM2~1$d&9Qa;B+)$NG6yPJX4vGWU@tciPdHi8IKQ?j9@d!hEuSVHG(+e$eQp` zAMhzawxn#GK+aO2h`zm<$8D$XU#Hf0ZoU{6G#xJfLM%^bAdJj~!MnXrZI*rkPz(QuAF zdC5t1WN=RakWCIOSq@|9qO+YlBb!-eY9UHRHv0+OcXK>W9;PWBo1n( z&L}%8sEw{fUq?f+;VoX-JiHw-hKX|kZ-|LC1Da89CFdX^8yErl;1LmJ7Bk!dBLw0S z5D-G8VoQKuxS%UA;w2L;dPr>)#9Vs;^794Vpt>){7~nosallHj=|;pAc2Nw0#AV%9 zu?f;U;v8Q}7j^I9+qR8ns&TJ1=%bVtbqf;5 z3`0n@Kv3u4er^>=FGhj{XV5J7l<`vS34IXFCQiLIeuMCq;5#0tWP?sO=5*Y~WJ2GjvK9Zb=@;kWe+T zLO5!`#gabR_k~6xT{NY@&XPS@pIxZM`WPE#kuY9o$tfmt&>7KjYGbXZ6#czjbx#~Y)EnV#Y z2xJinEpHKNNb_oQ+T8bd<5Efb__eT+^)6VY9pM4hmP?)i>9mj^=Bd?!nC<3(ggAISW~SM0d$2ud;M4CfqRM6W zV+7n*+O0!(Iw~VFp8trfN3Wi}_)><_b<(?j`}&uz@4x!0Uwr@j`*pwg;k(zrtXBtU zJd1DEo41E|y=UXaM0&Z~et3VzV86$A#xpWrB5$|c?B6eU>&@!-Tkjt~oJFVBCi_T8_Y9NA~lU(T!fT|I3|bK|%>EO3SX)e!trA`mUrql$x|>#62hmXBB_;iJdgTntqH|?A}Wb;pQU-HHQGvaB>%`N z*-BH1xy-~k?hBFnn)iKzHG&Zcwtc~uL{+yM#B&7rL7vy3h3>OgTN4Q9YSPlA)Fpbn zK^?L!wMg|NAhUoJRHv$_1in$;kffI66idLPn0;j*tD<#H^-IQ~C=AI*${!qafT#f0s?VRREQcHLqQLQ`vI9G z2QO<8x&zyz#GKUOHP;#oM@Q!aS}WQLOPqz$H99cv8d?ToNrzFzR8b~GRd%98Rj;;^ ztph2asPb(PdQ4Q)RDNy90@=magkqB#<`w5!Ap<8}&GEjd4~`}&nv(+>Jp5>uN0ij$ z2~sWF6CuG&c9FamDDz_@QH-~>=oQcLjF0pd-ANbm9tVaY)LfF$LhwQ)q2(qOi{J+E zeWhT=NXI7Zj2Kk|xiKb0O=bxxm2eO=%^W!3rqe`XW-=Ro))u1ug=0pZOp}C1$laWZ znJ>kMQFE|9IMK=@w@Y*6tPlt~bGBsEmQY>ah;t$_tyCmbN>C#}{nR);uEN z)o6T2W^l*@h_a$%(Vw+bnL79`1rN~BjkWVy@zG`@p)WX(Ms6e=-&|9Y)}`rOC9_7E zJTPvEM}3?l)QQdlq!q@iUlX02VE|W4VpLEPF3^=Wa+)`xx9DYD>9lTfrP`qZbxo)x zW`O3RMagC62#fqsfJ;CUd5#=Vj2svMGDCxsJhKOfQKA)1R2t=5kW1k3a+xNNl0YjC zQ&26cogY$3#L`GY(kO|XBx_-%FH>49aW@&L`NFj0D;1xqoy@6ZJ3~@#R+-|eS8YkU zOe$72PrZ2T*u9 z4sFa^UdtCSa0Cp$5^TIq>fmR8-E;5;Tuj2a>N73FPuxr@Hu#?Gi)R|b$qCVZX z?Lz$nWuuWk-Fu&ZJiNR-EDvjY|6J_f<+scIzil;M+4bkG3-;+AetWfe*=x#8H~HJ| z?)_SDh5!2c{k!F#i#}e{@5xZY^j%@LAV#2*cV$7dOhu!k(dcEt^z5jTI zb3fZ}Z`PmppIyEAe01^RV!Oq^>OPCt;_~W`wfX|*?hnh|!Mqc5%iW_#&%b-|A76w6 ABme*a literal 0 HcmV?d00001 diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift index f90aba85ad..1bd2c1503d 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift @@ -103,9 +103,9 @@ enum UserText { static func activateModalDescription(platform: SubscriptionPurchaseEnvironment.Environment) -> String { switch platform { case .appStore: - NSLocalizedString("subscription.appstore.activate.modal.description", value: "Access your subscription on other devices via Apple ID or an email address.", comment: "Activate subscription modal view subtitle description") + NSLocalizedString("subscription.appstore.activate.modal.description", value: "Access your Privacy Pro subscription on this device via Apple ID or an email address.", comment: "Activate subscription modal view subtitle description") case .stripe: - NSLocalizedString("subscription.activate.modal.description", value: "Access your subscription on other devices via an email address.", comment: "Activate subscription modal view subtitle description") + NSLocalizedString("subscription.activate.modal.description", value: "Access your Privacy Pro subscription via an email address.", comment: "Activate subscription modal view subtitle description") } } From 1a9bfbe410048d8775f117770839c922fb6d5a5f Mon Sep 17 00:00:00 2001 From: Federico Cappelli Date: Tue, 19 Mar 2024 11:02:59 +0000 Subject: [PATCH 37/51] Web pixels (handlers + pixels) (#2451) Task/Issue URL: https://app.asana.com/0/72649045549333/1205469290776415/f Implementation sub-task: https://app.asana.com/0/72649045549333/1206697233126329/f **Description**: The FE needs to fire pixels after some user actions, this PR implements the web handlers where the related pixels are fired. --- DuckDuckGo/InfoPlist.xcstrings | 2 +- DuckDuckGo/Statistics/PixelEvent.swift | 10 +++ DuckDuckGo/Statistics/PixelParameters.swift | 6 +- .../SubscriptionPagesUserScript.swift | 67 ++++++++++++++++--- 4 files changed, 75 insertions(+), 10 deletions(-) diff --git a/DuckDuckGo/InfoPlist.xcstrings b/DuckDuckGo/InfoPlist.xcstrings index ee95494024..cce98e8330 100644 --- a/DuckDuckGo/InfoPlist.xcstrings +++ b/DuckDuckGo/InfoPlist.xcstrings @@ -363,4 +363,4 @@ } }, "version" : "1.0" -} \ No newline at end of file +} diff --git a/DuckDuckGo/Statistics/PixelEvent.swift b/DuckDuckGo/Statistics/PixelEvent.swift index 54aed95aef..0af3664f30 100644 --- a/DuckDuckGo/Statistics/PixelEvent.swift +++ b/DuckDuckGo/Statistics/PixelEvent.swift @@ -250,6 +250,11 @@ extension Pixel { case privacyProSubscriptionManagementEmail case privacyProSubscriptionManagementPlanBilling case privacyProSubscriptionManagementRemoval + // Web pixels + case privacyProOfferMonthlyPriceClick + case privacyProOfferYearlyPriceClick + case privacyProAddEmailSuccess + case privacyProWelcomeFAQClick case dailyPixel(Event, isFirst: Bool) @@ -667,6 +672,11 @@ extension Pixel.Event { case .privacyProSubscriptionManagementEmail: return "m_mac_\(appDistribution)_privacy-pro_manage-email_edit_click" case .privacyProSubscriptionManagementPlanBilling: return "m_mac_\(appDistribution)_privacy-pro_settings_change-plan-or-billing_click" case .privacyProSubscriptionManagementRemoval: return "m_mac_\(appDistribution)_privacy-pro_settings_remove-from-device_click" + // Web + case .privacyProOfferMonthlyPriceClick: return "m_mac_\(appDistribution)_privacy-pro_offer_monthly-price_click" + case .privacyProOfferYearlyPriceClick: return "m_mac_\(appDistribution)_privacy-pro_offer_yearly-price_click" + case .privacyProAddEmailSuccess: return "m_mac_\(appDistribution)_privacy-pro_app_add-email_success_u" + case .privacyProWelcomeFAQClick: return "m_mac_\(appDistribution)_privacy-pro_welcome_faq_click_u" case .protectionToggledOffBreakageReport: return "m_mac_protection-toggled-off-breakage-report" case .toggleProtectionsDailyCount: return "m_mac_toggle-protections-daily-count" diff --git a/DuckDuckGo/Statistics/PixelParameters.swift b/DuckDuckGo/Statistics/PixelParameters.swift index 2313656cc7..c8be054048 100644 --- a/DuckDuckGo/Statistics/PixelParameters.swift +++ b/DuckDuckGo/Statistics/PixelParameters.swift @@ -201,7 +201,11 @@ extension Pixel.Event { .protectionToggledOffBreakageReport, .toggleProtectionsDailyCount, .toggleReportDoNotSend, - .toggleReportDismiss: + .toggleReportDismiss, + .privacyProOfferMonthlyPriceClick, + .privacyProOfferYearlyPriceClick, + .privacyProAddEmailSuccess, + .privacyProWelcomeFAQClick: return nil } } diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index a69c43fa24..3c08e86e73 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -91,16 +91,40 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { self.broker = broker } + struct Handlers { + static let getSubscription = "getSubscription" + static let setSubscription = "setSubscription" + static let backToSettings = "backToSettings" + static let getSubscriptionOptions = "getSubscriptionOptions" + static let subscriptionSelected = "subscriptionSelected" + static let activateSubscription = "activateSubscription" + static let featureSelected = "featureSelected" + static let completeStripePayment = "completeStripePayment" + // Pixels related events + static let subscriptionsMonthlyPriceClicked = "subscriptionsMonthlyPriceClicked" + static let subscriptionsYearlyPriceClicked = "subscriptionsYearlyPriceClicked" + static let subscriptionsUnknownPriceClicked = "subscriptionsUnknownPriceClicked" + static let subscriptionsAddEmailSuccess = "subscriptionsAddEmailSuccess" + static let subscriptionsWelcomeFaqClicked = "subscriptionsWelcomeFaqClicked" + } + + // swiftlint:disable:next cyclomatic_complexity func handler(forMethodNamed methodName: String) -> Subfeature.Handler? { switch methodName { - case "getSubscription": return getSubscription - case "setSubscription": return setSubscription - case "backToSettings": return backToSettings - case "getSubscriptionOptions": return getSubscriptionOptions - case "subscriptionSelected": return subscriptionSelected - case "activateSubscription": return activateSubscription - case "featureSelected": return featureSelected - case "completeStripePayment": return completeStripePayment + case Handlers.getSubscription: return getSubscription + case Handlers.setSubscription: return setSubscription + case Handlers.backToSettings: return backToSettings + case Handlers.getSubscriptionOptions: return getSubscriptionOptions + case Handlers.subscriptionSelected: return subscriptionSelected + case Handlers.activateSubscription: return activateSubscription + case Handlers.featureSelected: return featureSelected + case Handlers.completeStripePayment: return completeStripePayment + // Pixel related events + case Handlers.subscriptionsMonthlyPriceClicked: return subscriptionsMonthlyPriceClicked + case Handlers.subscriptionsYearlyPriceClicked: return subscriptionsYearlyPriceClicked + case Handlers.subscriptionsUnknownPriceClicked: return subscriptionsUnknownPriceClicked + case Handlers.subscriptionsAddEmailSuccess: return subscriptionsAddEmailSuccess + case Handlers.subscriptionsWelcomeFaqClicked: return subscriptionsWelcomeFaqClicked default: return nil } @@ -488,6 +512,33 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { return [String: String]() // cannot be nil } + // MARK: Pixel related actions + + func subscriptionsMonthlyPriceClicked(params: Any, original: WKScriptMessage) async -> Encodable? { + Pixel.fire(.privacyProOfferMonthlyPriceClick) + return nil + } + + func subscriptionsYearlyPriceClicked(params: Any, original: WKScriptMessage) async -> Encodable? { + Pixel.fire(.privacyProOfferYearlyPriceClick) + return nil + } + + func subscriptionsUnknownPriceClicked(params: Any, original: WKScriptMessage) async -> Encodable? { + // Not used + return nil + } + + func subscriptionsAddEmailSuccess(params: Any, original: WKScriptMessage) async -> Encodable? { + Pixel.fire(.privacyProAddEmailSuccess, limitTo: .initial) + return nil + } + + func subscriptionsWelcomeFaqClicked(params: Any, original: WKScriptMessage) async -> Encodable? { + Pixel.fire(.privacyProWelcomeFAQClick, limitTo: .initial) + return nil + } + // MARK: Push actions enum SubscribeActionName: String { From 3dc0733d527af2d2ed942f83564834148405eaa0 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Tue, 19 Mar 2024 04:21:57 -0700 Subject: [PATCH 38/51] Add VPN & PIR thank you modal (#2437) Task/Issue URL: https://app.asana.com/0/1199230911884351/1206800069675138/f Tech Design URL: CC: **Description**: This PR adds a new modal to show a Thank You message to users. It isn't yet triggered within the app automatically, but has Debug menu triggers to show how it can be displayed. --- DuckDuckGo.xcodeproj/project.pbxproj | 16 ++ .../Gift-96.imageset/Contents.json | 5 +- .../Gift-96.imageset/Gift-96.pdf | Bin 9684 -> 0 bytes .../Gift-96.imageset/Gift-New-96x96.pdf | Bin 0 -> 7383 bytes .../UserText+NetworkProtection.swift | 14 ++ .../Model/HomePageContinueSetUpModel.swift | 45 +++- .../MainWindow/MainViewController.swift | 11 + DuckDuckGo/Menus/MainMenu.swift | 3 + DuckDuckGo/Menus/MainMenuActions.swift | 21 ++ .../WaitlistThankYouPromptPresenter.swift | 128 ++++++++++ .../Waitlist/Views/WaitlistThankYouView.swift | 229 ++++++++++++++++++ 11 files changed, 470 insertions(+), 2 deletions(-) delete mode 100644 DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Gift-96.imageset/Gift-96.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Gift-96.imageset/Gift-New-96x96.pdf create mode 100644 DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift create mode 100644 DuckDuckGo/Waitlist/Views/WaitlistThankYouView.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 71f46f9bd0..ccafd9b0dd 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1201,6 +1201,9 @@ 4B4D60E22A0C883A00BCD287 /* AppMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60E12A0C883A00BCD287 /* AppMain.swift */; }; 4B4D60E32A0C883A00BCD287 /* AppMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60E12A0C883A00BCD287 /* AppMain.swift */; }; 4B4F72EC266B2ED300814C60 /* CollectionExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4F72EB266B2ED300814C60 /* CollectionExtension.swift */; }; + 4B520F632BA5573A006405C7 /* WaitlistThankYouView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B520F622BA5573A006405C7 /* WaitlistThankYouView.swift */; }; + 4B520F642BA5573A006405C7 /* WaitlistThankYouView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B520F622BA5573A006405C7 /* WaitlistThankYouView.swift */; }; + 4B520F652BA5573A006405C7 /* WaitlistThankYouView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B520F622BA5573A006405C7 /* WaitlistThankYouView.swift */; }; 4B59023E26B35F3600489384 /* ChromiumLoginReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B59023926B35F3600489384 /* ChromiumLoginReader.swift */; }; 4B59024026B35F3600489384 /* ChromiumDataImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B59023B26B35F3600489384 /* ChromiumDataImporter.swift */; }; 4B59024826B3673600489384 /* ThirdPartyBrowser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B59024726B3673600489384 /* ThirdPartyBrowser.swift */; }; @@ -1220,6 +1223,9 @@ 4B67854A2AA8DE75008A5004 /* NetworkProtectionFeatureVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD8679A2A9E9E000063B9F7 /* NetworkProtectionFeatureVisibility.swift */; }; 4B67854B2AA8DE76008A5004 /* NetworkProtectionFeatureVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD8679A2A9E9E000063B9F7 /* NetworkProtectionFeatureVisibility.swift */; }; 4B68DDFF2ACBA14100FB0973 /* FileLineError.swift in Sources */ = {isa = PBXBuildFile; fileRef = B696AFFA2AC5924800C93203 /* FileLineError.swift */; }; + 4B6B64842BA930420009FF9F /* WaitlistThankYouPromptPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B6B64832BA930420009FF9F /* WaitlistThankYouPromptPresenter.swift */; }; + 4B6B64852BA930420009FF9F /* WaitlistThankYouPromptPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B6B64832BA930420009FF9F /* WaitlistThankYouPromptPresenter.swift */; }; + 4B6B64862BA930420009FF9F /* WaitlistThankYouPromptPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B6B64832BA930420009FF9F /* WaitlistThankYouPromptPresenter.swift */; }; 4B70C00127B0793D000386ED /* DuckDuckGo-ExampleCrash.ips in Resources */ = {isa = PBXBuildFile; fileRef = 4B70BFFF27B0793D000386ED /* DuckDuckGo-ExampleCrash.ips */; }; 4B70C00227B0793D000386ED /* CrashReportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B70C00027B0793D000386ED /* CrashReportTests.swift */; }; 4B723E0526B0003E00E14D75 /* DataImportMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B723DFF26B0003E00E14D75 /* DataImportMocks.swift */; }; @@ -3680,6 +3686,7 @@ 4B4D60D22A0C84F700BCD287 /* UserText+NetworkProtection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UserText+NetworkProtection.swift"; sourceTree = ""; }; 4B4D60E12A0C883A00BCD287 /* AppMain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppMain.swift; sourceTree = ""; }; 4B4F72EB266B2ED300814C60 /* CollectionExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionExtension.swift; sourceTree = ""; }; + 4B520F622BA5573A006405C7 /* WaitlistThankYouView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistThankYouView.swift; sourceTree = ""; }; 4B59023926B35F3600489384 /* ChromiumLoginReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChromiumLoginReader.swift; sourceTree = ""; }; 4B59023B26B35F3600489384 /* ChromiumDataImporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChromiumDataImporter.swift; sourceTree = ""; }; 4B59024726B3673600489384 /* ThirdPartyBrowser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartyBrowser.swift; sourceTree = ""; }; @@ -3698,6 +3705,7 @@ 4B677454255DC18000025BD8 /* Bridging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Bridging.h; sourceTree = ""; }; 4B67853E2AA7C726008A5004 /* DailyPixel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DailyPixel.swift; sourceTree = ""; }; 4B6785432AA8DE1F008A5004 /* NetworkProtectionFeatureDisabler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionFeatureDisabler.swift; sourceTree = ""; }; + 4B6B64832BA930420009FF9F /* WaitlistThankYouPromptPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistThankYouPromptPresenter.swift; sourceTree = ""; }; 4B70BFFF27B0793D000386ED /* DuckDuckGo-ExampleCrash.ips */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "DuckDuckGo-ExampleCrash.ips"; sourceTree = ""; }; 4B70C00027B0793D000386ED /* CrashReportTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CrashReportTests.swift; sourceTree = ""; }; 4B723DEB26B0002B00E14D75 /* DataImport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataImport.swift; sourceTree = ""; }; @@ -5977,6 +5985,8 @@ 4B9DB0192A983B24000927DB /* WaitlistModalViewController.swift */, 3168506C2AF3AD1C009A2828 /* WaitlistViewControllerPresenter.swift */, 4B9DB01A2A983B24000927DB /* WaitlistRootView.swift */, + 4B520F622BA5573A006405C7 /* WaitlistThankYouView.swift */, + 4B6B64832BA930420009FF9F /* WaitlistThankYouPromptPresenter.swift */, ); path = Views; sourceTree = ""; @@ -9900,6 +9910,7 @@ 3706FA7E293F65D500E42796 /* LottieAnimationCache.swift in Sources */, 9F982F0E2B8224BF00231028 /* AddEditBookmarkFolderDialogViewModel.swift in Sources */, 3706FA7F293F65D500E42796 /* TabIndex.swift in Sources */, + 4B520F642BA5573A006405C7 /* WaitlistThankYouView.swift in Sources */, 3706FA80293F65D500E42796 /* TabLazyLoaderDataSource.swift in Sources */, 3706FA81293F65D500E42796 /* LoginImport.swift in Sources */, C13909FC2B861039001626ED /* AutofillActionPresenter.swift in Sources */, @@ -10426,6 +10437,7 @@ 4B9DB0452A983B24000927DB /* WaitlistModalViewController.swift in Sources */, B66CA41F2AD910B300447CF0 /* DataImportView.swift in Sources */, 3706FC0C293F65D500E42796 /* NSAttributedStringExtension.swift in Sources */, + 4B6B64852BA930420009FF9F /* WaitlistThankYouPromptPresenter.swift in Sources */, C1DAF3B62B9A44860059244F /* AutofillPopoverPresenter.swift in Sources */, 3706FC0D293F65D500E42796 /* AnimationView.swift in Sources */, 9FA173DB2B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift in Sources */, @@ -11476,6 +11488,7 @@ 4B957A9D2AC7AE700062CA31 /* NetworkProtectionSimulateFailureMenu.swift in Sources */, 4B957A9E2AC7AE700062CA31 /* PrivacyDashboardPermissionHandler.swift in Sources */, 4B957A9F2AC7AE700062CA31 /* TabCollectionViewModel.swift in Sources */, + 4B520F652BA5573A006405C7 /* WaitlistThankYouView.swift in Sources */, 4B957AA02AC7AE700062CA31 /* BookmarkManager.swift in Sources */, 4B957AA12AC7AE700062CA31 /* AboutModel.swift in Sources */, 4B957AA22AC7AE700062CA31 /* PasswordManagementCreditCardItemView.swift in Sources */, @@ -11732,6 +11745,7 @@ 4B957B802AC7AE700062CA31 /* NSAlertExtension.swift in Sources */, 4B957B812AC7AE700062CA31 /* ThirdPartyBrowser.swift in Sources */, 4B957B822AC7AE700062CA31 /* SearchNonexistentDomainNavigationResponder.swift in Sources */, + 4B6B64862BA930420009FF9F /* WaitlistThankYouPromptPresenter.swift in Sources */, B6B71C5A2B23379600487131 /* NSLayoutConstraintExtension.swift in Sources */, B65211272B29A43000B30633 /* BookmarkStoreMock.swift in Sources */, 4B957B832AC7AE700062CA31 /* CircularProgressView.swift in Sources */, @@ -12272,6 +12286,7 @@ 7B430EA12A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift in Sources */, 1E7E2E942902AC0E00C01B54 /* PrivacyDashboardPermissionHandler.swift in Sources */, AA9FF95F24A1FB690039E328 /* TabCollectionViewModel.swift in Sources */, + 4B520F632BA5573A006405C7 /* WaitlistThankYouView.swift in Sources */, 4BF0E5052AD2551A00FFEC9E /* NetworkProtectionPixelEvent.swift in Sources */, AAC5E4D125D6A709007F5990 /* BookmarkManager.swift in Sources */, 37CD54CD27F2FDD100F1F7B9 /* AboutModel.swift in Sources */, @@ -12531,6 +12546,7 @@ 7BFE95522A9DF1CE0081ABE9 /* NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift in Sources */, 1456D6E124EFCBC300775049 /* TabBarCollectionView.swift in Sources */, 4B4D60BF2A0C848A00BCD287 /* NetworkProtection+ConvenienceInitializers.swift in Sources */, + 4B6B64842BA930420009FF9F /* WaitlistThankYouPromptPresenter.swift in Sources */, 3158B1592B0BF76400AF130C /* DataBrokerProtectionFeatureDisabler.swift in Sources */, B655124829A79465009BFE1C /* NavigationActionExtension.swift in Sources */, 9FA173EB2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift in Sources */, diff --git a/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Gift-96.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Gift-96.imageset/Contents.json index 0223097db0..0d830310d5 100644 --- a/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Gift-96.imageset/Contents.json +++ b/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Gift-96.imageset/Contents.json @@ -1,12 +1,15 @@ { "images" : [ { - "filename" : "Gift-96.pdf", + "filename" : "Gift-New-96x96.pdf", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true } } diff --git a/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Gift-96.imageset/Gift-96.pdf b/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Gift-96.imageset/Gift-96.pdf deleted file mode 100644 index 41180d6d4063373a4e87e2a6ae4d3acaefac519f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9684 zcmeI2%WfUH5r+5u6umJp6F{xzwMk%LAY*$5K@!BVvm0cg9G~$x&?S*Hfs?1tS0uYg z=?I9t$;!uo-Ts>_)}2*kzj*ulhcmNm(`4p#zWLkVr+NPFyZQXh=ldVu-(TDx>;FRe z=WsEvi(Tb6zkJEali&Fp|Ll?W*(2SvN1Er1eECwiKVE+jaVPsg?mrx_Ki>Z`JHU4` z>hAvbcyTqIzdru!)8Y8;$M5G?@5lcg?xufE=WopX@oqwoMK|kwww^xe%jB0W6dk%; zo}!r#SJS!#CqGpD_~GtwJ%uIM6q23Y5@oWPuO`2^&3Na*QkLI*31$Ld0?VEmTYK=^ z++GQZGJFLQ0yfQusggD8To>9bwA>PSncJV9& z4`JCLWWsD0Cmav!$vh*H4SZdKtuo$q?#+@!Fkz-iXqE^N<1Sc6o^|msYD5oBUzcEJ z$3(Cc(RE>T?Sc(RdnL5mqY6eCKwuLwL@>0h>oviI2j!~@hRrU)8uwK&RH+iSR|%x) zp%OsvqMN3#x)3%K-FTK>J&TKAU@*9|qk^>u6+&$73OI@%s+Ox@DKxcV3tdmTm&(DG^g z9Hw8UpQr2T&n1HG7euf{OErQ$3g_ur5u6sAZNivg@uGf-;8@rTMk36&2#$+&#F~uD zV${$cwiR{ADPso}$*M)+3NLVb=dky^Z#R#K_$FER|62eYY3&-%F>xqXL=D~yVB_6WM_qUzQv*nl*_h6q-{*2NLRM#K=o(6X*q1uHx#UsbRY!ODFVj9jJ? z)>lcAPSZmr5JVo;S6#^047;&(kVI4xtCciYjeaVIB4VFh?W0&&9IjCmB7&*fu!XKC zKGOU~`4S%yH^Z{Zy?UTZsD7$?I85r#s;^5hakH{fh+q&^2U5WXMC~(RO|a@_Nvtn| zp=Di9{o!E*yDx}f%@{Tn?|)VRpQeznTkzK{`0EzDlH4?)w6^&2E!ax{YniN>trye! zE;}uuNm2Zs6~AUNW!vt1Mya~j2--1Smpc5>D&DD+O=Q3F7r4Yzx7pu-iBlCTX| zOdn2PDFG?}TE(Hu=x}9Nhb!_hL564R*I>N5znJ{|{AkKhdP7}b^cxGMj_J$OS4v>e zFXiEGv_`2~oMm*lBBe~O!xhs9rKE@wkj+`#sPNvF(c#Ll4p(H*g( z(-J6TmdxBJwa5rMLlY>iqoDxsIT?dy6=V??&4|S^irTsfD#f16E7i8lTep%B8iofo zmZf2l8aLZ{1X;{GDbYLFHAkt9|3q2=RjZ|T5jg^@bWn-{6cdr7gJ?T?M=jLWpvFl% zH_A(gsH^B%;Qtd{de ziP+Tx%_mT3c66(1i?b)pXpM2H?j2;0Z6g9D1bZIXDG&`OV~z_Ea3)CmdQ1 zHg>DAA~`w1RsV$-rZNjz^O4c3_4WYtMEl5qXTg-gsNfCQLpwU9ksUu zrtCnok5tZ~*-;g-trLk>)eo-SIO^EQXZH|jKH*KHBR_Rcr)pJsqx!E?9*ZZtB(99j zwB&9Fuly&lx;4r(NTqwUfrF)IOb1KWDsI+C5 zL4>$Jn7ATWk)@9%3%6G7YBI`3w*=m+X!i}!T0O9>j;L%tiJ&tB+9T5ukcQyQLtO8v zNh-}gQhcJ)=@h#^^|d=j{UzP*N^?GUwDaC6c5Ga`qr*rPFIvIXlEzaRjbQ(NLv2g`P3aGb2V_ZVXV7?nY+0!DeTEH8>g+x7-nshhMZYE zz^AO&GP|Yh$+Z$DH7R&GfNc|7sRQ~* zk~jMZ*_Z&U59EkaCnhfm8cNq(i!@RFeBRT&)q=A3Dpr zYKNql>`vQpLar28B7R3vct>^l2dMH%t+{ieGWM0ai1;X$eXJl{q^YV)K47{wv-?Pu zy;JPG_q97#O6lLU<5=}nb-Eq_`Yve7(W1Im8h%^xG(2g~&p86cn$vVgQPSv_!zPLm zssRPrU7JgC{gxJn)e&sp>o$V?(eJK0j;*v?8R)RBs8f=QBE2Q<2!#pZ|1&IUW5=D$*KlM(I z4kNb=E>e)m?O1kfMsmE6tY?$w#RIH>!OO zR%rmF$mU6o5uL@Kfs_eey1c>VfU#9jkZ>D8S2m2~Ks~y(oNETVS-gsuIY%yRVBBUG zsNR{&4M|vOipmw6UpW#c8&S;48YFHCUk}4bvc__E6kMPv7KT~VbLG|yCiy7_TCliZ zDwe}_yHY*Zi3}DEkqjN$f}bT*VN0*V)s&%65*DcclQhgL-H;QS$|=Q=;nf*>)Fs>oknkg@Fbb2m zl}g-mKd?h2fSi(~Co)Y3F$umQ7hjDU2@M62DEg`jiLrP76clT7K|=h_XA0u3Tio!p zvu$ZhjF7U@L1np9Qx)`875YB~7>*V_sdPRg4Q5*MaxuxRuM3_eCFsUIJqJ~Cl8`(J zrC@7bu>;a28tDg^>`h!Ul7h#PC1gm1569AxIzwqVt|RTY^*Hdrkp!;|+;U@jGVaDT z6USlY69q0+$^K`gYmRr}r9^^aF=J`@LPBSgd^{zZB!p6ule?LXFW^`td%`CKi^Dv? zXh;m1S1_GDXpX|ilUU?RPXdL#g!8O3c7c@Ho$3c69h82t+75QyZNWT*6(x9|MKsD9Ov`57axxe_~Y@zr;G1zKF>d4 zy;jHQL)H_LO>2#v(!1l`&9AqIZAFo+aKBU>fw->iyGyYMa;d2{- z`e#PJ|MUS?XOZCDysz`>=2zyL6aMc&21?}5fszP#rwj67QWN~u#r?(Q&BupRKi?jI zp4NGrs`&EvjGK@}f!j|$eURULy+c%ikDyEefd7=;`Y9XlYO+)ix+SH H^uvDv9PwaX diff --git a/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Gift-96.imageset/Gift-New-96x96.pdf b/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Gift-96.imageset/Gift-New-96x96.pdf new file mode 100644 index 0000000000000000000000000000000000000000..f4fd0b70d68b50e25d46f98792d6a7276bc3c733 GIT binary patch literal 7383 zcmeHM%Z_8W5#8UfxSIj&0CUCn127E4GjR|kK_>0&j4V1DdE9}zm9*M{ldsPyiuV*- z5;8sTCX3Oc!9FC5^=1|Ct9Ng|eQlRKO?Ec(;ctJR=J~6y=C8k=b`S66-^q>m<`27v z4^KbN9^j3vIzBz_Ztthf+ugtJw!53(elx%MZv1b1oc=l4#kAjS*1>CzIOpm*={UK) zpUmQFF}`ANb~RYAvp63yoR8awDLC$m>*#*+W~niFlV(TMaivX*2{D0!VqCVJu7LXi zbU41m9A{gXoNRE=#d@;M>xj^HDi_#(@=Gi>I}xUJ7T20PdZXLk|R zcA!b?DY~D2oPL-dra!;f#i+eA7YhY0C3Y;nI-4*EE{ABFi$#j2N5jSH$;HaRy4plt zOtHbmu7L0fJ!f?>S1w!(%IoT(F#qQ+rUrfHV)@d=RMYBW14jL9Cq>mJqdOp_UBngj z#ktH;pjKPT@kJa0ZjejxHeldg4Aq?ZLs$=yG}X@|HiRW7_XFcs!C|-h_REARBp5mR zc>atO+)^3$6WLI3k1XD!RzV(ATO7A6-Sxm|A95DS4!TjQD|r&-aw0H1`6URYmecVW zhU&*Bj(C7VB)pvzB*Xgwq+EK0O0T>qwjFZP%j}jZ(V5-;cT(amJ^QoWX!KuXHwFU9 z>E+D1!0R*-JB3@TPT+}Y9oS@$M^3q8^-o>~_oxqIbK030VhuNtceQ2(EpNcI#FUNk zvqGe?ofK3e!-!P4MK>TEPNc&?np`OnLZpGfypT-tF&AiMgsXEYCdOiB1*vK47h6Mc zMnjo2hD^fsdp%;nu29jlt{gucq3e{$Fk+VA7TwTd|5v>rxfcAN-N?Y`m#$4+es&`$ z=$RWax6r$4ugstQI-cI$T9+wNzXFYI_!~n92%?jWMb0wb#74C_tBunk;P9` zS!DJlE2lcv%MYnEtUYv<&iT~WNRQHfnriPS7tjNx7^V2_vz}K*UTpFq#91LF0+G2; zspv>&b&q1(=?ZbOpi~Gck>Oe(4GylXCp6Y-FY?S%p#WoJY6?I{5Z&DLTvk1v59Eo$ zFUdrMb|Ng*xm@imS75c)nW9)&oXtK5AjP=sWH~y}Xfg&dl~_G814!7KGdkzRq5^P@ zzzOk-_qo)h``l7eO&&Z>uuKHwa;t`f2ZA)ix8)6@6zO(_eTxU6rQq^S>w`u^e6+R} z-M5{PfUM0X$;Jv_31(KvE~%E9iOsu`KsHoUG2M_m| zUn?rP%qQy`*3VIFU`RnSuJot~J($oh4zdXDK5iAb8&Z=f)`leo?~8|73>+s%Um6-r zbu9v_@wthNm@C!7fGwpO#xt~qjdLc78oAak#o@&ujOQTaaO)rZCyU3Gq2V zFu6UEL2t!{FgaZPL`PBWn)H-sSZC*1L*^tS;3wIamnB9kbK|J9iP^RVd~ye9Qd~tr zlkzG$aC}XQE^mMv=}M{wB&h~fAy2uYeL_v{&N;y*b{CeQ@*1gTiW*mYjCw?G-G0y1 z*gh0A6H`v!^lDZ@((88ih)K%s+z}(2q(2~?4WZS|MSg`AvF<&TZtzV;1oAj?#DF?@oRCuD*k5`*tKRg_sj`R0_#m6lCHh=l|KX&ux-R*~6 z2mZKwe|P)M;ph2#q_3`c4l2@M%BYNHjQZqC>1KC4{Peip9cTGmcGJndIXpb=9&n<3 zD_g;Lw~xSP6w(bEzNiVrFKwIO-@ON`vq*3=ujjlu{Di&sg8w6sff7ovfszP#obc&!^SifD#}~)L{qD>8i@Wz Bool + private let isPIRBetaTester: () -> Bool + private let userDefaults: UserDefaults + + convenience init() { + self.init(isVPNBetaTester: { + return false + }, isPIRBetaTester: { + return false + }) + } + + init(isVPNBetaTester: @escaping () -> Bool, isPIRBetaTester: @escaping () -> Bool, userDefaults: UserDefaults = .standard) { + self.isVPNBetaTester = isVPNBetaTester + self.isPIRBetaTester = isPIRBetaTester + self.userDefaults = userDefaults + } + + // MARK: - Presentation + + // Presents a Thank You prompt to testers of the VPN or PIR. + // If the user tested both, the PIR prompt will be displayed. + @MainActor + func presentThankYouPromptIfNecessary(in window: NSWindow) { + guard canShowPromptCheck() else { + return + } + + if isPIRBetaTester() { + saveDidShowPromptCheck() + presentPIRThankYouPrompt(in: window) + } else if isVPNBetaTester() { + saveDidShowPromptCheck() + presentVPNThankYouPrompt(in: window) + } + } + + @MainActor + func presentVPNThankYouPrompt(in window: NSWindow) { + let thankYouModalView = WaitlistBetaThankYouDialogViewController(copy: .vpn) + let thankYouWindowController = thankYouModalView.wrappedInWindowController() + if let thankYouWindow = thankYouWindowController.window { + window.beginSheet(thankYouWindow) + } + } + + @MainActor + func presentPIRThankYouPrompt(in window: NSWindow) { + let thankYouModalView = WaitlistBetaThankYouDialogViewController(copy: .dbp) + let thankYouWindowController = thankYouModalView.wrappedInWindowController() + if let thankYouWindow = thankYouWindowController.window { + window.beginSheet(thankYouWindow) + } + } + + // MARK: - Eligibility + + var canShowVPNCard: Bool { + guard !self.userDefaults.bool(forKey: Constants.didDismissVPNCardKey) else { + return false + } + + return isVPNBetaTester() + } + + var canShowPIRCard: Bool { + guard !self.userDefaults.bool(forKey: Constants.didDismissPIRCardKey) else { + return false + } + + return isPIRBetaTester() + } + + func canShowPromptCheck() -> Bool { + return !self.userDefaults.bool(forKey: Constants.didShowThankYouPromptKey) + } + + // MARK: - Dismissal + + func didDismissVPNThankYouCard() { + self.userDefaults.setValue(true, forKey: Constants.didDismissVPNCardKey) + } + + func didDismissPIRThankYouCard() { + self.userDefaults.setValue(true, forKey: Constants.didDismissPIRCardKey) + } + + private func saveDidShowPromptCheck() { + self.userDefaults.setValue(true, forKey: Constants.didShowThankYouPromptKey) + } + + // MARK: - Debug + + func resetPromptCheck() { + self.userDefaults.removeObject(forKey: Constants.didShowThankYouPromptKey) + self.userDefaults.removeObject(forKey: Constants.didDismissVPNCardKey) + self.userDefaults.removeObject(forKey: Constants.didDismissPIRCardKey) + } + +} diff --git a/DuckDuckGo/Waitlist/Views/WaitlistThankYouView.swift b/DuckDuckGo/Waitlist/Views/WaitlistThankYouView.swift new file mode 100644 index 0000000000..b94d1846d2 --- /dev/null +++ b/DuckDuckGo/Waitlist/Views/WaitlistThankYouView.swift @@ -0,0 +1,229 @@ +// +// WaitlistThankYouView.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Combine +import SwiftUI + +// MARK: - Model + +struct WaitlistBetaThankYouCopy { + static let dbp = WaitlistBetaThankYouCopy( + title: UserText.dbpThankYouTitle, + subtitle: UserText.dbpThankYouSubtitle, + body1: UserText.dbpThankYouBody1, + body2: UserText.dbpThankYouBody2 + ) + + static let vpn = WaitlistBetaThankYouCopy( + title: UserText.vpnThankYouTitle, + subtitle: UserText.vpnThankYouSubtitle, + body1: UserText.vpnThankYouBody1, + body2: UserText.vpnThankYouBody2 + ) + + let title: String + let subtitle: String + let body1: String + let body2: String + + @available(macOS 12.0, *) + func boldedBold1() -> AttributedString { + return bolded(text: body1, boldedStrings: ["THANKYOU"]) + } + + @available(macOS 12.0, *) + func boldedBold2() -> AttributedString { + return bolded(text: body2, boldedStrings: ["duckduckgo.com/app"]) + } + + @available(macOS 12.0, *) + private func bolded(text: String, boldedStrings: [String]) -> AttributedString { + var attributedString = AttributedString(text) + + for boldedString in boldedStrings { + if let range = attributedString.range(of: boldedString) { + attributedString[range].font = .system(size: 14, weight: .semibold) + } + } + + return attributedString + } +} + +// MARK: - View Model + +protocol WaitlistBetaThankYouDialogViewModelDelegate: AnyObject { + func waitlistBetaThankYouViewModelDismissedView(_ viewModel: WaitlistBetaThankYouDialogViewModel) +} + +final class WaitlistBetaThankYouDialogViewModel: ObservableObject { + + enum ViewAction { + case close + } + + weak var delegate: WaitlistBetaThankYouDialogViewModelDelegate? + + init() {} + + @MainActor + func process(action: ViewAction) async { + switch action { + case .close: + delegate?.waitlistBetaThankYouViewModelDismissedView(self) + } + } + +} + +// MARK: - View + +final class WaitlistBetaThankYouDialogViewController: NSViewController { + + private let defaultSize = CGSize(width: 360, height: 498) + private let viewModel: WaitlistBetaThankYouDialogViewModel + + private var heightConstraint: NSLayoutConstraint? + private var cancellables = Set() + + private let copy: WaitlistBetaThankYouCopy + + init(copy: WaitlistBetaThankYouCopy) { + self.viewModel = WaitlistBetaThankYouDialogViewModel() + self.copy = copy + super.init(nibName: nil, bundle: nil) + self.viewModel.delegate = self + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + view = NSView(frame: NSRect(origin: CGPoint.zero, size: defaultSize)) + } + + override func viewDidLoad() { + super.viewDidLoad() + + let feedbackFormView = WaitlistBetaThankYouView(copy: self.copy) + let hostingView = NSHostingView(rootView: feedbackFormView.environmentObject(self.viewModel)) + hostingView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(hostingView) + + let heightConstraint = hostingView.heightAnchor.constraint(equalToConstant: defaultSize.height) + self.heightConstraint = heightConstraint + + NSLayoutConstraint.activate([ + heightConstraint, + hostingView.widthAnchor.constraint(equalToConstant: defaultSize.width), + hostingView.topAnchor.constraint(equalTo: view.topAnchor), + hostingView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + hostingView.leftAnchor.constraint(equalTo: view.leftAnchor), + hostingView.rightAnchor.constraint(equalTo: view.rightAnchor) + ]) + } + +} + +struct WaitlistBetaThankYouView: View { + + @EnvironmentObject var viewModel: WaitlistBetaThankYouDialogViewModel + + let copy: WaitlistBetaThankYouCopy + + var body: some View { + VStack(spacing: 0) { + VStack { + Text(copy.title) + .font(.system(size: 17, weight: .semibold)) + .padding([.leading, .trailing], 21.5) + .padding([.top, .bottom], 24) + .fixedSize(horizontal: false, vertical: true) + } + .frame(maxWidth: .infinity) + .background(Color.backgroundSecondary) + + Divider() + + Image("Gift-96") + .resizable() + .frame(width: 96, height: 96) + .padding([.top, .bottom], 24) + + Text(copy.subtitle) + .font(.system(size: 17, weight: .semibold)) + .padding([.leading, .trailing, .bottom], 14) + + if #available(macOS 12.0, *) { + Text(copy.boldedBold1()) + .font(.system(size: 14)) + .padding([.leading, .trailing, .bottom], 14) + .lineSpacing(2) + } else { + Text(copy.body1) + .font(.system(size: 14)) + .padding([.leading, .trailing, .bottom], 14) + .lineSpacing(2) + } + + if #available(macOS 12.0, *) { + Text(copy.boldedBold2()) + .font(.system(size: 14)) + .padding([.leading, .trailing, .bottom], 14) + .lineSpacing(2) + } else { + Text(copy.body2) + .font(.system(size: 14)) + .padding([.leading, .trailing, .bottom], 14) + .lineSpacing(2) + } + + Spacer() + + button(text: "Close", action: .close) + .padding(16) + } + .multilineTextAlignment(.center) + } + + @ViewBuilder + func button(text: String, action: WaitlistBetaThankYouDialogViewModel.ViewAction) -> some View { + Button(action: { + Task { + await viewModel.process(action: action) + } + }, label: { + Text(text) + .frame(maxWidth: .infinity) + }) + .controlSize(.large) + .keyboardShortcut(.defaultAction) + .frame(maxWidth: .infinity) + } + +} + +extension WaitlistBetaThankYouDialogViewController: WaitlistBetaThankYouDialogViewModelDelegate { + + func waitlistBetaThankYouViewModelDismissedView(_ viewModel: WaitlistBetaThankYouDialogViewModel) { + dismiss() + } + +} From 2f0b7f20c4883619e1afb9b4be4e3b2eb317bb04 Mon Sep 17 00:00:00 2001 From: Tom Strba <57389842+tomasstrba@users.noreply.github.com> Date: Tue, 19 Mar 2024 17:28:04 +0100 Subject: [PATCH 39/51] Updated Settings Page (#2329) Task/Issue URL: https://app.asana.com/0/0/1206535251404777/f **Description**: Settings page reorganized to enhance discoverability of privacy protections and to unify structure across platforms --- DuckDuckGo.xcodeproj/project.pbxproj | 202 ++- .../Colors/AlertGreen.colorset/Contents.json | 20 + .../Contents.json | 2 +- .../Images/Accessibility.imageset/Icon 18.pdf | Bin 0 -> 6134 bytes .../Contents.json | 12 + .../CookieProtectionIcon.imageset/Icon 13.pdf | Bin 0 -> 3835 bytes .../Contents.json | 12 + .../EmailProtectionIcon.imageset/Icon 12.pdf | Bin 0 -> 6306 bytes .../FireSettings.imageset/Contents.json | 12 + .../Images/FireSettings.imageset/Icon 17.pdf | Bin 0 -> 33137 bytes .../Images/GeneralIcon.imageset/Contents.json | 12 + .../Images/GeneralIcon.imageset/Icon 16.pdf | Bin 0 -> 7715 bytes .../Images/HomePage/Contents.json | 6 + .../HomePage/Rocket.imageset/rocket.pdf | Bin 6315 -> 0 bytes .../Contents.json | 0 .../DownloadsPreferences.pdf | Bin .../PrivateSearchIcon.imageset/Contents.json | 12 + .../PrivateSearchIcon.imageset/Icon 10.pdf | Bin 0 -> 3482 bytes .../Contents.json | 12 + .../Icon 11.pdf | Bin 0 -> 4673 bytes .../Autoconsent/AutoconsentUserScript.swift | 6 +- .../ContentOverlayViewController.swift | 3 +- .../Common/Extensions/URLExtension.swift | 11 +- DuckDuckGo/Common/Localizables/UserText.swift | 45 +- .../ContentBlocker/ContentBlocking.swift | 2 +- .../ScriptSourceProviding.swift | 10 +- DuckDuckGo/DBP/DBPHomeViewController.swift | 4 +- DuckDuckGo/Email/EmailUrlExtensions.swift | 5 + .../Model/HomePageContinueSetUpModel.swift | 4 +- .../View/HomePageViewController.swift | 21 +- DuckDuckGo/InfoPlist.xcstrings | 16 +- DuckDuckGo/Localizable.xcstrings | 1155 ++++++++++++++++- .../View/AddressBarTextField.swift | 8 +- DuckDuckGo/Preferences/Model/AboutModel.swift | 7 +- .../Model/AccessibilityPreferences.swift | 69 + .../Model/AppearancePreferences.swift | 43 - .../CookiePopupProtectionPreferences.swift | 52 + ...el.swift => DataClearingPreferences.swift} | 47 +- .../Model/DefaultBrowserPreferences.swift | 28 + .../Model/PreferencesSection.swift | 110 +- .../Model/PreferencesSidebarModel.swift | 11 +- .../Model/PrivacyProtectionStatus.swift | 77 ++ .../Model/PrivacySecurityPreferences.swift | 41 - .../Preferences/Model/SearchPreferences.swift | 64 + .../WebTrackingProtectionPreferences.swift | 52 + .../View/PreferencesAboutView.swift | 4 +- .../View/PreferencesAccessibilityView.swift | 54 + .../View/PreferencesAppearanceView.swift | 19 - ...PreferencesCookiePopupProtectionView.swift | 55 + .../View/PreferencesDataClearingView.swift | 54 + .../View/PreferencesDefaultBrowserView.swift | 69 + .../View/PreferencesDownloadsView.swift | 65 - .../View/PreferencesEmailProtectionView.swift | 81 ++ .../View/PreferencesGeneralView.swift | 61 +- .../View/PreferencesPrivacyView.swift | 88 -- .../View/PreferencesPrivateSearchView.swift | 55 + .../View/PreferencesRootView.swift | 129 +- .../Preferences/View/PreferencesSidebar.swift | 99 +- .../View/PreferencesSyncView.swift | 47 + ...PreferencesWebTrackingProtectionView.swift | 61 + .../View/PrivacyDashboardViewController.swift | 4 +- .../View/SaveCredentialsViewController.swift | 2 +- DuckDuckGo/Statistics/PixelEvent.swift | 12 + DuckDuckGo/Statistics/PixelParameters.swift | 4 + .../SuggestionContainerViewModel.swift | 2 +- .../Tab/Model/UserContentUpdating.swift | 6 +- .../TabExtensions/AutofillTabExtension.swift | 2 +- .../NavigationProtectionTabExtension.swift | 3 +- DuckDuckGo/Tab/UserScripts/UserScripts.swift | 4 +- .../Tab/View/BrowserTabViewController.swift | 3 +- DuckDuckGo/Tab/ViewModel/TabViewModel.swift | 12 +- .../AutoconsentIntegrationTests.swift | 17 +- .../HTTPSUpgradeIntegrationTests.swift | 4 +- .../History/HistoryIntegrationTests.swift | 6 +- ...NavigationProtectionIntegrationTests.swift | 10 +- .../PrivacyDashboardIntegrationTests.swift | 4 +- .../Sources/PreferencesViews/Constants.swift | 1 + .../InfoPlist.xcstrings | 186 +++ .../AutoconsentMessageProtocolTests.swift | 4 +- .../ContentBlockingUpdatingTests.swift | 6 +- .../HomePage/ContinueSetUpModelTests.swift | 27 +- .../AccessibilityPreferencesTests.swift | 44 + .../AppearancePreferencesTests.swift | 26 - ...ookiePopupProtectionPreferencesTests.swift | 44 + .../DataClearingPreferencesTests.swift | 43 + .../PreferencesSidebarModelTests.swift | 6 +- .../PrivacyProtectionStatusTests.swift | 67 + .../Preferences/SearchPreferencesTests.swift | 44 + ...ebTrackingProtectionPreferencesTests.swift | 44 + .../Tab/ViewModel/TabViewModelTests.swift | 6 +- UnitTests/Tab/WebViewTests.swift | 4 +- ...wModelTests+WithoutPinnedTabsManager.swift | 4 +- .../TabCollectionViewModelTests.swift | 4 +- 93 files changed, 3115 insertions(+), 674 deletions(-) create mode 100644 DuckDuckGo/Assets.xcassets/Colors/AlertGreen.colorset/Contents.json rename DuckDuckGo/Assets.xcassets/Images/{HomePage/Rocket.imageset => Accessibility.imageset}/Contents.json (78%) create mode 100644 DuckDuckGo/Assets.xcassets/Images/Accessibility.imageset/Icon 18.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/CookieProtectionIcon.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/CookieProtectionIcon.imageset/Icon 13.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/EmailProtectionIcon.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/EmailProtectionIcon.imageset/Icon 12.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/FireSettings.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/FireSettings.imageset/Icon 17.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/GeneralIcon.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/GeneralIcon.imageset/Icon 16.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/HomePage/Contents.json delete mode 100644 DuckDuckGo/Assets.xcassets/Images/HomePage/Rocket.imageset/rocket.pdf rename DuckDuckGo/Assets.xcassets/Images/{DownloadsPreferences.imageset => OtherPlatformsPreferences.imageset}/Contents.json (100%) rename DuckDuckGo/Assets.xcassets/Images/{DownloadsPreferences.imageset => OtherPlatformsPreferences.imageset}/DownloadsPreferences.pdf (100%) create mode 100644 DuckDuckGo/Assets.xcassets/Images/PrivateSearchIcon.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/PrivateSearchIcon.imageset/Icon 10.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/WebTrackingProtectionIcon.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/WebTrackingProtectionIcon.imageset/Icon 11.pdf create mode 100644 DuckDuckGo/Preferences/Model/AccessibilityPreferences.swift create mode 100644 DuckDuckGo/Preferences/Model/CookiePopupProtectionPreferences.swift rename DuckDuckGo/Preferences/Model/{PrivacyPreferencesModel.swift => DataClearingPreferences.swift} (51%) create mode 100644 DuckDuckGo/Preferences/Model/PrivacyProtectionStatus.swift delete mode 100644 DuckDuckGo/Preferences/Model/PrivacySecurityPreferences.swift create mode 100644 DuckDuckGo/Preferences/Model/SearchPreferences.swift create mode 100644 DuckDuckGo/Preferences/Model/WebTrackingProtectionPreferences.swift create mode 100644 DuckDuckGo/Preferences/View/PreferencesAccessibilityView.swift create mode 100644 DuckDuckGo/Preferences/View/PreferencesCookiePopupProtectionView.swift create mode 100644 DuckDuckGo/Preferences/View/PreferencesDataClearingView.swift create mode 100644 DuckDuckGo/Preferences/View/PreferencesDefaultBrowserView.swift delete mode 100644 DuckDuckGo/Preferences/View/PreferencesDownloadsView.swift create mode 100644 DuckDuckGo/Preferences/View/PreferencesEmailProtectionView.swift delete mode 100644 DuckDuckGo/Preferences/View/PreferencesPrivacyView.swift create mode 100644 DuckDuckGo/Preferences/View/PreferencesPrivateSearchView.swift create mode 100644 DuckDuckGo/Preferences/View/PreferencesSyncView.swift create mode 100644 DuckDuckGo/Preferences/View/PreferencesWebTrackingProtectionView.swift create mode 100644 NetworkProtectionAppExtension/InfoPlist.xcstrings create mode 100644 UnitTests/Preferences/AccessibilityPreferencesTests.swift create mode 100644 UnitTests/Preferences/CookiePopupProtectionPreferencesTests.swift create mode 100644 UnitTests/Preferences/DataClearingPreferencesTests.swift create mode 100644 UnitTests/Preferences/PrivacyProtectionStatusTests.swift create mode 100644 UnitTests/Preferences/SearchPreferencesTests.swift create mode 100644 UnitTests/Preferences/WebTrackingProtectionPreferencesTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index ccafd9b0dd..66a3b706f0 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -17,6 +17,15 @@ 14505A08256084EF00272CC6 /* UserAgent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14505A07256084EF00272CC6 /* UserAgent.swift */; }; 1456D6E124EFCBC300775049 /* TabBarCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1456D6E024EFCBC300775049 /* TabBarCollectionView.swift */; }; 14D9B8FB24F7E089000D4D13 /* AddressBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14D9B8F924F7E089000D4D13 /* AddressBarViewController.swift */; }; + 1D01A3D02B88CEC600FE8150 /* PreferencesAccessibilityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D01A3CF2B88CEC600FE8150 /* PreferencesAccessibilityView.swift */; }; + 1D01A3D12B88CEC600FE8150 /* PreferencesAccessibilityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D01A3CF2B88CEC600FE8150 /* PreferencesAccessibilityView.swift */; }; + 1D01A3D22B88CEC600FE8150 /* PreferencesAccessibilityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D01A3CF2B88CEC600FE8150 /* PreferencesAccessibilityView.swift */; }; + 1D01A3D42B88CF7700FE8150 /* AccessibilityPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D01A3D32B88CF7700FE8150 /* AccessibilityPreferences.swift */; }; + 1D01A3D52B88CF7700FE8150 /* AccessibilityPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D01A3D32B88CF7700FE8150 /* AccessibilityPreferences.swift */; }; + 1D01A3D62B88CF7700FE8150 /* AccessibilityPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D01A3D32B88CF7700FE8150 /* AccessibilityPreferences.swift */; }; + 1D01A3D82B88DF8B00FE8150 /* PreferencesSyncView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D01A3D72B88DF8B00FE8150 /* PreferencesSyncView.swift */; }; + 1D01A3D92B88DF8B00FE8150 /* PreferencesSyncView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D01A3D72B88DF8B00FE8150 /* PreferencesSyncView.swift */; }; + 1D01A3DA2B88DF8B00FE8150 /* PreferencesSyncView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D01A3D72B88DF8B00FE8150 /* PreferencesSyncView.swift */; }; 1D02633628D8A9A9005CBB41 /* BWEncryption.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D02633528D8A9A9005CBB41 /* BWEncryption.m */; settings = {COMPILER_FLAGS = "-Wno-deprecated -Wno-strict-prototypes"; }; }; 1D074B272909A433006E4AC3 /* PasswordManagerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D074B262909A433006E4AC3 /* PasswordManagerCoordinator.swift */; }; 1D12F2E2298BC660009A65FD /* InternalUserDeciderStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D12F2E1298BC660009A65FD /* InternalUserDeciderStoreMock.swift */; }; @@ -26,6 +35,12 @@ 1D1C36E429FAE8DA001FA40C /* FaviconManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D1C36E229FAE8DA001FA40C /* FaviconManagerTests.swift */; }; 1D1C36E629FB019C001FA40C /* HistoryTabExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D1C36E529FB019C001FA40C /* HistoryTabExtensionTests.swift */; }; 1D1C36E729FB019C001FA40C /* HistoryTabExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D1C36E529FB019C001FA40C /* HistoryTabExtensionTests.swift */; }; + 1D220BF82B86192200F8BBC6 /* PreferencesEmailProtectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D220BF72B86192200F8BBC6 /* PreferencesEmailProtectionView.swift */; }; + 1D220BF92B86192200F8BBC6 /* PreferencesEmailProtectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D220BF72B86192200F8BBC6 /* PreferencesEmailProtectionView.swift */; }; + 1D220BFA2B86192200F8BBC6 /* PreferencesEmailProtectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D220BF72B86192200F8BBC6 /* PreferencesEmailProtectionView.swift */; }; + 1D220BFC2B87AACF00F8BBC6 /* PrivacyProtectionStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D220BFB2B87AACF00F8BBC6 /* PrivacyProtectionStatus.swift */; }; + 1D220BFD2B87AACF00F8BBC6 /* PrivacyProtectionStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D220BFB2B87AACF00F8BBC6 /* PrivacyProtectionStatus.swift */; }; + 1D220BFE2B87AACF00F8BBC6 /* PrivacyProtectionStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D220BFB2B87AACF00F8BBC6 /* PrivacyProtectionStatus.swift */; }; 1D26EBAC2B74BECB0002A93F /* NSImageSendable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D26EBAB2B74BECB0002A93F /* NSImageSendable.swift */; }; 1D26EBAD2B74BECB0002A93F /* NSImageSendable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D26EBAB2B74BECB0002A93F /* NSImageSendable.swift */; }; 1D26EBAE2B74BECB0002A93F /* NSImageSendable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D26EBAB2B74BECB0002A93F /* NSImageSendable.swift */; }; @@ -62,6 +77,7 @@ 1D77921A28FDC79800BE0210 /* FaviconStoringMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D77921928FDC79800BE0210 /* FaviconStoringMock.swift */; }; 1D8057C82A83CAEE00F4FED6 /* SupportedOsChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8057C72A83CAEE00F4FED6 /* SupportedOsChecker.swift */; }; 1D8057C92A83CB3C00F4FED6 /* SupportedOsChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8057C72A83CAEE00F4FED6 /* SupportedOsChecker.swift */; }; + 1D85BCCA2BA982FC0065BA04 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 1D85BCC92BA982FC0065BA04 /* InfoPlist.xcstrings */; }; 1D8B7D6A2A38BF050045C6F6 /* FireproofDomainsStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6BBF1712744CE36004F850E /* FireproofDomainsStoreMock.swift */; }; 1D8B7D6B2A38BF060045C6F6 /* FireproofDomainsStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6BBF1712744CE36004F850E /* FireproofDomainsStoreMock.swift */; }; 1D8C2FE52B70F4C4005E4BBD /* TabSnapshotExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8C2FE42B70F4C4005E4BBD /* TabSnapshotExtensionTests.swift */; }; @@ -75,6 +91,18 @@ 1D9A4E5A2B43213B00F449E2 /* TabSnapshotExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9A4E592B43213B00F449E2 /* TabSnapshotExtension.swift */; }; 1D9A4E5B2B43213B00F449E2 /* TabSnapshotExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9A4E592B43213B00F449E2 /* TabSnapshotExtension.swift */; }; 1D9A4E5C2B43213B00F449E2 /* TabSnapshotExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9A4E592B43213B00F449E2 /* TabSnapshotExtension.swift */; }; + 1D9FDEB72B9B5D150040B78C /* SearchPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9FDEB62B9B5D150040B78C /* SearchPreferencesTests.swift */; }; + 1D9FDEB82B9B5D150040B78C /* SearchPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9FDEB62B9B5D150040B78C /* SearchPreferencesTests.swift */; }; + 1D9FDEBA2B9B5E090040B78C /* WebTrackingProtectionPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9FDEB92B9B5E090040B78C /* WebTrackingProtectionPreferencesTests.swift */; }; + 1D9FDEBB2B9B5E090040B78C /* WebTrackingProtectionPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9FDEB92B9B5E090040B78C /* WebTrackingProtectionPreferencesTests.swift */; }; + 1D9FDEBD2B9B5F0F0040B78C /* CookiePopupProtectionPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9FDEBC2B9B5F0F0040B78C /* CookiePopupProtectionPreferencesTests.swift */; }; + 1D9FDEBE2B9B5F0F0040B78C /* CookiePopupProtectionPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9FDEBC2B9B5F0F0040B78C /* CookiePopupProtectionPreferencesTests.swift */; }; + 1D9FDEC02B9B5FEA0040B78C /* AccessibilityPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9FDEBF2B9B5FEA0040B78C /* AccessibilityPreferencesTests.swift */; }; + 1D9FDEC12B9B5FEA0040B78C /* AccessibilityPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9FDEBF2B9B5FEA0040B78C /* AccessibilityPreferencesTests.swift */; }; + 1D9FDEC32B9B63C90040B78C /* DataClearingPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9FDEC22B9B63C90040B78C /* DataClearingPreferencesTests.swift */; }; + 1D9FDEC42B9B63C90040B78C /* DataClearingPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9FDEC22B9B63C90040B78C /* DataClearingPreferencesTests.swift */; }; + 1D9FDEC62B9B64DB0040B78C /* PrivacyProtectionStatusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9FDEC52B9B64DB0040B78C /* PrivacyProtectionStatusTests.swift */; }; + 1D9FDEC72B9B64DB0040B78C /* PrivacyProtectionStatusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9FDEC52B9B64DB0040B78C /* PrivacyProtectionStatusTests.swift */; }; 1DA6D0FD2A1FF9A100540406 /* HTTPCookie.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA6D0FC2A1FF9A100540406 /* HTTPCookie.swift */; }; 1DA6D0FE2A1FF9A100540406 /* HTTPCookie.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA6D0FC2A1FF9A100540406 /* HTTPCookie.swift */; }; 1DA6D1022A1FFA3700540406 /* HTTPCookieTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA6D0FF2A1FF9DC00540406 /* HTTPCookieTests.swift */; }; @@ -94,6 +122,27 @@ 1DC669722B6CF0D700AA0645 /* TabSnapshotStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DC6696F2B6CF0D700AA0645 /* TabSnapshotStore.swift */; }; 1DCFBC8A29ADF32B00313531 /* BurnerHomePageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DCFBC8929ADF32B00313531 /* BurnerHomePageView.swift */; }; 1DCFBC8B29ADF32B00313531 /* BurnerHomePageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DCFBC8929ADF32B00313531 /* BurnerHomePageView.swift */; }; + 1DDC84F72B83558F00670238 /* PreferencesPrivateSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDC84F62B83558F00670238 /* PreferencesPrivateSearchView.swift */; }; + 1DDC84F82B83558F00670238 /* PreferencesPrivateSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDC84F62B83558F00670238 /* PreferencesPrivateSearchView.swift */; }; + 1DDC84F92B83558F00670238 /* PreferencesPrivateSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDC84F62B83558F00670238 /* PreferencesPrivateSearchView.swift */; }; + 1DDC84FB2B8356CE00670238 /* PreferencesDefaultBrowserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDC84FA2B8356CE00670238 /* PreferencesDefaultBrowserView.swift */; }; + 1DDC84FC2B8356CE00670238 /* PreferencesDefaultBrowserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDC84FA2B8356CE00670238 /* PreferencesDefaultBrowserView.swift */; }; + 1DDC84FD2B8356CE00670238 /* PreferencesDefaultBrowserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDC84FA2B8356CE00670238 /* PreferencesDefaultBrowserView.swift */; }; + 1DDC84FF2B835BC000670238 /* SearchPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDC84FE2B835BC000670238 /* SearchPreferences.swift */; }; + 1DDC85002B835BC000670238 /* SearchPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDC84FE2B835BC000670238 /* SearchPreferences.swift */; }; + 1DDC85012B835BC000670238 /* SearchPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDC84FE2B835BC000670238 /* SearchPreferences.swift */; }; + 1DDC85032B83903E00670238 /* PreferencesWebTrackingProtectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDC85022B83903E00670238 /* PreferencesWebTrackingProtectionView.swift */; }; + 1DDC85042B83903E00670238 /* PreferencesWebTrackingProtectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDC85022B83903E00670238 /* PreferencesWebTrackingProtectionView.swift */; }; + 1DDC85052B83903E00670238 /* PreferencesWebTrackingProtectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDC85022B83903E00670238 /* PreferencesWebTrackingProtectionView.swift */; }; + 1DDD3EBC2B84DCB9004CBF2B /* WebTrackingProtectionPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDD3EBB2B84DCB9004CBF2B /* WebTrackingProtectionPreferences.swift */; }; + 1DDD3EBD2B84DCB9004CBF2B /* WebTrackingProtectionPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDD3EBB2B84DCB9004CBF2B /* WebTrackingProtectionPreferences.swift */; }; + 1DDD3EBE2B84DCB9004CBF2B /* WebTrackingProtectionPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDD3EBB2B84DCB9004CBF2B /* WebTrackingProtectionPreferences.swift */; }; + 1DDD3EC02B84F5D5004CBF2B /* PreferencesCookiePopupProtectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDD3EBF2B84F5D5004CBF2B /* PreferencesCookiePopupProtectionView.swift */; }; + 1DDD3EC12B84F5D5004CBF2B /* PreferencesCookiePopupProtectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDD3EBF2B84F5D5004CBF2B /* PreferencesCookiePopupProtectionView.swift */; }; + 1DDD3EC22B84F5D5004CBF2B /* PreferencesCookiePopupProtectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDD3EBF2B84F5D5004CBF2B /* PreferencesCookiePopupProtectionView.swift */; }; + 1DDD3EC42B84F96B004CBF2B /* CookiePopupProtectionPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDD3EC32B84F96B004CBF2B /* CookiePopupProtectionPreferences.swift */; }; + 1DDD3EC52B84F96B004CBF2B /* CookiePopupProtectionPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDD3EC32B84F96B004CBF2B /* CookiePopupProtectionPreferences.swift */; }; + 1DDD3EC62B84F96B004CBF2B /* CookiePopupProtectionPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDD3EC32B84F96B004CBF2B /* CookiePopupProtectionPreferences.swift */; }; 1DDF076328F815AD00EDFBE3 /* BWCommunicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDF075D28F815AD00EDFBE3 /* BWCommunicator.swift */; }; 1DDF076428F815AD00EDFBE3 /* BWManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDF075E28F815AD00EDFBE3 /* BWManager.swift */; }; 1DE03425298BC7F000CAB3D7 /* InternalUserDeciderStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D12F2E1298BC660009A65FD /* InternalUserDeciderStoreMock.swift */; }; @@ -318,7 +367,7 @@ 3706FB1A293F65D500E42796 /* OutlineSeparatorViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B92928626670D1600AD2C21 /* OutlineSeparatorViewCell.swift */; }; 3706FB1B293F65D500E42796 /* SafariDataImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB99CFD26FE191E001E4761 /* SafariDataImporter.swift */; }; 3706FB1D293F65D500E42796 /* StatisticsLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69B50342726A11F00758A2B /* StatisticsLoader.swift */; }; - 3706FB1F293F65D500E42796 /* PrivacyPreferencesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CD54C127F2FDD100F1F7B9 /* PrivacyPreferencesModel.swift */; }; + 3706FB1F293F65D500E42796 /* DataClearingPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CD54C127F2FDD100F1F7B9 /* DataClearingPreferences.swift */; }; 3706FB20293F65D500E42796 /* LocalUnprotectedDomains.swift in Sources */ = {isa = PBXBuildFile; fileRef = 336B39E22726B4B700C417D3 /* LocalUnprotectedDomains.swift */; }; 3706FB21293F65D500E42796 /* NavigationBarBadgeAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31CF3431288B0B1B0087244B /* NavigationBarBadgeAnimator.swift */; }; 3706FB22293F65D500E42796 /* NSTextViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 858A798426A8BB5D00A75A42 /* NSTextViewExtension.swift */; }; @@ -400,7 +449,6 @@ 3706FB83293F65D500E42796 /* NSApplicationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5C8F622591021700748EB7 /* NSApplicationExtension.swift */; }; 3706FB84293F65D500E42796 /* NSWindowExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA9E9A5525A3AE8400D1959D /* NSWindowExtension.swift */; }; 3706FB85293F65D500E42796 /* AddBookmarkPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC5E4C425D6A6E8007F5990 /* AddBookmarkPopover.swift */; }; - 3706FB86293F65D500E42796 /* PreferencesDownloadsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CC53EF27E8D1440028713D /* PreferencesDownloadsView.swift */; }; 3706FB87293F65D500E42796 /* ProcessExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68C2FB127706E6A00BF2C7D /* ProcessExtension.swift */; }; 3706FB88293F65D500E42796 /* PermissionAuthorizationQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6106BA526A7BEC80013B453 /* PermissionAuthorizationQuery.swift */; }; 3706FB89293F65D500E42796 /* BadgeAnimationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3171D6B9288984D00068632A /* BadgeAnimationView.swift */; }; @@ -418,7 +466,6 @@ 3706FB95293F65D500E42796 /* PermissionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6106BAA26A7BF1D0013B453 /* PermissionType.swift */; }; 3706FB96293F65D500E42796 /* RecentlyClosedWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC6881A28626C1900D54247 /* RecentlyClosedWindow.swift */; }; 3706FB97293F65D500E42796 /* ActionSpeech.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85707F29276A35FE00DC0649 /* ActionSpeech.swift */; }; - 3706FB99293F65D500E42796 /* PrivacySecurityPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0511A6262CAA5A00F6079C /* PrivacySecurityPreferences.swift */; }; 3706FB9A293F65D500E42796 /* FireproofDomainsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6830962274CDEC7004B46BB /* FireproofDomainsStore.swift */; }; 3706FB9B293F65D500E42796 /* PrivacyDashboardPermissionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7E2E932902AC0E00C01B54 /* PrivacyDashboardPermissionHandler.swift */; }; 3706FB9C293F65D500E42796 /* TabCollectionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA9FF95E24A1FB680039E328 /* TabCollectionViewModel.swift */; }; @@ -509,7 +556,7 @@ 3706FC01293F65D500E42796 /* ChromiumBookmarksReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB99CF926FE191E001E4761 /* ChromiumBookmarksReader.swift */; }; 3706FC02293F65D500E42796 /* Downloads.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = B6C0B23226E71BCD0031CB7F /* Downloads.xcdatamodeld */; }; 3706FC03293F65D500E42796 /* TabPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE8B10F258A456C00E81239 /* TabPreviewViewController.swift */; }; - 3706FC04293F65D500E42796 /* PreferencesPrivacyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CC53EB27E8A4D10028713D /* PreferencesPrivacyView.swift */; }; + 3706FC04293F65D500E42796 /* PreferencesDataClearingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CC53EB27E8A4D10028713D /* PreferencesDataClearingView.swift */; }; 3706FC05293F65D500E42796 /* NSPasteboardExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0135CD2729F1AA00D54834 /* NSPasteboardExtension.swift */; }; 3706FC06293F65D500E42796 /* OnboardingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85707F30276A7DCA00DC0649 /* OnboardingViewModel.swift */; }; 3706FC07293F65D500E42796 /* ScriptSourceProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85AC3B0425D6B1D800C7D2AA /* ScriptSourceProviding.swift */; }; @@ -1022,15 +1069,14 @@ 37BF3F22286F0A7A00BD9014 /* PinnedTabsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BF3F1F286F0A7A00BD9014 /* PinnedTabsView.swift */; }; 37CBCA9A2A8966E60050218F /* SyncSettingsAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CBCA992A8966E60050218F /* SyncSettingsAdapter.swift */; }; 37CBCA9B2A8966E60050218F /* SyncSettingsAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CBCA992A8966E60050218F /* SyncSettingsAdapter.swift */; }; - 37CC53EC27E8A4D10028713D /* PreferencesPrivacyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CC53EB27E8A4D10028713D /* PreferencesPrivacyView.swift */; }; - 37CC53F027E8D1440028713D /* PreferencesDownloadsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CC53EF27E8D1440028713D /* PreferencesDownloadsView.swift */; }; + 37CC53EC27E8A4D10028713D /* PreferencesDataClearingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CC53EB27E8A4D10028713D /* PreferencesDataClearingView.swift */; }; 37CC53F427E8D4620028713D /* NSPathControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CC53F327E8D4620028713D /* NSPathControlView.swift */; }; 37CD54B527F1AC1300F1F7B9 /* PreferencesSidebarModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CD54B427F1AC1300F1F7B9 /* PreferencesSidebarModelTests.swift */; }; 37CD54B727F1B28A00F1F7B9 /* DefaultBrowserPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CD54B627F1B28A00F1F7B9 /* DefaultBrowserPreferencesTests.swift */; }; 37CD54B927F1F8AC00F1F7B9 /* AppearancePreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CD54B827F1F8AC00F1F7B9 /* AppearancePreferencesTests.swift */; }; 37CD54BB27F25A4000F1F7B9 /* DownloadsPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CD54BA27F25A4000F1F7B9 /* DownloadsPreferencesTests.swift */; }; 37CD54BD27F2ECAE00F1F7B9 /* AutofillPreferencesModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CD54BC27F2ECAE00F1F7B9 /* AutofillPreferencesModelTests.swift */; }; - 37CD54C927F2FDD100F1F7B9 /* PrivacyPreferencesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CD54C127F2FDD100F1F7B9 /* PrivacyPreferencesModel.swift */; }; + 37CD54C927F2FDD100F1F7B9 /* DataClearingPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CD54C127F2FDD100F1F7B9 /* DataClearingPreferences.swift */; }; 37CD54CA27F2FDD100F1F7B9 /* AutofillPreferencesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CD54C227F2FDD100F1F7B9 /* AutofillPreferencesModel.swift */; }; 37CD54CB27F2FDD100F1F7B9 /* DownloadsPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CD54C327F2FDD100F1F7B9 /* DownloadsPreferences.swift */; }; 37CD54CC27F2FDD100F1F7B9 /* PreferencesSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CD54C427F2FDD100F1F7B9 /* PreferencesSection.swift */; }; @@ -1060,7 +1106,6 @@ 4B02198A25E05FAC00ED7DEA /* FireproofDomains.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B02198125E05FAC00ED7DEA /* FireproofDomains.swift */; }; 4B02199C25E063DE00ED7DEA /* FireproofDomainsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B02199925E063DE00ED7DEA /* FireproofDomainsTests.swift */; }; 4B0219A825E0646500ED7DEA /* WebsiteDataStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0219A725E0646500ED7DEA /* WebsiteDataStoreTests.swift */; }; - 4B0511BD262CAA5A00F6079C /* PrivacySecurityPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0511A6262CAA5A00F6079C /* PrivacySecurityPreferences.swift */; }; 4B0511C3262CAA5A00F6079C /* FireproofDomains.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4B0511AD262CAA5A00F6079C /* FireproofDomains.storyboard */; }; 4B0511CA262CAA5A00F6079C /* FireproofDomainsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0511B4262CAA5A00F6079C /* FireproofDomainsViewController.swift */; }; 4B0511E1262CAA8600F6079C /* NSOpenPanelExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0511DF262CAA8600F6079C /* NSOpenPanelExtensions.swift */; }; @@ -1466,7 +1511,7 @@ 4B957A032AC7AE700062CA31 /* LocalBookmarkStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 987799F829999973005D8EB6 /* LocalBookmarkStore.swift */; }; 4B957A042AC7AE700062CA31 /* BWEncryption.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D02633528D8A9A9005CBB41 /* BWEncryption.m */; settings = {COMPILER_FLAGS = "-Wno-deprecated -Wno-strict-prototypes"; }; }; 4B957A052AC7AE700062CA31 /* StatisticsLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69B50342726A11F00758A2B /* StatisticsLoader.swift */; }; - 4B957A072AC7AE700062CA31 /* PrivacyPreferencesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CD54C127F2FDD100F1F7B9 /* PrivacyPreferencesModel.swift */; }; + 4B957A072AC7AE700062CA31 /* DataClearingPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CD54C127F2FDD100F1F7B9 /* DataClearingPreferences.swift */; }; 4B957A082AC7AE700062CA31 /* LocalUnprotectedDomains.swift in Sources */ = {isa = PBXBuildFile; fileRef = 336B39E22726B4B700C417D3 /* LocalUnprotectedDomains.swift */; }; 4B957A092AC7AE700062CA31 /* InternalUserDeciderStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D36E657298AA3BA00AA485D /* InternalUserDeciderStore.swift */; }; 4B957A0A2AC7AE700062CA31 /* NewWindowPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B634DBE4293C944700C3C99E /* NewWindowPolicy.swift */; }; @@ -1584,7 +1629,6 @@ 4B957A812AC7AE700062CA31 /* KeychainType+ClientDefault.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD3AF5C2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift */; }; 4B957A822AC7AE700062CA31 /* SyncDebugMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 370A34B02AB24E3700C77F7C /* SyncDebugMenu.swift */; }; 4B957A832AC7AE700062CA31 /* AddBookmarkPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC5E4C425D6A6E8007F5990 /* AddBookmarkPopover.swift */; }; - 4B957A842AC7AE700062CA31 /* PreferencesDownloadsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CC53EF27E8D1440028713D /* PreferencesDownloadsView.swift */; }; 4B957A852AC7AE700062CA31 /* QRSharingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F7127D29F6779000594A45 /* QRSharingService.swift */; }; 4B957A862AC7AE700062CA31 /* ProcessExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68C2FB127706E6A00BF2C7D /* ProcessExtension.swift */; }; 4B957A872AC7AE700062CA31 /* PermissionAuthorizationQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6106BA526A7BEC80013B453 /* PermissionAuthorizationQuery.swift */; }; @@ -1605,7 +1649,6 @@ 4B957A962AC7AE700062CA31 /* PermissionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6106BAA26A7BF1D0013B453 /* PermissionType.swift */; }; 4B957A982AC7AE700062CA31 /* RecentlyClosedWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC6881A28626C1900D54247 /* RecentlyClosedWindow.swift */; }; 4B957A992AC7AE700062CA31 /* ActionSpeech.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85707F29276A35FE00DC0649 /* ActionSpeech.swift */; }; - 4B957A9A2AC7AE700062CA31 /* PrivacySecurityPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0511A6262CAA5A00F6079C /* PrivacySecurityPreferences.swift */; }; 4B957A9B2AC7AE700062CA31 /* ModalSheetCancellable.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6BE9FA9293F7955006363C6 /* ModalSheetCancellable.swift */; }; 4B957A9C2AC7AE700062CA31 /* FireproofDomainsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6830962274CDEC7004B46BB /* FireproofDomainsStore.swift */; }; 4B957A9D2AC7AE700062CA31 /* NetworkProtectionSimulateFailureMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B430EA02A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift */; }; @@ -1725,7 +1768,7 @@ 4B957B152AC7AE700062CA31 /* ChromiumBookmarksReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB99CF926FE191E001E4761 /* ChromiumBookmarksReader.swift */; }; 4B957B162AC7AE700062CA31 /* Downloads.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = B6C0B23226E71BCD0031CB7F /* Downloads.xcdatamodeld */; }; 4B957B172AC7AE700062CA31 /* TabPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE8B10F258A456C00E81239 /* TabPreviewViewController.swift */; }; - 4B957B182AC7AE700062CA31 /* PreferencesPrivacyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CC53EB27E8A4D10028713D /* PreferencesPrivacyView.swift */; }; + 4B957B182AC7AE700062CA31 /* PreferencesDataClearingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CC53EB27E8A4D10028713D /* PreferencesDataClearingView.swift */; }; 4B957B192AC7AE700062CA31 /* NSPasteboardExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0135CD2729F1AA00D54834 /* NSPasteboardExtension.swift */; }; 4B957B1A2AC7AE700062CA31 /* OnboardingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85707F30276A7DCA00DC0649 /* OnboardingViewModel.swift */; }; 4B957B1B2AC7AE700062CA31 /* ScriptSourceProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85AC3B0425D6B1D800C7D2AA /* ScriptSourceProviding.swift */; }; @@ -3397,6 +3440,9 @@ 14505A07256084EF00272CC6 /* UserAgent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAgent.swift; sourceTree = ""; }; 1456D6E024EFCBC300775049 /* TabBarCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarCollectionView.swift; sourceTree = ""; }; 14D9B8F924F7E089000D4D13 /* AddressBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressBarViewController.swift; sourceTree = ""; }; + 1D01A3CF2B88CEC600FE8150 /* PreferencesAccessibilityView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesAccessibilityView.swift; sourceTree = ""; }; + 1D01A3D32B88CF7700FE8150 /* AccessibilityPreferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessibilityPreferences.swift; sourceTree = ""; }; + 1D01A3D72B88DF8B00FE8150 /* PreferencesSyncView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesSyncView.swift; sourceTree = ""; }; 1D02633428D8A9A9005CBB41 /* BWEncryption.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BWEncryption.h; sourceTree = ""; }; 1D02633528D8A9A9005CBB41 /* BWEncryption.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BWEncryption.m; sourceTree = ""; }; 1D074B262909A433006E4AC3 /* PasswordManagerCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordManagerCoordinator.swift; sourceTree = ""; }; @@ -3404,6 +3450,8 @@ 1D1A33482A6FEB170080ACED /* BurnerMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BurnerMode.swift; sourceTree = ""; }; 1D1C36E229FAE8DA001FA40C /* FaviconManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconManagerTests.swift; sourceTree = ""; }; 1D1C36E529FB019C001FA40C /* HistoryTabExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryTabExtensionTests.swift; sourceTree = ""; }; + 1D220BF72B86192200F8BBC6 /* PreferencesEmailProtectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesEmailProtectionView.swift; sourceTree = ""; }; + 1D220BFB2B87AACF00F8BBC6 /* PrivacyProtectionStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyProtectionStatus.swift; sourceTree = ""; }; 1D26EBAB2B74BECB0002A93F /* NSImageSendable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSImageSendable.swift; sourceTree = ""; }; 1D26EBAF2B74DB600002A93F /* TabSnapshotCleanupService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabSnapshotCleanupService.swift; sourceTree = ""; }; 1D36E657298AA3BA00AA485D /* InternalUserDeciderStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalUserDeciderStore.swift; sourceTree = ""; }; @@ -3428,11 +3476,18 @@ 1D77921928FDC79800BE0210 /* FaviconStoringMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconStoringMock.swift; sourceTree = ""; }; 1D77921C28FFF27C00BE0210 /* RunningApplicationCheck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunningApplicationCheck.swift; sourceTree = ""; }; 1D8057C72A83CAEE00F4FED6 /* SupportedOsChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportedOsChecker.swift; sourceTree = ""; }; + 1D85BCC92BA982FC0065BA04 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = ""; }; 1D8C2FE42B70F4C4005E4BBD /* TabSnapshotExtensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabSnapshotExtensionTests.swift; sourceTree = ""; }; 1D8C2FE92B70F5A7005E4BBD /* MockWebViewSnapshotRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockWebViewSnapshotRenderer.swift; sourceTree = ""; }; 1D8C2FEC2B70F5D0005E4BBD /* MockViewSnapshotRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockViewSnapshotRenderer.swift; sourceTree = ""; }; 1D8C2FEF2B70F751005E4BBD /* MockTabSnapshotStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockTabSnapshotStore.swift; sourceTree = ""; }; 1D9A4E592B43213B00F449E2 /* TabSnapshotExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabSnapshotExtension.swift; sourceTree = ""; }; + 1D9FDEB62B9B5D150040B78C /* SearchPreferencesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchPreferencesTests.swift; sourceTree = ""; }; + 1D9FDEB92B9B5E090040B78C /* WebTrackingProtectionPreferencesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebTrackingProtectionPreferencesTests.swift; sourceTree = ""; }; + 1D9FDEBC2B9B5F0F0040B78C /* CookiePopupProtectionPreferencesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CookiePopupProtectionPreferencesTests.swift; sourceTree = ""; }; + 1D9FDEBF2B9B5FEA0040B78C /* AccessibilityPreferencesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessibilityPreferencesTests.swift; sourceTree = ""; }; + 1D9FDEC22B9B63C90040B78C /* DataClearingPreferencesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataClearingPreferencesTests.swift; sourceTree = ""; }; + 1D9FDEC52B9B64DB0040B78C /* PrivacyProtectionStatusTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivacyProtectionStatusTests.swift; sourceTree = ""; }; 1DA6D0FC2A1FF9A100540406 /* HTTPCookie.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPCookie.swift; sourceTree = ""; }; 1DA6D0FF2A1FF9DC00540406 /* HTTPCookieTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPCookieTests.swift; sourceTree = ""; }; 1DB67F282B6FE4A6003DF243 /* WebViewSnapshotRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewSnapshotRenderer.swift; sourceTree = ""; }; @@ -3441,6 +3496,13 @@ 1DB9617F29F67F3E00CF5568 /* FaviconNullStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconNullStore.swift; sourceTree = ""; }; 1DC6696F2B6CF0D700AA0645 /* TabSnapshotStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabSnapshotStore.swift; sourceTree = ""; }; 1DCFBC8929ADF32B00313531 /* BurnerHomePageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BurnerHomePageView.swift; sourceTree = ""; }; + 1DDC84F62B83558F00670238 /* PreferencesPrivateSearchView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesPrivateSearchView.swift; sourceTree = ""; }; + 1DDC84FA2B8356CE00670238 /* PreferencesDefaultBrowserView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesDefaultBrowserView.swift; sourceTree = ""; }; + 1DDC84FE2B835BC000670238 /* SearchPreferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchPreferences.swift; sourceTree = ""; }; + 1DDC85022B83903E00670238 /* PreferencesWebTrackingProtectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesWebTrackingProtectionView.swift; sourceTree = ""; }; + 1DDD3EBB2B84DCB9004CBF2B /* WebTrackingProtectionPreferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebTrackingProtectionPreferences.swift; sourceTree = ""; }; + 1DDD3EBF2B84F5D5004CBF2B /* PreferencesCookiePopupProtectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesCookiePopupProtectionView.swift; sourceTree = ""; }; + 1DDD3EC32B84F96B004CBF2B /* CookiePopupProtectionPreferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CookiePopupProtectionPreferences.swift; sourceTree = ""; }; 1DDF075C28F815AD00EDFBE3 /* BWCredential.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BWCredential.swift; sourceTree = ""; }; 1DDF075D28F815AD00EDFBE3 /* BWCommunicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BWCommunicator.swift; sourceTree = ""; }; 1DDF075E28F815AD00EDFBE3 /* BWManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BWManager.swift; sourceTree = ""; }; @@ -3565,15 +3627,14 @@ 37BF3F1E286F0A7A00BD9014 /* PinnedTabsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PinnedTabsViewModel.swift; sourceTree = ""; }; 37BF3F1F286F0A7A00BD9014 /* PinnedTabsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PinnedTabsView.swift; sourceTree = ""; }; 37CBCA992A8966E60050218F /* SyncSettingsAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncSettingsAdapter.swift; sourceTree = ""; }; - 37CC53EB27E8A4D10028713D /* PreferencesPrivacyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesPrivacyView.swift; sourceTree = ""; }; - 37CC53EF27E8D1440028713D /* PreferencesDownloadsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesDownloadsView.swift; sourceTree = ""; }; + 37CC53EB27E8A4D10028713D /* PreferencesDataClearingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesDataClearingView.swift; sourceTree = ""; }; 37CC53F327E8D4620028713D /* NSPathControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSPathControlView.swift; sourceTree = ""; }; 37CD54B427F1AC1300F1F7B9 /* PreferencesSidebarModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesSidebarModelTests.swift; sourceTree = ""; }; 37CD54B627F1B28A00F1F7B9 /* DefaultBrowserPreferencesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultBrowserPreferencesTests.swift; sourceTree = ""; }; 37CD54B827F1F8AC00F1F7B9 /* AppearancePreferencesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearancePreferencesTests.swift; sourceTree = ""; }; 37CD54BA27F25A4000F1F7B9 /* DownloadsPreferencesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsPreferencesTests.swift; sourceTree = ""; }; 37CD54BC27F2ECAE00F1F7B9 /* AutofillPreferencesModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillPreferencesModelTests.swift; sourceTree = ""; }; - 37CD54C127F2FDD100F1F7B9 /* PrivacyPreferencesModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivacyPreferencesModel.swift; sourceTree = ""; }; + 37CD54C127F2FDD100F1F7B9 /* DataClearingPreferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataClearingPreferences.swift; sourceTree = ""; }; 37CD54C227F2FDD100F1F7B9 /* AutofillPreferencesModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutofillPreferencesModel.swift; sourceTree = ""; }; 37CD54C327F2FDD100F1F7B9 /* DownloadsPreferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadsPreferences.swift; sourceTree = ""; }; 37CD54C427F2FDD100F1F7B9 /* PreferencesSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesSection.swift; sourceTree = ""; }; @@ -3602,7 +3663,6 @@ 4B02198125E05FAC00ED7DEA /* FireproofDomains.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FireproofDomains.swift; sourceTree = ""; }; 4B02199925E063DE00ED7DEA /* FireproofDomainsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FireproofDomainsTests.swift; sourceTree = ""; }; 4B0219A725E0646500ED7DEA /* WebsiteDataStoreTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebsiteDataStoreTests.swift; sourceTree = ""; }; - 4B0511A6262CAA5A00F6079C /* PrivacySecurityPreferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivacySecurityPreferences.swift; sourceTree = ""; }; 4B0511AD262CAA5A00F6079C /* FireproofDomains.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = FireproofDomains.storyboard; sourceTree = ""; }; 4B0511B3262CAA5A00F6079C /* RoundedSelectionRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoundedSelectionRowView.swift; sourceTree = ""; }; 4B0511B4262CAA5A00F6079C /* FireproofDomainsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FireproofDomainsViewController.swift; sourceTree = ""; }; @@ -5315,18 +5375,22 @@ children = ( 37CD54C427F2FDD100F1F7B9 /* PreferencesSection.swift */, 37CD54C627F2FDD100F1F7B9 /* PreferencesSidebarModel.swift */, - 37A4CEB9282E992F00D75B89 /* StartupPreferences.swift */, 37CD54C827F2FDD100F1F7B9 /* DefaultBrowserPreferences.swift */, + 1DDC84FE2B835BC000670238 /* SearchPreferences.swift */, + 1DDD3EBB2B84DCB9004CBF2B /* WebTrackingProtectionPreferences.swift */, + 1DDD3EC32B84F96B004CBF2B /* CookiePopupProtectionPreferences.swift */, + 4B41EDA22B1543B9001EEDF4 /* VPNPreferencesModel.swift */, + 37A4CEB9282E992F00D75B89 /* StartupPreferences.swift */, + 37CD54C327F2FDD100F1F7B9 /* DownloadsPreferences.swift */, 3775912C29AAC72700E26367 /* SyncPreferences.swift */, 37CD54C727F2FDD100F1F7B9 /* AppearancePreferences.swift */, - 37CD54C127F2FDD100F1F7B9 /* PrivacyPreferencesModel.swift */, - 4B0511A6262CAA5A00F6079C /* PrivacySecurityPreferences.swift */, - 37F19A6628E1B43200740DC6 /* DuckPlayerPreferences.swift */, 37CD54C227F2FDD100F1F7B9 /* AutofillPreferencesModel.swift */, 3776582E27F82E62009A6B35 /* AutofillPreferences.swift */, - 37CD54C327F2FDD100F1F7B9 /* DownloadsPreferences.swift */, + 1D01A3D32B88CF7700FE8150 /* AccessibilityPreferences.swift */, + 37CD54C127F2FDD100F1F7B9 /* DataClearingPreferences.swift */, + 37F19A6628E1B43200740DC6 /* DuckPlayerPreferences.swift */, 37CD54C527F2FDD100F1F7B9 /* AboutModel.swift */, - 4B41EDA22B1543B9001EEDF4 /* VPNPreferencesModel.swift */, + 1D220BFB2B87AACF00F8BBC6 /* PrivacyProtectionStatus.swift */, ); path = Model; sourceTree = ""; @@ -5404,13 +5468,19 @@ 37AFCE8627DA334800471A10 /* PreferencesRootView.swift */, 37AFCE8427DA2D3900471A10 /* PreferencesSidebar.swift */, 37AFCE8A27DB69BC00471A10 /* PreferencesGeneralView.swift */, + 1D01A3D72B88DF8B00FE8150 /* PreferencesSyncView.swift */, + 1DDC84FA2B8356CE00670238 /* PreferencesDefaultBrowserView.swift */, + 1DDC84F62B83558F00670238 /* PreferencesPrivateSearchView.swift */, + 1DDC85022B83903E00670238 /* PreferencesWebTrackingProtectionView.swift */, + 1DDD3EBF2B84F5D5004CBF2B /* PreferencesCookiePopupProtectionView.swift */, + 1D220BF72B86192200F8BBC6 /* PreferencesEmailProtectionView.swift */, + 4B41EDA62B1543C9001EEDF4 /* PreferencesVPNView.swift */, 37D2771427E870D4003365FD /* PreferencesAppearanceView.swift */, - 37CC53EB27E8A4D10028713D /* PreferencesPrivacyView.swift */, - 37F19A6428E1B3FB00740DC6 /* PreferencesDuckPlayerView.swift */, 379DE4BC27EA31AC002CC3DE /* PreferencesAutofillView.swift */, - 37CC53EF27E8D1440028713D /* PreferencesDownloadsView.swift */, + 1D01A3CF2B88CEC600FE8150 /* PreferencesAccessibilityView.swift */, + 37CC53EB27E8A4D10028713D /* PreferencesDataClearingView.swift */, + 37F19A6428E1B3FB00740DC6 /* PreferencesDuckPlayerView.swift */, 37AFCE9127DB8CAD00471A10 /* PreferencesAboutView.swift */, - 4B41EDA62B1543C9001EEDF4 /* PreferencesVPNView.swift */, ); path = View; sourceTree = ""; @@ -5420,12 +5490,18 @@ children = ( 37CD54B427F1AC1300F1F7B9 /* PreferencesSidebarModelTests.swift */, 37CD54B627F1B28A00F1F7B9 /* DefaultBrowserPreferencesTests.swift */, - 37CD54B827F1F8AC00F1F7B9 /* AppearancePreferencesTests.swift */, + 378205F7283BC6A600D1D4AA /* StartupPreferencesTests.swift */, + 1D9FDEB62B9B5D150040B78C /* SearchPreferencesTests.swift */, + 1D9FDEB92B9B5E090040B78C /* WebTrackingProtectionPreferencesTests.swift */, + 1D9FDEBC2B9B5F0F0040B78C /* CookiePopupProtectionPreferencesTests.swift */, 37CD54BA27F25A4000F1F7B9 /* DownloadsPreferencesTests.swift */, + 37CD54B827F1F8AC00F1F7B9 /* AppearancePreferencesTests.swift */, 3776583027F8325B009A6B35 /* AutofillPreferencesTests.swift */, 37CD54BC27F2ECAE00F1F7B9 /* AutofillPreferencesModelTests.swift */, - 378205F7283BC6A600D1D4AA /* StartupPreferencesTests.swift */, + 1D9FDEBF2B9B5FEA0040B78C /* AccessibilityPreferencesTests.swift */, + 1D9FDEC22B9B63C90040B78C /* DataClearingPreferencesTests.swift */, 3714B1E628EDB7FA0056C57A /* DuckPlayerPreferencesTests.swift */, + 1D9FDEC52B9B64DB0040B78C /* PrivacyProtectionStatusTests.swift */, ); path = Preferences; sourceTree = ""; @@ -5711,6 +5787,7 @@ 4B5F14F72A148B230060320F /* NetworkProtectionAppExtension */ = { isa = PBXGroup; children = ( + 1D85BCC92BA982FC0065BA04 /* InfoPlist.xcstrings */, 4B5F14F82A148B230060320F /* Info.plist */, ); path = NetworkProtectionAppExtension; @@ -9432,6 +9509,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 1D85BCCA2BA982FC0065BA04 /* InfoPlist.xcstrings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -9936,6 +10014,7 @@ 3706FA94293F65D500E42796 /* TabShadowConfig.swift in Sources */, 3706FA97293F65D500E42796 /* WindowDraggingView.swift in Sources */, B60D644A2AAF1B7C00B26F50 /* AddressBarTextSelectionNavigation.swift in Sources */, + 1D01A3D52B88CF7700FE8150 /* AccessibilityPreferences.swift in Sources */, 3706FA98293F65D500E42796 /* SecureVaultSorting.swift in Sources */, 3706FA99293F65D500E42796 /* PreferencesSidebarModel.swift in Sources */, 3706FA9A293F65D500E42796 /* DuckPlayerURLExtension.swift in Sources */, @@ -9976,6 +10055,7 @@ 4B9DB0332A983B24000927DB /* EnableWaitlistFeatureView.swift in Sources */, 3706FABA293F65D500E42796 /* BookmarkOutlineViewDataSource.swift in Sources */, 3706FABB293F65D500E42796 /* PasswordManagementBitwardenItemView.swift in Sources */, + 1D220BF92B86192200F8BBC6 /* PreferencesEmailProtectionView.swift in Sources */, 9FA173E42B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift in Sources */, 3706FABD293F65D500E42796 /* NSNotificationName+PasswordManager.swift in Sources */, 3706FABE293F65D500E42796 /* RulesCompilationMonitor.swift in Sources */, @@ -9988,10 +10068,12 @@ F1D43AEF2B98D8DF00BAB743 /* MainMenuActions+VanillaBrowser.swift in Sources */, B6E1491129A5C30A00AAFBE8 /* FBProtectionTabExtension.swift in Sources */, 3706FAC4293F65D500E42796 /* PrintingUserScript.swift in Sources */, + 1D01A3D92B88DF8B00FE8150 /* PreferencesSyncView.swift in Sources */, 9D9AE86C2AA76D1B0026E7DC /* LoginItemsManager.swift in Sources */, 4B9DB0392A983B24000927DB /* JoinedWaitlistView.swift in Sources */, 3706FEBF293F6EFF00E42796 /* BWError.swift in Sources */, 3706FAC6293F65D500E42796 /* ConnectBitwardenViewController.swift in Sources */, + 1DDC84FC2B8356CE00670238 /* PreferencesDefaultBrowserView.swift in Sources */, EEC589DA2A4F1CE400BCD60C /* AppLauncher.swift in Sources */, 3706FAC8293F65D500E42796 /* AppTrackerDataSetProvider.swift in Sources */, 3706FAC9293F65D500E42796 /* EncryptionKeyGeneration.swift in Sources */, @@ -10018,6 +10100,8 @@ 3706FADE293F65D500E42796 /* PreferencesDuckPlayerView.swift in Sources */, EEC4A65E2B277E8D00F7C0AA /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */, B66260E729ACAE4B00E9E3EE /* NavigationHotkeyHandler.swift in Sources */, + 3706FADF293F65D500E42796 /* AddBookmarkFolderModalView.swift in Sources */, + 1DDD3EC52B84F96B004CBF2B /* CookiePopupProtectionPreferences.swift in Sources */, 3706FAE0293F65D500E42796 /* BookmarkSidebarTreeController.swift in Sources */, 3706FAE1293F65D500E42796 /* HomePageFavoritesModel.swift in Sources */, 3706FAE2293F65D500E42796 /* SequenceExtensions.swift in Sources */, @@ -10074,6 +10158,7 @@ 3706FB07293F65D500E42796 /* Publisher.asVoid.swift in Sources */, 3706FB08293F65D500E42796 /* NavigationButtonMenuDelegate.swift in Sources */, 4BF97ADB2B43C5E000EB4240 /* VPNMetadataCollector.swift in Sources */, + 1DDC84F82B83558F00670238 /* PreferencesPrivateSearchView.swift in Sources */, 3706FB09293F65D500E42796 /* CrashReport.swift in Sources */, 1E0C72072ABC63BD00802009 /* SubscriptionPagesUserScript.swift in Sources */, 3706FB0A293F65D500E42796 /* NSPathControlView.swift in Sources */, @@ -10100,7 +10185,8 @@ 3793FDD829535EBA00A2E28F /* Assertions.swift in Sources */, B62B48572ADE730D000DECE5 /* FileImportView.swift in Sources */, B6676BE22AA986A700525A21 /* AddressBarTextEditor.swift in Sources */, - 3706FB1F293F65D500E42796 /* PrivacyPreferencesModel.swift in Sources */, + 3706FB1F293F65D500E42796 /* DataClearingPreferences.swift in Sources */, + 1D01A3D12B88CEC600FE8150 /* PreferencesAccessibilityView.swift in Sources */, 3706FB20293F65D500E42796 /* LocalUnprotectedDomains.swift in Sources */, 3707C719294B5D0F00682A9F /* HoveredLinkTabExtension.swift in Sources */, 3706FB21293F65D500E42796 /* NavigationBarBadgeAnimator.swift in Sources */, @@ -10237,7 +10323,6 @@ 37197EA42942441D00394917 /* NewWindowPolicy.swift in Sources */, 3706FB84293F65D500E42796 /* NSWindowExtension.swift in Sources */, 3706FB85293F65D500E42796 /* AddBookmarkPopover.swift in Sources */, - 3706FB86293F65D500E42796 /* PreferencesDownloadsView.swift in Sources */, 7BBD45B22A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift in Sources */, 3706FB87293F65D500E42796 /* ProcessExtension.swift in Sources */, 3706FB88293F65D500E42796 /* PermissionAuthorizationQuery.swift in Sources */, @@ -10266,7 +10351,6 @@ B690152D2ACBF4DA00AD0BAB /* MenuPreview.swift in Sources */, 1D36F4252A3B85C50052B527 /* TabCleanupPreparer.swift in Sources */, 3706FB97293F65D500E42796 /* ActionSpeech.swift in Sources */, - 3706FB99293F65D500E42796 /* PrivacySecurityPreferences.swift in Sources */, B6AFE6BD29A5D621002FF962 /* HTTPSUpgradeTabExtension.swift in Sources */, 3706FB9A293F65D500E42796 /* FireproofDomainsStore.swift in Sources */, 3706FB9B293F65D500E42796 /* PrivacyDashboardPermissionHandler.swift in Sources */, @@ -10285,6 +10369,7 @@ 3706FBA2293F65D500E42796 /* GeolocationService.swift in Sources */, 4B4D60C42A0C849600BCD287 /* NetworkProtectionInvitePresenter.swift in Sources */, 3706FBA3293F65D500E42796 /* FireproofingURLExtensions.swift in Sources */, + 1DDD3EC12B84F5D5004CBF2B /* PreferencesCookiePopupProtectionView.swift in Sources */, 3706FBA4293F65D500E42796 /* ContentOverlayPopover.swift in Sources */, 3706FBA5293F65D500E42796 /* TabShadowView.swift in Sources */, 3706FBA7293F65D500E42796 /* EncryptedValueTransformer.swift in Sources */, @@ -10337,6 +10422,7 @@ 3706FBCC293F65D500E42796 /* CustomRoundedCornersShape.swift in Sources */, 3706FBCD293F65D500E42796 /* LocaleExtension.swift in Sources */, 3706FBCE293F65D500E42796 /* SavePaymentMethodViewController.swift in Sources */, + 1DDC85002B835BC000670238 /* SearchPreferences.swift in Sources */, 3706FBD0293F65D500E42796 /* WebKitVersionProvider.swift in Sources */, 3706FBD1293F65D500E42796 /* NSCoderExtensions.swift in Sources */, B6D6A5DD2982A4CE001F5F11 /* Tab+Navigation.swift in Sources */, @@ -10397,6 +10483,7 @@ 3706FBF3293F65D500E42796 /* PseudoFolder.swift in Sources */, 1D26EBAD2B74BECB0002A93F /* NSImageSendable.swift in Sources */, 3706FBF5293F65D500E42796 /* PixelDataStore.swift in Sources */, + 1D220BFD2B87AACF00F8BBC6 /* PrivacyProtectionStatus.swift in Sources */, 3706FBF6293F65D500E42796 /* Pixel.swift in Sources */, 3706FBF7293F65D500E42796 /* PixelEvent.swift in Sources */, 3706FBF8293F65D500E42796 /* TabBarFooter.swift in Sources */, @@ -10419,7 +10506,7 @@ 3707C720294B5D2900682A9F /* WKWebsiteDataStoreExtension.swift in Sources */, 3706FC03293F65D500E42796 /* TabPreviewViewController.swift in Sources */, 4B9754EC2984300100D7B834 /* EmailManagerExtension.swift in Sources */, - 3706FC04293F65D500E42796 /* PreferencesPrivacyView.swift in Sources */, + 3706FC04293F65D500E42796 /* PreferencesDataClearingView.swift in Sources */, 3706FC05293F65D500E42796 /* NSPasteboardExtension.swift in Sources */, 1DCFBC8B29ADF32B00313531 /* BurnerHomePageView.swift in Sources */, 3706FC06293F65D500E42796 /* OnboardingViewModel.swift in Sources */, @@ -10462,10 +10549,12 @@ 3706FC1D293F65D500E42796 /* EmailManagerRequestDelegate.swift in Sources */, 3706FC1E293F65D500E42796 /* ApplicationVersionReader.swift in Sources */, 3706FC1F293F65D500E42796 /* BookmarksBarViewController.swift in Sources */, + 1DDC85042B83903E00670238 /* PreferencesWebTrackingProtectionView.swift in Sources */, 3706FC20293F65D500E42796 /* PreferencesAutofillView.swift in Sources */, 3706FC21293F65D500E42796 /* UserText+PasswordManager.swift in Sources */, 3706FC22293F65D500E42796 /* LoadingProgressView.swift in Sources */, 3706FC23293F65D500E42796 /* StatisticsStore.swift in Sources */, + 1DDD3EBD2B84DCB9004CBF2B /* WebTrackingProtectionPreferences.swift in Sources */, 3706FC25293F65D500E42796 /* ColorView.swift in Sources */, 3706FC26293F65D500E42796 /* RecentlyClosedCacheItem.swift in Sources */, 3706FC27293F65D500E42796 /* PopupBlockedPopover.swift in Sources */, @@ -10672,6 +10761,7 @@ 3706FDF0293F661700E42796 /* WebKitVersionProviderTests.swift in Sources */, 3706FDF1293F661700E42796 /* AtbAndVariantCleanupTests.swift in Sources */, 1D1C36E729FB019C001FA40C /* HistoryTabExtensionTests.swift in Sources */, + 1D9FDEBB2B9B5E090040B78C /* WebTrackingProtectionPreferencesTests.swift in Sources */, 3706FDF3293F661700E42796 /* ChromiumLoginReaderTests.swift in Sources */, 3706FDF4293F661700E42796 /* TabCollectionTests.swift in Sources */, 3706FDF5293F661700E42796 /* StartupPreferencesTests.swift in Sources */, @@ -10695,6 +10785,7 @@ 3706FE01293F661700E42796 /* PixelStoreMock.swift in Sources */, 3706FE02293F661700E42796 /* BookmarksBarViewModelTests.swift in Sources */, 3706FE03293F661700E42796 /* CoreDataStoreTests.swift in Sources */, + 1D9FDEC42B9B63C90040B78C /* DataClearingPreferencesTests.swift in Sources */, 3706FE04293F661700E42796 /* TreeControllerTests.swift in Sources */, 3706FE05293F661700E42796 /* DownloadsWebViewMock.m in Sources */, 3706FE06293F661700E42796 /* CoreDataEncryptionTesting.xcdatamodeld in Sources */, @@ -10709,6 +10800,7 @@ 3706FE0E293F661700E42796 /* FirefoxDataImporterTests.swift in Sources */, 3706FE0F293F661700E42796 /* CSVLoginExporterTests.swift in Sources */, 3706FE10293F661700E42796 /* TestNavigationDelegate.swift in Sources */, + 1D9FDEC12B9B5FEA0040B78C /* AccessibilityPreferencesTests.swift in Sources */, 3706FE11293F661700E42796 /* URLSuggestedFilenameTests.swift in Sources */, 4BE344EF2B23786F003FC223 /* VPNFeedbackFormViewModelTests.swift in Sources */, B6F56569299A414300A04298 /* WKWebViewMockingExtension.swift in Sources */, @@ -10726,6 +10818,7 @@ 3706FE1C293F661700E42796 /* ConnectBitwardenViewModelTests.swift in Sources */, 4B9DB0552A983B55000927DB /* MockWaitlistStorage.swift in Sources */, 3706FE1D293F661700E42796 /* PixelStoreTests.swift in Sources */, + 1D9FDEC72B9B64DB0040B78C /* PrivacyProtectionStatusTests.swift in Sources */, C13909F52B85FD79001626ED /* AutofillDeleteAllPasswordsExecutorTests.swift in Sources */, 857E44642A9F70F200ED77A7 /* CampaignVariantTests.swift in Sources */, 3706FE1E293F661700E42796 /* GeolocationProviderTests.swift in Sources */, @@ -10825,6 +10918,7 @@ 37716D8029707E5D00A9FC6D /* FireproofingReferenceTests.swift in Sources */, B6AA64742994B43300D99CD6 /* FutureExtensionTests.swift in Sources */, 3706FE5C293F661700E42796 /* DuckPlayerPreferencesTests.swift in Sources */, + 1D9FDEB82B9B5D150040B78C /* SearchPreferencesTests.swift in Sources */, 3706FE5D293F661700E42796 /* FileSystemDSL.swift in Sources */, 3706FE5E293F661700E42796 /* DataImportMocks.swift in Sources */, 3706FE5F293F661700E42796 /* CrashReportTests.swift in Sources */, @@ -10845,6 +10939,7 @@ C1E961F42B87B276001760E1 /* MockAutofillActionPresenter.swift in Sources */, 3706FE6E293F661700E42796 /* FirefoxBookmarksReaderTests.swift in Sources */, 4B9DB05B2A983B55000927DB /* MockWaitlistRequest.swift in Sources */, + 1D9FDEBE2B9B5F0F0040B78C /* CookiePopupProtectionPreferencesTests.swift in Sources */, 028904212A7B25770028369C /* AppConfigurationURLProviderTests.swift in Sources */, 3706FE6F293F661700E42796 /* LocalStatisticsStoreTests.swift in Sources */, 3706FE70293F661700E42796 /* HistoryCoordinatorTests.swift in Sources */, @@ -11118,6 +11213,7 @@ 4B9579542AC7AE700062CA31 /* DownloadListStore.swift in Sources */, 4B9579552AC7AE700062CA31 /* Logging.swift in Sources */, 4B9579562AC7AE700062CA31 /* CrashReportPromptPresenter.swift in Sources */, + 1DDC84F92B83558F00670238 /* PreferencesPrivateSearchView.swift in Sources */, B6B4D1CD2B0C8C9200C26286 /* FirefoxCompatibilityPreferences.swift in Sources */, 9FA173ED2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift in Sources */, 4B9579572AC7AE700062CA31 /* BWCredential.swift in Sources */, @@ -11167,6 +11263,7 @@ 9F514F932B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift in Sources */, 4B95797C2AC7AE700062CA31 /* CrashReporter.swift in Sources */, 4B95797D2AC7AE700062CA31 /* AddressBarTextSelectionNavigation.swift in Sources */, + 1D01A3DA2B88DF8B00FE8150 /* PreferencesSyncView.swift in Sources */, 4B37EE7D2B4CFF8300A89A61 /* SurveyURLBuilder.swift in Sources */, 4B95797E2AC7AE700062CA31 /* BadgeNotificationAnimationModel.swift in Sources */, 4B95797F2AC7AE700062CA31 /* HyperLink.swift in Sources */, @@ -11203,6 +11300,7 @@ 4B95799E2AC7AE700062CA31 /* EncryptionKeyGeneration.swift in Sources */, 4B95799F2AC7AE700062CA31 /* TabLazyLoader.swift in Sources */, B690152F2ACBF4DA00AD0BAB /* MenuPreview.swift in Sources */, + 1D01A3D22B88CEC600FE8150 /* PreferencesAccessibilityView.swift in Sources */, 4B9579A02AC7AE700062CA31 /* InvitedToWaitlistView.swift in Sources */, 4B9579A22AC7AE700062CA31 /* SaveCredentialsViewController.swift in Sources */, 4B9579A32AC7AE700062CA31 /* PopUpButton.swift in Sources */, @@ -11233,6 +11331,7 @@ 4B9579BD2AC7AE700062CA31 /* ChromiumDataImporter.swift in Sources */, 4B9579BE2AC7AE700062CA31 /* BackForwardListItemViewModel.swift in Sources */, 4B9579BF2AC7AE700062CA31 /* BWNotRespondingAlert.swift in Sources */, + 1DDC85052B83903E00670238 /* PreferencesWebTrackingProtectionView.swift in Sources */, 4B9579C02AC7AE700062CA31 /* DebugUserScript.swift in Sources */, 1DC669722B6CF0D700AA0645 /* TabSnapshotStore.swift in Sources */, 4B9579C12AC7AE700062CA31 /* RecentlyClosedTab.swift in Sources */, @@ -11251,6 +11350,7 @@ 4B9579CE2AC7AE700062CA31 /* CredentialsCleanupErrorHandling.swift in Sources */, 4B9579CF2AC7AE700062CA31 /* SafariBookmarksReader.swift in Sources */, 4B9579D02AC7AE700062CA31 /* HTTPCookie.swift in Sources */, + 1DDD3EC62B84F96B004CBF2B /* CookiePopupProtectionPreferences.swift in Sources */, 4B9579D12AC7AE700062CA31 /* SafariVersionReader.swift in Sources */, 4B9579D22AC7AE700062CA31 /* LoginFaviconView.swift in Sources */, 4B9579D32AC7AE700062CA31 /* FireproofDomainsViewController.swift in Sources */, @@ -11272,6 +11372,7 @@ 4B9579E02AC7AE700062CA31 /* BookmarkExtension.swift in Sources */, 4B9579E12AC7AE700062CA31 /* PasswordManagementCreditCardModel.swift in Sources */, B677FC522B06376B0099EB04 /* ReportFeedbackView.swift in Sources */, + 1D220BFE2B87AACF00F8BBC6 /* PrivacyProtectionStatus.swift in Sources */, 4B9579E22AC7AE700062CA31 /* NSEventExtension.swift in Sources */, 1D26EBB22B74DB600002A93F /* TabSnapshotCleanupService.swift in Sources */, 4B9579E32AC7AE700062CA31 /* Onboarding.swift in Sources */, @@ -11313,7 +11414,7 @@ 4B957A032AC7AE700062CA31 /* LocalBookmarkStore.swift in Sources */, 4B957A042AC7AE700062CA31 /* BWEncryption.m in Sources */, 4B957A052AC7AE700062CA31 /* StatisticsLoader.swift in Sources */, - 4B957A072AC7AE700062CA31 /* PrivacyPreferencesModel.swift in Sources */, + 4B957A072AC7AE700062CA31 /* DataClearingPreferences.swift in Sources */, 4B957A082AC7AE700062CA31 /* LocalUnprotectedDomains.swift in Sources */, 4B957A092AC7AE700062CA31 /* InternalUserDeciderStore.swift in Sources */, 4B957A0A2AC7AE700062CA31 /* NewWindowPolicy.swift in Sources */, @@ -11323,6 +11424,7 @@ 4B957A0E2AC7AE700062CA31 /* UserDialogRequest.swift in Sources */, 4B957A0F2AC7AE700062CA31 /* DownloadsCellView.swift in Sources */, 4B957A112AC7AE700062CA31 /* PublishedAfter.swift in Sources */, + 1DDC85012B835BC000670238 /* SearchPreferences.swift in Sources */, B6B5F58C2B03673B008DB58A /* BrowserImportMoreInfoView.swift in Sources */, 4B957A122AC7AE700062CA31 /* FirefoxBerkeleyDatabaseReader.swift in Sources */, 4B957A132AC7AE700062CA31 /* WebViewSnapshotView.swift in Sources */, @@ -11437,6 +11539,7 @@ B6080BC82B21E78100B418EF /* DataImportErrorView.swift in Sources */, 4B957A722AC7AE700062CA31 /* FaviconHostReference.swift in Sources */, 4B957A732AC7AE700062CA31 /* DownloadsTabExtension.swift in Sources */, + 1D220BFA2B86192200F8BBC6 /* PreferencesEmailProtectionView.swift in Sources */, 4B957A752AC7AE700062CA31 /* ASN1Parser.swift in Sources */, 4B957A762AC7AE700062CA31 /* FileDownloadManager.swift in Sources */, 4B957A772AC7AE700062CA31 /* BookmarkImport.swift in Sources */, @@ -11458,7 +11561,6 @@ 4B41EDA52B1543B9001EEDF4 /* VPNPreferencesModel.swift in Sources */, 4B957A822AC7AE700062CA31 /* SyncDebugMenu.swift in Sources */, 4B957A832AC7AE700062CA31 /* AddBookmarkPopover.swift in Sources */, - 4B957A842AC7AE700062CA31 /* PreferencesDownloadsView.swift in Sources */, 4B957A852AC7AE700062CA31 /* QRSharingService.swift in Sources */, 4B957A862AC7AE700062CA31 /* ProcessExtension.swift in Sources */, B68412162B694BA10092F66A /* NSObject+performSelector.m in Sources */, @@ -11476,13 +11578,13 @@ 4B957A902AC7AE700062CA31 /* BookmarkManagementSplitViewController.swift in Sources */, 4B957A912AC7AE700062CA31 /* CookieManagedNotificationContainerView.swift in Sources */, 4B957A922AC7AE700062CA31 /* FileManagerExtension.swift in Sources */, + 1DDD3EBE2B84DCB9004CBF2B /* WebTrackingProtectionPreferences.swift in Sources */, 4B957A932AC7AE700062CA31 /* PermissionModel.swift in Sources */, 4B957A942AC7AE700062CA31 /* PasteboardFolder.swift in Sources */, 4B957A952AC7AE700062CA31 /* CookieManagedNotificationView.swift in Sources */, 4B957A962AC7AE700062CA31 /* PermissionType.swift in Sources */, 4B957A982AC7AE700062CA31 /* RecentlyClosedWindow.swift in Sources */, 4B957A992AC7AE700062CA31 /* ActionSpeech.swift in Sources */, - 4B957A9A2AC7AE700062CA31 /* PrivacySecurityPreferences.swift in Sources */, 4B957A9B2AC7AE700062CA31 /* ModalSheetCancellable.swift in Sources */, 4B957A9C2AC7AE700062CA31 /* FireproofDomainsStore.swift in Sources */, 4B957A9D2AC7AE700062CA31 /* NetworkProtectionSimulateFailureMenu.swift in Sources */, @@ -11508,6 +11610,7 @@ 4B957AAD2AC7AE700062CA31 /* Tab+Dialogs.swift in Sources */, 4B957AAE2AC7AE700062CA31 /* PasteboardBookmark.swift in Sources */, 4B957AAF2AC7AE700062CA31 /* PinnedTabsManager.swift in Sources */, + 1D01A3D62B88CF7700FE8150 /* AccessibilityPreferences.swift in Sources */, 4B957AB02AC7AE700062CA31 /* HoverUserScript.swift in Sources */, 4B957AB12AC7AE700062CA31 /* MainMenuActions.swift in Sources */, 4B957AB22AC7AE700062CA31 /* WKWebView+SessionState.swift in Sources */, @@ -11624,6 +11727,8 @@ B66CA4212AD910B300447CF0 /* DataImportView.swift in Sources */, 4B957B162AC7AE700062CA31 /* Downloads.xcdatamodeld in Sources */, 4B957B172AC7AE700062CA31 /* TabPreviewViewController.swift in Sources */, + B6F9BDDA2B45B7D900677B33 /* AddBookmarkModalViewModel.swift in Sources */, + 4B957B182AC7AE700062CA31 /* PreferencesDataClearingView.swift in Sources */, 4B957B182AC7AE700062CA31 /* PreferencesPrivacyView.swift in Sources */, 4B957B192AC7AE700062CA31 /* NSPasteboardExtension.swift in Sources */, 4B957B1A2AC7AE700062CA31 /* OnboardingViewModel.swift in Sources */, @@ -11676,6 +11781,7 @@ 4B957B462AC7AE700062CA31 /* Tab+NSSecureCoding.swift in Sources */, 4B957B472AC7AE700062CA31 /* NSNotificationName+EmailManager.swift in Sources */, B6619EFE2B111CCC00CD9186 /* InstructionsFormatParser.swift in Sources */, + 1DDD3EC22B84F5D5004CBF2B /* PreferencesCookiePopupProtectionView.swift in Sources */, 4B957B482AC7AE700062CA31 /* MouseOverButton.swift in Sources */, 4B957B492AC7AE700062CA31 /* FireInfoViewController.swift in Sources */, 4B957B4A2AC7AE700062CA31 /* LoginItem+NetworkProtection.swift in Sources */, @@ -11726,6 +11832,7 @@ 4B957B712AC7AE700062CA31 /* TabPreviewWindowController.swift in Sources */, 4B957B722AC7AE700062CA31 /* NSSizeExtension.swift in Sources */, 4B957B732AC7AE700062CA31 /* Fire.swift in Sources */, + 1DDC84FD2B8356CE00670238 /* PreferencesDefaultBrowserView.swift in Sources */, 4B957B742AC7AE700062CA31 /* SyncBookmarksAdapter.swift in Sources */, B6ABC5982B4861D4008343B9 /* FocusableTextField.swift in Sources */, 4B957B752AC7AE700062CA31 /* RandomAccessCollectionExtension.swift in Sources */, @@ -11948,6 +12055,8 @@ 1D1A33492A6FEB170080ACED /* BurnerMode.swift in Sources */, 14505A08256084EF00272CC6 /* UserAgent.swift in Sources */, 987799F12999993C005D8EB6 /* LegacyBookmarkStore.swift in Sources */, + B6F9BDE02B45C1A800677B33 /* AddBookmarkFolderModalViewModel.swift in Sources */, + 1D220BF82B86192200F8BBC6 /* PreferencesEmailProtectionView.swift in Sources */, 4B8AC93526B3B2FD00879451 /* NSAlert+DataImport.swift in Sources */, AA7412BD24D2BEEE00D22FE0 /* MainWindow.swift in Sources */, AAD6D8882696DF6D002393B3 /* CrashReportPromptViewController.swift in Sources */, @@ -11965,6 +12074,7 @@ B65C7DFB2B886CF0001E2D5C /* WKPDFHUDViewWrapper.swift in Sources */, B68458CD25C7EB9000DC17B6 /* WKWebViewConfigurationExtensions.swift in Sources */, 85AC7ADD27BEB6EE00FFB69B /* HomePageDefaultBrowserModel.swift in Sources */, + 1D01A3D82B88DF8B00FE8150 /* PreferencesSyncView.swift in Sources */, B6619EFB2B111CC500CD9186 /* InstructionsFormatParser.swift in Sources */, 1DC669702B6CF0D700AA0645 /* TabSnapshotStore.swift in Sources */, AAC30A26268DFEE200D2D9CD /* CrashReporter.swift in Sources */, @@ -12088,6 +12198,7 @@ 98779A0029999B64005D8EB6 /* Bookmark.xcdatamodeld in Sources */, 85589E9E27BFE4500038AD11 /* DefaultBrowserPromptView.swift in Sources */, 4B4032842AAAC24400CCA602 /* WaitlistActivationDateStore.swift in Sources */, + 1D220BFC2B87AACF00F8BBC6 /* PrivacyProtectionStatus.swift in Sources */, AA512D1424D99D9800230283 /* FaviconManager.swift in Sources */, 7BB108592A43375D000AB95F /* PFMoveApplication.m in Sources */, 4B0AACAC28BC63ED001038AC /* ChromiumFaviconsReader.swift in Sources */, @@ -12108,7 +12219,7 @@ 1D02633628D8A9A9005CBB41 /* BWEncryption.m in Sources */, B6B5F5892B03673B008DB58A /* BrowserImportMoreInfoView.swift in Sources */, B69B503A2726A12500758A2B /* StatisticsLoader.swift in Sources */, - 37CD54C927F2FDD100F1F7B9 /* PrivacyPreferencesModel.swift in Sources */, + 37CD54C927F2FDD100F1F7B9 /* DataClearingPreferences.swift in Sources */, B6F1C80B2761C45400334924 /* LocalUnprotectedDomains.swift in Sources */, B69A14FA2B4D705D00B9417D /* BookmarkFolderPicker.swift in Sources */, 1D36E658298AA3BA00AA485D /* InternalUserDeciderStore.swift in Sources */, @@ -12250,7 +12361,6 @@ 370A34B12AB24E3700C77F7C /* SyncDebugMenu.swift in Sources */, 9FDA6C212B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */, AAC5E4C725D6A6E8007F5990 /* AddBookmarkPopover.swift in Sources */, - 37CC53F027E8D1440028713D /* PreferencesDownloadsView.swift in Sources */, B6F7127E29F6779000594A45 /* QRSharingService.swift in Sources */, B68C2FB227706E6A00BF2C7D /* ProcessExtension.swift in Sources */, B6106BA726A7BECC0013B453 /* PermissionAuthorizationQuery.swift in Sources */, @@ -12280,7 +12390,6 @@ B6106BAB26A7BF1D0013B453 /* PermissionType.swift in Sources */, AAC6881B28626C1900D54247 /* RecentlyClosedWindow.swift in Sources */, 85707F2A276A35FE00DC0649 /* ActionSpeech.swift in Sources */, - 4B0511BD262CAA5A00F6079C /* PrivacySecurityPreferences.swift in Sources */, B6BE9FAA293F7955006363C6 /* ModalSheetCancellable.swift in Sources */, B6830963274CDEC7004B46BB /* FireproofDomainsStore.swift in Sources */, 7B430EA12A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift in Sources */, @@ -12408,6 +12517,7 @@ 85A0118225AF60E700FA6A0C /* FindInPageModel.swift in Sources */, 7BA7CC4E2AD11F6F0042E5CE /* NetworkProtectionIPCTunnelController.swift in Sources */, 4B9292A226670D2A00AD2C21 /* PseudoFolder.swift in Sources */, + 1DDD3EC42B84F96B004CBF2B /* CookiePopupProtectionPreferences.swift in Sources */, 4BCF15D92ABB8A7F0083F6DF /* NetworkProtectionRemoteMessage.swift in Sources */, 4B05265E2B1AE5C70054955A /* VPNMetadataCollector.swift in Sources */, B6DA44022616B28300DD1EC2 /* PixelDataStore.swift in Sources */, @@ -12427,7 +12537,7 @@ AAE8B110258A456C00E81239 /* TabPreviewViewController.swift in Sources */, B6C8CAA72AD010DD0060E1CD /* YandexDataImporter.swift in Sources */, EE66666F2B56EDE4001D898D /* VPNLocationsHostingViewController.swift in Sources */, - 37CC53EC27E8A4D10028713D /* PreferencesPrivacyView.swift in Sources */, + 37CC53EC27E8A4D10028713D /* PreferencesDataClearingView.swift in Sources */, 4B0135CE2729F1AA00D54834 /* NSPasteboardExtension.swift in Sources */, 85707F31276A7DCA00DC0649 /* OnboardingViewModel.swift in Sources */, 85AC3B0525D6B1D800C7D2AA /* ScriptSourceProviding.swift in Sources */, @@ -12481,6 +12591,7 @@ 987799F32999993C005D8EB6 /* LegacyBookmarksStoreMigration.swift in Sources */, 4B379C1527BD91E3008A968E /* QuartzIdleStateProvider.swift in Sources */, 37F19A6728E1B43200740DC6 /* DuckPlayerPreferences.swift in Sources */, + 1D01A3D42B88CF7700FE8150 /* AccessibilityPreferences.swift in Sources */, B6C0B22E26E61CE70031CB7F /* DownloadViewModel.swift in Sources */, 4B41EDA72B1543C9001EEDF4 /* PreferencesVPNView.swift in Sources */, 9FA173DA2B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift in Sources */, @@ -12488,6 +12599,7 @@ B68458B825C7E8B200DC17B6 /* Tab+NSSecureCoding.swift in Sources */, 4B37EE612B4CFC3C00A89A61 /* SurveyURLBuilder.swift in Sources */, 85378DA0274E6F42007C5CBF /* NSNotificationName+EmailManager.swift in Sources */, + 1DDC84FF2B835BC000670238 /* SearchPreferences.swift in Sources */, B693955726F04BEC0015B914 /* MouseOverButton.swift in Sources */, AA61C0D02722159B00E6B681 /* FireInfoViewController.swift in Sources */, 9D9AE8692AA76CDC0026E7DC /* LoginItem+NetworkProtection.swift in Sources */, @@ -12504,9 +12616,11 @@ B69B503D2726A12500758A2B /* VariantManager.swift in Sources */, AA97BF4625135DD30014931A /* ApplicationDockMenu.swift in Sources */, 4B8A4DFF27C83B29005F40E8 /* SaveIdentityViewController.swift in Sources */, + 1DDC84FB2B8356CE00670238 /* PreferencesDefaultBrowserView.swift in Sources */, EEC589D92A4F1CE300BCD60C /* AppLauncher.swift in Sources */, 4BA1A69B258B076900F6F690 /* FileStore.swift in Sources */, B6A9E47F26146A800067D1B9 /* PixelArguments.swift in Sources */, + 1D01A3D02B88CEC600FE8150 /* PreferencesAccessibilityView.swift in Sources */, 37BF3F21286F0A7A00BD9014 /* PinnedTabsViewModel.swift in Sources */, EEC4A6692B2C87D300F7C0AA /* VPNLocationView.swift in Sources */, AAC5E4D225D6A709007F5990 /* BookmarkList.swift in Sources */, @@ -12570,6 +12684,7 @@ 85707F28276A34D900DC0649 /* DaxSpeech.swift in Sources */, 31F28C5328C8EECA00119F70 /* DuckURLSchemeHandler.swift in Sources */, AA13DCB4271480B0006D48D3 /* FirePopoverViewModel.swift in Sources */, + 1DDC84F72B83558F00670238 /* PreferencesPrivateSearchView.swift in Sources */, 1D43EB38292B636E0065E5D6 /* BWCommand.swift in Sources */, F41D174125CB131900472416 /* NSColorExtension.swift in Sources */, AAC5E4F625D6BF2C007F5990 /* AddressBarButtonsViewController.swift in Sources */, @@ -12586,6 +12701,7 @@ 4BB99D0226FE191E001E4761 /* ImportedBookmarks.swift in Sources */, 7B934C412A866DD400FC8F9C /* UserDefaults+NetworkProtectionShared.swift in Sources */, B626A75A29921FAA00053070 /* NavigationActionPolicyExtension.swift in Sources */, + 1DDD3EBC2B84DCB9004CBF2B /* WebTrackingProtectionPreferences.swift in Sources */, B603FD9E2A02712E00F3FCA9 /* CIImageExtension.swift in Sources */, AA6EF9B3250785D5004754E6 /* NSMenuExtension.swift in Sources */, AA7412B524D1536B00D22FE0 /* MainWindowController.swift in Sources */, @@ -12596,6 +12712,7 @@ B64C84EB2692DD650048FEBE /* PermissionAuthorizationPopover.swift in Sources */, 85378D9E274E664C007C5CBF /* PopoverMessageViewController.swift in Sources */, AA6FFB4624DC3B5A0028F4D0 /* WebView.swift in Sources */, + 1DDD3EC02B84F5D5004CBF2B /* PreferencesCookiePopupProtectionView.swift in Sources */, B693955026F04BEB0015B914 /* ShadowView.swift in Sources */, AA3D531D27A2F58F00074EC1 /* FeedbackSender.swift in Sources */, B6BDDA012942389000F68088 /* TabExtensions.swift in Sources */, @@ -12616,6 +12733,7 @@ 37AFCE8127DA2CA600471A10 /* PreferencesViewController.swift in Sources */, 4B02198A25E05FAC00ED7DEA /* FireproofDomains.swift in Sources */, 4B677442255DBEEA00025BD8 /* Database.swift in Sources */, + 1DDC85032B83903E00670238 /* PreferencesWebTrackingProtectionView.swift in Sources */, 4BE5336E286915A10019DBFD /* HorizontallyCenteredLayout.swift in Sources */, B6BCC5232AFCDABB002C5499 /* DataImportSourceViewModel.swift in Sources */, 4B92928B26670D1700AD2C21 /* BookmarksOutlineView.swift in Sources */, @@ -12679,6 +12797,7 @@ B662D3DE275613BB0035D4D6 /* EncryptionKeyStoreMock.swift in Sources */, 1D3B1ABF29369FC8006F4388 /* BWEncryptionTests.swift in Sources */, B6F56567299A414300A04298 /* WKWebViewMockingExtension.swift in Sources */, + 1D9FDEC62B9B64DB0040B78C /* PrivacyProtectionStatusTests.swift in Sources */, B6656E122B29E3BE008798A1 /* DownloadListStoreMock.swift in Sources */, 37D23780287EFEE200BCE03B /* PinnedTabsManagerTests.swift in Sources */, AA0877BA26D5161D00B05660 /* WebKitVersionProviderTests.swift in Sources */, @@ -12704,6 +12823,7 @@ FD23FD2B28816606007F6985 /* AutoconsentMessageProtocolTests.swift in Sources */, 1D77921A28FDC79800BE0210 /* FaviconStoringMock.swift in Sources */, 1D1C36E629FB019C001FA40C /* HistoryTabExtensionTests.swift in Sources */, + 1D9FDEBD2B9B5F0F0040B78C /* CookiePopupProtectionPreferencesTests.swift in Sources */, 7B09CBA92BA4BE8100CF245B /* NetworkProtectionPixelEventTests.swift in Sources */, B6DA441E2616C84600DD1EC2 /* PixelStoreMock.swift in Sources */, 4B434690285ED7A100177407 /* BookmarksBarViewModelTests.swift in Sources */, @@ -12756,6 +12876,7 @@ 4B9292C52667104B00AD2C21 /* CoreDataTestUtilities.swift in Sources */, 4B723E1926B000DC00E14D75 /* TemporaryFileCreator.swift in Sources */, 98EB5D1027516A4800681FE6 /* AppPrivacyConfigurationTests.swift in Sources */, + 1D9FDEBA2B9B5E090040B78C /* WebTrackingProtectionPreferencesTests.swift in Sources */, 4B9292C22667103100AD2C21 /* BookmarkTests.swift in Sources */, 5601FECD29B7973D00068905 /* TabBarViewItemTests.swift in Sources */, 1D8C2FE52B70F4C4005E4BBD /* TabSnapshotExtensionTests.swift in Sources */, @@ -12801,6 +12922,7 @@ 1D3B1AC22936B816006F4388 /* BWMessageIdGeneratorTests.swift in Sources */, B6C2C9F62760B659005B7F0A /* TestDataModel.xcdatamodeld in Sources */, 1DA6D1022A1FFA3700540406 /* HTTPCookieTests.swift in Sources */, + 1D9FDEC02B9B5FEA0040B78C /* AccessibilityPreferencesTests.swift in Sources */, B68172AE269EB43F006D1092 /* GeolocationServiceTests.swift in Sources */, B6AE74342609AFCE005B9B1A /* ProgressEstimationTests.swift in Sources */, B6619F032B17123200CD9186 /* DataImportViewModelTests.swift in Sources */, @@ -12855,6 +12977,7 @@ 56D145EE29E6DAD900E3488A /* DataImportProviderTests.swift in Sources */, 4BB99D0F26FE1A84001E4761 /* ChromiumBookmarksReaderTests.swift in Sources */, 4BC2621D293996410087A482 /* PixelEventTests.swift in Sources */, + 1D9FDEB72B9B5D150040B78C /* SearchPreferencesTests.swift in Sources */, 1D12F2E2298BC660009A65FD /* InternalUserDeciderStoreMock.swift in Sources */, 4BB99D1026FE1A84001E4761 /* FirefoxBookmarksReaderTests.swift in Sources */, 4B117F7D276C0CB5002F3D8C /* LocalStatisticsStoreTests.swift in Sources */, @@ -12868,6 +12991,7 @@ 857E44652A9F70F300ED77A7 /* CampaignVariantTests.swift in Sources */, 3776582D27F71652009A6B35 /* WebsiteBreakageReportTests.swift in Sources */, 31E163BA293A56F400963C10 /* BrokenSiteReportingReferenceTests.swift in Sources */, + 1D9FDEC32B9B63C90040B78C /* DataClearingPreferencesTests.swift in Sources */, 4B723E0826B0003E00E14D75 /* MockSecureVault.swift in Sources */, 37CD54B527F1AC1300F1F7B9 /* PreferencesSidebarModelTests.swift in Sources */, B6CA4824298CDC2E0067ECCE /* AdClickAttributionTabExtensionTests.swift in Sources */, diff --git a/DuckDuckGo/Assets.xcassets/Colors/AlertGreen.colorset/Contents.json b/DuckDuckGo/Assets.xcassets/Colors/AlertGreen.colorset/Contents.json new file mode 100644 index 0000000000..ba7383a80e --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Colors/AlertGreen.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0xC0", + "red" : "0x21" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/HomePage/Rocket.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Accessibility.imageset/Contents.json similarity index 78% rename from DuckDuckGo/Assets.xcassets/Images/HomePage/Rocket.imageset/Contents.json rename to DuckDuckGo/Assets.xcassets/Images/Accessibility.imageset/Contents.json index b899856ca3..313962dece 100644 --- a/DuckDuckGo/Assets.xcassets/Images/HomePage/Rocket.imageset/Contents.json +++ b/DuckDuckGo/Assets.xcassets/Images/Accessibility.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "rocket.pdf", + "filename" : "Icon 18.pdf", "idiom" : "universal" } ], diff --git a/DuckDuckGo/Assets.xcassets/Images/Accessibility.imageset/Icon 18.pdf b/DuckDuckGo/Assets.xcassets/Images/Accessibility.imageset/Icon 18.pdf new file mode 100644 index 0000000000000000000000000000000000000000..1f47d51258708f01c2427fa6f1f5f45250c3a497 GIT binary patch literal 6134 zcmbuDTW=*t6@}mXSMROXuI{l% zpn#v+>(t)0uWRq_SMT0_``Rvfn(S=m`@j5kn&+>+n!o;fI^Vt5zmq%S7r#5-eSG?1 z_JEf<_4xE~zPg<*-k$$)eL7$M`kVR9cgO!ukJH~LyO{Pjo9*HCczXHU_~~>znI(j5 zVns5R;BDZcCZIGwp6>X$D=yoEL^@{O5^Qm~%pPHzottgiejEZjbecW`-b}li{pjIo zT(VXZrD&W%pY0?axidUH`CBDu<`Knr=j$;$7x_U(+|^!>2CVt|Cuu9 zT#AWI1#5FEN}12mn3l4U?{V$zULRqxJ{Xhd@$mBaK!~ngN{Y4md4M)<8QxV4Xk3Zt z01rxbb>r6|vJ0H1xF*p+OS3>_x|JpS3M;C9d94vr-5^*5lQ+$gN_(1Ug zWx?%t4;4FfxN-+~C5NX`+(Tp+Pyjys;^&l@Uuvy1?@XF{UqcJy`Q?LXA?T^v~PS!Ja_nhwb3 z8*;<;Zq?8-rpT)MW=3q+e*lY^f8!81`78)GGFezBWiaMedI#1sqrpC-5D5`(S8 zRXOu^z5%PIK=Hz&uU;w?P_(-xg%V;uBGdsUqHCP8=ue(WAv*1n!a_{8NpewwWUTR~ zXr361$F6~_x?=WW$8;)tN9iJm`c*FZWIzaDAb>RD5`#Cv%&*;It1kqC39*2c;su6~ zY}QpVgyDmh3ZT@S0Tr2h=E5*$?G}ioq~sJ3{*i7XsybL zZxL;6<6|){wQ!VJV`>AK?Bt|G2NNk6%0Y++p_&6K%+lm6x}w~&v1}I}mC=_lI0kADQ0}M5Iqb5oi7H4X0O6Kg=9Gd-6^gBy z5Nl=>b@Fi2Fqab>LCcL(P1hPcDH%Bxl5&f4&-<#2N;{ldCk086oNixPM71X*QR6Kf zqaIuJ#TJc$wHhswtTu+U)v*vqFVJ4S+LhtFwc+TB8?d6LQ?d5mfqfaJ1h)5=n=}4I z<9i$CTAQxA^;~b(ykz6DGkZhlrFY?Id&R+cx=`Aa_1M^AYOJkPHyD8?05Cu)Hi`;% zMB7_Cv&w0NJ*SnQy@*gdDm8~KeF6{E#nFC@ zokj;#BS>}y_S+E)_nEA+>rA3!j4997(>SZtnBf^_V?WnwrJWj(Dx11dukYuwIOtPt zJ9M~pfU9v-T3WEWKw7H7*3@Wpe)0Tan%LPq2@eQ4lDT- z(#j&D9c*0(CrY^-*71$IXqPA?IeJ0uYih1c6ukX?D43$kBrAvV{Y zXhUu5M+R2Ev30U%T6qmh*+sEw2qX`G>y)fVmu0TH(l%Q>Bp4vrg#b2qn=}D?*^PKX zf!NS0_7GQLXd~gU@reHoZAVO$-jO9{vSn=|!abF3bko1Yu4i1q;MAJlUKd*MOC_yp zl3qLXp%SiI=oKbopEsQPT+{%Gi-27GR z0&#L0GfJ;8BU^>B+^q=4tO)bwrJQszL^CrvU^^t7C^CjrZ2PR>IJAyR7uW z{$O0)w0o9|6=A?N#No|~d@4uJ6Rbvu_yZwed&4Bqe8h1vgQDCK&)k10CwkJ%ru191 zv;ZWH?26JJQPBYtTV1`zy_GamBgTZ!Jv%BYrlV`Mtqd|ebcWvE@=TZLK6=*j<5~~A zw99t(Nrn(uNyR~C8wPDQjMql&;uJ5ME8?4_&*c{hVCU)U8IeO?5jX`YPHY983(x zhDYC35t@T*NFLjbD;gs;syA5_n3R;T<_R^e^_-<+l@md;6N>p=pm{70B#^cfV(53( zcf*(v9c#xq716$);&ttFazuuGj{8`9sLr;xH!RW24NLsk5GF=H)#CSDu2H+Ez~v#m zt>sjEDwJf1sW9kX?ZzgfraU5SZ3JYF}{pZ)F|O{OyrouB=fW(bDTCZ7$ne zOe3+;cdi7_gZ0SMZi=$}>Dl2mK`}K#)*OoOct2uOUbCNg{m@8Y;$WFDs>X|@j@8EN z=n+v(5|Fm#ZR1$A%dBf7ZeUb6^Fz8=$QwDW`;ZW?TpxNxSls%qZB0UQpy}7HqZ5S? z)Z7r-T%+;je9+V&w|zqz>(K2ERyd31Rrlf=@08T>XyYz85V^@bFwo1uWJp`UhNgiW zx4t8onbLgg8oF2npI^B{D!Xpk4jz5#+Ky1ycKC#%Pqh8}>-Mo$-(}a2xCFqDZ@4TL z`?uYTU*6r_KRwRh|Cw*S{9XL%pMN{g7w@hGJ$||Kr2y{5b1t@?}@|=Kk*Ke8-LYl)QoOt{%W<)}RI*J~#^+AEGaQd;K0$ zXL)crZ}+^p{}EaH!v05)gC_QugC;rnI4;~Be{=P8b#wpmQ_-Iu&L1WkYg)Z-o@)@s z8Myz!742!}Lv;UR41Eew#NwEe7nk?EFMYm3GK@YB0*|=ESt?d+Pe|=}D;Um4i zv^p5{6X(sF;S4F>+~2)>WxA-8(Y-$W_Pf&h^=tj+jhgol{8tSvzWQO_Kb}8nOW>Mk zozBO3vs0_P`LFFXufKh(Z{N57rjz&7v;(d*a+Z=$oBY#4OjGnjlojJ+l(4c4ciQ&B)r7}!yTCN_NzJ}J|8V+h9B zIkO9k-{*(ghtSk$qG%% zcGkxfTr;n(f$4%9jOFZR%bsZ$j!6Ln1qOJ9=|Cu%*Q`|#C@KUFGHqj#@S2F*BwVik zq&}&SU#Y$N`M;N)L|m2cDCFp=bb87M7m%fL@A_aE;j+V2tS_4gj~O~=(h!rjolhnX zgEpbOgh7RJm3Y_p>0+LtA|FRyEna9^x73+nLhuVj-4HdK23onET+qCf++g)sd=ccs zFj7ynkXed;QWd23cf&yaLMtYt`qFn|0Vpp|W_B0M;$I}?f~3M_osgbQYdjKSo1N=K zNLtAh^J-#|Yos3;2Ew&33jqv+>sKJ)TEd@Nkvru&V`;r&(@NOID1|BbaikV4>i`Z4 zpt*4sGtwhVPY-rKY%3nZKdya~Wyc*sY63+{< z4|%WT0o9wlTR6xd*Co~}`--MbQFVVUvj+438`x5>vR^%v0i7Gvx6v*QY=`JW*gd(^(grEvcP!4}UIBFWiaayo|cu}Ix2d9WqDU<1;q2L`QRY7k@# z81*W5l%sMK$F4|$KOnqc$aG*(4kjRokE6yPxX$}laV{kUT9FfVgFrnnkz;9==(=_e zB!6<0<-W_-imB027qM9}tdOKJ)mVBA1YIW8urma+9%UbhyY+ z)71;k>g)afa6ah|ztV+=p4BgZ|1s;;{pN8N;K%u4yLo%~qCfE2k)JxK-y(HXnAY>@ z@cB5+C(T!?wb0!j_UCy|9{E01!28XSFzp3uZe0xFHn#eH`#@4PN3hn#&+Xwe*_tc; zCqfz~`lxT1IKUGe*1&H!=grgM@zV6masH_2E})k1`Bw+;t=9z0W9TE9A5EXd@>tTz z4HFy!wPsmA2-{l`Kai6y87;p((&SS*v&8Xi|xb9B`K@*;c(U* ea8B^*{q~=k)aPsJ=6KHG*kG)>x%uX&cmD$7ANC*s literal 0 HcmV?d00001 diff --git a/DuckDuckGo/Assets.xcassets/Images/EmailProtectionIcon.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/EmailProtectionIcon.imageset/Contents.json new file mode 100644 index 0000000000..e7cd622579 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/EmailProtectionIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Icon 12.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/EmailProtectionIcon.imageset/Icon 12.pdf b/DuckDuckGo/Assets.xcassets/Images/EmailProtectionIcon.imageset/Icon 12.pdf new file mode 100644 index 0000000000000000000000000000000000000000..671d4a0b39c46b9cd5ea853f3391fd7a47995818 GIT binary patch literal 6306 zcmeI1O>Z2>5r*&cE9N4=4&Y&be*lJoSc-!n38K)s89p$pwaj3}O-MR$^6T?dPftyA zNf?ljqvs%v-tDfgs($;en%1kgZ@&4$E_s^lZ07Ud|1r(;mtW3beKj5L-pM~FH{w@+ zI^Mm1{Bibx*D~ti@&0)8VY+&A{P*qQc>VjY=hxqk|2jNO|C;P#+B2K&t+ozyZy`IVK;i1@)m`TI_((t*H{P4EiM>W+?Rb#d#OJ_^Mb~ObRJRd{rzh#`zG*3+Zw-HLHC|>$WJ>#lvK=94zaz`Zg{sKBrvW zoR$)5v}U$(n;MtmtIGyQc2F~U8bCbg8MhcD_e|P%i*;M}^MGOU`L>>>AEzIFGu=&p z{RBRh>Y{A$hmnaAfK6mE>~bGB@JU0E)V&m#?0I@N zf7p@1YTrA~&A!&S@!j(g>H&@fiEMV_@DQ~a6n0tI7084q#({%9`y*F7ggthoc+a`W z+b&g@g-QgB5t1vOBZKu~{2Z2Ai>Y#Co+sgxSe0yap0!P>^752bHCPMPkgJ_l2@#8_ zL}3d|B4%M#696;E#X*iFIpM69?7d*s=R%$^Q+!7LNRlN&j>!_LiPh`~3Gs_BL_Z8` z2^OIXX&*_>lQSN|us|gWavnI_P^RwpCo#;++i%^NVHC5-5BQ>m&JM|(ycN`FqdgN zhzNX~2J|ee=CztqN|9p&zk_E-#EZ<|rfC2}dri?vtq$1|OPO~@>JEyU6|%oZBMXw5 z`mCmOyB9U3u4buo+lv^$gu0>Yi01bn6%)fmu>l!b+dHr$sLZQaho|?S~G_p23P)Wx^&DtK3oNX1wx;G-K`#>0jl0Zl- z#wA%7qlZR5*65v7FYGxAYZOV3X#NU;A`>xXI4SnpI01E$X0JJe0OH}g-3;8`h_yW; zVq1KQ9fl22@ROuxUBeMtOs z8LCrU$<@KaR2xkrm!o)9lpNV&3G`SNp@EC^P*%W^hiy> z6)Xs-4Jah;WkfSe6BuX@3seoXUbHSE2rd?A3+9kCPPb@lu?C@?g^9r>Y3VEtG5!c* zBB@Js4vvB%^EkHrbeJfelrs*DREgriq#{+`k!DT3lr}PN)9Ixt)FSh&5=5BT0&HeNQo(i_m+4L zB|`-RjFR}^YM^NngwGV|ST4hrnnvxVtEa7}8Ss#(u5D*^5V+|zZIkerL+K?U61qA8bm^*Ef6*!SRnGr#?W8O z>zaesYi+vrYk6(;_5WI4Kc-cb)7>8Qub-Ai=9K9abJ@AiAl#?&{4(o zYoD8q80geFt$iM+Vf}bVrZ!9+kptg-(w70#xdX=MGA* z0cz_bx?1Y>I(gV8=p7xmGHg@ei`m9jePtMjq7PU8O51aZoxr*Y9mm&xd*q;q^++XG zq`yd4&|h^SX~#O&-I4F{ZbBng9~2Qe(0Tnuwy8xVt=Hv`8m#`L28Py4^CjD)f}>=V z^U+DAF9XqD!gMYj)i!s)^wLhQllxuqcCjpX$m@l3OeyMpGaI+xLtp*&?(Y2fFn{+w z_tHF9fBVlrkMq^roA<{K{Q3Cq_U7yJ&+~ULWW{P`_qWZ`_3`2S)BWN2Fms~k>u&D# z`R?&}$BJ_8y@79U?!jiRFd8&mjtiuh=2w5beTS>FL~uQC>%2by#9aH#{|Au45}P_$ zk^moc!9DQTH;*@`^ZTb$f4)EdFqt_|D=Fm9nG0dAVSxL??QMIT?-b(7?I{$q+|tOS zqhij~zNZkMK^G7eXP@|Ze{*{}-cS55!qp$%kdDtD&L57S&!64C`+QBx)%E%OI7`4S e!B^kj{`ZLY?SB()?jKt?$&@g?diA@%eDgn?l-5E3 literal 0 HcmV?d00001 diff --git a/DuckDuckGo/Assets.xcassets/Images/FireSettings.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/FireSettings.imageset/Contents.json new file mode 100644 index 0000000000..9a9c668121 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/FireSettings.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Icon 17.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/FireSettings.imageset/Icon 17.pdf b/DuckDuckGo/Assets.xcassets/Images/FireSettings.imageset/Icon 17.pdf new file mode 100644 index 0000000000000000000000000000000000000000..bf31e9a7386206c8c9b2d7fd8270ce01bfc9d1a7 GIT binary patch literal 33137 zcmd_zORwhHRp0S>y^1qIf&taf{UTYGzy>QyB8Z&<63U<{7sjM7PInVQUVT2_wg1o4 zUFQ@eWSSb-r+;VP_qwmO|L2eX`j>zCr~CKUcklKuyO(eO?jPU1y!`BEFJJuEfBD15 zmoNV2um0{IKYsWD=HD0p>Ek!Q`{5s64jTS?gMRhhxBv8qpLCf2UcZ|@tK)Bs_uZG@ zeE)|pzx(*jhkyCx_doynHq>wa>F<7+N&oWO@4nu!Uw{1m+kg7*!^iKN;@R@g|NPs3 ze)-#%-TU4A`|*eU&5y>}UvJ0LizzO*+wS0p^Yya7xsmtB!+n1{ zzZ~D+PRHYA|MKCTC0*~w{bB!dH7abU{c(S~oQ<%1zu#XD{qz?4V1J)!cRrr%=jCql z8T%jim+V9W1>keYS=l%UN$cJ}tTiNwt zce-4Re%J4?)9-%W9WLjW{eFf5eQ;ox-DQ7p6sPxx%jISjcjtaM?JhyLixu8J2Yq<= znRa%v+aE6Wc6@){&uF*z*WLYa-oNY*@Apo?Xm#Bk&iAWX_wV<+-Tv0GoKI)7b+nfS zb;yUq;dF2Q-R!~9`t|*Ga}L+{*A{$TJ~Lro4UAUw1Q~siomW{ z$Bfc$h_7}I?|i-OBlz>-cskxp?-BJMFq99& z0sG_CM4gn&?}}a{AKtyiZ0K_8`ml1>*A|aw1MOc(hacxV?T>Z@yV@gO(5l0_Ip||& zS-<^#x8ENjx7#t+YrgaSj351S+w_GDIUcmSKl;5zKD>L2q1D~>ewiECkw;fcxgaMr zc6on7?r8DDJ1j`bkVohD#K2->>%2g=^K7Fh;^uI4Qpe8jtRL)h$8O1|g`Mp0ej-R* zDn@jbZ-wQDcmMG2@85m%?!WxIWi(-Va$6=GA&oAHG*j*Gmz^_37Pm{6@`;ch=6CIc zwC&xFSHdYFjgZeTU%}iXW^2XRmRNB@_J_<90w??cacQIq~Tior^BxEIiFBPy!Jeg=eoXf9#7S9dVf!VrBZ02-RXR6dN+5XH@^Bz zyL$hq2@)%pBg*+2sQVs0VbKG5M6-MaywVG4C*t|G-_80j`_t)8@9a~7huzU~Plv-1 zxdvTNnAyUOLhdGG4YI%7`zif*kN34Zk?ftB^`8^n!?pyLvyh(Jdu}J*M~lO8cgd?A@8`t=*Y4x#4U|`*KWtC5#D+X>~%% z2$@K?YTS4`IP4*Af}q>Svxj36^O%fCVl^WzvWJ|ngj4;leuLb9q9vc}TC08N#nG8- zxrwjqJ>gC&TllUk9uNU;;AsQjbZ^ujwc|NGA7^|Gyx4*}+oR<3^-S!31-u?H2L*7( z?8l{VuZZHT9Yc$;a7aI1jz@Ac=&-w<(cu!*XA~GnDx8rzDRj76de8~c5EOTc7d5ze z5UXee?q}B5vVT<9lvV~*%O+9}ccL_%M-^RZt*-))`~B6jf#?u%20pa?vwNd1T`??4 zkvPmD2`FCPr}{{b^L2k3J_vR5qpe83-Tq?W&yBE+d|5gj#kjIrZ;f|%axbD`k2r%F zVRyY=5CTHG*p7}8vEjY@p2Na!rQACbcSQPKLI`}jpA#6MR~3w%)UQqdpBeTA0%{x{bov9dF06S6P5GP!jHNxf2@<8w&kE;p=%NDXwE| zp~-Pmc2wNGHV@_@O)vrOP2}y3X2&*uMQr3E)#9j$@~fk}QN=fM->Hmyu?)=Tx?1hi zRE!^ppcMJl74Yp+GiU+H&W-K37jr6hm92Nh*cX<_QYt5W$wDV#um|ss`+8LjsII9s$tM!7rE9(OQNhrZ4n zh|6HkCed8G8)6H~my2WKs4;?{dX6PL(BvaYBzx$X~VtYw(p`Lt2-mHEkO^-B_B zxaDacKl?V~)6hjT$J?g&E_yNG$OD$sk?|Q-Tu>h?huDei$&l!SPG#;+yZh}vws5iw z$!LXSE~nfvw|}>3Vi>iud>K3`N~#rCWr^lm1*JNtE4I2={!a{X?Y8x`?5FL`%}zgc z5cMCo36-!UjM$=KvD{#}@!6@H-Tv&`h)=7$9|-uH5f9`q&d(QT#%1|_wJ=-}w!iT) zVQ>4cH6Cf9wus$be@58}BOREcFp~E~JJ7~=)SusE(^pH)Ex0GlGG&ag?7j;D?rvFJ z9r;Kj3XKp)|Gv|hccUNvPNGt)14n)Y>ag(yK2+Ba@mXQRyVYtOO{ETcRBE%U}GFn~O zGii%JW{wW+MqI4bB@W$1&~2X>4Wj=yh$>mL#@gilv=eYkeSyYIt*aoIUxHX6TdN%U zHLF|CE2$k>*@CspZeP$9Jq={Dk5=0Or?fXtzfpBXDVkzMDU6m^HBce4HClW=d)?dW z1586rJL+R=M^$VP;k~-%ZLO}PI{?yDG zLoCA(t&y2^6@TS{O}k5ejjlIy#0R=EEXdKwML=fFiZ#&YbS2AK`?X`uOjs6=qRa!aDUGFT zEFU5GiSKAhg{RRnj%2UzL^RrKF%OQVJ7#pbIZG*KjG5To1kT>^+=5<9%-QRk0K&f# zEO1t&>k0xebg0aZk5Y{9$f;ZaFkLCS-0kVgaa$L`ZDmW;&N(a|5&ej6 zg6Oa{8kvJfR#&dq22v-EIwR+@WGB)f?iaL)zXO3_s@0Xvwsl2ywpiDF^2F(j5WAa! zc4?%6&SdS@brXQvO(3PNH2l_%x4d;NDluIdMxX2YX!qEd^{%}#Ft@ew5tf>4icglZ zR@dRI)s3#J)naOEJFm>aY;1|$;*-c~?R7CkP!#z&HJK25O?L%6X%P`yf)Ya(*g$o~ z`SM}2IuXo)NlYsg`r{cL1rJJxF@nyn1SYRQ!7^bbO`JLJgPCDvIsMK)A&e_R>SxiU zCd>((Y!7mi>8c;ktl%YEoRV=E5Kg6u>qpzr2VuGq=n&CU2Nfb-KmEwwHBu>yh(jx? z#j$IZv}jdAKuOJz)16=r5%ld0c5FYyI{kzJ-T7d-!L^dwS^Y*(hTI(Kv^fZuZvF0i zBD;RFDrT=IcW@3dS|B0d^V5%>$j(sSF3d0AUCI8zmYS8xUQ`q3u;b} zaIvjvwVjl-Cc!Ys$|xCo`Due?md;oN(`xle82D|lxIpcJZ$V{CjuERYhCxaK4U(c6 zqsq|%tHLUwIN*AeD@UU^eZA=KaSqSC7d0-GKiS+YU9u! z{c-}QA5Xhhmdg?uM3!?QDY_6YZTDU)w)yIJjjSE=T8AZJU(1~3fp4kvXSn}sJ9?cKfUtWVT(N5ssgr>0hX)~%MJEw9 z_gZLk&KbhHoPMm@ZW?lQrj5kkS|CFINx`%C-Z*#3H^+nF3Cn?a#|x9r>HP zWn82s?C>Q1!~l;96VK;9>QiwwIUHZoz=ucFd3b87txQN zQ$BOEFy1(8&w7PA2xA=qi({=Hw?JeUjoGQHbLqrjLX|iHHBWk>R%DN}fVd7$w}1*E zG)OURS+n{H@@`o6RiitA6s4FT(P7^ZbUW!Y@eCbTztWN2TDL%8YLx}lT?-YK!V7V3 z3lVErx{&{mS87!{VCJLaC&pXN4k1v#PH5V=wMRc9V^$?(G&=3V3e~)kqyT1}bSa?& z5#PiWvp*5pq0)wEg~8pgI>{YsurD~9TZjP_6z#Drh{ zWQ;~S!0feF!lg`#`jv1-fv+V`nf0NrQkjdoPLrJx-$K6;CORV^?OW6q-R8H*O-TB) z1Vt8^4tTzr9L~p3gt%+>04xp7z-oUC(s8UMv(*$W!IQ}~t%|alsw@GWR#l0v0;4P_ z9= z+`u?AX!+FBE{a;vwyhJ}^f9aC&$c>LIi*OtmVjSf>8R;Cf@DEk4#HC6q^%u>n1!%) zAFFe)3o5L#7Cc7(fFgSjaOp!E%jjP145HOmru9rPx7RnR-huX+`YZdp?)5;G8f~C5 z1=CeEPtMc=?OIpPwUgK&Og4~M%L8JPZ(XB~)s6);?FgI4XzB$PG1QVDKQ3~Iu2qA_EQ=;Ez9hj&Wzk8jwUgTk}b0$2h_WF^k;^;bt_w2UfMx>a5N9PZh zq3VZ(Vh=jU#v{@4o3-mE=Iq!`I6jEEyJl4r%IKN}7r!VHTBQ)ORn^>#?nh8{=#&K1*n)9EBj3ZJKAcTwaErCp^f`M2u94e{`B9uq;$FdE;EqH@6@Ji%5tEs^1RW;|It-2@G&dHWM*~g5zchAk~OQ6y#>im zu31w%kFFw-tDPfxw9At>t4(L`&R&`53#yFyvDf^j+S%+jE3LHJRnoC_t!RH*E$Uh0 zRo61$6n@VXOJNzT$iQlqnNcfO{o@H&dOV=Ii$}Y;f9wTZAnOh3OjnuW1&uW} zP_-L%oizEQYvrbaC1AF$1&V{D2eI39b^Qam%OU?jr`g#FKaD0HXgQMXfvM^kXnF4$ zc@BM>RqaabqzHxqyQZL(J9GpzDf7Bw$OS3-n#&2Kydnuoa?7@$=Gz2_vO8CL80jjo zsTCDQv~07gx|s!YuOnEa;X_$bS6$KxNesl~1);oZSXbBLMPnym@0Gj&lp7>)g`o0J zWQ^p&sK2aY!Rc*QhFgrqkhjdNaY-~u=Fwr|P@}>VpFGR?WtF58olVld5?INx&ye2K zUcN5gY(31zMFa}k^Me;e=DJ+a1dktJCVAo}9k|+2kqg>(XEge2pyE_>Fik;xF0kOy z>grAhs^)5g$Z`aor>}NJ^fu_x71V2)%)aqM9$ja3z>L5(D`Hu*rcnpN zt_w=EKNc+2I3PkBp0bCYnbftw(&|det*-3Cjo>TV;t^}d8c*^@jg-zTgvI;hcZ^=E zsz9F1T@fV8*dd#=T6CsSbE*8rFI>nrKn?4lQ3*cfpQ2&rdIGZOE$r#$e606 zD~+m3k|3L|_yU&##nclxMk|5qv_YKpO$o4FR++tg6Dx0Zbu3Fo`P~xRsnS`U0%L2R z(M&c*Q6)mddQE|A$9Up{6^L0-Ar!)bPmzIIE8&`vA0@OOb^()qBnSmnrO9fyJ;{;H z%juo2<%qDx>o3%08TSN4Ghv6Rz9Z32SXondxce5T9MMx)%7HBHwSI3CMj;SCV5 zv=CZQWLon4H?fW=C5-AVqQ4cfDVJNdx1lslvDJEWfM=B)t%6a5t@?o434sg&@e&F% zxxqn6iLGUIMhi-hZCL)^)>~lZ(K`*c)-#Sy>#^}|J?MFznl(Dzt+#g_%Os+G z(cAPc^R(u{M}BIJ6=rVO%1tbE9HrSebu}#+F|A^-F}L)FEnCd_#C>aS7IM3tmJ@K9 zmScM?tWbjcMnFj&Vz5<2J zm?9fiq3!gZ%;YvVOKIDW!V$Rg%Z6caNvG-H2dr_noQKsn*iN$;D&kUsmtZ z0aj+;;@eBFxh^l-wv}VYREOSTers+DxF;lGm7&duo+50OGekrAu30(dIpH!blf2HQ z9K8YveUc}|T1Gk`3w#+SSGkmsoEH_b&N5%In;+-02!NJ2RR}Q5Bon};R>@bQ+}tWi z$mwDg3=1L0+$X=c;NlHr3vW9*Pjzy7i+6aS^0bp)i@LGoK0}v8VAUU9P0jrj1RabMgT5?>rmRNVg3Sg|>o42MnXLT9(L5AicRJ|2^BYXT)50qETD{e0J+K0}TW_ZH>|15u1gYyygKrqx zSo@Z!>{OKSNM>+fw%$a}j42~fs(RLg!57BBj;f6qBg~CcfCXPxZ>m4KHKsUdC!i&D zzqqVCRyqW>o`=-Vk)y!rU7&qQ68;<$qm~G`puUzA=-emFYlgxC$sk4ZsEsInP_>|Ep&X$Ms;Xfmf91Fw8lQt7l0-(+Ojc*l zv|5jlOpN?^+T;DZ^j5&i16DmT4ht^G-lE#v2Qy1GIaQjBA!X%*qenWK<>%IBzyeND z1+DDdxp-PK(YMMw5yOemd+u0l$~nX>rR`R4Lk>^#kSc>48AKOWx2h#|SDk|qy_lK! zt@E;{his7G8~Fz<$Tc!(qA>OktD<98F3dXFNf-}qlA?`yPzkwOTKLkxvU7yO^pi?UpudMdR0)Jr-Ln87Yr>mQFTe>D|?C zGn8R*z?``iZP6#Fli6DK2xLYS?BCXdtREQg*?X$7TV*drx88D|TkpaMYi@b;2Zj$H zb90$CbTs~)P=V|<_q;v$jGQZgxu}xaGgMhYeHI{d57i3Yc(%kxEha5P&AYN9F?_;2 z#kk(TQPkV}x6u#j0!~d2ok}gc+97CtGIc~}+29-X@^jgLeW`#Jo&(1VmHQV!70%ek z!wR3QZ^ZV@&0L$g=b7CYu6&|SdN!-*yaSMC$nC2jm5nrZef2;tK_HQ-l-$FSuU^58 zuZ2+r;xwf2#-N+Rq2e**fcq|y(ta!NoBdKdn=pF&2(VzWV118?SVf7lj+%z*9S+iB z6|FEdek29SLARicYKwGl4c%_Z^Mn)y`N<2u=5V$6 zRx3oB%iMa4&>}v07UJ7h%Zs?~TlboMTm0KTt7&MThmGT$;H#CjV4>kx5BVk-kAU#I z0tcoC;l(LZoJ2QCDX|==04nymh|rKu61^OgGfAY z$SNnU75jQdcTc!Aq$^q@QBs4rh&d?}lND=!7;%k--DjO1OU26z0H?WTm8`D){133{nVv(qvp$Pc>A2Qey^Wb4nnLTKw2v` z8`A5Ag{&&Q!76c@R-l@~$_@gQ;#3^Rq9L>;}p=97}xMz1)_NaP*cc1DWR21%nq z=-+Xb!%*dhm4pb%uvo3CtqMWVTbmLJGoPr6%{P`)j~Z!ca$_O6ln@yslu#SIlfx4x zoIAy#OcCMJ1Jj+`M+dcsf<0kmgSd1;91_%Zrq(Y`5p;w~(f@#BsC#~z} zMORf7S_wl9x0MRwTR>zJhOEs8A`9q4tZv6t$+G+Z*DEe8Zd+DFfrsPI%IP%c_QOdly2q zPce42%G)}d7#U<1Gjb+O#=~*^8O1W&6LTZ|$=*yU-sf7U*IU)fQAB;9sQppr0=&`8 zGMgz<8VaY<8nQ3j{W-*HBC5M9BBoyjz5G+f53y`JR^P^_RDYxPA+vFD0Cn@p6Kq_O zGnF>$U`>S5c=qm86`;)we_^36H@cNES=_|CC`wMlIlYkNHw=N9|7M*}6Lnk-Qjb;eZlmtb_uGOWODHmca+(mf2X3o%to*yvxo(>ES$y z6%~sV}R#&-K>!ca?VKe zgmcZuG3-ti*_Obak>ndT#8MVQ=uR6|E!#SCEjG*zPAm4rAf93+kF81IZ6U&lkA5;v z8-|ysRgsrjd4bG@44v6ahq0}**CErYmo3xE(Jmx|`>o7b6$&IAf{3!%u!uZK#5=1{ z%~z=;Vn#;tmL*n0!`I$;jC3m>AJ8;ZUS{7nk7M1<})Fj(R>m zqhqSk3$H&q5ZAw6*RtTuUbL}UM_aU7CB&ugS&J2aPL_zwvM0_k#Y}XtL5fO^XT@~Z zewIr+O(|aw->~4i6JG!LvQ-#!Ahzx{1F_Pk9T2a-U+#w=i~C{*eA zieAjDLQvKD)zxPEQiXQC3UIn0VgU&TiATt)bn?YR7sS-ux>jdB(FIm;A5hS)7$+wr zhrF_^fvO!?5c!ju)vmgO4J`gqJ2q*txVrMJ<||}s^;Sqj_H%u|sdkyj)6OZP&LCFd z>WZTYfS__?s})5YSq7SRoN2~+;l6TD)tZsoJd_ z;nHYbFxHu_<;`YRV8psW4}5Oc;dNTS#?SgOoIx zHdVH)e$IYtRcS!2Jj06yjkK-`vMS0#hGV8xU(}dZ6|;t*rwu_;3t1KZwpt*5%{N)7 z`iU=gI11EDX$>MuGl*)b4e0^J>c==*`;!Uj1goYdck3r!XGm*(>Z9u5s1hgS-u0yqHt!HV zsL1v}-SM;%Q(AnlU9Wc+hND#L?r)tkv_XZo%XB<8w7vBE&xqi7S+1>1rd5o@mqiP>5^V~i-U;Jn@-OJ55X zwi|mSkg8h;fXCc>AHyup-mDz&-sc6$`p??f&>1Nm6DPno;%Zls$y^RQtBgj`8}7rp zX|f|)l@sbc@tiN?Yp^QM**DcuJEaUjfrB=s)U}F&)H%CHFywJBuf7k1re&7L@TR^h zgGABF2k<9l&N6mE5bQlPUgnA&J$3_6j&`9YK` z(t3;9$lZ#7nm~ko?z8l(sX)Xjb~0MQs5!Afp|%BLKnwuN#vEFc;!3m!WdKfBZuRgR zu+P&Bm>V}?7oI>d8B++FC7WusimZd=>6U{^Y)ncm(3#AF`BzKHvc;;WespZ%x)KQ|by$_qe52TBy3sxRdO7?+q`6*&dRaCTyXbHzK&Ey(B?H6xrfE>t-j zh>~#JH6e4(IWly-3a7`YD`M#pAZ9g9A2lt;ibqz^lLb&YW^eFdl_T;DX#zGtUpnH2 z6x-*?_%P0LU>a(9Tw+_fG)NsiH(zD%@tGZBX8nXM5i#cK6u61AQeG1IR$Vtymb5@D zOjG?hZLAMW;(U2?jtF1N9+5(fpDNKdf!mUu9*1-y1#iziT{63@XvB`>@GNn@@x+N? z{Yh3;pgW-%%E}M&O2pl`3jzn;UkU6I4XL(vB*s0_{vsqRaOa(RvP5d3M+SWi37f<* zLa2S9r+#n0WZD<0MDBV&ZTzxn9j2&P-}Wh#jwCu&mUGYEi&UGh)8Rb!(55GJy&@o~ z+z-oT{H|P9Q_n3{M$|156e+T|=g{f=yS^mrlARwBL>4LSXQcU%Y?{DySpoI~}$UyIN4@|yn3-lf(&Z~L%jJgx}2Ff2b6~X3i zkPl~y*duHES3CiQRvPg{uMlhR3B@%koSu)zuv1bR z+d_%WvazXS#8#}cuK|?dM_;Iw8^|4qi!8Z-R<6~2fJ>zS(K&Y29AQ`U8mZrF%xyCg zTzpz+jscbMwv9w^RT^LPL3E#qCgRV?>w*fH2n$$Yti#h9Rz4`8fJ^S+TR2#ULiw+uP}pZ?D&)?b$s#q>x)k|{=dU&^%a zoc1xGxz|gAGAWf<(8tFZ8;ytnE z2kNMhs2>D`NG|7AC_!?XVWP7xsF*HOs~`g*mCBzVl~| zOe{vDB38i$tWs~`j6xM***$~3?%Rk@PR(j4yBe9adpmavb?Bz2&D}x zdFU>fJOfohlrR^%RODAz_HN$AJbxzO%LGUB#z@+v2+RAMSD={M0P%%ZSCPv&<0q1& zc58ht`zfr%bI4K`lQNWclQp(!o@PdWA3QXsXvD$_$%So#?%B5ypK?>Mmu5$^U(ZLk zNJKYNoH!QHy)SVUkC+g1vD*&(ZSTf>%bLT&3`}C#4KqvVzpR5B(-BMX-eRSp6HOJhOSvHJc zvMVqb-O0Tm0s1DR^k_2I*S1nU=H%nh)(KW&0*-gTwAfLRSz#lGd!jwMNQAmjg2ufX z+Kd8b355e=svs@wwo02q*ep$qO?#CJk<6K-OjxzlgHZ%q9J(iOwPK$?kToBKrfZ!p zvY_W`wIr)@3&L-ZUO_dsVArfuIp??^GhfQ9RtPNhg2DSr${~lzT)>A zmcLc2vc;<(6*ov}wLy^LqugV?ZSg zd0B2beliSk4S98s&V?lEQ9!>U8Il0?do6R4n{O#AHOG<9yDS+egx*bu5I}>J(;Orl zeUMJM9ck6m^@R-qLn`7AK^L8T2xD=Oh;{X23(QIMQKr?(mezb^YIo?^GmEC5$FT}xj5Ft zV6J-lMab>Ay7-mfV=z-SAyYX#`zypW7?WxQo*h)yu9fP#p)AEt%e0c?>6@d|4;T8S z+ZXm7zLZ_9Xb>XkGD1V1ssiv#w<=$zUpjMaG4^=DK5ljX{}Lp6b}mZE3zr~PN&>YH zg)lUSTzgh22hKznj$r$+O6As_RE0u9L}Pfi(E7yzZ;dJ`M40X0(%--SlC(vY)q+I4bfWxi(~cM6r^c~7N#HbV69Use`~drt?Giw&l)K@ zo4v5~9_!?VFRWnQjP!{k@mA)n5SVyp)d1x^#D15{goi6l6y7TRJr=)Au~g;)bKFi09;6>J!C z3wqOQaes6nnwpoZ9aoZZt7{)q!p1>;oI3&1w^C3PsF%p(c3n}TV9|hd!|E!MN8Q)1 z7Z8nR)Tv+vQIMmX1N+FDXSt{lo(g`vJ>II_JlX^`;ZY@Zn}Wx910K^!E%AU70nMtIX4`9ZE8AXW9JhA!=XPqB0A348e5|h3kIdSbFOROMXgXk%uk4qi6qpOD_WMS z9NGAaEW8eSvOz)tkqAO^;qq1l`zDU%cnb6M#S>1+$Xp=;1gB^NXB%=1Z6r;=kwX>p zCcIvYsf5>OC~o=muVMn@(wAIwZo&i!@qtPS3^X?F3Obl?YbU)oA6>&y?r62^S=)5o zY8qsbsxDYvx#mNe*)Nl0U?x;aGF=Ji`LJC6>zb7v$DZ4|%HU4B9+mBpFsTg*03X=0Oav)@q5g z!p)<@r)znFHs<#@D5y-{f*9@F3Fkx2_VakR1y9;EkXz#dbR{AecM+|n7=oBAvo_Xp zb*^(v-W07&Zqe#>IWz=XRWE~vAAFZ-*@~x+%O83Disy@=z69l2XlpaYxkC!MN zwp$fD41ZL&t@U*v2u;fpBimjp_X-D9K{`Uqj=}v{l^%W6iA!1anh) zkdjifN0ID&ZxFe-w$0os-kXY~2M? zFQZgguDH0as5HXX${RG=1%&z*zX4fyLN4`+-ESNP* zme#J6S&A`-iuo{5FZ&iG2ecqDh^5_-E>~fJldG*Omw5IXBaU(h_2JG&lcI2PYgRll zd!-i3g4UJ!w5FgbvC}{~F4MK*m91;<{5Fs`u-Y-k7Ssc{1?31#S33Q%V8;DACO0<5 zf?3_u){cpw>z3Nkbqh#lD0*>7GP4ba=7LVX@I<-N{CaD|aVXqPPQk*;;rI1_xhIPphlmf+9|RD2 zuEZqYkyCgI62Gi03$?GHW>eY=TQCgslO)A7nK%`43qlC?W54Rz+}1d1J>4Oyq**v7 z`SeQKyPvXD&RAX_#?G2|hY@sY-C_|BZt839LL&tQkZ`YKV{Li&KKCbkA$UnRVC^ae z#ujFT+r@|`G~5BM7;p2vKKi9j@^Z}C35X@HP&w-{$Nh;}HUggWR0-1OG({=!YJ_Ij z1ZZ3!UnelPmR)63x5Zn*UKYfkE`~6%-pVET9V<066(KU3V>wo7GN&p(T>WmRPqdY{ zDcUO&l)Pgg=e?F@<+vxrMe6b!Q<_yDI)c);vK(EDoY5$e#|{a!wP!Du)}F`Qdvy+- z&S!$yE8NZVC5z+^8PN2OcI<(XF+5OdhM`3kboOX!yu8UICPl%h7!*=DOokEXC9CCm z)X&8|k)JH?x!}U{^sO3O<8Pb5M@B_|woX?P{=L$aMu|fDn;GMDm|A%j|JeW@8K7ko zRpJ(sP2egal#(A3^kOPmBcC~nPG}|g-hKS$x8JY7GVQ1SlJ+}nu>DDG&idh4i@}Zl zfA^=hrJetO{!`nfO8VzAwjU-Jy8ZkFpFfm*fBr`w5S)LB*XPea{^T{@6Mc4C>6yCy zIOk8WAA9_p_ujRwKc&u=d)_(%|H-Xye_DP1cKY8?B;E9nf8zWv{`rSr{pN=+fA~1R z|BL63K0UwEffHz4@;%{I&K!`|1z> z@a6w`=*M4Ve=|nF_9x_@JNeg!`}xn;whi{+4}WsxCnvT2*?FZ5>x4EY`?vp;`xnma zci+GJ!qNWnTYotCGu+vL>y}QJ=hOuLPp1B}Z@&5Vhwoqhwm%~O58r+H`>#HJ_pamp55IJ{fBgNoUw{0Qmp}gfZ~tT+@fW}T_FL!M n@y&gG@tfcOuaBLD!~On;FTeX?PDcvycz*XsfAklB^_Tw}g{fF; literal 0 HcmV?d00001 diff --git a/DuckDuckGo/Assets.xcassets/Images/GeneralIcon.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/GeneralIcon.imageset/Contents.json new file mode 100644 index 0000000000..e12d3b5fd5 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/GeneralIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Icon 16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/GeneralIcon.imageset/Icon 16.pdf b/DuckDuckGo/Assets.xcassets/Images/GeneralIcon.imageset/Icon 16.pdf new file mode 100644 index 0000000000000000000000000000000000000000..9ab1864ed29b529ac0d88dc26866086ab3263f2e GIT binary patch literal 7715 zcma*sL602CaRuOe|BAjWAQ@0IGpj1I3J?TXyOv?thS!vDf)3Q|XhmZPoF;fZmfG*hecaCj`g_Sy}Oj#-bbItFF!orJzmz^zAb&}*Sg+%J+^hfzPl{9 zag?%7W9@s{>-B-L9eu6!T9@0g?c2Itms_bvJ4Q^a|JTA)+px{Gw%bwGt?$=6k1nb5dRw+yTDPBQ_uiL%uH(Vj z?N@JYIXoR>ulv}9{Wj`0I+s1Xv~`-cy6&T0tH3Nn=Eq zt@F7g_S;d{dfjjR=zBe8pvO`=%UasP66adqocB5sfL7Pjw{;n1XWNc#i$^!^92^?A zwlBRq;H`~C@UHzPlC5mhv8}C5PWo-_BDdKq&enD`Hiz5y{Te99ZQHM#u&u}F9ILHK z97{Rs!sncF8*3Zeb-k4~5}#}~_GRbna@(wu7?G;2&*Zjklp<0BHO78ev-GX^Vy|`^ zZR@S&x%F7s9@vhhN>SE1heHCxwk~z?p&aYF_k0z1MpAu2<)25B-X)+|*8L+JtjwTPfKy=)Q+V1YF_N6#E2hz0E4#8IQgwqH{kLy*Z#%^aG@4 zU9Ce^&MY<|jA>tcSMJNLx1)F$|A1rJOABDQImCv4=%cR(v<5|W0TU+h^-vyOg+)~5 zbU*M1j8=fLhy7OM()v<>xmC~3{m@lYzcG%w_!I_PrL`rSYO3YD_JaaY?7%+;ll{1& z$hR(2f^gJN{*`?eW-Ms{SHxq85ynopYe;+i+W+) zTdQH5Rq@wFI1W`Hm(vu8!Rv8T$>SstPA_H2*TK5lnDi!EG8|HtSKM(PPv%Nui4rXY zV9|$lbTIQJh$Z!!@0IWgffZxzsWMi0bLzPkgTOf30xWfYQZ4H-VN=C8kcOLm*67@` z?R5icRwx|Ha_;F!C3wLN`1+XitNur9;T{xXCv@bYym7s?HeiFF$4%rL3ShKMUTt}J z=0LA8?qK2*zD+D`ZJk^L$_}S%KTVJk5AM{fV8BtF;lLqbDdGijs=*kKT_}SD8!FXl z>VZ>-V#RZsoB|Pq6Iu&#Pp50zmi@L{l5nQ8cV}OL63Mmf$7RlY_-}ODyUU-#3=IUk zN08Hyw|9v`k<}4wljwVui<94qSvs)lyv={>gJIUzEMlRHR$+ zC2^pcTCUy!gnwV2Fz5c`-(4Or|Nj4%Hn3SnnZ54N#}Grb4J_%+i15n~r?$a==@gQn z3yGH40$ZIBV#J*lUBKjaCMzw4P&k-sB?Tr4ba9Z}G#4CHzSQoI8qL!8( zL6P=#G@zU-i{7(^_7tctl>$RE+7HtvSz#KACGNdwrLECEG0H$y+-a>EtZ5Ue1lMM z!UWJLk)!s~-AdC(vJO?Lx7d`P{n#KEo`Jy*8)ktH%fH*+bx1L$uypjO;sVaT8_wx@yvUt!@IiF3c)D*TI76KnDq;!!YbjFa zC18kX<{gAq7Ru~rn^B-L@Sr;raO=i&MwyD`*&+cYuIR7H`$t9AY_!R|>kj`<{|EtC zVsvA~tS?5jA+Rulc0+YjrZw4&$i_AE9H?U%p`T3}CQW5sAxn40y|?JP=`^>5JruVn z(p-w-wv5t+IwTz-@j0AW1~NLa8(9d|GI~_B*l{RtR@ILVu^}F}aWvbPPEkOBGKe!( z0Tt(^L<|ISOL47N%+8T|&hO)q=vgV=Bt$-uVOu;(r_E>2s|dnYa%d+SDK|l7LQI{qFdY^g`I!rev4HX@ z-i~wvQr52Ugj4?u#yw8alW=%dE2ZC4^AJpGh$ew&HZ(yx1CvuQx?dl>v0rLFLV(ee zMvyar?gu?otZL^RA*)=Nf+HO?Bc|C>OifTaqcSV45<~01(%)(Vf(B9dDF;bjdhO~#5-A_bWzkCX(f0ti>VF*Dk2{LM*@Wzfr@TtqkyD= zUm|ZXN~u#}y3B7Ue@r4ZK8;bQcZU-DJdtJ{EVve=b_H_D<(4UL$Z7vZw@nkyY+em! zUgsRzp&qPLiqO17sJwHOLw*F-ic4$3-`41YhJfy{kqzf82_@%tO)r~-@k#wamglBa z$n{pNQHbiyl?z^T)C_*ybfdG)`EvUbSPF^~NS@W52Q6}8!4|}Bh&3F6(N&UgNR8_; zxe5YCPT{=Fm$6k|^yLXNt-+=Qsw(!iO9Mb;k|Ls+t&v^|PuZ9vaV=Mq5Dr~EXGPwa z@|0Xt7AzQkp&BxDkbLWrc3jeMlNUJ4q-%Zxk&ILG3(RoU^Qb2rB5fA^2qY!!2{k%& zv1pcgYuTfGaW{)IHB;gckvUCnubCJ+>)9?)Z)DhVP=~pDe|gN$MpgMe@8!q3kH75Y zN4)bFzku)44}LKJ!w-S4{{Hdt>BIB&cfa?;puboD@xT9de|`1soA2*m4F7un-P=|9s6)kFQ_0`}*ne!~LT-BA8FcZ{NIU zyKXP0&wibZ>C>;4um1V%cU-;Z1h20j-}Cj;pFLZD;{P+tXFKUCpY4GxEStYncJbO-MR${Oj|bn%UuU zcPV7;eUL^q)75o3Rdu@c=KZ^GzVK5W20yxS`;UJP_Tqa8KFg+uA@4z&a;j8 zMRDmqq8CZ^-SbtPGAfC1m_n{TSJsi5?gAMJQ;I%RGuUNslGt0R85cq&X+ajWLglf)`0tj zn$QFLttjQ8pdya9lI4J2>B_}nsbszqhKj8&I>%aGl7XCXKQtdf73TQX027XthBowj z_GBmJFtyOLg4_FIxf*@_a3L{pLJ;c~DU5n5WXQbir4RHIN}fP>Haj;}dyt zOKmkm26E(Fb){Kth1Ltp!&GZ)VCQzAgs57@8CMf7w^3D9SS`O?B>fd7nKA~Dc;8&; zSRnM&noQPO73#r7CXE3*y~`0v23RHL!a7<)0Df9`XxT3)xHTZnyV^;HI!D4cqJ&4g z$hbre_QQZUOahsA!=8C1cj8Gv^5n>zr4Wl}NS5Mbm-B=zf-!0qKvA=4YT$SQsf)hO zGV(zuq=r(YdikmV9>A~{MvtY`=d}{BL_1DB1iGjau;#?ap_hRett-PM3fy^>BNdWxrbl!FXX#1W7&QArSrDKsjRDOEH& zHEp5`Gy#l`%mYP*v7H-Z<@70j{DkY$i}aIH~9 zuF<3B{p%m%+piAyGUSicDK1unI9SrTNhDv&-d`c*FJhvDe{);w_?@vol^9 z5`EclR5FxE!ySCn?7>4Js-@CCMKPHDeryw6xf|M|IIbf=RDs-LeneQ2rlALC)zf9i zN{kpgR0)j)o&(wKumK0bl8DchS)K|!t_qhO8dV`462kWwrYK>ZXcu~80_~B_0yE0A zb!ZA^D+ypnqX{EB#VJ?ltQzNqUPrG|t3UKy3`F}!*dmReOwt|=fwVOU9Yu!FHQ14e zW1}@N%Nv>llvC+FLZBM?K2WXM8k*I97c?KIUL)oZA{sF-f#gwf47OuPVxfSiAavJL zGv6Hiz|dV>f?3M5%s3#8grMRqT-wTjSVj7bJdaeu9GD2+DFlk7LR?83mYgmk_JJAj zp5fyuShS04G1|0-=?~{T74-AzU;|(t<_SndTm8~*AjC5Zgii|z^c^KE8~dRPz>2Bq z`A7yVblfO0dfca~JM(lt2b9K=RRUvScC_-m&KL{di_Ad_XsD18F_u|G^g^8b)Ur~X z10p*2DWD|%OcHXyNLg)IC99=->liZ!+E>YH!n$tt&?0*d&weGapPa%vY0-ogJ9$Pj z?lfl_hdMBzm)#Ce3bgR?EUEA<`q8>h)$Ay6n3t$EgIVlsmGF6BT!7hmoxOsg0$#et zteqH;a&?!T1$&ak&b@U+ie%B*OsTZ*Nfrx^{Zm+vVIi-V)4f)}6rWvuK6Wy7DEQ&Z4=VwXwmRM?D3q^Zz-yX|9J%%TiC} zN0k^g(6cBTZ<+gyDvqQa!i|M7*wn{}7iSRribGMLbBWs}8)77u4^z|`m?HsPMv|_l4vJ7G+Xf*LHs*T9Di~vQ3^DUKYRm?A+eR)pFeJCc zV9Zel0PD>8-K_*yE8fO$vpwnuW?Hy)9j@}fs9mH6GVYLF_pT#v28eSAYtRiU4qsf z+VCq0IVW(Mq)LdJllgw&@L?w8+y{XTcvfn#v20bBA{PU?$KX_+fGR|@^0d6wD1c!IKT=4gU;Os|e*5$|e)o4SsQF#|?Z5xpj2G{(K5iDlpEn zG=8U0&Ao1Y@UZe(^!&8bbh&xl{`hdcc^tW487~)gZ@2ePn|pS|`&Ib<>H*nE=uNbD zOD@rRdw%i9n-7>e%7e>sx6j+{kF51a_CF%o(*z*)G|9oob;D=J-(Edk-EKd=T>8_) z=KH~ob(nqCpCd;%=n~@PCurz5a)Mlby7Ja<6>-8i7LnP5ub2i0qT=>+Jw06A+-@EQ z{yX8~5AO)aXOG*v&FAB1Hy=KqgK}}X-9C+S*rI;%?aj{{thfJGxO#XpcbGVFc=P6W HfBEKr6W-}f diff --git a/DuckDuckGo/Assets.xcassets/Images/DownloadsPreferences.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/OtherPlatformsPreferences.imageset/Contents.json similarity index 100% rename from DuckDuckGo/Assets.xcassets/Images/DownloadsPreferences.imageset/Contents.json rename to DuckDuckGo/Assets.xcassets/Images/OtherPlatformsPreferences.imageset/Contents.json diff --git a/DuckDuckGo/Assets.xcassets/Images/DownloadsPreferences.imageset/DownloadsPreferences.pdf b/DuckDuckGo/Assets.xcassets/Images/OtherPlatformsPreferences.imageset/DownloadsPreferences.pdf similarity index 100% rename from DuckDuckGo/Assets.xcassets/Images/DownloadsPreferences.imageset/DownloadsPreferences.pdf rename to DuckDuckGo/Assets.xcassets/Images/OtherPlatformsPreferences.imageset/DownloadsPreferences.pdf diff --git a/DuckDuckGo/Assets.xcassets/Images/PrivateSearchIcon.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/PrivateSearchIcon.imageset/Contents.json new file mode 100644 index 0000000000..a17c70f1fe --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/PrivateSearchIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Icon 10.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/PrivateSearchIcon.imageset/Icon 10.pdf b/DuckDuckGo/Assets.xcassets/Images/PrivateSearchIcon.imageset/Icon 10.pdf new file mode 100644 index 0000000000000000000000000000000000000000..73ae44c94e97ff7b24e0784b029aa1d4e59a5c62 GIT binary patch literal 3482 zcmeHJOK%%D5We$Q@Dd;?5Q^^y5EzJ`DB2*d)kA=whxK}6E0wjbc3n96^_}5zNlJ22 z=UyIc>f7NwzL_EC)#d5=TfrP5f^ynleT)$;Lrj7HD+anK(zMBXFJr249rmB`KUK!;J|wVx-qjQHXV>Ni!k5lA0$!Gk@=gm=NCl+~VtpB4(jYbldBcx7gvCI!(? z$)xhe*&>w_Y?$-G2RqZjA%x_4T?Wht&Lcs}a}9n%9yN#hMPBy!Y{?I%F*}1zq`k8- zkDNiJ!va(Lo0vzzj91nuaM&5oVFQW8bQ$9VmCObOIf0O!QQQbXlI~n@8L1j2W7#PK zq)S%{hSM->su_}ydF`wKcjg|2OJ<5DTg2NY1z0yqMoUwOtgm2|-2yr$gK-)Xp)hvK z1(X29E~TKLL;`#QNZ?^OpbZs0Ce05ZfFguzzq~xeN+S^ zEuZvR<7JR^z5}D6(!%kuiwSMDMib@mVHVd=J`hXHeZ@5-H-k{L*p<+fkZ++J_gJ-i zTxSHcYrT~LbCaZU=N4557OVaSB9)ce8PJea0T=rg3ly)xT|ALV$fqza3Va@yyn&l|jI@{cksKs7!y zLQH)WbwQCQo$GS`Ijrcx!k?bMXVQ*oo6;_o%vm=X#ZfE`VR6d(1l^CBF2m5-(|EBQ zo3V@E&qVyU{Ll=4#@#f>;m@6^s|;tmyTt{?l1t;=?ze+q3z+NPuOIH3|5iQEZU;IL zNW*514XAJxO$@zg=vx41w*y%KwWmD{DbHjH6;4^wHk?IIH>>7H2UpZoW?(FfdGJ2zH)FQ}i^#wXUgCT`(vYBv zCxoU5(Ai&BH{dG82(IYt@k##(XT^d410aHYZ>$bCKZ_uU)%dUf+=qIhxD_rNdG7xT4PuD*B31&}+8 S&326Jz;GkTt5;_q&;J1ev)Ri4 literal 0 HcmV?d00001 diff --git a/DuckDuckGo/Assets.xcassets/Images/WebTrackingProtectionIcon.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/WebTrackingProtectionIcon.imageset/Contents.json new file mode 100644 index 0000000000..09604788db --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/WebTrackingProtectionIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Icon 11.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/WebTrackingProtectionIcon.imageset/Icon 11.pdf b/DuckDuckGo/Assets.xcassets/Images/WebTrackingProtectionIcon.imageset/Icon 11.pdf new file mode 100644 index 0000000000000000000000000000000000000000..59f2624371964e36da805c6753e50bffc07732f8 GIT binary patch literal 4673 zcmeI0U5^~a6^8HoSJaIqh(vFFo~n`%64)V%k|@rSJH*8@9?YWgPO`H^gkPWMt?ua= z;|&t`j3ul0?W$Af^F62f#p_pJeiG(7O~HA0{Pu^*xlccJpM5s%AKvKCDPH3@f89U4 zefr)dz+0_4Jw5L4-c2{J_W!xx?Qj3|g?stc^22 zLvg!zlb=iMIiSOwQ%IF_=i1HJ=1#i@UcWCmI&FzQ@b7%s*y|(zgydqW@K9Rlz4H)~ zb)VdheKlG36)c7ls=31yQ6*XGF3chL#;F+*xy1z1di1`#GW%LW&Mu*1+J`uIXn^1fOE3N?g~$Ewl_UfE5V4OQ-79Zrrp$(R~aA6tW=lesvz zP*OoRq}Ak$u{F+MISzSfoA4j;OSxk+>pH@!9N*9^u`14%%?~H}*$S>?9g+=aDlRD* zF%r~!i+!d)@C)}1&Te3crZgtR_r{2%mIruGwy6{_AXpO3{SSf*p-$aGllg_(EaJ5>1 zE-v6E5-6@O&!+&b)v&uJ;#KhB8jCTtqLNz=*vx^97CkmZHMlKb*ad$kIn+g%)N2NXSc3D? zTtlxuyGHx5s+NJ;Mw^L138I58fpV|{(eBvVbIO560{g{ew1z&@fa=PUq0(J|da;gz ztje)=+6+-tR}8UnMx5c-(g)Xu9z%!z!RQD|8pv6l5E74b3axV*yJ>3lKq(Dc>80pE zj-+6IoD^OsgjWNHaWGA%)l9pHv!`3&*FIM|E)-3} zqixXCh)1ZNHb*0|KvQF5ojXcmkeW*>^sfV0^XQhr=#weN4a55j7NnzARkbCpBH|zJ zllsuCfa37IDs7GyMR%;?MmLsD!q$Yx>wj1j{H$(l+y4-#0B_xBZo5j*{O zu@S!>)WC}ojk6~zPQw0z3s-LuC8~(g!>*mmR7Hvs_Qm%-I}sNQLe7i`MIt0C0-COo zG(5Oe@tAsu3)+-0?0`NLapp-5XR3nCe91~HrBDkbleTuFR7U1A_|WCiEmKk?*piYX z5tsRDWHGtR{J>U9#S_8-GSM}nszD_(|jXrM5O-d~QI-oT&p;s?1-qwJ>rK>~Y;k#HN%VX^{3Pv$CyEoMRrOj`vZUv8>1_Ng@tuqdl(Ax5Q*+C*A_Ql6nD zQR0%6HfpJt22;aUm08UgZ={T;n0jS9R(Hh=$TBmZARk0;3j}oyJ9>d}#>uS1;V6xE zMIsyEFk3Mr8q{~- z2u+g4Xsno-RcaCl&Xn9tU2C=$luJh|w!$hccU1)WNoJu6XKA(^%mqDCwd7qTofl~- zB2R-fg~AU`U@JU17>)-@9PO0tjIv5uc;1Vxn3+k$IM`7UC07MP9gX5tYbNfTO5SHE zAGxETqzH3yBpGxnZ<(o)LXb%$g@odSqXIVL<{SeSq^x*hGXC>;Qic*1Wh~x>xV`+r zl>-_E2}vA-bJ>JqQW|DKw!%WVxe|Z{Du^OHfFY`p7HF98vXUVrN<*uJP`_kWDY^+Q z`)D&t)e^i$GCIli}p+|+E zae%fU&B#dx2-*Q-gVA5d!5#C+$V$-g7M7qt{f#Tr@IDk|9Hwe2WD)Cfw$)3G>PP2i z7~yJm(rtge%AfE3y5cY219ZXMJ_pd%E}sc*K7V*PKAqgx-|*Rh-_1Y&`>(ybd42bG zzXE^Tzq!Br;`o#Mdh`*EU6+TzMRxq$<>@Txc7Hnl__*7joIXt4uHs%EAD;FPoJhOf zz}I(=V2(aLpsQ~eg0^oOH-EW*gQ`vz+`98QFONU6H+-P~3FJx=?e#6{s35Y6%f zNcY)wi*|F+K-Pt TcaKlQ97-ZgFJAod?_d58P79)> literal 0 HcmV?d00001 diff --git a/DuckDuckGo/Autoconsent/AutoconsentUserScript.swift b/DuckDuckGo/Autoconsent/AutoconsentUserScript.swift index b43155f629..c43f7dc27c 100644 --- a/DuckDuckGo/Autoconsent/AutoconsentUserScript.swift +++ b/DuckDuckGo/Autoconsent/AutoconsentUserScript.swift @@ -41,7 +41,7 @@ final class AutoconsentUserScript: NSObject, WKScriptMessageHandlerWithReply, Us private weak var selfTestFrameInfo: WKFrameInfo? private var topUrl: URL? - private let preferences = PrivacySecurityPreferences.shared + private let preferences = CookiePopupProtectionPreferences.shared private let management = AutoconsentManagement.shared public var messageNames: [String] { MessageName.allCases.map(\.rawValue) } @@ -209,7 +209,7 @@ extension AutoconsentUserScript { return } - if preferences.autoconsentEnabled == false { + if preferences.isAutoconsentEnabled == false { // this will only happen if the user has just declined a prompt in this tab replyHandler([ "type": "ok" ], nil) // this is just to prevent a Promise rejection return @@ -240,7 +240,7 @@ extension AutoconsentUserScript { "rules": nil, // rules are bundled with the content script atm "config": [ "enabled": true, - "autoAction": preferences.autoconsentEnabled == true ? "optOut" : nil, + "autoAction": preferences.isAutoconsentEnabled == true ? "optOut" : nil, "disabledCmps": disabledCMPs, "enablePrehide": true, "enableCosmeticRules": true, diff --git a/DuckDuckGo/Autofill/ContentOverlayViewController.swift b/DuckDuckGo/Autofill/ContentOverlayViewController.swift index 93b95b830c..bbc73f1f3a 100644 --- a/DuckDuckGo/Autofill/ContentOverlayViewController.swift +++ b/DuckDuckGo/Autofill/ContentOverlayViewController.swift @@ -343,7 +343,8 @@ extension ContentOverlayViewController: SecureVaultManagerDelegate { } public func secureVaultManager(_: SecureVaultManager, didRequestRuntimeConfigurationForDomain domain: String, completionHandler: @escaping (String?) -> Void) { - let properties = ContentScopeProperties(gpcEnabled: PrivacySecurityPreferences.shared.gpcEnabled, + let isGPCEnabled = WebTrackingProtectionPreferences.shared.isGPCEnabled + let properties = ContentScopeProperties(gpcEnabled: isGPCEnabled, sessionKey: topAutofillUserScript?.sessionKey ?? "", featureToggles: ContentScopeFeatureToggles.supportedFeaturesOnMacOS(privacyConfigurationManager.privacyConfig)) diff --git a/DuckDuckGo/Common/Extensions/URLExtension.swift b/DuckDuckGo/Common/Extensions/URLExtension.swift index 1e367e4e6c..3f5b12e8f7 100644 --- a/DuckDuckGo/Common/Extensions/URLExtension.swift +++ b/DuckDuckGo/Common/Extensions/URLExtension.swift @@ -340,13 +340,21 @@ extension URL { } static var cookieConsentPopUpManagement: URL { - return URL(string: "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/#cookie-consent-pop-up-management")! + return URL(string: "https://duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/#cookie-pop-up-management")! } static var gpcLearnMore: URL { return URL(string: "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/gpc/")! } + static var privateSearchLearnMore: URL { + return URL(string: "https://duckduckgo.com/duckduckgo-help-pages/search-privacy/")! + } + + static var searchSettings: URL { + return URL(string: "https://duckduckgo.com/settings/")! + } + static var ddgLearnMore: URL { return URL(string: "https://duckduckgo.com/duckduckgo-help-pages/get-duckduckgo/get-duckduckgo-browser-on-mac/")! } @@ -362,6 +370,7 @@ extension URL { static var duckDuckGoEmail = URL(string: "https://duckduckgo.com/email-protection")! static var duckDuckGoEmailLogin = URL(string: "https://duckduckgo.com/email")! + static var duckDuckGoEmailInfo = URL(string: "https://duckduckgo.com/duckduckgo-help-pages/email-protection/what-is-duckduckgo-email-protection/")! static var duckDuckGoMorePrivacyInfo = URL(string: "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/atb/")! var isDuckDuckGo: Bool { diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 3c142fd364..66e81e8608 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -329,10 +329,12 @@ struct UserText { value: "Fireproofing this site will keep you signed in after using the Fire Button.", comment: "Fireproof confirmation message") static let webTrackingProtectionSettingsTitle = NSLocalizedString("web.tracking.protection.title", value: "Web Tracking Protection", comment: "Web tracking protection settings section title") - static let webTrackingProtectionExplenation = NSLocalizedString("web.tracking.protection.explenation", value: "DuckDuckGo automatically blocks hidden trackers as you browse the web.", comment: "feature explanation in settings") - static let autoconsentSettingsTitle = NSLocalizedString("autoconsent.title", value: "Cookie Pop-ups", comment: "Autoconsent settings section title") + static let webTrackingProtectionExplenation = NSLocalizedString("web.tracking.protection.explenation", value: "DuckDuckGo automatically blocks hidden trackers as you browse the web.", comment: "Privacy feature explanation in the browser settings") static let autoconsentCheckboxTitle = NSLocalizedString("autoconsent.checkbox.title", value: "Automatically handle cookie pop-ups", comment: "Autoconsent settings checkbox title") static let autoconsentExplanation = NSLocalizedString("autoconsent.explanation", value: "DuckDuckGo will try to select the most private settings available and hide these pop-ups for you.", comment: "Autoconsent feature explanation in settings") + static let privateSearchExplanation = NSLocalizedString("private.search.explenation", value: "DuckDuckGo Private Search is your default search engine, so you can search the web without being tracked.", comment: "feature explanation in settings") + static let webTrackingProtectionExplanation = NSLocalizedString("web.tracking.protection.explanation", value: "DuckDuckGo automatically blocks hidden trackers as you browse the web.", comment: "feature explanation in settings") + static let emailProtectionExplanation = NSLocalizedString("email.protection.explanation", value: "Block email trackers and hide your address without switching your email provider.", comment: "Email protection feature explanation in settings. The feature blocks email trackers and hides original email address.") // Misc @@ -342,7 +344,6 @@ struct UserText { static let duckPlayerOff = NSLocalizedString("duck-player.off", value: "Never use Duck Player", comment: "Private YouTube Player option") static let duckPlayerExplanation = NSLocalizedString("duck-player.explanation", value: "Duck Player provides a clean viewing experience without personalized ads and prevents viewing activity from influencing your YouTube recommendations.", comment: "Private YouTube Player explanation in settings") - static let gpcSettingsTitle = NSLocalizedString("gpc.title", value: "Global Privacy Control (GPC)", comment: "GPC settings title") static let gpcCheckboxTitle = NSLocalizedString("gpc.checkbox.title", value: "Enable Global Privacy Control", comment: "GPC settings checkbox title") static let gpcExplanation = NSLocalizedString("gpc.explanation", value: "Tells participating websites not to sell or share your data.", comment: "GPC explanation in settings") static let learnMore = NSLocalizedString("learnmore.link", value: "Learn More", comment: "Learn More link") @@ -516,8 +517,8 @@ struct UserText { static let settings = NSLocalizedString("settings", value: "Settings", comment: "Menu item for opening settings") - static let general = NSLocalizedString("preferences.general", value: "General", comment: "Show general preferences") - static let sync = NSLocalizedString("preferences.sync", value: "Sync & Backup", comment: "Show sync preferences") + static let general = NSLocalizedString("preferences.general", value: "General", comment: "Title of the option to show the General preferences") + static let sync = NSLocalizedString("preferences.sync", value: "Sync & Backup", comment: "Title of the option to show the Sync preferences") static let syncAutoLockPrompt = NSLocalizedString("preferences.sync.auto-lock-prompt", value:"Unlock device to setup Sync & Backup", comment: "Reason for auth when setting up Sync") static let syncBookmarkPausedAlertTitle = NSLocalizedString("alert.sync-bookmarks-paused-title", value: "Bookmarks Sync is Paused", comment: "Title for alert shown when sync bookmarks paused for too many items") static let syncBookmarkPausedAlertDescription = NSLocalizedString("alert.sync-bookmarks-paused-description", value: "You have exceeded the bookmarks sync limit. Try deleting some bookmarks. Until this is resolved your bookmarks will not be backed up.", comment: "Description for alert shown when sync bookmarks paused for too many items") @@ -526,14 +527,29 @@ struct UserText { static let syncPausedTitle = NSLocalizedString("alert.sync.warning.sync-paused", value: "Sync & Backup is Paused", comment: "Title of the warning message") static let syncUnavailableMessage = NSLocalizedString("alert.sync.warning.sync-unavailable-message", value: "Sorry, but Sync & Backup is currently unavailable. Please try again later.", comment: "Data syncing unavailable warning message") static let syncUnavailableMessageUpgradeRequired = NSLocalizedString("alert.sync.warning.data-syncing-disabled-upgrade-required", value: "Sorry, but Sync & Backup is no longer available in this app version. Please update DuckDuckGo to the latest version to continue.", comment: "Data syncing unavailable warning message") - static let defaultBrowser = NSLocalizedString("preferences.default-browser", value: "Default Browser", comment: "Show default browser preferences") - static let appearance = NSLocalizedString("preferences.appearance", value: "Appearance", comment: "Show appearance preferences") - static let privacy = NSLocalizedString("preferences.privacy", value: "Privacy", comment: "Show privacy browser preferences") - static let vpn = NSLocalizedString("preferences.vpn", value: "VPN", comment: "Show VPN preferences") - static let duckPlayer = NSLocalizedString("preferences.duck-player", value: "Duck Player", comment: "Show Duck Player browser preferences") - static let about = NSLocalizedString("preferences.about", value: "About", comment: "Show about screen") - - static let downloads = NSLocalizedString("preferences.downloads", value: "Downloads", comment: "Show downloads browser preferences") + static let privacyProtections = NSLocalizedString("preferences.privacy-protections", value: "Privacy Protections", comment: "The section header in Preferences representing browser features related to privacy protection") + static let mainSettings = NSLocalizedString("preferences.main-settings", value: "Main Settings", comment: "Section header in Preferences for main settings") + static let preferencesOn = NSLocalizedString("preferences.on", value: "On", comment: "Status indicator of a browser privacy protection feature.") + static let preferencesOff = NSLocalizedString("preferences.off", value: "Off", comment: "Status indicator of a browser privacy protection feature.") + static let preferencesAlwaysOn = NSLocalizedString("preferences.always-on", value: "Always On", comment: "Status indicator of a browser privacy protection feature.") + static let duckduckgoOnOtherPlatforms = NSLocalizedString("preferences.duckduckgo-on-other-platforms", value: "DuckDuckGo on Other Platforms", comment: "Button presented to users to navigate them to our product page which presents all other products for other platforms") + static let defaultBrowser = NSLocalizedString("preferences.default-browser", value: "Default Browser", comment: "Title of the option to show the Default Browser Preferences") + static let privateSearch = NSLocalizedString("preferences.private-search", value: "Private Search", comment: "Title of the option to show the Private Search preferences") + static let appearance = NSLocalizedString("preferences.appearance", value: "Appearance", comment: "Title of the option to show the Appearance preferences") + static let dataClearing = NSLocalizedString("preferences.data-clearing", value: "Data Clearing", comment: "Title of the option to show the Data Clearing preferences") + static let webTrackingProtection = NSLocalizedString("preferences.web-tracking-protection", value: "Web Tracking Protection", comment: "Title of the option to show the Web Tracking Protection preferences") + static let emailProtectionPreferences = NSLocalizedString("preferences.email-protection", value: "Email Protection", comment: "Title of the option to show the Email Protection preferences") + static let autofillEnabledFor = NSLocalizedString("preferences.autofill-enabled-for", value: "Autofill enabled in this browser for:", comment: "Label presented before the email account in email protection preferences") + + static let vpn = NSLocalizedString("preferences.vpn", value: "VPN", comment: "Title of the option to show the VPN preferences") + static let duckPlayer = NSLocalizedString("preferences.duck-player", value: "Duck Player", comment: "Title of the option to show the Duck Player browser preferences") + static let about = NSLocalizedString("preferences.about", value: "About", comment: "Title of the option to show the About screen") + + static let accessibility = NSLocalizedString("preferences.accessibility", value: "Accessibility", comment: "Title of the option to show the Accessibility browser preferences") + static let cookiePopUpProtection = NSLocalizedString("preferences.cookie-pop-up-protection", value: "Cookie Pop-Up Protection", comment: "Title of the option to show the Cookie Pop-Up Protection preferences") + static let downloads = NSLocalizedString("preferences.downloads", value: "Downloads", comment: "Title of the downloads browser preferences") + static let support = NSLocalizedString("preferences.support", value: "Support", comment: "Open support page") + static let isDefaultBrowser = NSLocalizedString("preferences.default-browser.active", value: "DuckDuckGo is your default browser", comment: "Indicate that the browser is the default") static let isNotDefaultBrowser = NSLocalizedString("preferences.default-browser.inactive", value: "DuckDuckGo is not your default browser.", comment: "Indicate that the browser is not the default") static let makeDefaultBrowser = NSLocalizedString("preferences.default-browser.button.make-default", value: "Make DuckDuckGo Default…", comment: "represents a prompt message asking the user to make DuckDuckGo their default browser.") @@ -568,10 +584,9 @@ struct UserText { static let addressBar = NSLocalizedString("preferences.appearance.address-bar", value: "Address Bar", comment: "Theme preferences") static let showFullWebsiteAddress = NSLocalizedString("preferences.appearance.show-full-url", value: "Full website address", comment: "Option to show full URL in the address bar") static let showAutocompleteSuggestions = NSLocalizedString("preferences.appearance.show-autocomplete-suggestions", value: "Autocomplete suggestions", comment: "Option to show autocomplete suggestions in the address bar") - static let zoomSettingTitle = NSLocalizedString("preferences.appearance.zoom", value: "Zoom", comment: "Zoom settings section title") static let zoomPickerTitle = NSLocalizedString("preferences.appearance.zoom-picker", value: "Default page zoom", comment: "Default page zoom picker title") static let defaultZoomPageMoreOptionsItem = NSLocalizedString("more-options.zoom.default-zoom-page", value: "Change Default Page Zoom…", comment: "Default page zoom picker title") - static let autofill = NSLocalizedString("preferences.autofill", value: "Autofill", comment: "Show Autofill preferences") + static let autofill = NSLocalizedString("preferences.autofill", value: "Passwords", comment: "Show Autofill preferences") static let aboutDuckDuckGo = NSLocalizedString("preferences.about.about-duckduckgo", value: "About DuckDuckGo", comment: "About screen") static let privacySimplified = NSLocalizedString("preferences.about.privacy-simplified", value: "Privacy, simplified.", comment: "About screen") diff --git a/DuckDuckGo/ContentBlocker/ContentBlocking.swift b/DuckDuckGo/ContentBlocker/ContentBlocking.swift index dee72a7514..795f5ec581 100644 --- a/DuckDuckGo/ContentBlocker/ContentBlocking.swift +++ b/DuckDuckGo/ContentBlocker/ContentBlocking.swift @@ -86,7 +86,7 @@ final class AppContentBlocking { privacyConfigurationManager: privacyConfigurationManager, trackerDataManager: trackerDataManager, configStorage: configStorage, - privacySecurityPreferences: PrivacySecurityPreferences.shared, + webTrackingProtectionPreferences: WebTrackingProtectionPreferences.shared, tld: tld) adClickAttributionRulesProvider = AdClickAttributionRulesProvider(config: adClickAttribution, diff --git a/DuckDuckGo/ContentBlocker/ScriptSourceProviding.swift b/DuckDuckGo/ContentBlocker/ScriptSourceProviding.swift index 4013498fff..979ecaf469 100644 --- a/DuckDuckGo/ContentBlocker/ScriptSourceProviding.swift +++ b/DuckDuckGo/ContentBlocker/ScriptSourceProviding.swift @@ -37,7 +37,7 @@ protocol ScriptSourceProviding { // refactor: ScriptSourceProvider to be passed to init methods as `some ScriptSourceProviding`, DefaultScriptSourceProvider to be killed // swiftlint:disable:next identifier_name func DefaultScriptSourceProvider() -> ScriptSourceProviding { - ScriptSourceProvider(configStorage: ConfigurationStore.shared, privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager, privacySettings: PrivacySecurityPreferences.shared, contentBlockingManager: ContentBlocking.shared.contentBlockingManager, trackerDataManager: ContentBlocking.shared.trackerDataManager, tld: ContentBlocking.shared.tld) + ScriptSourceProvider(configStorage: ConfigurationStore.shared, privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager, webTrackingProtectionPreferences: WebTrackingProtectionPreferences.shared, contentBlockingManager: ContentBlocking.shared.contentBlockingManager, trackerDataManager: ContentBlocking.shared.trackerDataManager, tld: ContentBlocking.shared.tld) } struct ScriptSourceProvider: ScriptSourceProviding { @@ -52,19 +52,19 @@ struct ScriptSourceProvider: ScriptSourceProviding { let privacyConfigurationManager: PrivacyConfigurationManaging let contentBlockingManager: ContentBlockerRulesManagerProtocol let trackerDataManager: TrackerDataManager - let privacySettings: PrivacySecurityPreferences + let webTrakcingProtectionPreferences: WebTrackingProtectionPreferences let tld: TLD init(configStorage: ConfigurationStoring, privacyConfigurationManager: PrivacyConfigurationManaging, - privacySettings: PrivacySecurityPreferences, + webTrackingProtectionPreferences: WebTrackingProtectionPreferences, contentBlockingManager: ContentBlockerRulesManagerProtocol, trackerDataManager: TrackerDataManager, tld: TLD) { self.configStorage = configStorage self.privacyConfigurationManager = privacyConfigurationManager - self.privacySettings = privacySettings + self.webTrakcingProtectionPreferences = webTrackingProtectionPreferences self.contentBlockingManager = contentBlockingManager self.trackerDataManager = trackerDataManager self.tld = tld @@ -83,7 +83,7 @@ struct ScriptSourceProvider: ScriptSourceProviding { public func buildAutofillSource() -> AutofillUserScriptSourceProvider { let privacyConfig = self.privacyConfigurationManager.privacyConfig return DefaultAutofillSourceProvider.Builder(privacyConfigurationManager: privacyConfigurationManager, - properties: ContentScopeProperties(gpcEnabled: privacySettings.gpcEnabled, + properties: ContentScopeProperties(gpcEnabled: webTrakcingProtectionPreferences.isGPCEnabled, sessionKey: self.sessionKey ?? "", featureToggles: ContentScopeFeatureToggles.supportedFeaturesOnMacOS(privacyConfig)), isDebug: AutofillPreferences().debugScriptEnabled) diff --git a/DuckDuckGo/DBP/DBPHomeViewController.swift b/DuckDuckGo/DBP/DBPHomeViewController.swift index 9313c0530a..f3b2cf95d0 100644 --- a/DuckDuckGo/DBP/DBPHomeViewController.swift +++ b/DuckDuckGo/DBP/DBPHomeViewController.swift @@ -46,9 +46,9 @@ final class DBPHomeViewController: NSViewController { inlineIconCredentials: false, thirdPartyCredentialsProvider: false) - let privacySettings = PrivacySecurityPreferences.shared + let isGPCEnabled = WebTrackingProtectionPreferences.shared.isGPCEnabled let sessionKey = UUID().uuidString - let prefs = ContentScopeProperties(gpcEnabled: privacySettings.gpcEnabled, + let prefs = ContentScopeProperties(gpcEnabled: isGPCEnabled, sessionKey: sessionKey, featureToggles: features) diff --git a/DuckDuckGo/Email/EmailUrlExtensions.swift b/DuckDuckGo/Email/EmailUrlExtensions.swift index 0055748b94..6b86f032c6 100644 --- a/DuckDuckGo/Email/EmailUrlExtensions.swift +++ b/DuckDuckGo/Email/EmailUrlExtensions.swift @@ -25,6 +25,7 @@ extension EmailUrls { static let emailProtectionLink = "https://duckduckgo.com/email" static let emailProtectionInContextSignupLink = "https://duckduckgo.com/email/start-incontext" static let emailProtectionAccountLink = "https://duckduckgo.com/email/settings/account" + static let emailProtectionSupportLink = "https://duckduckgo.com/email/settings/support" } var emailProtectionLink: URL { @@ -39,6 +40,10 @@ extension EmailUrls { return URL(string: Url.emailProtectionAccountLink)! } + var emailProtectionSupportLink: URL { + return URL(string: Url.emailProtectionSupportLink)! + } + func isDuckDuckGoEmailProtection(url: URL) -> Bool { return url.absoluteString.starts(with: Url.emailProtectionLink) } diff --git a/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift b/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift index cd9f965ea9..6fb1869064 100644 --- a/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift +++ b/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift @@ -71,7 +71,6 @@ extension HomePage.Models { private let dataImportProvider: DataImportStatusProviding private let tabCollectionViewModel: TabCollectionViewModel private let emailManager: EmailManager - private let privacyPreferences: PrivacySecurityPreferences private let duckPlayerPreferences: DuckPlayerPreferencesPersistor private let randomNumberGenerator: RandomNumberGenerating @@ -144,7 +143,6 @@ extension HomePage.Models { dataImportProvider: DataImportStatusProviding, tabCollectionViewModel: TabCollectionViewModel, emailManager: EmailManager = EmailManager(), - privacyPreferences: PrivacySecurityPreferences = PrivacySecurityPreferences.shared, duckPlayerPreferences: DuckPlayerPreferencesPersistor, homePageRemoteMessaging: HomePageRemoteMessaging, privacyConfigurationManager: PrivacyConfigurationManaging = AppPrivacyFeatures.shared.contentBlocking.privacyConfigurationManager, @@ -153,7 +151,6 @@ extension HomePage.Models { self.dataImportProvider = dataImportProvider self.tabCollectionViewModel = tabCollectionViewModel self.emailManager = emailManager - self.privacyPreferences = privacyPreferences self.duckPlayerPreferences = duckPlayerPreferences self.homePageRemoteMessaging = homePageRemoteMessaging self.privacyConfigurationManager = privacyConfigurationManager @@ -170,6 +167,7 @@ extension HomePage.Models { switch featureType { case .defaultBrowser: do { + Pixel.fire(.defaultRequestedFromHomepageSetupView) try defaultBrowserProvider.presentDefaultBrowserPrompt() } catch { defaultBrowserProvider.openSystemPreferences() diff --git a/DuckDuckGo/HomePage/View/HomePageViewController.swift b/DuckDuckGo/HomePage/View/HomePageViewController.swift index 03f2cb4e08..96fb2aca47 100644 --- a/DuckDuckGo/HomePage/View/HomePageViewController.swift +++ b/DuckDuckGo/HomePage/View/HomePageViewController.swift @@ -42,7 +42,9 @@ final class HomePageViewController: NSViewController { var defaultBrowserModel: HomePage.Models.DefaultBrowserModel! var recentlyVisitedModel: HomePage.Models.RecentlyVisitedModel! var featuresModel: HomePage.Models.ContinueSetUpModel! - var appearancePreferences: AppearancePreferences! + let accessibilityPreferences: AccessibilityPreferences + let appearancePreferences: AppearancePreferences + let defaultBrowserPreferences: DefaultBrowserPreferences var cancellables = Set() @UserDefaultsWrapper(key: .defaultBrowserDismissed, defaultValue: false) @@ -56,13 +58,19 @@ final class HomePageViewController: NSViewController { bookmarkManager: BookmarkManager, historyCoordinating: HistoryCoordinating = HistoryCoordinator.shared, fireViewModel: FireViewModel? = nil, - onboardingViewModel: OnboardingViewModel = OnboardingViewModel()) { + onboardingViewModel: OnboardingViewModel = OnboardingViewModel(), + accessibilityPreferences: AccessibilityPreferences = AccessibilityPreferences.shared, + appearancePreferences: AppearancePreferences = AppearancePreferences.shared, + defaultBrowserPreferences: DefaultBrowserPreferences = DefaultBrowserPreferences.shared) { self.tabCollectionViewModel = tabCollectionViewModel self.bookmarkManager = bookmarkManager self.historyCoordinating = historyCoordinating self.fireViewModel = fireViewModel ?? FireCoordinator.fireViewModel self.onboardingViewModel = onboardingViewModel + self.accessibilityPreferences = accessibilityPreferences + self.appearancePreferences = appearancePreferences + self.defaultBrowserPreferences = defaultBrowserPreferences super.init(nibName: nil, bundle: nil) } @@ -72,7 +80,6 @@ final class HomePageViewController: NSViewController { defaultBrowserModel = createDefaultBrowserModel() recentlyVisitedModel = createRecentlyVisitedModel() featuresModel = createFeatureModel() - appearancePreferences = AppearancePreferences.shared refreshModels() @@ -81,6 +88,7 @@ final class HomePageViewController: NSViewController { .environmentObject(defaultBrowserModel) .environmentObject(recentlyVisitedModel) .environmentObject(featuresModel) + .environmentObject(accessibilityPreferences) .environmentObject(appearancePreferences) .onTapGesture { [weak self] in // Remove focus from the address bar if interacting with this view. @@ -149,8 +157,9 @@ final class HomePageViewController: NSViewController { } func createDefaultBrowserModel() -> HomePage.Models.DefaultBrowserModel { - return .init(isDefault: DefaultBrowserPreferences().isDefault, wasClosed: defaultBrowserDismissed, requestSetDefault: { [weak self] in - let defaultBrowserPreferencesModel = DefaultBrowserPreferences() + return .init(isDefault: DefaultBrowserPreferences.shared.isDefault, wasClosed: defaultBrowserDismissed, requestSetDefault: { [weak self] in + Pixel.fire(.defaultRequestedFromHomepage) + let defaultBrowserPreferencesModel = DefaultBrowserPreferences.shared defaultBrowserPreferencesModel.becomeDefault { [weak self] isDefault in _ = defaultBrowserPreferencesModel self?.defaultBrowserModel.isDefault = isDefault @@ -196,7 +205,7 @@ final class HomePageViewController: NSViewController { } func refreshDefaultBrowserModel() { - let prefs = DefaultBrowserPreferences() + let prefs = DefaultBrowserPreferences.shared if prefs.isDefault { defaultBrowserDismissed = false } diff --git a/DuckDuckGo/InfoPlist.xcstrings b/DuckDuckGo/InfoPlist.xcstrings index cce98e8330..e4f7972ce7 100644 --- a/DuckDuckGo/InfoPlist.xcstrings +++ b/DuckDuckGo/InfoPlist.xcstrings @@ -7,7 +7,7 @@ "localizations" : { "de" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "DuckDuckGo" } }, @@ -19,43 +19,43 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "DuckDuckGo" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "DuckDuckGo" } }, "it" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "DuckDuckGo" } }, "nl" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "DuckDuckGo" } }, "pl" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "DuckDuckGo" } }, "pt" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "DuckDuckGo" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "DuckDuckGo" } } diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 94ea695d1b..ce218cd1b2 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -53,8 +53,57 @@ } } }, - "-1 -> 0 ... 1" : { - + " %@" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : " %@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : " %@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : " %@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : " %@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : " %@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : " %@" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : " %@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : " %@" + } + } + } }, "**%lld** tracking attempts blocked" : { "comment" : "The number of tracking attempts blocked in the last 7 days, shown on a new tab, translate as: Tracking attempts blocked: %@", @@ -430,24 +479,6 @@ } } } - }, - "0->1->nil" : { - - }, - "10%" : { - - }, - "20%" : { - - }, - "50%" : { - - }, - "80%" : { - - }, - "100%" : { - }, "about.app_name" : { "comment" : "Application name to be displayed in the About dialog", @@ -1966,9 +1997,6 @@ } } } - }, - "Animate" : { - }, "auth.alert.login.button" : { "comment" : "Authentication Alert Sign In Button", @@ -2872,7 +2900,7 @@ }, "autoconsent.title" : { "comment" : "Autoconsent settings section title", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -15926,6 +15954,66 @@ } } }, + "email.protection.explanation" : { + "comment" : "Email protection feature explanation in settings. The feature blocks email trackers and hides original email address.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "E-Mail-Tracker blockieren und deine Adresse verbergen, ohne den E-Mail-Anbieter zu wechseln." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Block email trackers and hide your address without switching your email provider." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bloquea los rastreadores de correo electrónico y oculta tu dirección sin cambiar de proveedor de correo electrónico." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bloquez les traqueurs d'e-mails et masquez votre adresse sans changer de fournisseur de messagerie." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Blocca i sistemi di tracciamento delle e-mail e nascondi il tuo indirizzo, senza cambiare il provider di posta elettronica." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Blokkeer e-mailtrackers en verberg je adres zonder van e-mailprovider te wisselen." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zablokuj mechanizmy śledzące pocztę e-mail i ukryj swój adres bez zmiany dostawcy poczty e-mail." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bloqueia os rastreadores de e-mail e oculta o teu endereço sem alterares o teu fornecedor de e-mail." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Чтобы блокировать трекеры и скрывать свой адрес, вовсе не нужно менять почтовый сервис." + } + } + } + }, "Enter Full Screen" : { "comment" : "Main Menu View item", "localizations" : { @@ -19567,7 +19655,7 @@ }, "gpc.title" : { "comment" : "GPC settings title", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -24174,9 +24262,6 @@ } } } - }, - "Indeterminate (-1)" : { - }, "invite.dialog.get.started.button" : { "comment" : "Get Started button on an invite dialog", @@ -29790,9 +29875,6 @@ } } } - }, - "nil->0->nil" : { - }, "no.access.to.downloads.folder.header" : { "comment" : "Header of the alert dialog warning the user they need to give the browser permission to access the Downloads folder", @@ -31678,7 +31760,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Apri collegamento in una nuova scheda" + "value" : "Apri link in una nuova scheda" } }, "nl" : { @@ -41693,7 +41775,7 @@ } }, "preferences.about" : { - "comment" : "Show about screen", + "comment" : "Title of the option to show the About screen", "extractionState" : "extracted_with_value", "localizations" : { "de" : { @@ -42172,8 +42254,128 @@ } } }, + "preferences.accessibility" : { + "comment" : "Title of the option to show the Accessibility browser preferences", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Barrierefreiheit" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Accessibility" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accesibilidad" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accessibilité" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accessibilità" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toegankelijkheid" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dostępność" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Acessibilidade" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Универсальный доступ" + } + } + } + }, + "preferences.always-on" : { + "comment" : "Status indicator of a browser privacy protection feature.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Immer aktiviert" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Always On" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Siempre activado" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toujours activé" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sempre attiva" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Altijd aan" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zawsze włączone" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sempre ligada" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Всегда включено" + } + } + } + }, "preferences.appearance" : { - "comment" : "Show appearance preferences", + "comment" : "Title of the option to show the Appearance preferences", "extractionState" : "extracted_with_value", "localizations" : { "de" : { @@ -42654,7 +42856,7 @@ }, "preferences.appearance.zoom" : { "comment" : "Zoom settings section title", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -42779,61 +42981,241 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Autovervollständigen" + "value" : "Passwörter" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "Autofill" + "value" : "Passwords" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Autocompletar" + "value" : "Contraseñas" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Saisie automatique" + "value" : "Mots de passe" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Compilazione automatica" + "value" : "Password" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Automatisch invullen" + "value" : "Wachtwoorden" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Autouzupełnianie" + "value" : "Hasła" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Preenchimento automático" + "value" : "Palavras-passe" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Автозаполнение" + "value" : "Пароли" + } + } + } + }, + "preferences.autofill-enabled-for" : { + "comment" : "Label presented before the email account in email protection preferences", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Autovervollständigen in diesem Browser aktiviert für:" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Autofill enabled in this browser for:" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Autocompletar habilitado en este navegador para:" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "La saisie automatique est activée dans ce navigateur pour :" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Compilazione automatica abilitata in questo browser per:" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "'Automatisch aanvullen' is ingeschakeld in deze browser voor:" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Autouzupełnianie włączone w tej przeglądarce dla:" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Preenchimento automático ativado neste navegador para:" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "В этом браузере включено автозаполнение для:" + } + } + } + }, + "preferences.cookie-pop-up-protection" : { + "comment" : "Title of the option to show the Cookie Pop-Up Protection preferences", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cookie-Pop-up-Schutz" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Cookie Pop-Up Protection" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Protección contra ventanas emergentes de cookies" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Protection contre les fenêtres contextuelles des cookies" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Protezione pop-up dei cookie" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bescherming tegen cookiepop-ups" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ochrona przed wyskakującymi okienkami dotyczącymi plików cookie" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Proteção contra pop-ups de cookies" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Защита от всплывающих окон куки" + } + } + } + }, + "preferences.data-clearing" : { + "comment" : "Title of the option to show the Data Clearing preferences", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Datenlöschung" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Data Clearing" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eliminación de datos" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Effacement des données" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancellazione dati" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gegevens wissen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Czyszczenie danych" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limpeza de Dados" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Очистка данных" } } } }, "preferences.default-browser" : { - "comment" : "Show default browser preferences", + "comment" : "Title of the option to show the Default Browser Preferences", "extractionState" : "extracted_with_value", "localizations" : { "de" : { @@ -43073,7 +43455,7 @@ } }, "preferences.downloads" : { - "comment" : "Show downloads browser preferences", + "comment" : "Title of the downloads browser preferences", "extractionState" : "extracted_with_value", "localizations" : { "de" : { @@ -43133,7 +43515,7 @@ } }, "preferences.duck-player" : { - "comment" : "Show Duck Player browser preferences", + "comment" : "Title of the option to show the Duck Player browser preferences", "extractionState" : "extracted_with_value", "localizations" : { "de" : { @@ -43192,8 +43574,128 @@ } } }, + "preferences.duckduckgo-on-other-platforms" : { + "comment" : "Button presented to users to navigate them to our product page which presents all other products for other platforms", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo auf anderen Plattformen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "DuckDuckGo on Other Platforms" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo en otras plataformas" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo sur d'autres plateformes" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo su altre piattaforme" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo op andere platforms" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo na innych platformach" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo Noutras Plataformas" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo для других платформ" + } + } + } + }, + "preferences.email-protection" : { + "comment" : "Title of the option to show the Email Protection preferences", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "E-Mail-Schutz" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Email Protection" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Protección del correo electrónico" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Protection des e-mails" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Protezione email" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "E-mailbescherming" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ochrona poczty e-mail" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Proteção de e-mail" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Защита электронной почты" + } + } + } + }, "preferences.general" : { - "comment" : "Show general preferences", + "comment" : "Title of the option to show the General preferences", "extractionState" : "extracted_with_value", "localizations" : { "de" : { @@ -43252,6 +43754,186 @@ } } }, + "preferences.main-settings" : { + "comment" : "Section header in Preferences for main settings", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Haupteinstellungen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Main Settings" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ajustes principales" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Réglages principaux" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Impostazioni principali" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hoofdinstellingen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ustawienia główne" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Definições Principais" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Основные настройки" + } + } + } + }, + "preferences.off" : { + "comment" : "Status indicator of a browser privacy protection feature.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aus" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Off" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desactivado" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Désactivé" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Disattivato" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uit" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wył." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desligado" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Выкл." + } + } + } + }, + "preferences.on" : { + "comment" : "Status indicator of a browser privacy protection feature.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "An" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "On" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Activado" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Activé" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "On" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aan" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wł." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ligado" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вкл." + } + } + } + }, "preferences.on-startup" : { "comment" : "Name of the preferences section related to app startup", "extractionState" : "extracted_with_value", @@ -43314,7 +43996,7 @@ }, "preferences.privacy" : { "comment" : "Show privacy browser preferences", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -43372,6 +44054,126 @@ } } }, + "preferences.privacy-protections" : { + "comment" : "The section header in Preferences representing browser features related to privacy protection", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Datenschutz" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Privacy Protections" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Protecciones de privacidad" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Protections de la confidentialité" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Protezioni della Privacy" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Privacybescherming" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mechanizmy Ochrony Prywatności" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Proteções de Privacidade" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Защита конфиденциальности" + } + } + } + }, + "preferences.private-search" : { + "comment" : "Title of the option to show the Private Search preferences", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Private Suche" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Private Search" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Búsqueda privada" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recherche privée" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ricerca Privata" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Privézoekopdracht" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Prywatne wyszukiwanie" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pesquisa Privada" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Частный поиск" + } + } + } + }, "preferences.reopen-windows" : { "comment" : "Option to control session restoration", "extractionState" : "extracted_with_value", @@ -43492,8 +44294,68 @@ } } }, + "preferences.support" : { + "comment" : "Open support page", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unterstützung" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Support" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ayuda" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aide" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Supporto" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Support" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wsparcie" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Assistência" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Поддержка" + } + } + } + }, "preferences.sync" : { - "comment" : "Show sync preferences", + "comment" : "Title of the option to show the Sync preferences", "extractionState" : "extracted_with_value", "localizations" : { "de" : { @@ -43613,7 +44475,7 @@ } }, "preferences.vpn" : { - "comment" : "Show VPN preferences", + "comment" : "Title of the option to show the VPN preferences", "extractionState" : "extracted_with_value", "localizations" : { "de" : { @@ -43672,6 +44534,66 @@ } } }, + "preferences.web-tracking-protection" : { + "comment" : "Title of the option to show the Web Tracking Protection preferences", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Web Tracking Protection" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Web Tracking Protection" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Protección de rastreo en la web" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Protection contre le pistage sur le Web" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Protezione dal tracciamento web" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bescherming tegen webtracking" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ochrona przed śledzeniem w sieci" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Proteção contra rastreamento na internet" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Защита от отслеживания онлайн" + } + } + } + }, "print.menu.item" : { "comment" : "Menu item title", "extractionState" : "extracted_with_value", @@ -43732,6 +44654,66 @@ } } }, + "private.search.explenation" : { + "comment" : "feature explanation in settings", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo Private Search ist deine Standardsuchmaschine – du kannst also das Web durchsuchen, ohne getrackt zu werden." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "DuckDuckGo Private Search is your default search engine, so you can search the web without being tracked." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La búsqueda privada de DuckDuckGo es tu motor de búsqueda predeterminado para que puedas buscar en la web sin que te rastreen." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo Private Search est votre moteur de recherche par défaut, qui vous permet d'effectuer des recherches sur le Web sans être suivi(e)." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo Private Search è il tuo motore di ricerca predefinito, quindi puoi effettuare ricerche sul Web senza essere tracciato." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo Private Search is je standaardzoekmachine, zodat je op het web kunt zoeken zonder gevolgd te worden." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo Private Search to domyślna wyszukiwarka, dzięki której możesz wyszukiwać treści w sieci, unikając śledzenia." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "O DuckDuckGo Private Search é o teu motor de pesquisa predefinido, para que possas pesquisar a web sem seres rastreado." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo Private Search — ваша поисковая система по умолчанию. Вы можете пользоваться поиском в интернете, не опасаясь слежки." + } + } + } + }, "quit" : { "comment" : "Quit button", "extractionState" : "extracted_with_value", @@ -44363,9 +45345,6 @@ } } } - }, - "Reset (nil)" : { - }, "restart.bitwarden" : { "comment" : "Button to restart Bitwarden application", @@ -46473,9 +47452,6 @@ } } } - }, - "Slow animations" : { - }, "Smart Copy/Paste" : { "comment" : "Main Menu Edit-Substitutions item", @@ -49937,7 +50913,7 @@ } } }, - "web.tracking.protection.explenation" : { + "web.tracking.protection.explanation" : { "comment" : "feature explanation in settings", "extractionState" : "extracted_with_value", "localizations" : { @@ -49997,6 +50973,66 @@ } } }, + "web.tracking.protection.explenation" : { + "comment" : "Privacy feature explanation in the browser settings", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo blockiert beim Durchsuchen des Internets automatisch ausgeblendete Tracker." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "DuckDuckGo automatically blocks hidden trackers as you browse the web." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo bloquea automáticamente los rastreadores ocultos mientras navegas por la web." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo bloque automatiquement les traqueurs cachés lorsque vous naviguez sur le Web." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo blocca automaticamente i sistemi di tracciamento nascosti mentre navighi sul web." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo blokkeert automatisch verborgen trackers terwijl je op het web surft." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Podczas przeglądania stron internetowych DuckDuckGo automatycznie blokuje ukryte mechanizmy śledzące." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "O DuckDuckGo bloqueia automaticamente os rastreadores ocultos enquanto navegas na Internet." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo автоматически блокирует скрытые трекеры, пока вы посещаете сайты." + } + } + } + }, "web.tracking.protection.title" : { "comment" : "Web tracking protection settings section title", "extractionState" : "extracted_with_value", @@ -50162,9 +51198,6 @@ } } } - }, - "Zero" : { - }, "zoom" : { "comment" : "Menu with Zooming commands", diff --git a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift index f75a983cca..75bcc66066 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift @@ -57,6 +57,8 @@ final class AddressBarTextField: NSTextField { private var addressBarStringCancellable: AnyCancellable? private var contentTypeCancellable: AnyCancellable? + private let searchPreferences: SearchPreferences = SearchPreferences.shared + private enum TextDidChangeEventType { case none case userAppendingTextToTheEnd @@ -624,9 +626,9 @@ final class AddressBarTextField: NSTextField { } @objc func toggleAutocomplete(_ menuItem: NSMenuItem) { - AppearancePreferences.shared.showAutocompleteSuggestions.toggle() + searchPreferences.showAutocompleteSuggestions.toggle() - let shouldShowAutocomplete = AppearancePreferences.shared.showAutocompleteSuggestions + let shouldShowAutocomplete = searchPreferences.showAutocompleteSuggestions menuItem.state = shouldShowAutocomplete ? .on : .off @@ -1084,7 +1086,7 @@ private extension NSMenuItem { action: #selector(AddressBarTextField.toggleAutocomplete(_:)), keyEquivalent: "" ) - menuItem.state = AppearancePreferences.shared.showAutocompleteSuggestions ? .on : .off + menuItem.state = SearchPreferences.shared.showAutocompleteSuggestions ? .on : .off return menuItem } diff --git a/DuckDuckGo/Preferences/Model/AboutModel.swift b/DuckDuckGo/Preferences/Model/AboutModel.swift index 7c44d07efd..52508bd9cb 100644 --- a/DuckDuckGo/Preferences/Model/AboutModel.swift +++ b/DuckDuckGo/Preferences/Model/AboutModel.swift @@ -19,7 +19,7 @@ import SwiftUI import Common -final class AboutModel: ObservableObject { +final class AboutModel: ObservableObject, PreferencesTabOpening { let appVersion = AppVersion() #if NETWORK_PROTECTION @@ -37,11 +37,6 @@ final class AboutModel: ObservableObject { let displayableAboutURL: String = URL.aboutDuckDuckGo .toString(decodePunycode: false, dropScheme: true, dropTrailingSlash: false) - @MainActor - func openURL(_ url: URL) { - WindowControllersManager.shared.show(url: url, source: .ui, newTab: true) - } - @MainActor func openFeedbackForm() { FeedbackPresenter.presentFeedbackForm() diff --git a/DuckDuckGo/Preferences/Model/AccessibilityPreferences.swift b/DuckDuckGo/Preferences/Model/AccessibilityPreferences.swift new file mode 100644 index 0000000000..c096191757 --- /dev/null +++ b/DuckDuckGo/Preferences/Model/AccessibilityPreferences.swift @@ -0,0 +1,69 @@ +// +// AccessibilityPreferences.swift +// +// Copyright © 2022 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 AppKit +import Bookmarks +import Common + +protocol AccessibilityPreferencesPersistor { + var defaultPageZoom: CGFloat { get set } +} + +struct AccessibilityPreferencesUserDefaultsPersistor: AccessibilityPreferencesPersistor { + @UserDefaultsWrapper(key: .defaultPageZoom, defaultValue: DefaultZoomValue.percent100.rawValue) + var defaultPageZoom: CGFloat +} + +enum DefaultZoomValue: CGFloat, CaseIterable { + case percent50 = 0.5 + case percent75 = 0.75 + case percent85 = 0.85 + case percent100 = 1.0 + case percent115 = 1.15 + case percent125 = 1.25 + case percent150 = 1.50 + case percent175 = 1.75 + case percent200 = 2.0 + case percent250 = 2.5 + case percent300 = 3.0 + + var displayString: String { + let percentage = (self.rawValue * 100).rounded() + return String(format: "%.0f%%", percentage) + } + + var index: Int {DefaultZoomValue.allCases.firstIndex(of: self) ?? 3} +} + +final class AccessibilityPreferences: ObservableObject { + static let shared = AccessibilityPreferences() + + @Published var defaultPageZoom: DefaultZoomValue { + didSet { + persistor.defaultPageZoom = defaultPageZoom.rawValue + } + } + + init(persistor: AccessibilityPreferencesPersistor = AccessibilityPreferencesUserDefaultsPersistor()) { + self.persistor = persistor + defaultPageZoom = .init(rawValue: persistor.defaultPageZoom) ?? .percent100 + } + + private var persistor: AccessibilityPreferencesPersistor +} diff --git a/DuckDuckGo/Preferences/Model/AppearancePreferences.swift b/DuckDuckGo/Preferences/Model/AppearancePreferences.swift index ab73548d08..833bab73f0 100644 --- a/DuckDuckGo/Preferences/Model/AppearancePreferences.swift +++ b/DuckDuckGo/Preferences/Model/AppearancePreferences.swift @@ -23,9 +23,7 @@ import Common protocol AppearancePreferencesPersistor { var showFullURL: Bool { get set } - var showAutocompleteSuggestions: Bool { get set } var currentThemeName: String { get set } - var defaultPageZoom: CGFloat { get set } var favoritesDisplayMode: String? { get set } var isFavoriteVisible: Bool { get set } var isContinueSetUpVisible: Bool { get set } @@ -39,15 +37,9 @@ struct AppearancePreferencesUserDefaultsPersistor: AppearancePreferencesPersisto @UserDefaultsWrapper(key: .showFullURL, defaultValue: false) var showFullURL: Bool - @UserDefaultsWrapper(key: .showAutocompleteSuggestions, defaultValue: true) - var showAutocompleteSuggestions: Bool - @UserDefaultsWrapper(key: .currentThemeName, defaultValue: ThemeName.systemDefault.rawValue) var currentThemeName: String - @UserDefaultsWrapper(key: .defaultPageZoom, defaultValue: DefaultZoomValue.percent100.rawValue) - var defaultPageZoom: CGFloat - @UserDefaultsWrapper(key: .favoritesDisplayMode, defaultValue: FavoritesDisplayMode.displayNative(.desktop).description) var favoritesDisplayMode: String? @@ -85,27 +77,6 @@ enum HomeButtonPosition: String, CaseIterable { case right } -enum DefaultZoomValue: CGFloat, CaseIterable { - case percent50 = 0.5 - case percent75 = 0.75 - case percent85 = 0.85 - case percent100 = 1.0 - case percent115 = 1.15 - case percent125 = 1.25 - case percent150 = 1.50 - case percent175 = 1.75 - case percent200 = 2.0 - case percent250 = 2.5 - case percent300 = 3.0 - - var displayString: String { - let percentage = (self.rawValue * 100).rounded() - return String(format: "%.0f%%", percentage) - } - - var index: Int {DefaultZoomValue.allCases.firstIndex(of: self) ?? 3} -} - enum ThemeName: String, Equatable, CaseIterable { case light case dark @@ -181,24 +152,12 @@ final class AppearancePreferences: ObservableObject { } } - @Published var showAutocompleteSuggestions: Bool { - didSet { - persistor.showAutocompleteSuggestions = showAutocompleteSuggestions - } - } - @Published var favoritesDisplayMode: FavoritesDisplayMode { didSet { persistor.favoritesDisplayMode = favoritesDisplayMode.description } } - @Published var defaultPageZoom: DefaultZoomValue { - didSet { - persistor.defaultPageZoom = defaultPageZoom.rawValue - } - } - @Published var isFavoriteVisible: Bool { didSet { persistor.isFavoriteVisible = isFavoriteVisible @@ -262,12 +221,10 @@ final class AppearancePreferences: ObservableObject { self.persistor = persistor currentThemeName = .init(rawValue: persistor.currentThemeName) ?? .systemDefault showFullURL = persistor.showFullURL - showAutocompleteSuggestions = persistor.showAutocompleteSuggestions favoritesDisplayMode = persistor.favoritesDisplayMode.flatMap(FavoritesDisplayMode.init) ?? .default isFavoriteVisible = persistor.isFavoriteVisible isRecentActivityVisible = persistor.isRecentActivityVisible isContinueSetUpVisible = persistor.isContinueSetUpVisible - defaultPageZoom = .init(rawValue: persistor.defaultPageZoom) ?? .percent100 showBookmarksBar = persistor.showBookmarksBar bookmarksBarAppearance = persistor.bookmarksBarAppearance homeButtonPosition = persistor.homeButtonPosition diff --git a/DuckDuckGo/Preferences/Model/CookiePopupProtectionPreferences.swift b/DuckDuckGo/Preferences/Model/CookiePopupProtectionPreferences.swift new file mode 100644 index 0000000000..3082f23a0a --- /dev/null +++ b/DuckDuckGo/Preferences/Model/CookiePopupProtectionPreferences.swift @@ -0,0 +1,52 @@ +// +// CookiePopupProtectionPreferences.swift +// +// Copyright © 2022 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 AppKit +import Bookmarks +import Common + +protocol CookiePopupProtectionPreferencesPersistor { + var autoconsentEnabled: Bool { get set } +} + +struct CookiePopupProtectionPreferencesUserDefaultsPersistor: CookiePopupProtectionPreferencesPersistor { + + @UserDefaultsWrapper(key: .autoconsentEnabled, defaultValue: true) + var autoconsentEnabled: Bool + +} + +final class CookiePopupProtectionPreferences: ObservableObject, PreferencesTabOpening { + + static let shared = CookiePopupProtectionPreferences() + + @Published + var isAutoconsentEnabled: Bool { + didSet { + persistor.autoconsentEnabled = isAutoconsentEnabled + } + } + + init(persistor: CookiePopupProtectionPreferencesPersistor = CookiePopupProtectionPreferencesUserDefaultsPersistor()) { + self.persistor = persistor + isAutoconsentEnabled = persistor.autoconsentEnabled + } + + private var persistor: CookiePopupProtectionPreferencesPersistor +} diff --git a/DuckDuckGo/Preferences/Model/PrivacyPreferencesModel.swift b/DuckDuckGo/Preferences/Model/DataClearingPreferences.swift similarity index 51% rename from DuckDuckGo/Preferences/Model/PrivacyPreferencesModel.swift rename to DuckDuckGo/Preferences/Model/DataClearingPreferences.swift index 212336b3a8..e336ab7cbd 100644 --- a/DuckDuckGo/Preferences/Model/PrivacyPreferencesModel.swift +++ b/DuckDuckGo/Preferences/Model/DataClearingPreferences.swift @@ -1,5 +1,5 @@ // -// PrivacyPreferencesModel.swift +// DataClearingPreferences.swift // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -18,25 +18,14 @@ import Foundation -final class PrivacyPreferencesModel: ObservableObject { +final class DataClearingPreferences: ObservableObject, PreferencesTabOpening { - @Published - var isLoginDetectionEnabled: Bool { - didSet { - privacySecurityPreferences.loginDetectionEnabled = isLoginDetectionEnabled - } - } + static let shared = DataClearingPreferences() @Published - var isGPCEnabled: Bool { - didSet { - privacySecurityPreferences.gpcEnabled = isGPCEnabled - } - } - - @Published var isAutoconsentEnabled: Bool { + var isLoginDetectionEnabled: Bool { didSet { - privacySecurityPreferences.autoconsentEnabled = isAutoconsentEnabled + persistor.loginDetectionEnabled = isLoginDetectionEnabled } } @@ -47,24 +36,28 @@ final class PrivacyPreferencesModel: ObservableObject { guard let fireproofDomainsWindow = fireproofDomainsWindowController.window, let parentWindowController = WindowControllersManager.shared.lastKeyMainWindowController else { - assertionFailure("Privacy Preferences: Failed to present FireproofDomainsViewController") + assertionFailure("DataClearingPreferences: Failed to present FireproofDomainsViewController") return } parentWindowController.window?.beginSheet(fireproofDomainsWindow) } - @MainActor - func openURL(_ url: URL) { - WindowControllersManager.shared.show(url: url, source: .ui, newTab: true) + init(persistor: FireButtonPreferencesPersistor = FireButtonPreferencesUserDefaultsPersistor()) { + self.persistor = persistor + isLoginDetectionEnabled = persistor.loginDetectionEnabled } - init(privacySecurityPreferences: PrivacySecurityPreferences = .shared) { - self.privacySecurityPreferences = privacySecurityPreferences - isLoginDetectionEnabled = privacySecurityPreferences.loginDetectionEnabled - isGPCEnabled = privacySecurityPreferences.gpcEnabled - isAutoconsentEnabled = privacySecurityPreferences.autoconsentEnabled - } + private var persistor: FireButtonPreferencesPersistor +} + +protocol FireButtonPreferencesPersistor { + var loginDetectionEnabled: Bool { get set } +} + +struct FireButtonPreferencesUserDefaultsPersistor: FireButtonPreferencesPersistor { + + @UserDefaultsWrapper(key: .loginDetectionEnabled, defaultValue: false) + var loginDetectionEnabled: Bool - private let privacySecurityPreferences: PrivacySecurityPreferences } diff --git a/DuckDuckGo/Preferences/Model/DefaultBrowserPreferences.swift b/DuckDuckGo/Preferences/Model/DefaultBrowserPreferences.swift index ecaf76e24e..a40e390510 100644 --- a/DuckDuckGo/Preferences/Model/DefaultBrowserPreferences.swift +++ b/DuckDuckGo/Preferences/Model/DefaultBrowserPreferences.swift @@ -71,6 +71,8 @@ struct SystemDefaultBrowserProvider: DefaultBrowserProvider { final class DefaultBrowserPreferences: ObservableObject { + static let shared = DefaultBrowserPreferences() + @Published private(set) var isDefault: Bool = false { didSet { // Temporary pixel for first time user import data @@ -113,11 +115,37 @@ final class DefaultBrowserPreferences: ObservableObject { do { try defaultBrowserProvider.presentDefaultBrowserPrompt() + repeatCheckIfDefault() } catch { defaultBrowserProvider.openSystemPreferences() } } + var executionCount = 0 + let maxNumberOfExecutions = 60 + var timer: Timer? + + // Monitors for changes in default browser setting over the next minute. + // The reason is there is no API to get a notification for this change. + private func repeatCheckIfDefault() { + timer?.invalidate() + executionCount = 0 + timer = Timer.scheduledTimer(timeInterval: 1.0, + target: self, + selector: #selector(timerFired), + userInfo: nil, + repeats: true) + } + + @objc private func timerFired() { + checkIfDefault() + + executionCount += 1 + if executionCount >= maxNumberOfExecutions { + timer?.invalidate() + } + } + private var appDidBecomeActiveCancellable: AnyCancellable? private let defaultBrowserProvider: DefaultBrowserProvider } diff --git a/DuckDuckGo/Preferences/Model/PreferencesSection.swift b/DuckDuckGo/Preferences/Model/PreferencesSection.swift index a928eb6202..785350403e 100644 --- a/DuckDuckGo/Preferences/Model/PreferencesSection.swift +++ b/DuckDuckGo/Preferences/Model/PreferencesSection.swift @@ -29,52 +29,79 @@ struct PreferencesSection: Hashable, Identifiable { @MainActor static func defaultSections(includingDuckPlayer: Bool, includingSync: Bool, includingVPN: Bool) -> [PreferencesSection] { - let regularPanes: [PreferencePaneIdentifier] = { - - var panes: [PreferencePaneIdentifier] = [.general, .appearance, .privacy, .autofill, .downloads] + var privacyPanes: [PreferencePaneIdentifier] = [.defaultBrowser, .privateSearch, .webTrackingProtection, .cookiePopupProtection, .emailProtection] - if DefaultSubscriptionFeatureAvailability().isFeatureAvailable() { -#if SUBSCRIPTION - panes = [.privacy, .subscription, .general, .appearance, .autofill, .downloads] +#if NETWORK_PROTECTION + if includingVPN { + privacyPanes.append(.vpn) + } #endif - } + + let regularPanes: [PreferencePaneIdentifier] = { + var panes: [PreferencePaneIdentifier] = [.general, .appearance, .autofill, .accessibility, .dataClearing] if includingSync { - if let generalIndex = panes.firstIndex(of: .general) { - panes.insert(.sync, at: generalIndex + 1) - } + panes.insert(.sync, at: 1) } if includingDuckPlayer { panes.append(.duckPlayer) } -#if NETWORK_PROTECTION - if includingVPN { - panes.append(.vpn) - } -#endif - return panes }() - return [ + let otherPanes: [PreferencePaneIdentifier] = [.about, .otherPlatforms] + + var sections: [PreferencesSection] = [ + .init(id: .privacyProtections, panes: privacyPanes), .init(id: .regularPreferencePanes, panes: regularPanes), - .init(id: .about, panes: [.about]) + .init(id: .about, panes: otherPanes) ] + +#if SUBSCRIPTION + if DefaultSubscriptionFeatureAvailability().isFeatureAvailable() { + let subscriptionPanes: [PreferencePaneIdentifier] = [.subscription] + sections.insert(.init(id: .privacyPro, panes: subscriptionPanes), at: 1) + } +#endif + + return sections } } enum PreferencesSectionIdentifier: Hashable, CaseIterable { + case privacyProtections + case privacyPro case regularPreferencePanes case about + + var displayName: String? { + switch self { + case .privacyProtections: + return UserText.privacyProtections + case .privacyPro: + return nil + case .regularPreferencePanes: + return UserText.mainSettings + case .about: + return nil + } + } + } enum PreferencePaneIdentifier: String, Equatable, Hashable, Identifiable { + case defaultBrowser + case privateSearch + case webTrackingProtection + case cookiePopupProtection + case emailProtection + case general case sync case appearance - case privacy + case dataClearing #if NETWORK_PROTECTION case vpn #endif @@ -82,8 +109,9 @@ enum PreferencePaneIdentifier: String, Equatable, Hashable, Identifiable { case subscription #endif case autofill - case downloads + case accessibility case duckPlayer = "duckplayer" + case otherPlatforms = "https://duckduckgo.com/app" case about var id: Self { @@ -106,6 +134,16 @@ enum PreferencePaneIdentifier: String, Equatable, Hashable, Identifiable { @MainActor var displayName: String { switch self { + case .defaultBrowser: + return UserText.defaultBrowser + case .privateSearch: + return UserText.privateSearch + case .webTrackingProtection: + return UserText.webTrackingProtection + case .cookiePopupProtection: + return UserText.cookiePopUpProtection + case .emailProtection: + return UserText.emailProtectionPreferences case .general: return UserText.general case .sync: @@ -119,8 +157,8 @@ enum PreferencePaneIdentifier: String, Equatable, Hashable, Identifiable { return UserText.sync case .appearance: return UserText.appearance - case .privacy: - return UserText.privacy + case .dataClearing: + return UserText.dataClearing #if NETWORK_PROTECTION case .vpn: return UserText.vpn @@ -131,25 +169,37 @@ enum PreferencePaneIdentifier: String, Equatable, Hashable, Identifiable { #endif case .autofill: return UserText.autofill - case .downloads: - return UserText.downloads + case .accessibility: + return UserText.accessibility case .duckPlayer: return UserText.duckPlayer case .about: return UserText.about + case .otherPlatforms: + return UserText.duckduckgoOnOtherPlatforms } } var preferenceIconName: String { switch self { + case .defaultBrowser: + return "DefaultBrowser" + case .privateSearch: + return "PrivateSearchIcon" + case .webTrackingProtection: + return "WebTrackingProtectionIcon" + case .cookiePopupProtection: + return "CookieProtectionIcon" + case .emailProtection: + return "EmailProtectionIcon" case .general: - return "Rocket" + return "GeneralIcon" case .sync: return "Sync" case .appearance: return "Appearance" - case .privacy: - return "Privacy" + case .dataClearing: + return "FireSettings" #if NETWORK_PROTECTION case .vpn: return "VPN" @@ -160,12 +210,14 @@ enum PreferencePaneIdentifier: String, Equatable, Hashable, Identifiable { #endif case .autofill: return "Autofill" - case .downloads: - return "DownloadsPreferences" + case .accessibility: + return "Accessibility" case .duckPlayer: return "DuckPlayerSettings" case .about: return "About" + case .otherPlatforms: + return "OtherPlatformsPreferences" } } } diff --git a/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift b/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift index de53764fec..97c5a06a42 100644 --- a/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift +++ b/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift @@ -27,7 +27,7 @@ final class PreferencesSidebarModel: ObservableObject { @Published private(set) var sections: [PreferencesSection] = [] @Published var selectedTabIndex: Int = 0 - @Published private(set) var selectedPane: PreferencePaneIdentifier = .general + @Published private(set) var selectedPane: PreferencePaneIdentifier = .defaultBrowser var selectedTabContent: AnyPublisher { $selectedTabIndex.map { [tabSwitcherTabs] in tabSwitcherTabs[$0] }.eraseToAnyPublisher() @@ -125,7 +125,16 @@ final class PreferencesSidebarModel: ObservableObject { } } + @MainActor func selectPane(_ identifier: PreferencePaneIdentifier) { + // Open a new tab in case of special panes + if identifier.rawValue.hasPrefix(URL.NavigationalScheme.https.rawValue), + let url = URL(string: identifier.rawValue) { + WindowControllersManager.shared.show(url: url, + source: .ui, + newTab: true) + } + if sections.flatMap(\.panes).contains(identifier), identifier != selectedPane { selectedPane = identifier } diff --git a/DuckDuckGo/Preferences/Model/PrivacyProtectionStatus.swift b/DuckDuckGo/Preferences/Model/PrivacyProtectionStatus.swift new file mode 100644 index 0000000000..67474fabe6 --- /dev/null +++ b/DuckDuckGo/Preferences/Model/PrivacyProtectionStatus.swift @@ -0,0 +1,77 @@ +// +// PrivacyProtectionStatus.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import Combine +import BrowserServicesKit + +final class PrivacyProtectionStatus: ObservableObject { + + static func status(for preferencePane: PreferencePaneIdentifier) -> PrivacyProtectionStatus { + switch preferencePane { + case .defaultBrowser: + return PrivacyProtectionStatus(statusPublisher: DefaultBrowserPreferences.shared.$isDefault) { isDefault in + isDefault ? .on : .off + } + case .privateSearch: + return PrivacyProtectionStatus(statusIndicator: .on) + case .webTrackingProtection: + return PrivacyProtectionStatus(statusIndicator: .on) + case .cookiePopupProtection: + return PrivacyProtectionStatus(statusPublisher: CookiePopupProtectionPreferences.shared.$isAutoconsentEnabled) { isAutoconsentEnabled in + isAutoconsentEnabled ? .on : .off + } + case .emailProtection: + let publisher = Publishers.Merge( + NotificationCenter.default.publisher(for: .emailDidSignIn), + NotificationCenter.default.publisher(for: .emailDidSignOut) + ) + return PrivacyProtectionStatus(statusPublisher: publisher, initialValue: EmailManager().isSignedIn ? .on : .off) { _ in + EmailManager().isSignedIn ? .on : .off + } + default: + return PrivacyProtectionStatus() + } + } + + var statusSubscription: AnyCancellable? + @Published var status: Preferences.StatusIndicator? + + // Initializer for observable properties + init(statusPublisher: T, + initialValue: Preferences.StatusIndicator? = nil, + transform: @escaping (T.Output) -> Preferences.StatusIndicator?) where T.Failure == Never { + self.status = initialValue + + statusSubscription = statusPublisher + .map(transform) + .sink { [weak self] newStatus in + self?.status = newStatus + } + } + + // Initializer for items without a status + init() { + self.status = nil + } + + // Initializer for items with static status + init(statusIndicator: Preferences.StatusIndicator) { + self.status = statusIndicator + } +} diff --git a/DuckDuckGo/Preferences/Model/PrivacySecurityPreferences.swift b/DuckDuckGo/Preferences/Model/PrivacySecurityPreferences.swift deleted file mode 100644 index 96b8b2b4a0..0000000000 --- a/DuckDuckGo/Preferences/Model/PrivacySecurityPreferences.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// PrivacySecurityPreferences.swift -// -// Copyright © 2021 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import Combine - -final class PrivacySecurityPreferences { - static let shared = PrivacySecurityPreferences() - - private init() {} - - @UserDefaultsWrapper(key: .loginDetectionEnabled, defaultValue: false) - var loginDetectionEnabled: Bool - - @Published - var gpcEnabled: Bool = UserDefaultsWrapper(key: .gpcEnabled, defaultValue: true).wrappedValue { - didSet { - let udWrapper = UserDefaultsWrapper(key: .gpcEnabled, defaultValue: true) - udWrapper.wrappedValue = gpcEnabled - } - } - - @UserDefaultsWrapper(key: .autoconsentEnabled, defaultValue: true) - var autoconsentEnabled: Bool - -} diff --git a/DuckDuckGo/Preferences/Model/SearchPreferences.swift b/DuckDuckGo/Preferences/Model/SearchPreferences.swift new file mode 100644 index 0000000000..bbe5cfb70c --- /dev/null +++ b/DuckDuckGo/Preferences/Model/SearchPreferences.swift @@ -0,0 +1,64 @@ +// +// SearchPreferences.swift +// +// Copyright © 2022 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 AppKit +import Bookmarks +import Common + +protocol SearchPreferencesPersistor { + var showAutocompleteSuggestions: Bool { get set } +} + +struct SearchPreferencesUserDefaultsPersistor: SearchPreferencesPersistor { + @UserDefaultsWrapper(key: .showAutocompleteSuggestions, defaultValue: true) + var showAutocompleteSuggestions: Bool +} + +final class SearchPreferences: ObservableObject, PreferencesTabOpening { + + static let shared = SearchPreferences() + + @Published var showAutocompleteSuggestions: Bool { + didSet { + persistor.showAutocompleteSuggestions = showAutocompleteSuggestions + } + } + + init(persistor: SearchPreferencesPersistor = SearchPreferencesUserDefaultsPersistor()) { + self.persistor = persistor + showAutocompleteSuggestions = persistor.showAutocompleteSuggestions + } + + private var persistor: SearchPreferencesPersistor +} + +protocol PreferencesTabOpening { + + func openNewTab(with url: URL) + +} + +extension PreferencesTabOpening { + + @MainActor + func openNewTab(with url: URL) { + WindowControllersManager.shared.show(url: url, source: .ui, newTab: true) + } + +} diff --git a/DuckDuckGo/Preferences/Model/WebTrackingProtectionPreferences.swift b/DuckDuckGo/Preferences/Model/WebTrackingProtectionPreferences.swift new file mode 100644 index 0000000000..5ef1539af1 --- /dev/null +++ b/DuckDuckGo/Preferences/Model/WebTrackingProtectionPreferences.swift @@ -0,0 +1,52 @@ +// +// WebTrackingProtectionPreferences.swift +// +// Copyright © 2022 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 AppKit +import Bookmarks +import Common + +protocol WebTrackingProtectionPreferencesPersistor { + var gpcEnabled: Bool { get set } +} + +struct WebTrackingProtectionPreferencesUserDefaultsPersistor: WebTrackingProtectionPreferencesPersistor { + + @UserDefaultsWrapper(key: .gpcEnabled, defaultValue: true) + var gpcEnabled: Bool + +} + +final class WebTrackingProtectionPreferences: ObservableObject, PreferencesTabOpening { + + static let shared = WebTrackingProtectionPreferences() + + @Published + var isGPCEnabled: Bool { + didSet { + persistor.gpcEnabled = isGPCEnabled + } + } + + init(persistor: WebTrackingProtectionPreferencesPersistor = WebTrackingProtectionPreferencesUserDefaultsPersistor()) { + self.persistor = persistor + isGPCEnabled = persistor.gpcEnabled + } + + private var persistor: WebTrackingProtectionPreferencesPersistor +} diff --git a/DuckDuckGo/Preferences/View/PreferencesAboutView.swift b/DuckDuckGo/Preferences/View/PreferencesAboutView.swift index d039da2091..d298ef89fd 100644 --- a/DuckDuckGo/Preferences/View/PreferencesAboutView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesAboutView.swift @@ -70,11 +70,11 @@ extension Preferences { .padding(.bottom, 8) TextButton(UserText.moreAt(url: model.displayableAboutURL)) { - model.openURL(.aboutDuckDuckGo) + model.openNewTab(with: .aboutDuckDuckGo) } TextButton(UserText.privacyPolicy) { - model.openURL(.privacyPolicy) + model.openNewTab(with: .privacyPolicy) } #if FEEDBACK diff --git a/DuckDuckGo/Preferences/View/PreferencesAccessibilityView.swift b/DuckDuckGo/Preferences/View/PreferencesAccessibilityView.swift new file mode 100644 index 0000000000..fa38e9e2dd --- /dev/null +++ b/DuckDuckGo/Preferences/View/PreferencesAccessibilityView.swift @@ -0,0 +1,54 @@ +// +// PreferencesAccessibilityView.swift +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import AppKit +import Combine +import PreferencesViews +import SwiftUI +import SwiftUIExtensions + +extension Preferences { + + struct AccessibilityView: View { + @ObservedObject var model: AccessibilityPreferences + + var body: some View { + PreferencePane(UserText.accessibility) { + + // SECTION 1: Zoom Setting + PreferencePaneSection { + + HStack { + Text(UserText.zoomPickerTitle) + NSPopUpButtonView(selection: $model.defaultPageZoom) { + let button = NSPopUpButton() + button.setContentHuggingPriority(.defaultHigh, for: .horizontal) + + for value in DefaultZoomValue.allCases { + let item = button.menu?.addItem(withTitle: value.displayString, action: nil, keyEquivalent: "") + item?.representedObject = value + } + return button + } + } + } + + } + } + } +} diff --git a/DuckDuckGo/Preferences/View/PreferencesAppearanceView.swift b/DuckDuckGo/Preferences/View/PreferencesAppearanceView.swift index 8e9e46fdfd..be30d68da2 100644 --- a/DuckDuckGo/Preferences/View/PreferencesAppearanceView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesAppearanceView.swift @@ -98,7 +98,6 @@ extension Preferences { // SECTION 2: Address Bar PreferencePaneSection(UserText.addressBar) { ToggleMenuItem(UserText.showFullWebsiteAddress, isOn: $model.showFullURL) - ToggleMenuItem(UserText.showAutocompleteSuggestions, isOn: $model.showAutocompleteSuggestions) } // SECTION 3: New Tab Page @@ -131,24 +130,6 @@ extension Preferences { .disabled(!model.showBookmarksBar) } } - - // SECTION 5: Zoom Setting - PreferencePaneSection(UserText.zoomSettingTitle) { - - HStack { - Text(UserText.zoomPickerTitle) - NSPopUpButtonView(selection: $model.defaultPageZoom) { - let button = NSPopUpButton() - button.setContentHuggingPriority(.defaultHigh, for: .horizontal) - - for value in DefaultZoomValue.allCases { - let item = button.menu?.addItem(withTitle: value.displayString, action: nil, keyEquivalent: "") - item?.representedObject = value - } - return button - } - } - } } } } diff --git a/DuckDuckGo/Preferences/View/PreferencesCookiePopupProtectionView.swift b/DuckDuckGo/Preferences/View/PreferencesCookiePopupProtectionView.swift new file mode 100644 index 0000000000..b1b22cd69c --- /dev/null +++ b/DuckDuckGo/Preferences/View/PreferencesCookiePopupProtectionView.swift @@ -0,0 +1,55 @@ +// +// PreferencesCookiePopupProtectionView.swift +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import AppKit +import Combine +import PreferencesViews +import SwiftUI +import SwiftUIExtensions + +extension Preferences { + + struct CookiePopupProtectionView: View { + @ObservedObject var model: CookiePopupProtectionPreferences + + var body: some View { + PreferencePane(UserText.cookiePopUpProtection, spacing: 4) { + + // SECTION 1: Status Indicator + PreferencePaneSection { + StatusIndicatorView(status: model.isAutoconsentEnabled ? .on : .off, isLarge: true) + } + + // SECTION 2: Description + PreferencePaneSection { + VStack(alignment: .leading, spacing: 1) { + TextMenuItemCaption(UserText.autoconsentExplanation) + TextButton(UserText.learnMore) { + model.openNewTab(with: .cookieConsentPopUpManagement) + } + } + } + + // SECTION 3: Search Settings + PreferencePaneSection { + ToggleMenuItem(UserText.autoconsentCheckboxTitle, isOn: $model.isAutoconsentEnabled) + } + } + } + } +} diff --git a/DuckDuckGo/Preferences/View/PreferencesDataClearingView.swift b/DuckDuckGo/Preferences/View/PreferencesDataClearingView.swift new file mode 100644 index 0000000000..dc3f28607c --- /dev/null +++ b/DuckDuckGo/Preferences/View/PreferencesDataClearingView.swift @@ -0,0 +1,54 @@ +// +// PreferencesDataClearingView.swift +// +// Copyright © 2022 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 PreferencesViews +import SwiftUI +import SwiftUIExtensions + +extension Preferences { + + struct DataClearingView: View { + @ObservedObject var model: DataClearingPreferences + + var body: some View { + PreferencePane(UserText.dataClearing) { + + // SECTION 1: Fireproof Site + PreferencePaneSection(UserText.fireproofSites) { + + PreferencePaneSubSection { + ToggleMenuItem(UserText.fireproofCheckboxTitle, isOn: $model.isLoginDetectionEnabled) + VStack(alignment: .leading, spacing: 1) { + TextMenuItemCaption(UserText.fireproofExplanation) + TextButton(UserText.learnMore) { + model.openNewTab(with: .theFireButton) + } + } + } + + PreferencePaneSubSection { + Button(UserText.manageFireproofSites) { + model.presentManageFireproofSitesDialog() + } + } + } + + } + } + } +} diff --git a/DuckDuckGo/Preferences/View/PreferencesDefaultBrowserView.swift b/DuckDuckGo/Preferences/View/PreferencesDefaultBrowserView.swift new file mode 100644 index 0000000000..cc7692b366 --- /dev/null +++ b/DuckDuckGo/Preferences/View/PreferencesDefaultBrowserView.swift @@ -0,0 +1,69 @@ +// +// PreferencesDefaultBrowserView.swift +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import AppKit +import Combine +import PreferencesViews +import SwiftUI +import SwiftUIExtensions + +extension Preferences { + + struct DefaultBrowserView: View { + @ObservedObject var defaultBrowserModel: DefaultBrowserPreferences + let status: PrivacyProtectionStatus + + var body: some View { + PreferencePane(UserText.defaultBrowser, spacing: 4) { + + // SECTION 1: Status Indicator + if let status = status.status { + PreferencePaneSection { + StatusIndicatorView(status: status, isLarge: true) + } + } + + // SECTION 2: Default Browser + PreferencePaneSection { + + PreferencePaneSubSection { + HStack { + if defaultBrowserModel.isDefault { + Text(UserText.isDefaultBrowser) + } else { + HStack { + Image(.warning).foregroundColor(Color(.linkBlue)) + Text(UserText.isNotDefaultBrowser) + } + .padding(.trailing, 8) + Button(action: { + Pixel.fire(.defaultRequestedFromSettings) + defaultBrowserModel.becomeDefault() + }) { + Text(UserText.makeDefaultBrowser) + .fixedSize(horizontal: true, vertical: false) + .multilineTextAlignment(.center) + } + } + } + } + } + } + } + } +} diff --git a/DuckDuckGo/Preferences/View/PreferencesDownloadsView.swift b/DuckDuckGo/Preferences/View/PreferencesDownloadsView.swift deleted file mode 100644 index 9ffd8ee3a4..0000000000 --- a/DuckDuckGo/Preferences/View/PreferencesDownloadsView.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// PreferencesDownloadsView.swift -// -// Copyright © 2022 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 PreferencesViews -import SwiftUI -import SwiftUIExtensions - -extension Preferences { - - struct DownloadsView: View { - @ObservedObject var model: DownloadsPreferences - - var body: some View { - PreferencePane(UserText.downloads) { - - PreferencePaneSubSection { - ToggleMenuItem(UserText.downloadsOpenPopupOnCompletion, - isOn: $model.shouldOpenPopupOnCompletion) - } - - // MARK: Location - PreferencePaneSection(UserText.downloadsLocation) { - - HStack { - NSPathControlView(url: model.selectedDownloadLocation) -#if !APPSTORE - Button(UserText.downloadsChangeDirectory) { - model.presentDownloadDirectoryPanel() - } -#endif - } - .disabled(model.alwaysRequestDownloadLocation) - ToggleMenuItem(UserText.downloadsAlwaysAsk, - isOn: $model.alwaysRequestDownloadLocation) - } - } - } - } -} - -#Preview { - VStack { - HStack { - Preferences.DownloadsView(model: DownloadsPreferences(persistor: DownloadsPreferencesUserDefaultsPersistor())) - .padding() - Spacer() - }.frame(width: 500) - - }.background(Color.preferencesBackground) -} diff --git a/DuckDuckGo/Preferences/View/PreferencesEmailProtectionView.swift b/DuckDuckGo/Preferences/View/PreferencesEmailProtectionView.swift new file mode 100644 index 0000000000..cf864f651e --- /dev/null +++ b/DuckDuckGo/Preferences/View/PreferencesEmailProtectionView.swift @@ -0,0 +1,81 @@ +// +// PreferencesEmailProtectionView.swift +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import AppKit +import Combine +import PreferencesViews +import SwiftUI +import SwiftUIExtensions +import BrowserServicesKit + +extension Preferences { + struct EmailProtectionView: View, PreferencesTabOpening { + var emailManager: EmailManager + @ObservedObject var protectionStatus: PrivacyProtectionStatus = PrivacyProtectionStatus.status(for: .emailProtection) + + var body: some View { + PreferencePane(UserText.emailProtectionPreferences, spacing: 4) { + + // SECTION 1: Status Indicator + PreferencePaneSection { + StatusIndicatorView(status: protectionStatus.status ?? .off, isLarge: true) + } + + // SECTION 2: Description + PreferencePaneSection { + VStack(alignment: .leading, spacing: 1) { + TextMenuItemCaption(UserText.emailProtectionExplanation) + TextButton(UserText.learnMore) { + openNewTab(with: .duckDuckGoEmailInfo) + } + } + } + + // SECTION 3: Current Account Info + PreferencePaneSection { + if emailManager.isSignedIn { + if let userEmail = emailManager.userEmail { + Text(UserText.autofillEnabledFor) + Text(" \(userEmail)").bold() + } + Button(UserText.emailOptionsMenuManageAccountSubItem + "…") { + openNewTab(with: EmailUrls().emailProtectionAccountLink) + } + Button(UserText.emailOptionsMenuTurnOffSubItem) { + let alert = NSAlert.disableEmailProtection() + let response = alert.runModal() + if response == .alertFirstButtonReturn { + try? emailManager.signOut() + } + } + + // Support + PreferencePaneSubSection { + TextButton(UserText.support) { + openNewTab(with: EmailUrls().emailProtectionSupportLink) + } + } + } else { + Button(UserText.emailOptionsMenuTurnOnSubItem + "…") { + openNewTab(with: EmailUrls().emailProtectionLink) + } + } + } + } + } + } +} diff --git a/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift b/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift index 6ad97d6463..591961ee75 100644 --- a/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift @@ -25,38 +25,15 @@ import SwiftUIExtensions extension Preferences { struct GeneralView: View { - @ObservedObject var defaultBrowserModel: DefaultBrowserPreferences @ObservedObject var startupModel: StartupPreferences + @ObservedObject var downloadsModel: DownloadsPreferences + @ObservedObject var searchModel: SearchPreferences @State private var showingCustomHomePageSheet = false var body: some View { PreferencePane(UserText.general) { - // SECTION 1: Default Browser - PreferencePaneSection(UserText.defaultBrowser) { - - PreferencePaneSubSection { - HStack { - if defaultBrowserModel.isDefault { - Image(.solidCheckmark) - Text(UserText.isDefaultBrowser) - } else { - Image(.warning).foregroundColor(Color(.linkBlue)) - Text(UserText.isNotDefaultBrowser) - Button(action: { - defaultBrowserModel.becomeDefault() - }) { - Text(UserText.makeDefaultBrowser) - .fixedSize(horizontal: false, vertical: true) - .multilineTextAlignment(.center) - .lineLimit(2) - } - } - } - } - } - - // SECTION 2: On Startup + // SECTION 1: On Startup PreferencePaneSection(UserText.onStartup) { PreferencePaneSubSection { @@ -70,7 +47,7 @@ extension Preferences { } } - // SECTION 3: Home Page + // SECTION 2: Home Page PreferencePaneSection(UserText.homePage) { PreferencePaneSubSection { @@ -103,7 +80,7 @@ extension Preferences { Text(UserText.homeButtonMode(for: position)).tag(position) } } - .scaledToFit() + .fixedSize() .onChange(of: startupModel.homeButtonPosition) { _ in startupModel.updateHomeButton() } @@ -114,6 +91,34 @@ extension Preferences { CustomHomePageSheet(startupModel: startupModel, isSheetPresented: $showingCustomHomePageSheet) } + // SECTION 3: Search Settings + PreferencePaneSection(UserText.privateSearch) { + ToggleMenuItem(UserText.showAutocompleteSuggestions, isOn: $searchModel.showAutocompleteSuggestions) + } + + // SECTION 4: Downloads + PreferencePaneSection(UserText.downloads) { + PreferencePaneSubSection { + ToggleMenuItem(UserText.downloadsOpenPopupOnCompletion, + isOn: $downloadsModel.shouldOpenPopupOnCompletion) + }.padding(.bottom, 5) + + // MARK: Location + PreferencePaneSubSection { + Text(UserText.downloadsLocation).bold() + HStack { + NSPathControlView(url: downloadsModel.selectedDownloadLocation) +#if !APPSTORE + Button(UserText.downloadsChangeDirectory) { + downloadsModel.presentDownloadDirectoryPanel() + } +#endif + } + .disabled(downloadsModel.alwaysRequestDownloadLocation) + ToggleMenuItem(UserText.downloadsAlwaysAsk, + isOn: $downloadsModel.alwaysRequestDownloadLocation) + } + } } } } diff --git a/DuckDuckGo/Preferences/View/PreferencesPrivacyView.swift b/DuckDuckGo/Preferences/View/PreferencesPrivacyView.swift deleted file mode 100644 index 22755196e7..0000000000 --- a/DuckDuckGo/Preferences/View/PreferencesPrivacyView.swift +++ /dev/null @@ -1,88 +0,0 @@ -// -// PreferencesPrivacyView.swift -// -// Copyright © 2022 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 PreferencesViews -import SwiftUI -import SwiftUIExtensions - -extension Preferences { - - struct PrivacyView: View { - @ObservedObject var model: PrivacyPreferencesModel - - var body: some View { - PreferencePane(UserText.privacy) { - - // SECTION 1: Web Tracking Protection Section - PreferencePaneSection(UserText.webTrackingProtectionSettingsTitle) { - - VStack(alignment: .leading, spacing: 0) { - TextMenuItemCaption(UserText.webTrackingProtectionExplenation) - TextButton(UserText.learnMore) { - model.openURL(.webTrackingProtection) - } - } - } - - // SECTION 2: Cookie Consent Pop-ups - PreferencePaneSection(UserText.autoconsentSettingsTitle) { - - ToggleMenuItem(UserText.autoconsentCheckboxTitle, isOn: $model.isAutoconsentEnabled) - VStack(alignment: .leading, spacing: 0) { - TextMenuItemCaption(UserText.autoconsentExplanation) - TextButton(UserText.learnMore) { - model.openURL(.cookieConsentPopUpManagement) - } - } - } - - // SECTION 3: Fireproof Site - PreferencePaneSection(UserText.fireproofSites) { - - PreferencePaneSubSection { - ToggleMenuItem(UserText.fireproofCheckboxTitle, isOn: $model.isLoginDetectionEnabled) - VStack(alignment: .leading, spacing: 0) { - TextMenuItemCaption(UserText.fireproofExplanation) - TextButton(UserText.learnMore) { - model.openURL(.theFireButton) - } - } - } - - PreferencePaneSubSection { - Button(UserText.manageFireproofSites) { - model.presentManageFireproofSitesDialog() - } - } - } - - // SECTION 4: Global privacy control - PreferencePaneSection(UserText.gpcSettingsTitle) { - - ToggleMenuItem(UserText.gpcCheckboxTitle, isOn: $model.isGPCEnabled) - VStack(alignment: .leading, spacing: 0) { - TextMenuItemCaption(UserText.gpcExplanation) - TextButton(UserText.learnMore) { - model.openURL(.gpcLearnMore) - } - } - } - } - } - } -} diff --git a/DuckDuckGo/Preferences/View/PreferencesPrivateSearchView.swift b/DuckDuckGo/Preferences/View/PreferencesPrivateSearchView.swift new file mode 100644 index 0000000000..b43f5db8fd --- /dev/null +++ b/DuckDuckGo/Preferences/View/PreferencesPrivateSearchView.swift @@ -0,0 +1,55 @@ +// +// PreferencesPrivateSearchView.swift +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import AppKit +import Combine +import PreferencesViews +import SwiftUI +import SwiftUIExtensions + +extension Preferences { + + struct PrivateSearchView: View { + @ObservedObject var model: SearchPreferences + + var body: some View { + PreferencePane(UserText.privateSearch, spacing: 4) { + + // SECTION 1: Status Indicator + PreferencePaneSection { + StatusIndicatorView(status: .alwaysOn, isLarge: true) + } + + // SECTION 2: Description + PreferencePaneSection { + VStack(alignment: .leading, spacing: 1) { + TextMenuItemCaption(UserText.privateSearchExplanation) + TextButton(UserText.learnMore) { + model.openNewTab(with: .privateSearchLearnMore) + } + } + } + + // SECTION 3: Search Settings + PreferencePaneSection { + ToggleMenuItem(UserText.showAutocompleteSuggestions, isOn: $model.showAutocompleteSuggestions) + } + } + } + } +} diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index 76c6d00af1..5b01e77577 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -21,6 +21,7 @@ import PreferencesViews import SwiftUI import SwiftUIExtensions import SyncUI +import BrowserServicesKit #if SUBSCRIPTION import Subscription @@ -30,9 +31,16 @@ import SubscriptionUI enum Preferences { enum Const { - static let sidebarWidth: CGFloat = 256 + static var sidebarWidth: CGFloat { + switch Locale.current.languageCode { + case "en": + return 310 + default: + return 355 + } + } static let paneContentWidth: CGFloat = 524 - static let panePaddingHorizontal: CGFloat = 48 + static let panePaddingHorizontal: CGFloat = 40 static let panePaddingVertical: CGFloat = 40 } @@ -55,58 +63,75 @@ enum Preferences { var body: some View { HStack(spacing: 0) { Sidebar().environmentObject(model).frame(width: Const.sidebarWidth) - Color(NSColor.separatorColor).frame(width: 1) - ScrollView(.vertical) { HStack(spacing: 0) { - VStack(alignment: .leading) { - - switch model.selectedPane { - case .general: - GeneralView(defaultBrowserModel: DefaultBrowserPreferences(), startupModel: StartupPreferences.shared) - case .sync: - SyncView() - case .appearance: - AppearanceView(model: .shared) - case .privacy: - PrivacyView(model: PrivacyPreferencesModel()) + contentView + Spacer() + } + } + .frame(maxWidth: .infinity) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.preferencesBackground) + } + + @ViewBuilder + var contentView: some View { + VStack(alignment: .leading) { + switch model.selectedPane { + case .defaultBrowser: + DefaultBrowserView(defaultBrowserModel: DefaultBrowserPreferences.shared, + status: PrivacyProtectionStatus.status(for: .defaultBrowser)) + case .privateSearch: + PrivateSearchView(model: SearchPreferences.shared) + case .webTrackingProtection: + WebTrackingProtectionView(model: WebTrackingProtectionPreferences.shared) + case .cookiePopupProtection: + CookiePopupProtectionView(model: CookiePopupProtectionPreferences.shared) + case .emailProtection: + EmailProtectionView(emailManager: EmailManager()) + case .general: + GeneralView(startupModel: StartupPreferences.shared, + downloadsModel: DownloadsPreferences.shared, + searchModel: SearchPreferences.shared) + case .sync: + SyncView() + case .appearance: + AppearanceView(model: .shared) + case .dataClearing: + DataClearingView(model: DataClearingPreferences.shared) #if NETWORK_PROTECTION - case .vpn: - VPNView(model: VPNPreferencesModel()) + case .vpn: + VPNView(model: VPNPreferencesModel()) #endif #if SUBSCRIPTION - case .subscription: - SubscriptionUI.PreferencesSubscriptionView(model: subscriptionModel!) + case .subscription: + SubscriptionUI.PreferencesSubscriptionView(model: subscriptionModel!) #endif - case .autofill: - AutofillView(model: AutofillPreferencesModel()) - case .downloads: - DownloadsView(model: .shared) - case .duckPlayer: - DuckPlayerView(model: .shared) - case .about: + case .autofill: + AutofillView(model: AutofillPreferencesModel()) + case .accessibility: + AccessibilityView(model: AccessibilityPreferences.shared) + case .duckPlayer: + DuckPlayerView(model: .shared) + case .otherPlatforms: + // Opens a new tab + Spacer() + case .about: #if NETWORK_PROTECTION - let netPInvitePresenter = NetworkProtectionInvitePresenter() - AboutView(model: AboutModel(netPInvitePresenter: netPInvitePresenter)) + let netPInvitePresenter = NetworkProtectionInvitePresenter() + AboutView(model: AboutModel(netPInvitePresenter: netPInvitePresenter)) #else - AboutView(model: AboutModel()) + AboutView(model: AboutModel()) #endif - } - } - .frame(maxWidth: Const.paneContentWidth, maxHeight: .infinity, alignment: .topLeading) - .padding(.vertical, Const.panePaddingVertical) - .padding(.horizontal, Const.panePaddingHorizontal) - - Spacer() - } } - .frame(maxWidth: .infinity) } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color.preferencesBackground) + .frame(maxWidth: Const.paneContentWidth, maxHeight: .infinity, alignment: .topLeading) + .padding(.vertical, Const.panePaddingVertical) + .padding(.horizontal, Const.panePaddingHorizontal) } #if SUBSCRIPTION @@ -168,27 +193,3 @@ enum Preferences { #endif } } - -struct SyncView: View { - - var body: some View { - if let syncService = NSApp.delegateTyped.syncService, let syncDataProviders = NSApp.delegateTyped.syncDataProviders { - SyncUI.ManagementView(model: SyncPreferences(syncService: syncService, syncBookmarksAdapter: syncDataProviders.bookmarksAdapter)) - .onAppear { - requestSync() - } - } else { - FailedAssertionView("Failed to initialize Sync Management View") - } - } - - private func requestSync() { - Task { @MainActor in - guard let syncService = (NSApp.delegate as? AppDelegate)?.syncService else { - return - } - os_log(.debug, log: OSLog.sync, "Requesting sync if enabled") - syncService.scheduler.notifyDataChanged() - } - } -} diff --git a/DuckDuckGo/Preferences/View/PreferencesSidebar.swift b/DuckDuckGo/Preferences/View/PreferencesSidebar.swift index 1a0c1babc6..b6f65c0a44 100644 --- a/DuckDuckGo/Preferences/View/PreferencesSidebar.swift +++ b/DuckDuckGo/Preferences/View/PreferencesSidebar.swift @@ -22,22 +22,109 @@ import SwiftUIExtensions extension Preferences { - struct SidebarItem: View { + struct SidebarSectionHeader: View { + let section: PreferencesSectionIdentifier + + var body: some View { + Group { + if let name = section.displayName { + Text(name) + .padding(.horizontal, 16) + .padding(.bottom, 3) + .font(PreferencesViews.Const.Fonts.sideBarHeader) + .foregroundColor(.secondary) + .frame(maxWidth: .infinity, minHeight: 31, alignment: .leading) + } + } + } + } + + struct PaneSidebarItem: View { let pane: PreferencePaneIdentifier let isSelected: Bool let action: () -> Void + @ObservedObject var protectionStatus: PrivacyProtectionStatus + + init(pane: PreferencePaneIdentifier, isSelected: Bool, action: @escaping () -> Void) { + self.pane = pane + self.isSelected = isSelected + self.action = action + self.protectionStatus = PrivacyProtectionStatus.status(for: pane) + } var body: some View { Button(action: action) { HStack(spacing: 6) { Image(pane.preferenceIconName).frame(width: 16, height: 16) Text(pane.displayName).font(PreferencesViews.Const.Fonts.sideBarItem) + + Spacer() + + if let status = protectionStatus.status { + StatusIndicatorView(status: status) + } } } .buttonStyle(SidebarItemButtonStyle(isSelected: isSelected)) } } + enum StatusIndicator: Equatable { + case alwaysOn + case on + case off + case custom(String) + + var text: String { + switch self { + case .alwaysOn: + return UserText.preferencesAlwaysOn + case .on: + return UserText.preferencesOn + case .off: + return UserText.preferencesOff + case .custom(let customText): + return customText + } + } + } + + struct StatusIndicatorView: View { + var status: StatusIndicator + var isLarge: Bool = false + + private var fontSize: CGFloat { + isLarge ? 13 : 10 + } + + private var circleSize: CGFloat { + isLarge ? 7 : 5 + } + + var body: some View { + HStack(spacing: isLarge ? 6 : 4) { + Circle() + .frame(width: circleSize, height: circleSize) + .foregroundColor(colorForStatus(status)) + + Text(status.text) + .font(.system(size: fontSize)) + .foregroundColor(.secondary) + } + } + + private func colorForStatus(_ status: StatusIndicator) -> Color { + switch status { + case .on, .alwaysOn: + return .alertGreen + case .off: + return Color.secondary.opacity(0.33) + case .custom: + return .orange + } + } + } + struct TabSwitcher: View { @EnvironmentObject var model: PreferencesSidebarModel @@ -61,7 +148,7 @@ extension Preferences { return button }) .padding(.horizontal, 3) - .frame(height: 60) + .frame(height: 51) .onAppear(perform: model.resetTabSelectionIfNeeded) } } @@ -77,20 +164,22 @@ extension Preferences { ScrollView { VStack(spacing: 0) { ForEach(model.sections) { section in + SidebarSectionHeader(section: section.id) sidebarSection(section) } - } + }.padding(.bottom, 16) } } .padding(.top, 18) - .padding(.horizontal, 20) + .padding(.horizontal, 10) } @ViewBuilder private func sidebarSection(_ section: PreferencesSection) -> some View { ForEach(section.panes) { pane in - SidebarItem(pane: pane, isSelected: model.selectedPane == pane) { + PaneSidebarItem(pane: pane, + isSelected: model.selectedPane == pane) { model.selectPane(pane) } } diff --git a/DuckDuckGo/Preferences/View/PreferencesSyncView.swift b/DuckDuckGo/Preferences/View/PreferencesSyncView.swift new file mode 100644 index 0000000000..dab27f7101 --- /dev/null +++ b/DuckDuckGo/Preferences/View/PreferencesSyncView.swift @@ -0,0 +1,47 @@ +// +// PreferencesSyncView.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import Common +import SyncUI +import SwiftUIExtensions +import BrowserServicesKit + +struct SyncView: View { + + var body: some View { + if let syncService = NSApp.delegateTyped.syncService, let syncDataProviders = NSApp.delegateTyped.syncDataProviders { + SyncUI.ManagementView(model: SyncPreferences(syncService: syncService, syncBookmarksAdapter: syncDataProviders.bookmarksAdapter)) + .onAppear { + requestSync() + } + } else { + FailedAssertionView("Failed to initialize Sync Management View") + } + } + + private func requestSync() { + Task { @MainActor in + guard let syncService = (NSApp.delegate as? AppDelegate)?.syncService else { + return + } + os_log(.debug, log: OSLog.sync, "Requesting sync if enabled") + syncService.scheduler.notifyDataChanged() + } + } +} diff --git a/DuckDuckGo/Preferences/View/PreferencesWebTrackingProtectionView.swift b/DuckDuckGo/Preferences/View/PreferencesWebTrackingProtectionView.swift new file mode 100644 index 0000000000..90b5fa821a --- /dev/null +++ b/DuckDuckGo/Preferences/View/PreferencesWebTrackingProtectionView.swift @@ -0,0 +1,61 @@ +// +// PreferencesWebTrackingProtectionView.swift +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import AppKit +import Combine +import PreferencesViews +import SwiftUI +import SwiftUIExtensions + +extension Preferences { + + struct WebTrackingProtectionView: View { + @ObservedObject var model: WebTrackingProtectionPreferences + + var body: some View { + PreferencePane(UserText.webTrackingProtection, spacing: 4) { + + // SECTION 1: Status Indicator + PreferencePaneSection { + StatusIndicatorView(status: .alwaysOn, isLarge: true) + } + + // SECTION 2: Description + PreferencePaneSection { + VStack(alignment: .leading, spacing: 1) { + TextMenuItemCaption(UserText.webTrackingProtectionExplanation) + TextButton(UserText.learnMore) { + model.openNewTab(with: .webTrackingProtection) + } + } + } + + // SECTION 3: Global privacy control + PreferencePaneSection { + ToggleMenuItem(UserText.gpcCheckboxTitle, isOn: $model.isGPCEnabled) + VStack(alignment: .leading, spacing: 1) { + TextMenuItemCaption(UserText.gpcExplanation) + TextButton(UserText.learnMore) { + model.openNewTab(with: .gpcLearnMore) + } + }.padding(.leading, 19) + } + } + } + } +} diff --git a/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardViewController.swift b/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardViewController.swift index d30d1700d7..6074ac2343 100644 --- a/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardViewController.swift +++ b/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardViewController.swift @@ -221,7 +221,7 @@ extension PrivacyDashboardViewController: PrivacyDashboardControllerDelegate { switch target { case .cookiePopupManagement: - tabCollection.appendNewTab(with: .settings(pane: .privacy), selected: true) + tabCollection.appendNewTab(with: .settings(pane: .dataClearing), selected: true) default: tabCollection.appendNewTab(with: .anySettingsPane, selected: true) } @@ -339,7 +339,7 @@ extension PrivacyDashboardViewController { tdsETag: ContentBlocking.shared.contentBlockingManager.currentRules.first?.etag, blockedTrackerDomains: blockedTrackerDomains, installedSurrogates: installedSurrogates, - isGPCEnabled: PrivacySecurityPreferences.shared.gpcEnabled, + isGPCEnabled: WebTrackingProtectionPreferences.shared.isGPCEnabled, ampURL: ampURL, urlParametersRemoved: urlParametersRemoved, protectionsState: protectionsState, diff --git a/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift b/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift index cc1ae4fcba..4647fd79a8 100644 --- a/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift +++ b/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift @@ -279,7 +279,7 @@ final class SaveCredentialsViewController: NSViewController { delegate?.shouldCloseSaveCredentialsViewController(self) } - guard PrivacySecurityPreferences.shared.loginDetectionEnabled else { + guard DataClearingPreferences.shared.isLoginDetectionEnabled else { notifyDelegate() return } diff --git a/DuckDuckGo/Statistics/PixelEvent.swift b/DuckDuckGo/Statistics/PixelEvent.swift index 0af3664f30..bd576d9bf0 100644 --- a/DuckDuckGo/Statistics/PixelEvent.swift +++ b/DuckDuckGo/Statistics/PixelEvent.swift @@ -258,6 +258,12 @@ extension Pixel { case dailyPixel(Event, isFirst: Bool) + // Default Browser + case defaultRequestedFromHomepage + case defaultRequestedFromHomepageSetupView + case defaultRequestedFromSettings + case defaultRequestedFromOnboarding + case protectionToggledOffBreakageReport case toggleProtectionsDailyCount case toggleReportDoNotSend @@ -639,6 +645,11 @@ extension Pixel.Event { case .networkProtectionGeoswitchingNoLocations: return "m_mac_netp_ev_geoswitching_no_locations" + case .defaultRequestedFromHomepage: return "m_mac_default_requested_from_homepage" + case .defaultRequestedFromHomepageSetupView: return "m_mac_default_requested_from_homepage_setup_view" + case .defaultRequestedFromSettings: return "m_mac_default_requested_from_settings" + case .defaultRequestedFromOnboarding: return "m_mac_default_requested_from_onboarding" + // MARK: - Subscription case .privacyProSubscriptionActive: return "m_mac_\(appDistribution)_privacy-pro_app_subscription_active" case .privacyProOfferScreenImpression: return "m_mac_\(appDistribution)_privacy-pro_offer_screen_impression" @@ -682,6 +693,7 @@ extension Pixel.Event { case .toggleProtectionsDailyCount: return "m_mac_toggle-protections-daily-count" case .toggleReportDoNotSend: return "m_mac_toggle-report-do-not-send" case .toggleReportDismiss: return "m_mac_toggle-report-dismiss" + } } } diff --git a/DuckDuckGo/Statistics/PixelParameters.swift b/DuckDuckGo/Statistics/PixelParameters.swift index c8be054048..bca6af9060 100644 --- a/DuckDuckGo/Statistics/PixelParameters.swift +++ b/DuckDuckGo/Statistics/PixelParameters.swift @@ -166,6 +166,10 @@ extension Pixel.Event { .dataBrokerEnableLoginItemDaily, .dataBrokerDisableLoginItemDaily, .dataBrokerResetLoginItemDaily, + .defaultRequestedFromHomepage, + .defaultRequestedFromHomepageSetupView, + .defaultRequestedFromSettings, + .defaultRequestedFromOnboarding, .privacyProSubscriptionActive, .privacyProOfferScreenImpression, .privacyProPurchaseAttempt, diff --git a/DuckDuckGo/Suggestions/ViewModel/SuggestionContainerViewModel.swift b/DuckDuckGo/Suggestions/ViewModel/SuggestionContainerViewModel.swift index 8d205d7fcc..8ad5149a9b 100644 --- a/DuckDuckGo/Suggestions/ViewModel/SuggestionContainerViewModel.swift +++ b/DuckDuckGo/Suggestions/ViewModel/SuggestionContainerViewModel.swift @@ -75,7 +75,7 @@ final class SuggestionContainerViewModel { } func setUserStringValue(_ userStringValue: String, userAppendedStringToTheEnd: Bool) { - guard AppearancePreferences.shared.showAutocompleteSuggestions else { + guard SearchPreferences.shared.showAutocompleteSuggestions else { return } diff --git a/DuckDuckGo/Tab/Model/UserContentUpdating.swift b/DuckDuckGo/Tab/Model/UserContentUpdating.swift index ba9f8ba7b6..721608ad24 100644 --- a/DuckDuckGo/Tab/Model/UserContentUpdating.swift +++ b/DuckDuckGo/Tab/Model/UserContentUpdating.swift @@ -50,13 +50,13 @@ final class UserContentUpdating { privacyConfigurationManager: PrivacyConfigurationManaging, trackerDataManager: TrackerDataManager, configStorage: ConfigurationStoring, - privacySecurityPreferences: PrivacySecurityPreferences, + webTrackingProtectionPreferences: WebTrackingProtectionPreferences, tld: TLD) { let makeValue: (Update) -> NewContent = { rulesUpdate in let sourceProvider = ScriptSourceProvider(configStorage: configStorage, privacyConfigurationManager: privacyConfigurationManager, - privacySettings: privacySecurityPreferences, + webTrackingProtectionPreferences: webTrackingProtectionPreferences, contentBlockingManager: contentBlockerRulesManager, trackerDataManager: trackerDataManager, tld: tld) @@ -78,7 +78,7 @@ final class UserContentUpdating { // 1. Collect updates from ContentBlockerRulesManager and generate UserScripts based on its output cancellable = contentBlockerRulesManager.updatesPublisher // regenerate UserScripts on gpcEnabled preference updated - .combineLatest(privacySecurityPreferences.$gpcEnabled) + .combineLatest(webTrackingProtectionPreferences.$isGPCEnabled) .map { $0.0 } // drop gpcEnabled value: $0.1 .combineLatest(onNotificationWithInitial(.autofillUserSettingsDidChange), combine) .combineLatest(onNotificationWithInitial(.autofillScriptDebugSettingsDidChange), combine) diff --git a/DuckDuckGo/Tab/TabExtensions/AutofillTabExtension.swift b/DuckDuckGo/Tab/TabExtensions/AutofillTabExtension.swift index e4b1c54fa2..74974a747d 100644 --- a/DuckDuckGo/Tab/TabExtensions/AutofillTabExtension.swift +++ b/DuckDuckGo/Tab/TabExtensions/AutofillTabExtension.swift @@ -198,7 +198,7 @@ extension AutofillTabExtension: SecureVaultManagerDelegate { supportedFeatures.passwordGeneration = false } - return ContentScopeProperties(gpcEnabled: PrivacySecurityPreferences.shared.gpcEnabled, + return ContentScopeProperties(gpcEnabled: WebTrackingProtectionPreferences.shared.isGPCEnabled, sessionKey: autofillScript?.sessionKey ?? "", featureToggles: supportedFeatures) } diff --git a/DuckDuckGo/Tab/TabExtensions/NavigationProtectionTabExtension.swift b/DuckDuckGo/Tab/TabExtensions/NavigationProtectionTabExtension.swift index 539f7a29e3..aa8fa230fc 100644 --- a/DuckDuckGo/Tab/TabExtensions/NavigationProtectionTabExtension.swift +++ b/DuckDuckGo/Tab/TabExtensions/NavigationProtectionTabExtension.swift @@ -101,9 +101,10 @@ extension NavigationProtectionTabExtension: NavigationResponder { request = newRequest } + let isGPCEnabled = WebTrackingProtectionPreferences.shared.isGPCEnabled if let newRequest = GPCRequestFactory().requestForGPC(basedOn: request, config: contentBlocking.privacyConfigurationManager.privacyConfig, - gpcEnabled: PrivacySecurityPreferences.shared.gpcEnabled) { + gpcEnabled: isGPCEnabled) { request = newRequest } diff --git a/DuckDuckGo/Tab/UserScripts/UserScripts.swift b/DuckDuckGo/Tab/UserScripts/UserScripts.swift index 4cde694a07..4dfa1c5b50 100644 --- a/DuckDuckGo/Tab/UserScripts/UserScripts.swift +++ b/DuckDuckGo/Tab/UserScripts/UserScripts.swift @@ -50,10 +50,10 @@ final class UserScripts: UserScriptsProvider { clickToLoadScript = ClickToLoadUserScript(scriptSourceProvider: sourceProvider) contentBlockerRulesScript = ContentBlockerRulesUserScript(configuration: sourceProvider.contentBlockerRulesConfig!) surrogatesScript = SurrogatesUserScript(configuration: sourceProvider.surrogatesConfig!) - let privacySettings = PrivacySecurityPreferences.shared + let isGPCEnabled = WebTrackingProtectionPreferences.shared.isGPCEnabled let privacyConfig = sourceProvider.privacyConfigurationManager.privacyConfig let sessionKey = sourceProvider.sessionKey ?? "" - let prefs = ContentScopeProperties(gpcEnabled: privacySettings.gpcEnabled, + let prefs = ContentScopeProperties(gpcEnabled: isGPCEnabled, sessionKey: sessionKey, featureToggles: ContentScopeFeatureToggles.supportedFeaturesOnMacOS(privacyConfig)) contentScopeUserScript = ContentScopeUserScript(sourceProvider.privacyConfigurationManager, properties: prefs) diff --git a/DuckDuckGo/Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Tab/View/BrowserTabViewController.swift index 37b06c15d5..eccbde153a 100644 --- a/DuckDuckGo/Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Tab/View/BrowserTabViewController.swift @@ -1092,12 +1092,13 @@ extension BrowserTabViewController: OnboardingDelegate { } func onboardingDidRequestSetDefault(completion: @escaping () -> Void) { - let defaultBrowserPreferences = DefaultBrowserPreferences() + let defaultBrowserPreferences = DefaultBrowserPreferences.shared if defaultBrowserPreferences.isDefault { completion() return } + Pixel.fire(.defaultRequestedFromOnboarding) defaultBrowserPreferences.becomeDefault { _ in _ = defaultBrowserPreferences withAnimation { diff --git a/DuckDuckGo/Tab/ViewModel/TabViewModel.swift b/DuckDuckGo/Tab/ViewModel/TabViewModel.swift index 9d6fea5595..c6853be3fb 100644 --- a/DuckDuckGo/Tab/ViewModel/TabViewModel.swift +++ b/DuckDuckGo/Tab/ViewModel/TabViewModel.swift @@ -35,6 +35,7 @@ final class TabViewModel { private(set) var tab: Tab private let appearancePreferences: AppearancePreferences + private let accessibilityPreferences: AccessibilityPreferences private var cancellables = Set() @Published private(set) var canGoForward: Bool = false @@ -78,9 +79,12 @@ final class TabViewModel { !isShowingErrorPage && canReload && !tab.webView.isInFullScreenMode } - init(tab: Tab, appearancePreferences: AppearancePreferences = .shared) { + init(tab: Tab, + appearancePreferences: AppearancePreferences = .shared, + accessibilityPreferences: AccessibilityPreferences = .shared) { self.tab = tab self.appearancePreferences = appearancePreferences + self.accessibilityPreferences = accessibilityPreferences subscribeToUrl() subscribeToCanGoBackForwardAndReload() @@ -88,7 +92,7 @@ final class TabViewModel { subscribeToFavicon() subscribeToTabError() subscribeToPermissions() - subscribeToAppearancePreferences() + subscribeToPreferences() subscribeToWebViewDidFinishNavigation() tab.$isLoading .assign(to: \.isLoading, onWeaklyHeld: self) @@ -209,12 +213,12 @@ final class TabViewModel { .store(in: &cancellables) } - private func subscribeToAppearancePreferences() { + private func subscribeToPreferences() { appearancePreferences.$showFullURL.dropFirst().sink { [weak self] newValue in guard let self = self, let url = self.tabURL, let host = self.tabHostURL else { return } self.updatePassiveAddressBarString(showURL: newValue, url: url, hostURL: host) }.store(in: &cancellables) - appearancePreferences.$defaultPageZoom.sink { [weak self] newValue in + accessibilityPreferences.$defaultPageZoom.sink { [weak self] newValue in guard let self = self else { return } self.tab.webView.defaultZoomValue = newValue self.tab.webView.zoomLevel = newValue diff --git a/IntegrationTests/AutoconsentIntegrationTests.swift b/IntegrationTests/AutoconsentIntegrationTests.swift index b674b41635..63f54da582 100644 --- a/IntegrationTests/AutoconsentIntegrationTests.swift +++ b/IntegrationTests/AutoconsentIntegrationTests.swift @@ -39,7 +39,7 @@ class AutoconsentIntegrationTests: XCTestCase { @MainActor override func setUp() { // disable GPC redirects - PrivacySecurityPreferences.shared.gpcEnabled = false + WebTrackingProtectionPreferences.shared.isGPCEnabled = false window = WindowsManager.openNewWindow(with: Tab(content: .none)) } @@ -49,7 +49,7 @@ class AutoconsentIntegrationTests: XCTestCase { window.close() window = nil - PrivacySecurityPreferences.shared.gpcEnabled = true + WebTrackingProtectionPreferences.shared.isGPCEnabled = true } // MARK: - Tests @@ -57,9 +57,8 @@ class AutoconsentIntegrationTests: XCTestCase { @MainActor func testWhenAutoconsentEnabled_cookieConsentManaged() async throws { // enable the feature - PrivacySecurityPreferences.shared.autoconsentEnabled = true + CookiePopupProtectionPreferences.shared.isAutoconsentEnabled = true let url = URL(string: "http://privacy-test-pages.site/features/autoconsent/")! - let tab = self.tabViewModel.tab // expect cookieConsentManaged to be published @@ -84,9 +83,8 @@ class AutoconsentIntegrationTests: XCTestCase { @MainActor func testCosmeticRule_whenFakeCookieBannerIsDisplayed_bannerIsHidden() async throws { // enable the feature - PrivacySecurityPreferences.shared.autoconsentEnabled = true + CookiePopupProtectionPreferences.shared.isAutoconsentEnabled = true let url = URL(string: "http://privacy-test-pages.site/features/autoconsent/banner.html")! - let tab = self.tabViewModel.tab // expect `cosmetic` to be published let cookieConsentManagedPromise = tab.privacyInfoPublisher @@ -135,9 +133,8 @@ class AutoconsentIntegrationTests: XCTestCase { @MainActor func testCosmeticRule_whenFakeCookieBannerIsDisplayedAndScriptsAreReloaded_bannerIsHidden() async throws { // enable the feature - PrivacySecurityPreferences.shared.autoconsentEnabled = true + CookiePopupProtectionPreferences.shared.isAutoconsentEnabled = true let url = URL(string: "http://privacy-test-pages.site/features/autoconsent/banner.html")! - let tab = self.tabViewModel.tab // expect `cosmetic` to be published let cookieConsentManagedPromise = tab.privacyInfoPublisher @@ -160,8 +157,8 @@ class AutoconsentIntegrationTests: XCTestCase { os_log("navigationResponse: %s", "\(String(describing: response))") // cause UserScripts reload (ContentBlockingUpdating) - PrivacySecurityPreferences.shared.gpcEnabled = true - PrivacySecurityPreferences.shared.gpcEnabled = false + WebTrackingProtectionPreferences.shared.isGPCEnabled = true + WebTrackingProtectionPreferences.shared.isGPCEnabled = false return .allow }) diff --git a/IntegrationTests/HTTPSUpgrade/HTTPSUpgradeIntegrationTests.swift b/IntegrationTests/HTTPSUpgrade/HTTPSUpgradeIntegrationTests.swift index 14c8527e8a..9dbc287a0a 100644 --- a/IntegrationTests/HTTPSUpgrade/HTTPSUpgradeIntegrationTests.swift +++ b/IntegrationTests/HTTPSUpgrade/HTTPSUpgradeIntegrationTests.swift @@ -38,7 +38,7 @@ class HTTPSUpgradeIntegrationTests: XCTestCase { @MainActor override func setUp() async throws { // disable GPC redirects - PrivacySecurityPreferences.shared.gpcEnabled = false + WebTrackingProtectionPreferences.shared.isGPCEnabled = false window = WindowsManager.openNewWindow(with: .none)! @@ -51,7 +51,7 @@ class HTTPSUpgradeIntegrationTests: XCTestCase { window.close() window = nil - PrivacySecurityPreferences.shared.gpcEnabled = true + WebTrackingProtectionPreferences.shared.isGPCEnabled = true } // MARK: - Tests diff --git a/IntegrationTests/History/HistoryIntegrationTests.swift b/IntegrationTests/History/HistoryIntegrationTests.swift index 33d06a3436..9a59e297f0 100644 --- a/IntegrationTests/History/HistoryIntegrationTests.swift +++ b/IntegrationTests/History/HistoryIntegrationTests.swift @@ -63,7 +63,7 @@ class HistoryIntegrationTests: XCTestCase { override func tearDown() async throws { window?.close() window = nil - PrivacySecurityPreferences.shared.gpcEnabled = true + WebTrackingProtectionPreferences.shared.isGPCEnabled = true } // MARK: - Tests @@ -199,7 +199,7 @@ class HistoryIntegrationTests: XCTestCase { @MainActor func testWhenScriptTrackerLoaded_trackerAddedToHistory() async throws { - PrivacySecurityPreferences.shared.gpcEnabled = false + WebTrackingProtectionPreferences.shared.isGPCEnabled = false let tab = Tab(content: .newtab) window = WindowsManager.openNewWindow(with: tab)! @@ -227,7 +227,7 @@ class HistoryIntegrationTests: XCTestCase { @MainActor func testWhenSurrogateTrackerLoaded_trackerAddedToHistory() async throws { - PrivacySecurityPreferences.shared.gpcEnabled = false + WebTrackingProtectionPreferences.shared.isGPCEnabled = false let tab = Tab(content: .newtab) window = WindowsManager.openNewWindow(with: tab)! diff --git a/IntegrationTests/NavigationProtection/NavigationProtectionIntegrationTests.swift b/IntegrationTests/NavigationProtection/NavigationProtectionIntegrationTests.swift index 13417aaecb..f36748c64b 100644 --- a/IntegrationTests/NavigationProtection/NavigationProtectionIntegrationTests.swift +++ b/IntegrationTests/NavigationProtection/NavigationProtectionIntegrationTests.swift @@ -47,7 +47,7 @@ class NavigationProtectionIntegrationTests: XCTestCase { window?.close() window = nil - PrivacySecurityPreferences.shared.gpcEnabled = true + WebTrackingProtectionPreferences.shared.isGPCEnabled = true } // MARK: - Tests @@ -55,7 +55,7 @@ class NavigationProtectionIntegrationTests: XCTestCase { @MainActor func testAMPLinks() async throws { // disable GPC redirects - PrivacySecurityPreferences.shared.gpcEnabled = false + WebTrackingProtectionPreferences.shared.isGPCEnabled = false var onDidCancel: ((NavigationAction, [ExpectedNavigation]?) -> Void)? var onWillStart: ((Navigation) -> Void)? @@ -132,7 +132,7 @@ class NavigationProtectionIntegrationTests: XCTestCase { @MainActor func testReferrerTrimming() async throws { // disable GPC redirects - PrivacySecurityPreferences.shared.gpcEnabled = false + WebTrackingProtectionPreferences.shared.isGPCEnabled = false var lastRedirectedNavigation: Navigation? var onDidFinish: ((Navigation) -> Void)? @@ -205,11 +205,11 @@ class NavigationProtectionIntegrationTests: XCTestCase { let url = URL(string: "https://privacy-test-pages.site/privacy-protections/gpc/")! // disable GPC redirects - PrivacySecurityPreferences.shared.gpcEnabled = false + WebTrackingProtectionPreferences.shared.isGPCEnabled = false _=try await tab.setUrl(url, source: .link)?.result.get() // enable GPC redirects - PrivacySecurityPreferences.shared.gpcEnabled = true + WebTrackingProtectionPreferences.shared.isGPCEnabled = true // expect popup to open and then close var oldValue: TabViewModel! = self.tabViewModel diff --git a/IntegrationTests/PrivacyDashboard/PrivacyDashboardIntegrationTests.swift b/IntegrationTests/PrivacyDashboard/PrivacyDashboardIntegrationTests.swift index 169b967391..6af50b4681 100644 --- a/IntegrationTests/PrivacyDashboard/PrivacyDashboardIntegrationTests.swift +++ b/IntegrationTests/PrivacyDashboard/PrivacyDashboardIntegrationTests.swift @@ -33,7 +33,7 @@ class PrivacyDashboardIntegrationTests: XCTestCase { @MainActor override func setUp() { // disable GPC redirects - PrivacySecurityPreferences.shared.gpcEnabled = false + WebTrackingProtectionPreferences.shared.isGPCEnabled = false window = WindowsManager.openNewWindow(with: .none)! } @@ -43,7 +43,7 @@ class PrivacyDashboardIntegrationTests: XCTestCase { window.close() window = nil - PrivacySecurityPreferences.shared.gpcEnabled = true + WebTrackingProtectionPreferences.shared.isGPCEnabled = true } // MARK: - Tests diff --git a/LocalPackages/SwiftUIExtensions/Sources/PreferencesViews/Constants.swift b/LocalPackages/SwiftUIExtensions/Sources/PreferencesViews/Constants.swift index 7c15b69516..beb1f64ec6 100644 --- a/LocalPackages/SwiftUIExtensions/Sources/PreferencesViews/Constants.swift +++ b/LocalPackages/SwiftUIExtensions/Sources/PreferencesViews/Constants.swift @@ -34,6 +34,7 @@ public enum Const { public enum Fonts { public static let popUpButton: NSFont = .preferredFont(forTextStyle: .title1, options: [:]) + public static let sideBarHeader: Font = .system(size: 11) public static let sideBarItem: Font = .body public static let preferencePaneTitle: Font = .title2.weight(.semibold) public static let preferencePaneSectionHeader: Font = .title3.weight(.semibold) diff --git a/NetworkProtectionAppExtension/InfoPlist.xcstrings b/NetworkProtectionAppExtension/InfoPlist.xcstrings new file mode 100644 index 0000000000..cd83eb4310 --- /dev/null +++ b/NetworkProtectionAppExtension/InfoPlist.xcstrings @@ -0,0 +1,186 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "CFBundleDisplayName" : { + "comment" : "Bundle display name", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "NetworkProtectionAppExtension" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "NetworkProtectionAppExtension" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "NetworkProtectionAppExtension" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "NetworkProtectionAppExtension" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "NetworkProtectionAppExtension" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "NetworkProtectionAppExtension" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "NetworkProtectionAppExtension" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "NetworkProtectionAppExtension" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "NetworkProtectionAppExtension" + } + } + } + }, + "CFBundleName" : { + "comment" : "Bundle name", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "NetworkProtectionAppExtension" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "NetworkProtectionAppExtension" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "NetworkProtectionAppExtension" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "NetworkProtectionAppExtension" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "NetworkProtectionAppExtension" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "NetworkProtectionAppExtension" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "NetworkProtectionAppExtension" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "NetworkProtectionAppExtension" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "NetworkProtectionAppExtension" + } + } + } + }, + "NSHumanReadableCopyright" : { + "comment" : "Copyright (human-readable)", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copyright © 2023 DuckDuckGo. Alle Rechte vorbehalten." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Copyright © 2023 DuckDuckGo. All rights reserved." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copyright © 2023 DuckDuckGo. Todos los derechos reservados." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copyright © 2023 DuckDuckGo. Tous droits réservés." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copyright © 2023 DuckDuckGo. Tutti i diritti riservati." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copyright © 2023 DuckDuckGo. Alle rechten voorbehouden." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copyright © 2023 DuckDuckGo. Wszelkie prawa zastrzeżone." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copyright © 2023 DuckDuckGo. Todos os direitos reservados." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "© 2023 DuckDuckGo. Все права защищены." + } + } + } + } + }, + "version" : "1.0" +} \ No newline at end of file diff --git a/UnitTests/Autoconsent/AutoconsentMessageProtocolTests.swift b/UnitTests/Autoconsent/AutoconsentMessageProtocolTests.swift index d8a1a6f356..a3cc50adfc 100644 --- a/UnitTests/Autoconsent/AutoconsentMessageProtocolTests.swift +++ b/UnitTests/Autoconsent/AutoconsentMessageProtocolTests.swift @@ -26,7 +26,7 @@ class AutoconsentMessageProtocolTests: XCTestCase { let userScript = AutoconsentUserScript( scriptSource: ScriptSourceProvider(configStorage: MockConfigurationStore(), privacyConfigurationManager: MockPrivacyConfigurationManager(), - privacySettings: PrivacySecurityPreferences.shared, // todo: mock + webTrackingProtectionPreferences: WebTrackingProtectionPreferences.shared, // mock contentBlockingManager: ContentBlockerRulesManagerMock(), trackerDataManager: TrackerDataManager(etag: ConfigurationStore.shared.loadEtag(for: .trackerDataSet), data: ConfigurationStore.shared.loadData(for: .trackerDataSet), @@ -38,7 +38,7 @@ class AutoconsentMessageProtocolTests: XCTestCase { override func setUp() { super.setUp() - PrivacySecurityPreferences.shared.autoconsentEnabled = true + CookiePopupProtectionPreferences.shared.isAutoconsentEnabled = true } func replyToJson(msg: Any) -> String { diff --git a/UnitTests/ContentBlocker/ContentBlockingUpdatingTests.swift b/UnitTests/ContentBlocker/ContentBlockingUpdatingTests.swift index 79020eea36..08421e6bc4 100644 --- a/UnitTests/ContentBlocker/ContentBlockingUpdatingTests.swift +++ b/UnitTests/ContentBlocker/ContentBlockingUpdatingTests.swift @@ -25,7 +25,7 @@ import BrowserServicesKit final class ContentBlockingUpdatingTests: XCTestCase { - let preferences = PrivacySecurityPreferences.shared + let preferences = WebTrackingProtectionPreferences.shared let rulesManager = ContentBlockerRulesManagerMock() var updating: UserContentUpdating! @@ -37,7 +37,7 @@ final class ContentBlockingUpdatingTests: XCTestCase { embeddedDataProvider: AppTrackerDataSetProvider(), errorReporting: nil), configStorage: MockConfigurationStore(), - privacySecurityPreferences: preferences, + webTrackingProtectionPreferences: preferences, tld: TLD()) } @@ -107,7 +107,7 @@ final class ContentBlockingUpdatingTests: XCTestCase { } rulesManager.updatesSubject.send(Self.testUpdate()) - preferences.gpcEnabled = !preferences.gpcEnabled + preferences.isGPCEnabled = !preferences.isGPCEnabled withExtendedLifetime(c) { waitForExpectations(timeout: 0, handler: nil) diff --git a/UnitTests/HomePage/ContinueSetUpModelTests.swift b/UnitTests/HomePage/ContinueSetUpModelTests.swift index 817fc5bfb0..c182554a38 100644 --- a/UnitTests/HomePage/ContinueSetUpModelTests.swift +++ b/UnitTests/HomePage/ContinueSetUpModelTests.swift @@ -68,8 +68,8 @@ final class ContinueSetUpModelTests: XCTestCase { var tabCollectionVM: TabCollectionViewModel! var emailManager: EmailManager! var emailStorage: MockEmailStorage! - var privacyPreferences: PrivacySecurityPreferences! var duckPlayerPreferences: DuckPlayerPreferencesPersistor! + var coookiePopupProtectionPreferences: MockCookiePopupProtectionPreferencesPersistor! var privacyConfigManager: MockPrivacyConfigurationManager! var randomNumberGenerator: MockRandomNumberGenerator! let userDefaults = UserDefaults(suiteName: "\(Bundle.main.bundleIdentifier!).\(NSApplication.runType)")! @@ -86,7 +86,6 @@ final class ContinueSetUpModelTests: XCTestCase { tabCollectionVM = TabCollectionViewModel() emailStorage = MockEmailStorage() emailManager = EmailManager(storage: emailStorage) - privacyPreferences = PrivacySecurityPreferences.shared duckPlayerPreferences = DuckPlayerPreferencesPersistorMock() privacyConfigManager = MockPrivacyConfigurationManager() let config = MockPrivacyConfiguration() @@ -123,7 +122,6 @@ final class ContinueSetUpModelTests: XCTestCase { dataImportProvider: capturingDataImportProvider, tabCollectionViewModel: tabCollectionVM, emailManager: emailManager, - privacyPreferences: privacyPreferences, duckPlayerPreferences: duckPlayerPreferences, homePageRemoteMessaging: messaging, privacyConfigurationManager: privacyConfigManager, @@ -138,7 +136,6 @@ final class ContinueSetUpModelTests: XCTestCase { tabCollectionVM = nil emailManager = nil emailStorage = nil - privacyPreferences = nil vm = nil } @@ -162,14 +159,12 @@ final class ContinueSetUpModelTests: XCTestCase { capturingDefaultBrowserProvider.isDefault = true capturingDataImportProvider.didImport = true duckPlayerPreferences.youtubeOverlayAnyButtonPressed = true - privacyPreferences.autoconsentEnabled = true vm = HomePage.Models.ContinueSetUpModel( defaultBrowserProvider: capturingDefaultBrowserProvider, dataImportProvider: capturingDataImportProvider, tabCollectionViewModel: tabCollectionVM, emailManager: emailManager, - privacyPreferences: privacyPreferences, duckPlayerPreferences: duckPlayerPreferences, homePageRemoteMessaging: createMessaging() ) @@ -406,22 +401,6 @@ final class ContinueSetUpModelTests: XCTestCase { XCTAssertTrue(vm.visibleFeaturesMatrix[0].count <= vm.itemsPerRow) } - @MainActor func testWhenUserHasCookieConsentEnabledThenCorrectElementsAreVisible() { - let expectedMatrix = expectedFeatureMatrixWithout(types: [.surveyDay14]) - - privacyPreferences.autoconsentEnabled = true - vm = HomePage.Models.ContinueSetUpModel.fixture(privacyPreferences: privacyPreferences, appGroupUserDefaults: userDefaults) - - vm.shouldShowAllFeatures = true - - XCTAssertTrue(doTheyContainTheSameElements(matrix1: vm.visibleFeaturesMatrix, matrix2: expectedMatrix)) - - vm.shouldShowAllFeatures = false - - XCTAssertEqual(vm.visibleFeaturesMatrix.count, 1) - XCTAssertTrue(vm.visibleFeaturesMatrix[0].count <= vm.itemsPerRow) - } - @MainActor func testWhenAskedToPerformActionForDuckPlayerThenItOpensYoutubeVideo() { vm.performAction(for: .duckplayer) @@ -519,7 +498,6 @@ final class ContinueSetUpModelTests: XCTestCase { @MainActor func testThatWhenIfAllFeatureActiveThenVisibleMatrixIsEmpty() { capturingDefaultBrowserProvider.isDefault = true emailStorage.isEmailProtectionEnabled = true - privacyPreferences.autoconsentEnabled = true duckPlayerPreferences.youtubeOverlayAnyButtonPressed = true capturingDataImportProvider.didImport = true userDefaults.set(false, forKey: UserDefaultsWrapper.Key.homePageShowSurveyDay0.rawValue) @@ -530,7 +508,6 @@ final class ContinueSetUpModelTests: XCTestCase { dataImportProvider: capturingDataImportProvider, tabCollectionViewModel: tabCollectionVM, emailManager: emailManager, - privacyPreferences: privacyPreferences, duckPlayerPreferences: duckPlayerPreferences, homePageRemoteMessaging: createMessaging() ) @@ -627,7 +604,6 @@ extension HomePage.Models.ContinueSetUpModel { defaultBrowserProvider: DefaultBrowserProvider = CapturingDefaultBrowserProvider(), dataImportProvider: DataImportStatusProviding = CapturingDataImportProvider(), emailManager: EmailManager = EmailManager(storage: MockEmailStorage()), - privacyPreferences: PrivacySecurityPreferences = PrivacySecurityPreferences.shared, duckPlayerPreferences: DuckPlayerPreferencesPersistor = DuckPlayerPreferencesPersistorMock(), privacyConfig: MockPrivacyConfiguration = MockPrivacyConfiguration(), appGroupUserDefaults: UserDefaults, @@ -667,7 +643,6 @@ extension HomePage.Models.ContinueSetUpModel { dataImportProvider: dataImportProvider, tabCollectionViewModel: TabCollectionViewModel(), emailManager: emailManager, - privacyPreferences: privacyPreferences, duckPlayerPreferences: duckPlayerPreferences, homePageRemoteMessaging: messaging, privacyConfigurationManager: manager, diff --git a/UnitTests/Preferences/AccessibilityPreferencesTests.swift b/UnitTests/Preferences/AccessibilityPreferencesTests.swift new file mode 100644 index 0000000000..819f2c6a0a --- /dev/null +++ b/UnitTests/Preferences/AccessibilityPreferencesTests.swift @@ -0,0 +1,44 @@ +// +// AccessibilityPreferencesTests.swift +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +class MockAccessibilityPreferencesPersistor: AccessibilityPreferencesPersistor { + var defaultPageZoom: CGFloat = DefaultZoomValue.percent100.rawValue +} + +class AccessibilityPreferencesTests: XCTestCase { + + func testWhenInitializedThenItLoadsPersistedDefaultPageZoom() { + let mockPersistor = MockAccessibilityPreferencesPersistor() + mockPersistor.defaultPageZoom = DefaultZoomValue.percent150.rawValue + let accessibilityPreferences = AccessibilityPreferences(persistor: mockPersistor) + + XCTAssertEqual(accessibilityPreferences.defaultPageZoom, DefaultZoomValue.percent150) + } + + func testWhenDefaultPageZoomUpdatedThenPersistorUpdates() { + let mockPersistor = MockAccessibilityPreferencesPersistor() + let accessibilityPreferences = AccessibilityPreferences(persistor: mockPersistor) + accessibilityPreferences.defaultPageZoom = .percent75 + + XCTAssertEqual(mockPersistor.defaultPageZoom, DefaultZoomValue.percent75.rawValue) + } + +} diff --git a/UnitTests/Preferences/AppearancePreferencesTests.swift b/UnitTests/Preferences/AppearancePreferencesTests.swift index 6ffbaec043..34e725fde4 100644 --- a/UnitTests/Preferences/AppearancePreferencesTests.swift +++ b/UnitTests/Preferences/AppearancePreferencesTests.swift @@ -25,20 +25,16 @@ struct AppearancePreferencesPersistorMock: AppearancePreferencesPersistor { var isContinueSetUpVisible: Bool var isRecentActivityVisible: Bool var showFullURL: Bool - var showAutocompleteSuggestions: Bool var currentThemeName: String var favoritesDisplayMode: String? - var defaultPageZoom: CGFloat var showBookmarksBar: Bool var bookmarksBarAppearance: BookmarksBarAppearance var homeButtonPosition: HomeButtonPosition init( showFullURL: Bool = false, - showAutocompleteSuggestions: Bool = true, currentThemeName: String = ThemeName.systemDefault.rawValue, favoritesDisplayMode: String? = FavoritesDisplayMode.displayNative(.desktop).description, - defaultPageZoom: CGFloat = DefaultZoomValue.percent100.rawValue, isContinueSetUpVisible: Bool = true, isFavoriteVisible: Bool = true, isRecentActivityVisible: Bool = true, @@ -47,10 +43,8 @@ struct AppearancePreferencesPersistorMock: AppearancePreferencesPersistor { homeButtonPosition: HomeButtonPosition = .right ) { self.showFullURL = showFullURL - self.showAutocompleteSuggestions = showAutocompleteSuggestions self.currentThemeName = currentThemeName self.favoritesDisplayMode = favoritesDisplayMode - self.defaultPageZoom = defaultPageZoom self.isContinueSetUpVisible = isContinueSetUpVisible self.isFavoriteVisible = isFavoriteVisible self.isRecentActivityVisible = isRecentActivityVisible @@ -66,10 +60,8 @@ final class AppearancePreferencesTests: XCTestCase { var model = AppearancePreferences( persistor: AppearancePreferencesPersistorMock( showFullURL: false, - showAutocompleteSuggestions: true, currentThemeName: ThemeName.systemDefault.rawValue, favoritesDisplayMode: FavoritesDisplayMode.displayNative(.desktop).description, - defaultPageZoom: DefaultZoomValue.percent100.rawValue, isContinueSetUpVisible: true, isFavoriteVisible: true, isRecentActivityVisible: true, @@ -78,10 +70,8 @@ final class AppearancePreferencesTests: XCTestCase { ) XCTAssertEqual(model.showFullURL, false) - XCTAssertEqual(model.showAutocompleteSuggestions, true) XCTAssertEqual(model.currentThemeName, ThemeName.systemDefault) XCTAssertEqual(model.favoritesDisplayMode, .displayNative(.desktop)) - XCTAssertEqual(model.defaultPageZoom, DefaultZoomValue.percent100) XCTAssertEqual(model.isFavoriteVisible, true) XCTAssertEqual(model.isContinueSetUpVisible, true) XCTAssertEqual(model.isRecentActivityVisible, true) @@ -90,10 +80,8 @@ final class AppearancePreferencesTests: XCTestCase { model = AppearancePreferences( persistor: AppearancePreferencesPersistorMock( showFullURL: true, - showAutocompleteSuggestions: false, currentThemeName: ThemeName.light.rawValue, favoritesDisplayMode: FavoritesDisplayMode.displayUnified(native: .desktop).description, - defaultPageZoom: DefaultZoomValue.percent50.rawValue, isContinueSetUpVisible: false, isFavoriteVisible: false, isRecentActivityVisible: false, @@ -101,10 +89,8 @@ final class AppearancePreferencesTests: XCTestCase { ) ) XCTAssertEqual(model.showFullURL, true) - XCTAssertEqual(model.showAutocompleteSuggestions, false) XCTAssertEqual(model.currentThemeName, ThemeName.light) XCTAssertEqual(model.favoritesDisplayMode, .displayUnified(native: .desktop)) - XCTAssertEqual(model.defaultPageZoom, DefaultZoomValue.percent50) XCTAssertEqual(model.isFavoriteVisible, false) XCTAssertEqual(model.isContinueSetUpVisible, false) XCTAssertEqual(model.isRecentActivityVisible, false) @@ -143,18 +129,6 @@ final class AppearancePreferencesTests: XCTestCase { XCTAssertEqual(NSApp.appearance?.name, ThemeName.systemDefault.appearance?.name) } - func testWhenZoomLevelChangedInAppearancePreferencesThenThePersisterAndUserDefaultsZoomValuesAreUpdated() { - UserDefaultsWrapper.clearAll() - let randomZoomLevel = DefaultZoomValue.allCases.randomElement()! - let persister = AppearancePreferencesUserDefaultsPersistor() - let model = AppearancePreferences(persistor: persister) - model.defaultPageZoom = randomZoomLevel - - XCTAssertEqual(persister.defaultPageZoom, randomZoomLevel.rawValue) - let savedZoomValue = UserDefaultsWrapper(key: .defaultPageZoom, defaultValue: DefaultZoomValue.percent100.rawValue).wrappedValue - XCTAssertEqual(savedZoomValue, randomZoomLevel.rawValue) - } - func testWhenNewTabPreferencesAreUpdatedThenPersistedValuesAreUpdated() throws { let model = AppearancePreferences(persistor: AppearancePreferencesPersistorMock()) diff --git a/UnitTests/Preferences/CookiePopupProtectionPreferencesTests.swift b/UnitTests/Preferences/CookiePopupProtectionPreferencesTests.swift new file mode 100644 index 0000000000..fa8cf471cc --- /dev/null +++ b/UnitTests/Preferences/CookiePopupProtectionPreferencesTests.swift @@ -0,0 +1,44 @@ +// +// CookiePopupProtectionPreferencesTests.swift +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +class MockCookiePopupProtectionPreferencesPersistor: CookiePopupProtectionPreferencesPersistor { + var autoconsentEnabled: Bool = false +} + +class CookiePopupProtectionPreferencesTests: XCTestCase { + + func testWhenInitializedThenItLoadsPersistedAutoconsentSetting() { + let mockPersistor = MockCookiePopupProtectionPreferencesPersistor() + mockPersistor.autoconsentEnabled = true + let cookiePopupPreferences = CookiePopupProtectionPreferences(persistor: mockPersistor) + + XCTAssertTrue(cookiePopupPreferences.isAutoconsentEnabled) + } + + func testWhenIsAutoconsentEnabledUpdatedThenPersistorUpdates() { + let mockPersistor = MockCookiePopupProtectionPreferencesPersistor() + let cookiePopupPreferences = CookiePopupProtectionPreferences(persistor: mockPersistor) + cookiePopupPreferences.isAutoconsentEnabled = false + + XCTAssertFalse(mockPersistor.autoconsentEnabled) + } + +} diff --git a/UnitTests/Preferences/DataClearingPreferencesTests.swift b/UnitTests/Preferences/DataClearingPreferencesTests.swift new file mode 100644 index 0000000000..5563fb6e90 --- /dev/null +++ b/UnitTests/Preferences/DataClearingPreferencesTests.swift @@ -0,0 +1,43 @@ +// +// DataClearingPreferencesTests.swift +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +class MockFireButtonPreferencesPersistor: FireButtonPreferencesPersistor { + var loginDetectionEnabled: Bool = false +} + +class DataClearingPreferencesTests: XCTestCase { + + func testWhenInitializedThenItLoadsPersistedLoginDetectionSetting() { + let mockPersistor = MockFireButtonPreferencesPersistor() + mockPersistor.loginDetectionEnabled = true + let dataClearingPreferences = DataClearingPreferences(persistor: mockPersistor) + + XCTAssertTrue(dataClearingPreferences.isLoginDetectionEnabled) + } + + func testWhenIsLoginDetectionEnabledUpdatedThenPersistorUpdates() { + let mockPersistor = MockFireButtonPreferencesPersistor() + let dataClearingPreferences = DataClearingPreferences(persistor: mockPersistor) + dataClearingPreferences.isLoginDetectionEnabled = true + + XCTAssertTrue(mockPersistor.loginDetectionEnabled) + } +} diff --git a/UnitTests/Preferences/PreferencesSidebarModelTests.swift b/UnitTests/Preferences/PreferencesSidebarModelTests.swift index cb9a8f0c60..09c31efdb0 100644 --- a/UnitTests/Preferences/PreferencesSidebarModelTests.swift +++ b/UnitTests/Preferences/PreferencesSidebarModelTests.swift @@ -40,7 +40,7 @@ final class PreferencesSidebarModelTests: XCTestCase { } func testWhenInitializedThenFirstPaneInFirstSectionIsSelected() throws { - let sections: [PreferencesSection] = [.init(id: .regularPreferencePanes, panes: [.appearance, .downloads, .autofill])] + let sections: [PreferencesSection] = [.init(id: .regularPreferencePanes, panes: [.appearance, .autofill])] let model = PreferencesSidebarModel(loadSections: sections) XCTAssertEqual(model.selectedPane, .appearance) @@ -72,7 +72,7 @@ final class PreferencesSidebarModelTests: XCTestCase { } func testWhenSelectPaneIsCalledWithNonexistentPaneThenItHasNoEffect() throws { - let sections: [PreferencesSection] = [.init(id: .regularPreferencePanes, panes: [.appearance, .downloads])] + let sections: [PreferencesSection] = [.init(id: .regularPreferencePanes, panes: [.appearance, .autofill])] let model = PreferencesSidebarModel(loadSections: sections) model.selectPane(.general) @@ -80,7 +80,7 @@ final class PreferencesSidebarModelTests: XCTestCase { } func testWhenSelectedTabIndexIsChangedThenSelectedPaneIsNotAffected() throws { - let sections: [PreferencesSection] = [.init(id: .regularPreferencePanes, panes: [.general, .appearance, .downloads, .autofill])] + let sections: [PreferencesSection] = [.init(id: .regularPreferencePanes, panes: [.general, .appearance, .autofill])] let tabs: [Tab.TabContent] = [.anySettingsPane, .bookmarks] let model = PreferencesSidebarModel(loadSections: sections, tabSwitcherTabs: tabs) diff --git a/UnitTests/Preferences/PrivacyProtectionStatusTests.swift b/UnitTests/Preferences/PrivacyProtectionStatusTests.swift new file mode 100644 index 0000000000..4ec5f7af1f --- /dev/null +++ b/UnitTests/Preferences/PrivacyProtectionStatusTests.swift @@ -0,0 +1,67 @@ +// +// PrivacyProtectionStatusTests.swift +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +import Combine +@testable import DuckDuckGo_Privacy_Browser + +class PrivacyProtectionStatusTests: XCTestCase { + + var cancellables: Set = [] + + override func tearDown() { + super.tearDown() + cancellables.removeAll() + } + + func testWhenStatusUpdatedFromPublisherThenReflectsNewStatus() { + // Example of how you might test updates from a publisher + let subject = PassthroughSubject() + let status = PrivacyProtectionStatus(statusPublisher: subject) { isOn in + return isOn ? .on : .off + } + + // Initial value should be nil + XCTAssertNil(status.status) + + let expectation = XCTestExpectation(description: "Status updates to .on") + status.$status.sink { newStatus in + if newStatus == .on { + expectation.fulfill() + } + }.store(in: &cancellables) + + subject.send(true) + + wait(for: [expectation], timeout: 1.0) + } + + func testWhenInitializedWithStaticStatusIndicatorThenSetsStatusCorrectly() { + // Test initialization with a static status indicator + let status = PrivacyProtectionStatus(statusIndicator: .on) + + XCTAssertEqual(status.status, .on) + } + + func testWhenInitializedWithoutStatusThenStatusIsNil() { + // Test initialization without a status + let status = PrivacyProtectionStatus() + + XCTAssertNil(status.status) + } +} diff --git a/UnitTests/Preferences/SearchPreferencesTests.swift b/UnitTests/Preferences/SearchPreferencesTests.swift new file mode 100644 index 0000000000..60f5e3a87c --- /dev/null +++ b/UnitTests/Preferences/SearchPreferencesTests.swift @@ -0,0 +1,44 @@ +// +// SearchPreferencesTests.swift +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +class MockSearchPreferencesPersistor: SearchPreferencesPersistor { + var showAutocompleteSuggestions: Bool = false +} + +class SearchPreferencesTests: XCTestCase { + + func testWhenInitializedThenItLoadsPersistedValues() { + let mockPersistor = MockSearchPreferencesPersistor() + mockPersistor.showAutocompleteSuggestions = true + let searchPreferences = SearchPreferences(persistor: mockPersistor) + + XCTAssertTrue(searchPreferences.showAutocompleteSuggestions) + } + + func testWhenShowAutocompleteSuggestionsUpdatedThenPersistorUpdates() { + let mockPersistor = MockSearchPreferencesPersistor() + let searchPreferences = SearchPreferences(persistor: mockPersistor) + searchPreferences.showAutocompleteSuggestions = true + + XCTAssertTrue(mockPersistor.showAutocompleteSuggestions) + } + +} diff --git a/UnitTests/Preferences/WebTrackingProtectionPreferencesTests.swift b/UnitTests/Preferences/WebTrackingProtectionPreferencesTests.swift new file mode 100644 index 0000000000..428634e4e8 --- /dev/null +++ b/UnitTests/Preferences/WebTrackingProtectionPreferencesTests.swift @@ -0,0 +1,44 @@ +// +// WebTrackingProtectionPreferencesTests.swift +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +class MockWebTrackingProtectionPreferencesPersistor: WebTrackingProtectionPreferencesPersistor { + var gpcEnabled: Bool = false +} + +class WebTrackingProtectionPreferencesTests: XCTestCase { + + func testWhenInitializedThenItLoadsPersistedGPCSetting() { + let mockPersistor = MockWebTrackingProtectionPreferencesPersistor() + mockPersistor.gpcEnabled = true + let webTrackingPreferences = WebTrackingProtectionPreferences(persistor: mockPersistor) + + XCTAssertTrue(webTrackingPreferences.isGPCEnabled) + } + + func testWhenIsGPCEnabledUpdatedThenPersistorUpdates() { + let mockPersistor = MockWebTrackingProtectionPreferencesPersistor() + let webTrackingPreferences = WebTrackingProtectionPreferences(persistor: mockPersistor) + webTrackingPreferences.isGPCEnabled = true + + XCTAssertTrue(mockPersistor.gpcEnabled) + } + +} diff --git a/UnitTests/Tab/ViewModel/TabViewModelTests.swift b/UnitTests/Tab/ViewModel/TabViewModelTests.swift index 7dd1ce9b73..9dbaa4a0fd 100644 --- a/UnitTests/Tab/ViewModel/TabViewModelTests.swift +++ b/UnitTests/Tab/ViewModel/TabViewModelTests.swift @@ -214,7 +214,7 @@ final class TabViewModelTests: XCTestCase { @MainActor func testThatDefaultValueForTabsWebViewIsOne() { UserDefaultsWrapper.clearAll() - let tabVM = TabViewModel(tab: Tab(), appearancePreferences: AppearancePreferences()) + let tabVM = TabViewModel(tab: Tab(), appearancePreferences: AppearancePreferences(), accessibilityPreferences: AccessibilityPreferences()) XCTAssertEqual(tabVM.tab.webView.zoomLevel, DefaultZoomValue.percent100) } @@ -224,7 +224,7 @@ final class TabViewModelTests: XCTestCase { UserDefaultsWrapper.clearAll() let tabVM = TabViewModel(tab: Tab()) let randomZoomLevel = DefaultZoomValue.allCases.randomElement()! - AppearancePreferences.shared.defaultPageZoom = randomZoomLevel + AccessibilityPreferences.shared.defaultPageZoom = randomZoomLevel XCTAssertEqual(tabVM.tab.webView.zoomLevel, randomZoomLevel) } @@ -233,7 +233,7 @@ final class TabViewModelTests: XCTestCase { func testWhenAppearancePreferencesZoomLevelIsSetAndANewTabIsOpenThenItsWebViewHasTheLatestValueOfZoomLevel() { UserDefaultsWrapper.clearAll() let randomZoomLevel = DefaultZoomValue.allCases.randomElement()! - AppearancePreferences.shared.defaultPageZoom = randomZoomLevel + AccessibilityPreferences.shared.defaultPageZoom = randomZoomLevel let tabVM = TabViewModel(tab: Tab(), appearancePreferences: AppearancePreferences()) diff --git a/UnitTests/Tab/WebViewTests.swift b/UnitTests/Tab/WebViewTests.swift index df9920004e..aedaeccc0d 100644 --- a/UnitTests/Tab/WebViewTests.swift +++ b/UnitTests/Tab/WebViewTests.swift @@ -134,7 +134,7 @@ final class WebViewTests: XCTestCase { let tabVM = TabViewModel(tab: Tab()) let randomZoomLevel = DefaultZoomValue.percent300 // Select Default zoom - AppearancePreferences.shared.defaultPageZoom = randomZoomLevel + AccessibilityPreferences.shared.defaultPageZoom = randomZoomLevel // Zooming out tabVM.tab.webView.zoomOut() @@ -147,7 +147,7 @@ final class WebViewTests: XCTestCase { XCTAssertEqual(tabVM.tab.webView.zoomLevel, randomZoomLevel) // Set new default zoom - AppearancePreferences.shared.defaultPageZoom = .percent75 + AccessibilityPreferences.shared.defaultPageZoom = .percent75 XCTAssertEqual(tabVM.tab.webView.zoomLevel, .percent75) } diff --git a/UnitTests/TabBar/ViewModel/TabCollectionViewModelTests+WithoutPinnedTabsManager.swift b/UnitTests/TabBar/ViewModel/TabCollectionViewModelTests+WithoutPinnedTabsManager.swift index 091bd0d2b1..875c7746a4 100644 --- a/UnitTests/TabBar/ViewModel/TabCollectionViewModelTests+WithoutPinnedTabsManager.swift +++ b/UnitTests/TabBar/ViewModel/TabCollectionViewModelTests+WithoutPinnedTabsManager.swift @@ -147,9 +147,9 @@ extension TabCollectionViewModelTests { tabCollectionViewModel.tabCollection.append(tab: .init(content: .settings(pane: .appearance))) tabCollectionViewModel.tabCollection.append(tab: .init(content: .newtab)) - XCTAssertTrue(tabCollectionViewModel.selectDisplayableTabIfPresent(.settings(pane: .privacy))) + XCTAssertTrue(tabCollectionViewModel.selectDisplayableTabIfPresent(.settings(pane: .general))) XCTAssert(tabCollectionViewModel.selectedTabViewModel === tabCollectionViewModel.tabViewModel(at: 1)) - XCTAssertEqual(tabCollectionViewModel.selectedTabViewModel?.tab.content, .settings(pane: .privacy)) + XCTAssertEqual(tabCollectionViewModel.selectedTabViewModel?.tab.content, .settings(pane: .general)) } func test_WithoutPinnedTabsManager_WhenPreferencesTabIsPresentThenOpeningPreferencesWithAnyPaneDoesNotUpdatePaneOnExistingTab() { diff --git a/UnitTests/TabBar/ViewModel/TabCollectionViewModelTests.swift b/UnitTests/TabBar/ViewModel/TabCollectionViewModelTests.swift index c27417fba0..efc39feac0 100644 --- a/UnitTests/TabBar/ViewModel/TabCollectionViewModelTests.swift +++ b/UnitTests/TabBar/ViewModel/TabCollectionViewModelTests.swift @@ -138,9 +138,9 @@ final class TabCollectionViewModelTests: XCTestCase { tabCollectionViewModel.tabCollection.append(tab: .init(content: .settings(pane: .appearance))) tabCollectionViewModel.tabCollection.append(tab: .init(content: .newtab)) - XCTAssertTrue(tabCollectionViewModel.selectDisplayableTabIfPresent(.settings(pane: .privacy))) + XCTAssertTrue(tabCollectionViewModel.selectDisplayableTabIfPresent(.settings(pane: .general))) XCTAssert(tabCollectionViewModel.selectedTabViewModel === tabCollectionViewModel.tabViewModel(at: 1)) - XCTAssertEqual(tabCollectionViewModel.selectedTabViewModel?.tab.content, .settings(pane: .privacy)) + XCTAssertEqual(tabCollectionViewModel.selectedTabViewModel?.tab.content, .settings(pane: .general)) } func testWhenPreferencesTabIsPresentThenOpeningPreferencesWithAnyPaneDoesNotUpdatePaneOnExistingTab() { From cf1e711f7a47deab1d15381bd4568f615a365b62 Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Tue, 19 Mar 2024 19:52:54 +0100 Subject: [PATCH 40/51] Display error messaging for cancelled subscriptions (#2394) Task/Issue URL: https://app.asana.com/0/72649045549333/1205438842252963/f Description: When we detect that entitlements have expire AND Netp is enabled: Disable NetP Show revoked notification and make sure that Netp disabled notification is NOT shown. On opening the status view we show the revoked dialog. When we get 403 response from /register (or any controller APIs), we handle the expired entitlement (refer to first bullet point in this section). (Handled by the iOS equivalent project When rekey is attempted with invalid entitlements, we will get 403. (Handled by the iOS equivalent project If user attempts to enable Netp with invalid entitlements, we will get 403. (Handled by the iOS equivalent project We do a periodic check every 20 minutes calling the subscription API to get entitlements (Handled by the NetworkProtectionEntitlementsMonitor implemented in the iOS equivalent project ) If check succeeds and user has entitlement, we do nothing. If check succeeds and user has no entitlement, we handle the expired entitlement (refer to first bullet point in this section). If check fails (no internet, BE failure, tunnel failure), we do nothing. If Netp is disabled, we stop this check. On top of this, we also added an additional check on the Settings page. To correctly show the Netp entry, we check for entitlement through subscription API whenever the Settings page is foregrounded. If check succeeds and user has entitlement, we show the Netp settings entry. If check succeeds and user has no entitlement, we handle the expired entitlement (refer to first bullet point in this section) and also hide Netp settings entry. If check fails (no internet, be failure, tunnel failure), we retain whatever the previous visibility of the Settings entry if available, else hide entry. --- .../NetworkProtection/DuckDuckGoVPN.xcconfig | 8 +- .../NetworkProtectionSystemExtension.xcconfig | 8 +- DuckDuckGo.xcodeproj/project.pbxproj | 31 ++++++-- .../xcshareddata/swiftpm/Package.resolved | 4 +- DuckDuckGo/Application/AppDelegate.swift | 3 +- DuckDuckGo/Application/URLEventHandler.swift | 6 +- .../MainWindow/MainViewController.swift | 2 +- .../View/NavigationBarViewController.swift | 16 ++-- .../AppLauncher.swift | 3 + ...rkProtection+ConvenienceInitializers.swift | 29 +++++++- .../NetworkProtectionAppEvents.swift | 2 + ...etworkProtectionNavBarPopoverManager.swift | 30 +++++--- .../NetworkProtectionTunnelController.swift | 27 ++++++- .../VPNLocation/VPNLocationViewModel.swift | 12 --- ...rkProtectionSubscriptionEventHandler.swift | 43 ++++++++--- ...rkProtectionUNNotificationsPresenter.swift | 12 ++- ...UserText+NetworkProtectionExtensions.swift | 2 + .../MacPacketTunnelProvider.swift | 34 +++++++-- ...ore+SubscriptionTokenKeychainStorage.swift | 45 +++++++++++ .../Model/PreferencesSidebarModel.swift | 21 +++++- .../AccountManagerExtension.swift | 1 + .../SubscriptionFeatureAvailability.swift | 29 +++++--- .../NetworkProtectionFeatureDisabler.swift | 13 +--- .../NetworkProtectionFeatureVisibility.swift | 5 ++ .../DuckDuckGoNotificationsAppDelegate.swift | 12 +++ DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift | 52 ++++++++++++- ...tion+VPNAgentConvenienceInitializers.swift | 33 +++++++++ DuckDuckGoVPN/NetworkProtectionBouncer.swift | 26 ++++++- DuckDuckGoVPN/VPNUninstaller.swift | 60 +++++++++++++++ LocalPackages/LoginItems/Package.swift | 1 + .../Sources/LoginItems/LoginItem.swift | 4 + .../AppLaunching/AppLaunching.swift | 1 + ...serDefault+ShowVPNUninstalledMessage.swift | 44 +++++++++++ .../UserDefault+VPNEnabledViaWaitlist.swift | 44 +++++++++++ ...NetworkProtectionExpiredEntitlements.swift | 44 +++++++++++ .../UserText+NetworkProtectionUI.swift | 7 ++ .../Menu/StatusBarMenu.swift | 9 ++- .../NetworkProtectionPopover.swift | 8 +- .../NetworkProtectionStatusView.swift | 9 ++- .../NetworkProtectionStatusViewModel.swift | 37 +++++++++- .../SubscriptionExpiredView.swift | 74 +++++++++++++++++++ .../NetworkProtectionStatusBarMenuTests.swift | 8 +- LocalPackages/SyncUI/Package.swift | 1 + .../SystemExtensionManager/Package.swift | 1 + ...rotectionAgentNotificationsPresenter.swift | 8 +- 45 files changed, 756 insertions(+), 113 deletions(-) create mode 100644 DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift create mode 100644 DuckDuckGoVPN/NetworkProtection+VPNAgentConvenienceInitializers.swift create mode 100644 DuckDuckGoVPN/VPNUninstaller.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserDefault+ShowVPNUninstalledMessage.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserDefault+VPNEnabledViaWaitlist.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserDefaults+NetworkProtectionExpiredEntitlements.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/SubscriptionExpiredView/SubscriptionExpiredView.swift diff --git a/Configuration/App/NetworkProtection/DuckDuckGoVPN.xcconfig b/Configuration/App/NetworkProtection/DuckDuckGoVPN.xcconfig index 662be4ddeb..b3b7f7ef35 100644 --- a/Configuration/App/NetworkProtection/DuckDuckGoVPN.xcconfig +++ b/Configuration/App/NetworkProtection/DuckDuckGoVPN.xcconfig @@ -49,10 +49,10 @@ PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*] = PROVISIONING_PROFILE_SPECIFIER[config=Review][sdk=macosx*] = macOS NetP VPN App - Review (XPC) PROVISIONING_PROFILE_SPECIFIER[config=Release][sdk=macosx*] = macOS NetP VPN App - Release (XPC) -FEATURE_FLAGS[arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_PROTECTION -FEATURE_FLAGS[config=CI][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_PROTECTION -FEATURE_FLAGS[config=Debug][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_PROTECTION -FEATURE_FLAGS[config=Review][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_PROTECTION +FEATURE_FLAGS[arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_PROTECTION SUBSCRIPTION +FEATURE_FLAGS[config=CI][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_PROTECTION SUBSCRIPTION +FEATURE_FLAGS[config=Debug][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_PROTECTION SUBSCRIPTION +FEATURE_FLAGS[config=Review][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_PROTECTION SUBSCRIPTION SWIFT_OBJC_BRIDGING_HEADER = SKIP_INSTALL = YES diff --git a/Configuration/Extensions/NetworkProtection/NetworkProtectionSystemExtension.xcconfig b/Configuration/Extensions/NetworkProtection/NetworkProtectionSystemExtension.xcconfig index 9cc1c7ae28..325f9024b7 100644 --- a/Configuration/Extensions/NetworkProtection/NetworkProtectionSystemExtension.xcconfig +++ b/Configuration/Extensions/NetworkProtection/NetworkProtectionSystemExtension.xcconfig @@ -31,10 +31,10 @@ INFOPLIST_FILE = NetworkProtectionSystemExtension/Info.plist INFOPLIST_KEY_NSHumanReadableCopyright = Copyright © 2023 DuckDuckGo. All rights reserved. INFOPLIST_KEY_NSSystemExtensionUsageDescription = DuckDuckGo VPN -FEATURE_FLAGS[arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_EXTENSION NETWORK_PROTECTION -FEATURE_FLAGS[config=CI][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_EXTENSION NETWORK_PROTECTION -FEATURE_FLAGS[config=Debug][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_EXTENSION NETWORK_PROTECTION -FEATURE_FLAGS[config=Review][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_EXTENSION NETWORK_PROTECTION +FEATURE_FLAGS[arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_EXTENSION NETWORK_PROTECTION SUBSCRIPTION +FEATURE_FLAGS[config=CI][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_EXTENSION NETWORK_PROTECTION SUBSCRIPTION +FEATURE_FLAGS[config=Debug][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_EXTENSION NETWORK_PROTECTION SUBSCRIPTION +FEATURE_FLAGS[config=Review][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_EXTENSION NETWORK_PROTECTION SUBSCRIPTION PRODUCT_BUNDLE_IDENTIFIER[sdk=*] = $(SYSEX_BUNDLE_ID) PRODUCT_BUNDLE_IDENTIFIER[config=CI][sdk=*] = $(SYSEX_BUNDLE_ID) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 66a3b706f0..1596da5917 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2278,8 +2278,6 @@ 7BA7CC562AD11FFB0042E5CE /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; 7BA7CC582AD1203A0042E5CE /* UserText+NetworkProtection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60D22A0C84F700BCD287 /* UserText+NetworkProtection.swift */; }; 7BA7CC592AD1203B0042E5CE /* UserText+NetworkProtection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60D22A0C84F700BCD287 /* UserText+NetworkProtection.swift */; }; - 7BA7CC5A2AD120640042E5CE /* NetworkProtection+ConvenienceInitializers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60692A0B29FA00BCD287 /* NetworkProtection+ConvenienceInitializers.swift */; }; - 7BA7CC5B2AD120640042E5CE /* NetworkProtection+ConvenienceInitializers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60692A0B29FA00BCD287 /* NetworkProtection+ConvenienceInitializers.swift */; }; 7BA7CC5C2AD120C30042E5CE /* EventMapping+NetworkProtectionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60722A0B29FA00BCD287 /* EventMapping+NetworkProtectionError.swift */; }; 7BA7CC5D2AD120C30042E5CE /* EventMapping+NetworkProtectionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60722A0B29FA00BCD287 /* EventMapping+NetworkProtectionError.swift */; }; 7BA7CC5F2AD1210C0042E5CE /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = 7BA7CC5E2AD1210C0042E5CE /* Networking */; }; @@ -3223,6 +3221,11 @@ EE0629762B90EE8C00D868B4 /* AccountManagerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0629712B90EE8C00D868B4 /* AccountManagerExtension.swift */; }; EE2F9C5B2B90F2FF00D45FC9 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = EE2F9C5A2B90F2FF00D45FC9 /* Subscription */; }; EE339228291BDEFD009F62C1 /* JSAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE339227291BDEFD009F62C1 /* JSAlertController.swift */; }; + EE3424602BA0853900173B1B /* VPNUninstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE34245D2BA0853900173B1B /* VPNUninstaller.swift */; }; + EE3424612BA0853900173B1B /* VPNUninstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE34245D2BA0853900173B1B /* VPNUninstaller.swift */; }; + EE66418C2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE66418B2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift */; }; + EE66418D2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE66418B2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift */; }; + EE66418F2B9B1BD1005BCD17 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = EE66418E2B9B1BD1005BCD17 /* Subscription */; }; EE66666F2B56EDE4001D898D /* VPNLocationsHostingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE66666E2B56EDE4001D898D /* VPNLocationsHostingViewController.swift */; }; EE6666702B56EDE4001D898D /* VPNLocationsHostingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE66666E2B56EDE4001D898D /* VPNLocationsHostingViewController.swift */; }; EE6666712B56EDE4001D898D /* VPNLocationsHostingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE66666E2B56EDE4001D898D /* VPNLocationsHostingViewController.swift */; }; @@ -3258,6 +3261,8 @@ EEC8EB402982CD550065AA39 /* JSAlertViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF53E172950CED5002D78F4 /* JSAlertViewModelTests.swift */; }; EECE10E529DD77E60044D027 /* FeatureFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = EECE10E429DD77E60044D027 /* FeatureFlag.swift */; }; EECE10E629DD77E60044D027 /* FeatureFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = EECE10E429DD77E60044D027 /* FeatureFlag.swift */; }; + EEDE50112BA360C80017F3C4 /* NetworkProtection+VPNAgentConvenienceInitializers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEDE50102BA360C80017F3C4 /* NetworkProtection+VPNAgentConvenienceInitializers.swift */; }; + EEDE50122BA360C80017F3C4 /* NetworkProtection+VPNAgentConvenienceInitializers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEDE50102BA360C80017F3C4 /* NetworkProtection+VPNAgentConvenienceInitializers.swift */; }; EEF12E6F2A2111880023E6BF /* MacPacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */; }; EEF53E182950CED5002D78F4 /* JSAlertViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF53E172950CED5002D78F4 /* JSAlertViewModelTests.swift */; }; F1D43AEE2B98D8DF00BAB743 /* MainMenuActions+VanillaBrowser.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D43AED2B98D8DF00BAB743 /* MainMenuActions+VanillaBrowser.swift */; }; @@ -4637,6 +4642,8 @@ EAFAD6C92728BD1200F9DF00 /* clickToLoad.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = clickToLoad.js; sourceTree = ""; }; EE0629712B90EE8C00D868B4 /* AccountManagerExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountManagerExtension.swift; sourceTree = ""; }; EE339227291BDEFD009F62C1 /* JSAlertController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSAlertController.swift; sourceTree = ""; }; + EE34245D2BA0853900173B1B /* VPNUninstaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNUninstaller.swift; sourceTree = ""; }; + EE66418B2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift"; sourceTree = ""; }; EE66666E2B56EDE4001D898D /* VPNLocationsHostingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNLocationsHostingViewController.swift; sourceTree = ""; }; EEA3EEB02B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionVPNCountryLabelsModel.swift; sourceTree = ""; }; EEA3EEB22B24EC0600E8333A /* VPNLocationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VPNLocationViewModel.swift; sourceTree = ""; }; @@ -4647,6 +4654,7 @@ EEC4A66C2B2C894F00F7C0AA /* VPNLocationPreferenceItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNLocationPreferenceItemModel.swift; sourceTree = ""; }; EEC4A6702B2C90AB00F7C0AA /* VPNLocationPreferenceItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNLocationPreferenceItem.swift; sourceTree = ""; }; EECE10E429DD77E60044D027 /* FeatureFlag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureFlag.swift; sourceTree = ""; }; + EEDE50102BA360C80017F3C4 /* NetworkProtection+VPNAgentConvenienceInitializers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NetworkProtection+VPNAgentConvenienceInitializers.swift"; sourceTree = ""; }; EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacPacketTunnelProvider.swift; sourceTree = ""; }; EEF53E172950CED5002D78F4 /* JSAlertViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSAlertViewModelTests.swift; sourceTree = ""; }; F1D43AED2B98D8DF00BAB743 /* MainMenuActions+VanillaBrowser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MainMenuActions+VanillaBrowser.swift"; sourceTree = ""; }; @@ -4734,6 +4742,7 @@ buildActionMask = 2147483647; files = ( 37269F012B332FC8005E8E46 /* Common in Frameworks */, + EE66418F2B9B1BD1005BCD17 /* Subscription in Frameworks */, EE7295E92A545BC4008C0991 /* NetworkProtection in Frameworks */, 4B2537772A11BFE100610219 /* PixelKit in Frameworks */, 7BBE2B7B2B61663C00697445 /* NetworkProtectionProxy in Frameworks */, @@ -5761,6 +5770,7 @@ 4B41ED9F2B15437A001EEDF4 /* NetworkProtectionNotificationsPresenterFactory.swift */, EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */, 7B0099802B65C6B300FE7C31 /* MacTransparentProxyProvider.swift */, + EE66418B2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift */, ); path = NetworkExtensionTargets; sourceTree = ""; @@ -6414,9 +6424,11 @@ 7BA7CC132AD11DC80042E5CE /* AppLauncher+DefaultInitializer.swift */, 7BA7CC0E2AD11DC80042E5CE /* DuckDuckGoVPNAppDelegate.swift */, 7BD1688D2AD4A4C400D24876 /* NetworkExtensionController.swift */, + EEDE50102BA360C80017F3C4 /* NetworkProtection+VPNAgentConvenienceInitializers.swift */, 7BA7CC152AD11DC80042E5CE /* NetworkProtectionBouncer.swift */, 7B8DB3192B504D7500EC16DA /* VPNAppEventsHandler.swift */, 7B0694972B6E980F00FA4DBA /* VPNProxyLauncher.swift */, + EE34245D2BA0853900173B1B /* VPNUninstaller.swift */, 7BA7CC112AD11DC80042E5CE /* TunnelControllerIPCService.swift */, 7BA7CC172AD11DC80042E5CE /* UserText.swift */, 7BA7CC122AD11DC80042E5CE /* Assets.xcassets */, @@ -8800,6 +8812,7 @@ EE7295E82A545BC4008C0991 /* NetworkProtection */, 37269F002B332FC8005E8E46 /* Common */, 7BBE2B7A2B61663C00697445 /* NetworkProtectionProxy */, + EE66418E2B9B1BD1005BCD17 /* Subscription */, ); productName = NetworkProtectionSystemExtension; productReference = 4B25375A2A11BE7300610219 /* com.duckduckgo.macos.vpn.network-extension.debug.systemextension */; @@ -11067,6 +11080,7 @@ files = ( 4B25377A2A11C01700610219 /* UserText+NetworkProtectionExtensions.swift in Sources */, B65DA5F42A77D3FA00CBEE8D /* BundleExtension.swift in Sources */, + EE66418D2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift in Sources */, 4B2D062D2A11C12300DE1F49 /* Logging.swift in Sources */, 7B2E52252A5FEC09000C6D39 /* NetworkProtectionAgentNotificationsPresenter.swift in Sources */, B602E8232A1E260E006D261F /* Bundle+NetworkProtectionExtensions.swift in Sources */, @@ -11090,10 +11104,10 @@ files = ( B6F92BA22A691580002ABA6B /* UserDefaultsWrapper.swift in Sources */, 4B2D065B2A11D1FF00DE1F49 /* Logging.swift in Sources */, - 7BA7CC5B2AD120640042E5CE /* NetworkProtection+ConvenienceInitializers.swift in Sources */, 7BA7CC3A2AD11E2D0042E5CE /* DuckDuckGoVPNAppDelegate.swift in Sources */, 7BAF9E4C2A8A3CCA002D3B6E /* UserDefaults+NetworkProtectionShared.swift in Sources */, 7BA7CC592AD1203B0042E5CE /* UserText+NetworkProtection.swift in Sources */, + EEDE50112BA360C80017F3C4 /* NetworkProtection+VPNAgentConvenienceInitializers.swift in Sources */, 7BA7CC562AD11FFB0042E5CE /* NetworkProtectionOptionKeyExtension.swift in Sources */, 7B2DDCFA2A93B25F0039D884 /* KeychainType+ClientDefault.swift in Sources */, 7BA7CC4C2AD11EC70042E5CE /* NetworkProtectionControllerErrorStore.swift in Sources */, @@ -11104,6 +11118,7 @@ EE0629742B90EE8C00D868B4 /* AccountManagerExtension.swift in Sources */, 7BA7CC5D2AD120C30042E5CE /* EventMapping+NetworkProtectionError.swift in Sources */, 7BA7CC4A2AD11EA00042E5CE /* NetworkProtectionTunnelController.swift in Sources */, + EE3424602BA0853900173B1B /* VPNUninstaller.swift in Sources */, 7BD1688E2AD4A4C400D24876 /* NetworkExtensionController.swift in Sources */, 7BA7CC3E2AD11E380042E5CE /* TunnelControllerIPCService.swift in Sources */, 7BA7CC402AD11E3D0042E5CE /* AppLauncher+DefaultInitializer.swift in Sources */, @@ -11127,9 +11142,9 @@ 4BA7C4DB2B3F63AE00AFE511 /* NetworkExtensionController.swift in Sources */, 4B2D067C2A13340900DE1F49 /* Logging.swift in Sources */, 7B1459552B7D438F00047F2C /* VPNProxyLauncher.swift in Sources */, + EEDE50122BA360C80017F3C4 /* NetworkProtection+VPNAgentConvenienceInitializers.swift in Sources */, B6F92BAD2A6937B5002ABA6B /* OptionalExtension.swift in Sources */, 4BA7C4D92B3F61FB00AFE511 /* BundleExtension.swift in Sources */, - 7BA7CC5A2AD120640042E5CE /* NetworkProtection+ConvenienceInitializers.swift in Sources */, EEC589DC2A4F1CE800BCD60C /* AppLauncher.swift in Sources */, 7BA7CC3F2AD11E3D0042E5CE /* AppLauncher+DefaultInitializer.swift in Sources */, 4B0EF7292B5780EB009D6481 /* VPNAppEventsHandler.swift in Sources */, @@ -11138,6 +11153,7 @@ EE0629752B90EE8C00D868B4 /* AccountManagerExtension.swift in Sources */, 7BA7CC582AD1203A0042E5CE /* UserText+NetworkProtection.swift in Sources */, 7BA7CC4B2AD11EC60042E5CE /* NetworkProtectionControllerErrorStore.swift in Sources */, + EE3424612BA0853900173B1B /* VPNUninstaller.swift in Sources */, 4BF0E5152AD25A2600FFEC9E /* DuckDuckGoUserAgent.swift in Sources */, 7BFE95592A9DF2AF0081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift in Sources */, 7BA7CC5C2AD120C30042E5CE /* EventMapping+NetworkProtectionError.swift in Sources */, @@ -11175,6 +11191,7 @@ 4B41EDA02B15437A001EEDF4 /* NetworkProtectionNotificationsPresenterFactory.swift in Sources */, 7B25856C2BA2F2D000D49F79 /* AppLauncher.swift in Sources */, 4B4D609F2A0B2C7300BCD287 /* Logging.swift in Sources */, + EE66418C2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift in Sources */, 7B7DFB202B7E736B009EA1A3 /* MacPacketTunnelProvider.swift in Sources */, 4B4D60A12A0B2D6100BCD287 /* NetworkProtectionOptionKeyExtension.swift in Sources */, B602E8182A1E2570006D261F /* URL+NetworkProtection.swift in Sources */, @@ -14074,7 +14091,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 126.1.0; + version = 126.2.0; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { @@ -14774,6 +14791,10 @@ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = Subscription; }; + EE66418E2B9B1BD1005BCD17 /* Subscription */ = { + isa = XCSwiftPackageProductDependency; + productName = Subscription; + }; EE7295E22A545B9A008C0991 /* NetworkProtection */ = { isa = XCSwiftPackageProductDependency; package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7a11e2163c..7d241ed988 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "f4894b9c00dd7514c66d6b929c12315e0cd9c151", - "version" : "126.1.0" + "branch" : "graeme/expired-entitlements-stuff", + "revision" : "925d0dd50e47f38c7fe922622002e8961569bc32" } }, { diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index d0aa5916c6..e4f938425a 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -82,7 +82,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, FileDownloadManagerDel var privacyDashboardWindow: NSWindow? #if NETWORK_PROTECTION && SUBSCRIPTION - private let networkProtectionSubscriptionEventHandler = NetworkProtectionSubscriptionEventHandler() + // Needs to be lazy as indirectly depends on AppDelegate + private lazy var networkProtectionSubscriptionEventHandler = NetworkProtectionSubscriptionEventHandler() #endif #if DBP && SUBSCRIPTION diff --git a/DuckDuckGo/Application/URLEventHandler.swift b/DuckDuckGo/Application/URLEventHandler.swift index d73a1ea414..e0e752ce6f 100644 --- a/DuckDuckGo/Application/URLEventHandler.swift +++ b/DuckDuckGo/Application/URLEventHandler.swift @@ -143,8 +143,12 @@ final class URLEventHandler { case AppLaunchCommand.showVPNLocations.launchURL: WindowControllersManager.shared.showPreferencesTab(withSelectedPane: .vpn) WindowControllersManager.shared.showLocationPickerSheet() - case AppLaunchCommand.moveAppToApplications.launchURL: +#if SUBSCRIPTION + case AppLaunchCommand.showPrivacyPro.launchURL: + WindowControllersManager.shared.showTab(with: .subscription(.subscriptionPurchase)) +#endif #if !APPSTORE && !DEBUG + case AppLaunchCommand.moveAppToApplications.launchURL: // this should be run after NSApplication.shared is set PFMoveToApplicationsFolderIfNecessary(false) #endif diff --git a/DuckDuckGo/MainWindow/MainViewController.swift b/DuckDuckGo/MainWindow/MainViewController.swift index 8dae1a9c7f..e5950d0231 100644 --- a/DuckDuckGo/MainWindow/MainViewController.swift +++ b/DuckDuckGo/MainWindow/MainViewController.swift @@ -75,7 +75,7 @@ final class MainViewController: NSViewController { let ipcClient = TunnelControllerIPCClient(machServiceName: vpnBundleID) ipcClient.register() - return NetworkProtectionNavBarPopoverManager(ipcClient: ipcClient) + return NetworkProtectionNavBarPopoverManager(ipcClient: ipcClient, networkProtectionFeatureDisabler: NetworkProtectionFeatureDisabler()) }() let networkProtectionStatusReporter: NetworkProtectionStatusReporter = { var connectivityIssuesObserver: ConnectivityIssueObserver! diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index aa050d3a98..fbe69492b5 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -438,14 +438,20 @@ final class NavigationBarViewController: NSViewController { object: nil) #if NETWORK_PROTECTION - NotificationCenter.default.addObserver(self, - selector: #selector(showVPNUninstalledFeedback(_:)), - name: NetworkProtectionFeatureDisabler.vpnUninstalledNotificationName, - object: nil) + UserDefaults.netP + .publisher(for: \.networkProtectionShouldShowVPNUninstalledMessage) + .receive(on: DispatchQueue.main) + .sink { [weak self] shouldShowUninstalledMessage in + if shouldShowUninstalledMessage { + self?.showVPNUninstalledFeedback() + UserDefaults.netP.networkProtectionShouldShowVPNUninstalledMessage = false + } + } + .store(in: &cancellables) #endif } - @objc private func showVPNUninstalledFeedback(_ sender: Notification) { + @objc private func showVPNUninstalledFeedback() { guard view.window?.isKeyWindow == true else { return } DispatchQueue.main.async { diff --git a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/AppLauncher.swift b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/AppLauncher.swift index 0ec2416135..b85ed68d8b 100644 --- a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/AppLauncher.swift +++ b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/AppLauncher.swift @@ -36,6 +36,7 @@ extension AppLaunchCommand { case .showVPNLocations: return "showVPNLocations" case .enableOnDemand: return "enableOnDemand" case .moveAppToApplications: return "moveAppToApplications" + case .showPrivacyPro: return "showPrivacyPro" } } } @@ -97,6 +98,8 @@ extension AppLaunchCommand { return "networkprotection://show-settings/locations" case .moveAppToApplications: return "networkprotection://move-app-to-applications" + case .showPrivacyPro: + return "networkprotection://show-privacy-pro" default: return nil } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift index fcdb50ff75..8eed556fac 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift @@ -22,6 +22,10 @@ import Foundation import NetworkProtection import Common +#if SUBSCRIPTION +import Subscription +#endif + extension NetworkProtectionDeviceManager { static func create() -> NetworkProtectionDeviceManager { @@ -32,7 +36,7 @@ extension NetworkProtectionDeviceManager { tokenStore: tokenStore, keyStore: keyStore, errorEvents: .networkProtectionAppDebugEvents, - isSubscriptionEnabled: false) + isSubscriptionEnabled: DefaultSubscriptionFeatureAvailability().isFeatureAvailable()) } } @@ -42,16 +46,21 @@ extension NetworkProtectionCodeRedemptionCoordinator { self.init(environment: settings.selectedEnvironment, tokenStore: NetworkProtectionKeychainTokenStore(), errorEvents: .networkProtectionAppDebugEvents, - isSubscriptionEnabled: false) + isSubscriptionEnabled: DefaultSubscriptionFeatureAvailability().isFeatureAvailable()) } } extension NetworkProtectionKeychainTokenStore { convenience init() { +#if SUBSCRIPTION + let accessTokenProvider: () -> String? = { AccountManager().accessToken } +#else + let accessTokenProvider: () -> String? = { return nil } +#endif self.init(keychainType: .default, errorEvents: .networkProtectionAppDebugEvents, - isSubscriptionEnabled: false, - accessTokenProvider: { nil }) + isSubscriptionEnabled: DefaultSubscriptionFeatureAvailability().isFeatureAvailable(), + accessTokenProvider: accessTokenProvider) } } @@ -62,4 +71,16 @@ extension NetworkProtectionKeychainKeyStore { } } +extension NetworkProtectionLocationListCompositeRepository { + convenience init() { + let settings = VPNSettings(defaults: .netP) + self.init( + environment: settings.selectedEnvironment, + tokenStore: NetworkProtectionKeychainTokenStore(), + errorEvents: .networkProtectionAppDebugEvents, + isSubscriptionEnabled: DefaultSubscriptionFeatureAvailability().isFeatureAvailable() + ) + } +} + #endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift index ce62ed2734..23d20a89d2 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift @@ -72,9 +72,11 @@ final class NetworkProtectionAppEvents { /// func applicationDidBecomeActive() { guard featureVisibility.isNetworkProtectionVisible() else { + UserDefaults.netP.networkProtectionVPNEnabledViaWaitlist = false featureVisibility.disableForAllUsers() return } + UserDefaults.netP.networkProtectionVPNEnabledViaWaitlist = true } private func restartNetworkProtectionIfVersionChanged(using loginItemsManager: LoginItemsManager) { diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift index 16853163e8..331892d0f5 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift @@ -24,6 +24,10 @@ import NetworkProtection import NetworkProtectionIPC import NetworkProtectionUI +#if SUBSCRIPTION +import Subscription +#endif + #if NETWORK_PROTECTION protocol NetworkProtectionIPCClient { @@ -34,6 +38,7 @@ protocol NetworkProtectionIPCClient { func start() func stop() } + extension TunnelControllerIPCClient: NetworkProtectionIPCClient { public var ipcStatusObserver: any NetworkProtection.ConnectionStatusObserver { connectionStatusObserver } public var ipcServerInfoObserver: any NetworkProtection.ConnectionServerInfoObserver { serverInfoObserver } @@ -43,9 +48,12 @@ extension TunnelControllerIPCClient: NetworkProtectionIPCClient { final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { private var networkProtectionPopover: NetworkProtectionPopover? let ipcClient: NetworkProtectionIPCClient + let networkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling - init(ipcClient: NetworkProtectionIPCClient) { + init(ipcClient: TunnelControllerIPCClient, + networkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling) { self.ipcClient = ipcClient + self.networkProtectionFeatureDisabler = networkProtectionFeatureDisabler } var isShown: Bool { @@ -56,15 +64,8 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { #endif } - private func show(_ popover: NSPopover, positionedBelow view: NSView) { - view.isHidden = false - - popover.show(positionedBelow: view.bounds.insetFromLineOfDeath(flipped: view.isFlipped), in: view) - } - // swiftlint:disable:next function_body_length func show(positionedBelow view: NSView, withDelegate delegate: NSPopoverDelegate) { - let popover = networkProtectionPopover ?? { let controller = NetworkProtectionIPCTunnelController(ipcClient: ipcClient) @@ -117,8 +118,11 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { } }, agentLoginItem: LoginItem.vpnMenu, - isMenuBarStatusView: false - ) + isMenuBarStatusView: false, + userDefaults: .netP, + uninstallHandler: { [weak self] in + _ = await self?.networkProtectionFeatureDisabler.disable(keepAuthToken: false, uninstallSystemExtension: true) + }) popover.delegate = delegate networkProtectionPopover = popover @@ -128,6 +132,12 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { show(popover, positionedBelow: view) } + private func show(_ popover: NSPopover, positionedBelow view: NSView) { + view.isHidden = false + + popover.show(positionedBelow: view.bounds.insetFromLineOfDeath(flipped: view.isFlipped), in: view) + } + func toggle(positionedBelow view: NSView, withDelegate delegate: NSPopoverDelegate) { if let networkProtectionPopover, networkProtectionPopover.isShown { networkProtectionPopover.close() diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift index 619da111ff..8ecf83100d 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift @@ -34,6 +34,10 @@ import SystemExtensionManager import SystemExtensions #endif +#if SUBSCRIPTION +import Subscription +#endif + typealias NetworkProtectionStatusChangeHandler = (NetworkProtection.ConnectionStatus) -> Void typealias NetworkProtectionConfigChangeHandler = () -> Void @@ -72,6 +76,12 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr /// Auth token store private let tokenStore: NetworkProtectionTokenStore +#if SUBSCRIPTION + // MARK: - Subscriptions + + private let accountManager = AccountManager() +#endif + // MARK: - Debug Options Support private let networkExtensionBundleID: String @@ -538,7 +548,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr var options = [String: NSObject]() options[NetworkProtectionOptionKey.activationAttemptId] = UUID().uuidString as NSString - guard let authToken = try tokenStore.fetchToken() as NSString? else { + guard let authToken = try fetchAuthToken() else { throw StartError.noAuthToken } options[NetworkProtectionOptionKey.authToken] = authToken @@ -738,6 +748,21 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr throw TunnelFailureError(errorDescription: errorMessage.value) } } + + private func fetchAuthToken() throws -> NSString? { +#if SUBSCRIPTION + if let accessToken = accountManager.accessToken { + os_log(.error, log: .networkProtection, "🟢 TunnelController found token: %{public}d", accessToken) + return Self.adaptAccessTokenForVPN(accessToken) as NSString? + } +#endif + os_log(.error, log: .networkProtection, "🔴 TunnelController found no token :(") + return try tokenStore.fetchToken() as NSString? + } + + private static func adaptAccessTokenForVPN(_ token: String) -> String { + "ddg:\(token)" + } } #endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift index fb1b74c2ff..24dfd9c4ab 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift @@ -185,18 +185,6 @@ extension VPNCityItemModel { } } -extension NetworkProtectionLocationListCompositeRepository { - convenience init() { - let settings = VPNSettings(defaults: .netP) - self.init( - environment: settings.selectedEnvironment, - tokenStore: NetworkProtectionKeychainTokenStore(), - errorEvents: .networkProtectionAppDebugEvents, - isSubscriptionEnabled: false - ) - } -} - extension VPNLocationViewModel { convenience init() { let locationListRepository = NetworkProtectionLocationListCompositeRepository() diff --git a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift index 7ad69d346b..9f7287b8a7 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift @@ -21,6 +21,7 @@ import Foundation import Subscription import NetworkProtection +import NetworkProtectionUI final class NetworkProtectionSubscriptionEventHandler { @@ -28,40 +29,60 @@ final class NetworkProtectionSubscriptionEventHandler { private let networkProtectionRedemptionCoordinator: NetworkProtectionCodeRedeeming private let networkProtectionTokenStorage: NetworkProtectionTokenStore private let networkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling + private let userDefaults: UserDefaults init(accountManager: AccountManaging = AccountManager(), networkProtectionRedemptionCoordinator: NetworkProtectionCodeRedeeming = NetworkProtectionCodeRedemptionCoordinator(), networkProtectionTokenStorage: NetworkProtectionTokenStore = NetworkProtectionKeychainTokenStore(), - networkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling = NetworkProtectionFeatureDisabler()) { + networkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling = NetworkProtectionFeatureDisabler(), + userDefaults: UserDefaults = .netP) { self.accountManager = accountManager self.networkProtectionRedemptionCoordinator = networkProtectionRedemptionCoordinator self.networkProtectionTokenStorage = networkProtectionTokenStorage self.networkProtectionFeatureDisabler = networkProtectionFeatureDisabler + self.userDefaults = userDefaults + } + + private lazy var entitlementMonitor = NetworkProtectionEntitlementMonitor() + + private func setUpEntitlementMonitoring() { + guard AccountManager().isUserAuthenticated else { return } + let entitlementsCheck = { + await AccountManager().hasEntitlement(for: .networkProtection, cachePolicy: .reloadIgnoringLocalCacheData) + } + + Task { + await entitlementMonitor.start(entitlementCheck: entitlementsCheck) { result in + switch result { + case .validEntitlement: + UserDefaults.netP.networkProtectionEntitlementsExpired = false + case .invalidEntitlement: + UserDefaults.netP.networkProtectionEntitlementsExpired = true + case .error: + break + } + } + } } func registerForSubscriptionAccountManagerEvents() { NotificationCenter.default.addObserver(self, selector: #selector(handleAccountDidSignIn), name: .accountDidSignIn, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(handleAccountDidSignOut), name: .accountDidSignOut, object: nil) + setUpEntitlementMonitoring() } @objc private func handleAccountDidSignIn() { - guard let token = accountManager.accessToken else { + guard accountManager.accessToken != nil else { assertionFailure("[NetP Subscription] AccountManager signed in but token could not be retrieved") return } - - Task { - do { - try await networkProtectionRedemptionCoordinator.exchange(accessToken: token) - print("[NetP Subscription] Exchanged access token for auth token successfully") - } catch { - print("[NetP Subscription] Failed to exchange access token for auth token: \(error)") - } - } + userDefaults.networkProtectionEntitlementsExpired = false + setUpEntitlementMonitoring() } @objc private func handleAccountDidSignOut() { print("[NetP Subscription] Deleted NetP auth token after signing out from Privacy Pro") + userDefaults.networkProtectionEntitlementsExpired = true Task { await networkProtectionFeatureDisabler.disable(keepAuthToken: false, uninstallSystemExtension: false) diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/AppExtensionAndNotificationTargets/NetworkProtectionUNNotificationsPresenter.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/AppExtensionAndNotificationTargets/NetworkProtectionUNNotificationsPresenter.swift index eefd12cff5..3977fccef5 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/AppExtensionAndNotificationTargets/NetworkProtectionUNNotificationsPresenter.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/AppExtensionAndNotificationTargets/NetworkProtectionUNNotificationsPresenter.swift @@ -47,6 +47,7 @@ extension UNNotificationCategory { /// This class takes care of requesting the presentation of notifications using UNNotificationCenter /// final class NetworkProtectionUNNotificationsPresenter: NSObject, NetworkProtectionNotificationsPresenter { + private static let threadIdentifier = "com.duckduckgo.NetworkProtectionNotificationsManager.threadIdentifier" private let appLauncher: AppLauncher @@ -134,6 +135,12 @@ final class NetworkProtectionUNNotificationsPresenter: NSObject, NetworkProtecti showNotification(.superseded, content) } + func showEntitlementNotification() { + let content = notificationContent(title: UserText.networkProtectionEntitlementExpiredNotificationTitle, + subtitle: UserText.networkProtectionEntitlementExpiredNotificationBody) + showNotification(.expiredEntitlement, content) + } + func showTestNotification() { // These strings are deliberately hardcoded as we don't want them localized, they're only for debugging: let content = notificationContent(title: "Test notification", @@ -141,10 +148,6 @@ final class NetworkProtectionUNNotificationsPresenter: NSObject, NetworkProtecti showNotification(.test, content) } - func showEntitlementNotification() { - // todo - } - private func showNotification(_ identifier: NetworkProtectionNotificationIdentifier, _ content: UNNotificationContent) { let request = UNNotificationRequest(identifier: identifier.rawValue, content: content, trigger: .none) @@ -166,6 +169,7 @@ public enum NetworkProtectionNotificationIdentifier: String { case reconnecting = "network-protection.notification.reconnecting" case connected = "network-protection.notification.connected" case superseded = "network-protection.notification.superseded" + case expiredEntitlement = "network-protection.notification.expired-entitlement" case test = "network-protection.notification.test" } diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionAndNotificationTargets/UserText+NetworkProtectionExtensions.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionAndNotificationTargets/UserText+NetworkProtectionExtensions.swift index 607fb081be..ecc8f96073 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionAndNotificationTargets/UserText+NetworkProtectionExtensions.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionAndNotificationTargets/UserText+NetworkProtectionExtensions.swift @@ -43,4 +43,6 @@ final class UserText { static let networkProtectionSupersededNotificationSubtitle = NSLocalizedString("network.protection.superceded.notification.subtitle", value: "Another VPN app on your Mac may have disabled it.", comment: "The subtitle of the notification shown when VPN connection is replaced by another app VPN connection taking over") static let networkProtectionSupersededReconnectActionTitle = NSLocalizedString("network.protection.superceded.action.reconnect.title", value: "Reconnect", comment: "The title of the `Reconnect` notification action button shown when VPN connection is replaced by another app VPN connection taking over") + static let networkProtectionEntitlementExpiredNotificationTitle = NSLocalizedString("network.protection.entitlement.expired.notification.title", value: "VPN disconnected", comment: "The title of the notification when Privacy Pro subscription expired") + static let networkProtectionEntitlementExpiredNotificationBody = NSLocalizedString("network.protection.entitlement.expired.notification.body", value: "Subscribe to Privacy Pro to reconnect DuckDuckGo VPN.", comment: "The body of the notification when Privacy Pro subscription expired") } diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift index da8d81fc24..acb06735b3 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift @@ -24,6 +24,10 @@ import NetworkExtension import Networking import PixelKit +#if SUBSCRIPTION +import Subscription +#endif + final class MacPacketTunnelProvider: PacketTunnelProvider { // MARK: - Additional Status Info @@ -265,6 +269,8 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { // MARK: - Initialization @objc public init() { + let isSubscriptionEnabled = false + #if NETP_SYSTEM_EXTENSION let defaults = UserDefaults.standard #else @@ -274,12 +280,26 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { let tunnelHealthStore = NetworkProtectionTunnelHealthStore(notificationCenter: notificationCenter) let controllerErrorStore = NetworkProtectionTunnelErrorStore(notificationCenter: notificationCenter) let debugEvents = Self.networkProtectionDebugEvents(controllerErrorStore: controllerErrorStore) - let tokenStore = NetworkProtectionKeychainTokenStore(keychainType: Bundle.keychainType, - serviceName: Self.tokenServiceName, - errorEvents: debugEvents, - isSubscriptionEnabled: false, - accessTokenProvider: { nil }) let notificationsPresenter = NetworkProtectionNotificationsPresenterFactory().make(settings: settings, defaults: defaults) + let tokenStore = NetworkProtectionKeychainTokenStore(keychainType: Bundle.keychainType, + serviceName: Self.tokenServiceName, + errorEvents: debugEvents, + isSubscriptionEnabled: isSubscriptionEnabled, + accessTokenProvider: { nil } + ) +#if SUBSCRIPTION + + let accountManager = AccountManager( + accessTokenStorage: tokenStore, + entitlementsCache: UserDefaultsCache<[Entitlement]>(key: UserDefaultsCacheKey.subscriptionEntitlements) + ) + SubscriptionPurchaseEnvironment.currentServiceEnvironment = settings.selectedEnvironment == .production ? .production : .staging + let entitlementsCheck = { + await accountManager.hasEntitlement(for: .networkProtection, cachePolicy: .reloadIgnoringLocalCacheData) + } +#else + let entitlementsCheck: (() async -> Result)? = nil +#endif super.init(notificationsPresenter: notificationsPresenter, tunnelHealthStore: tunnelHealthStore, @@ -290,8 +310,8 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { providerEvents: Self.packetTunnelProviderEvents, settings: settings, defaults: defaults, - isSubscriptionEnabled: false, - entitlementCheck: nil) + isSubscriptionEnabled: isSubscriptionEnabled, + entitlementCheck: entitlementsCheck) setupPixels() observeServerChanges() diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift new file mode 100644 index 0000000000..2987ab2ff9 --- /dev/null +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift @@ -0,0 +1,45 @@ +// +// NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#if SUBSCRIPTION + +import Foundation +import Subscription +import NetworkProtection +import Common + +extension NetworkProtectionKeychainTokenStore: SubscriptionTokenStorage { + public func store(accessToken: String) throws { + try store(accessToken) + } + + public func getAccessToken() throws -> String? { + guard var token = try fetchToken() else { return nil } + if token.hasPrefix("ddg:") { + token = token.replacingOccurrences(of: "ddg:", with: "") + } + os_log("🔵 Wrapper successfully fetched token %{token}@", log: .networkProtection, type: .info, token) + return token + } + + public func removeAccessToken() throws { + try deleteToken() + } +} + +#endif diff --git a/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift b/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift index 97c5a06a42..18cbf84cda 100644 --- a/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift +++ b/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift @@ -21,6 +21,10 @@ import Combine import DDGSync import SwiftUI +#if SUBSCRIPTION +import Subscription +#endif + final class PreferencesSidebarModel: ObservableObject { let tabSwitcherTabs: [Tab.TabContent] @@ -75,10 +79,13 @@ final class PreferencesSidebarModel: ObservableObject { tabSwitcherTabs: [Tab.TabContent] = Tab.TabContent.displayableTabTypes, privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager, syncService: DDGSyncing, - includeDuckPlayer: Bool + includeDuckPlayer: Bool, + userDefaults: UserDefaults = .netP ) { let loadSections = { -#if NETWORK_PROTECTION +#if SUBSCRIPTION + let includingVPN = !userDefaults.networkProtectionEntitlementsExpired && DefaultNetworkProtectionVisibility().isOnboarded +#elseif NETWORK_PROTECTION let includingVPN = DefaultNetworkProtectionVisibility().isOnboarded #else let includingVPN = false @@ -113,6 +120,16 @@ final class PreferencesSidebarModel: ObservableObject { self.refreshSections() } .store(in: &cancellables) + + UserDefaults.netP.publisher(for: \.networkProtectionEntitlementsExpired) + .receive(on: DispatchQueue.main) + .sink { [weak self] entitlementsExpired in + guard let self else { return } + if !entitlementsExpired && self.selectedPane == .vpn { + self.selectedPane = .general + } + self.refreshSections() + }.store(in: &cancellables) } #endif diff --git a/DuckDuckGo/Subscription/AccountManagerExtension.swift b/DuckDuckGo/Subscription/AccountManagerExtension.swift index c1758fb2e5..5f30dad322 100644 --- a/DuckDuckGo/Subscription/AccountManagerExtension.swift +++ b/DuckDuckGo/Subscription/AccountManagerExtension.swift @@ -17,6 +17,7 @@ // #if SUBSCRIPTION +import Foundation import Subscription public extension AccountManager { diff --git a/DuckDuckGo/Subscription/SubscriptionFeatureAvailability.swift b/DuckDuckGo/Subscription/SubscriptionFeatureAvailability.swift index 6599bd0ee9..9090ffad02 100644 --- a/DuckDuckGo/Subscription/SubscriptionFeatureAvailability.swift +++ b/DuckDuckGo/Subscription/SubscriptionFeatureAvailability.swift @@ -24,6 +24,7 @@ import Subscription #if NETWORK_PROTECTION import NetworkProtection +import BrowserServicesKit #endif protocol SubscriptionFeatureAvailability { @@ -36,8 +37,8 @@ struct DefaultSubscriptionFeatureAvailability: SubscriptionFeatureAvailability { #if SUBSCRIPTION_OVERRIDE_ENABLED return true #elseif SUBSCRIPTION - print("isUserAuthenticated: [\(AccountManager().isUserAuthenticated)] | isSubscriptionInternalTestingEnabled: [\(isSubscriptionInternalTestingEnabled)] isInternalUser: [\(isInternalUser)] | isVPNActivated: [\(isVPNActivated)] | isDBPActivated: [\(isDBPActivated)]") - return AccountManager().isUserAuthenticated || (isSubscriptionInternalTestingEnabled && isInternalUser && !isVPNActivated && !isDBPActivated) + print("isUserAuthenticated: [\(AccountManager().isUserAuthenticated)] | isSubscriptionInternalTestingEnabled: [\(isSubscriptionInternalTestingEnabled)] isInternalUser: [\(isInternalUser)] | isDBPActivated: [\(isDBPActivated)]") + return AccountManager().isUserAuthenticated || (isSubscriptionInternalTestingEnabled && isInternalUser && !isDBPActivated) #else return false #endif @@ -48,15 +49,7 @@ struct DefaultSubscriptionFeatureAvailability: SubscriptionFeatureAvailability { } private var isInternalUser: Bool { - NSApp.delegateTyped.internalUserDecider.isInternalUser - } - - private var isVPNActivated: Bool { -#if NETWORK_PROTECTION - return NetworkProtectionKeychainTokenStore().isFeatureActivated -#else - return false -#endif + Self.internalUserDecider.isInternalUser } private var isDBPActivated: Bool { @@ -66,4 +59,18 @@ struct DefaultSubscriptionFeatureAvailability: SubscriptionFeatureAvailability { return false #endif } + + private static var internalUserDecider: InternalUserDecider = { + let keyStore = EncryptionKeyStore() + let fileStore: FileStore + do { + let encryptionKey = NSApplication.runType.requiresEnvironment ? try keyStore.readKey() : nil + fileStore = EncryptedFileStore(encryptionKey: encryptionKey) + } catch { + fileStore = EncryptedFileStore() + } + + let internalUserDeciderStore = InternalUserDeciderStore(fileStore: fileStore) + return DefaultInternalUserDecider(store: internalUserDeciderStore) + }() } diff --git a/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift b/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift index f33b15c8c7..dd5572e94c 100644 --- a/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift +++ b/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift @@ -33,8 +33,6 @@ protocol NetworkProtectionFeatureDisabling { } final class NetworkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling { - static let vpnUninstalledNotificationName = NSNotification.Name(rawValue: "com.duckduckgo.NetworkProtection.uninstalled") - private let log: OSLog private let loginItemsManager: LoginItemsManager private let pinningManager: LocalPinningManager @@ -91,7 +89,7 @@ final class NetworkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling } unpinNetworkProtection() - postVPNUninstalledNotification() + notifyVPNUninstalled() return true } @@ -126,14 +124,11 @@ final class NetworkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling settings.resetToDefaults() } - private func postVPNUninstalledNotification() { - Task { @MainActor in + private func notifyVPNUninstalled() { // Wait a bit since the NetP button is likely being hidden + Task { try? await Task.sleep(nanoseconds: 500 * NSEC_PER_MSEC) - - NotificationCenter.default.post( - name: Self.vpnUninstalledNotificationName, - object: nil) + userDefaults.networkProtectionShouldShowVPNUninstalledMessage = true } } } diff --git a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift b/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift index 6e9386c491..08d52679a3 100644 --- a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift +++ b/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift @@ -24,6 +24,7 @@ import Common import NetworkExtension import NetworkProtection import NetworkProtectionUI +import LoginItems protocol NetworkProtectionFeatureVisibility { func isNetworkProtectionVisible() -> Bool @@ -72,11 +73,15 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { /// Returns whether the VPN should be uninstalled automatically. /// This is only true when the user is not an Easter Egg user, the waitlist test has ended, and the user is onboarded. func shouldUninstallAutomatically() -> Bool { +#if SUBSCRIPTION + return defaults.networkProtectionEntitlementsExpired && LoginItem.vpnMenu.status.isInstalled +#else let waitlistAccessEnded = isWaitlistUser && !waitlistIsOngoing let isNotEasterEggUser = !isEasterEggUser let isOnboarded = UserDefaults.netP.networkProtectionOnboardingStatus != .default return isNotEasterEggUser && waitlistAccessEnded && isOnboarded +#endif } /// Whether the user is fully onboarded diff --git a/DuckDuckGoNotifications/DuckDuckGoNotificationsAppDelegate.swift b/DuckDuckGoNotifications/DuckDuckGoNotificationsAppDelegate.swift index bd226a82c6..41677dc8f1 100644 --- a/DuckDuckGoNotifications/DuckDuckGoNotificationsAppDelegate.swift +++ b/DuckDuckGoNotifications/DuckDuckGoNotificationsAppDelegate.swift @@ -111,6 +111,12 @@ final class DuckDuckGoNotificationsAppDelegate: NSObject, NSApplicationDelegate os_log("Got notification: listener started") self?.notificationsPresenter.requestAuthorization() }.store(in: &cancellables) + + distributedNotificationCenter.publisher(for: .showExpiredEntitlementNotification) + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.showEntitlementNotification() + }.store(in: &cancellables) } func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { @@ -139,6 +145,12 @@ final class DuckDuckGoNotificationsAppDelegate: NSObject, NSApplicationDelegate notificationsPresenter.showSupersededNotification() } + func showEntitlementNotification() { + os_log("Presenting Entitlements notification", log: .networkProtection, type: .info) + + notificationsPresenter.showEntitlementNotification() + } + func showTestNotification() { os_log("Presenting test notification", log: .networkProtection, type: .info) notificationsPresenter.showTestNotification() diff --git a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift index 4ca0fe2aa5..c44f70bbd7 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift +++ b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift @@ -47,6 +47,7 @@ final class DuckDuckGoVPNApplication: NSApplication { super.init() self.delegate = _delegate + #if DEBUG && SUBSCRIPTION let accountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) @@ -144,7 +145,7 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { }() private func handleControllerEvent(_ event: TransparentProxyController.Event) { - PixelKit.fire(event) + } @MainActor @@ -200,6 +201,10 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { VPNAppEventsHandler(tunnelController: tunnelController) }() + private lazy var vpnUninstaller: VPNUninstaller = { + VPNUninstaller(networkExtensionController: networkExtensionController, vpnConfigurationManager: VPNConfigurationManager()) + }() + /// The status bar NetworkProtection menu /// /// For some reason the App will crash if this is initialized right away, which is why it was changed to be lazy. @@ -249,12 +254,21 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { ] }, agentLoginItem: nil, - isMenuBarStatusView: true) + isMenuBarStatusView: true, + userDefaults: .netP, + uninstallHandler: { [weak self] in + guard let self else { return } + await self.vpnUninstaller.uninstall(includingSystemExtension: true) + } + ) } @MainActor func applicationDidFinishLaunching(_ aNotification: Notification) { APIRequest.Headers.setUserAgent(UserAgent.duckDuckGoUserAgent()) +#if SUBSCRIPTION + SubscriptionPurchaseEnvironment.currentServiceEnvironment = tunnelSettings.selectedEnvironment == .production ? .production : .staging +#endif os_log("DuckDuckGoVPN started", log: .networkProtectionLoginItemLog, type: .info) @@ -308,6 +322,8 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { let launchedOnStartup = launchInformation.wasLaunchedByStartup launchInformation.update() + setUpSubscriptionMonitoring() + if launchedOnStartup { Task { let isConnected = await tunnelController.isConnected @@ -338,6 +354,38 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { } }.store(in: &cancellables) } + + private lazy var entitlementMonitor = NetworkProtectionEntitlementMonitor() + + private func setUpSubscriptionMonitoring() { +#if SUBSCRIPTION + guard AccountManager().isUserAuthenticated else { return } + let entitlementsCheck = { + await AccountManager().hasEntitlement(for: .networkProtection, cachePolicy: .reloadIgnoringLocalCacheData) + } + + Task { + await entitlementMonitor.start(entitlementCheck: entitlementsCheck) { [weak self] result in + switch result { + case .validEntitlement: + UserDefaults.netP.networkProtectionEntitlementsExpired = false + case .invalidEntitlement: + UserDefaults.netP.networkProtectionEntitlementsExpired = true + guard let self else { return } + Task { + let isConnected = await self.tunnelController.isConnected + if isConnected { + await self.tunnelController.stop() + DistributedNotificationCenter.default().post(.showExpiredEntitlementNotification) + } + } + case .error: + break + } + } + } +#endif + } } extension NSApplication { diff --git a/DuckDuckGoVPN/NetworkProtection+VPNAgentConvenienceInitializers.swift b/DuckDuckGoVPN/NetworkProtection+VPNAgentConvenienceInitializers.swift new file mode 100644 index 0000000000..b61f1779c1 --- /dev/null +++ b/DuckDuckGoVPN/NetworkProtection+VPNAgentConvenienceInitializers.swift @@ -0,0 +1,33 @@ +// +// NetworkProtection+VPNAgentConvenienceInitializers.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import NetworkProtection + +#if SUBSCRIPTION +import Subscription +#endif + +extension NetworkProtectionKeychainTokenStore { + convenience init() { + self.init(keychainType: .default, + errorEvents: .networkProtectionAppDebugEvents, + isSubscriptionEnabled: false, + accessTokenProvider: { return nil }) + } +} diff --git a/DuckDuckGoVPN/NetworkProtectionBouncer.swift b/DuckDuckGoVPN/NetworkProtectionBouncer.swift index 52963e7b61..e34540d016 100644 --- a/DuckDuckGoVPN/NetworkProtectionBouncer.swift +++ b/DuckDuckGoVPN/NetworkProtectionBouncer.swift @@ -22,6 +22,10 @@ import NetworkProtection import ServiceManagement import AppKit +#if SUBSCRIPTION +import Subscription +#endif + /// Class that implements the necessary logic to ensure the VPN is enabled, or prevent the app from running otherwise. /// final class NetworkProtectionBouncer { @@ -30,13 +34,31 @@ final class NetworkProtectionBouncer { /// current app. /// func requireAuthTokenOrKillApp(controller: TunnelController) async { +#if SUBSCRIPTION + let accountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) + let result = await accountManager.hasEntitlement(for: .networkProtection, cachePolicy: .reloadIgnoringLocalCacheData) + switch result { + case .success(true): + return + case .failure: + break + case .success(false): + os_log(.error, log: .networkProtection, "🔴 Stopping: DuckDuckGo VPN not authorized. Missing entitlement.") + await controller.stop() + + // EXIT_SUCCESS ensures the login item won't relaunch + // Ref: https://developer.apple.com/documentation/servicemanagement/smappservice/register() + // See where it mentions: + // "If the helper crashes or exits with a non-zero status, the system relaunches it" + exit(EXIT_SUCCESS) + } +#endif let keychainStore = NetworkProtectionKeychainTokenStore(keychainType: .default, errorEvents: nil, isSubscriptionEnabled: false, accessTokenProvider: { nil }) - guard keychainStore.isFeatureActivated else { - os_log(.error, log: .networkProtection, "🔴 Stopping: DuckDuckGo VPN not authorized.") + os_log(.error, log: .networkProtection, "🔴 Stopping: DuckDuckGo VPN not authorized. Missing token.") await controller.stop() diff --git a/DuckDuckGoVPN/VPNUninstaller.swift b/DuckDuckGoVPN/VPNUninstaller.swift new file mode 100644 index 0000000000..52593d9faa --- /dev/null +++ b/DuckDuckGoVPN/VPNUninstaller.swift @@ -0,0 +1,60 @@ +// +// VPNUninstaller.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import NetworkProtection +import NetworkProtectionIPC + +protocol VPNUninstalling { + func uninstall(includingSystemExtension: Bool) async +} + +final class VPNUninstaller: VPNUninstalling { + let networkExtensionController: NetworkExtensionController + let vpnConfiguration: VPNConfigurationManager + let defaults: UserDefaults + + init(networkExtensionController: NetworkExtensionController, vpnConfigurationManager: VPNConfigurationManager, defaults: UserDefaults = .netP) { + self.networkExtensionController = networkExtensionController + self.vpnConfiguration = vpnConfigurationManager + self.defaults = defaults + } + + func uninstall(includingSystemExtension: Bool) async { +#if NETP_SYSTEM_EXTENSION + if includingSystemExtension { + do { + try await networkExtensionController.deactivateSystemExtension() + defaults.networkProtectionOnboardingStatus = .isOnboarding(step: .userNeedsToAllowExtension) + } catch { + + } + } +#endif + + await vpnConfiguration.removeVPNConfiguration() + + if defaults.networkProtectionOnboardingStatus == .completed { + defaults.networkProtectionOnboardingStatus = .isOnboarding(step: .userNeedsToAllowVPNConfiguration) + } + + defaults.networkProtectionShouldShowVPNUninstalledMessage = true + + exit(EXIT_SUCCESS) + } +} diff --git a/LocalPackages/LoginItems/Package.swift b/LocalPackages/LoginItems/Package.swift index b1a72a5caa..0e2d71d1b3 100644 --- a/LocalPackages/LoginItems/Package.swift +++ b/LocalPackages/LoginItems/Package.swift @@ -14,6 +14,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "2.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", branch: "graeme/expired-entitlements-stuff") ], targets: [ .target( diff --git a/LocalPackages/LoginItems/Sources/LoginItems/LoginItem.swift b/LocalPackages/LoginItems/Sources/LoginItems/LoginItem.swift index 87fda099f7..48a16a8689 100644 --- a/LocalPackages/LoginItems/Sources/LoginItems/LoginItem.swift +++ b/LocalPackages/LoginItems/Sources/LoginItems/LoginItem.swift @@ -52,6 +52,10 @@ public struct LoginItem: Equatable, Hashable { self == .enabled } + public var isInstalled: Bool { + self == .enabled || self == .requiresApproval + } + @available(macOS 13.0, *) public init(_ status: SMAppService.Status) { switch status { diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/AppLaunching/AppLaunching.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/AppLaunching/AppLaunching.swift index 2e408557b0..e64f0a605a 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/AppLaunching/AppLaunching.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/AppLaunching/AppLaunching.swift @@ -32,6 +32,7 @@ public enum AppLaunchCommand: Codable { case stopVPN case enableOnDemand case moveAppToApplications + case showPrivacyPro } public protocol AppLaunching { diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserDefault+ShowVPNUninstalledMessage.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserDefault+ShowVPNUninstalledMessage.swift new file mode 100644 index 0000000000..8f85e818ea --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserDefault+ShowVPNUninstalledMessage.swift @@ -0,0 +1,44 @@ +// +// UserDefault+ShowVPNUninstalledMessage.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +public extension UserDefaults { + private enum Key { + static var networkProtectionShouldShowVPNUninstalledMessage = "networkProtectionShouldShowVPNUninstalledMessage" + } + + // Convenience declaration + private var networkProtectionShowVPNUninstalledMessageRawValueKey: String { + Key.networkProtectionShouldShowVPNUninstalledMessage + } + + /// For KVO to work across processes (Menu App + Main App) we need to declare this dynamic var in a `UserDefaults` + /// extension, and the key for this property must match its name exactly. + /// + @objc + dynamic var networkProtectionShouldShowVPNUninstalledMessage: Bool { + get { + value(forKey: networkProtectionShowVPNUninstalledMessageRawValueKey) as? Bool ?? false + } + + set { + set(newValue, forKey: networkProtectionShowVPNUninstalledMessageRawValueKey) + } + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserDefault+VPNEnabledViaWaitlist.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserDefault+VPNEnabledViaWaitlist.swift new file mode 100644 index 0000000000..397f67f6ac --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserDefault+VPNEnabledViaWaitlist.swift @@ -0,0 +1,44 @@ +// +// UserDefault+VPNEnabledViaWaitlist.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +public extension UserDefaults { + private enum Key { + static var networkProtectionVPNEnabledViaWaitlist = "networkProtectionVPNEnabledViaWaitlist" + } + + // Convenience declaration + private var networkProtectionVPNEnabledViaWaitlistRawValueKey: String { + Key.networkProtectionVPNEnabledViaWaitlist + } + + /// For KVO to work across processes (Menu App + Main App) we need to declare this dynamic var in a `UserDefaults` + /// extension, and the key for this property must match its name exactly. + /// + @objc + dynamic var networkProtectionVPNEnabledViaWaitlist: Bool { + get { + value(forKey: networkProtectionVPNEnabledViaWaitlistRawValueKey) as? Bool ?? false + } + + set { + set(newValue, forKey: networkProtectionVPNEnabledViaWaitlistRawValueKey) + } + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserDefaults+NetworkProtectionExpiredEntitlements.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserDefaults+NetworkProtectionExpiredEntitlements.swift new file mode 100644 index 0000000000..0768592eff --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserDefaults+NetworkProtectionExpiredEntitlements.swift @@ -0,0 +1,44 @@ +// +// UserDefaults+NetworkProtectionExpiredEntitlements.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +public extension UserDefaults { + private enum Key { + static var networkProtectionEntitlementsExpired = "networkProtectionEntitlementsExpired" + } + + // Convenience declaration + private var networkProtectionEntitlementsExpiredRawValueKey: String { + Key.networkProtectionEntitlementsExpired + } + + /// For KVO to work across processes (Menu App + Main App) we need to declare this dynamic var in a `UserDefaults` + /// extension, and the key for this property must match its name exactly. + /// + @objc + dynamic var networkProtectionEntitlementsExpired: Bool { + get { + value(forKey: networkProtectionEntitlementsExpiredRawValueKey) as? Bool ?? false + } + + set { + set(newValue, forKey: networkProtectionEntitlementsExpiredRawValueKey) + } + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserText+NetworkProtectionUI.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserText+NetworkProtectionUI.swift index 1ec0e4d17a..402f85098f 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserText+NetworkProtectionUI.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserText+NetworkProtectionUI.swift @@ -67,4 +67,11 @@ final class UserText { let localized = NSLocalizedString("network.protection.server.location.link", value: "%@...", comment: "Clickable text linking to the server location picker screen") return String(format: localized, location) } + + // MARK: Subscription Expired + + static let networkProtectionSubscriptionExpiredTitle = NSLocalizedString("network.protection.subscription.expired.title", value: "VPN disconnected", comment: "Title for the prompt that tells the user their subscription expired.") + static let networkProtectionSubscriptionExpiredSubtitle = NSLocalizedString("network.protection.subscription.expired.subtitle", value: "Subscribe to Privacy Pro to reconnect DuckDuckGo VPN.", comment: "Subtitle for the prompt that tells the user their subscription expired.") + static let networkProtectionSubscriptionExpiredResubscribeButton = NSLocalizedString("network.protection.subscription.expired.resubscribe.button", value: "Subscribe to Privacy Pro", comment: "Button for the prompt that takes the user to the page to resubscribe.") + static let networkProtectionSubscriptionExpiredUninstallButton = NSLocalizedString("network.protection.subscription.expired.uninstall.button", value: "Uninstall DuckDuckGo VPN", comment: "Button for the prompt that uninstalls the VPN.") } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Menu/StatusBarMenu.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Menu/StatusBarMenu.swift index 488970f7d4..1833f8482e 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Menu/StatusBarMenu.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Menu/StatusBarMenu.swift @@ -56,7 +56,9 @@ public final class StatusBarMenu: NSObject { appLauncher: AppLaunching, menuItems: @escaping () -> [MenuItem], agentLoginItem: LoginItem?, - isMenuBarStatusView: Bool) { + isMenuBarStatusView: Bool, + userDefaults: UserDefaults, + uninstallHandler: @escaping () async -> Void) { self.model = model let statusItem = statusItem ?? NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) @@ -69,7 +71,10 @@ public final class StatusBarMenu: NSObject { appLauncher: appLauncher, menuItems: menuItems, agentLoginItem: agentLoginItem, - isMenuBarStatusView: isMenuBarStatusView) + isMenuBarStatusView: isMenuBarStatusView, + userDefaults: userDefaults, + uninstallHandler: uninstallHandler) + popover.behavior = .transient super.init() diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionPopover.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionPopover.swift index efbe5872eb..0acbf84065 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionPopover.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionPopover.swift @@ -56,7 +56,9 @@ public final class NetworkProtectionPopover: NSPopover { appLauncher: AppLaunching, menuItems: @escaping () -> [MenuItem], agentLoginItem: LoginItem?, - isMenuBarStatusView: Bool) { + isMenuBarStatusView: Bool, + userDefaults: UserDefaults, + uninstallHandler: @escaping () async -> Void) { self.statusReporter = statusReporter self.model = NetworkProtectionStatusView.Model(controller: controller, @@ -66,7 +68,9 @@ public final class NetworkProtectionPopover: NSPopover { appLauncher: appLauncher, menuItems: menuItems, agentLoginItem: agentLoginItem, - isMenuBarStatusView: isMenuBarStatusView) + isMenuBarStatusView: isMenuBarStatusView, + userDefaults: userDefaults, + uninstallHandler: uninstallHandler) super.init() diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift index a70e22f729..aae033b17d 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift @@ -50,7 +50,14 @@ public struct NetworkProtectionStatusView: View { public var body: some View { VStack(spacing: 0) { - if let promptActionViewModel = model.promptActionViewModel { + if model.shouldShowSubscriptionExpired { + SubscriptionExpiredView { + model.openPrivacyPro() + } uninstallButtonHandler: { + model.uninstallVPN() + } + .padding(5) + } else if let promptActionViewModel = model.promptActionViewModel { PromptActionView(model: promptActionViewModel) .padding(.horizontal, 5) .padding(.top, 5) diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift index 454ec28102..7df51daf58 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift @@ -57,7 +57,7 @@ extension NetworkProtectionStatusView { private(set) var onboardingStatus: OnboardingStatus = .completed var tunnelControllerViewDisabled: Bool { - onboardingStatus != .completed || loginItemNeedsApproval + onboardingStatus != .completed || loginItemNeedsApproval || shouldShowSubscriptionExpired } @MainActor @@ -94,6 +94,10 @@ extension NetworkProtectionStatusView { /// private let runLoopMode: RunLoop.Mode? + private let appLauncher: AppLaunching + + private let uninstallHandler: () async -> Void + private var cancellables = Set() // MARK: - Dispatch Queues @@ -113,7 +117,9 @@ extension NetworkProtectionStatusView { menuItems: @escaping () -> [MenuItem], agentLoginItem: LoginItem?, isMenuBarStatusView: Bool, - runLoopMode: RunLoop.Mode? = nil) { + runLoopMode: RunLoop.Mode? = nil, + userDefaults: UserDefaults, + uninstallHandler: @escaping () async -> Void) { self.tunnelController = controller self.onboardingStatusPublisher = onboardingStatusPublisher @@ -123,6 +129,8 @@ extension NetworkProtectionStatusView { self.agentLoginItem = agentLoginItem self.isMenuBarStatusView = isMenuBarStatusView self.runLoopMode = runLoopMode + self.appLauncher = appLauncher + self.uninstallHandler = uninstallHandler tunnelControllerViewModel = TunnelControllerViewModel(controller: tunnelController, onboardingStatusPublisher: onboardingStatusPublisher, @@ -146,8 +154,14 @@ extension NetworkProtectionStatusView { onboardingStatusPublisher .receive(on: DispatchQueue.main) .sink { [weak self] status in - self?.onboardingStatus = status - } + self?.onboardingStatus = status + } + .store(in: &cancellables) + + userDefaults + .publisher(for: \.networkProtectionEntitlementsExpired) + .receive(on: DispatchQueue.main) + .assign(to: \.shouldShowSubscriptionExpired, onWeaklyHeld: self) .store(in: &cancellables) } @@ -164,6 +178,18 @@ extension NetworkProtectionStatusView { } } + func openPrivacyPro() { + Task { + await appLauncher.launchApp(withCommand: .showPrivacyPro) + } + } + + func uninstallVPN() { + Task { + await uninstallHandler() + } + } + private func subscribeToStatusChanges() { statusReporter.statusObserver.publisher .receive(on: DispatchQueue.main) @@ -280,6 +306,9 @@ extension NetworkProtectionStatusView { let tunnelControllerViewModel: TunnelControllerViewModel + @Published + var shouldShowSubscriptionExpired: Bool = false + var promptActionViewModel: PromptActionView.Model? { #if !APPSTORE && !DEBUG guard Bundle.main.isInApplicationDirectory else { diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/SubscriptionExpiredView/SubscriptionExpiredView.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/SubscriptionExpiredView/SubscriptionExpiredView.swift new file mode 100644 index 0000000000..a4fe590b8e --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/SubscriptionExpiredView/SubscriptionExpiredView.swift @@ -0,0 +1,74 @@ +// +// SubscriptionExpiredView.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftUI +import SwiftUIExtensions + +struct SubscriptionExpiredView: View { + enum Constants { + static let backgroundCornerRadius = 6.0 + } + + let subscribeButtonHandler: () -> Void + let uninstallButtonHandler: () -> Void + + public var body: some View { + VStack(alignment: .leading, spacing: 5) { + Text(UserText.networkProtectionSubscriptionExpiredTitle) + .font(.system(size: 13).weight(.bold)) + .foregroundColor(Color(.defaultText)) + .multilineText() + + Text(UserText.networkProtectionSubscriptionExpiredSubtitle) + .font(.system(size: 13)) + .foregroundColor(Color(.defaultText)) + .multilineText() + + Button(UserText.networkProtectionSubscriptionExpiredResubscribeButton, action: subscribeButtonHandler) + .buttonStyle(DefaultActionButtonStyle(enabled: true)) + .padding(.top, 3) + + Divider() + .padding(.top, 8) + .padding(.bottom, 3) + + Button(UserText.networkProtectionSubscriptionExpiredUninstallButton, action: uninstallButtonHandler) + .buttonStyle(.borderless) + .foregroundColor(.accentColor) + .padding(.top, 3) + } + .padding(.vertical, 16) + .padding(.horizontal, 10) + .cornerRadius(8) + .background( + RoundedRectangle(cornerRadius: Constants.backgroundCornerRadius, style: .circular) + .stroke(Color(.onboardingStepBorder), lineWidth: 1) + .background( + RoundedRectangle(cornerRadius: Constants.backgroundCornerRadius, style: .circular) + .fill(Color(.onboardingStepBackground)) + ) + ) + } +} + +struct SubscriptionExpiredView_Preview: PreviewProvider { + static var previews: some View { + SubscriptionExpiredView(subscribeButtonHandler: {}, uninstallButtonHandler: {}) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionStatusBarMenuTests.swift b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionStatusBarMenuTests.swift index b680b062b6..5b9e7e0dea 100644 --- a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionStatusBarMenuTests.swift +++ b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionStatusBarMenuTests.swift @@ -56,7 +56,9 @@ final class StatusBarMenuTests: XCTestCase { appLauncher: MockAppLauncher(), menuItems: { [] }, agentLoginItem: nil, - isMenuBarStatusView: false) + isMenuBarStatusView: false, + userDefaults: .standard, + uninstallHandler: { }) menu.show() @@ -79,7 +81,9 @@ final class StatusBarMenuTests: XCTestCase { appLauncher: MockAppLauncher(), menuItems: { [] }, agentLoginItem: nil, - isMenuBarStatusView: false) + isMenuBarStatusView: false, + userDefaults: .standard, + uninstallHandler: { }) menu.hide() diff --git a/LocalPackages/SyncUI/Package.swift b/LocalPackages/SyncUI/Package.swift index 63994d4a45..4693eb6b4a 100644 --- a/LocalPackages/SyncUI/Package.swift +++ b/LocalPackages/SyncUI/Package.swift @@ -15,6 +15,7 @@ let package = Package( dependencies: [ .package(path: "../SwiftUIExtensions"), .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "2.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", branch: "graeme/expired-entitlements-stuff") ], targets: [ .target( diff --git a/LocalPackages/SystemExtensionManager/Package.swift b/LocalPackages/SystemExtensionManager/Package.swift index bf7cce608f..58eeb26cc1 100644 --- a/LocalPackages/SystemExtensionManager/Package.swift +++ b/LocalPackages/SystemExtensionManager/Package.swift @@ -17,6 +17,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "2.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", branch: "graeme/expired-entitlements-stuff") ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. diff --git a/NetworkProtectionSystemExtension/NetworkProtectionAgentNotificationsPresenter.swift b/NetworkProtectionSystemExtension/NetworkProtectionAgentNotificationsPresenter.swift index 7787d3d93b..8f2ffe08d6 100644 --- a/NetworkProtectionSystemExtension/NetworkProtectionAgentNotificationsPresenter.swift +++ b/NetworkProtectionSystemExtension/NetworkProtectionAgentNotificationsPresenter.swift @@ -48,11 +48,11 @@ final class NetworkProtectionAgentNotificationsPresenter: NetworkProtectionNotif notificationCenter.post(.showVPNSupersededNotification) } - func showTestNotification() { - notificationCenter.post(.showTestNotification) + func showEntitlementNotification() { + notificationCenter.post(.showExpiredEntitlementNotification) } - func showEntitlementNotification() { - // todo + func showTestNotification() { + notificationCenter.post(.showTestNotification) } } From 604e6be6151348d731f7d27485337e77d597e6fb Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Tue, 19 Mar 2024 13:00:27 -0700 Subject: [PATCH 41/51] Add Privacy Pro to App Store build (#2440) Task/Issue URL: https://app.asana.com/0/1199230911884351/1206861044153287/f Tech Design URL: CC: Description: This PR adds Privacy Pro to the App Store build. --- Configuration/Common.xcconfig | 2 +- DuckDuckGo.xcodeproj/project.pbxproj | 27 +++++++++++++++++++ .../xcshareddata/swiftpm/Package.resolved | 4 +-- ...kDuckGo Privacy Browser App Store.xcscheme | 3 +++ .../DuckDuckGoVPNAppStore.entitlements | 1 + .../InputFilesChecker/InputFilesChecker.swift | 2 -- LocalPackages/LoginItems/Package.swift | 2 +- .../XCTestCase+PixelKit.swift | 3 +++ LocalPackages/SyncUI/Package.swift | 2 +- .../SystemExtensionManager/Package.swift | 2 +- 10 files changed, 40 insertions(+), 8 deletions(-) diff --git a/Configuration/Common.xcconfig b/Configuration/Common.xcconfig index 5c38d4e8e4..392d63d532 100644 --- a/Configuration/Common.xcconfig +++ b/Configuration/Common.xcconfig @@ -21,7 +21,7 @@ COMBINE_HIDPI_IMAGES = YES DEVELOPMENT_TEAM = HKE973VLUW DEVELOPMENT_TEAM[config=CI][sdk=*] = -FEATURE_FLAGS = FEEDBACK DBP NETWORK_PROTECTION +FEATURE_FLAGS = FEEDBACK DBP NETWORK_PROTECTION SUBSCRIPTION GCC_PREPROCESSOR_DEFINITIONS[config=CI][arch=*][sdk=*] = DEBUG=1 CI=1 $(inherited) GCC_PREPROCESSOR_DEFINITIONS[config=Debug][arch=*][sdk=*] = DEBUG=1 $(inherited) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 1596da5917..a80901b936 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2111,6 +2111,11 @@ 4BBF09232830812900EE1418 /* FileSystemDSL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBF09222830812900EE1418 /* FileSystemDSL.swift */; }; 4BBF0925283083EC00EE1418 /* FileSystemDSLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBF0924283083EC00EE1418 /* FileSystemDSLTests.swift */; }; 4BC2621D293996410087A482 /* PixelEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BC2621C293996410087A482 /* PixelEventTests.swift */; }; + 4BCBE4552BA7E16600FC75A1 /* NetworkProtectionSubscriptionEventHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B2F565B2B38F93E001214C0 /* NetworkProtectionSubscriptionEventHandler.swift */; }; + 4BCBE4562BA7E16900FC75A1 /* DataBrokerProtectionSubscriptionEventHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB5789712B2CA70F0009DFE2 /* DataBrokerProtectionSubscriptionEventHandler.swift */; }; + 4BCBE4582BA7E17800FC75A1 /* SubscriptionUI in Frameworks */ = {isa = PBXBuildFile; productRef = 4BCBE4572BA7E17800FC75A1 /* SubscriptionUI */; }; + 4BCBE45A2BA7E17800FC75A1 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = 4BCBE4592BA7E17800FC75A1 /* Subscription */; }; + 4BCBE45C2BA7E18500FC75A1 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = 4BCBE45B2BA7E18500FC75A1 /* Subscription */; }; 4BCF15D72ABB8A110083F6DF /* NetworkProtectionRemoteMessaging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF15D62ABB8A110083F6DF /* NetworkProtectionRemoteMessaging.swift */; }; 4BCF15D92ABB8A7F0083F6DF /* NetworkProtectionRemoteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF15D82ABB8A7F0083F6DF /* NetworkProtectionRemoteMessage.swift */; }; 4BCF15EC2ABB9AF80083F6DF /* NetworkProtectionRemoteMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF15E42ABB98990083F6DF /* NetworkProtectionRemoteMessageTests.swift */; }; @@ -4683,10 +4688,12 @@ 378F44E629B4BDEE00899924 /* SwiftUIExtensions in Frameworks */, 3706FCA7293F65D500E42796 /* BrowserServicesKit in Frameworks */, 3129788A2B64131200B67619 /* DataBrokerProtection in Frameworks */, + 4BCBE4582BA7E17800FC75A1 /* SubscriptionUI in Frameworks */, 3706FCA9293F65D500E42796 /* ContentBlocking in Frameworks */, 85D44B882BA08D30001B4AB5 /* Suggestions in Frameworks */, 4BF97AD12B43C43F00EB4240 /* NetworkProtectionIPC in Frameworks */, 37F44A5F298C17830025E7FE /* Navigation in Frameworks */, + 4BCBE45A2BA7E17800FC75A1 /* Subscription in Frameworks */, B6EC37FF29B8D915001ACE79 /* Configuration in Frameworks */, 372217822B33380700B8E9C2 /* TestUtils in Frameworks */, 3706FCAA293F65D500E42796 /* UserScript in Frameworks */, @@ -4772,6 +4779,7 @@ buildActionMask = 2147483647; files = ( 7BFCB7502ADE7E2300DA3EA7 /* PixelKit in Frameworks */, + 4BCBE45C2BA7E18500FC75A1 /* Subscription in Frameworks */, 7BA7CC612AD1211C0042E5CE /* Networking in Frameworks */, 7BEEA5142AD1236300A9E72B /* NetworkProtectionIPC in Frameworks */, 7B00997F2B6508C200FE7C31 /* NetworkProtectionProxy in Frameworks */, @@ -8691,7 +8699,9 @@ 7B1459562B7D43E500047F2C /* NetworkProtectionProxy */, 85E2BBCF2B8F534A00DBEC7A /* History */, F1D43AF42B98E48900BAB743 /* BareBonesBrowserKit */, + 4BCBE4572BA7E17800FC75A1 /* SubscriptionUI */, 85D44B872BA08D30001B4AB5 /* Suggestions */, + 4BCBE4592BA7E17800FC75A1 /* Subscription */, ); productName = DuckDuckGo; productReference = 3706FD05293F65D500E42796 /* DuckDuckGo App Store.app */; @@ -8877,6 +8887,7 @@ 7BFCB74F2ADE7E2300DA3EA7 /* PixelKit */, 7B00997E2B6508C200FE7C31 /* NetworkProtectionProxy */, 4BA7C4DC2B3F64E500AFE511 /* LoginItems */, + 4BCBE45B2BA7E18500FC75A1 /* Subscription */, ); productName = DuckDuckGoAgentAppStore; productReference = 4B2D06692A13318400DE1F49 /* DuckDuckGo VPN App Store.app */; @@ -10611,6 +10622,7 @@ 3706FC43293F65D500E42796 /* PinnedTabsViewModel.swift in Sources */, 85D0327C2B8E3D090041D1FB /* HistoryCoordinatorExtension.swift in Sources */, B6685E4329A61C470043D2EE /* DownloadsTabExtension.swift in Sources */, + 4BCBE4562BA7E16900FC75A1 /* DataBrokerProtectionSubscriptionEventHandler.swift in Sources */, 3706FC44293F65D500E42796 /* BookmarkList.swift in Sources */, 3706FC45293F65D500E42796 /* BookmarkTableRowView.swift in Sources */, 7BEC20462B0F505F00243D3E /* AddBookmarkFolderPopoverView.swift in Sources */, @@ -10693,6 +10705,7 @@ 3706FC81293F65D500E42796 /* DispatchQueueExtensions.swift in Sources */, C13909F02B85FD4E001626ED /* AutofillActionExecutor.swift in Sources */, 3706FC82293F65D500E42796 /* PermissionAuthorizationPopover.swift in Sources */, + 4BCBE4552BA7E16600FC75A1 /* NetworkProtectionSubscriptionEventHandler.swift in Sources */, 3706FC83293F65D500E42796 /* PopoverMessageViewController.swift in Sources */, 4BF97ADA2B43C5DC00EB4240 /* VPNFeedbackCategory.swift in Sources */, 9D9AE86E2AA76D1F0026E7DC /* LoginItem+NetworkProtection.swift in Sources */, @@ -14492,6 +14505,20 @@ isa = XCSwiftPackageProductDependency; productName = LoginItems; }; + 4BCBE4572BA7E17800FC75A1 /* SubscriptionUI */ = { + isa = XCSwiftPackageProductDependency; + productName = SubscriptionUI; + }; + 4BCBE4592BA7E17800FC75A1 /* Subscription */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Subscription; + }; + 4BCBE45B2BA7E18500FC75A1 /* Subscription */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Subscription; + }; 4BF97AD02B43C43F00EB4240 /* NetworkProtectionIPC */ = { isa = XCSwiftPackageProductDependency; productName = NetworkProtectionIPC; diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7d241ed988..462aa74011 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "branch" : "graeme/expired-entitlements-stuff", - "revision" : "925d0dd50e47f38c7fe922622002e8961569bc32" + "branch" : "126.2.0", + "revision" : "0c73586c2628381b8a63be65fd2bc1824e58d7f9" } }, { diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser App Store.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser App Store.xcscheme index 29eecc0cc8..f5feca4eef 100644 --- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser App Store.xcscheme +++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser App Store.xcscheme @@ -106,6 +106,9 @@ + + diff --git a/DuckDuckGoVPN/DuckDuckGoVPNAppStore.entitlements b/DuckDuckGoVPN/DuckDuckGoVPNAppStore.entitlements index 5e706138b5..234e25f597 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNAppStore.entitlements +++ b/DuckDuckGoVPN/DuckDuckGoVPNAppStore.entitlements @@ -4,6 +4,7 @@ com.apple.developer.networking.networkextension + app-proxy-provider packet-tunnel-provider com.apple.security.app-sandbox diff --git a/LocalPackages/BuildToolPlugins/Plugins/InputFilesChecker/InputFilesChecker.swift b/LocalPackages/BuildToolPlugins/Plugins/InputFilesChecker/InputFilesChecker.swift index 00053f594d..cd6186444d 100644 --- a/LocalPackages/BuildToolPlugins/Plugins/InputFilesChecker/InputFilesChecker.swift +++ b/LocalPackages/BuildToolPlugins/Plugins/InputFilesChecker/InputFilesChecker.swift @@ -26,8 +26,6 @@ let nonSandboxedExtraInputFiles: Set = [ .init("BWManager.swift", .source), .init("UpdateController.swift", .source), .init("PFMoveApplication.m", .source), - .init("NetworkProtectionSubscriptionEventHandler.swift", .source), - .init("DataBrokerProtectionSubscriptionEventHandler.swift", .source), .init("DuckDuckGo VPN.app", .unknown), .init("DuckDuckGo Notifications.app", .unknown), .init("DuckDuckGo Personal Information Removal.app", .unknown) diff --git a/LocalPackages/LoginItems/Package.swift b/LocalPackages/LoginItems/Package.swift index 0e2d71d1b3..389c3f9d97 100644 --- a/LocalPackages/LoginItems/Package.swift +++ b/LocalPackages/LoginItems/Package.swift @@ -14,7 +14,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "2.0.0"), - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", branch: "graeme/expired-entitlements-stuff") + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.2.0") ], targets: [ .target( diff --git a/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift index 6d742bd532..9017d8774a 100644 --- a/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift +++ b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift @@ -111,6 +111,9 @@ public extension XCTestCase { let knownExpectedParameters = knownExpectedParameters(for: event) let callbackExecutedExpectation = expectation(description: "The PixelKit callback has been executed") + // Ensure PixelKit is torn down before setting it back up, avoiding unit test race conditions: + PixelKit.tearDown() + PixelKit.setUp(dryRun: false, appVersion: "1.0.5", source: "test-app", diff --git a/LocalPackages/SyncUI/Package.swift b/LocalPackages/SyncUI/Package.swift index 4693eb6b4a..b01805eb86 100644 --- a/LocalPackages/SyncUI/Package.swift +++ b/LocalPackages/SyncUI/Package.swift @@ -15,7 +15,7 @@ let package = Package( dependencies: [ .package(path: "../SwiftUIExtensions"), .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "2.0.0"), - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", branch: "graeme/expired-entitlements-stuff") + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", branch: "126.2.0") ], targets: [ .target( diff --git a/LocalPackages/SystemExtensionManager/Package.swift b/LocalPackages/SystemExtensionManager/Package.swift index 58eeb26cc1..53f7b3e21b 100644 --- a/LocalPackages/SystemExtensionManager/Package.swift +++ b/LocalPackages/SystemExtensionManager/Package.swift @@ -17,7 +17,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "2.0.0"), - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", branch: "graeme/expired-entitlements-stuff") + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", branch: "126.2.0") ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. From 59a72eaac95f500c4b04ecc3a624c969db36cdb2 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Tue, 19 Mar 2024 21:13:52 +0100 Subject: [PATCH 42/51] Use SubscriptionFeatureAvailability to determine availability of the subscription (#2436) Task/Issue URL: https://app.asana.com/0/0/1206841322374473/f **Description**: Use BSK provided SubscriptionFeatureAvailability to show or hide Subscription experience in the UI. **Steps to test this PR**: Test internal user override 1. Enable internal user 2. More options menu and settings should display Privacy Pro Test privacy config with flags disabled 1. Disable internal user 2. Prepare and host modified privacy config disabling all privacyPro feature flags: - `isLaunched` - `isLaunchedStripe` - `allowPurchase` - `allowPurchaseStripe` - `isLaunchedOverride` - `isLaunchedOverrideStripe` 4. Privacy Pro should not be accessible in more options menu and in settings Test build with purchase via `App Store` enabling only: - `isLaunched` and `allowPurchase` - `isLaunchedOverride` and `allowPurchase` Result: Privacy Pro options should be available - `isLaunchedStripe` and `allowPurchaseStripe` - `isLaunchedOverrideStripe` and `allowPurchaseStripe` Result: Privacy Pro options should be disabled Test build with purchase via `Stripe` enabling only: - `isLaunchedStripe` and `allowPurchaseStripe` - `isLaunchedOverrideStripe` and `allowPurchaseStripe` Result: Privacy Pro options should be available - `isLaunched` and `allowPurchase` - `isLaunchedOverride` and `allowPurchase` Result: Privacy Pro options should be disabled Test build enabling all flags except `allowPurchase` and `allowPurchaseStripe`: Result: - Privacy Pro options should be available - Opening purchase page should not show the purchase options disallowing to purchase --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --------- Co-authored-by: Fernando Bunn --- DuckDuckGo.xcodeproj/project.pbxproj | 13 +--- .../xcshareddata/swiftpm/Package.resolved | 4 +- DuckDuckGo/Application/AppDelegate.swift | 19 +++-- .../Utilities/UserDefaultsWrapper.swift | 1 + .../DBP/DataBrokerProtectionAppEvents.swift | 8 +- ...ataBrokerProtectionFeatureVisibility.swift | 31 +++++++- .../View/AddressBarTextField.swift | 2 +- .../NavigationBar/View/MoreOptionsMenu.swift | 11 +-- .../View/NavigationBarViewController.swift | 2 +- ...rkProtection+ConvenienceInitializers.swift | 9 ++- .../Model/PreferencesSection.swift | 2 +- .../SubscriptionFeatureAvailability.swift | 76 ------------------- .../SubscriptionPagesUserScript.swift | 2 + DuckDuckGo/Tab/UserScripts/UserScripts.swift | 2 +- .../WaitlistViewControllerPresenter.swift | 5 ++ .../DataBrokerProtection/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- LocalPackages/SyncUI/Package.swift | 2 +- .../SystemExtensionManager/Package.swift | 2 +- 20 files changed, 80 insertions(+), 117 deletions(-) delete mode 100644 DuckDuckGo/Subscription/SubscriptionFeatureAvailability.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index a80901b936..6329646836 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -165,9 +165,6 @@ 1E950E432912A10D0051A99B /* UserScript in Frameworks */ = {isa = PBXBuildFile; productRef = 1E950E422912A10D0051A99B /* UserScript */; }; 1EA7B8D32B7E078C000330A4 /* SubscriptionUI in Frameworks */ = {isa = PBXBuildFile; productRef = 1EA7B8D22B7E078C000330A4 /* SubscriptionUI */; }; 1EA7B8D52B7E078C000330A4 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = 1EA7B8D42B7E078C000330A4 /* Subscription */; }; - 1EA7B8D82B7E1283000330A4 /* SubscriptionFeatureAvailability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EA7B8D72B7E1283000330A4 /* SubscriptionFeatureAvailability.swift */; }; - 1EA7B8D92B7E1283000330A4 /* SubscriptionFeatureAvailability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EA7B8D72B7E1283000330A4 /* SubscriptionFeatureAvailability.swift */; }; - 1EA7B8DA2B7E1283000330A4 /* SubscriptionFeatureAvailability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EA7B8D72B7E1283000330A4 /* SubscriptionFeatureAvailability.swift */; }; 1ED910D52B63BFB300936947 /* IdentityTheftRestorationPagesUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED910D42B63BFB300936947 /* IdentityTheftRestorationPagesUserScript.swift */; }; 1ED910D62B63BFB300936947 /* IdentityTheftRestorationPagesUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED910D42B63BFB300936947 /* IdentityTheftRestorationPagesUserScript.swift */; }; 1ED910D72B63BFB300936947 /* IdentityTheftRestorationPagesUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED910D42B63BFB300936947 /* IdentityTheftRestorationPagesUserScript.swift */; }; @@ -3525,7 +3522,6 @@ 1E7E2E8F29029A2A00C01B54 /* ContentBlockingRulesUpdateObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentBlockingRulesUpdateObserver.swift; sourceTree = ""; }; 1E7E2E932902AC0E00C01B54 /* PrivacyDashboardPermissionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardPermissionHandler.swift; sourceTree = ""; }; 1E862A882A9FC01200F84D4B /* SubscriptionUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = SubscriptionUI; sourceTree = ""; }; - 1EA7B8D72B7E1283000330A4 /* SubscriptionFeatureAvailability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionFeatureAvailability.swift; sourceTree = ""; }; 1ED910D42B63BFB300936947 /* IdentityTheftRestorationPagesUserScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityTheftRestorationPagesUserScript.swift; sourceTree = ""; }; 310E79BE294A19A8007C49E8 /* FireproofingReferenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FireproofingReferenceTests.swift; sourceTree = ""; }; 311B262628E73E0A00FD181A /* TabShadowConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabShadowConfig.swift; sourceTree = ""; }; @@ -5135,7 +5131,6 @@ isa = PBXGroup; children = ( EE0629712B90EE8C00D868B4 /* AccountManagerExtension.swift */, - 1EA7B8D72B7E1283000330A4 /* SubscriptionFeatureAvailability.swift */, ); path = Subscription; sourceTree = ""; @@ -10124,7 +10119,6 @@ 3706FADE293F65D500E42796 /* PreferencesDuckPlayerView.swift in Sources */, EEC4A65E2B277E8D00F7C0AA /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */, B66260E729ACAE4B00E9E3EE /* NavigationHotkeyHandler.swift in Sources */, - 3706FADF293F65D500E42796 /* AddBookmarkFolderModalView.swift in Sources */, 1DDD3EC52B84F96B004CBF2B /* CookiePopupProtectionPreferences.swift in Sources */, 3706FAE0293F65D500E42796 /* BookmarkSidebarTreeController.swift in Sources */, 3706FAE1293F65D500E42796 /* HomePageFavoritesModel.swift in Sources */, @@ -10404,7 +10398,6 @@ B66260E829ACD0C900E9E3EE /* DuckPlayerTabExtension.swift in Sources */, 3706FBAA293F65D500E42796 /* HoverUserScript.swift in Sources */, 3706FBAC293F65D500E42796 /* MainMenuActions.swift in Sources */, - 1EA7B8D92B7E1283000330A4 /* SubscriptionFeatureAvailability.swift in Sources */, 4BF97AD92B43C5C000EB4240 /* Bundle+VPN.swift in Sources */, 3706FBAE293F65D500E42796 /* DataImport.swift in Sources */, 3706FBAF293F65D500E42796 /* FireproofDomains.xcdatamodeld in Sources */, @@ -11718,7 +11711,6 @@ 4B957AF22AC7AE700062CA31 /* DailyPixel.swift in Sources */, 9FDA6C232B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */, 4B957AF32AC7AE700062CA31 /* NavigationHotkeyHandler.swift in Sources */, - 1EA7B8DA2B7E1283000330A4 /* SubscriptionFeatureAvailability.swift in Sources */, 4B957AF42AC7AE700062CA31 /* ClickToLoadUserScript.swift in Sources */, 4B957AF52AC7AE700062CA31 /* WindowControllersManager.swift in Sources */, 4B957AF62AC7AE700062CA31 /* FireAnimationView.swift in Sources */, @@ -11757,9 +11749,8 @@ B66CA4212AD910B300447CF0 /* DataImportView.swift in Sources */, 4B957B162AC7AE700062CA31 /* Downloads.xcdatamodeld in Sources */, 4B957B172AC7AE700062CA31 /* TabPreviewViewController.swift in Sources */, - B6F9BDDA2B45B7D900677B33 /* AddBookmarkModalViewModel.swift in Sources */, 4B957B182AC7AE700062CA31 /* PreferencesDataClearingView.swift in Sources */, - 4B957B182AC7AE700062CA31 /* PreferencesPrivacyView.swift in Sources */, + 4B957B182AC7AE700062CA31 /* PreferencesDataClearingView.swift in Sources */, 4B957B192AC7AE700062CA31 /* NSPasteboardExtension.swift in Sources */, 4B957B1A2AC7AE700062CA31 /* OnboardingViewModel.swift in Sources */, 4B957B1B2AC7AE700062CA31 /* ScriptSourceProviding.swift in Sources */, @@ -12085,7 +12076,6 @@ 1D1A33492A6FEB170080ACED /* BurnerMode.swift in Sources */, 14505A08256084EF00272CC6 /* UserAgent.swift in Sources */, 987799F12999993C005D8EB6 /* LegacyBookmarkStore.swift in Sources */, - B6F9BDE02B45C1A800677B33 /* AddBookmarkFolderModalViewModel.swift in Sources */, 1D220BF82B86192200F8BBC6 /* PreferencesEmailProtectionView.swift in Sources */, 4B8AC93526B3B2FD00879451 /* NSAlert+DataImport.swift in Sources */, AA7412BD24D2BEEE00D22FE0 /* MainWindow.swift in Sources */, @@ -12782,7 +12772,6 @@ 9F514F912B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift in Sources */, 9FA173E32B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift in Sources */, 37534CA8281198CD002621E7 /* AdjacentItemEnumerator.swift in Sources */, - 1EA7B8D82B7E1283000330A4 /* SubscriptionFeatureAvailability.swift in Sources */, 987799F62999996B005D8EB6 /* BookmarkDatabase.swift in Sources */, 4BE53374286E39F10019DBFD /* ChromiumKeychainPrompt.swift in Sources */, B6553692268440D700085A79 /* WKProcessPool+GeolocationProvider.swift in Sources */, diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 462aa74011..91ab528b64 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "branch" : "126.2.0", - "revision" : "0c73586c2628381b8a63be65fd2bc1824e58d7f9" + "revision" : "0c73586c2628381b8a63be65fd2bc1824e58d7f9", + "version" : "126.2.0" } }, { diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index e4f938425a..0be2485308 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -81,6 +81,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate, FileDownloadManagerDel let bookmarksManager = LocalBookmarkManager.shared var privacyDashboardWindow: NSWindow? +#if SUBSCRIPTION + let subscriptionFeatureAvailability: SubscriptionFeatureAvailability +#endif + #if NETWORK_PROTECTION && SUBSCRIPTION // Needs to be lazy as indirectly depends on AppDelegate private lazy var networkProtectionSubscriptionEventHandler = NetworkProtectionSubscriptionEventHandler() @@ -183,6 +187,15 @@ final class AppDelegate: NSObject, NSApplicationDelegate, FileDownloadManagerDel featureFlagger = DefaultFeatureFlagger(internalUserDecider: internalUserDecider, privacyConfig: AppPrivacyFeatures.shared.contentBlocking.privacyConfigurationManager.privacyConfig) +#if SUBSCRIPTION + #if APPSTORE || !STRIPE + SubscriptionPurchaseEnvironment.current = .appStore + #else + SubscriptionPurchaseEnvironment.current = .stripe + #endif + subscriptionFeatureAvailability = DefaultSubscriptionFeatureAvailability(privacyConfigurationManager: AppPrivacyFeatures.shared.contentBlocking.privacyConfigurationManager, + purchasePlatform: SubscriptionPurchaseEnvironment.current) +#endif } func applicationWillFinishLaunching(_ notification: Notification) { @@ -247,12 +260,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, FileDownloadManagerDel defaultValue: defaultEnvironment).wrappedValue SubscriptionPurchaseEnvironment.currentServiceEnvironment = currentEnvironment - #if APPSTORE || !STRIPE - SubscriptionPurchaseEnvironment.current = .appStore - #else - SubscriptionPurchaseEnvironment.current = .stripe - #endif - Task { let accountManager = AccountManager() do { diff --git a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift index da443d381e..e11769ae4e 100644 --- a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift +++ b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift @@ -147,6 +147,7 @@ public struct UserDefaultsWrapper { case dataBrokerProtectionTermsAndConditionsAccepted = "data-broker-protection.waitlist-terms-and-conditions.accepted" case shouldShowDBPWaitlistInvitedCardUI = "shouldShowDBPWaitlistInvitedCardUI" + case dataBrokerProtectionCleanedUpFromWaitlistToPrivacyPro = "data-broker-protection.cleaned-up-from-waitlist-to-privacy-pro" // VPN diff --git a/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift b/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift index 11292e7ac3..d16a781a99 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift @@ -35,7 +35,9 @@ struct DataBrokerProtectionAppEvents { let loginItemsManager = LoginItemsManager() let featureVisibility = DefaultDataBrokerProtectionFeatureVisibility() - guard featureVisibility.isFeatureVisible() else { + guard !featureVisibility.cleanUpDBPForPrivacyProIfNecessary() else { return } + + guard featureVisibility.isFeatureVisible() && !featureVisibility.isPrivacyProEnabled() else { featureVisibility.disableAndDeleteForWaitlistUsers() return } @@ -58,7 +60,9 @@ struct DataBrokerProtectionAppEvents { func applicationDidBecomeActive() { let featureVisibility = DefaultDataBrokerProtectionFeatureVisibility() - guard featureVisibility.isFeatureVisible() else { + guard !featureVisibility.cleanUpDBPForPrivacyProIfNecessary() else { return } + + guard featureVisibility.isFeatureVisible() && !featureVisibility.isPrivacyProEnabled() else { featureVisibility.disableAndDeleteForWaitlistUsers() return } diff --git a/DuckDuckGo/DBP/DataBrokerProtectionFeatureVisibility.swift b/DuckDuckGo/DBP/DataBrokerProtectionFeatureVisibility.swift index 9065dfcaf0..3f7f04f97f 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionFeatureVisibility.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionFeatureVisibility.swift @@ -27,22 +27,29 @@ protocol DataBrokerProtectionFeatureVisibility { func isFeatureVisible() -> Bool func disableAndDeleteForAllUsers() func disableAndDeleteForWaitlistUsers() + func isPrivacyProEnabled() -> Bool } struct DefaultDataBrokerProtectionFeatureVisibility: DataBrokerProtectionFeatureVisibility { private let privacyConfigurationManager: PrivacyConfigurationManaging private let featureDisabler: DataBrokerProtectionFeatureDisabling private let pixelHandler: EventMapping + private let userDefaults: UserDefaults + + @UserDefaultsWrapper(key: .dataBrokerProtectionCleanedUpFromWaitlistToPrivacyPro, defaultValue: false) + var dataBrokerProtectionCleanedUpFromWaitlistToPrivacyPro: Bool /// Temporary code to use while we have both redeem flow for diary study users. Should be removed later static var bypassWaitlist = false init(privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager, featureDisabler: DataBrokerProtectionFeatureDisabling = DataBrokerProtectionFeatureDisabler(), - pixelHandler: EventMapping = DataBrokerProtectionPixelsHandler()) { + pixelHandler: EventMapping = DataBrokerProtectionPixelsHandler(), + userDefaults: UserDefaults = .standard) { self.privacyConfigurationManager = privacyConfigurationManager self.featureDisabler = featureDisabler self.pixelHandler = pixelHandler + self.userDefaults = userDefaults } var waitlistIsOngoing: Bool { @@ -84,6 +91,15 @@ struct DefaultDataBrokerProtectionFeatureVisibility: DataBrokerProtectionFeature DataBrokerProtectionWaitlist().waitlistStorage.isWaitlistUser } + func isPrivacyProEnabled() -> Bool { +#if SUBSCRIPTION + return NSApp.delegateTyped.subscriptionFeatureAvailability.isFeatureAvailable +#else + return false +#endif + + } + func disableAndDeleteForAllUsers() { featureDisabler.disableAndDelete() @@ -99,6 +115,19 @@ struct DefaultDataBrokerProtectionFeatureVisibility: DataBrokerProtectionFeature featureDisabler.disableAndDelete() } + /// Returns true if a cleanup was performed, false otherwise + func cleanUpDBPForPrivacyProIfNecessary() -> Bool { + let wasWaitlistUser = DataBrokerProtectionWaitlist().waitlistStorage.getWaitlistInviteCode() != nil + + if isPrivacyProEnabled() && wasWaitlistUser && !dataBrokerProtectionCleanedUpFromWaitlistToPrivacyPro { + disableAndDeleteForWaitlistUsers() + dataBrokerProtectionCleanedUpFromWaitlistToPrivacyPro = true + return true + } else { + return false + } + } + /// If we want to prevent new users from joining the waitlist while still allowing waitlist users to continue using it, /// we should set isWaitlistEnabled to false and isWaitlistBetaActive to true. /// To remove it from everyone, isWaitlistBetaActive should be set to false diff --git a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift index 75bcc66066..bad6b6ab63 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift @@ -347,7 +347,7 @@ final class AddressBarTextField: NSTextField { #endif #if SUBSCRIPTION - if DefaultSubscriptionFeatureAvailability().isFeatureAvailable() { + if NSApp.delegateTyped.subscriptionFeatureAvailability.isFeatureAvailable { if providedUrl.isChild(of: URL.subscriptionBaseURL) || providedUrl.isChild(of: URL.identityTheftRestoration) { self.updateValue(selectedTabViewModel: nil, addressBarString: nil) // reset self.window?.makeFirstResponder(nil) diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift index b2dd2d0a7f..26fc57d85b 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift @@ -318,7 +318,7 @@ final class MoreOptionsMenu: NSMenu { var items: [NSMenuItem] = [] #if SUBSCRIPTION - if DefaultSubscriptionFeatureAvailability().isFeatureAvailable() && !AccountManager().isUserAuthenticated { + if NSApp.delegateTyped.subscriptionFeatureAvailability.isFeatureAvailable && !AccountManager().isUserAuthenticated { items.append(contentsOf: makeInactiveSubscriptionItems()) } else { items.append(contentsOf: makeActiveSubscriptionItems()) // this adds NETP and DBP only if conditionally enabled @@ -345,7 +345,7 @@ final class MoreOptionsMenu: NSMenu { items.append(networkProtectionItem) #if SUBSCRIPTION - if DefaultSubscriptionFeatureAvailability().isFeatureAvailable() && AccountManager().isUserAuthenticated { + if NSApp.delegateTyped.subscriptionFeatureAvailability.isFeatureAvailable && AccountManager().isUserAuthenticated { Task { let isMenuItemEnabled: Bool @@ -368,7 +368,8 @@ final class MoreOptionsMenu: NSMenu { #endif // NETWORK_PROTECTION #if DBP - if DefaultDataBrokerProtectionFeatureVisibility().isFeatureVisible() { + let dbpVisibility = DefaultDataBrokerProtectionFeatureVisibility() + if dbpVisibility.isFeatureVisible() || dbpVisibility.isPrivacyProEnabled() { let dataBrokerProtectionItem = NSMenuItem(title: UserText.dataBrokerProtectionOptionsMenuItem, action: #selector(openDataBrokerProtection), keyEquivalent: "") @@ -377,7 +378,7 @@ final class MoreOptionsMenu: NSMenu { items.append(dataBrokerProtectionItem) #if SUBSCRIPTION - if DefaultSubscriptionFeatureAvailability().isFeatureAvailable() && AccountManager().isUserAuthenticated { + if NSApp.delegateTyped.subscriptionFeatureAvailability.isFeatureAvailable && AccountManager().isUserAuthenticated { Task { let isMenuItemEnabled: Bool @@ -409,7 +410,7 @@ final class MoreOptionsMenu: NSMenu { .withImage(.itrIcon) items.append(identityTheftRestorationItem) - if DefaultSubscriptionFeatureAvailability().isFeatureAvailable() && AccountManager().isUserAuthenticated { + if NSApp.delegateTyped.subscriptionFeatureAvailability.isFeatureAvailable && AccountManager().isUserAuthenticated { Task { let isMenuItemEnabled: Bool diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index fbe69492b5..bfb86ac0e1 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -327,7 +327,7 @@ final class NavigationBarViewController: NSViewController { } #if SUBSCRIPTION - if DefaultSubscriptionFeatureAvailability().isFeatureAvailable() { + if NSApp.delegateTyped.subscriptionFeatureAvailability.isFeatureAvailable { let accountManager = AccountManager() let networkProtectionTokenStorage = NetworkProtectionKeychainTokenStore() diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift index 8eed556fac..9a961818cc 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift @@ -28,6 +28,7 @@ import Subscription extension NetworkProtectionDeviceManager { + @MainActor static func create() -> NetworkProtectionDeviceManager { let settings = VPNSettings(defaults: .netP) let keyStore = NetworkProtectionKeychainKeyStore() @@ -36,7 +37,7 @@ extension NetworkProtectionDeviceManager { tokenStore: tokenStore, keyStore: keyStore, errorEvents: .networkProtectionAppDebugEvents, - isSubscriptionEnabled: DefaultSubscriptionFeatureAvailability().isFeatureAvailable()) + isSubscriptionEnabled: NSApp.delegateTyped.subscriptionFeatureAvailability.isFeatureAvailable) } } @@ -46,7 +47,7 @@ extension NetworkProtectionCodeRedemptionCoordinator { self.init(environment: settings.selectedEnvironment, tokenStore: NetworkProtectionKeychainTokenStore(), errorEvents: .networkProtectionAppDebugEvents, - isSubscriptionEnabled: DefaultSubscriptionFeatureAvailability().isFeatureAvailable()) + isSubscriptionEnabled: NSApp.delegateTyped.subscriptionFeatureAvailability.isFeatureAvailable) } } @@ -59,7 +60,7 @@ extension NetworkProtectionKeychainTokenStore { #endif self.init(keychainType: .default, errorEvents: .networkProtectionAppDebugEvents, - isSubscriptionEnabled: DefaultSubscriptionFeatureAvailability().isFeatureAvailable(), + isSubscriptionEnabled: NSApp.delegateTyped.subscriptionFeatureAvailability.isFeatureAvailable, accessTokenProvider: accessTokenProvider) } } @@ -78,7 +79,7 @@ extension NetworkProtectionLocationListCompositeRepository { environment: settings.selectedEnvironment, tokenStore: NetworkProtectionKeychainTokenStore(), errorEvents: .networkProtectionAppDebugEvents, - isSubscriptionEnabled: DefaultSubscriptionFeatureAvailability().isFeatureAvailable() + isSubscriptionEnabled: NSApp.delegateTyped.subscriptionFeatureAvailability.isFeatureAvailable ) } } diff --git a/DuckDuckGo/Preferences/Model/PreferencesSection.swift b/DuckDuckGo/Preferences/Model/PreferencesSection.swift index 785350403e..b05bcc4dd9 100644 --- a/DuckDuckGo/Preferences/Model/PreferencesSection.swift +++ b/DuckDuckGo/Preferences/Model/PreferencesSection.swift @@ -60,7 +60,7 @@ struct PreferencesSection: Hashable, Identifiable { ] #if SUBSCRIPTION - if DefaultSubscriptionFeatureAvailability().isFeatureAvailable() { + if NSApp.delegateTyped.subscriptionFeatureAvailability.isFeatureAvailable { let subscriptionPanes: [PreferencePaneIdentifier] = [.subscription] sections.insert(.init(id: .privacyPro, panes: subscriptionPanes), at: 1) } diff --git a/DuckDuckGo/Subscription/SubscriptionFeatureAvailability.swift b/DuckDuckGo/Subscription/SubscriptionFeatureAvailability.swift deleted file mode 100644 index 9090ffad02..0000000000 --- a/DuckDuckGo/Subscription/SubscriptionFeatureAvailability.swift +++ /dev/null @@ -1,76 +0,0 @@ -// -// SubscriptionFeatureAvailability.swift -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import AppKit - -#if SUBSCRIPTION -import Subscription -#endif - -#if NETWORK_PROTECTION -import NetworkProtection -import BrowserServicesKit -#endif - -protocol SubscriptionFeatureAvailability { - func isFeatureAvailable() -> Bool -} - -struct DefaultSubscriptionFeatureAvailability: SubscriptionFeatureAvailability { - - func isFeatureAvailable() -> Bool { -#if SUBSCRIPTION_OVERRIDE_ENABLED - return true -#elseif SUBSCRIPTION - print("isUserAuthenticated: [\(AccountManager().isUserAuthenticated)] | isSubscriptionInternalTestingEnabled: [\(isSubscriptionInternalTestingEnabled)] isInternalUser: [\(isInternalUser)] | isDBPActivated: [\(isDBPActivated)]") - return AccountManager().isUserAuthenticated || (isSubscriptionInternalTestingEnabled && isInternalUser && !isDBPActivated) -#else - return false -#endif - } - - private var isSubscriptionInternalTestingEnabled: Bool { - UserDefaultsWrapper(key: .subscriptionInternalTesting, defaultValue: false).wrappedValue - } - - private var isInternalUser: Bool { - Self.internalUserDecider.isInternalUser - } - - private var isDBPActivated: Bool { -#if DBP - return DataBrokerProtectionManager.shared.dataManager.fetchProfile(ignoresCache: true) != nil -#else - return false -#endif - } - - private static var internalUserDecider: InternalUserDecider = { - let keyStore = EncryptionKeyStore() - let fileStore: FileStore - do { - let encryptionKey = NSApplication.runType.requiresEnvironment ? try keyStore.readKey() : nil - fileStore = EncryptedFileStore(encryptionKey: encryptionKey) - } catch { - fileStore = EncryptedFileStore() - } - - let internalUserDeciderStore = InternalUserDeciderStore(fileStore: fileStore) - return DefaultInternalUserDecider(store: internalUserDeciderStore) - }() -} diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift index 3c08e86e73..f270783d8d 100644 --- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift @@ -185,6 +185,8 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { } func getSubscriptionOptions(params: Any, original: WKScriptMessage) async throws -> Encodable? { + guard await NSApp.delegateTyped.subscriptionFeatureAvailability.isSubscriptionPurchaseAllowed else { return SubscriptionOptions.empty } + if SubscriptionPurchaseEnvironment.current == .appStore { if #available(macOS 12.0, *) { switch await AppStorePurchaseFlow.subscriptionOptions() { diff --git a/DuckDuckGo/Tab/UserScripts/UserScripts.swift b/DuckDuckGo/Tab/UserScripts/UserScripts.swift index 4dfa1c5b50..d4cb62cdce 100644 --- a/DuckDuckGo/Tab/UserScripts/UserScripts.swift +++ b/DuckDuckGo/Tab/UserScripts/UserScripts.swift @@ -87,7 +87,7 @@ final class UserScripts: UserScriptsProvider { } #if SUBSCRIPTION - if DefaultSubscriptionFeatureAvailability().isFeatureAvailable() { + if NSApp.delegateTyped.subscriptionFeatureAvailability.isFeatureAvailable { subscriptionPagesUserScript.registerSubfeature(delegate: SubscriptionPagesUseSubscriptionFeature()) userScripts.append(subscriptionPagesUserScript) diff --git a/DuckDuckGo/Waitlist/Views/WaitlistViewControllerPresenter.swift b/DuckDuckGo/Waitlist/Views/WaitlistViewControllerPresenter.swift index fba944d198..f51b0b695c 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistViewControllerPresenter.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistViewControllerPresenter.swift @@ -81,6 +81,11 @@ struct NetworkProtectionWaitlistViewControllerPresenter: WaitlistViewControllerP struct DataBrokerProtectionWaitlistViewControllerPresenter: WaitlistViewControllerPresenter { static func shouldPresentWaitlist() -> Bool { +#if SUBSCRIPTION + if NSApp.delegateTyped.subscriptionFeatureAvailability.isFeatureAvailable { + return false + } +#endif let waitlist = DataBrokerProtectionWaitlist() let accepted = UserDefaults().bool(forKey: UserDefaultsWrapper.Key.dataBrokerProtectionTermsAndConditionsAccepted.rawValue) diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 51a9a0276c..25aa35182d 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -29,7 +29,7 @@ let package = Package( targets: ["DataBrokerProtection"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.2.0"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 85d865a47b..2dc7d68c5c 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.2.0"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 90909e471e..f2568a8053 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.2.0"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/LocalPackages/SyncUI/Package.swift b/LocalPackages/SyncUI/Package.swift index b01805eb86..1728c280de 100644 --- a/LocalPackages/SyncUI/Package.swift +++ b/LocalPackages/SyncUI/Package.swift @@ -15,7 +15,7 @@ let package = Package( dependencies: [ .package(path: "../SwiftUIExtensions"), .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "2.0.0"), - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", branch: "126.2.0") + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.2.0") ], targets: [ .target( diff --git a/LocalPackages/SystemExtensionManager/Package.swift b/LocalPackages/SystemExtensionManager/Package.swift index 53f7b3e21b..b088e62b79 100644 --- a/LocalPackages/SystemExtensionManager/Package.swift +++ b/LocalPackages/SystemExtensionManager/Package.swift @@ -17,7 +17,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "2.0.0"), - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", branch: "126.2.0") + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.2.0") ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. From 04c2026176488b70cb83b26dddfac9474b46fe9e Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Tue, 19 Mar 2024 22:05:16 +0000 Subject: [PATCH 43/51] Connect thank you message to DBP method (#2456) Task/Issue URL: https://app.asana.com/0/1204167627774280/1206824862073996/f Tech Design URL: CC: **Description**: Connect thank you message to DBP method --- .../DBP/DataBrokerProtectionFeatureVisibility.swift | 11 +++++++++-- .../Views/WaitlistThankYouPromptPresenter.swift | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo/DBP/DataBrokerProtectionFeatureVisibility.swift b/DuckDuckGo/DBP/DataBrokerProtectionFeatureVisibility.swift index 3f7f04f97f..46de122bd1 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionFeatureVisibility.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionFeatureVisibility.swift @@ -28,6 +28,7 @@ protocol DataBrokerProtectionFeatureVisibility { func disableAndDeleteForAllUsers() func disableAndDeleteForWaitlistUsers() func isPrivacyProEnabled() -> Bool + func isEligibleForThankYouMessage() -> Bool } struct DefaultDataBrokerProtectionFeatureVisibility: DataBrokerProtectionFeatureVisibility { @@ -91,6 +92,10 @@ struct DefaultDataBrokerProtectionFeatureVisibility: DataBrokerProtectionFeature DataBrokerProtectionWaitlist().waitlistStorage.isWaitlistUser } + private var wasWaitlistUser: Bool { + DataBrokerProtectionWaitlist().waitlistStorage.getWaitlistInviteCode() != nil + } + func isPrivacyProEnabled() -> Bool { #if SUBSCRIPTION return NSApp.delegateTyped.subscriptionFeatureAvailability.isFeatureAvailable @@ -100,6 +105,10 @@ struct DefaultDataBrokerProtectionFeatureVisibility: DataBrokerProtectionFeature } + func isEligibleForThankYouMessage() -> Bool { + return wasWaitlistUser && isPrivacyProEnabled() + } + func disableAndDeleteForAllUsers() { featureDisabler.disableAndDelete() @@ -117,8 +126,6 @@ struct DefaultDataBrokerProtectionFeatureVisibility: DataBrokerProtectionFeature /// Returns true if a cleanup was performed, false otherwise func cleanUpDBPForPrivacyProIfNecessary() -> Bool { - let wasWaitlistUser = DataBrokerProtectionWaitlist().waitlistStorage.getWaitlistInviteCode() != nil - if isPrivacyProEnabled() && wasWaitlistUser && !dataBrokerProtectionCleanedUpFromWaitlistToPrivacyPro { disableAndDeleteForWaitlistUsers() dataBrokerProtectionCleanedUpFromWaitlistToPrivacyPro = true diff --git a/DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift b/DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift index 81fa1f9179..78eecff705 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift @@ -34,7 +34,7 @@ final class WaitlistThankYouPromptPresenter { self.init(isVPNBetaTester: { return false }, isPIRBetaTester: { - return false + return DefaultDataBrokerProtectionFeatureVisibility().isEligibleForThankYouMessage() }) } From 0aca432265bedf726cc03d42286176d7fb9248ef Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Wed, 20 Mar 2024 18:15:02 +1100 Subject: [PATCH 44/51] Update Pointfree snapshot testing library to 1.15.4 (#2449) Task/Issue URL: https://app.asana.com/0/0/1206873900527044/f **Description**: Update Pointfree snapshot library to 1.15.4 --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 6329646836..d36c43f513 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -14109,7 +14109,7 @@ repositoryURL = "https://github.com/pointfreeco/swift-snapshot-testing"; requirement = { kind = exactVersion; - version = 1.15.3; + version = 1.15.4; }; }; B6DA44152616C13800DD1EC2 /* XCRemoteSwiftPackageReference "OHHTTPStubs" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 91ab528b64..fbe5e5ab94 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -131,8 +131,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-snapshot-testing", "state" : { - "revision" : "e7b77228b34057041374ebef00c0fd7739d71a2b", - "version" : "1.15.3" + "revision" : "5b0c434778f2c1a4c9b5ebdb8682b28e84dd69bd", + "version" : "1.15.4" } }, { From 933a0e01973c7efb64766074846f760c673e133f Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Wed, 20 Mar 2024 12:56:58 +0100 Subject: [PATCH 45/51] Ensure smooth subs updates (#2424) Task/Issue URL: https://app.asana.com/0/1199230911884351/1206820787941753/f ## Description When upgrading from regular use to subscription, we're presenting a "Thank You" message to the user while disabling the VPN app. --- DuckDuckGo.xcodeproj/project.pbxproj | 31 +++++++-- DuckDuckGo/Application/AppDelegate.swift | 3 +- DuckDuckGo/Menus/MainMenuActions.swift | 6 ++ ...rkProtection+ConvenienceInitializers.swift | 6 +- .../NetworkProtectionAppEvents.swift | 21 +++--- ...atureAvailability+DefaultInitializer.swift | 30 +++++++++ .../NetworkProtectionFeatureDisabler.swift | 1 + .../NetworkProtectionFeatureVisibility.swift | 67 +++++++++++++++++-- .../Waitlist/UserDefaults+vpnLegacyUser.swift | 40 +++++++++++ .../WaitlistThankYouPromptPresenter.swift | 2 +- LocalPackages/LoginItems/Package.swift | 1 - .../UserDefault+VPNEnabledViaWaitlist.swift | 44 ------------ LocalPackages/SyncUI/Package.swift | 1 - .../SystemExtensionManager/Package.swift | 1 - UnitTests/Menus/MoreOptionsMenuTests.swift | 10 ++- 15 files changed, 192 insertions(+), 72 deletions(-) create mode 100644 DuckDuckGo/Subscription/DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift create mode 100644 DuckDuckGo/Waitlist/UserDefaults+vpnLegacyUser.swift delete mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserDefault+VPNEnabledViaWaitlist.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index d36c43f513..acd1c8bb72 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2234,6 +2234,7 @@ 7B31FD902AD1257B0086AA24 /* NetworkProtectionIPC in Frameworks */ = {isa = PBXBuildFile; productRef = 7B31FD8F2AD1257B0086AA24 /* NetworkProtectionIPC */; }; 7B3618C22ADE75C8000D6154 /* NetworkProtectionNavBarPopoverManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B3618C12ADE75C8000D6154 /* NetworkProtectionNavBarPopoverManager.swift */; }; 7B3618C52ADE77D3000D6154 /* NetworkProtectionNavBarPopoverManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B3618C12ADE75C8000D6154 /* NetworkProtectionNavBarPopoverManager.swift */; }; + 7B37C7A52BAA32A50062546A /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = 7B37C7A42BAA32A50062546A /* Subscription */; }; 7B430EA12A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B430EA02A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift */; }; 7B430EA22A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B430EA02A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift */; }; 7B4CE8E726F02135009134B1 /* TabBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4CE8E626F02134009134B1 /* TabBarTests.swift */; }; @@ -2242,6 +2243,9 @@ 7B624F172BA25C1F00A6C544 /* NetworkProtectionUI in Frameworks */ = {isa = PBXBuildFile; productRef = 7B624F162BA25C1F00A6C544 /* NetworkProtectionUI */; }; 7B7DFB202B7E736B009EA1A3 /* MacPacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */; }; 7B7DFB222B7E7473009EA1A3 /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = 7B7DFB212B7E7473009EA1A3 /* Networking */; }; + 7B7FCD0F2BA33B2700C04FBE /* UserDefaults+vpnLegacyUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7FCD0E2BA33B2700C04FBE /* UserDefaults+vpnLegacyUser.swift */; }; + 7B7FCD102BA33B2700C04FBE /* UserDefaults+vpnLegacyUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7FCD0E2BA33B2700C04FBE /* UserDefaults+vpnLegacyUser.swift */; }; + 7B7FCD112BA33B2700C04FBE /* UserDefaults+vpnLegacyUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7FCD0E2BA33B2700C04FBE /* UserDefaults+vpnLegacyUser.swift */; }; 7B8C083C2AE1268E00F4C67F /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7B8C083B2AE1268E00F4C67F /* PixelKit */; }; 7B8DB31A2B504D7500EC16DA /* VPNAppEventsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8DB3192B504D7500EC16DA /* VPNAppEventsHandler.swift */; }; 7B934C412A866DD400FC8F9C /* UserDefaults+NetworkProtectionShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B934C402A866DD400FC8F9C /* UserDefaults+NetworkProtectionShared.swift */; }; @@ -2288,6 +2292,9 @@ 7BAF9E4C2A8A3CCA002D3B6E /* UserDefaults+NetworkProtectionShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B934C402A866DD400FC8F9C /* UserDefaults+NetworkProtectionShared.swift */; }; 7BAF9E4D2A8A3CCB002D3B6E /* UserDefaults+NetworkProtectionShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B934C402A866DD400FC8F9C /* UserDefaults+NetworkProtectionShared.swift */; }; 7BB108592A43375D000AB95F /* PFMoveApplication.m in Sources */ = {isa = PBXBuildFile; fileRef = 7BB108582A43375D000AB95F /* PFMoveApplication.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + 7BBA7CE62BAB03C1007579A3 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBA7CE52BAB03C1007579A3 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift */; }; + 7BBA7CE72BAB03C1007579A3 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBA7CE52BAB03C1007579A3 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift */; }; + 7BBA7CEA2BAB03C1007579A3 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBA7CE52BAB03C1007579A3 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift */; }; 7BBD44282AD730A400D0A064 /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7BBD44272AD730A400D0A064 /* PixelKit */; }; 7BBD45B12A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBD45B02A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift */; }; 7BBD45B22A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBD45B02A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift */; }; @@ -3227,7 +3234,6 @@ EE3424612BA0853900173B1B /* VPNUninstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE34245D2BA0853900173B1B /* VPNUninstaller.swift */; }; EE66418C2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE66418B2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift */; }; EE66418D2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE66418B2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift */; }; - EE66418F2B9B1BD1005BCD17 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = EE66418E2B9B1BD1005BCD17 /* Subscription */; }; EE66666F2B56EDE4001D898D /* VPNLocationsHostingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE66666E2B56EDE4001D898D /* VPNLocationsHostingViewController.swift */; }; EE6666702B56EDE4001D898D /* VPNLocationsHostingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE66666E2B56EDE4001D898D /* VPNLocationsHostingViewController.swift */; }; EE6666712B56EDE4001D898D /* VPNLocationsHostingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE66666E2B56EDE4001D898D /* VPNLocationsHostingViewController.swift */; }; @@ -3977,6 +3983,7 @@ 7B6EC5E42AE2D8AF004FE6DF /* DuckDuckGoDBPAgentAppStore.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = DuckDuckGoDBPAgentAppStore.xcconfig; sourceTree = ""; }; 7B6EC5E52AE2D8AF004FE6DF /* DuckDuckGoDBPAgent.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = DuckDuckGoDBPAgent.xcconfig; sourceTree = ""; }; 7B76E6852AD5D77600186A84 /* XPCHelper */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = XPCHelper; sourceTree = ""; }; + 7B7FCD0E2BA33B2700C04FBE /* UserDefaults+vpnLegacyUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+vpnLegacyUser.swift"; sourceTree = ""; }; 7B8DB3192B504D7500EC16DA /* VPNAppEventsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNAppEventsHandler.swift; sourceTree = ""; }; 7B934C3D2A866CFF00FC8F9C /* NetworkProtectionOnboardingMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionOnboardingMenu.swift; sourceTree = ""; }; 7B934C402A866DD400FC8F9C /* UserDefaults+NetworkProtectionShared.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UserDefaults+NetworkProtectionShared.swift"; sourceTree = ""; }; @@ -3996,6 +4003,7 @@ 7BA7CC4D2AD11F6F0042E5CE /* NetworkProtectionIPCTunnelController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionIPCTunnelController.swift; sourceTree = ""; }; 7BB108572A43375D000AB95F /* PFMoveApplication.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFMoveApplication.h; sourceTree = ""; }; 7BB108582A43375D000AB95F /* PFMoveApplication.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFMoveApplication.m; sourceTree = ""; }; + 7BBA7CE52BAB03C1007579A3 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift"; sourceTree = ""; }; 7BBD45B02A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionDebugUtilities.swift; sourceTree = ""; }; 7BD01C182AD8319C0088B32E /* IPCServiceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPCServiceManager.swift; sourceTree = ""; }; 7BD1688D2AD4A4C400D24876 /* NetworkExtensionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkExtensionController.swift; sourceTree = ""; }; @@ -4745,9 +4753,9 @@ buildActionMask = 2147483647; files = ( 37269F012B332FC8005E8E46 /* Common in Frameworks */, - EE66418F2B9B1BD1005BCD17 /* Subscription in Frameworks */, EE7295E92A545BC4008C0991 /* NetworkProtection in Frameworks */, 4B2537772A11BFE100610219 /* PixelKit in Frameworks */, + 7B37C7A52BAA32A50062546A /* Subscription in Frameworks */, 7BBE2B7B2B61663C00697445 /* NetworkProtectionProxy in Frameworks */, 4B2D062C2A11C0E100DE1F49 /* Networking in Frameworks */, 4B25375B2A11BE7300610219 /* NetworkExtension.framework in Frameworks */, @@ -5131,6 +5139,7 @@ isa = PBXGroup; children = ( EE0629712B90EE8C00D868B4 /* AccountManagerExtension.swift */, + 7BBA7CE52BAB03C1007579A3 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift */, ); path = Subscription; sourceTree = ""; @@ -6028,6 +6037,7 @@ children = ( 7BD8679A2A9E9E000063B9F7 /* NetworkProtectionFeatureVisibility.swift */, 4B6785432AA8DE1F008A5004 /* NetworkProtectionFeatureDisabler.swift */, + 7B7FCD0E2BA33B2700C04FBE /* UserDefaults+vpnLegacyUser.swift */, 31F2D1FE2AF026D800BF0144 /* WaitlistTermsAndConditionsActionHandler.swift */, 31C9ADE42AF0564500CEF57D /* WaitlistFeatureSetupHandler.swift */, 4B9DB0072A983B23000927DB /* Waitlist.swift */, @@ -8817,7 +8827,7 @@ EE7295E82A545BC4008C0991 /* NetworkProtection */, 37269F002B332FC8005E8E46 /* Common */, 7BBE2B7A2B61663C00697445 /* NetworkProtectionProxy */, - EE66418E2B9B1BD1005BCD17 /* Subscription */, + 7B37C7A42BAA32A50062546A /* Subscription */, ); productName = NetworkProtectionSystemExtension; productReference = 4B25375A2A11BE7300610219 /* com.duckduckgo.macos.vpn.network-extension.debug.systemextension */; @@ -10298,6 +10308,7 @@ 9FEE986A2B85B869002E44E8 /* BookmarksDialogViewModel.swift in Sources */, 3706FB6A293F65D500E42796 /* AddressBarButton.swift in Sources */, 4B41EDA42B1543B9001EEDF4 /* VPNPreferencesModel.swift in Sources */, + 7B7FCD102BA33B2700C04FBE /* UserDefaults+vpnLegacyUser.swift in Sources */, 3706FB6C293F65D500E42796 /* FaviconStore.swift in Sources */, 3706FB6D293F65D500E42796 /* SuggestionListCharacteristics.swift in Sources */, 377D801F2AB48191002AF251 /* FavoritesDisplayModeSyncHandler.swift in Sources */, @@ -10562,6 +10573,7 @@ 3706FC19293F65D500E42796 /* NSNotificationName+Favicons.swift in Sources */, 3706FC1A293F65D500E42796 /* PinningManager.swift in Sources */, 4B37EE782B4CFF3900A89A61 /* DataBrokerProtectionRemoteMessage.swift in Sources */, + 7BBA7CE72BAB03C1007579A3 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift in Sources */, 3706FC1B293F65D500E42796 /* TabCollectionViewModel+NSSecureCoding.swift in Sources */, 3706FC1D293F65D500E42796 /* EmailManagerRequestDelegate.swift in Sources */, 3706FC1E293F65D500E42796 /* ApplicationVersionReader.swift in Sources */, @@ -11551,6 +11563,7 @@ 4B957A662AC7AE700062CA31 /* SuggestionListCharacteristics.swift in Sources */, 4B957A672AC7AE700062CA31 /* TimeIntervalExtension.swift in Sources */, 4B957A682AC7AE700062CA31 /* NetworkProtectionFeatureDisabler.swift in Sources */, + 7BBA7CEA2BAB03C1007579A3 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift in Sources */, 4B957A692AC7AE700062CA31 /* BookmarkListViewController.swift in Sources */, 4B957A6A2AC7AE700062CA31 /* SecureVaultLoginImporter.swift in Sources */, 4B957A6B2AC7AE700062CA31 /* WKProcessPoolExtension.swift in Sources */, @@ -11961,6 +11974,7 @@ 4B957BCB2AC7AE700062CA31 /* PreferencesAppearanceView.swift in Sources */, 4B957BCC2AC7AE700062CA31 /* NSMenuItemExtension.swift in Sources */, 4B957BCD2AC7AE700062CA31 /* ContiguousBytesExtension.swift in Sources */, + 7B7FCD112BA33B2700C04FBE /* UserDefaults+vpnLegacyUser.swift in Sources */, 4B957BCE2AC7AE700062CA31 /* AdjacentItemEnumerator.swift in Sources */, 4B957BCF2AC7AE700062CA31 /* BookmarkDatabase.swift in Sources */, 4B957BD02AC7AE700062CA31 /* ChromiumKeychainPrompt.swift in Sources */, @@ -12204,6 +12218,7 @@ B68C92C1274E3EF4002AC6B0 /* PopUpWindow.swift in Sources */, AA5FA6A0275F948900DCE9C9 /* Favicons.xcdatamodeld in Sources */, 3158B1502B0BF75200AF130C /* DataBrokerProtectionLoginItemScheduler.swift in Sources */, + 7BBA7CE62BAB03C1007579A3 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift in Sources */, B684592225C93BE000DC17B6 /* Publisher.asVoid.swift in Sources */, 4B9DB01D2A983B24000927DB /* Waitlist.swift in Sources */, BBDFDC5A2B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift in Sources */, @@ -12772,6 +12787,7 @@ 9F514F912B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift in Sources */, 9FA173E32B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift in Sources */, 37534CA8281198CD002621E7 /* AdjacentItemEnumerator.swift in Sources */, + 7B7FCD0F2BA33B2700C04FBE /* UserDefaults+vpnLegacyUser.swift in Sources */, 987799F62999996B005D8EB6 /* BookmarkDatabase.swift in Sources */, 4BE53374286E39F10019DBFD /* ChromiumKeychainPrompt.swift in Sources */, B6553692268440D700085A79 /* WKProcessPool+GeolocationProvider.swift in Sources */, @@ -14545,6 +14561,11 @@ isa = XCSwiftPackageProductDependency; productName = NetworkProtectionIPC; }; + 7B37C7A42BAA32A50062546A /* Subscription */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Subscription; + }; 7B5DD6992AE51FFA001DE99C /* PixelKit */ = { isa = XCSwiftPackageProductDependency; productName = PixelKit; @@ -14807,10 +14828,6 @@ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = Subscription; }; - EE66418E2B9B1BD1005BCD17 /* Subscription */ = { - isa = XCSwiftPackageProductDependency; - productName = Subscription; - }; EE7295E22A545B9A008C0991 /* NetworkProtection */ = { isa = XCSwiftPackageProductDependency; package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index 0be2485308..1afd63ec5d 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -193,8 +193,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, FileDownloadManagerDel #else SubscriptionPurchaseEnvironment.current = .stripe #endif - subscriptionFeatureAvailability = DefaultSubscriptionFeatureAvailability(privacyConfigurationManager: AppPrivacyFeatures.shared.contentBlocking.privacyConfigurationManager, - purchasePlatform: SubscriptionPurchaseEnvironment.current) + subscriptionFeatureAvailability = DefaultSubscriptionFeatureAvailability() #endif } diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index 3a926dbbcc..0800b3af45 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -716,6 +716,11 @@ extension MainViewController { guard let internalUserDecider = NSApp.delegateTyped.internalUserDecider as? DefaultInternalUserDecider else { return } let state = internalUserDecider.isInternalUser internalUserDecider.debugSetInternalUserState(!state) + + // Aid to transition VPN from waitlist to subscription + // by resetting this we allow users to go back to waitlist + // and re-test. + resetThankYouModalChecks(nil) } @objc func resetDailyPixels(_ sender: Any?) { @@ -782,6 +787,7 @@ extension MainViewController { @objc func resetThankYouModalChecks(_ sender: Any?) { let presenter = WaitlistThankYouPromptPresenter() presenter.resetPromptCheck() + UserDefaults.netP.removeObject(forKey: UserDefaults.vpnLegacyUserAccessDisabledOnceKey) } @objc func showVPNThankYouModal(_ sender: Any?) { diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift index 9a961818cc..113f737b58 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift @@ -53,6 +53,10 @@ extension NetworkProtectionCodeRedemptionCoordinator { extension NetworkProtectionKeychainTokenStore { convenience init() { + self.init(isSubscriptionEnabled: NSApp.delegateTyped.subscriptionFeatureAvailability.isFeatureAvailable) + } + + convenience init(isSubscriptionEnabled: Bool) { #if SUBSCRIPTION let accessTokenProvider: () -> String? = { AccountManager().accessToken } #else @@ -60,7 +64,7 @@ extension NetworkProtectionKeychainTokenStore { #endif self.init(keychainType: .default, errorEvents: .networkProtectionAppDebugEvents, - isSubscriptionEnabled: NSApp.delegateTyped.subscriptionFeatureAvailability.isFeatureAvailable, + isSubscriptionEnabled: isSubscriptionEnabled, accessTokenProvider: accessTokenProvider) } } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift index 23d20a89d2..2d240b6c0d 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift @@ -47,8 +47,15 @@ final class NetworkProtectionAppEvents { // MARK: - Feature Visibility private let featureVisibility: NetworkProtectionFeatureVisibility + private let featureDisabler: NetworkProtectionFeatureDisabling + private let defaults: UserDefaults - init(featureVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility()) { + init(featureVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(), + featureDisabler: NetworkProtectionFeatureDisabling = NetworkProtectionFeatureDisabler(), + defaults: UserDefaults = .netP) { + + self.defaults = defaults + self.featureDisabler = featureDisabler self.featureVisibility = featureVisibility } @@ -58,8 +65,9 @@ final class NetworkProtectionAppEvents { let loginItemsManager = LoginItemsManager() Task { @MainActor in - if featureVisibility.shouldUninstallAutomatically() { - featureVisibility.disableForAllUsers() + let disabled = await featureVisibility.disableIfUserHasNoAccess() + + guard !disabled else { return } @@ -71,12 +79,9 @@ final class NetworkProtectionAppEvents { /// Call this method when the app becomes active to run the associated NetP logic. /// func applicationDidBecomeActive() { - guard featureVisibility.isNetworkProtectionVisible() else { - UserDefaults.netP.networkProtectionVPNEnabledViaWaitlist = false - featureVisibility.disableForAllUsers() - return + Task { @MainActor in + await featureVisibility.disableIfUserHasNoAccess() } - UserDefaults.netP.networkProtectionVPNEnabledViaWaitlist = true } private func restartNetworkProtectionIfVersionChanged(using loginItemsManager: LoginItemsManager) { diff --git a/DuckDuckGo/Subscription/DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift b/DuckDuckGo/Subscription/DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift new file mode 100644 index 0000000000..3e24a33be7 --- /dev/null +++ b/DuckDuckGo/Subscription/DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift @@ -0,0 +1,30 @@ +// +// DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +#if SUBSCRIPTION +import Subscription +#endif + +extension DefaultSubscriptionFeatureAvailability { + convenience init() { + self.init(privacyConfigurationManager: AppPrivacyFeatures.shared.contentBlocking.privacyConfigurationManager, + purchasePlatform: SubscriptionPurchaseEnvironment.current) + } +} diff --git a/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift b/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift index dd5572e94c..3056dd76a4 100644 --- a/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift +++ b/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift @@ -29,6 +29,7 @@ import SystemExtensions protocol NetworkProtectionFeatureDisabling { /// - Returns: `true` if the uninstallation was completed. `false` if it was cancelled by the user or an error. /// + @discardableResult func disable(keepAuthToken: Bool, uninstallSystemExtension: Bool) async -> Bool } diff --git a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift b/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift index 08d52679a3..255dbd6dff 100644 --- a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift +++ b/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift @@ -26,14 +26,23 @@ import NetworkProtection import NetworkProtectionUI import LoginItems +#if SUBSCRIPTION +import Subscription +#endif + protocol NetworkProtectionFeatureVisibility { + var isEligibleForThankYouMessage: Bool { get } + func isNetworkProtectionVisible() -> Bool func shouldUninstallAutomatically() -> Bool - func disableForAllUsers() + func disableForAllUsers() async func disableForWaitlistUsers() + @discardableResult + func disableIfUserHasNoAccess() async -> Bool } struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { + private static var subscriptionAuthTokenPrefix: String { "ddg:" } private let featureDisabler: NetworkProtectionFeatureDisabling private let featureOverrides: WaitlistBetaOverriding private let networkProtectionFeatureActivation: NetworkProtectionFeatureActivation @@ -70,11 +79,17 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { return isEasterEggUser || waitlistIsOngoing } + /// We've had to add this method because accessing the singleton in app delegate is crashing the integration tests. + /// + var subscriptionFeatureAvailability: DefaultSubscriptionFeatureAvailability { + DefaultSubscriptionFeatureAvailability() + } + /// Returns whether the VPN should be uninstalled automatically. /// This is only true when the user is not an Easter Egg user, the waitlist test has ended, and the user is onboarded. func shouldUninstallAutomatically() -> Bool { #if SUBSCRIPTION - return defaults.networkProtectionEntitlementsExpired && LoginItem.vpnMenu.status.isInstalled + return subscriptionFeatureAvailability.isFeatureAvailable && defaults.networkProtectionEntitlementsExpired && LoginItem.vpnMenu.status.isInstalled #else let waitlistAccessEnded = isWaitlistUser && !waitlistIsOngoing let isNotEasterEggUser = !isEasterEggUser @@ -145,10 +160,22 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { } } - func disableForAllUsers() { - Task { - await featureDisabler.disable(keepAuthToken: false, uninstallSystemExtension: false) + func disableForAllUsers() async { + await featureDisabler.disable(keepAuthToken: true, uninstallSystemExtension: false) + } + + /// Disables the VPN for legacy users, if necessary. + /// + /// This method does not seek to remove tokens or uninstall anything. + /// + private func disableVPNForLegacyUsersIfSubscriptionAvailable() async -> Bool { + guard isEligibleForThankYouMessage && !defaults.vpnLegacyUserAccessDisabledOnce else { + return false } + + defaults.vpnLegacyUserAccessDisabledOnce = true + await featureDisabler.disable(keepAuthToken: true, uninstallSystemExtension: false) + return true } func disableForWaitlistUsers() { @@ -160,6 +187,36 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { await featureDisabler.disable(keepAuthToken: false, uninstallSystemExtension: false) } } + + /// A method meant to be called safely from different places to disable the VPN if the user isn't meant to have access to it. + /// + @discardableResult + func disableIfUserHasNoAccess() async -> Bool { + if shouldUninstallAutomatically() { + await disableForAllUsers() + return true + } + + return await disableVPNForLegacyUsersIfSubscriptionAvailable() + } + + // MARK: - Subscription Start Support + + /// To query whether we're a legacy (waitlist or easter egg) user. + /// + private func isPreSubscriptionUser() -> Bool { + guard let token = try? NetworkProtectionKeychainTokenStore(isSubscriptionEnabled: false).fetchToken() else { + return false + } + + return !token.hasPrefix(Self.subscriptionAuthTokenPrefix) + } + + /// Checks whether the VPN needs to be disabled. + /// + var isEligibleForThankYouMessage: Bool { + isPreSubscriptionUser() && subscriptionFeatureAvailability.isFeatureAvailable + } } #endif diff --git a/DuckDuckGo/Waitlist/UserDefaults+vpnLegacyUser.swift b/DuckDuckGo/Waitlist/UserDefaults+vpnLegacyUser.swift new file mode 100644 index 0000000000..68891bc44b --- /dev/null +++ b/DuckDuckGo/Waitlist/UserDefaults+vpnLegacyUser.swift @@ -0,0 +1,40 @@ +// +// UserDefaults+vpnLegacyUser.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +extension UserDefaults { + static var vpnLegacyUserAccessDisabledOnceKey: String { + "vpnLegacyUserAccessDisabledOnce" + } + + @objc + dynamic var vpnLegacyUserAccessDisabledOnce: Bool { + get { + bool(forKey: Self.vpnLegacyUserAccessDisabledOnceKey) + } + + set { + set(newValue, forKey: Self.vpnLegacyUserAccessDisabledOnceKey) + } + } + + func resetVPNLegacyUserAccessDisabledOnce() { + removeObject(forKey: Self.vpnLegacyUserAccessDisabledOnceKey) + } +} diff --git a/DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift b/DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift index 78eecff705..90555f5a3e 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift @@ -32,7 +32,7 @@ final class WaitlistThankYouPromptPresenter { convenience init() { self.init(isVPNBetaTester: { - return false + return DefaultNetworkProtectionVisibility().isEligibleForThankYouMessage }, isPIRBetaTester: { return DefaultDataBrokerProtectionFeatureVisibility().isEligibleForThankYouMessage() }) diff --git a/LocalPackages/LoginItems/Package.swift b/LocalPackages/LoginItems/Package.swift index 389c3f9d97..b1a72a5caa 100644 --- a/LocalPackages/LoginItems/Package.swift +++ b/LocalPackages/LoginItems/Package.swift @@ -14,7 +14,6 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "2.0.0"), - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.2.0") ], targets: [ .target( diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserDefault+VPNEnabledViaWaitlist.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserDefault+VPNEnabledViaWaitlist.swift deleted file mode 100644 index 397f67f6ac..0000000000 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserDefault+VPNEnabledViaWaitlist.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// UserDefault+VPNEnabledViaWaitlist.swift -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -public extension UserDefaults { - private enum Key { - static var networkProtectionVPNEnabledViaWaitlist = "networkProtectionVPNEnabledViaWaitlist" - } - - // Convenience declaration - private var networkProtectionVPNEnabledViaWaitlistRawValueKey: String { - Key.networkProtectionVPNEnabledViaWaitlist - } - - /// For KVO to work across processes (Menu App + Main App) we need to declare this dynamic var in a `UserDefaults` - /// extension, and the key for this property must match its name exactly. - /// - @objc - dynamic var networkProtectionVPNEnabledViaWaitlist: Bool { - get { - value(forKey: networkProtectionVPNEnabledViaWaitlistRawValueKey) as? Bool ?? false - } - - set { - set(newValue, forKey: networkProtectionVPNEnabledViaWaitlistRawValueKey) - } - } -} diff --git a/LocalPackages/SyncUI/Package.swift b/LocalPackages/SyncUI/Package.swift index 1728c280de..63994d4a45 100644 --- a/LocalPackages/SyncUI/Package.swift +++ b/LocalPackages/SyncUI/Package.swift @@ -15,7 +15,6 @@ let package = Package( dependencies: [ .package(path: "../SwiftUIExtensions"), .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "2.0.0"), - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.2.0") ], targets: [ .target( diff --git a/LocalPackages/SystemExtensionManager/Package.swift b/LocalPackages/SystemExtensionManager/Package.swift index b088e62b79..bf7cce608f 100644 --- a/LocalPackages/SystemExtensionManager/Package.swift +++ b/LocalPackages/SystemExtensionManager/Package.swift @@ -17,7 +17,6 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "2.0.0"), - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.2.0") ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. diff --git a/UnitTests/Menus/MoreOptionsMenuTests.swift b/UnitTests/Menus/MoreOptionsMenuTests.swift index cf74805e7b..18973b356f 100644 --- a/UnitTests/Menus/MoreOptionsMenuTests.swift +++ b/UnitTests/Menus/MoreOptionsMenuTests.swift @@ -190,12 +190,20 @@ final class NetworkProtectionVisibilityMock: NetworkProtectionFeatureVisibility return visible } - func disableForAllUsers() { + func disableForAllUsers() async { // intentional no-op } func disableForWaitlistUsers() { // intentional no-op } + + var isEligibleForThankYouMessage: Bool { + false + } + + func disableIfUserHasNoAccess() async -> Bool { + return false + } } #endif From 64858b45f45b7a32490a717e90ee9535323ff75e Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Wed, 20 Mar 2024 14:52:02 +0100 Subject: [PATCH 46/51] Makes a method thread safe (#2452) Task/Issue URL: https://app.asana.com/0/0/1206878016688977/f ## Description Makes a method thread safe --- .../Sources/XPCHelper/XPCServer.swift | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/LocalPackages/XPCHelper/Sources/XPCHelper/XPCServer.swift b/LocalPackages/XPCHelper/Sources/XPCHelper/XPCServer.swift index ab7f17fd71..0b2756d9f2 100644 --- a/LocalPackages/XPCHelper/Sources/XPCHelper/XPCServer.swift +++ b/LocalPackages/XPCHelper/Sources/XPCHelper/XPCServer.swift @@ -23,7 +23,7 @@ private class XPCConnectionsManager: NSObject, NSXPCListenerDelegate { private let clientInterface: NSXPCInterface private let serverInterface: NSXPCInterface - private let queue: DispatchQueue + fileprivate let queue: DispatchQueue weak var delegate: AnyObject? /// The active connections @@ -135,16 +135,18 @@ extension XPCServer { } public func forEachClient(do callback: @escaping (ClientInterface) -> Void) { - for connection in connectionsManager.connections { - let client: ClientInterface + connectionsManager.queue.async { + for connection in self.connectionsManager.connections { + let client: ClientInterface + + do { + client = try self.client(for: connection) + } catch { + continue + } - do { - client = try self.client(for: connection) - } catch { - continue + callback(client) } - - callback(client) } } } From 764ba21d8ca12d11948841b16092eac609f59adf Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Wed, 20 Mar 2024 16:17:39 +0100 Subject: [PATCH 47/51] Wire VPN environment to subs menu item (#2460) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/0/1206886385075816/f **Description**: The VPN environment and subs environment need to be in sync, but we have to set them individually from the debug menu. This makes sure that, when the subs environment is changed, it changes the VPN’s too. **Steps to test this PR**: 1. Go to Debug -> Subscription -> Environment 2. Change the value 3. Go to Debug -> VPN -> Environment 4. Check it’s the same value 5. Repeat steps 1-4 with the opposite value — ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --- DuckDuckGo/Menus/MainMenu.swift | 25 +++++++++++-------- .../DebugMenu/SubscriptionDebugMenu.swift | 2 +- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/DuckDuckGo/Menus/MainMenu.swift b/DuckDuckGo/Menus/MainMenu.swift index 1f06a18bec..5aa6e8cb6c 100644 --- a/DuckDuckGo/Menus/MainMenu.swift +++ b/DuckDuckGo/Menus/MainMenu.swift @@ -627,16 +627,21 @@ import SubscriptionUI let currentEnvironmentWrapper = UserDefaultsWrapper(key: .subscriptionEnvironment, defaultValue: SubscriptionPurchaseEnvironment.ServiceEnvironment.default) let isInternalTestingWrapper = UserDefaultsWrapper(key: .subscriptionInternalTesting, defaultValue: false) - SubscriptionDebugMenu(currentEnvironment: { currentEnvironmentWrapper.wrappedValue.rawValue }, - updateEnvironment: { - guard let newEnvironment = SubscriptionPurchaseEnvironment.ServiceEnvironment(rawValue: $0) else { return } - currentEnvironmentWrapper.wrappedValue = newEnvironment - SubscriptionPurchaseEnvironment.currentServiceEnvironment = newEnvironment }, - isInternalTestingEnabled: { isInternalTestingWrapper.wrappedValue }, - updateInternalTestingFlag: { isInternalTestingWrapper.wrappedValue = $0 }, - currentViewController: { - WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController - }, subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) + SubscriptionDebugMenu( + currentEnvironment: { currentEnvironmentWrapper.wrappedValue.rawValue }, + updateEnvironment: { + guard let newEnvironment = SubscriptionPurchaseEnvironment.ServiceEnvironment(rawValue: $0) else { return } + currentEnvironmentWrapper.wrappedValue = newEnvironment + SubscriptionPurchaseEnvironment.currentServiceEnvironment = newEnvironment + VPNSettings(defaults: .netP).selectedEnvironment = newEnvironment == .staging ? .staging : .production + }, + isInternalTestingEnabled: { isInternalTestingWrapper.wrappedValue }, + updateInternalTestingFlag: { isInternalTestingWrapper.wrappedValue = $0 }, + currentViewController: { + WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController + }, + subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs) + ) #endif NSMenuItem(title: "Logging").submenu(setupLoggingMenu()) diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift index bde629f646..c828e28920 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift @@ -271,7 +271,7 @@ public final class SubscriptionDebugMenu: NSMenuItem { private func askAndUpdateEnvironment(to newEnvironmentString: String) { let alert = makeAlert(title: "Are you sure you want to change the environment to \(newEnvironmentString.capitalized)", - message: "Please make sure you have manually removed your current active Subscription and reset all related features. \nYou may also need to change environment of related features e.g. VPN's to a matching one.", + message: "Please make sure you have manually removed your current active Subscription and reset all related features. \nYou may also need to change environment of related features.", buttonNames: ["Yes", "No"]) let response = alert.runModal() From 4c30e6c7b3eec03aa03c1c92e468a5f5662b6104 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Wed, 20 Mar 2024 15:19:01 +0000 Subject: [PATCH 48/51] Fix sparkle strings for thank you message (#2461) Task/Issue URL: https://app.asana.com/0/1204167627774280/1206888588805890/f **Description**: Fix sparkle strings for thank you message --- .../Common/Localizables/UserText+NetworkProtection.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift b/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift index a6082d73a1..69d36bb228 100644 --- a/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift +++ b/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift @@ -392,10 +392,16 @@ extension UserText { static let dbpThankYouTitle = "Personal Information Removal early access is over" static let dbpThankYouSubtitle = "Thank you for being a tester!" static let dbpThankYouBody1 = "To continue using Personal Information Removal, subscribe to DuckDuckGo Privacy Pro and get 40% off with promo code THANKYOU" - static let dbpThankYouBody2 = "Offer redeemable for a limited time only in the desktop version of the DuckDuckGo browser by U.S. testers when you download from duckduckgo.com/app" static let vpnThankYouTitle = "DuckDuckGo VPN early access is over" static let vpnThankYouSubtitle = "Thank you for being a tester!" static let vpnThankYouBody1 = "To continue using the VPN, subscribe to DuckDuckGo Privacy Pro and get 40% off with promo code THANKYOU" + +#if APPSTORE + static let dbpThankYouBody2 = "Offer redeemable for a limited time only in the desktop version of the DuckDuckGo browser by U.S. testers when you download from duckduckgo.com/app" static let vpnThankYouBody2 = "Offer redeemable for a limited time only in the desktop version of the DuckDuckGo browser by U.S. testers when you download from duckduckgo.com/app" +#else + static let dbpThankYouBody2 = "Offer redeemable for a limited time in the desktop version of the DuckDuckGo browser by U.S. beta testers only." + static let vpnThankYouBody2 = "Offer redeemable for a limited time in the desktop version of the DuckDuckGo browser by U.S. beta testers only." +#endif } From 59d3a63ce49f6981876e509ceeb0f9b815c96edf Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Wed, 20 Mar 2024 18:29:17 +0100 Subject: [PATCH 49/51] Updates VPN 128 assets (#2459) Task/Issue URL: https://app.asana.com/0/0/1206887289237359/f ## Description Updates the VPN 128 assets used for the icon shown in the status view. --- .../Icons/VPN-128.imageset/Contents.json | 2 +- .../Network-Protetion-VPN-128.pdf | Bin 0 -> 14844 bytes .../Icons/VPN-128.imageset/VPN-128.pdf | Bin 11566 -> 0 bytes .../VPN-Disabled-128.imageset/Contents.json | 2 +- .../Network-Protetion-VPN-Disabled-128.pdf | Bin 0 -> 14855 bytes .../VPN-Disabled-128.pdf | Bin 11174 -> 0 bytes 6 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPN-128.imageset/Network-Protetion-VPN-128.pdf delete mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPN-128.imageset/VPN-128.pdf create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPN-Disabled-128.imageset/Network-Protetion-VPN-Disabled-128.pdf delete mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPN-Disabled-128.imageset/VPN-Disabled-128.pdf diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPN-128.imageset/Contents.json b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPN-128.imageset/Contents.json index 011b2db3d3..86f2574f00 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPN-128.imageset/Contents.json +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPN-128.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "VPN-128.pdf", + "filename" : "Network-Protetion-VPN-128.pdf", "idiom" : "universal" } ], diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPN-128.imageset/Network-Protetion-VPN-128.pdf b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPN-128.imageset/Network-Protetion-VPN-128.pdf new file mode 100644 index 0000000000000000000000000000000000000000..54953a618cb9ffd53f43d172d78d4bf9cc0bd323 GIT binary patch literal 14844 zcmeI(+pZm1bq3)3{S@m42_kv9&Ibu0r4OJeL6GiTAfa5C-J7P7+iq-k1bO;=|EyZG zYTKsU4N;;<*+RH_)I5#zKgO)}qhEjW^C#_fzPoEL&E@6q{&;t}{Pd@nr~mesZyqjB zfAg!~{qf=c4Vu5F|MKwS%Qt_xbOwI5lYa5)<+tCQCY1l)-jzAq#Lunw>e-9e-#mNu z@Z$b2@4Ww~%bQo}UH9pMU$iH{~opfBEX`o%Y$o>zCiYx_@|GcK-CwZ+`Ll zn`dtxw*P9&Kljfr&2?(>clp`Rw*T$3Kl$~PU40AKCb{?0Z zZwC+Sgfv}V-@mx)*6Z9g>oOiE`}(f$u2a9Z%X}HG%RIDGd$D@oPGh%RS_{q7IE`o8 zbe-mA?x)NB-D%`{ZI`~^MmBxhuj8d_uT!_?sqe4D+Rj~f>H6zBFI_)p_UgRd;+XAv z?b?1^*2~aeyK!ii>C(2>Wtsc-OzX3?Q<{7X?62cGkNvXE?dPSrAaz4Gk2|TI7o=fq zn}wXp)Vy_?YrA&+G+hQy^Mb8Qd+mCgB9UFYuEThc9C^I1WYjeW@%bI@=6-4W?jZJ9 z?=2Tm&+9OC?d3hvzrXv#-S6*S-2Lm_-NTD7c1!RSZ@G&%Y;H5QZQd7ZU#Z{YOmzSM z&Qu+yy3ltz(=c6^b?xT)~6MvmX)^eW(ZetnI$a|;m^869GyKV5Z8HK+pLS+G~?3l-7I==<>f}ieu%{dhcnRv!XD2MbGCJtj2-x1(+h=9 z9Cql2w#1x0{ypg|&+ohNm7v*{k)bYK$Nl(PGp*xHR%ol0xczkzd&u%cyT8MzV-9#a zjU2CC*R)*vN$1hxrg!gC%Xj)&9A)>jeyM)OaZgEWPgf-Y{KmNt-#U_!JoX#0^F6O> z%6TiH%(sM6&Ga_%ysC9SUCPLhetCKN%Wt2*`RbeJe@W_iJZj5`pH3e>w7c!!&_n+I z>Z0QZk6dpfzn7=sHu8IS?>~6tDMkNvj*8^`-z}t0k5s*5kvV!i<>#lLJp9L3_Ya@_ z;-kyQpWmJS>;CnTyWyRi>3>(iS!3hfNZyiIGj*ekRkAm;_m}Mb)@_9G6^6UQ4=Apd04Ntl)3$y~>-L`DoUbc;7V3|#@ zn%i->Ob~&krjl6~TcTjVA4*v9P$~C!-E}R*g9c>`)6mW+-83)bBKM9C>KVYSnZF><&+?77P8`Q2t_Ym6g|jnmnH3S-Mbsf^v5SB-IJY%{ZI z#YPw-<#+w(NDD#VUC{Rjda#VY_L1NPWa#GQ zPcc?v!M@)fF(T7?u8lD|i`VU^-xvb{D~H>wA+p)!uq{9VkCgv-zpy_v@zDT<1RM3sQblJcDt$PioHb%G+R$!EwFM} z?X=1vplrA2DzE2vo0+lkjWHRc8r9C&aHbT-qMtfyXRO`K3`aX-s@l3=wTH(Z-2VU% zJ|ad_r29XUf|HaVr{E;zZdaHcc(^AqAUTO8Au2d;?fjDH@Ip^HB zsl%%@>+niB%ePj4_3eAQP2F@evsUp}{x;}XO1>D;u*!M?1nbyoHkQ`yFkGh%kLfDy zhOc)UbO%{!wN`4k-0Y-67I4m#0K= zEOK%~{n}nn3qm`V8@ro`k0}QY%B^z_>hW}Y3$VXu9T@DODlU|@-Fz7nf&pbUTu_QO z-Hq0)1h;`Spwnz4mqjgn(GwiIcB00S(mTLd9aj}PXb4UtMV@F*{vLyPUfaok`mk#R zLTM+>1YI{rU~Os3Cn7qP>jT==-}D$;T+(ygY%v8}v*;uW4U2rElcyCypRxnkiOF?f zP}=_bse_^tJF6t3EKrTJ`bEL4z!cFMBLn|XTbBYV0@P90l-NSkRE0~?SRlDPrsX1` z^h{49hpQH3+t}Oc^PIw*mt>nYY-hF}RXp7#&HejIG007gAvan$1%?r^?0mEN&@v%& z&)UU=I83L0bMl4u!{b7uN>3}U^x3_2@=7aw+hv#^jbzek)>y~LQIpq>t&l?z8?A3v zyqXKWdq`PI*9RFq6*(mNhPz^L#zo5Wc_AqqeT`&=Cq5z)(U&t z^07L)BR+hYtxH6#(Kql&@8+~L&!^MJQyOB2-wECGvKE__m0SdnPstBbGJ}e>^m_iR zE12swr=OgTJHk=|gH#K1@=4P~1&@sARK z+tKOCuAvrZN_3XfTtN|&<1rHMwzQH(#I7c8T^u9kUZ`nJbwSJJ-3BLHj;jJ7;KS9l z)p7yXthtLj;eilydlp!uJ=(Az@r3_z7m+0ab&B)4aC4{3bC8m@m_j^2;w~}PWg8s} zKyj7MhH2l8(=v)^@u+f}hcOo6eyzCPV$(P~O;Odb63;B}Fm$FU+E1R+HsrtdlH1lL z*J{!oX91L%Y?9ZQ<`f?R_pE``@?Nh^U}?Oi3ZpI^-tVP%!(F zE@Cc1e1|mSuXR~zY6>Fa7lj4xjt&bD^1VcWZtgY#BJn?86p;0FkW5Wr=ASY9+qise z;k<2}V@hcl3s>4Nq{9)%zL8B#)fgi(7srTPEvEyo>87-DVUV(GGG4(X`8QxX%|;D*A`e8EyA=GI)wDL3SF5=bLF$OYhPu(=HB znhLn_oe9?SR-ix7u8Xzj`OIoo7{U`jXo!FY$@)-=3n>thxEKpC{W9UT!B8FyjlH7Yhd}>)P zDp!Cj*>jq(+2pGR9o|(GL>1u6-CpD+GWR(>quVrjB;bR_*K(##9Dh6#-fuvZjAZ0M zm~rsZobEZAU{W+m`##o5e?EYAgm>ct(4TUlpz3@rkgP;PkxKw)=RA8bTzP}I05&f| zpt?G79QNT|BrV_s^6+zSeI611f`>N9!}xtj5m^NM^Z~YJ^NoIodZJ%|%zI>3WEn&% z;$(S1Trea`Qd7$Xu!_n)&Jfz+eSlO3pxh4j>voZLa*JJNi`gt<`zB4jiQy8H zBns)Cl{l{Obf(eRt@nG`zp;4kX2!TgNZ4Q%eNG&U} zJhMyvLVIxu(BDXt*}zcZ2oAX@U@O(_fZ4GmIw0vpI+^V;4o;Tyq%hehHBNzsnz<{( ze2|$?p<~xg``ndkQ62G`Ph=7ibCL23ZxQhslI%! zDaeVb>4T(oCHNP&O#rkHn#>zX(E|%t%LxaU_E0bWDU*|86u<99S(h1wSd3 z*>J2tmlLT9!>!qFbva6C+MjSmp)q%jXWUVKo0cNBKEPHkfN)@w~m zO;&Lu45$Wca<2+w%a^%PQvQlBZBIo>dU%Tr5r1q%cp&(+`sN?6_IAE(CW7;NN}y#5rBsw9=E&-h9skXzp2b2{$-Y}n-~x3er=+<+ zJE)BCQnU(+JQQrLXDD*#$(3fd=xOaV$SV~vNoOmafn!C5@IFTbVBcrt&V(Mf4Hovg%HC&c^q@oSR|jX=r{W1;FcxLxD(gESOk0 z62>*mLe-D(jbfmda91<|nn;T|@tL+eg4qZ4q%$&Jcc*zjwQ|LlkFon+l6O)b40oK# znaLaePC`7NUg{synLhI=Ichki@IK`69?3n5ydM-t7nept3CQm9oxnV!a&cgF_9d)QP3GJnpsl2GMbyGf9=!%K3HX{axPrjY7GE z(c#LC+(A<61b1bteWs!hY>2E>8u`TqXjOU2r%OH_3kt8Crw-K6l=N@Xf$IRRv=DEU zto62BfAD3~Nui8Cr8bxNUWMj{NfG7~akzjOM4#=m*Icrli22g3)3jBTA0umweIhKn zSf;`=DSh)&uph)!+<;(0_HDHUdr&JaSOB@~T1lL;p05(4ZLO7DMLelQo~QMoC`H=0 zI7pgHx1*ei`;m{;stArw}@?&Jt@042=!TxdeE-UsfqAsO3=7Re6F2Lr4ZIeH~S|PPHFP2*tc6U19E{ zdgGmsXdr0rFhZ-dPm=Sgf%^hM^LcUa++yZPDX22BIQT)-ut@{_H2=iscl>}TpRjK~ zY*0(~Q~ll@C_MeN;U8bTc=_h_<+u4cn!o+`dB1-4<^Hp|r@wso;;Uyr_?K;2>i9KX zeob-vW#7jyU%YvEVat4Xer$0ViSyCl~zk4K5*xX#~gc{d7g@0d#l9rDMc{F?3^k_>Pg z_2$*Hubw}=x{G-K=@Y{J@#~jgKm6qK$tL2bpS^rZz7b#1*VE6x`p<{Rf|_2x WdG_i}kxuR!y1O6!=+j^Q{J#K{cyrhQ literal 0 HcmV?d00001 diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPN-128.imageset/VPN-128.pdf b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPN-128.imageset/VPN-128.pdf deleted file mode 100644 index 7ae5315347df2908c170a81509bb8855d729fd1d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11566 zcmeI2TW=-B5y#)pr;rVmgVD*m$Se9_VaN$`{t|f zemY*9pz%BV%kk>v>Bq$~@OdYFeslfi=V?OyyS=M(+Qb*EcXNJq`}6tD@#^BY2k*aF zPB(9kcgUZ7(sy|A=DSnn<+JOX*PZtKczgZk=Hhr;cYgNk>GKz-^V6~YW$Hf{=gYR* zwDDU$`K0~VXFqvz{p<2AujM7L^}*P@(@NZ*{?06zxjo$+&tD&YIh;K+%gft?+28Bz zKkAddb7d{UgI!G8$6vfYY^#Ym+QiPT{&+~mnvj;;i>rh6>y~WJe%Y7I%j<*js|(TD zGEu`Cv&$(he)VZ{W|qaQK1QG1a&fSBjn*gQ7ZwSQmCfQep8V#*;_T{uHqkELt#OMG zmk=roDKguab@Seayo7CyA#AJ@>{>!J!7rYzor$a%c(P@)esO7aCS>QC4K<};j9nbu zdY88-v$NS^*cQi};xcoBS?!ixNit3zeBV3dEtpD0)_{mUt=W5)TAUt|=PB7z@s+gdPj^oJFUk!$hLHW(U2xX8XBj}5C0mW`@ocvr%Lu%@IKfCw>T z>*{(CL)yoow`N)_igJ@$kdp>hRaY;du3ZcP*X-ehvaJ4TrMN^1f2x zWEw1zWhxWy+73N zi{i|JB7!n)`}-s97-|3g$nfZqZj5}#?lT(x?(UU!{D)KLU{ED&uYhLNLt6m7i%vx}^wl;(AwVp#$6LUt z2?ol+pd5O`vw8%2J}QKqwXd?x;&&}K!OU)EoQ3X3fZKUczQo?-5I9-oUM)~1R6A8Y zTnu|weO*EmGm~gn1ian1WbU7?!vOStKroq@i)I*~K8qad&MFBF>b9Fv4op(5i+T zCu3K!Fp&^els!QcBN!5g5Ll~tktn1K+S~~cRCS2=6%kp~(bXoNEI3qYA|9!qnzu?f zQM#0qC#!55nH-oPa&#VaKWwKRiFlt_Ieaz2swXS=RWQLcm9V`c6;VBOoRDK`%W`yW zqYx|77A$|u&?>+RMteDX@xS|Lq?C(8+>uskWJ^uUO)xM)%|iDRQ|acTe2Jy>vsLcZ z5LH4oSJlJCp`SZ;tqQvoqF7rD5Jfa9fMG={+aRd{vfgBsJ&ts1@ZTJf{iAyi)TTQ~N*!E(%ex(z5Y~n& zcuKz_CZP`4)t3)n=>k(=v8%IvfxeQa`#M}Hk|OruO4EmjuXF(kNL@_jN{1`MI$SAt z5i&el!BW^ZRarVD?dje6)uYh;MZZ#{|Bim|ztROJ{gN*33-sZGx1BYm)U>$`SDHTP zN=ljbQ<^fY!&Mp`u9R87THEQ{y~VXH`p)S&-s_rqnp{CrgplbuNfuD(aFIMoEjVVu zLiCaB`}IVWI8UNQ+lM^eZmxFUsut+_@KSz~X><}wdPpRKyEUg`C=PX`O48rWX_qvC zFh zJvgX|yfab?Ne_-*9&-wPB|3BTnraU&B`QofbEJK>BPKmKaxdVN zJ(`$v4=(7jO$oyRX-hF#I(VASAdQJJNxw~daA1^+W~1we(nb%itdy(Xn7Ie1iOJN1 z8??Cxr_?(=xS6{5;K=4b&do zpsJBxN-yjWwHfxyBfH8{LkefLt6ZoRXjck~SZgDhGfA#7DT*~0C&1nO!>+bCPJbz6 z(e!6G`~&1NIR)t`TD+>D4gxl%obz}{k$$mvbY1BwlCpAWmI6u21aEjtUzu(tzBtiH zRVf-Q`{U6%63=1mwiSBSgNg|z_Q{Kh^sh~!)tX=#lSVZ9v_g?iBbm0Rt5`SOkQVo= zSw&1_(ZWT_hW;egHpO3(2B=%3@n5Q-e6lL>l+bv#^qgS7k*<4AnWAZ;McB?@95N58 zbn5ZhTpJ}ipI$wM#75ViGsMjUY~okAsno^-JkBJ%pKiH#q0r?9((urPA=3TF22efy zdlV@rE1)1a;!KWs8#qDTvmp^)oqUk)|3r)UDkvj&3xgVK)Wurbn$Cf;)YbSSwJ|l1 z^wR+;Fab_6Z53<)eso-41lStOwU->@*jGu2 z0`Q<5?SRQR!DZ{g0LEDk0X5WvOEefEQI66vXX-;Dip8W%;95sUMhpnON@)qMRwu<> zaiW=o$}zomQhgi;&e5}PTMrM#gt?E%E;VLi%#6UP%|;gj_P75h5Yc&XSfv;*FFD$Uh7&g*5XDRpFA_fojevws z@EyX?pwe@re@V{+{0ri?VrNV*p@@$au8i!4K}{PgkF$E@4uGE5*|Svt;cns6YpyAN zd2AP~5U5j;8xSz9#(pZ2dgBKodb#n#mE^t)XCa5-BBkE=!6>tELGe()TQ2rMp! z0@%gb;!K4IEHd`RG1@*#N!60?6VW-?RScc>F&RzPaey){u}oCJ;gyCZ?GlL2`$(2y z74*AI3u*#9v79Q-8DBQeZ$TMLv8C(Bg)#f;DD=rIYQEKLBlV z3rB#7bPa1t0e*8I1qsiM+*t`8p=$2@ByJESiPb^`3xO=gZ48rcDkN?bxnXngMsih? zBi2aRVk<$o3uOz?5!b~mx#%f$yEvnWuO(7ANZf3a8VSRMK~fs=U8(mAaLhF)hA5bU zEs%N|Hd7B((xD48SN`p!ecEqk+st>gl8`O~wjIEW_ zY{p3-K&oL|avXVwTCRhcN?48cMut&XrAUlfZsZZ|CXl;M!lN25@3O?=iB$a?m5ZKr zoLc^!gy@mY=b||sp+BH>5c}P=*l4O*GHB9a4v33(4w?omE3 zrl~T3mxbK2>P<2WjEcC5BCvS5wysxBU7HvI`58IT@IN^vE^X*K%hW!OM2x3;U92=i zo>Bx^sXsCZ(KKC0wc%PkG7o=~;bM60D52?@>Pkw{{pR4=8B1Wb4g@2_kZ@G)>I20T z#|(@CV&o>M9j;-FbS#(TfVi#>BRA+IS`wzD_nkMFB&U*`uc~}H2Bjf`v~i*(8Jb3d z`~ndqT7AKKBBX|e?&rR9wJV8WgdozZ&LSi6Bl+QvT@uz-f85VPN}fR_`@!1zB7zK7 zq3f;0eZHt{3>}9q2`%cwVRqZ#2`qJC^F++ah>$#*EQQBzZ5SB`FJD)nay8XSZ%V7N zY_BFB={z9lZhP2%w?e$97q8;|)Pm%#Jh(`@>|4+pKBprk((Puf#L0y8E$OjNe=89& z`$82D-lsmw8hNE3UVjY?NG~WekOI@#YPD}RY|K<~&~P6+PHz%t5Z^A2+R-GbFk;;kZ{;B`dfbwA6 z>Y-He9UOe@CzWDd1`k&%i7NOY>%@sl9PR4ecOjSznzBPQQEJ~(oNo|2Yc&+rU3Qd1 z#KDdVD%U}k@#!{9ioGhI1oh69KB(wQ(FG=e9QqUU9JsQx7wPNj2Kq{eD+MgmBT`~@z6bkvc#{tXcE**Y3)hHU8Nm>gV;0n* zLo__RZUXYES_U_RrbkqRN0;(W7NBmdXY!V|VWL-5Crj7D@|Bw8fMVU$)Y1eYS=;D< zR6F3)weAssXuUuMf=O)($xZ#(4(H+~m3#myt$H|Swk~N4P==BDl9uze9-o29r*VAsH&Gl-CvQp9r+G9$P$D@2Ieh)V4lZas zh{o!DJe|a*9tk^5wHybZ16JKiGXmkOmigAuQSra+u%?LjmXFEMQ~zv?-j~%6oCsyp z*JOXXy1G8yF5k+hp8Wc^uV0_P?4P8beR=%;)%kmWH7ZMuFI7>kel`2_`s#GNVoMHI ziC@ddv8RP|tfc94EFrZo;^fQRKfn4OUKbJJ`LeV8^!g2R%>w~$p-d}*mD5Th;jL~s zB!7B-I={Srd3WlsH^(0i+fokI;G5OsAXo+D{*w<-<+l)N>W}K^A&Jboz8s_E^P@W? zaz^?D_em16<+o3y5R#hXE=d|FJLz!k@uTI#SKoisRCo6L m`uenpTIv_gXD?p;^N5V7?DlkibE-m9GH>DV!3WR2`s`ncm*2+# diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPN-Disabled-128.imageset/Contents.json b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPN-Disabled-128.imageset/Contents.json index 0b89f4df48..20e5eff4ab 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPN-Disabled-128.imageset/Contents.json +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPN-Disabled-128.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "VPN-Disabled-128.pdf", + "filename" : "Network-Protetion-VPN-Disabled-128.pdf", "idiom" : "universal" } ], diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPN-Disabled-128.imageset/Network-Protetion-VPN-Disabled-128.pdf b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPN-Disabled-128.imageset/Network-Protetion-VPN-Disabled-128.pdf new file mode 100644 index 0000000000000000000000000000000000000000..bee84f6a0acf36ee399645f3fc3cf4fb7aec6efd GIT binary patch literal 14855 zcmeI(OOG7abqDZ$e~R81umh-B?+0KQh#oeK#IPe}VZg|O9*t!gBT^b}nVf6lG`(QiKg`6umVKAqZgbAJ8%Kb_9!pZ@gx;@|)J{r&mH zZ-4#!Ki%IwqWOFAulKLMdHmzKGw{ou^ouvIfB61pLizXhuFTmcer3HkFJHa={^gtd zS9gDX=lw6wk8ggse?tD*&-NX@`r-GF&Q<(ef^j7 z@6OGoDSyX9+YDFQyj-9D!&;`keSCBO^1IWYPwn+Zo&QOFs_**YG7sah^ygu`tj*AN z?fLGzQ*#;n{MqvP!`r)8r>?y$!??`z`IC0Nj7`(_&H1}i*Id@ctmgcQ5yLR7>vTpO zvu=p}WnAZJ8lDix%QAJ{xSsE_Kpnegfo>Si54YsLz4XmEj`P{ZZ95K)b^FfI`*~Q- z?R@Fbmg$ywcX}`R;nX&lsqb6N-z+gWW79V+T5}n?Wu4|j@Sk+kr62nh=XZoOg>fT8 zU1nN2x0kM&R>uo%X!>Ot(Ux{1##`FmX}$DwH%{FxwY5&?NSmydZtUCU{9a+-o&I?G z!|B!O-%h9dS6}Zs_Y`|S#hy3AAC@)m3zafI&K-t-xI=&L``A>!e3UyZ(QwnPS3~JI z1EXX-GvgXB?c5Cs3YBFrOFJ*) zvYxwl<@!I`eGGTE`)R6nKUV4o(!lh8YWFpYe=y!(lTyh#zwOS+TYEY>QqJ?I7r$js z9~^i+#M9X?zWB@IFTQ$w`FLNp{9-8oyL(9)d!~8+yyvTQf8)Hq%=`3L=NG^F;o~~SLOy>jApaSq-(C#ci*9@I zo=6>&+bni$s=>-PH-UObEQNDh#G1dX;(D&h>8cT_Yr_mzUZ0Dy>b#M$@>kb**Hj`Bq3O>%p-eDV4He|~#+|K%?}J%9Gq z>E>T|Z;K9#av{kg9T~ZTr{k%(jd;E&8^73KeTx$udc%d&253UPaY`Cwv481B!`H+m!+BErZcFKrR0^1 zj5N+I3I_b4gaHqg0-9acLOf{QWg4bI*cafZ1nhGMI;~hb_RG9(9vf$<=Yr?3W<1P! zJ$-ut6$iPbatOgAELXsb&+C$w>x-Vu%XZ2jMx0o60s;93O zSUIeAx-mu$cc*I4RbCIL&CJ;N#uyk-jjA$MDb?`q%&W$@Gq#!8v|=NSk#gOy+QR`4 z5`{}#{zwmove!NmJP$eYM|$v*f@h^IMNG~c51ubAZ)LgG*;b4c=5g$&5pCG4bbA1m zwpT?ihNlwS7MPnN(Jafywv7}952UQpnEUn&<;CC|n|EvmaD=~#)VR^ISwp2n(`uA3 zSZTyIt&=lY$tFtz(Gm~tkNsjbVj{dd@i~ii9zzr%ZC_L)kgp-RMRR@jB&S zBl=W%#e+N1qtL=w8`Ix?b7w4`xKnn9Xs6^LmMuG_KVYSmLhr3g?=PIuUg=)-< zjc<&}7}bDw#)ex;VJ!NoqjtuM!2(>rvMz;@Pn;hU7ij1*ZngE z{6IZqUSGcZsxELNU-UTO8Au2d;?fn?`$^qh0wrVg(MuEQ(k zR=yu4Se@S8^t9IYry|&;{KM|gA4(5>y`E0~9buXwWH_!UxZKpR^Pk3xt zNrY5SQHd%O>S|+mW%QVG(1f%)=b#=>?|XZ! z0|~unb-4GS4wn|>bD}YQ4h?)jr@O|CVd1nerm$UUPBdIr#n6+HaIB(28!|T%b?l7} z0=4HhbE;lA(a`1!gTyPbYVe`95bU_$OQee(YA)1jTkdZ1*qEA7VQT7`^mq z66g+V@5)T6J5=M-swYs1Ad!oeoisKbc&<&{_03d9Y2OZW3Tv{NEvK+v(S1V8^n%RzVK3 zcg;8lE|2a77&IGMh(hEgoOw`rp3}`==BWsPrl@LI3mjL#Tf>c9Gf$;S)3@uW3D7ch zkwrJDcQ0YF@Q*mmW4i_T`(mj=q>Y6Qs!TNO=m?HV9rSAheIGzRXq zbFKqvUDqqVZ+krhHKyC1dFZ+(#rB+PIE7)-^~8u*L}Klu5&|2$j;*m*SFxB8z*^ek zCY59=v3HYe$b-n7`&L!l#yO^ix5dJlZG_2TNB67cE>_t_WXfa2ZfJ8lDTZ#S8mV!6 zW!Fj6=@W`7$*w|?8W^vY=qhI%={QATuBp;7C^yIbl&P(|)1p^IO@xW*dYEahjH4#) z)mSs1my9yk@(E*Tvx_wMr1nklSl3vp5J(vvNkx#ZtROcuvU_sqgJw4FFuc#+M{+$^ zWsaSZ+`D1>)Voj;V?}do=E-cXDI&Vj<0Y=MXF&&T8rDU?Kx1e8B(%=OqL>^9L&HOn z*t)j4?PVuILqk2eejb~`1E-Lq^a5RfyGe}gdJ7W>cV?qb!@%Xr9K9#aTmg?|arw|D zUe0RJdUM$$S}af4He2m&1{)#2cBJ4fmFDHtW+GB%#b>`cp_b) zIuHju526Xo!vEQ6BNwN24H{(=Qen>GbQiCXs*H@|=l(MKc1oV2EuNh0j$HnwdH01b z^@{|o?LO4WoLmI!`dI|W*|_!{YG@2nOhI$|IwWu0(p<@QCF$KzWj3c3oXo+&z;2R5 zu*3XGcVzt=P2kv^GSns=>N5<{&RRERr2ZXa4GlBtWf?~(5@$(OZNtrT@@NO6BUw;#G;WM^qa2yCH7SaPh1NGKeVWZ`JND!awTO`=q=_PPq4EAL2rpt!-^kzg z(SHtFvbS6InJjXi#Z8VCNpfWwPA}@muNJnn|Cl zWa;E2O?kd^KILOv#Z;bu>52Qvo^GlCE+4-UQ^DRu@LgqAOjN}5Wr7Pvzb z1qvdKC^&pVOQ6%#T8G!cRZ@2R+-x>S?J{M;1(Wg>6T#cr8NmYvYoHIBVAf(jCNNHc zC8-@PDP_Cv#9wS{GaoYIehI?d0RzFMOo2Ia7V>cmLED?*Ka*LJWxSiIW_dwvVdZLU zt_qeRf~h{j0c^xhmdP9VqP@Bkpo5w2#B70O-Rnio__p<2!owQz=f?Tmdm@z(a^_1RGv_K{MXWJ>k^1WJ3eCy+buSz|sP z1bW<+u+@CWG^Y%113Zdn#YSSk;@fs|7c4Gk*$Zz3;Z$H5DVhT|g|?-+1BqwcHT5w; zAl7xx%}Ax-Dw#%mLR7ErrDP`;WK$?oDcyip{D}P3Ku(Uz`V0JIzo`{V)zknrW|v%$ zkIsY{A%yMT* zGlb*y2(K_~F)OG5Ny>v#1Y-|PlUC&>LUu_3tTy=5ZZvL~eYvMvm;n;MT1$D3Pw(=j zsi|~g6iMSW|95!FKoBa0k5agN-}WxMkpcq?W(+0@LMa#PqLBJB38;ixs`ssE1S)yw z^G}Y2rDWZhY^T>DbSub&Pse7+@(xad?lB`cQ7M{Mh*%I>N=<@56U7rdLIa*Q2S%Os z8;V=Yu;Nh}i5+vCk}rX@g(hS(bK$pd*hy62MB=@wGSno2ps;**3zZjCDATq<^zY$2 z^7z9)mG4$_K|ekFaO$JltqUnFB(j-^>Oe*E5$#rB44$HM3Qq56VT*EC8U|dl5&Qu;_`v^}6$PioQECtQSg_zz>_kOUF3%5=aj8j9TM#={a zTwKS9uS3%Talkg*CnA@+Enjiol8h2FZ>HXza+w0&5jo$I(o>>6p=s%*xK9q;CexR_nuc#z^56r`yFUrLOs7lAc*EedvxjuS1G1IjuWQ$Mg_2e*@jhHIJ{q%O?>fPnA!MXcf-9QhC_0<`)a`we9KVZRu+S z796zFbBE;xhJ#qU(8nqVsV;KUtrlgrHR{8A##hpo0=Lsu;<0;O;~*s~Z`?-K0k{!a zgn;pcmQvHG2=cAlL1mj{%#1$@oghGRyu_%#hQt)L)I$=<(u`Cb zL(E(C0s?@j;(fXQ4DbdwG&Zw{gxs~Nwt_%_5q_q;V2cVZJTt0BgS5*>O*)6x%Sj^Js(qXn-WFA zF5l#HSF!l zTYBR03Dl`h(jG5$8-U_yaNSh;A^=#fV8DGT3GVWMq_*jCjaA)T%OW-Ecn0t4M5x)O zT4qpNdR=Xbp%f)EwdYh=4)(jD%G*O1cS;@A+t5RNlW&cPlt9^{Z<^{rV!mwD{*&uUsOEWuWTs-(JdU%vg?Y58PEp0B6=?DY?s{tgdsw_n>`zc|-pxaB=uzCAWQ*#GS1M&JK(uKb-}T}dv8-XTHgPe-*nJMWN`Qcp)Ike`n#oz!Qe zMz!%1l43^Kc}7aV<{3%1=Q+u*4lC*L&C72e?%$lEhF^Y82|s@O`n&s|oPYf7*FV`b q@#4$Zuc@K_QuD=E-~QKqBu1ZaA78$CEHVr>A5TB}(HFn|`F{h(E_+=7 literal 0 HcmV?d00001 diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPN-Disabled-128.imageset/VPN-Disabled-128.pdf b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPN-Disabled-128.imageset/VPN-Disabled-128.pdf deleted file mode 100644 index c1ed98cd5dc031d4e8ab8635c9657c3050092833..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11174 zcmeI2TW=&s702J_Q}j!u1j*yBYgbFrN@TN%5Fo;aw}=NbJJ~Gq+F%8I2AtMBi={QBOY5zY2MFa<~O}~!Olm&^H=%VJLA~tgz|@R`mxjh_|Ej~onh?!!0B%6{NdAIypTkW zS8pW3oxdl`-yN^s-hDR(fUmXd_U`6*@qTyy`SG9c4#(GDem1@Qdi&SmcK7$)`71ZQ zz1^Xk%JDp%A^OJrSlL78!s%%;;Nksl-lw1+dV9FIJzVXUeOO$KVT$`A+-`cmOZ(zW zTFR96Zq;wP1eT&NIRf*EH+*0Cm4zOq)VG(+LJ0CY9d-?^i&Hy@?U%qZ=zRgUfGW%! z9)x0<%87?~S{UT83;W#IH(02|fD}y7RF)}JlA*!QJM4UA4H}HDOJIxkyFtC*M;HsD zzH{jJPFTfim%{;ZX7O5vye@%x!4_a*%q96qqtqDNK>8!$>Puq`Od!Ay8e>S=&TEXJ z3-QzGYK);X@zbeqj6tAr*dE0QL@GuO4FPhOOvM`VY6l^ZOJD)E7acCf7Lk*sbQw`6 z+4N%&={nY@+g)ACjWKsJGib2$4%k+w!RV5`NeO7ys5c8V4$V$Y4u@gSCa+6iGBc=e zjFmLZ%osD1GR6+O4XiU(&5Y=Wro|Xie(XPo-FLfhcUQYVRu}d^!G-;Dl5R?;)rURU z;q8bD*If2BI?07gr7tgXjFStOs@7GmQDml}E%F+XhcsRU(e`BpQW;E`NJH!0)K9{a zB-0uGS1(|-jWp$b5|TVQf1*C076T-8cWDkFQ9|(|3R^X_D&a=SI8_!V4k5+JQ#5gk zA$c%?Qf0LgNN2RU6G70#A@5tovc%EFCQnuzsxYy)Yz*GGGlMtgHlA#}&3h1~BtAL~ zb{@9VR3h(_l|xr!ta`FhUl~(OGY;D$CrFn=j}uZ;LyPL#K`GZryJGos0agR7Vsw=5 z<$v9ijSYhwcKH-(4Yr)5+!!M!m|56)GL>#VMwcw5>(;0@Lo^P}Tuly#UDrEyZ34R# zP}Y_cK(XeRjkOtX!?27w@NKNJ=aFu0XsNO%S&viAEbcpW_z4be0VE7Me#yu+gY``G zOGd^Ee>@}4|Kp6Ts^I@QBS$%~Rmc|Lo+_!mrQvA7;~5EgpB|J!^7*_7I>N3U{P+<$ zup!ta7QYX^Bj}i{+_&upQHj{M8_X7u9~lBAqQZAklbWDS`6KMviWpmqZFk2WVgKW+-m{DchjbPhu zkga^5Wx2uh=&Yu!f?dlTDB3vv!9u-Ys_7fOPjTLt6lmhq4VL|ymb^MaQw%A(DbITf z&O)shNyziOMkb7sZlLQA<224S5ZVPzbcyoWfC6!pQz<3Q z7hZLoA9Ga21;od~sX;vq{Zy+QW1Rgolz~)BT&c_k$WevPj$WHrhtisLq}q%PXxfLB z>Mc!_6FwmrI9j2hB>JSP9G_?qft5^qy?_(-Q204G@gg$0h2qhK4lE}x4mL2iho{*q z4Lk*<;2!U@3Y6`rTyTs>Nkr8i~WrhnBH?Ka^#XymY(%H?c1iMBAQ@LDa9W<|0 za%oL;4KeRSPAOOHbSikdxI|tQ$(dRmX<^svJSFh>GE?RPE35OK>A@53fq_2(z1*~b zKhIxF^-~P&;X?j842Fu@Cr=;oUIw@zBHR$M6$F5I>7i+n7-X#U4Jq>s5dOzTK&i}* zu8^dGr-zVz?cg#t>{NTGD=kPAV~dQCct1#WIWr-cCr4X8Bc7^coJOJ_Fg%!d@=_A5 zo$ah;JZhSmHVi0)_*v5yhYjSi^2$OI$}(Q3mLY2Zfe9dsIbr))mBV!yw`gQR6FAc= zR8uP+lSB?U2`!c9jmSHidz?UYWe!JF-!y>C9Ok9e5!C?F;3K+%@$BQ`*TO^s%LJQ( z60xsDnX-YvM{Hb|Ofx;8NRij+N^yY!MY6cZ3Kgg$L1V<&2P_kV(?Spcr$I4wd?(QN zRL%7FiKv8DVs6DwGYpwA(FT_#;S@6ikfji0;-VoS`jQ-RN{6zE_K03>Vy!f#S>gm^ zJputSkg-+(lR_UZAW{0jIU87pGeDY54QRG10p)0z1A0FR8qpX|i3Ug#I6p8l2$TvI zOCXw6Si+#iJ628<$f%i}XqII)U(AoJb1#E*E(fe;5C`eF{D8Iyt|^WpODx0~1J$6e zj-8VYNZyEy{bbrQw2ICi4(S@G5?$+qO~Hp8ER>tS{)|YGHEweYPG2+o!vJ9+Lyju#i3qYe1+3|S)}E{U zmGBf5il`l^8dtdWd!=9U2DD0JgnN{eXT&3?M_?*qB-xlKVHnymOCFv@@|D!IYND@Gi8UHcQoXEboM59Pm zAz5%!G?R)#Wsxe1T84?`+AR5uCGD0bMNesdYkSYDH?4%n_ zJ}6zTwLc__occq0L2|Y-IjJN1b#_^~mN>Z(f8=^Gs>oOF_tg8i9=s;%EU6(;Ltze> z9Fa;zpGt1*ZS@pg6FII^htT?Jz-o0FJXH>*nC&zHkz+FIMCJw>Shm?6avBnhv#*|b z!$4L07@M;YFB4TUD(>514i6Cs&%+k$t6_;U91LsKWlIH8j8&`atRZb;<4R04ZiujR z4z-mYK#WPPcEN525Z_Hi>egHm?&|&RBkAE4_1A2;uMJ3Lu44KQYU|+ESZa&}9Zv5^rPIl$f|M7EBsbq7V$Z z>-GVd%odDK*nfNbp;v6nI(qrcMHz(>d5Z>sB(+)eXA8Y@^14H&oTMz$4M%mzhj;dN zi!X;9R~qywyXxIVDq=yI~F-_CG5$NVxrw<@4M59I5xK-6y0wn;6TtSu!0Q2Pt)YA6R{WOah zAbgw9Ne^B&`^*IgwgIwJx2|pU>2B{t3NsTr1@bIOl7W`GCWU1BVmz;s0(AsrGEOFR zcd2N+K*pk>)<~le7?ByNR8y!b`HeOV1{<2CGxoZ-vnZjGDhy$r&qU3?SoM=-nh>@` zZbqq>I*B}IHql09n*KCSc<01jXIa`DP?ep0y`einUg}$)1P+yMw+`LYibFf)fSCw# z#Nk82T<+NtnD9yZe9>Nwu1(++B@wyV=0m#kRtUHW{%n{3Jb* zvB*xu5IGSF?DCK!)E`b{2nKLtLiUuzZLdEG+`c}(3$TOiohPmzilt^h8Y;jCxKg(gT-en&5+k_0XAFUD?|y_ z8g~RPyID}K2_hzq4u&ln*K;Ml;SsYZpRe_c7YcxxLQF%f`lE>>O0E?11B9b6UlAHS z+%}S1i4Ve4r!xKZk#A3@kZ?_CgPdBP-ak^4%FR~Bbdbi|{ghL`2#bRpV^nwYmGI3c z2^e2PVsq+VeQ>U3U5#}^VF)$>auBSPHDqJemYRyVdSv zUw=DJ=U-jCJ$AtFk8j>xe0Kf)^o`u4{>Fm)ffHhm1@ARE^dHLbT<{W`PNaO@!jZe-@Sp>Ni4jcPUpP5{s-^6M-1Eo*v)M*fkFI|A|>Y(*(f%$DpFFCL&1tF;gn6x%VQIR^0^Ycftio7i%4KcXRRX@_4h;hqS->9C3Vnd;R|S$@KBNH=i`2 ooWH)lzMI5wmHPSD@BV#+`ucI`#m!x1hms??-A5n2`okCh0UT+7O#lD@ From 72e4e51210ff251c149d1db05a49f1d2a217f5a5 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Wed, 20 Mar 2024 10:55:40 -0700 Subject: [PATCH 50/51] iOS UI improvements (#2412) Task/Issue URL: https://app.asana.com/0/1199230911884351/1206836179343895/f Tech Design URL: CC: Description: This PR updates BSK to the iOS UI improvements branch. --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 +-- .../MacPacketTunnelProvider.swift | 2 +- .../VPNMetadataCollector.swift | 2 +- .../DataBrokerProtection/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- .../TunnelControllerViewModel.swift | 4 +-- .../TunnelControllerViewModelTests.swift | 27 ++++++++++++++----- LocalPackages/SubscriptionUI/Package.swift | 2 +- 9 files changed, 31 insertions(+), 16 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index acd1c8bb72..85d4a50c36 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -14109,7 +14109,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 126.2.0; + version = 127.0.0; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index fbe5e5ab94..23eb204428 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "0c73586c2628381b8a63be65fd2bc1824e58d7f9", - "version" : "126.2.0" + "revision" : "a693511191833453c0469e073c7090dd4a192a74", + "version" : "127.0.0" } }, { diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift index acb06735b3..00b275a063 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift @@ -387,7 +387,7 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { } let serverStatusInfo = NetworkProtectionStatusServerInfo( - serverLocation: serverInfo.serverLocation, + serverLocation: serverInfo.attributes, serverAddress: serverInfo.endpoint?.host.hostWithoutPort ) let payload = ServerSelectedNotificationObjectEncoder().encode(serverStatusInfo) diff --git a/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift b/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift index 571eb7bd34..58849d4211 100644 --- a/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift +++ b/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift @@ -236,7 +236,7 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { let connectionState = String(describing: statusReporter.statusObserver.recentValue) let lastErrorMessage = statusReporter.connectionErrorObserver.recentValue ?? "none" - let connectedServer = statusReporter.serverInfoObserver.recentValue.serverLocation ?? "none" + let connectedServer = statusReporter.serverInfoObserver.recentValue.serverLocation?.serverLocation ?? "none" let connectedServerIP = statusReporter.serverInfoObserver.recentValue.serverAddress ?? "none" return .init(onboardingState: onboardingState, connectionState: connectionState, diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 25aa35182d..af3abd24c7 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -29,7 +29,7 @@ let package = Package( targets: ["DataBrokerProtection"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.2.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "127.0.0"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 2dc7d68c5c..9c95668e6d 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.2.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "127.0.0"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems"), diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift index ab1443ba8d..f2dae8b810 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift @@ -95,7 +95,7 @@ public final class TunnelControllerViewModel: ObservableObject { connectionStatus = statusReporter.statusObserver.recentValue internalServerAddress = statusReporter.serverInfoObserver.recentValue.serverAddress - internalServerLocation = statusReporter.serverInfoObserver.recentValue.serverLocation + internalServerLocation = statusReporter.serverInfoObserver.recentValue.serverLocation?.serverLocation // Particularly useful when unit testing with an initial status of our choosing. refreshInternalIsRunning() @@ -146,7 +146,7 @@ public final class TunnelControllerViewModel: ObservableObject { Task { @MainActor in self.internalServerAddress = serverInfo.serverAddress - self.internalServerLocation = serverInfo.serverLocation + self.internalServerLocation = serverInfo.serverLocation?.serverLocation } } .store(in: &cancellables) diff --git a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/TunnelControllerViewModelTests.swift b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/TunnelControllerViewModelTests.swift index 3e8068b76f..83caf2cff6 100644 --- a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/TunnelControllerViewModelTests.swift +++ b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/TunnelControllerViewModelTests.swift @@ -28,7 +28,7 @@ final class TunnelControllerViewModelTests: XCTestCase { private class MockStatusReporter: NetworkProtectionStatusReporter { static let defaultServerInfo = NetworkProtectionStatusServerInfo( - serverLocation: "New York, USA", + serverLocation: TunnelControllerViewModelTests.serverAttributes(), serverAddress: "127.0.0.1") let statusObserver: ConnectionStatusObserver @@ -138,14 +138,13 @@ final class TunnelControllerViewModelTests: XCTestCase { /// @MainActor func testProperlyReflectsStatusConnected() async throws { - let mockServerLocation = "Los Angeles, United States" let mockServerIP = "127.0.0.1" let mockDate = Date().addingTimeInterval(-59) let mockDateString = "00:00:59" let controller = MockTunnelController() let serverInfo = NetworkProtectionStatusServerInfo( - serverLocation: mockServerLocation, + serverLocation: Self.serverAttributes(), serverAddress: mockServerIP) let statusReporter = MockStatusReporter(status: .connected(connectedDate: mockDate), serverInfo: serverInfo) let model = TunnelControllerViewModel( @@ -161,7 +160,7 @@ final class TunnelControllerViewModelTests: XCTestCase { XCTAssertEqual(model.featureStatusDescription, UserText.networkProtectionStatusViewFeatureOn) XCTAssertTrue(model.showServerDetails) XCTAssertEqual(model.serverAddress, mockServerIP) - XCTAssertEqual(model.serverLocation, "Los Angeles, United States...") + XCTAssertEqual(model.serverLocation, "El Segundo, CA...") } /// We expect the model to properly reflect the connecting status. @@ -211,11 +210,10 @@ final class TunnelControllerViewModelTests: XCTestCase { @MainActor func testStopsNetworkProtection() async throws { let mockDate = Date().addingTimeInterval(-59) - let mockServerLocation = "Los Angeles, United States" let mockServerIP = "127.0.0.1" let controller = MockTunnelController() - let serverInfo = NetworkProtectionStatusServerInfo(serverLocation: mockServerLocation, serverAddress: mockServerIP) + let serverInfo = NetworkProtectionStatusServerInfo(serverLocation: Self.serverAttributes(), serverAddress: mockServerIP) let statusReporter = MockStatusReporter( status: .connected(connectedDate: mockDate), serverInfo: serverInfo) @@ -237,4 +235,21 @@ final class TunnelControllerViewModelTests: XCTestCase { await fulfillment(of: [networkProtectionWasStopped], timeout: 0.1) } + + fileprivate static func serverAttributes() -> NetworkProtectionServerInfo.ServerAttributes { + let json = """ + { + "city": "El Segundo", + "country": "us", + "latitude": 33.9192, + "longitude": -118.4165, + "region": "North America", + "state": "ca", + "tzOffset": -28800 + } + """ + + // swiftlint:disable:next force_try + return try! JSONDecoder().decode(NetworkProtectionServerInfo.ServerAttributes.self, from: json.data(using: .utf8)!) + } } diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index f2568a8053..ccf7c0509a 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "126.2.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "127.0.0"), .package(path: "../SwiftUIExtensions") ], targets: [ From 234e2d5e11a38f1d5fb469d90e88e8a744e17ad2 Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Wed, 20 Mar 2024 20:05:32 +0100 Subject: [PATCH 51/51] Reset subs VPN state from debug menu (#2462) Task/Issue URL: https://app.asana.com/0/72649045549333/1206886385075824/f Description: Clearing the user's internal state does not clear subscription state. We want it cleared, so that testers can go back to testing the waitlist and work on a clean slate. Adds this to To reset all state / reset all state keeping invite code. To clear internal user state. --- DuckDuckGo/Menus/MainMenuActions.swift | 1 + .../BothAppTargets/NetworkProtectionDebugUtilities.swift | 1 + 2 files changed, 2 insertions(+) diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index 0800b3af45..4798b176f9 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -721,6 +721,7 @@ extension MainViewController { // by resetting this we allow users to go back to waitlist // and re-test. resetThankYouModalChecks(nil) + UserDefaults.netP.networkProtectionEntitlementsExpired = false } @objc func resetDailyPixels(_ sender: Any?) { diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift index 8c89f9dc6a..25959d1c25 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift @@ -71,6 +71,7 @@ final class NetworkProtectionDebugUtilities { UserDefaults().removeObject(forKey: UserDefaultsWrapper.Key.networkProtectionTermsAndConditionsAccepted.rawValue) NotificationCenter.default.post(name: .networkProtectionWaitlistAccessChanged, object: nil) + UserDefaults.netP.networkProtectionEntitlementsExpired = false } func removeSystemExtensionAndAgents() async throws {