From 7270c513b0f6183d4bb19927bda8de466cea785c Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Tue, 29 Oct 2024 11:00:46 +0100 Subject: [PATCH 01/22] Update home page settings popover message (#3468) Task/Issue URL: https://app.asana.com/0/72649045549333/1208641833599433/f --- DuckDuckGo/Common/Localizables/UserText.swift | 4 +- DuckDuckGo/Localizable.xcstrings | 38 +++++++++---------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index d9ec142e79..aeb44a6e48 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -1182,8 +1182,8 @@ struct UserText { static let bookmarksBarPromptAccept = NSLocalizedString("bookmarks.bar.prompt.accept", value: "Show", comment: "Accept button label on bookmarks bar prompt") // MARK: Home Page Settings - static let homePageSettingsOnboardingTitle = NSLocalizedString("home.page.settings.onboarding.title", value: "Add extra personality to your new tab page", comment: "Home Page Settings Onboarding message title") - static let homePageSettingsOnboardingMessage = NSLocalizedString("home.page.settings.onboarding.message", value: "Customize the background, theme, and even what content you see. Give it a try!", comment: "Home Page Settings Onboarding message") + static let homePageSettingsOnboardingTitle = NSLocalizedString("home.page.settings.onboarding.title", value: "New search box, custom backgrounds & more!", comment: "Home Page Settings Onboarding message title") + static let homePageSettingsOnboardingMessage = NSLocalizedString("home.page.settings.onboarding.message", value: "Add extra personality and pick what you want to see on your new tab page. Give it a try!", comment: "Home Page Settings Onboarding message") static let homePageSettingsTitle = NSLocalizedString("home.page.settings.header", value: "Customize", comment: "Home Page Settings title") static let goToSettings = NSLocalizedString("home.page.settings.go.to.settings", value: "Go to Settings", comment: "Settings button caption") static let background = NSLocalizedString("home.page.settings.background", value: "Background", comment: "Section title in Home Page Settings to customization Home Page background") diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 918c1bea94..0d45c59159 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -27359,55 +27359,55 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Passe den Hintergrund, das Design und sogar die angezeigten Inhalte an. Versuch es doch mal!" + "value" : "Personalisiere deine neue Tab-Seite. Probier’s doch mal aus!" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "Customize the background, theme, and even what content you see. Give it a try!" + "value" : "Add extra personality and pick what you want to see on your new tab page. Give it a try!" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Personaliza el fondo, el tema e incluso el contenido que ves. ¡Pruébalo!" + "value" : "Añade más personalidad y elige lo que quieres ver en tu nueva pestaña. ¡Pruébalo!" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Personnalisez l'arrière-plan, le thème et même le contenu que vous voyez. Essayez par vous-même !" + "value" : "Ajoutez une personnalité supplémentaire et choisissez ce que vous voulez voir sur votre nouvelle page à onglet. Essayez par vous-même !" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Personalizza lo sfondo, il tema e persino i contenuti che vedi. Prova!" + "value" : "Aggiungi un tocco di personalità e scegli cosa vuoi vedere nella tua nuova scheda. Prova!" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Pas de achtergrond, het thema en zelfs de inhoud aan die je ziet. Probeer het maar eens!" + "value" : "Voeg extra persoonlijkheid toe en kies wat je op je nieuwe tabbladpagina wilt zien. Probeer het maar eens!" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Dostosuj tło, motyw oraz wyświetlaną zawartość. Spróbuj!" + "value" : "Wybierz, co chcesz zobaczyć na stronie nowej karty, aby zyskała bardziej indywidualny charakter. Spróbuj!" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Personaliza o fundo, o tema e até o conteúdo que vês. Experimenta!" + "value" : "Adiciona personalidade extra e escolhe o que queres ver na tua página do novo separador. Experimenta!" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Можно настроить фон, тему и даже отображаемый контент. Давайте попробуем!" + "value" : "Добавьте дополнительную персонализацию и выберите, что должно присутствовать на странице новой вкладки. Попробуйте!" } } } @@ -27419,55 +27419,55 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Personalisiere deine neue Tab-Seite" + "value" : "Neues Suchfeld, benutzerdefinierte Hintergründe und mehr!" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "Add extra personality to your new tab page" + "value" : "New search box, custom backgrounds & more!" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Añade más personalidad a tu página de nueva pestaña" + "value" : "¡Nuevo cuadro de búsqueda, fondos personalizados y mucho más!" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Ajoutez une personnalité supplémentaire à votre nouvelle page à onglet" + "value" : "Nouvelle boîte de recherche, arrière-plans personnalisés et plus encore !" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Aggiungi un tocco di personalità alla tua nuova scheda" + "value" : "Nuova casella di ricerca, sfondi personalizzati e altro ancora!" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Voeg extra persoonlijkheid toe aan je nieuwe tabbladpagina" + "value" : "Nieuw zoekvak, aangepaste achtergronden en meer!" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Dostosuj stronę nowej zakładki do potrzeb" + "value" : "Nowe pole wyszukiwania, niestandardowe tła i nie tylko!" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Adiciona personalidade extra à tua página do novo separador" + "value" : "Nova caixa de pesquisa, fundos personalizados e mais!" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Дополнительная персонализация новой вкладки" + "value" : "Новая строка поиска, собственные фоны и многое другое" } } } @@ -62878,4 +62878,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file From c212844e781d80a20ec00575290f4dbbd823da35 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Wed, 30 Oct 2024 07:26:32 +0000 Subject: [PATCH 02/22] Bump version to 1.112.0 (292) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index b6f88103f6..3a2f81093c 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 291 +CURRENT_PROJECT_VERSION = 292 From b0e1d89e04721336118f54e8a472b060c9471c09 Mon Sep 17 00:00:00 2001 From: Sabrina Tardio <44158575+SabrinaTardio@users.noreply.github.com> Date: Wed, 30 Oct 2024 10:01:19 +0100 Subject: [PATCH 03/22] add action to skip onboarding (#3472) Task/Issue URL: https://app.asana.com/0/1204186595873227/1208509741953377/f **Description**: Adds a debug action to skip the onboarding --- DuckDuckGo/Menus/MainMenu.swift | 1 + DuckDuckGo/Menus/MainMenuActions.swift | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/DuckDuckGo/Menus/MainMenu.swift b/DuckDuckGo/Menus/MainMenu.swift index e3f67e7aa9..bdf08d8e44 100644 --- a/DuckDuckGo/Menus/MainMenu.swift +++ b/DuckDuckGo/Menus/MainMenu.swift @@ -611,6 +611,7 @@ final class MainMenu: NSMenu { NSMenuItem(title: "150 Tabs", action: #selector(MainViewController.addDebugTabs(_:)), representedObject: 150) } } + NSMenuItem(title: "Skip Onboarding", action: #selector(MainViewController.skipOnboarding)) NSMenuItem(title: "Reset Data") { NSMenuItem(title: "Reset Default Browser Prompt", action: #selector(MainViewController.resetDefaultBrowserPrompt)) NSMenuItem(title: "Reset Default Grammar Checks", action: #selector(MainViewController.resetDefaultGrammarChecks)) diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index 197e3ae0dd..00f780c18b 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -849,6 +849,13 @@ extension MainViewController { UserDefaults.standard.set(true, forKey: UserDefaultsWrapper.Key.homePageShowEmailProtection.rawValue) } + @objc func skipOnboarding(_ sender: Any?) { + UserDefaults.standard.set(true, forKey: UserDefaultsWrapper.Key.onboardingFinished.rawValue) + Application.appDelegate.onboardingStateMachine.state = .onboardingCompleted + WindowControllersManager.shared.updatePreventUserInteraction(prevent: false) + WindowControllersManager.shared.replaceTabWith(Tab(content: .newtab)) + } + @objc func resetOnboarding(_ sender: Any?) { UserDefaults.standard.set(false, forKey: UserDefaultsWrapper.Key.onboardingFinished.rawValue) } From d40852e7c22e9d97892af49e2df159cba34c6f58 Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Wed, 30 Oct 2024 20:26:53 +0100 Subject: [PATCH 04/22] Add pixels to help diagnose sync deactivation (#3476) Task/Issue URL: https://app.asana.com/0/1201493110486074/1208640658073530/f **Description**: Sync is being randomly deactivated for some users and we don't know why. This PR: - Sends a missing pixel when device refresh fails (which can result in deactivation) - Sends a high-level pixels which recreates this scenario so we can assess the impact of this issue and know when we've fixed it. **Steps to test this PR**: 1. (just for testing) Add a line of code to easily deactivate sync in a non-typical way (e.g by calling `try await syncService.deleteAccount()` from `updateDeviceName`) so you can mimic unexpected deactivation. 2. Make sure Sync is enabled 3. Trigger the fake unexpected deactivation 4. Minimize the browser and maximise again. 5. Make sure the `m_mac_sync_was_disabled_unexpectedly` pixel is fired. **Definition of Done**: * [ ] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/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) --- DuckDuckGo.xcodeproj/project.pbxproj | 6 ++ DuckDuckGo/Application/AppDelegate.swift | 10 ++- .../Preferences/Model/SyncPreferences.swift | 29 +++++---- DuckDuckGo/Statistics/GeneralPixel.swift | 5 ++ DuckDuckGo/Sync/SyncDiagnosisHelper.swift | 63 +++++++++++++++++++ 5 files changed, 98 insertions(+), 15 deletions(-) create mode 100644 DuckDuckGo/Sync/SyncDiagnosisHelper.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index a40d4df2c9..a3acccb424 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2948,6 +2948,8 @@ EED9A6762C37FE6900E0FAB9 /* login_deduplication_test_data.csv in Resources */ = {isa = PBXBuildFile; fileRef = EED9A6732C37FE6800E0FAB9 /* login_deduplication_test_data.csv */; }; EED9A6772C37FE6900E0FAB9 /* login_deduplication_starting_data.csv in Resources */ = {isa = PBXBuildFile; fileRef = EED9A6742C37FE6900E0FAB9 /* login_deduplication_starting_data.csv */; }; EED9A6782C37FE6900E0FAB9 /* login_deduplication_starting_data.csv in Resources */ = {isa = PBXBuildFile; fileRef = EED9A6742C37FE6900E0FAB9 /* login_deduplication_starting_data.csv */; }; + EEDFA38A2CD148DE00D1C558 /* SyncDiagnosisHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEDFA3892CD148DE00D1C558 /* SyncDiagnosisHelper.swift */; }; + EEDFA38B2CD148DE00D1C558 /* SyncDiagnosisHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEDFA3892CD148DE00D1C558 /* SyncDiagnosisHelper.swift */; }; EEE0E1CD2C32F5690058E148 /* CSVImporterIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEE0E1CC2C32F5690058E148 /* CSVImporterIntegrationTests.swift */; }; EEE0E1CF2C32F6530058E148 /* mock_login_data_large.csv in Resources */ = {isa = PBXBuildFile; fileRef = EEE0E1CE2C32F6530058E148 /* mock_login_data_large.csv */; }; EEE0E1D02C32F6530058E148 /* mock_login_data_large.csv in Resources */ = {isa = PBXBuildFile; fileRef = EEE0E1CE2C32F6530058E148 /* mock_login_data_large.csv */; }; @@ -4706,6 +4708,7 @@ EED735352BB46B6000F173D6 /* AutocompleteTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutocompleteTests.swift; sourceTree = ""; }; EED9A6732C37FE6800E0FAB9 /* login_deduplication_test_data.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = login_deduplication_test_data.csv; sourceTree = ""; }; EED9A6742C37FE6900E0FAB9 /* login_deduplication_starting_data.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = login_deduplication_starting_data.csv; sourceTree = ""; }; + EEDFA3892CD148DE00D1C558 /* SyncDiagnosisHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncDiagnosisHelper.swift; sourceTree = ""; }; EEE0E1CC2C32F5690058E148 /* CSVImporterIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CSVImporterIntegrationTests.swift; sourceTree = ""; }; EEE0E1CE2C32F6530058E148 /* mock_login_data_large.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = mock_login_data_large.csv; sourceTree = ""; }; EEE11C5D2C7F54AD000ABD7E /* AutofillLoginImportState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillLoginImportState.swift; sourceTree = ""; }; @@ -5518,6 +5521,7 @@ 37CEFCA82A6737A2001EF741 /* CredentialsCleanupErrorHandling.swift */, 37A6A8F02AFCC988008580A3 /* FaviconsFetcherOnboarding.swift */, 37A6A8F52AFCCA59008580A3 /* FaviconsFetcherOnboardingViewController.swift */, + EEDFA3892CD148DE00D1C558 /* SyncDiagnosisHelper.swift */, ); path = Sync; sourceTree = ""; @@ -10798,6 +10802,7 @@ 3706FAD4293F65D500E42796 /* DataExtension.swift in Sources */, 3706FAD6293F65D500E42796 /* ConfigurationStore.swift in Sources */, FD22255E2C64B68500199373 /* AutoconsentExperiment.swift in Sources */, + EEDFA38B2CD148DE00D1C558 /* SyncDiagnosisHelper.swift in Sources */, 3706FAD7293F65D500E42796 /* Feedback.swift in Sources */, 1D0DE9422C3BB9CC0037ABC2 /* ReleaseNotesParser.swift in Sources */, EED4D3D92C874AE200C79EEA /* PopoverInfoViewController.swift in Sources */, @@ -12744,6 +12749,7 @@ 856CADF0271710F400E79BB0 /* HoverUserScript.swift in Sources */, B6DE57F62B05EA9000CD54B9 /* SheetHostingWindow.swift in Sources */, AA6EF9B525081B4C004754E6 /* MainMenuActions.swift in Sources */, + EEDFA38A2CD148DE00D1C558 /* SyncDiagnosisHelper.swift in Sources */, EED4D3D82C874AE200C79EEA /* PopoverInfoViewController.swift in Sources */, 56A0541F2C1CA1F5007D8FAB /* OnboardingTabExtension.swift in Sources */, B63D466925BEB6C200874977 /* WKWebView+SessionState.swift in Sources */, diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index c8c5f29855..a3ef983fee 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -433,8 +433,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { guard didFinishLaunching else { return } PixelExperiment.fireOnboardingTestPixels() - syncService?.initializeIfNeeded() - syncService?.scheduler.notifyAppLifecycleEvent() + initializeSync() NetworkProtectionAppEvents(featureGatekeeper: DefaultVPNFeatureGatekeeper(subscriptionManager: subscriptionManager)).applicationDidBecomeActive() @@ -457,6 +456,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate { } } + private func initializeSync() { + guard let syncService else { return } + syncService.initializeIfNeeded() + syncService.scheduler.notifyAppLifecycleEvent() + SyncDiagnosisHelper(syncService: syncService).diagnoseAccountStatus() + } + func applicationDidResignActive(_ notification: Notification) { Task { @MainActor in await vpnRedditSessionWorkaround.removeRedditSessionWorkaround() diff --git a/DuckDuckGo/Preferences/Model/SyncPreferences.swift b/DuckDuckGo/Preferences/Model/SyncPreferences.swift index 2718a4fe67..f6be1b3b6c 100644 --- a/DuckDuckGo/Preferences/Model/SyncPreferences.swift +++ b/DuckDuckGo/Preferences/Model/SyncPreferences.swift @@ -293,19 +293,6 @@ final class SyncPreferences: ObservableObject, SyncUI.ManagementViewModel { presentDialog(for: .removeDevice(device)) } - func turnOffSync() { - Task { @MainActor in - do { - try await syncService.disconnect() - managementDialogModel.endFlow() - syncPausedStateManager.syncDidTurnOff() - } catch { - managementDialogModel.syncErrorMessage = SyncErrorMessage(type: .unableToTurnSyncOff, description: error.localizedDescription) - PixelKit.fire(DebugEvent(GeneralPixel.syncLogoutError(error: error))) - } - } - } - @MainActor func manageBookmarks() { guard let mainVC = WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController else { return } @@ -389,6 +376,7 @@ final class SyncPreferences: ObservableObject, SyncUI.ManagementViewModel { let registeredDevices = try await syncService.fetchDevices() mapDevices(registeredDevices) } catch { + PixelKit.fire(DebugEvent(GeneralPixel.syncRefreshDevicesError(error: error))) Logger.sync.debug("Failed to refresh devices: \(error)") } } @@ -450,12 +438,27 @@ final class SyncPreferences: ObservableObject, SyncUI.ManagementViewModel { extension SyncPreferences: ManagementDialogModelDelegate { + func turnOffSync() { + Task { @MainActor in + do { + try await syncService.disconnect() + managementDialogModel.endFlow() + syncPausedStateManager.syncDidTurnOff() + SyncDiagnosisHelper(syncService: syncService).didManuallyDisableSync() + } catch { + managementDialogModel.syncErrorMessage = SyncErrorMessage(type: .unableToTurnSyncOff, description: error.localizedDescription) + PixelKit.fire(DebugEvent(GeneralPixel.syncLogoutError(error: error))) + } + } + } + func deleteAccount() { Task { @MainActor in do { try await syncService.deleteAccount() managementDialogModel.endFlow() syncPausedStateManager.syncDidTurnOff() + SyncDiagnosisHelper(syncService: syncService).didManuallyDisableSync() } catch { managementDialogModel.syncErrorMessage = SyncErrorMessage(type: .unableToDeleteData, description: error.localizedDescription) PixelKit.fire(DebugEvent(GeneralPixel.syncDeleteAccountError(error: error))) diff --git a/DuckDuckGo/Statistics/GeneralPixel.swift b/DuckDuckGo/Statistics/GeneralPixel.swift index 416c3dfec3..2f43b9d84a 100644 --- a/DuckDuckGo/Statistics/GeneralPixel.swift +++ b/DuckDuckGo/Statistics/GeneralPixel.swift @@ -174,6 +174,7 @@ enum GeneralPixel: PixelKitEventV2 { case syncBookmarksValidationErrorDaily case syncCredentialsValidationErrorDaily case syncSettingsValidationErrorDaily + case syncDebugWasDisabledUnexpectedly // Remote Messaging Framework case remoteMessageShown @@ -397,6 +398,7 @@ enum GeneralPixel: PixelKitEventV2 { case syncLogoutError(error: Error) case syncUpdateDeviceError(error: Error) case syncRemoveDeviceError(error: Error) + case syncRefreshDevicesError(error: Error) case syncDeleteAccountError(error: Error) case syncLoginExistingAccountError(error: Error) case syncCannotCreateRecoveryPDF @@ -687,6 +689,7 @@ enum GeneralPixel: PixelKitEventV2 { case .syncBookmarksValidationErrorDaily: return "m_mac_sync_bookmarks_validation_error_daily" case .syncCredentialsValidationErrorDaily: return "m_mac_sync_credentials_validation_error_daily" case .syncSettingsValidationErrorDaily: return "m_mac_sync_settings_validation_error_daily" + case .syncDebugWasDisabledUnexpectedly: return "m_mac_sync_was_disabled_unexpectedly" case .remoteMessageShown: return "m_mac_remote_message_shown" case .remoteMessageShownUnique: return "m_mac_remote_message_shown_unique" @@ -1010,6 +1013,7 @@ enum GeneralPixel: PixelKitEventV2 { case .syncLogoutError: return "sync_logout_error" case .syncUpdateDeviceError: return "sync_update_device_error" case .syncRemoveDeviceError: return "sync_remove_device_error" + case .syncRefreshDevicesError: return "sync_refresh_devices_error" case .syncDeleteAccountError: return "sync_delete_account_error" case .syncLoginExistingAccountError: return "sync_login_existing_account_error" case .syncCannotCreateRecoveryPDF: return "sync_cannot_create_recovery_pdf" @@ -1065,6 +1069,7 @@ enum GeneralPixel: PixelKitEventV2 { .syncLogoutError(let error), .syncUpdateDeviceError(let error), .syncRemoveDeviceError(let error), + .syncRefreshDevicesError(let error), .syncDeleteAccountError(let error), .syncLoginExistingAccountError(let error), .bookmarksCouldNotLoadDatabase(let error?): diff --git a/DuckDuckGo/Sync/SyncDiagnosisHelper.swift b/DuckDuckGo/Sync/SyncDiagnosisHelper.swift new file mode 100644 index 0000000000..dcce535fe3 --- /dev/null +++ b/DuckDuckGo/Sync/SyncDiagnosisHelper.swift @@ -0,0 +1,63 @@ +// +// SyncDiagnosisHelper.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 DDGSync +import PixelKit + +struct SyncDiagnosisHelper { + private let userDefaults = UserDefaults.standard + private let syncService: DDGSyncing + + @UserDefaultsWrapper(key: .syncManuallyDisabledKey) + private var syncManuallyDisabled: Bool? + + @UserDefaultsWrapper(key: .syncWasDisabledUnexpectedlyPixelFiredKey, defaultValue: false) + private var syncWasDisabledUnexpectedlyPixelFired: Bool + + init(syncService: DDGSyncing) { + self.syncService = syncService + } + +// Non-user-initiated deactivation +// For events to help understand the impact of https://app.asana.com/0/1201493110486074/1208538487332133/f + + func didManuallyDisableSync() { + syncManuallyDisabled = true + } + + func diagnoseAccountStatus() { + if syncService.account == nil { + // Nil value means sync was never on in the first place. So don't fire in this case. + if syncManuallyDisabled == false, + !syncWasDisabledUnexpectedlyPixelFired { + PixelKit.fire(DebugEvent(GeneralPixel.syncDebugWasDisabledUnexpectedly), frequency: .dailyAndCount) + syncWasDisabledUnexpectedlyPixelFired = true + } + } else { + syncManuallyDisabled = false + syncWasDisabledUnexpectedlyPixelFired = false + } + } + +} + +extension UserDefaultsWrapper.DefaultsKey { + static let syncManuallyDisabledKey = Self(rawValue: "com.duckduckgo.app.key.debug.SyncManuallyDisabled") + static let syncWasDisabledUnexpectedlyPixelFiredKey = Self(rawValue: "com.duckduckgo.app.key.debug.SyncWasDisabledUnexpectedlyPixelFired") +} From 8a907fc034e22951af9adbf2742b99770da8b48f Mon Sep 17 00:00:00 2001 From: Christopher Brind Date: Wed, 30 Oct 2024 20:10:30 +0000 Subject: [PATCH 05/22] add system info to webkit termination validation (#3473) Task/Issue URL: https://app.asana.com/0/392891325557410/1208654005436040/f Tech Design URL: CC: **Description**: Adds system info (disk space remaining, memory remaining, os version) to the webkit termination pixel. **Steps to test this PR**: 1. Use non-App Store build 2. Open a page in a new tab 3. Use kill the process 4. Observe pixel being fired with parameters 1. With App Store build 2. Repeat above, no parameters should be added --- DuckDuckGo.xcodeproj/project.pbxproj | 6 ++ .../Statistics/ATB/StatisticsLoader.swift | 3 +- DuckDuckGo/Tab/Model/SystemInfo.swift | 76 +++++++++++++++++++ DuckDuckGo/Tab/Model/Tab.swift | 10 ++- 4 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 DuckDuckGo/Tab/Model/SystemInfo.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index cff2085141..ea53332616 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1934,6 +1934,8 @@ 85AC3B4925DAC9BD00C7D2AA /* ConfigurationStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85AC3B4825DAC9BD00C7D2AA /* ConfigurationStorageTests.swift */; }; 85AC7ADB27BD628400FFB69B /* HomePage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85AC7ADA27BD628400FFB69B /* HomePage.swift */; }; 85AC7ADD27BEB6EE00FFB69B /* HomePageDefaultBrowserModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85AC7ADC27BEB6EE00FFB69B /* HomePageDefaultBrowserModel.swift */; }; + 85B49AFA2CD1B7C5007FAA2A /* SystemInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85B49AF92CD1B7C5007FAA2A /* SystemInfo.swift */; }; + 85B49AFB2CD1B7C5007FAA2A /* SystemInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85B49AF92CD1B7C5007FAA2A /* SystemInfo.swift */; }; 85B7184A27677C2D00B4277F /* Onboarding.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 85B7184927677C2D00B4277F /* Onboarding.storyboard */; }; 85B7184C27677C6500B4277F /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85B7184B27677C6500B4277F /* OnboardingViewController.swift */; }; 85B7184E27677CBB00B4277F /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85B7184D27677CBB00B4277F /* RootView.swift */; }; @@ -4067,6 +4069,7 @@ 85AC7ADA27BD628400FFB69B /* HomePage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePage.swift; sourceTree = ""; }; 85AC7ADC27BEB6EE00FFB69B /* HomePageDefaultBrowserModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePageDefaultBrowserModel.swift; sourceTree = ""; }; 85AE2FF124A33A2D002D507F /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; + 85B49AF92CD1B7C5007FAA2A /* SystemInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemInfo.swift; sourceTree = ""; }; 85B7184927677C2D00B4277F /* Onboarding.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Onboarding.storyboard; sourceTree = ""; }; 85B7184B27677C6500B4277F /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = ""; }; 85B7184D27677CBB00B4277F /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = ""; }; @@ -8084,6 +8087,7 @@ B634DBE2293C8FFF00C3C99E /* UserDialogRequest.swift */, 1D1A33482A6FEB170080ACED /* BurnerMode.swift */, 56406D4A2C636A8900BF8FA2 /* SpecialPagesUserScriptExtension.swift */, + 85B49AF92CD1B7C5007FAA2A /* SystemInfo.swift */, ); path = Model; sourceTree = ""; @@ -11318,6 +11322,7 @@ 3706FBB5293F65D500E42796 /* UserContentUpdating.swift in Sources */, 4B4D60B72A0C847D00BCD287 /* NetworkProtectionNavBarButtonModel.swift in Sources */, 3706FBB6293F65D500E42796 /* ChromiumPreferences.swift in Sources */, + 85B49AFB2CD1B7C5007FAA2A /* SystemInfo.swift in Sources */, 3706FBB7293F65D500E42796 /* FirePopoverViewController.swift in Sources */, 3706FBB8293F65D500E42796 /* SavePaymentMethodPopover.swift in Sources */, B6CC266D2BAD9CD800F53F8D /* FileProgressPresenter.swift in Sources */, @@ -12883,6 +12888,7 @@ 3706FEC8293F6F7500E42796 /* BWManagement.swift in Sources */, 316C48F02CC2B232000B08C1 /* AIChatPreferencesStorage.swift in Sources */, B6830961274CDE99004B46BB /* FireproofDomainsContainer.swift in Sources */, + 85B49AFA2CD1B7C5007FAA2A /* SystemInfo.swift in Sources */, B687B7CC2947A1E9001DEA6F /* ExternalAppSchemeHandler.swift in Sources */, B65536AE2685E17200085A79 /* GeolocationService.swift in Sources */, 4B02198925E05FAC00ED7DEA /* FireproofingURLExtensions.swift in Sources */, diff --git a/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift b/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift index aabf7a5abe..a4f7f7404b 100644 --- a/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift +++ b/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift @@ -224,8 +224,7 @@ final class StatisticsLoader { DispatchQueue.global().asyncAfter(deadline: .now() + randomDelay) { PixelKit.fire(GeneralPixel.dailyOsVersionCounter, - frequency: .legacyDaily, - includeAppVersionParameter: false) + frequency: .legacyDaily) } } diff --git a/DuckDuckGo/Tab/Model/SystemInfo.swift b/DuckDuckGo/Tab/Model/SystemInfo.swift new file mode 100644 index 0000000000..58240a11e5 --- /dev/null +++ b/DuckDuckGo/Tab/Model/SystemInfo.swift @@ -0,0 +1,76 @@ +// +// SystemInfo.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 Common + +#if !APPSTORE + +final class SystemInfo { + + static func pixelParameters(appVersion: AppVersion = AppVersion.shared) async -> [String: String] { + let availableMemoryPercent = Self.getAvailableMemoryPercent() + let availableDiskSpacePercent = Self.getAvailableDiskSpacePercent() + return [ + "available_memory": String(availableMemoryPercent), + "available_diskspace": String(format: "%.2f", availableDiskSpacePercent), + "os_version": appVersion.osVersion, + ] + } + + static func getAvailableMemoryPercent() -> Int { + let process = Process() + process.executableURL = URL(fileURLWithPath: "/usr/bin/memory_pressure") + + let pipe = Pipe() + process.standardOutput = pipe + + do { + try process.run() + process.waitUntilExit() + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + if let output = String(data: data, encoding: .utf8) { + // Should be the last line, but just in case search for it explicitly + let lines = output.split(separator: "\n") + if let memoryLine = lines.first(where: { $0.contains("System-wide memory free percentage") }), + let range = memoryLine.range(of: "\\d+", options: .regularExpression), + let percentage = Int(memoryLine[range]) { + return percentage + } + } + } catch { + assertionFailure("Unable to run memory_pressure") + } + + return -1 + } + + static func getAvailableDiskSpacePercent() -> Double { + guard let attributes = try? FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory()), + let totalSpace = attributes[.systemSize] as? UInt64, + let freeSpace = attributes[.systemFreeSize] as? UInt64 else { + return -1.0 + } + + return Double(freeSpace) / Double(totalSpace) * 100 + } + +} + +#endif diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift index af5691c907..e4449ac5bb 100644 --- a/DuckDuckGo/Tab/Model/Tab.swift +++ b/DuckDuckGo/Tab/Model/Tab.swift @@ -1274,7 +1274,15 @@ extension Tab/*: NavigationResponder*/ { // to be moved to Tab+Navigation.swift loadErrorHTML(error, header: UserText.webProcessCrashPageHeader, forUnreachableURL: url, alternate: true) } - PixelKit.fire(DebugEvent(GeneralPixel.webKitDidTerminate, error: error)) + Task { +#if APPSTORE + let additionalParameters = [String: String]() +#else + let additionalParameters = await SystemInfo.pixelParameters() +#endif + + PixelKit.fire(DebugEvent(GeneralPixel.webKitDidTerminate, error: error), withAdditionalParameters: additionalParameters) + } } @MainActor From fc7eaf4f358fab57ab5d82e87b72919726827862 Mon Sep 17 00:00:00 2001 From: Anh Do <18567+quanganhdo@users.noreply.github.com> Date: Wed, 30 Oct 2024 16:15:43 -0400 Subject: [PATCH 06/22] New tagline (#3401) Task/Issue URL: https://app.asana.com/0/1205591970852438/1208504563612286/f Tech Design URL: CC: **Description**: Updates new tagline. **Steps to test this PR**: 1. Check the About page **Definition of Done**: * [ ] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/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) --- DuckDuckGo/Common/Localizables/UserText.swift | 2 +- DuckDuckGo/Localizable.xcstrings | 62 ++++++++++++++++++- .../View/PreferencesAboutView.swift | 4 +- SyncE2EUITests/CriticalPathsTests.swift | 2 +- UITests/OnboardingUITests.swift | 4 +- .../TestBookmarksData/bookmarks_brave.html | 2 +- .../TestBookmarksData/bookmarks_chrome.html | 2 +- .../bookmarks_ddg_android.html | 2 +- .../TestBookmarksData/bookmarks_ddg_ios.html | 4 +- .../bookmarks_ddg_macos.html | 2 +- .../TestBookmarksData/bookmarks_firefox.html | 2 +- .../TestBookmarksData/bookmarks_safari.html | 2 +- .../snapshot.bookmarks_brave.json | 2 +- .../snapshot.bookmarks_chrome.json | 2 +- .../snapshot.bookmarks_ddg_android.json | 2 +- .../snapshot.bookmarks_ddg_ios.json | 2 +- .../snapshot.bookmarks_ddg_macos.json | 2 +- .../snapshot.bookmarks_firefox.json | 2 +- .../snapshot.bookmarks_safari.json | 2 +- 19 files changed, 82 insertions(+), 22 deletions(-) diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 847085afdc..11c92ff97c 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -693,7 +693,7 @@ struct UserText { 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") + static let duckduckgoTagline = NSLocalizedString("preferences.about.duckduckgo-tagline", value: "Your protection, our priority.", comment: "About screen") static let aboutUnsupportedDeviceInfo1 = NSLocalizedString("preferences.about.unsupported-device-info1", value: "DuckDuckGo is no longer providing browser updates for your version of macOS.", comment: "This string represents a message informing the user that DuckDuckGo is no longer providing browser updates for their version of macOS") static func aboutUnsupportedDeviceInfo2(version: String) -> String { let localized = NSLocalizedString("preferences.about.unsupported-device-info2", value: "Please update to macOS %@ or later to use the most recent version of DuckDuckGo. You can also keep using your current version of the browser, but it will not receive further updates.", comment: "Copy in section that tells the user to update their macOS version since their current version is unsupported") diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 0d45c59159..1da35b097b 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -50858,6 +50858,66 @@ } } }, + "preferences.about.duckduckgo-tagline" : { + "comment" : "About screen", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dein Schutz, unsere Priorität." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Your protection, our priority." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tu protección, nuestra prioridad." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Votre protection, notre priorité." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "La tua protezione, la nostra priorità." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jouw bescherming, onze prioriteit." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Twoje bezpieczeństwo jest naszym priorytetem." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "A tua proteção, a nossa prioridade." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ваша защита — наш приоритет." + } + } + } + }, "preferences.about.more-at" : { "comment" : "Link to the about page", "extractionState" : "extracted_with_value", @@ -50980,7 +51040,7 @@ }, "preferences.about.privacy-simplified" : { "comment" : "About screen", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { diff --git a/DuckDuckGo/Preferences/View/PreferencesAboutView.swift b/DuckDuckGo/Preferences/View/PreferencesAboutView.swift index ecf966ac2d..e76daee578 100644 --- a/DuckDuckGo/Preferences/View/PreferencesAboutView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesAboutView.swift @@ -108,7 +108,7 @@ extension Preferences { #if APPSTORE Text(UserText.duckDuckGoForMacAppStore).font(.companyName) - Text(UserText.privacySimplified).font(.privacySimplified) + Text(UserText.duckduckgoTagline).font(.privacySimplified) .fixedSize(horizontal: false, vertical: true) .multilineTextAlignment(.leading) @@ -121,7 +121,7 @@ extension Preferences { #else Text(UserText.duckDuckGo).font(.companyName) - Text(UserText.privacySimplified).font(.privacySimplified) + Text(UserText.duckduckgoTagline).font(.privacySimplified) .fixedSize(horizontal: false, vertical: true) .multilineTextAlignment(.leading) diff --git a/SyncE2EUITests/CriticalPathsTests.swift b/SyncE2EUITests/CriticalPathsTests.swift index 51c2d5c557..32562cbd1b 100644 --- a/SyncE2EUITests/CriticalPathsTests.swift +++ b/SyncE2EUITests/CriticalPathsTests.swift @@ -374,7 +374,7 @@ final class CriticalPathsTests: XCTestCase { } let duckduckgoBookmark = bookmarksWindow.staticTexts["www.duckduckgo.com"] let stackOverflow = bookmarksWindow.staticTexts["Stack Overflow - Where Developers Learn, Share, & Build Careers"] - let privacySimplified = bookmarksWindow.staticTexts["DuckDuckGo — Privacy, simplified."] + let privacySimplified = bookmarksWindow.staticTexts["DuckDuckGo — Your protection, our priority."] let wolfram = bookmarksWindow.staticTexts["Wolfram|Alpha: Computational Intelligence"] let news = bookmarksWindow.staticTexts["news"] let codes = bookmarksWindow.staticTexts["code"] diff --git a/UITests/OnboardingUITests.swift b/UITests/OnboardingUITests.swift index d83b751520..06b73047fc 100644 --- a/UITests/OnboardingUITests.swift +++ b/UITests/OnboardingUITests.swift @@ -110,8 +110,8 @@ final class OnboardingUITests: XCTestCase { startBrowsingButton.click() // AfterOnboarding - let duckduckgoPrivacySimplifiedWindow = app.windows["DuckDuckGo — Privacy, simplified."] - XCTAssertTrue(duckduckgoPrivacySimplifiedWindow.webViews["DuckDuckGo — Privacy, simplified."].waitForExistence(timeout: UITests.Timeouts.elementExistence)) + let duckduckgoPrivacySimplifiedWindow = app.windows["DuckDuckGo — Your protection, our priority."] + XCTAssertTrue(duckduckgoPrivacySimplifiedWindow.webViews["DuckDuckGo — Your protection, our priority."].waitForExistence(timeout: UITests.Timeouts.elementExistence)) XCTAssertTrue(duckduckgoPrivacySimplifiedWindow.buttons["NavigationBarViewController.optionsButton"].isEnabled) } diff --git a/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_brave.html b/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_brave.html index 7ba22d7cdd..d6c224a11b 100644 --- a/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_brave.html +++ b/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_brave.html @@ -47,7 +47,7 @@

Bookmarks

The Wall Street Journal - Breaking News, Business, Financial & Economic News, World News and Video

-

DuckDuckGo — Privacy, simplified. +
DuckDuckGo — Your protection, our priority.

DupeFolderNameContents

MacRumors: Apple News and Rumors diff --git a/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_chrome.html b/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_chrome.html index 08fca4e9b7..54f9bdb6ae 100644 --- a/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_chrome.html +++ b/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_chrome.html @@ -48,7 +48,7 @@

Bookmarks

The Wall Street Journal - Breaking News, Business, Financial & Economic News, World News and Video

-

DuckDuckGo — Privacy, simplified. +
DuckDuckGo — Your protection, our priority.

DupeFolderNameContents

MacRumors: Apple News and Rumors diff --git a/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_ddg_android.html b/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_ddg_android.html index 8a87fa9f41..1e7cb6bf92 100644 --- a/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_ddg_android.html +++ b/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_ddg_android.html @@ -47,7 +47,7 @@

Bookmarks

The Wall Street Journal - Breaking News, Business, Financial & Economic News, World News and Video

-

DuckDuckGo — Privacy, simplified. +
DuckDuckGo — Your protection, our priority.

DupeFolderNameContents

MacRumors: Apple News and Rumors diff --git a/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_ddg_ios.html b/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_ddg_ios.html index 9d592f0a73..5897360127 100644 --- a/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_ddg_ios.html +++ b/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_ddg_ios.html @@ -13,7 +13,7 @@

Bookmarks

FolderA-2

-

DuckDuckGo — Privacy, simplified. +
DuckDuckGo — Your protection, our priority.

FolderB

@@ -22,4 +22,4 @@

Bookmarks

Wikipedia

- \ No newline at end of file + diff --git a/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_ddg_macos.html b/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_ddg_macos.html index 9965f7d234..d3027ccc37 100644 --- a/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_ddg_macos.html +++ b/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_ddg_macos.html @@ -42,7 +42,7 @@

Bookmarks

The Wall Street Journal - Breaking News, Business, Financial & Economic News, World News and Video

-

DuckDuckGo — Privacy, simplified. +
DuckDuckGo — Your protection, our priority.

DupeFolderNameContents

MacRumors: Apple News and Rumors diff --git a/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_firefox.html b/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_firefox.html index 92c60fcd13..24f2804799 100644 --- a/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_firefox.html +++ b/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_firefox.html @@ -55,7 +55,7 @@

Bookmarks Menu

The Wall Street Journal - Breaking News, Business, Financial & Economic News, World News and Video

-

DuckDuckGo — Privacy, simplified. +
DuckDuckGo — Your protection, our priority.

DupeFolderNameContents

MacRumors: Apple News and Rumors diff --git a/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_safari.html b/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_safari.html index 560cbf6533..cf1e87b8be 100644 --- a/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_safari.html +++ b/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_safari.html @@ -53,7 +53,7 @@

Bookmarks

The Wall Street Journal - Breaking News, Business, Financial & Economic News, World News and Video

-

DuckDuckGo — Privacy, simplified. +
DuckDuckGo — Your protection, our priority.

DupeFolderNameContents

MacRumors: Apple News and Rumors diff --git a/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_brave.json b/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_brave.json index 80fbf50a09..15976f795f 100644 --- a/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_brave.json +++ b/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_brave.json @@ -109,7 +109,7 @@ "type" : "folder" }, { - "name" : "DuckDuckGo — Privacy, simplified.", + "name" : "DuckDuckGo — Your protection, our priority.", "type" : "bookmark", "url" : "https:\/\/duckduckgo.com\/" }, diff --git a/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_chrome.json b/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_chrome.json index cbcd35b844..baed9b3f74 100644 --- a/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_chrome.json +++ b/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_chrome.json @@ -116,7 +116,7 @@ "type" : "folder" }, { - "name" : "DuckDuckGo — Privacy, simplified.", + "name" : "DuckDuckGo — Your protection, our priority.", "type" : "bookmark", "url" : "https:\/\/duckduckgo.com\/" }, diff --git a/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_ddg_android.json b/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_ddg_android.json index cfd7ed9645..fea09751a1 100644 --- a/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_ddg_android.json +++ b/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_ddg_android.json @@ -109,7 +109,7 @@ "type" : "folder" }, { - "name" : "DuckDuckGo — Privacy, simplified.", + "name" : "DuckDuckGo — Your protection, our priority.", "type" : "bookmark", "url" : "https:\/\/duckduckgo.com\/" }, diff --git a/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_ddg_ios.json b/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_ddg_ios.json index 51dd407681..941910f466 100644 --- a/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_ddg_ios.json +++ b/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_ddg_ios.json @@ -39,7 +39,7 @@ { "children" : [ { - "name" : "DuckDuckGo — Privacy, simplified.", + "name" : "DuckDuckGo — Your protection, our priority.", "type" : "bookmark", "url" : "https:\/\/duckduckgo.com" } diff --git a/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_ddg_macos.json b/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_ddg_macos.json index 390cac5521..dc6afebdc5 100644 --- a/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_ddg_macos.json +++ b/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_ddg_macos.json @@ -116,7 +116,7 @@ "type" : "folder" }, { - "name" : "DuckDuckGo — Privacy, simplified.", + "name" : "DuckDuckGo — Your protection, our priority.", "type" : "bookmark", "url" : "https:\/\/duckduckgo.com\/" }, diff --git a/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_firefox.json b/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_firefox.json index d3428b67b2..bca8960b81 100644 --- a/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_firefox.json +++ b/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_firefox.json @@ -135,7 +135,7 @@ "type" : "folder" }, { - "name" : "DuckDuckGo — Privacy, simplified.", + "name" : "DuckDuckGo — Your protection, our priority.", "type" : "bookmark", "url" : "https:\/\/duckduckgo.com\/" }, diff --git a/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_safari.json b/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_safari.json index 9431af6f7b..f84e20f19e 100644 --- a/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_safari.json +++ b/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_safari.json @@ -138,7 +138,7 @@ "type" : "folder" }, { - "name" : "DuckDuckGo — Privacy, simplified.", + "name" : "DuckDuckGo — Your protection, our priority.", "type" : "bookmark", "url" : "https:\/\/duckduckgo.com\/" }, From c67128d619267ebfdacb1ebb517f1d12b72ee2b7 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Thu, 31 Oct 2024 05:21:28 +0000 Subject: [PATCH 07/22] Bump version to 1.112.0 (293) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 3a2f81093c..9ba329c56c 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 292 +CURRENT_PROJECT_VERSION = 293 From 1afd93e3b13203f5fe368b304e381aa7607aa143 Mon Sep 17 00:00:00 2001 From: Dax Mobile <44842493+daxmobile@users.noreply.github.com> Date: Thu, 31 Oct 2024 21:48:10 +1100 Subject: [PATCH 08/22] Update BSK with autofill 15.1.0 (#3480) Task/Issue URL: https://app.asana.com/0/1208660715919854/1208660715919854 Autofill Release: https://github.com/duckduckgo/duckduckgo-autofill/releases/tag/15.1.0 BSK PR: https://github.com/duckduckgo/BrowserServicesKit/pull/1044 ## Description Updates Autofill to version [15.1.0](https://github.com/duckduckgo/duckduckgo-autofill/releases/tag/15.1.0). --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 8 ++++---- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 6bdadbfda7..63392cec0e 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -14688,7 +14688,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 201.0.0; + version = 201.0.1; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 51a522e596..dac185471b 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" : "e5946eee6af859690cc1cc5e51daef3c8368981b", - "version" : "201.0.0" + "revision" : "884a5eac964eeeb6d38780a6b90feaf5a5b3cfcf", + "version" : "201.0.1" } }, { @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/duckduckgo-autofill.git", "state" : { - "revision" : "945ac09a0189dc6736db617867fde193ea984b20", - "version" : "15.0.0" + "revision" : "c992041d16ec10d790e6204dce9abf9966d1363c", + "version" : "15.1.0" } }, { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index deea3ccec5..edc54a510d 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: "201.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "201.0.1"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), .package(path: "../Freemium"), diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 684c8f86b8..fc4bbe802e 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -32,7 +32,7 @@ let package = Package( .library(name: "VPNAppLauncher", targets: ["VPNAppLauncher"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "201.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "201.0.1"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.3"), .package(path: "../AppLauncher"), .package(path: "../UDSHelper"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 2f7f0c808d..5dc074861d 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: "201.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "201.0.1"), .package(path: "../SwiftUIExtensions") ], targets: [ From 9022def6fe0a44fb54e2ec5dfb7b2b7e76974518 Mon Sep 17 00:00:00 2001 From: Sabrina Tardio <44158575+SabrinaTardio@users.noreply.github.com> Date: Thu, 31 Oct 2024 12:49:26 +0100 Subject: [PATCH 09/22] update UI test (#3469) Task/Issue URL: https://app.asana.com/0/72649045549333/1208077416568735/f **Description**: Updates the UI test --- .../MainWindow/MainWindowController.swift | 4 + UITests/Common/UITests.swift | 2 + UITests/OnboardingUITests.swift | 105 ++++++++---------- 3 files changed, 54 insertions(+), 57 deletions(-) diff --git a/DuckDuckGo/MainWindow/MainWindowController.swift b/DuckDuckGo/MainWindow/MainWindowController.swift index 5911d9c289..a21f293ae6 100644 --- a/DuckDuckGo/MainWindow/MainWindowController.swift +++ b/DuckDuckGo/MainWindow/MainWindowController.swift @@ -74,8 +74,12 @@ final class MainWindowController: NSWindowController { return false #elseif REVIEW if Application.runType == .uiTests { + Application.appDelegate.onboardingStateMachine.state = .onboardingCompleted return false } else { + if Application.runType == .uiTestsOnboarding { + Application.appDelegate.onboardingStateMachine.state = .onboardingCompleted + } let onboardingIsComplete = OnboardingViewModel.isOnboardingFinished || LocalStatisticsStore().waitlistUnlocked return !onboardingIsComplete } diff --git a/UITests/Common/UITests.swift b/UITests/Common/UITests.swift index a92f64ce90..ff590a386e 100644 --- a/UITests/Common/UITests.swift +++ b/UITests/Common/UITests.swift @@ -56,6 +56,7 @@ enum UITests { /// - Parameter requestedToggleState: How the autocomplete checkbox state should be set static func setAutocompleteToggleBeforeTestcaseRuns(_ requestedToggleState: Bool) { let app = XCUIApplication() + app.launchEnvironment["UITEST_MODE"] = "1" app.launch() app.typeKey(",", modifierFlags: [.command]) // Open settings @@ -96,6 +97,7 @@ enum UITests { notificationCenter.typeKey(.escape, modifierFlags: []) } let app = XCUIApplication() + app.launchEnvironment["UITEST_MODE"] = "1" app.launch() app.typeKey("n", modifierFlags: .command) app.typeKey("w", modifierFlags: [.command, .option]) diff --git a/UITests/OnboardingUITests.swift b/UITests/OnboardingUITests.swift index d83b751520..32af9112d0 100644 --- a/UITests/OnboardingUITests.swift +++ b/UITests/OnboardingUITests.swift @@ -20,6 +20,10 @@ import XCTest final class OnboardingUITests: XCTestCase { + override func tearDownWithError() throws { + try resetApplicationData() + } + func testOnboardingToBrowsing() throws { try resetApplicationData() continueAfterFailure = false @@ -35,84 +39,71 @@ final class OnboardingUITests: XCTestCase { XCTAssertFalse(optionsButton.isEnabled) // Get Started - XCTAssertTrue(welcomeWindow.webViews["Welcome"].staticTexts["Tired of being tracked online? We can help!"].waitForExistence(timeout: UITests.Timeouts.elementExistence)) - let getStartedButton = welcomeWindow.webViews["Welcome"].buttons["Get Started"] + XCTAssertTrue(welcomeWindow.webViews["Welcome"].staticTexts["Ready for a faster browser that keeps you protected?"].waitForExistence(timeout: UITests.Timeouts.elementExistence)) + + let getStartedButton = welcomeWindow.webViews["Welcome"].buttons["Let’s Do It!"] XCTAssertTrue(getStartedButton.waitForExistence(timeout: UITests.Timeouts.elementExistence)) getStartedButton.click() + // When it clicks on the button the y it's not alligned + let centerCoordinate = getStartedButton.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.2)) + centerCoordinate.tap() + + // Protections activated + XCTAssertTrue(welcomeWindow.webViews["Welcome"].staticTexts["Protections activated!"].waitForExistence(timeout: UITests.Timeouts.elementExistence)) - // Default Privacy - XCTAssertTrue(welcomeWindow.webViews["Welcome"].staticTexts["Unlike other browsers, DuckDuckGo comes with privacy by default"].waitForExistence(timeout: UITests.Timeouts.elementExistence)) - XCTAssertTrue(welcomeWindow.webViews["Welcome"].staticTexts["Private Search"].waitForExistence(timeout: UITests.Timeouts.elementExistence)) - XCTAssertTrue(welcomeWindow.webViews["Welcome"].staticTexts["Advanced Tracking Protection"].waitForExistence(timeout: UITests.Timeouts.elementExistence)) - XCTAssertTrue(welcomeWindow.webViews["Welcome"].staticTexts["Automatic Cookie Pop-Up Blocking"].waitForExistence(timeout: UITests.Timeouts.elementExistence)) - let gotItButton = welcomeWindow.webViews["Welcome"].buttons["Got It"] - XCTAssertTrue(gotItButton.waitForExistence(timeout: UITests.Timeouts.elementExistence)) - gotItButton.click() - - // Fewer ads and popups - XCTAssertTrue(welcomeWindow.webViews["Welcome"].staticTexts["Private also means fewer ads and pop-ups"].waitForExistence(timeout: UITests.Timeouts.elementExistence)) - XCTAssertTrue(welcomeWindow.webViews["Welcome"].staticTexts["While browsing the web"].waitForExistence(timeout: UITests.Timeouts.elementExistence)) - let seeWithTrackerBlockingButton = welcomeWindow.webViews["Welcome"].buttons["See With Tracker Blocking"] - XCTAssertTrue(seeWithTrackerBlockingButton.waitForExistence(timeout: UITests.Timeouts.elementExistence)) - seeWithTrackerBlockingButton.click() - XCTAssertTrue(gotItButton.waitForExistence(timeout: UITests.Timeouts.elementExistence)) - gotItButton.click() - XCTAssertTrue(welcomeWindow.webViews["Welcome"].staticTexts["While watching YouTube"].waitForExistence(timeout: UITests.Timeouts.elementExistence)) - let seeWithDuckPlayerButton = welcomeWindow.webViews["Welcome"].buttons["See With Duck Player"] - XCTAssertTrue(seeWithDuckPlayerButton.waitForExistence(timeout: UITests.Timeouts.elementExistence)) - seeWithDuckPlayerButton.click() - let nextGotItButton = welcomeWindow.webViews["Welcome"].buttons["Got It"] - XCTAssertTrue(nextGotItButton.waitForExistence(timeout: UITests.Timeouts.elementExistence)) - nextGotItButton.click() - welcomeWindow.webViews["Welcome"].click() - let nextButton = welcomeWindow.webViews["Welcome"].buttons["Next"] - XCTAssertTrue(nextButton.waitForExistence(timeout: UITests.Timeouts.elementExistence)) - nextButton.click() - - // Make Privacy your go-to - XCTAssertTrue(welcomeWindow.webViews["Welcome"].staticTexts["Make privacy your go-to"].waitForExistence(timeout: UITests.Timeouts.elementExistence)) let skipButton = welcomeWindow.webViews["Welcome"].buttons["Skip"] XCTAssertTrue(skipButton.waitForExistence(timeout: UITests.Timeouts.elementExistence)) skipButton.click() - welcomeWindow.webViews["Welcome"].click() - let importButton = welcomeWindow.webViews["Welcome"].buttons["Import"] - XCTAssertTrue(importButton.waitForExistence(timeout: UITests.Timeouts.elementExistence)) - importButton.click() + + // Let’s get you set up + XCTAssertTrue(welcomeWindow.webViews["Welcome"].staticTexts["Let’s get you set up!"].waitForExistence(timeout: UITests.Timeouts.elementExistence)) + + XCTAssertTrue(skipButton.waitForExistence(timeout: UITests.Timeouts.elementExistence)) + skipButton.click() + + let importNowButton = welcomeWindow.webViews["Welcome"].buttons["Import Now"] + XCTAssertTrue(importNowButton.waitForExistence(timeout: UITests.Timeouts.elementExistence)) + importNowButton.click() + let cancelButton = welcomeWindow.sheets.buttons["Cancel"] XCTAssertTrue(cancelButton.waitForExistence(timeout: UITests.Timeouts.elementExistence)) cancelButton.click() + + let nextButtonSetUp = welcomeWindow.webViews["Welcome"].buttons["Next"] + XCTAssertTrue(nextButtonSetUp.waitForExistence(timeout: UITests.Timeouts.elementExistence)) + nextButtonSetUp.click() + + // Duck Player + XCTAssertTrue(welcomeWindow.webViews["Welcome"].staticTexts["Drowning in ads on YouTube? Not with Duck Player!"].waitForExistence(timeout: UITests.Timeouts.elementExistence)) + + let nextButtonDuckPlayer = welcomeWindow.webViews["Welcome"].buttons["Next"] + XCTAssertTrue(nextButtonDuckPlayer.waitForExistence(timeout: UITests.Timeouts.elementExistence)) + nextButtonDuckPlayer.click() + + // Customize Experience + XCTAssertTrue(welcomeWindow.webViews["Welcome"].staticTexts["Let’s customize a few things…"].waitForExistence(timeout: UITests.Timeouts.elementExistence)) + + // Session Restore XCTAssertTrue(skipButton.waitForExistence(timeout: UITests.Timeouts.elementExistence)) skipButton.click() - welcomeWindow.webViews["Welcome"].click() - XCTAssertTrue(nextButton.waitForExistence(timeout: UITests.Timeouts.elementExistence)) - nextButton.click() - - // Customize your experience - XCTAssertTrue(welcomeWindow.webViews["Welcome"].staticTexts["Customize your experience"].waitForExistence(timeout: UITests.Timeouts.elementExistence)) - XCTAssertTrue(welcomeWindow.webViews["Welcome"].staticTexts["Make DuckDuckGo work just the way you want."].waitForExistence(timeout: UITests.Timeouts.elementExistence)) - let showBookmarksBarButton = welcomeWindow.webViews["Welcome"].buttons["Show Bookmarks Bar"] - XCTAssertTrue(showBookmarksBarButton.waitForExistence(timeout: UITests.Timeouts.elementExistence)) - showBookmarksBarButton.click() - XCTAssertTrue(welcomeWindow.collectionViews["BookmarksBarViewController.bookmarksBarCollectionView"].waitForExistence(timeout: UITests.Timeouts.elementExistence)) + let enableSessionRestoreButton = welcomeWindow.webViews["Welcome"].buttons["Enable Session Restore"] XCTAssertTrue(enableSessionRestoreButton.waitForExistence(timeout: UITests.Timeouts.elementExistence)) enableSessionRestoreButton.click() - welcomeWindow.webViews["Welcome"].click() + let showHomeButton = welcomeWindow.webViews["Welcome"].buttons["Show Home Button"] XCTAssertTrue(showHomeButton.waitForExistence(timeout: UITests.Timeouts.elementExistence)) showHomeButton.click() - XCTAssertTrue(welcomeWindow.children(matching: .button).element(boundBy: 3).waitForExistence(timeout: UITests.Timeouts.elementExistence)) - welcomeWindow.webViews["Welcome"].click() - XCTAssertTrue(nextButton.waitForExistence(timeout: UITests.Timeouts.elementExistence)) - nextButton.click() + + // Start Browsing let startBrowsingButton = welcomeWindow.webViews["Welcome"].buttons["Start Browsing"] XCTAssertTrue(startBrowsingButton.waitForExistence(timeout: UITests.Timeouts.elementExistence)) startBrowsingButton.click() - // AfterOnboarding - let duckduckgoPrivacySimplifiedWindow = app.windows["DuckDuckGo — Privacy, simplified."] - XCTAssertTrue(duckduckgoPrivacySimplifiedWindow.webViews["DuckDuckGo — Privacy, simplified."].waitForExistence(timeout: UITests.Timeouts.elementExistence)) - XCTAssertTrue(duckduckgoPrivacySimplifiedWindow.buttons["NavigationBarViewController.optionsButton"].isEnabled) + // After Onboarding + let duckduckgoPage = app.windows["DuckDuckGo — Privacy, simplified."] + XCTAssertTrue(duckduckgoPage.webViews["DuckDuckGo — Privacy, simplified."].waitForExistence(timeout: UITests.Timeouts.elementExistence)) + XCTAssertTrue(duckduckgoPage.buttons["NavigationBarViewController.optionsButton"].isEnabled) } func resetApplicationData() throws { From 5bc18747ef174d1b96f3c7dfff85c5102bfc65ac Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Thu, 31 Oct 2024 16:56:38 +0500 Subject: [PATCH 10/22] Fix crash on empty bookmarks html root element (#3482) Task/Issue URL: https://app.asana.com/0/1202406491309510/1208666105063018/f --- DuckDuckGo.xcodeproj/project.pbxproj | 6 +++++ .../Common/Extensions/XMLNodeExtension.swift | 27 +++++++++++++++++++ .../Bookmarks/HTML/BookmarkHTMLReader.swift | 20 +++++++------- 3 files changed, 43 insertions(+), 10 deletions(-) create mode 100644 DuckDuckGo/Common/Extensions/XMLNodeExtension.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 63392cec0e..8286041afb 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1844,6 +1844,8 @@ 843965132C6F2FFE004C8899 /* NSDragOperationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843965112C6F2FFE004C8899 /* NSDragOperationExtension.swift */; }; 843965152C737022004C8899 /* NSPasteboardExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843965142C737022004C8899 /* NSPasteboardExtension.swift */; }; 843965162C737022004C8899 /* NSPasteboardExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843965142C737022004C8899 /* NSPasteboardExtension.swift */; }; + 843AD3DC2CD389CC00163067 /* XMLNodeExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843AD3DB2CD389C500163067 /* XMLNodeExtension.swift */; }; + 843AD3DD2CD389CC00163067 /* XMLNodeExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843AD3DB2CD389C500163067 /* XMLNodeExtension.swift */; }; 843D73BB2C786E5400E4F9DC /* BookmarkListPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F1C8DA2C774CA900716446 /* BookmarkListPopover.swift */; }; 843D73BC2C786E5400E4F9DC /* BookmarkListPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F1C8DA2C774CA900716446 /* BookmarkListPopover.swift */; }; 844D7DA42C9443EA00BE61D4 /* NSPrintInfoExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844D7DA32C9443E500BE61D4 /* NSPrintInfoExtension.swift */; }; @@ -3999,6 +4001,7 @@ 8426108C2C9811EC0070D5F9 /* KeyEquivalentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyEquivalentView.swift; sourceTree = ""; }; 843965112C6F2FFE004C8899 /* NSDragOperationExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSDragOperationExtension.swift; sourceTree = ""; }; 843965142C737022004C8899 /* NSPasteboardExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSPasteboardExtension.swift; sourceTree = ""; }; + 843AD3DB2CD389C500163067 /* XMLNodeExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XMLNodeExtension.swift; sourceTree = ""; }; 844D7DA32C9443E500BE61D4 /* NSPrintInfoExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSPrintInfoExtension.swift; sourceTree = ""; }; 84537A022C998C24008723BC /* FireWindowSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FireWindowSession.swift; sourceTree = ""; }; 848648A02C76F4B20082282D /* BookmarksBarMenuViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksBarMenuViewController.swift; sourceTree = ""; }; @@ -8509,6 +8512,7 @@ AA6FFB4324DC33320028F4D0 /* NSViewExtension.swift */, AA9E9A5525A3AE8400D1959D /* NSWindowExtension.swift */, B643BF1327ABF772000BACEC /* NSWorkspaceExtension.swift */, + 843AD3DB2CD389C500163067 /* XMLNodeExtension.swift */, B6A9E46A2614618A0067D1B9 /* OperatingSystemVersionExtension.swift */, EEE50C282C38249C003DD7FF /* OptionalExtension.swift */, B637273C26CCF0C200C8CB02 /* OptionalExtension.swift */, @@ -11540,6 +11544,7 @@ C18194602C7CDD0E00381092 /* PromotionViewModel.swift in Sources */, 3706FC31293F65D500E42796 /* PermissionButton.swift in Sources */, 9F6434622BEC82B700D2D8A0 /* AttributionPixelHandler.swift in Sources */, + 843AD3DC2CD389CC00163067 /* XMLNodeExtension.swift in Sources */, 3706FC32293F65D500E42796 /* MoreOptionsMenu.swift in Sources */, 3706FC34293F65D500E42796 /* PermissionAuthorizationViewController.swift in Sources */, 3706FC35293F65D500E42796 /* BookmarkNode.swift in Sources */, @@ -12785,6 +12790,7 @@ 3797C7A02C61806500DA77FB /* HomePageSettingsView.swift in Sources */, B6F41031264D2B23003DA42C /* ProgressExtension.swift in Sources */, 4B723E0F26B0006500E14D75 /* CSVParser.swift in Sources */, + 843AD3DD2CD389CC00163067 /* XMLNodeExtension.swift in Sources */, B63BDF7E27FDAA640072D75B /* PrivacyDashboardWebView.swift in Sources */, 37CD54CF27F2FDD100F1F7B9 /* AppearancePreferences.swift in Sources */, 3199AF7B2C80734A003AEBDC /* DuckPlayerOnboardingLocationValidator.swift in Sources */, diff --git a/DuckDuckGo/Common/Extensions/XMLNodeExtension.swift b/DuckDuckGo/Common/Extensions/XMLNodeExtension.swift new file mode 100644 index 0000000000..86e1829aad --- /dev/null +++ b/DuckDuckGo/Common/Extensions/XMLNodeExtension.swift @@ -0,0 +1,27 @@ +// +// XMLNodeExtension.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. +// + +extension XMLNode { + + func childIfExists(at index: Int) -> XMLNode? { + assert(index >= 0) + guard childCount > index else { return nil } + return child(at: index) + } + +} diff --git a/DuckDuckGo/DataImport/Bookmarks/HTML/BookmarkHTMLReader.swift b/DuckDuckGo/DataImport/Bookmarks/HTML/BookmarkHTMLReader.swift index 5524315669..305ae118f0 100644 --- a/DuckDuckGo/DataImport/Bookmarks/HTML/BookmarkHTMLReader.swift +++ b/DuckDuckGo/DataImport/Bookmarks/HTML/BookmarkHTMLReader.swift @@ -141,9 +141,9 @@ final class BookmarkHTMLReader { private func validateHTMLBookmarksDocument(_ document: XMLDocument) throws -> XMLNode? { let root = document.rootElement() - guard let body = root?.child(at: 1) else { throw ImportError(type: .validationBody, underlyingError: nil) } + guard let body = root?.childIfExists(at: 1) else { throw ImportError(type: .validationBody, underlyingError: nil) } // get /html/body/*[0] - let cursor = body.child(at: 0) + let cursor = body.childIfExists(at: 0) return cursor } @@ -155,12 +155,12 @@ final class BookmarkHTMLReader { switch cursor?.htmlTag { case .dl: let originalCursorValue = cursor - cursor = cursor?.child(at: 0) + cursor = cursor?.childIfExists(at: 0) dlLoop: while cursor != nil { switch cursor?.htmlTag { case .dd: - if cursor?.child(at: 0)?.htmlTag == .h3 { - cursor = cursor?.child(at: 0) + if cursor?.childIfExists(at: 0)?.htmlTag == .h3 { + cursor = cursor?.childIfExists(at: 0) break dlLoop } cursor = cursor?.nextSibling @@ -195,7 +195,7 @@ final class BookmarkHTMLReader { itemType = cursor?.itemType(inSafariFormat: false) switch itemType { case .some: - cursor = cursor?.child(at: 0) + cursor = cursor?.childIfExists(at: 0) case .none: cursor = cursor?.nextSibling } @@ -243,12 +243,12 @@ final class BookmarkHTMLReader { private func readFolderContents(_ node: XMLNode?) throws -> [ImportedBookmarks.BookmarkOrFolder] { var cursor = node - cursor = cursor?.child(at: 0) + cursor = cursor?.childIfExists(at: 0) var children = [ImportedBookmarks.BookmarkOrFolder]() while cursor != nil { - let firstChild = cursor?.child(at: 0) + let firstChild = cursor?.childIfExists(at: 0) switch (cursor?.htmlTag, firstChild?.htmlTag) { case (.dd, .h3): children.append(try readFolder(firstChild)) @@ -354,13 +354,13 @@ private extension XMLNode { return .folder case .dt: return .bookmark - case .dl where child(at: 0)?.child(at: 0)?.htmlTag == .a: + case .dl where childIfExists(at: 0)?.childIfExists(at: 0)?.htmlTag == .a: return .safariTopLevelBookmarks default: return nil } } else { - switch (htmlTag, child(at: 0)?.htmlTag) { + switch (htmlTag, childIfExists(at: 0)?.htmlTag) { case (.dd, .h3): return .folder case (.dt, .a): From 49ff187c436eb99a8238457d66c1090276bf5166 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 31 Oct 2024 14:58:24 +0100 Subject: [PATCH 11/22] Fix Sync E2E tests (#3486) Task/Issue URL: https://app.asana.com/0/1201037661562251/1208665937532326/f Description: Revert test bookmark title to "Privacy, simplified". --- SyncE2EUITests/CriticalPathsTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SyncE2EUITests/CriticalPathsTests.swift b/SyncE2EUITests/CriticalPathsTests.swift index 32562cbd1b..51c2d5c557 100644 --- a/SyncE2EUITests/CriticalPathsTests.swift +++ b/SyncE2EUITests/CriticalPathsTests.swift @@ -374,7 +374,7 @@ final class CriticalPathsTests: XCTestCase { } let duckduckgoBookmark = bookmarksWindow.staticTexts["www.duckduckgo.com"] let stackOverflow = bookmarksWindow.staticTexts["Stack Overflow - Where Developers Learn, Share, & Build Careers"] - let privacySimplified = bookmarksWindow.staticTexts["DuckDuckGo — Your protection, our priority."] + let privacySimplified = bookmarksWindow.staticTexts["DuckDuckGo — Privacy, simplified."] let wolfram = bookmarksWindow.staticTexts["Wolfram|Alpha: Computational Intelligence"] let news = bookmarksWindow.staticTexts["news"] let codes = bookmarksWindow.staticTexts["code"] From b16b54c8fd9b6785ce48349cc11b90b50038056e Mon Sep 17 00:00:00 2001 From: Pete Smith <5278441+aataraxiaa@users.noreply.github.com> Date: Thu, 31 Oct 2024 15:29:37 +0000 Subject: [PATCH 12/22] Freemium PIR: Manual Removal Links (#3466) Task/Issue URL: https://app.asana.com/0/72649045549333/1208619435900636/f FE PR: https://dub.duckduckgo.com/duckduckgo/static-pages/pull/1012 **Description**: Adds a manual removal link to each Freemium PIR result record view --- .../DataBrokerProtectionDataManager.swift | 4 +- .../Model/DBPUICommunicationModel.swift | 20 +-- .../Model/DataBroker.swift | 8 +- .../Storage/Mappers.swift | 3 +- .../UI/DBPUICommunicationLayer.swift | 2 +- .../DataBrokerProtection/UI/UIMapper.swift | 21 ++-- .../DBPUICommunicationModelTests.swift | 81 ++++++++++-- ...kerProfileQueryOperationManagerTests.swift | 25 ++-- .../DataBrokerProtectionUpdaterTests.swift | 6 +- .../MapperToModelTests.swift | 118 ++++++++++++++++++ .../MapperToUITests.swift | 67 ++++++++++ .../MismatchCalculatorUseCaseTests.swift | 6 +- .../DataBrokerProtectionTests/Mocks.swift | 11 +- .../OperationPreferredDateUpdaterTests.swift | 3 +- 14 files changed, 329 insertions(+), 46 deletions(-) create mode 100644 LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MapperToModelTests.swift diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift index ffb525774a..f6ad3673cb 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift @@ -409,10 +409,10 @@ extension InMemoryDataCache: DBPUICommunicationDelegate { // 2. We map the brokers to the UI model .flatMap { dataBroker -> [DBPUIDataBroker] in var result: [DBPUIDataBroker] = [] - result.append(DBPUIDataBroker(name: dataBroker.name, url: dataBroker.url, parentURL: dataBroker.parent)) + result.append(DBPUIDataBroker(name: dataBroker.name, url: dataBroker.url, parentURL: dataBroker.parent, optOutUrl: dataBroker.optOutUrl)) for mirrorSite in dataBroker.mirrorSites { - result.append(DBPUIDataBroker(name: mirrorSite.name, url: mirrorSite.url, parentURL: dataBroker.parent)) + result.append(DBPUIDataBroker(name: mirrorSite.name, url: mirrorSite.url, parentURL: dataBroker.parent, optOutUrl: dataBroker.optOutUrl)) } return result } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift index 24fe6b6a1a..68e5f19d0e 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift @@ -127,12 +127,14 @@ struct DBPUIDataBroker: Codable, Hashable { let url: String let date: Double? let parentURL: String? + let optOutUrl: String - init(name: String, url: String, date: Double? = nil, parentURL: String?) { + init(name: String, url: String, date: Double? = nil, parentURL: String?, optOutUrl: String) { self.name = name self.url = url self.date = date self.parentURL = parentURL + self.optOutUrl = optOutUrl } func hash(into hasher: inout Hasher) { @@ -170,7 +172,8 @@ extension DBPUIDataBrokerProfileMatch { dataBrokerName: String, dataBrokerURL: String, dataBrokerParentURL: String?, - parentBrokerOptOutJobData: [OptOutJobData]?) { + parentBrokerOptOutJobData: [OptOutJobData]?, + optOutUrl: String) { let extractedProfile = optOutJobData.extractedProfile /* @@ -205,7 +208,7 @@ extension DBPUIDataBrokerProfileMatch { extractedProfile.doesMatchExtractedProfile(parentOptOut.extractedProfile) } ?? false - self.init(dataBroker: DBPUIDataBroker(name: dataBrokerName, url: dataBrokerURL, parentURL: dataBrokerParentURL), + self.init(dataBroker: DBPUIDataBroker(name: dataBrokerName, url: dataBrokerURL, parentURL: dataBrokerParentURL, optOutUrl: optOutUrl), name: extractedProfile.fullName ?? "No name", addresses: extractedProfile.addresses?.map {DBPUIUserProfileAddress(addressCityState: $0) } ?? [], alternativeNames: extractedProfile.alternativeNames ?? [String](), @@ -217,12 +220,13 @@ extension DBPUIDataBrokerProfileMatch { hasMatchingRecordOnParentBroker: hasFoundParentMatch) } - init(optOutJobData: OptOutJobData, dataBroker: DataBroker, parentBrokerOptOutJobData: [OptOutJobData]?) { + init(optOutJobData: OptOutJobData, dataBroker: DataBroker, parentBrokerOptOutJobData: [OptOutJobData]?, optOutUrl: String) { self.init(optOutJobData: optOutJobData, dataBrokerName: dataBroker.name, dataBrokerURL: dataBroker.url, dataBrokerParentURL: dataBroker.parent, - parentBrokerOptOutJobData: parentBrokerOptOutJobData) + parentBrokerOptOutJobData: parentBrokerOptOutJobData, + optOutUrl: optOutUrl) } /// Generates an array of `DBPUIDataBrokerProfileMatch` objects from the provided query data. @@ -253,7 +257,8 @@ extension DBPUIDataBrokerProfileMatch { // Create a profile match for the current data broker and append it to the list of profiles. profiles.append(DBPUIDataBrokerProfileMatch(optOutJobData: optOutJobData, dataBroker: dataBroker, - parentBrokerOptOutJobData: parentBrokerOptOutJobData)) + parentBrokerOptOutJobData: parentBrokerOptOutJobData, + optOutUrl: dataBroker.optOutUrl)) // Handle mirror sites associated with the data broker. if !dataBroker.mirrorSites.isEmpty { @@ -264,7 +269,8 @@ extension DBPUIDataBrokerProfileMatch { dataBrokerName: mirrorSite.name, dataBrokerURL: mirrorSite.url, dataBrokerParentURL: dataBroker.parent, - parentBrokerOptOutJobData: parentBrokerOptOutJobData) + parentBrokerOptOutJobData: parentBrokerOptOutJobData, + optOutUrl: dataBroker.optOutUrl) } return nil } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DataBroker.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DataBroker.swift index 9014bcb185..5328a699f6 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DataBroker.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DataBroker.swift @@ -115,6 +115,7 @@ public struct DataBroker: Codable, Sendable { let schedulingConfig: DataBrokerScheduleConfig let parent: String? let mirrorSites: [MirrorSite] + let optOutUrl: String var isFakeBroker: Bool { name.contains("fake") // A future improvement will be to add a property in the JSON file. @@ -128,6 +129,7 @@ public struct DataBroker: Codable, Sendable { case schedulingConfig case parent case mirrorSites + case optOutUrl } init(id: Int64? = nil, @@ -137,7 +139,8 @@ public struct DataBroker: Codable, Sendable { version: String, schedulingConfig: DataBrokerScheduleConfig, parent: String? = nil, - mirrorSites: [MirrorSite] = [MirrorSite]() + mirrorSites: [MirrorSite] = [MirrorSite](), + optOutUrl: String ) { self.id = id self.name = name @@ -153,6 +156,7 @@ public struct DataBroker: Codable, Sendable { self.schedulingConfig = schedulingConfig self.parent = parent self.mirrorSites = mirrorSites + self.optOutUrl = optOutUrl } public init(from decoder: Decoder) throws { @@ -179,6 +183,8 @@ public struct DataBroker: Codable, Sendable { mirrorSites = [MirrorSite]() } + optOutUrl = (try? container.decode(String.self, forKey: .optOutUrl)) ?? "" + id = nil } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/Mappers.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/Mappers.swift index fc221f3872..ab3be3f110 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/Mappers.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/Mappers.swift @@ -176,7 +176,8 @@ struct MapperToModel { version: decodedBroker.version, schedulingConfig: decodedBroker.schedulingConfig, parent: decodedBroker.parent, - mirrorSites: decodedBroker.mirrorSites + mirrorSites: decodedBroker.mirrorSites, + optOutUrl: decodedBroker.optOutUrl ) } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DBPUICommunicationLayer.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DBPUICommunicationLayer.swift index 16f78bfe45..5992962044 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DBPUICommunicationLayer.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DBPUICommunicationLayer.swift @@ -79,7 +79,7 @@ struct DBPUICommunicationLayer: Subfeature { weak var delegate: DBPUICommunicationDelegate? private enum Constants { - static let version = 7 + static let version = 8 } internal init(webURLSettings: DataBrokerProtectionWebUIURLSettingsRepresentable, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift index 4636f3b58d..36efa17943 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift @@ -73,7 +73,8 @@ struct MapperToUI { let profileMatch = DBPUIDataBrokerProfileMatch(optOutJobData: optOutJob, dataBroker: dataBroker, - parentBrokerOptOutJobData: parentBrokerOptOutJobData) + parentBrokerOptOutJobData: parentBrokerOptOutJobData, + optOutUrl: dataBroker.optOutUrl) if extractedProfile.removedDate == nil { inProgressOptOuts.append(profileMatch) @@ -87,7 +88,8 @@ struct MapperToUI { dataBrokerName: mirrorSite.name, dataBrokerURL: mirrorSite.url, dataBrokerParentURL: dataBroker.parent, - parentBrokerOptOutJobData: parentBrokerOptOutJobData) + parentBrokerOptOutJobData: parentBrokerOptOutJobData, + optOutUrl: dataBroker.optOutUrl) if let extractedProfileRemovedDate = extractedProfile.removedDate, mirrorSite.shouldWeIncludeMirrorSite(for: extractedProfileRemovedDate) { @@ -135,13 +137,15 @@ struct MapperToUI { brokers.append(DBPUIDataBroker(name: $0.dataBroker.name, url: $0.dataBroker.url, date: $0.scanJobData.lastRunDate!.timeIntervalSince1970, - parentURL: $0.dataBroker.parent)) + parentURL: $0.dataBroker.parent, + optOutUrl: $0.dataBroker.optOutUrl)) for mirrorSite in $0.dataBroker.mirrorSites where mirrorSite.addedAt < $0.scanJobData.lastRunDate! { brokers.append(DBPUIDataBroker(name: mirrorSite.name, url: mirrorSite.url, date: $0.scanJobData.lastRunDate!.timeIntervalSince1970, - parentURL: $0.dataBroker.parent)) + parentURL: $0.dataBroker.parent, + optOutUrl: $0.dataBroker.optOutUrl)) } return brokers @@ -171,7 +175,8 @@ struct MapperToUI { brokers.append(DBPUIDataBroker(name: $0.dataBroker.name, url: $0.dataBroker.url, date: $0.scanJobData.preferredRunDate!.timeIntervalSince1970, - parentURL: $0.dataBroker.parent)) + parentURL: $0.dataBroker.parent, + optOutUrl: $0.dataBroker.optOutUrl)) for mirrorSite in $0.dataBroker.mirrorSites { if let removedDate = mirrorSite.removedAt { @@ -179,13 +184,15 @@ struct MapperToUI { brokers.append(DBPUIDataBroker(name: mirrorSite.name, url: mirrorSite.url, date: $0.scanJobData.preferredRunDate!.timeIntervalSince1970, - parentURL: $0.dataBroker.parent)) + parentURL: $0.dataBroker.parent, + optOutUrl: $0.dataBroker.optOutUrl)) } } else { brokers.append(DBPUIDataBroker(name: mirrorSite.name, url: mirrorSite.url, date: $0.scanJobData.preferredRunDate!.timeIntervalSince1970, - parentURL: $0.dataBroker.parent)) + parentURL: $0.dataBroker.parent, + optOutUrl: $0.dataBroker.optOutUrl)) } } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DBPUICommunicationModelTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DBPUICommunicationModelTests.swift index 68b71f4fdd..aff311ce82 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DBPUICommunicationModelTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DBPUICommunicationModelTests.swift @@ -46,7 +46,8 @@ final class DBPUICommunicationModelTests: XCTestCase { dataBrokerName: "doesn't matter for the test", dataBrokerURL: "see above", dataBrokerParentURL: "whatever", - parentBrokerOptOutJobData: nil) + parentBrokerOptOutJobData: nil, + optOutUrl: "broker.com") // Then XCTAssertEqual(profileMatch.foundDate, createdDate.timeIntervalSince1970) @@ -77,7 +78,8 @@ final class DBPUICommunicationModelTests: XCTestCase { dataBrokerName: "doesn't matter for the test", dataBrokerURL: "see above", dataBrokerParentURL: "whatever", - parentBrokerOptOutJobData: nil) + parentBrokerOptOutJobData: nil, + optOutUrl: "broker.com") // Then XCTAssertEqual(profileMatch.foundDate, foundEventDate.timeIntervalSince1970) @@ -116,7 +118,8 @@ final class DBPUICommunicationModelTests: XCTestCase { dataBrokerName: "doesn't matter for the test", dataBrokerURL: "see above", dataBrokerParentURL: "whatever", - parentBrokerOptOutJobData: nil) + parentBrokerOptOutJobData: nil, + optOutUrl: "broker.com") // Then XCTAssertEqual(profileMatch.foundDate, foundEventDate2.timeIntervalSince1970) @@ -147,7 +150,8 @@ final class DBPUICommunicationModelTests: XCTestCase { dataBrokerName: "doesn't matter for the test", dataBrokerURL: "see above", dataBrokerParentURL: "whatever", - parentBrokerOptOutJobData: [parentOptOut]) + parentBrokerOptOutJobData: [parentOptOut], + optOutUrl: "broker.com") // Then XCTAssertTrue(profileMatch.hasMatchingRecordOnParentBroker) @@ -177,7 +181,8 @@ final class DBPUICommunicationModelTests: XCTestCase { dataBrokerParentURL: "whatever", parentBrokerOptOutJobData: [parentOptOutNonmatching1, parentOptOutMatching, - parentOptOutNonmatching2]) + parentOptOutNonmatching2], + optOutUrl: "broker.com") // Then XCTAssertTrue(profileMatch.hasMatchingRecordOnParentBroker) @@ -203,7 +208,8 @@ final class DBPUICommunicationModelTests: XCTestCase { dataBrokerURL: "see above", dataBrokerParentURL: "whatever", parentBrokerOptOutJobData: [parentOptOutNonmatching1, - parentOptOutNonmatching2]) + parentOptOutNonmatching2], + optOutUrl: "broker.com") // Then XCTAssertFalse(profileMatch.hasMatchingRecordOnParentBroker) @@ -225,9 +231,70 @@ final class DBPUICommunicationModelTests: XCTestCase { dataBrokerName: "doesn't matter for the test", dataBrokerURL: "see above", dataBrokerParentURL: "whatever", - parentBrokerOptOutJobData: [parentOptOut]) + parentBrokerOptOutJobData: [parentOptOut], + optOutUrl: "broker.com") // Then XCTAssertTrue(profileMatch.hasMatchingRecordOnParentBroker) } + + // MARK: - `profileMatches` Broker OptOut URL & Name tests + + func testProfileMatches_optOutUrlAndBrokerNameForChildBroker() { + // Given + let extractedProfile = ExtractedProfile(id: 1, name: "Sample Name", profileUrl: "profile.com") + + let childBroker = BrokerProfileQueryData.mock( + dataBrokerName: "ChildBroker", + url: "child.com", + parentURL: "parent.com", + optOutUrl: "child.com/optout", + extractedProfile: extractedProfile + ) + + let parentBroker = BrokerProfileQueryData.mock( + dataBrokerName: "ParentBroker", + url: "parent.com", + optOutUrl: "parent.com/optout", + extractedProfile: extractedProfile + ) + + // When + let results = DBPUIDataBrokerProfileMatch.profileMatches(from: [childBroker, parentBroker]) + + // Then + XCTAssertEqual(results.count, 2) + + let childProfile = results.first { $0.dataBroker.name == "ChildBroker" } + XCTAssertEqual(childProfile?.dataBroker.optOutUrl, "child.com/optout") + } + + func testProfileMatches_optOutUrlAndBrokerNameForParentBroker() { + // Given + let extractedProfile = ExtractedProfile(id: 1, name: "Sample Name", profileUrl: "profile.com") + + let childBroker = BrokerProfileQueryData.mock( + dataBrokerName: "ChildBroker", + url: "child.com", + parentURL: "parent.com", + optOutUrl: "parent.com/optout", + extractedProfile: extractedProfile + ) + + let parentBroker = BrokerProfileQueryData.mock( + dataBrokerName: "ParentBroker", + url: "parent.com", + optOutUrl: "parent.com/optout", + extractedProfile: extractedProfile + ) + + // When + let results = DBPUIDataBrokerProfileMatch.profileMatches(from: [childBroker, parentBroker]) + + // Then + XCTAssertEqual(results.count, 2) + + let childProfile = results.first { $0.dataBroker.name == "ChildBroker" } + XCTAssertEqual(childProfile?.dataBroker.optOutUrl, "parent.com/optout") + } } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift index b41f802d47..f33b340479 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift @@ -44,7 +44,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let extractedProfileId: Int64 = 1 let currentPreferredRunDate = Date() - let mockDataBroker = DataBroker(name: "databroker", url: "databroker.com", steps: [Step](), version: "1.0", schedulingConfig: config) + let mockDataBroker = DataBroker(name: "databroker", url: "databroker.com", steps: [Step](), version: "1.0", schedulingConfig: config, optOutUrl: "") let mockProfileQuery = ProfileQuery(id: profileQueryId, firstName: "a", lastName: "b", city: "c", state: "d", birthYear: 1222) let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] @@ -92,7 +92,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let extractedProfileId: Int64 = 1 let currentPreferredRunDate = Date() - let mockDataBroker = DataBroker(name: "databroker", url: "databroker.com", steps: [Step](), version: "1.0", schedulingConfig: config) + let mockDataBroker = DataBroker(name: "databroker", url: "databroker.com", steps: [Step](), version: "1.0", schedulingConfig: config, optOutUrl: "") let mockProfileQuery = ProfileQuery(id: profileQueryId, firstName: "a", lastName: "b", city: "c", state: "d", birthYear: 1222) let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] @@ -143,7 +143,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let extractedProfileId: Int64 = 1 let currentPreferredRunDate = Date() - let mockDataBroker = DataBroker(name: "databroker", url: "databroker.com", steps: [Step](), version: "1.0", schedulingConfig: config) + let mockDataBroker = DataBroker(name: "databroker", url: "databroker.com", steps: [Step](), version: "1.0", schedulingConfig: config, optOutUrl: "") let mockProfileQuery = ProfileQuery(id: profileQueryId, firstName: "a", lastName: "b", city: "c", state: "d", birthYear: 1222) let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] @@ -888,7 +888,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let extractedProfileId: Int64 = 1 let currentPreferredRunDate = Date() - let mockDataBroker = DataBroker(name: "databroker", url: "databroker.com", steps: [Step](), version: "1.0", schedulingConfig: config) + let mockDataBroker = DataBroker(name: "databroker", url: "databroker.com", steps: [Step](), version: "1.0", schedulingConfig: config, optOutUrl: "") let mockProfileQuery = ProfileQuery(id: profileQueryId, firstName: "a", lastName: "b", city: "c", state: "d", birthYear: 1222) let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] @@ -913,7 +913,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let currentPreferredRunDate = Date() let expectedPreferredRunDate = Date().addingTimeInterval(config.confirmOptOutScan.hoursToSeconds) - let mockDataBroker = DataBroker(name: "databroker", url: "databroker.com", steps: [Step](), version: "1.0", schedulingConfig: config) + let mockDataBroker = DataBroker(name: "databroker", url: "databroker.com", steps: [Step](), version: "1.0", schedulingConfig: config, optOutUrl: "") let mockProfileQuery = ProfileQuery(id: profileQueryId, firstName: "a", lastName: "b", city: "c", state: "d", birthYear: 1222) let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] @@ -987,7 +987,8 @@ extension DataBroker { retryError: 0, confirmOptOutScan: 0, maintenanceScan: 0 - ) + ), + optOutUrl: "" ) } @@ -1006,7 +1007,8 @@ extension DataBroker { confirmOptOutScan: 0, maintenanceScan: 0 ), - parent: "some" + parent: "some", + optOutUrl: "" ) } @@ -1020,7 +1022,8 @@ extension DataBroker { retryError: 0, confirmOptOutScan: 0, maintenanceScan: 0 - ) + ), + optOutUrl: "" ) } @@ -1033,7 +1036,8 @@ extension DataBroker { retryError: 0, confirmOptOutScan: 0, maintenanceScan: 0 - ) + ), + optOutUrl: "" ) } @@ -1052,7 +1056,8 @@ extension DataBroker { confirmOptOutScan: 0, maintenanceScan: 0 ), - mirrorSites: mirroSites + mirrorSites: mirroSites, + optOutUrl: "" ) } } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionUpdaterTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionUpdaterTests.swift index 4f70f8934a..93145f1be4 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionUpdaterTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionUpdaterTests.swift @@ -111,7 +111,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { if let vault = self.vault { let sut = DefaultDataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) repository.lastCheckedVersion = nil - resources.brokersList = [.init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.1", schedulingConfig: .mock)] + resources.brokersList = [.init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.1", schedulingConfig: .mock, optOutUrl: "")] vault.shouldReturnOldVersionBroker = true sut.checkForUpdatesInBrokerJSONFiles() @@ -129,7 +129,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { if let vault = self.vault { let sut = DefaultDataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) repository.lastCheckedVersion = nil - resources.brokersList = [.init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.1", schedulingConfig: .mock)] + resources.brokersList = [.init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.1", schedulingConfig: .mock, optOutUrl: "")] vault.shouldReturnNewVersionBroker = true sut.checkForUpdatesInBrokerJSONFiles() @@ -146,7 +146,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { if let vault = self.vault { let sut = DefaultDataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) repository.lastCheckedVersion = nil - resources.brokersList = [.init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.0", schedulingConfig: .mock)] + resources.brokersList = [.init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.0", schedulingConfig: .mock, optOutUrl: "")] vault.profileQueries = [.mock] sut.checkForUpdatesInBrokerJSONFiles() diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MapperToModelTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MapperToModelTests.swift new file mode 100644 index 0000000000..c3a70fa5de --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MapperToModelTests.swift @@ -0,0 +1,118 @@ +// +// MapperToModelTests.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 +@testable import DataBrokerProtection + +final class MapperToModelTests: XCTestCase { + + private var sut = MapperToModel(mechanism: {_ in Data()}) + private var jsonDecoder: JSONDecoder! + private var jsonEncoder: JSONEncoder! + + override func setUpWithError() throws { + jsonDecoder = JSONDecoder() + jsonEncoder = JSONEncoder() + } + + func testMapToModel_validData() throws { + // Given + let brokerData = DataBroker( + id: 1, + name: "TestBroker", + url: "https://example.com", + steps: [], + version: "1.0", + schedulingConfig: DataBrokerScheduleConfig(retryError: 1, confirmOptOutScan: 2, maintenanceScan: 3), + parent: "ParentBroker", + mirrorSites: [], + optOutUrl: "https://example.com/opt-out" + ) + let jsonData = try jsonEncoder.encode(brokerData) + let brokerDB = BrokerDB(id: 1, name: "TestBroker", json: jsonData, version: "1.0", url: "https://example.com") + + // When + let result = try sut.mapToModel(brokerDB) + + // Then + XCTAssertEqual(result.id, brokerDB.id) + XCTAssertEqual(result.name, brokerDB.name) + XCTAssertEqual(result.url, brokerData.url) + XCTAssertEqual(result.version, brokerData.version) + XCTAssertEqual(result.steps.count, brokerData.steps.count) + XCTAssertEqual(result.parent, brokerData.parent) + XCTAssertEqual(result.mirrorSites.count, brokerData.mirrorSites.count) + XCTAssertEqual(result.optOutUrl, brokerData.optOutUrl) + } + + func testMapToModel_missingOptionalFields() throws { + // Given + let brokerData = """ + { + "name": "TestBroker", + "url": "https://example.com", + "steps": [], + "version": "1.0", + "schedulingConfig": {"retryError": 1, "confirmOptOutScan": 2, "maintenanceScan": 3} + } + """.data(using: .utf8)! + let brokerDB = BrokerDB(id: 1, name: "TestBroker", json: brokerData, version: "1.0", url: "https://example.com") + + // When + let result = try sut.mapToModel(brokerDB) + + // Then + XCTAssertNil(result.parent) + XCTAssertEqual(result.mirrorSites.count, 0) + XCTAssertEqual(result.optOutUrl, "") + } + + func testMapToModel_invalidJSONStructure() throws { + // Given + let invalidJsonData = """ + { + "invalidKey": "value" + } + """.data(using: .utf8)! + let brokerDB = BrokerDB(id: 1, name: "InvalidBroker", json: invalidJsonData, version: "1.0", url: "https://example.com") + + // When & Then + XCTAssertThrowsError(try sut.mapToModel(brokerDB)) { error in + XCTAssertTrue(error is DecodingError) + } + } + + func testMapToModel_missingUrlFallbackToName() throws { + // Given + let brokerData = """ + { + "name": "TestBroker", + "steps": [], + "version": "1.0", + "schedulingConfig": {"retryError": 1, "confirmOptOutScan": 2, "maintenanceScan": 3} + } + """.data(using: .utf8)! + let brokerDB = BrokerDB(id: 1, name: "TestBroker", json: brokerData, version: "1.0", url: "") + + // When + let result = try sut.mapToModel(brokerDB) + + // Then + XCTAssertEqual(result.url, brokerDB.name) + } +} diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MapperToUITests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MapperToUITests.swift index a47d13d87a..edff8837e8 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MapperToUITests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MapperToUITests.swift @@ -394,6 +394,73 @@ final class MapperToUITests: XCTestCase { XCTAssertEqual(result.scanProgress.scannedBrokers, expected) } + // MARK: - `maintenanceScanState` Broker OptOut URL & Name tests + + func testMaintenanceScanState_childBrokerWithOwnOptOutUrl() { + // Given + let extractedProfile = ExtractedProfile(id: 2, name: "Another Sample", profileUrl: "anotherprofile.com", removedDate: nil) + + let childBroker = BrokerProfileQueryData.mock( + dataBrokerName: "ChildBrokerWithOwnOptOut", + url: "child.com", + parentURL: "parent.com", + optOutUrl: "child.com/optout", + extractedProfile: extractedProfile + ) + + let parentBroker = BrokerProfileQueryData.mock( + dataBrokerName: "ParentBroker", + url: "parent.com", + optOutUrl: "parent.com/optout", + extractedProfile: extractedProfile + ) + + // When + let state = sut.maintenanceScanState([childBroker, parentBroker]) + + // Then + XCTAssertEqual(state.inProgressOptOuts.count, 2) + XCTAssertEqual(state.completedOptOuts.count, 0) + + let childProfile = state.inProgressOptOuts.first { $0.dataBroker.name == "ChildBrokerWithOwnOptOut" } + XCTAssertEqual(childProfile?.dataBroker.optOutUrl, "child.com/optout") + + let parentProfile = state.inProgressOptOuts.first { $0.dataBroker.name == "ParentBroker" } + XCTAssertEqual(parentProfile?.dataBroker.optOutUrl, "parent.com/optout") + } + + func testMaintenanceScanState_childBrokerWithParentOptOutUrl() { + // Given + let extractedProfile = ExtractedProfile(id: 1, name: "Sample Name", profileUrl: "profile.com", removedDate: nil) + + let childBroker = BrokerProfileQueryData.mock( + dataBrokerName: "ChildBroker", + url: "child.com", + parentURL: "parent.com", + optOutUrl: "parent.com/optout", + extractedProfile: extractedProfile + ) + + let parentBroker = BrokerProfileQueryData.mock( + dataBrokerName: "ParentBroker", + url: "parent.com", + optOutUrl: "parent.com/optout", + extractedProfile: extractedProfile + ) + + // When + let state = sut.maintenanceScanState([childBroker, parentBroker]) + + // Then + XCTAssertEqual(state.inProgressOptOuts.count, 2) + XCTAssertEqual(state.completedOptOuts.count, 0) + + let childProfile = state.inProgressOptOuts.first { $0.dataBroker.name == "ChildBroker" } + XCTAssertEqual(childProfile?.dataBroker.optOutUrl, "parent.com/optout") + + let parentProfile = state.inProgressOptOuts.first { $0.dataBroker.name == "ParentBroker" } + XCTAssertEqual(childProfile?.dataBroker.optOutUrl, "parent.com/optout") + } } extension DBPUIScanProgress.ScannedBroker { diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift index fb2af7862b..06ef01eae4 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift @@ -155,7 +155,8 @@ extension BrokerProfileQueryData { url: "parent.com", steps: [Step](), version: "1.0.0", - schedulingConfig: DataBrokerScheduleConfig.mock + schedulingConfig: DataBrokerScheduleConfig.mock, + optOutUrl: "" ), profileQuery: ProfileQuery(firstName: "John", lastName: "Doe", city: "Miami", state: "FL", birthYear: 50), scanJobData: ScanJobData(brokerId: 1, profileQueryId: 1, historyEvents: historyEvents) @@ -170,7 +171,8 @@ extension BrokerProfileQueryData { steps: [Step](), version: "1.0.0", schedulingConfig: DataBrokerScheduleConfig.mock, - parent: "parent.com" + parent: "parent.com", + optOutUrl: "" ), profileQuery: ProfileQuery(firstName: "John", lastName: "Doe", city: "Miami", state: "FL", birthYear: 50), scanJobData: ScanJobData(brokerId: 2, profileQueryId: 1, historyEvents: historyEvents) diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift index 3647654770..e9c98d5773 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift @@ -33,6 +33,7 @@ extension BrokerProfileQueryData { dataBrokerName: String = "test", url: String = "test.com", parentURL: String? = nil, + optOutUrl: String? = nil, lastRunDate: Date? = nil, preferredRunDate: Date? = nil, extractedProfile: ExtractedProfile? = nil, @@ -48,7 +49,8 @@ extension BrokerProfileQueryData { version: "1.0.0", schedulingConfig: DataBrokerScheduleConfig.mock, parent: parentURL, - mirrorSites: mirrorSites + mirrorSites: mirrorSites, + optOutUrl: optOutUrl ?? "" ), profileQuery: ProfileQuery(firstName: "John", lastName: "Doe", city: "Miami", state: "FL", birthYear: 50, deprecated: deprecated), scanJobData: ScanJobData(brokerId: 1, @@ -623,9 +625,9 @@ final class DataBrokerProtectionSecureVaultMock: DataBrokerProtectionSecureVault func fetchBroker(with name: String) throws -> DataBroker? { if shouldReturnOldVersionBroker { - return .init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.0", schedulingConfig: .mock) + return .init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.0", schedulingConfig: .mock, optOutUrl: "") } else if shouldReturnNewVersionBroker { - return .init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.1", schedulingConfig: .mock) + return .init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.1", schedulingConfig: .mock, optOutUrl: "") } return nil @@ -1231,7 +1233,8 @@ extension DataBroker { retryError: 0, confirmOptOutScan: 0, maintenanceScan: 0 - ) + ), + optOutUrl: "" ) } } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateUpdaterTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateUpdaterTests.swift index e7fdb4cb45..76d2e4bb65 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateUpdaterTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateUpdaterTests.swift @@ -42,7 +42,8 @@ final class OperationPreferredDateUpdaterTests: XCTestCase { retryError: 1, confirmOptOutScan: confirmOptOutScanHours, maintenanceScan: 1 - ) + ), + optOutUrl: "" ) databaseMock.childBrokers = [childBroker] From 3acab5b2fce862ae055349b556203047e2a4b5eb Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Thu, 31 Oct 2024 18:04:27 +0100 Subject: [PATCH 13/22] Add to Dock - Update BSK version (#3479) Task/Issue URL: https://app.asana.com/0/1206329551987282/1208577512136709 **Description**: Add the Add to Dock Promo view to the final dialog of the onboarding flow on iOS. --- 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 8286041afb..567547296a 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -14694,7 +14694,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 201.0.1; + version = 202.0.0; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index dac185471b..f9bc000234 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" : "884a5eac964eeeb6d38780a6b90feaf5a5b3cfcf", - "version" : "201.0.1" + "revision" : "de77673bd4fa7b8012c8d1f16cbc73b064539a57", + "version" : "202.0.0" } }, { @@ -75,7 +75,7 @@ { "identity" : "lottie-spm", "kind" : "remoteSourceControl", - "location" : "https://github.com/airbnb/lottie-spm", + "location" : "https://github.com/airbnb/lottie-spm.git", "state" : { "revision" : "1d29eccc24cc8b75bff9f6804155112c0ffc9605", "version" : "4.4.3" diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index edc54a510d..cf68b2bc23 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: "201.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "202.0.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), .package(path: "../Freemium"), diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index fc4bbe802e..05b4d8247e 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -32,7 +32,7 @@ let package = Package( .library(name: "VPNAppLauncher", targets: ["VPNAppLauncher"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "201.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "202.0.0"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.3"), .package(path: "../AppLauncher"), .package(path: "../UDSHelper"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 5dc074861d..f172f69593 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: "201.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "202.0.0"), .package(path: "../SwiftUIExtensions") ], targets: [ From 6204ff2138391e610ea38883e4ba4334399a027e Mon Sep 17 00:00:00 2001 From: Anh Do <18567+quanganhdo@users.noreply.github.com> Date: Thu, 31 Oct 2024 15:00:18 -0400 Subject: [PATCH 14/22] Refactor automatic update flow to use custom Sparkle user driver (#3274) Task/Issue URL: https://app.asana.com/0/1201037661562251/1208183525704010/f Tech Design URL: CC: **Description**: Refactors automatic update flow to use a custom Sparkle user driver. **Steps to test this PR**: 1. Refer to https://app.asana.com/0/1201037661562251/1208230039908704/f **Definition of Done**: * [ ] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/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) --------- Co-authored-by: Alexey Martemyanov Co-authored-by: Juan Manuel Pereira Co-authored-by: Sabrina Tardio <44158575+SabrinaTardio@users.noreply.github.com> --- DuckDuckGo.xcodeproj/project.pbxproj | 20 +- .../xcshareddata/swiftpm/Package.resolved | 8 +- .../Images/Check.imageset/Check-Color-16.svg | 5 + .../Images/Check.imageset/Contents.json | 12 + .../Contents.json | 2 +- .../Exclamation-High-Color-16.svg | 5 + .../Icon 22.pdf | Bin 1141 -> 0 bytes .../Contents.json | 2 +- .../Document-Color-16.pdf | Bin 1327 -> 0 bytes .../Release-Notes-Color-16.svg | 7 + .../Contents.json | 2 +- .../Exclamation-Color-16-2.svg | 5 + .../Icon 19.pdf | Bin 1140 -> 0 bytes DuckDuckGo/Common/Localizables/UserText.swift | 16 +- .../Utilities/UserDefaultsWrapper.swift | 1 + DuckDuckGo/Localizable.xcstrings | 544 ++++++++++++++++-- .../NavigationBar/View/MoreOptionsMenu.swift | 18 +- .../View/MoreOptionsMenuButton.swift | 12 +- .../Preferences/Model/AboutPreferences.swift | 27 +- .../View/PreferencesAboutView.swift | 107 +++- DuckDuckGo/Updates/AppRestarter.swift | 83 --- .../Updates/BinaryOwnershipChecker.swift | 66 --- .../Updates/ReleaseNotesTabExtension.swift | 116 +++- .../Updates/ReleaseNotesUserScript.swift | 12 +- DuckDuckGo/Updates/UpdateController.swift | 253 ++++---- .../Updates/UpdateNotificationPresenter.swift | 11 +- DuckDuckGo/Updates/UpdateUserDriver.swift | 214 +++++++ .../InputFilesChecker/InputFilesChecker.swift | 3 +- .../DataBrokerProtection/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- .../PopoverMessageView.swift | 2 +- .../Updates/BinaryOwnershipCheckerTests.swift | 87 --- 33 files changed, 1118 insertions(+), 528 deletions(-) create mode 100644 DuckDuckGo/Assets.xcassets/Images/Check.imageset/Check-Color-16.svg create mode 100644 DuckDuckGo/Assets.xcassets/Images/Check.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/CriticalUpdateNotificationInfo.imageset/Exclamation-High-Color-16.svg delete mode 100644 DuckDuckGo/Assets.xcassets/Images/CriticalUpdateNotificationInfo.imageset/Icon 22.pdf delete mode 100644 DuckDuckGo/Assets.xcassets/Images/ReleaseNotesIndicator.imageset/Document-Color-16.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/ReleaseNotesIndicator.imageset/Release-Notes-Color-16.svg create mode 100644 DuckDuckGo/Assets.xcassets/Images/UpdateNotificationInfo.imageset/Exclamation-Color-16-2.svg delete mode 100644 DuckDuckGo/Assets.xcassets/Images/UpdateNotificationInfo.imageset/Icon 19.pdf delete mode 100644 DuckDuckGo/Updates/AppRestarter.swift delete mode 100644 DuckDuckGo/Updates/BinaryOwnershipChecker.swift create mode 100644 DuckDuckGo/Updates/UpdateUserDriver.swift delete mode 100644 UnitTests/Updates/BinaryOwnershipCheckerTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 567547296a..f4d4acb3db 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -54,8 +54,6 @@ 1D01A3D92B88DF8B00FE8150 /* 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 */; }; - 1D0DE93E2C3BA9840037ABC2 /* AppRestarter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0DE93D2C3BA9840037ABC2 /* AppRestarter.swift */; }; - 1D0DE93F2C3BA9840037ABC2 /* AppRestarter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0DE93D2C3BA9840037ABC2 /* AppRestarter.swift */; }; 1D0DE9412C3BB9CC0037ABC2 /* ReleaseNotesParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0DE9402C3BB9CC0037ABC2 /* ReleaseNotesParser.swift */; }; 1D0DE9422C3BB9CC0037ABC2 /* ReleaseNotesParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0DE9402C3BB9CC0037ABC2 /* ReleaseNotesParser.swift */; }; 1D12F2E2298BC660009A65FD /* InternalUserDeciderStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D12F2E1298BC660009A65FD /* InternalUserDeciderStoreMock.swift */; }; @@ -69,8 +67,6 @@ 1D220BF92B86192200F8BBC6 /* 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 */; }; - 1D232E942C7860DA0043840D /* BinaryOwnershipChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D232E932C7860DA0043840D /* BinaryOwnershipChecker.swift */; }; - 1D232E992C7870D90043840D /* BinaryOwnershipCheckerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D232E962C786E7D0043840D /* BinaryOwnershipCheckerTests.swift */; }; 1D26EBAC2B74BECB0002A93F /* NSImageSendable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D26EBAB2B74BECB0002A93F /* NSImageSendable.swift */; }; 1D26EBAD2B74BECB0002A93F /* NSImageSendable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D26EBAB2B74BECB0002A93F /* NSImageSendable.swift */; }; 1D26EBB02B74DB600002A93F /* TabSnapshotCleanupService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D26EBAF2B74DB600002A93F /* TabSnapshotCleanupService.swift */; }; @@ -2766,6 +2762,7 @@ BD384ACA2BBC821A00EF3735 /* vpn-light-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = BD384AC72BBC821100EF3735 /* vpn-light-mode.json */; }; BD384ACB2BBC821B00EF3735 /* vpn-dark-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = BD384AC82BBC821100EF3735 /* vpn-dark-mode.json */; }; BD384ACC2BBC821B00EF3735 /* vpn-light-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = BD384AC72BBC821100EF3735 /* vpn-light-mode.json */; }; + BD6367252C877BE1009DE7A8 /* UpdateUserDriver.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD6367242C877BE1009DE7A8 /* UpdateUserDriver.swift */; }; BD7090CF2C5182FB009EED82 /* UnifiedFeedbackFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7090CE2C5182FB009EED82 /* UnifiedFeedbackFormView.swift */; }; BD7090D02C5182FB009EED82 /* UnifiedFeedbackFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7090CE2C5182FB009EED82 /* UnifiedFeedbackFormView.swift */; }; BD7090D22C52ECFE009EED82 /* UnifiedMetadataCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7090D12C52ECFE009EED82 /* UnifiedMetadataCollector.swift */; }; @@ -3302,7 +3299,6 @@ 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 = ""; }; - 1D0DE93D2C3BA9840037ABC2 /* AppRestarter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRestarter.swift; sourceTree = ""; }; 1D0DE9402C3BB9CC0037ABC2 /* ReleaseNotesParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReleaseNotesParser.swift; sourceTree = ""; }; 1D12F2E1298BC660009A65FD /* InternalUserDeciderStoreMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalUserDeciderStoreMock.swift; sourceTree = ""; }; 1D1A33482A6FEB170080ACED /* BurnerMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BurnerMode.swift; sourceTree = ""; }; @@ -3310,8 +3306,6 @@ 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 = ""; }; - 1D232E932C7860DA0043840D /* BinaryOwnershipChecker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BinaryOwnershipChecker.swift; sourceTree = ""; }; - 1D232E962C786E7D0043840D /* BinaryOwnershipCheckerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BinaryOwnershipCheckerTests.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 = ""; }; @@ -4665,6 +4659,7 @@ BBFF355C2C4AF26200DA3289 /* BookmarksSortModeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksSortModeTests.swift; sourceTree = ""; }; BD384AC72BBC821100EF3735 /* vpn-light-mode.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "vpn-light-mode.json"; sourceTree = ""; }; BD384AC82BBC821100EF3735 /* vpn-dark-mode.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "vpn-dark-mode.json"; sourceTree = ""; }; + BD6367242C877BE1009DE7A8 /* UpdateUserDriver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateUserDriver.swift; sourceTree = ""; }; BD7090CE2C5182FB009EED82 /* UnifiedFeedbackFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnifiedFeedbackFormView.swift; sourceTree = ""; }; BD7090D12C52ECFE009EED82 /* UnifiedMetadataCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnifiedMetadataCollector.swift; sourceTree = ""; }; BD7090D52C540D5D009EED82 /* EmptyMetadataCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyMetadataCollector.swift; sourceTree = ""; }; @@ -5238,12 +5233,11 @@ 1DA84D312C119AE70011C80F /* UpdateMenuItemFactory.swift */, 1D72D59B2BFF61B200AEDE36 /* UpdateNotificationPresenter.swift */, 1D9297BE2C1B062900A38521 /* ApplicationUpdateDetector.swift */, - 1D0DE93D2C3BA9840037ABC2 /* AppRestarter.swift */, - 1D232E932C7860DA0043840D /* BinaryOwnershipChecker.swift */, 1D39E5762C2BFD5700757339 /* ReleaseNotesTabExtension.swift */, 1D39E5792C2C0F3700757339 /* ReleaseNotesUserScript.swift */, 1D0DE9402C3BB9CC0037ABC2 /* ReleaseNotesParser.swift */, 1D710F4A2C48F1F200C3975F /* UpdateDialogHelper.swift */, + BD6367242C877BE1009DE7A8 /* UpdateUserDriver.swift */, ); path = Updates; sourceTree = ""; @@ -5280,7 +5274,6 @@ children = ( 1D838A312C44F0180078373F /* ReleaseNotesParserTests.swift */, 1D638D602C44F2BA00530DD5 /* ApplicationUpdateDetectorTests.swift */, - 1D232E962C786E7D0043840D /* BinaryOwnershipCheckerTests.swift */, ); path = Updates; sourceTree = ""; @@ -11729,7 +11722,6 @@ 3707C71E294B5D2900682A9F /* URLRequestExtension.swift in Sources */, 3706FCA3293F65D500E42796 /* WKProcessPool+GeolocationProvider.swift in Sources */, 372A0FED2B2379310033BF7F /* SyncMetricsEventsHandler.swift in Sources */, - 1D0DE93F2C3BA9840037ABC2 /* AppRestarter.swift in Sources */, 3706FCA4293F65D500E42796 /* RecentlyClosedMenu.swift in Sources */, 8400DC4C2C6E26AE006509D2 /* ItemCachingCollectionView.swift in Sources */, 4B9DB02D2A983B24000927DB /* WaitlistKeychainStorage.swift in Sources */, @@ -12651,7 +12643,6 @@ B6BF5D8929470BC4006742B1 /* HTTPSUpgradeTabExtension.swift in Sources */, 1D36E65B298ACD2900AA485D /* AppIconChanger.swift in Sources */, 4B4D60E22A0C883A00BCD287 /* AppMain.swift in Sources */, - 1D0DE93E2C3BA9840037ABC2 /* AppRestarter.swift in Sources */, 7BCB90C22C18626E008E3543 /* VPNControllerXPCClient+ConvenienceInitializers.swift in Sources */, 4B9DB0202A983B24000927DB /* ProductWaitlistRequest.swift in Sources */, 7B60B0022C5145EC008E32A3 /* VPNUIActionHandler.swift in Sources */, @@ -13174,7 +13165,6 @@ 37BF3F21286F0A7A00BD9014 /* PinnedTabsViewModel.swift in Sources */, EEC4A6692B2C87D300F7C0AA /* VPNLocationView.swift in Sources */, AAC5E4D225D6A709007F5990 /* BookmarkList.swift in Sources */, - 1D232E942C7860DA0043840D /* BinaryOwnershipChecker.swift in Sources */, B602E81D2A1E25B1006D261F /* NEOnDemandRuleExtension.swift in Sources */, 56A0543E2C215FB3007D8FAB /* OnboardingUserScript.swift in Sources */, C1372EF42BBC5BAD003F8793 /* SecureTextField.swift in Sources */, @@ -13203,6 +13193,7 @@ 37219B3A2CBFD4F300C9D7A8 /* NewTabPageSearchBoxExperiment+Logger.swift in Sources */, 37AAA41C2C9CB9C0002A5377 /* AddressBarTextFieldView.swift in Sources */, AA6820EB25503D6A005ED0D5 /* Fire.swift in Sources */, + BD6367252C877BE1009DE7A8 /* UpdateUserDriver.swift in Sources */, 3158B1492B0BF73000AF130C /* DBPHomeViewController.swift in Sources */, 9F56CFA92B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift in Sources */, 37445F9C2A1569F00029F789 /* SyncBookmarksAdapter.swift in Sources */, @@ -13522,7 +13513,6 @@ 567A23E12C89B1EE0010F66C /* BrowserTabViewControllerOnboardingTests.swift in Sources */, 986189E62A7CFB3E001B4519 /* LocalBookmarkStoreSavingTests.swift in Sources */, AA652CD325DDA6E9009059CC /* LocalBookmarkManagerTests.swift in Sources */, - 1D232E992C7870D90043840D /* BinaryOwnershipCheckerTests.swift in Sources */, CBDD5DE329A67F2700832877 /* MockConfigurationStore.swift in Sources */, 9F3910692B68D87B00CB5112 /* ProgressExtensionTests.swift in Sources */, 560C6ED02CCA5C6000D411E2 /* CapturingOnboardingNavigationDelegate.swift in Sources */, @@ -14694,7 +14684,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 202.0.0; + version = 202.1.0; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index f9bc000234..150832d719 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" : "de77673bd4fa7b8012c8d1f16cbc73b064539a57", - "version" : "202.0.0" + "revision" : "8a1bc5526e14c589ca2cc74e6e7d125952b79bc1", + "version" : "202.1.0" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "revision" : "b74549bd869fdecc16fad851f2f608b1724764df", - "version" : "6.25.0" + "revision" : "48fee2508995d4ac02d18b3d55424adedcb4ce4f", + "version" : "6.28.0" } }, { diff --git a/DuckDuckGo/Assets.xcassets/Images/Check.imageset/Check-Color-16.svg b/DuckDuckGo/Assets.xcassets/Images/Check.imageset/Check-Color-16.svg new file mode 100644 index 0000000000..644b9a9c61 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Check.imageset/Check-Color-16.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/DuckDuckGo/Assets.xcassets/Images/Check.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Check.imageset/Contents.json new file mode 100644 index 0000000000..14f73a2877 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Check.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Check-Color-16.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/CriticalUpdateNotificationInfo.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/CriticalUpdateNotificationInfo.imageset/Contents.json index c4a79c7d54..f1d879d17e 100644 --- a/DuckDuckGo/Assets.xcassets/Images/CriticalUpdateNotificationInfo.imageset/Contents.json +++ b/DuckDuckGo/Assets.xcassets/Images/CriticalUpdateNotificationInfo.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "Icon 22.pdf", + "filename" : "Exclamation-High-Color-16.svg", "idiom" : "universal" } ], diff --git a/DuckDuckGo/Assets.xcassets/Images/CriticalUpdateNotificationInfo.imageset/Exclamation-High-Color-16.svg b/DuckDuckGo/Assets.xcassets/Images/CriticalUpdateNotificationInfo.imageset/Exclamation-High-Color-16.svg new file mode 100644 index 0000000000..67505545b4 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/CriticalUpdateNotificationInfo.imageset/Exclamation-High-Color-16.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/DuckDuckGo/Assets.xcassets/Images/CriticalUpdateNotificationInfo.imageset/Icon 22.pdf b/DuckDuckGo/Assets.xcassets/Images/CriticalUpdateNotificationInfo.imageset/Icon 22.pdf deleted file mode 100644 index 97d2e7db8964ba0eb18a8870a48a8a28fe11e1fc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1141 zcmY!laBvK;I`dFTEr~!5FAK2q*+Jp}3?dH8Gc~f^qJYvwnvS1X|wLbs4m* z$D*%ad$Q|BAn(SO;~Li%7j6@qp2a#Rxv0CNL1NMKuPHUJ-d*xN&UmiOZ|O6O%KT!U zx^o+p8&0HGddg^ZrEzt!k^la=1sfMe zE^k@cHHp)>O7m9B+dp$><>ci_ZrHTcB7RlGG!>=qV&}GrEO$9361zrGqxhQn(y45l zz6#$z7L(pSv2~M2VA%76JK2TK?0!F`XL;h%#+<)Lwi~{C{o(z`>b2U3TMk^Z;hJfG zeuMVkBac27={{np-7mU3$2+zly>3D7=ORAA3D!L})-i!kzGm6XE>oSfKQXVos=-F( zLYVKx3#Bc$-))gPzaX~rU(In(4TI#E-kwl~zcEf;0(y@QZ`vYxecmeln-^V{xII6` zaFbe4T{=b64#oYmjd+~Vz|E;lJH@HVTNK7 z$b-dj3qcNpc-%R$BrzvH9aU>ZQED2Ofr2>~#QPvp!OYau*i->13%pZ!Vo?d$JC;TUT&k+B{%!zjyNZVZ diff --git a/DuckDuckGo/Assets.xcassets/Images/ReleaseNotesIndicator.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/ReleaseNotesIndicator.imageset/Contents.json index 552be25080..e224dcfc1c 100644 --- a/DuckDuckGo/Assets.xcassets/Images/ReleaseNotesIndicator.imageset/Contents.json +++ b/DuckDuckGo/Assets.xcassets/Images/ReleaseNotesIndicator.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "Document-Color-16.pdf", + "filename" : "Release-Notes-Color-16.svg", "idiom" : "universal" } ], diff --git a/DuckDuckGo/Assets.xcassets/Images/ReleaseNotesIndicator.imageset/Document-Color-16.pdf b/DuckDuckGo/Assets.xcassets/Images/ReleaseNotesIndicator.imageset/Document-Color-16.pdf deleted file mode 100644 index 5db4a4627fe956be30544b99cfa3d54452b4a537..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1327 zcmY!laBvK;I`dFTEr~!5FAK2q*+Jp}3?dH8Gc~f^ll-={ylbfjz&qzxA{f{=T?v zeZvvSA1#3b9Y26N6c{}?*KTBE-U<8~(|j zf45y&zAg3;%lpn3-TNLe3aDAgT|agG!Ow~!@7C4w=0-oSIZf?wRkGbyHqmHq%!U<* zMdzEQU+vvv^;IaFvsC$NxbI45i{GCdtq|H;TsWo|2P~sBnR%7 zFj6$MZ%I6M_V0> zVcVVS|5tHHD6c9|n$z?k`lM}y-uDB3jJKWkx~sIQ=M;udeyOS+d57&?tjG~%%x)1#*M3tL0YHBruE;!QF>#5?F_-Q|-O{cT*FJl) zW?#i2ftMGyX7MfXZeBmxGG_h+8NSMjy|$n2QWyGn?V8~*Nubl{nNU-=tD~ z1-=Ovj$WU&q;KIg4b7w{i~Uz#NfURSEl_^u<-T22(aon9`^@#q3=2u;>9)F;)jpTm z#VkjAYbmGyjIAzCdR+z2EfUfz5>_*GTNRi-;=Jv#TmNHA?(8=z-O>3Bjhk8DTv-(G zUQ{9S%Ir7WPAi48N%YMWk=c+to%36hFN0~8PagAkJuZa@oZp!i{MeCl;Ye4@PqtP4 z{tmAMjK%J^2MK7pt8g)2B$i8jzqK(`R$y}fy}5bT=Eb~u(`KLh zVa1#IJE8Y&!IU=1(qpMr2?_d6@hX?s2{$ycZws9&;`Pz(Tv+#Kr(fK+>L&aLC2nZy zge7xOx`iZvGgC`=;x~bd0yBlaV_sf_{hGsA!1Pf+TP-=00X;E@&u>vR?g7Ob2OF8G~m89kYZ3N{(m|#F+5l~FQ z6e@(2Q$a%T9IWr1nF4gG0>}qJ3NU+|^Gktx4Kdtb49T&uz%WBG3FN_IxP>5xK|Jo9 zSdy5NpN^`vq9`?u%Rs@L3*vncsbFSmYHX?i6ovvr0|hWkArCHO40I<5AcV}#%`t>5 zEzyN6&4FQqqS4U61n7LIP)Si@W=?7m7bx~TU4X%%QJkNfs;QunnW6~^6#bz5{1OF_ hhrt1?ADmg03UocV_)07)0ei>L(7=>SRn^ts4FDq#*%JT& diff --git a/DuckDuckGo/Assets.xcassets/Images/ReleaseNotesIndicator.imageset/Release-Notes-Color-16.svg b/DuckDuckGo/Assets.xcassets/Images/ReleaseNotesIndicator.imageset/Release-Notes-Color-16.svg new file mode 100644 index 0000000000..440fa7505c --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/ReleaseNotesIndicator.imageset/Release-Notes-Color-16.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/DuckDuckGo/Assets.xcassets/Images/UpdateNotificationInfo.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/UpdateNotificationInfo.imageset/Contents.json index 8976fe4303..a68acc82ac 100644 --- a/DuckDuckGo/Assets.xcassets/Images/UpdateNotificationInfo.imageset/Contents.json +++ b/DuckDuckGo/Assets.xcassets/Images/UpdateNotificationInfo.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "Icon 19.pdf", + "filename" : "Exclamation-Color-16-2.svg", "idiom" : "universal" } ], diff --git a/DuckDuckGo/Assets.xcassets/Images/UpdateNotificationInfo.imageset/Exclamation-Color-16-2.svg b/DuckDuckGo/Assets.xcassets/Images/UpdateNotificationInfo.imageset/Exclamation-Color-16-2.svg new file mode 100644 index 0000000000..39ad1e8ec7 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/UpdateNotificationInfo.imageset/Exclamation-Color-16-2.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/DuckDuckGo/Assets.xcassets/Images/UpdateNotificationInfo.imageset/Icon 19.pdf b/DuckDuckGo/Assets.xcassets/Images/UpdateNotificationInfo.imageset/Icon 19.pdf deleted file mode 100644 index 64eb71ba5fbc2bd1f8383136d02f8e19254f4734..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1140 zcmY!laBvK;I`dFTEr~!5FAK2q*+Jp}3?dH8Gc~f^lxhX+LH|o;}aCzx5p1bn;mx ztID|p>Mm;?va}a^)!0wYG2+W=ZIGFDbE9pU%<}U7-RHmWsjl2LUoP(I+?uDGe+5*< zy_z+RFRznr_F=Q8nvl~njSY-*e7gnz7u#vR{T2Sg@pxm*^Q)~d-Y)t2X6?n*?R-2D zLJn%$za}g1FbPyx?DZzXS?-s&DUX1bcBep@L-?90Ctvx#z3OTEf8XIBM}-2c_grJW zv~0&3P6f-!Yu3s3-3{fk(6WAgRnJ?(L(TEz#obfF7OrS~T;{>aH0y;QTjBIolcH5T z0+|=8unRhFF>+aPDe+s^w@b!vA|{({zIw5x<9Asgd-KmPcTX@Z4B%C6k-Z?QohslH zbjh&E)v&>5=9_2!eGe|VG#35)dp6!p=gcy{=SRF=cVx=%7LNf7`cg*SC-G??FTV}E+;hA>fjg1Lw7M(7U*r~Pa zD!;*>?hA8y@-kPKcwF%d-ZSgK0h30d@QWAPonQXne{22z|DFGC)YpXEe`!`Sw}|nr zfYY{L51wxio8~RJD)pJA!!FBjkIPK2_fDD`va{`)ykezCfUoZg#n&F&Ub*z9Pgt|$ z)r9M6^^9BUy(&RT1RBS%!~%+aNFp*ZH-;x66SycaY3V!W<>i+YD@4aa6HZ8FL8^j& zKw>&9CHbbNWF|W0S13d)80Z;*0fJ#@1`|TCU?v5n7U!21C8riEfD$t(6@n6`bADb) zY97!=P)ddg1|${%#S~1TLP+TyBm~a~`rer-K&L8zd=R7nv&T8V6sXq_!~MmObPo#* zGZd3R9xR4i2yz(2 { // Updates case automaticUpdates = "updates.automatic" + case pendingUpdateShown = "pending.update.shown" // Experiments case pixelExperimentInstalled = "pixel.experiment.installed" diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 1da35b097b..deb90efe90 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -37881,6 +37881,66 @@ } } }, + "notification.auto.update.action" : { + "comment" : "Action to take when an automatic update is available.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zum Aktualisieren neu starten." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Restart to update." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reinicia para actualizar." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Redémarrer pour mettre à jour." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Riavvia per aggiornare." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Start opnieuw om bij te werken." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uruchom ponownie, aby zaktualizować." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reiniciar para atualizar." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Перезапустите приложение." + } + } + } + }, "notification.badge.cookiesmanaged" : { "comment" : "Notification that appears when browser automatically handle cookies", "extractionState" : "extracted_with_value", @@ -38122,121 +38182,181 @@ } }, "notification.critical.update" : { - "comment" : "Notification informing user a critical update is required.", + "comment" : "Notification informing user a critical update is available.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kritisches Update erforderlich." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Critical update needed." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se necesita una actualización crítica." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Une mise à jour critique est nécessaire." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aggiornamento critico necessario." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kritieke update nodig." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wymagana krytyczna aktualizacja." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Atualização crítica necessária." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Требуется критическое обновление." + } + } + } + }, + "notification.manual.update.action" : { + "comment" : "Action to take when a manual update is available.", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", - "value" : "Kritisches Update erforderlich. Zum Aktualisieren neu starten." + "value" : "Klicke hier, um zu aktualisieren." } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "Critical update required. Restart to update." + "value" : "Click here to update." } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Se requiere una actualización crítica. Reinicia para actualizar." + "value" : "Haz clic aquí para actualizar." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Une mise à jour critique est requise. Redémarrer pour mettre à jour." + "value" : "Cliquez ici pour mettre à jour." } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Aggiornamento critico obbligatorio. Riavvia per aggiornare." + "value" : "Fai clic qui per aggiornare." } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Kritieke update vereist. Start opnieuw om bij te werken." + "value" : "Klik hier om te updaten." } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Wymagana krytyczna aktualizacja. Uruchom ponownie, aby zaktualizować." + "value" : "Kliknij tutaj, aby zaktualizować." } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Atualização crítica obrigatória. Reiniciar para atualizar." + "value" : "Clica aqui para atualizar." } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Требуется критическое обновление. Перезапустите приложение." + "value" : "Нажмите здесь, чтобы обновить." } } } }, "notification.update.available" : { - "comment" : "Notification informing user the a version of app is available", + "comment" : "Notification informing user the a version of app is available.", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", - "value" : "Neue Version verfügbar. Zum Aktualisieren neu starten." + "value" : "Neue Version verfügbar." } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "New version available. Restart to update." + "value" : "New version available." } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Nueva versión disponible. Reinicia para actualizar." + "value" : "Nueva versión disponible." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Une nouvelle version est disponible. Redémarrer pour mettre à jour." + "value" : "Une nouvelle version est disponible." } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Nuova versione disponibile. Riavvia per aggiornare." + "value" : "Nuova versione disponibile." } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Nieuwe versie beschikbaar. Start opnieuw om bij te werken." + "value" : "Nieuwe versie beschikbaar." } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Dostępna nowa wersja. Uruchom ponownie, aby zaktualizować." + "value" : "Dostępna nowa wersja." } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Nova versão disponível. Reiniciar para atualizar." + "value" : "Nova versão disponível." } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Доступна новая версия. Перезапустите приложение." + "value" : "Доступна новая версия." } } } @@ -56801,6 +56921,66 @@ } } }, + "settings.downloading.update" : { + "comment" : "Label informing users the app is currently downloading the update. This will contain a percentage", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update wird heruntergeladen %@" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Downloading update %@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Descargando actualización %@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Téléchargement de la mise à jour %@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Download dell'aggiornamento %@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update %@ downloaden" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pobieranie aktualizacji %@" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "A transferir a atualização %@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Загрузка обновления %@" + } + } + } + }, "settings.last.checked" : { "comment" : "Label informing users what is the last time the app checked for the update.", "extractionState" : "extracted_with_value", @@ -56921,6 +57101,66 @@ } } }, + "settings.newer.critical.update.available" : { + "comment" : "Label informing users the critical update of the app is available to install.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kritisches Update erforderlich" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Critical update needed" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se necesita una actualización crítica" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Une mise à jour critique est nécessaire" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aggiornamento critico necessario" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kritieke update nodig" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wymagana krytyczna aktualizacja" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Atualização crítica necessária" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Требуется критическое обновление" + } + } + } + }, "settings.newer.version.available" : { "comment" : "Label informing users the newer version of the app is available to install.", "extractionState" : "extracted_with_value", @@ -56981,8 +57221,68 @@ } } }, + "settings.preparing.update" : { + "comment" : "Label informing users the app is preparing to update.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update vorbereiten" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Preparing update" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Preparando la actualización" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Préparation de la mise à jour" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Preparazione dell'aggiornamento" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update voorbereiden" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Przygotowanie aktualizacji" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "A preparar a atualização" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подготовка обновления" + } + } + } + }, "settings.restart.to.update" : { - "comment" : "Button label trigering restart and update of the application.", + "comment" : "Button label triggering restart and update of the application.", "extractionState" : "extracted_with_value", "localizations" : { "de" : { @@ -56994,7 +57294,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "Restart to Update" + "value" : "Restart To Update" } }, "es" : { @@ -57041,6 +57341,126 @@ } } }, + "settings.retry.update" : { + "comment" : "Button label triggering a retry of the update.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update nochmal versuchen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Retry Update" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reintentar actualización" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Réessayer la mise à jour" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Riprova l'aggiornamento" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update opnieuw proberen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ponów próbę aktualizacji" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Repetir atualização" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Повторить попытку обновления" + } + } + } + }, + "settings.run.update" : { + "comment" : "Button label triggering update of the application.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo aktualisieren" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Update DuckDuckGo" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualizar DuckDuckGo" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mettre à jour DuckDuckGo" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aggiorna DuckDuckGo" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo bijwerken" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zaktualizuj DuckDuckGo" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Atualizar o DuckDuckGo" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Обновить DuckDuckGo" + } + } + } + }, "settings.up.to.date" : { "comment" : "Label informing users the app is currently up to date and no update is required.", "extractionState" : "extracted_with_value", @@ -57101,6 +57521,66 @@ } } }, + "settings.update.failed" : { + "comment" : "Label informing users the app is unable to update.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update fehlgeschlagen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Update failed" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualización fallida" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Échec de la mise à jour" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aggiornamento non riuscito" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update mislukt" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Niepowodzenie aktualizacji" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "A atualização falhou" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не удалось обновить" + } + } + } + }, "share.menu.item" : { "comment" : "Menu item title", "extractionState" : "extracted_with_value", @@ -61695,61 +62175,61 @@ } }, "update.available.menu.item" : { - "comment" : "Title of the menu item that informs user that a new update is available. Clicking on the menu item restarts the app and installs the update", + "comment" : "Title of the menu item that informs user that a new update is available. Clicking on the menu item installs the update", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", - "value" : "Update verfügbar – Jetzt neu starten" + "value" : "Update verfügbar – jetzt installieren" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "Update Available - Restart Now" + "value" : "Update Available - Install Now" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Actualización disponible - Reiniciar ahora" + "value" : "Actualización disponible - Instalar ahora" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Mise à jour disponible : redémarrer" + "value" : "Mise à jour disponible : installer" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Aggiornamento disponibile - Riavvia ora" + "value" : "Aggiornamento disponibile - Installa ora" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Update beschikbaar - nu opnieuw opstarten" + "value" : "Update beschikbaar - nu installeren" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Dostępna aktualizacja — uruchom ponownie teraz" + "value" : "Dostępna aktualizacja — zainstaluj teraz" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Atualização disponível – Reiniciar agora" + "value" : "Atualização disponível – Instalar agora" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Доступно обновление: перезапустить" + "value" : "Доступно обновление – установить сейчас" } } } diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift index 8d05402487..554d665a9e 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift @@ -49,7 +49,7 @@ protocol OptionsButtonMenuDelegate: AnyObject { func optionsButtonMenuRequestedIdentityTheftRestoration(_ menu: NSMenu) } -final class MoreOptionsMenu: NSMenu { +final class MoreOptionsMenu: NSMenu, NSMenuDelegate { weak var actionDelegate: OptionsButtonMenuDelegate? @@ -118,6 +118,8 @@ final class MoreOptionsMenu: NSMenu { } self.emailManager.requestDelegate = self + delegate = self + setupMenuItems() } @@ -311,8 +313,10 @@ final class MoreOptionsMenu: NSMenu { private func addUpdateItem() { #if SPARKLE guard NSApp.runType != .uiTests, - let update = Application.appDelegate.updateController.latestUpdate, - !update.isInstalled + let updateController = Application.appDelegate.updateController, + let update = updateController.latestUpdate, + !update.isInstalled, + updateController.updateProgress.isDone else { return } @@ -478,6 +482,14 @@ final class MoreOptionsMenu: NSMenu { return networkProtectionItem } + func menuWillOpen(_ menu: NSMenu) { +#if SPARKLE + guard let updateController = Application.appDelegate.updateController else { return } + if updateController.hasPendingUpdate && updateController.needsNotificationDot { + updateController.needsNotificationDot = false + } +#endif + } } final class EmailOptionsButtonSubMenu: NSMenu { diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenuButton.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenuButton.swift index 4e2934e4be..a81893b20e 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenuButton.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenuButton.swift @@ -40,6 +40,9 @@ final class MoreOptionsMenuButton: MouseOverButton { var isNotificationVisible: Bool = false { didSet { updateNotificationVisibility() +#if SPARKLE + needsDisplay = isNotificationVisible != oldValue +#endif } } @@ -50,8 +53,8 @@ final class MoreOptionsMenuButton: MouseOverButton { if NSApp.runType != .uiTests { updateController = Application.appDelegate.updateController } -#endif subscribeToUpdateInfo() +#endif } override func updateLayer() { @@ -61,10 +64,11 @@ final class MoreOptionsMenuButton: MouseOverButton { private func subscribeToUpdateInfo() { #if SPARKLE - cancellable = updateController?.isUpdateAvailableToInstallPublisher + guard let updateController else { return } + cancellable = Publishers.CombineLatest(updateController.hasPendingUpdatePublisher, updateController.notificationDotPublisher) .receive(on: DispatchQueue.main) - .sink { [weak self] isAvailable in - self?.isNotificationVisible = isAvailable + .sink { [weak self] hasPendingUpdate, needsNotificationDot in + self?.isNotificationVisible = hasPendingUpdate && needsNotificationDot } #endif } diff --git a/DuckDuckGo/Preferences/Model/AboutPreferences.swift b/DuckDuckGo/Preferences/Model/AboutPreferences.swift index 67eaa2128b..63b5433ce8 100644 --- a/DuckDuckGo/Preferences/Model/AboutPreferences.swift +++ b/DuckDuckGo/Preferences/Model/AboutPreferences.swift @@ -25,25 +25,6 @@ final class AboutPreferences: ObservableObject, PreferencesTabOpening { static let shared = AboutPreferences() #if SPARKLE - enum UpdateState { - - case loading - case upToDate - case newVersionAvailable - - init(from update: Update?, isLoading: Bool) { - if isLoading { - self = .loading - } else { - if let update, !update.isInstalled { - self = .newVersionAvailable - } else { - self = .upToDate - } - } - } - } - @Published var updateState = UpdateState.upToDate var updateController: UpdateControllerProtocol? { @@ -90,10 +71,10 @@ final class AboutPreferences: ObservableObject, PreferencesTabOpening { #if SPARKLE func checkForUpdate() { - updateController?.checkForUpdateInBackground() + updateController?.checkForUpdateIfNeeded() } - func restartToUpdate() { + func runUpdate() { updateController?.runUpdate() } @@ -101,7 +82,7 @@ final class AboutPreferences: ObservableObject, PreferencesTabOpening { guard let updateController, !subscribed else { return } cancellable = updateController.latestUpdatePublisher - .combineLatest(updateController.isUpdateBeingLoadedPublisher) + .combineLatest(updateController.updateProgressPublisher) .receive(on: DispatchQueue.main) .sink { [weak self] _ in self?.refreshUpdateState() @@ -114,7 +95,7 @@ final class AboutPreferences: ObservableObject, PreferencesTabOpening { private func refreshUpdateState() { guard let updateController else { return } - updateState = UpdateState(from: updateController.latestUpdate, isLoading: updateController.isUpdateBeingLoaded) + updateState = UpdateState(from: updateController.latestUpdate, progress: updateController.updateProgress) } #endif diff --git a/DuckDuckGo/Preferences/View/PreferencesAboutView.swift b/DuckDuckGo/Preferences/View/PreferencesAboutView.swift index e76daee578..3ba655735a 100644 --- a/DuckDuckGo/Preferences/View/PreferencesAboutView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesAboutView.swift @@ -51,6 +51,11 @@ extension Preferences { } } } + .onAppear { +#if SPARKLE && !DEBUG + model.checkForUpdate() +#endif + } } } @@ -139,6 +144,15 @@ extension Preferences { } } +#if SPARKLE + private var hasPendingUpdate: Bool { + model.updateController?.hasPendingUpdate == true + } + private var hasCriticalUpdate: Bool { + model.updateController?.latestUpdate?.type == .critical + } +#endif + @ViewBuilder private var versionText: some View { HStack(spacing: 0) { @@ -150,35 +164,77 @@ extension Preferences { })) #if SPARKLE switch model.updateState { - case .loading: - Text(" — " + UserText.checkingForUpdate) case .upToDate: Text(" — " + UserText.upToDate) - case .newVersionAvailable: - Text(" — " + UserText.newerVersionAvailable) + case .updateCycle(let progress): + if hasPendingUpdate { + if hasCriticalUpdate { + Text(" — " + UserText.newerCriticalUpdateAvailable) + } else { + Text(" — " + UserText.newerVersionAvailable) + } + } else { + text(for: progress) + } } #endif } } #if SPARKLE + private var formatter: NumberFormatter { + let formatter = NumberFormatter() + formatter.numberStyle = .percent + formatter.maximumFractionDigits = 0 + return formatter + } + + @ViewBuilder + private func text(for progress: UpdateCycleProgress) -> some View { + switch progress { + case .updateCycleDidStart: + Text(" — " + UserText.checkingForUpdate) + case .downloadDidStart: + Text(" — " + String(format: UserText.downloadingUpdate, "")) + case .downloading(let percentage): + Text(" — " + String(format: UserText.downloadingUpdate, + formatter.string(from: NSNumber(value: percentage)) ?? "")) + case .extractionDidStart, .extracting, .readyToInstallAndRelaunch, .installationDidStart, .installing: + Text(" — " + UserText.preparingUpdate) + case .updaterError: + Text(" — " + UserText.updateFailed) + case .updateCycleNotStarted, .updateCycleDone: + EmptyView() + } + } + @ViewBuilder private var statusIcon: some View { switch model.updateState { - case .loading: - ProgressView() - .scaleEffect(0.6) case .upToDate: - Image(systemName: "checkmark.circle.fill") + Image(nsImage: .check) .foregroundColor(.green) - case .newVersionAvailable: - Image(systemName: "exclamationmark.circle.fill") - .foregroundColor(.red) + case .updateCycle(let progress): + if hasPendingUpdate { + if hasCriticalUpdate { + Image(nsImage: .criticalUpdateNotificationInfo) + .foregroundColor(.red) + } else { + Image(nsImage: .updateNotificationInfo) + .foregroundColor(.blue) + } + } else if progress.isFailed { + Image(nsImage: .criticalUpdateNotificationInfo) + .foregroundColor(.red) + } else { + ProgressView() + .scaleEffect(0.6) + } } } private var lastCheckedText: some View { - let lastChecked = model.updateState != .loading ? "\(lastCheckedFormattedDate(model.lastUpdateCheckDate))" : "-" + let lastChecked = model.updateController?.updateProgress.isIdle == true ? lastCheckedFormattedDate(model.lastUpdateCheckDate) : "-" return Text("\(UserText.lastChecked): \(lastChecked)") .foregroundColor(.secondary) } @@ -200,22 +256,29 @@ extension Preferences { @ViewBuilder private var updateButton: some View { switch model.updateState { - case .loading: - Button(UserText.checkForUpdate) { - model.checkForUpdate() - } - .buttonStyle(UpdateButtonStyle(enabled: false)) - .disabled(true) case .upToDate: Button(UserText.checkForUpdate) { model.checkForUpdate() } .buttonStyle(UpdateButtonStyle(enabled: true)) - case .newVersionAvailable: - Button(UserText.restartToUpdate) { - model.restartToUpdate() + case .updateCycle(let progress): + if hasPendingUpdate { + Button(model.areAutomaticUpdatesEnabled ? UserText.restartToUpdate : UserText.runUpdate) { + model.runUpdate() + } + .buttonStyle(UpdateButtonStyle(enabled: true)) + } else if progress.isFailed { + Button(UserText.retryUpdate) { + model.checkForUpdate() + } + .buttonStyle(UpdateButtonStyle(enabled: true)) + } else { + Button(UserText.checkForUpdate) { + model.checkForUpdate() + } + .buttonStyle(UpdateButtonStyle(enabled: false)) + .disabled(true) } - .buttonStyle(UpdateButtonStyle(enabled: true)) } } #endif diff --git a/DuckDuckGo/Updates/AppRestarter.swift b/DuckDuckGo/Updates/AppRestarter.swift deleted file mode 100644 index fa498016f7..0000000000 --- a/DuckDuckGo/Updates/AppRestarter.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// AppRestarter.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 AppRestarting { - - func restart() - -} - -final class AppRestarter: AppRestarting { - - func restart() { - let pid = ProcessInfo.processInfo.processIdentifier - let destinationPath = Bundle.main.bundlePath - - guard isValidApplicationBundle(at: destinationPath) else { - print("Invalid destination path") - return - } - - let preOpenCmd = "/usr/bin/xattr -d -r com.apple.quarantine \(shellQuotedString(destinationPath))" - let openCmd = "/usr/bin/open \(shellQuotedString(destinationPath))" - - let script = """ - (while /bin/kill -0 \(pid) >&/dev/null; do /bin/sleep 0.1; done; \(preOpenCmd); \(openCmd)) & - """ - - let task = Process() - task.launchPath = "/bin/sh" - task.arguments = ["-c", script] - - do { - try task.run() - } catch { - print("Unable to launch the task: \(error)") - return - } - - // Terminate the current app instance - exit(0) - } - - private func isValidApplicationBundle(at path: String) -> Bool { - let fileManager = FileManager.default - var isDirectory: ObjCBool = false - let exists = fileManager.fileExists(atPath: path, isDirectory: &isDirectory) - let isAppBundle = path.hasSuffix(".app") && isDirectory.boolValue - return exists && isAppBundle - } - - private func shellQuotedString(_ string: String) -> String { - // Validate that the string is a valid file path - guard isValidFilePath(string) else { - fatalError("Invalid file path") - } - let escapedString = string.replacingOccurrences(of: "'", with: "'\\''") - return "'\(escapedString)'" - } - - private func isValidFilePath(_ path: String) -> Bool { - // Perform validation to ensure the path is a valid and safe file path - let fileManager = FileManager.default - return fileManager.fileExists(atPath: path) - } - -} diff --git a/DuckDuckGo/Updates/BinaryOwnershipChecker.swift b/DuckDuckGo/Updates/BinaryOwnershipChecker.swift deleted file mode 100644 index e3825f92ae..0000000000 --- a/DuckDuckGo/Updates/BinaryOwnershipChecker.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// BinaryOwnershipChecker.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 Common -import os.log - -protocol BinaryOwnershipChecking { - func isCurrentUserOwner() -> Bool -} - -/// A class responsible for checking whether the current user owns the binary of the app. -/// The result is cached after the first check to avoid repeated file system access. -final class BinaryOwnershipChecker: BinaryOwnershipChecking { - - private let fileManager: FileManager - private var ownershipCache: Bool? - - init(fileManager: FileManager = .default) { - self.fileManager = fileManager - } - - /// Checks if the current user owns the binary of the currently running app. - /// The method caches the result after the first check to improve performance on subsequent calls. - /// - Returns: `true` if the current user is the owner, `false` otherwise. - func isCurrentUserOwner() -> Bool { - if let cachedResult = ownershipCache { - return cachedResult - } - - guard let binaryPath = Bundle.main.executablePath else { - Logger.updates.debug("Failed to get the binary path") - ownershipCache = false - return false - } - - do { - let attributes = try fileManager.attributesOfItem(atPath: binaryPath) - if let ownerID = attributes[FileAttributeKey.ownerAccountID] as? NSNumber { - let isOwner = ownerID.intValue == getuid() - ownershipCache = isOwner - return isOwner - } - } catch { - Logger.updates.error("Failed to get binary file attributes: \(error.localizedDescription, privacy: .public)") - } - - ownershipCache = false - return false - } -} diff --git a/DuckDuckGo/Updates/ReleaseNotesTabExtension.swift b/DuckDuckGo/Updates/ReleaseNotesTabExtension.swift index 9ad7e59e48..76e06dc537 100644 --- a/DuckDuckGo/Updates/ReleaseNotesTabExtension.swift +++ b/DuckDuckGo/Updates/ReleaseNotesTabExtension.swift @@ -32,6 +32,15 @@ protocol ReleaseNotesUserScriptProvider { extension UserScripts: ReleaseNotesUserScriptProvider {} public struct ReleaseNotesValues: Codable { + enum Status: String { + case loaded + case loading + case updateReady + case updateDownloading + case updatePreparing + case updateError + case criticalUpdateReady + } let status: String let currentVersion: String @@ -40,7 +49,8 @@ public struct ReleaseNotesValues: Codable { let releaseTitle: String? let releaseNotes: [String]? let releaseNotesPrivacyPro: [String]? - + let downloadProgress: Double? + let automaticUpdate: Bool? } final class ReleaseNotesTabExtension: NavigationResponder { @@ -84,7 +94,7 @@ final class ReleaseNotesTabExtension: NavigationResponder { return } let updateController = Application.appDelegate.updateController! - Publishers.CombineLatest(updateController.isUpdateBeingLoadedPublisher, updateController.latestUpdatePublisher) + Publishers.CombineLatest(updateController.updateProgressPublisher, updateController.latestUpdatePublisher) .receive(on: DispatchQueue.main) .sink { [weak self] _ in guard let self else { return } @@ -93,6 +103,14 @@ final class ReleaseNotesTabExtension: NavigationResponder { .store(in: &cancellables) } + @MainActor + func navigationDidFinish(_ navigation: Navigation) { +#if !DEBUG + guard NSApp.runType != .uiTests, navigation.url.isReleaseNotesScheme else { return } + let updateController = Application.appDelegate.updateController! + updateController.checkForUpdateIfNeeded() +#endif + } } protocol ReleaseNotesTabExtensionProtocol: AnyObject, NavigationResponder {} @@ -107,49 +125,87 @@ extension TabExtensions { extension ReleaseNotesValues { - init(status: String, + init(status: Status, currentVersion: String, - lastUpdate: UInt) { - self.init(status: status, - currentVersion: currentVersion, - latestVersion: nil, - lastUpdate: lastUpdate, - releaseTitle: nil, - releaseNotes: nil, - releaseNotesPrivacyPro: nil) + latestVersion: String? = nil, + lastUpdate: UInt, + releaseTitle: String? = nil, + releaseNotes: [String]? = nil, + releaseNotesPrivacyPro: [String]? = nil, + downloadProgress: Double? = nil, + automaticUpdate: Bool? = nil) { + self.status = status.rawValue + self.currentVersion = currentVersion + self.latestVersion = latestVersion + self.lastUpdate = lastUpdate + self.releaseTitle = releaseTitle + self.releaseNotes = releaseNotes + self.releaseNotesPrivacyPro = releaseNotesPrivacyPro + self.downloadProgress = downloadProgress + self.automaticUpdate = automaticUpdate } init(from updateController: UpdateController?) { let currentVersion = "\(AppVersion().versionNumber) (\(AppVersion().buildNumber))" let lastUpdate = UInt((updateController?.lastUpdateCheckDate ?? Date()).timeIntervalSince1970) - let status: String - let latestVersion: String - guard let updateController, !updateController.isUpdateBeingLoaded else { - self.init(status: "loading", + guard let updateController, let latestUpdate = updateController.latestUpdate else { + self.init(status: updateController?.updateProgress.toStatus ?? .loaded, currentVersion: currentVersion, lastUpdate: lastUpdate) return } - if let latestUpdate = updateController.latestUpdate { - status = latestUpdate.isInstalled ? "loaded" : "updateReady" - latestVersion = "\(latestUpdate.version) (\(latestUpdate.build))" - self.init(status: status, - currentVersion: currentVersion, - latestVersion: latestVersion, - lastUpdate: lastUpdate, - releaseTitle: latestUpdate.title, - releaseNotes: latestUpdate.releaseNotes, - releaseNotesPrivacyPro: latestUpdate.releaseNotesPrivacyPro) - return - } else { - self.init(status: "loaded", - currentVersion: currentVersion, - lastUpdate: lastUpdate) + let updateState = UpdateState(from: updateController.latestUpdate, progress: updateController.updateProgress) + + let status: Status + let downloadProgress: Double? + switch updateState { + case .upToDate: + status = .loaded + downloadProgress = nil + case .updateCycle(let progress): + if updateController.hasPendingUpdate { + status = updateController.latestUpdate?.type == .critical ? .criticalUpdateReady : .updateReady + } else { + status = progress.toStatus + } + downloadProgress = progress.toDownloadProgress } + + self.init(status: status, + currentVersion: currentVersion, + latestVersion: latestUpdate.versionString, + lastUpdate: lastUpdate, + releaseTitle: latestUpdate.title, + releaseNotes: latestUpdate.releaseNotes, + releaseNotesPrivacyPro: latestUpdate.releaseNotesPrivacyPro, + downloadProgress: downloadProgress, + automaticUpdate: updateController.areAutomaticUpdatesEnabled) } +} +private extension Update { + var versionString: String? { + "\(version) \(build)" + } +} + +private extension UpdateCycleProgress { + var toStatus: ReleaseNotesValues.Status { + switch self { + case .updateCycleDidStart: return .loading + case .downloadDidStart, .downloading: return .updateDownloading + case .extractionDidStart, .extracting, .readyToInstallAndRelaunch, .installationDidStart, .installing: return .updatePreparing + case .updaterError: return .updateError + case .updateCycleNotStarted, .updateCycleDone: return .updateReady + } + } + + var toDownloadProgress: Double? { + guard case .downloading(let percentage) = self else { return nil } + return percentage + } } #else diff --git a/DuckDuckGo/Updates/ReleaseNotesUserScript.swift b/DuckDuckGo/Updates/ReleaseNotesUserScript.swift index 199d0da2d0..620c2ff3b9 100644 --- a/DuckDuckGo/Updates/ReleaseNotesUserScript.swift +++ b/DuckDuckGo/Updates/ReleaseNotesUserScript.swift @@ -43,6 +43,7 @@ final class ReleaseNotesUserScript: NSObject, Subfeature { case reportPageException case reportInitException case browserRestart + case retryUpdate } override init() { @@ -57,7 +58,8 @@ final class ReleaseNotesUserScript: NSObject, Subfeature { .initialSetup: initialSetup, .reportPageException: reportPageException, .reportInitException: reportInitException, - .browserRestart: browserRestart + .browserRestart: browserRestart, + .retryUpdate: retryUpdate, ] @MainActor @@ -108,6 +110,14 @@ extension ReleaseNotesUserScript { return InitialSetupResult(env: env, locale: Locale.current.identifier) } + @MainActor + private func retryUpdate(params: Any, original: WKScriptMessage) async throws -> Encodable? { + DispatchQueue.main.async { [weak self] in + self?.updateController.checkForUpdateIfNeeded() + } + return nil + } + struct InitialSetupResult: Encodable { let env: String let locale: String diff --git a/DuckDuckGo/Updates/UpdateController.swift b/DuckDuckGo/Updates/UpdateController.swift index 2d7b4ffec8..f044522f63 100644 --- a/DuckDuckGo/Updates/UpdateController.swift +++ b/DuckDuckGo/Updates/UpdateController.swift @@ -31,17 +31,18 @@ protocol UpdateControllerProtocol: AnyObject { var latestUpdate: Update? { get } var latestUpdatePublisher: Published.Publisher { get } - var isUpdateAvailableToInstall: Bool { get } - var isUpdateAvailableToInstallPublisher: Published.Publisher { get } + var hasPendingUpdate: Bool { get } + var hasPendingUpdatePublisher: Published.Publisher { get } - var isUpdateBeingLoaded: Bool { get } - var isUpdateBeingLoadedPublisher: Published.Publisher { get } + var needsNotificationDot: Bool { get set } + var notificationDotPublisher: AnyPublisher { get } - var lastUpdateCheckDate: Date? { get } + var updateProgress: UpdateCycleProgress { get } + var updateProgressPublisher: Published.Publisher { get } - func checkForUpdate() - func checkForUpdateInBackground() + var lastUpdateCheckDate: Date? { get } + func checkForUpdateIfNeeded() func runUpdate() var areAutomaticUpdatesEnabled: Bool { get set } @@ -59,85 +60,75 @@ final class UpdateController: NSObject, UpdateControllerProtocol { lazy var notificationPresenter = UpdateNotificationPresenter() let willRelaunchAppPublisher: AnyPublisher - @Published private(set) var isUpdateBeingLoaded = false - var isUpdateBeingLoadedPublisher: Published.Publisher { $isUpdateBeingLoaded } - // Struct used to cache data until the updater finishes checking for updates struct UpdateCheckResult { let item: SUAppcastItem let isInstalled: Bool } - private var updateCheckResult: UpdateCheckResult? + private var cachedUpdateResult: UpdateCheckResult? - @Published private(set) var latestUpdate: Update? { + @Published private(set) var updateProgress = UpdateCycleProgress.default { didSet { - if let latestUpdate, !latestUpdate.isInstalled { - if !shouldShowManualUpdateDialog { - switch latestUpdate.type { - case .critical: - notificationPresenter.showUpdateNotification(icon: NSImage.criticalUpdateNotificationInfo, text: UserText.criticalUpdateNotification, presentMultiline: true) - case .regular: - notificationPresenter.showUpdateNotification(icon: NSImage.updateNotificationInfo, text: UserText.updateAvailableNotification, presentMultiline: true) - } - } - isUpdateAvailableToInstall = !latestUpdate.isInstalled - } else { - isUpdateAvailableToInstall = false + if let cachedUpdateResult { + latestUpdate = Update(appcastItem: cachedUpdateResult.item, isInstalled: cachedUpdateResult.isInstalled) + hasPendingUpdate = latestUpdate?.isInstalled == false && updateProgress.isIdle + needsNotificationDot = hasPendingUpdate } + showUpdateNotificationIfNeeded() } } + var updateProgressPublisher: Published.Publisher { $updateProgress } + + @Published private(set) var latestUpdate: Update? + var latestUpdatePublisher: Published.Publisher { $latestUpdate } - @Published private(set) var isUpdateAvailableToInstall = false - var isUpdateAvailableToInstallPublisher: Published.Publisher { $isUpdateAvailableToInstall } + @Published private(set) var hasPendingUpdate = false + var hasPendingUpdatePublisher: Published.Publisher { $hasPendingUpdate } + + var lastUpdateCheckDate: Date? { updater?.lastUpdateCheckDate } + var lastUpdateNotificationShownDate: Date = .distantPast - var lastUpdateCheckDate: Date? { - updater.updater.lastUpdateCheckDate + private var shouldShowUpdateNotification: Bool { + Date().timeIntervalSince(lastUpdateNotificationShownDate) > .days(7) } @UserDefaultsWrapper(key: .automaticUpdates, defaultValue: true) var areAutomaticUpdatesEnabled: Bool { didSet { - Logger.updates.debug("areAutomaticUpdatesEnabled: \(self.areAutomaticUpdatesEnabled)") - if updater.updater.automaticallyDownloadsUpdates != areAutomaticUpdatesEnabled { - updater.updater.automaticallyDownloadsUpdates = areAutomaticUpdatesEnabled - - // Reinitialize in order to reset the current loaded state - if !areAutomaticUpdatesEnabled { - configureUpdater() - latestUpdate = nil - } + Logger.updates.log("areAutomaticUpdatesEnabled: \(self.areAutomaticUpdatesEnabled)") + if oldValue != areAutomaticUpdatesEnabled { + userDriver?.cancelAndDismissCurrentUpdate() + try? configureUpdater() } } } - var automaticUpdateFlow: Bool { - // In case the current user is not the owner of the binary, we have to switch - // to manual update flow because the authentication is required. - return areAutomaticUpdatesEnabled && binaryOwnershipChecker.isCurrentUserOwner() + @UserDefaultsWrapper(key: .pendingUpdateShown, defaultValue: false) + var needsNotificationDot: Bool { + didSet { + notificationDotSubject.send(needsNotificationDot) + } } - var shouldShowManualUpdateDialog = false + private let notificationDotSubject = CurrentValueSubject(false) + lazy var notificationDotPublisher = notificationDotSubject.eraseToAnyPublisher() - private(set) var updater: SPUStandardUpdaterController! - private var appRestarter: AppRestarting + private(set) var updater: SPUUpdater? + private(set) var userDriver: UpdateUserDriver? private let willRelaunchAppSubject = PassthroughSubject() private var internalUserDecider: InternalUserDecider - private let binaryOwnershipChecker: BinaryOwnershipChecking + private var updateProcessCancellable: AnyCancellable! // MARK: - Public - init(internalUserDecider: InternalUserDecider, - appRestarter: AppRestarting = AppRestarter(), - binaryOwnershipChecker: BinaryOwnershipChecking = BinaryOwnershipChecker()) { + init(internalUserDecider: InternalUserDecider) { willRelaunchAppPublisher = willRelaunchAppSubject.eraseToAnyPublisher() self.internalUserDecider = internalUserDecider - self.appRestarter = appRestarter - self.binaryOwnershipChecker = binaryOwnershipChecker super.init() - configureUpdater() + try? configureUpdater() } func checkNewApplicationVersion() { @@ -151,80 +142,80 @@ final class UpdateController: NSObject, UpdateControllerProtocol { } } - func checkForUpdate() { - Logger.updates.debug("Checking for updates") - - updater.updater.checkForUpdates() - } - - func checkForUpdateInBackground() { - Logger.updates.debug("Checking for updates in background") - - updater.updater.checkForUpdatesInBackground() - } + func checkForUpdateIfNeeded() { + guard let updater, !updater.sessionInProgress else { return } - @objc func runUpdate() { - PixelKit.fire(DebugEvent(GeneralPixel.updaterDidRunUpdate)) + Logger.updates.log("Checking for updates") - if automaticUpdateFlow { - appRestarter.restart() - } else { - updater.userDriver.activeUpdateAlert?.hideUnnecessaryUpdateButtons() - shouldShowManualUpdateDialog = true - checkForUpdate() - } + updater.checkForUpdates() } // MARK: - Private - private func configureUpdater() { + private func configureUpdater() throws { + // Workaround to reset the updater state + cachedUpdateResult = nil + latestUpdate = nil + // The default configuration of Sparkle updates is in Info.plist - updater = SPUStandardUpdaterController(updaterDelegate: self, userDriverDelegate: self) - shouldShowManualUpdateDialog = false + userDriver = UpdateUserDriver(internalUserDecider: internalUserDecider, + areAutomaticUpdatesEnabled: areAutomaticUpdatesEnabled) + guard let userDriver else { return } - if updater.updater.automaticallyDownloadsUpdates != automaticUpdateFlow { - updater.updater.automaticallyDownloadsUpdates = automaticUpdateFlow - } + updater = SPUUpdater(hostBundle: Bundle.main, applicationBundle: Bundle.main, userDriver: userDriver, delegate: self) + + updateProcessCancellable = userDriver.updateProgressPublisher + .assign(to: \.updateProgress, onWeaklyHeld: self) + + try updater?.start() #if DEBUG - updater.updater.automaticallyChecksForUpdates = false - updater.updater.automaticallyDownloadsUpdates = false - updater.updater.updateCheckInterval = 0 + updater?.automaticallyChecksForUpdates = false + updater?.automaticallyDownloadsUpdates = false + updater?.updateCheckInterval = 0 #else - // Load the appcast to retrieve information about the latest update (required for displaying Release Notes) - checkForUpdateInBackground() + checkForUpdateIfNeeded() #endif } - @objc private func openUpdatesPage() { - notificationPresenter.openUpdatesPage() - } - -} + private func showUpdateNotificationIfNeeded() { + guard let latestUpdate, hasPendingUpdate, shouldShowUpdateNotification else { return } + + let action = areAutomaticUpdatesEnabled ? UserText.autoUpdateAction : UserText.manualUpdateAction + + switch latestUpdate.type { + case .critical: + notificationPresenter.showUpdateNotification( + icon: NSImage.criticalUpdateNotificationInfo, + text: "\(UserText.criticalUpdateNotification) \(action)", + presentMultiline: true + ) + case .regular: + notificationPresenter.showUpdateNotification( + icon: NSImage.updateNotificationInfo, + text: "\(UserText.updateAvailableNotification) \(action)", + presentMultiline: true + ) + } -extension UpdateController: SPUStandardUserDriverDelegate { + lastUpdateNotificationShownDate = Date() + } - func standardUserDriverShouldHandleShowingScheduledUpdate(_ update: SUAppcastItem, andInImmediateFocus immediateFocus: Bool) -> Bool { - return shouldShowManualUpdateDialog + @objc func openUpdatesPage() { + notificationPresenter.openUpdatesPage() } - func standardUserDriverWillHandleShowingUpdate(_ handleShowingUpdate: Bool, forUpdate update: SUAppcastItem, state: SPUUserUpdateState) {} + @objc func runUpdate() { + if let userDriver { + PixelKit.fire(DebugEvent(GeneralPixel.updaterDidRunUpdate)) + userDriver.resume() + } + } } extension UpdateController: SPUUpdaterDelegate { - func updater(_ updater: SPUUpdater, mayPerform updateCheck: SPUUpdateCheck) throws { - Logger.updates.debug("Updater started performing the update check. (isInternalUser: \(self.internalUserDecider.isInternalUser)") - - onUpdateCheckStart() - } - - private func onUpdateCheckStart() { - updateCheckResult = nil - isUpdateBeingLoaded = true - } - func allowedChannels(for updater: SPUUpdater) -> Set { if internalUserDecider.isInternalUser { return Set([Constants.internalChannelName]) @@ -251,63 +242,41 @@ extension UpdateController: SPUUpdaterDelegate { } func updater(_ updater: SPUUpdater, didFindValidUpdate item: SUAppcastItem) { - Logger.updates.debug("Updater did find valid update: \(item.displayVersionString)(\(item.versionString))") - + Logger.updates.log("Updater did find valid update: \(item.displayVersionString)(\(item.versionString))") PixelKit.fire(DebugEvent(GeneralPixel.updaterDidFindUpdate)) - - if !automaticUpdateFlow { - // For manual updates, we can present the available update without waiting for the update cycle to finish. The Sparkle flow downloads the update later - updateCheckResult = UpdateCheckResult(item: item, isInstalled: false) - onUpdateCheckEnd() - } + cachedUpdateResult = UpdateCheckResult(item: item, isInstalled: false) } func updaterDidNotFindUpdate(_ updater: SPUUpdater, error: any Error) { - let item = (error as NSError).userInfo["SULatestAppcastItemFound"] as? SUAppcastItem - Logger.updates.debug("Updater did not find update: \(String(describing: item?.displayVersionString))(\(String(describing: item?.versionString)))") - if let item { - // User is running the latest version - updateCheckResult = UpdateCheckResult(item: item, isInstalled: true) - } + let nsError = error as NSError + guard let item = nsError.userInfo["SULatestAppcastItemFound"] as? SUAppcastItem else { return } + Logger.updates.log("Updater did not find update: \(String(describing: item.displayVersionString))(\(String(describing: item.versionString)))") PixelKit.fire(DebugEvent(GeneralPixel.updaterDidNotFindUpdate, error: error)) + + cachedUpdateResult = UpdateCheckResult(item: item, isInstalled: true) } func updater(_ updater: SPUUpdater, didDownloadUpdate item: SUAppcastItem) { - Logger.updates.debug("Updater did download update: \(item.displayVersionString)(\(item.versionString))") - - if automaticUpdateFlow { - // For automatic updates, the available item has to be downloaded - updateCheckResult = UpdateCheckResult(item: item, isInstalled: false) - return - } - + Logger.updates.log("Updater did download update: \(item.displayVersionString)(\(item.versionString))") PixelKit.fire(DebugEvent(GeneralPixel.updaterDidDownloadUpdate)) } - func updater(_ updater: SPUUpdater, didFinishUpdateCycleFor updateCheck: SPUUpdateCheck, error: (any Error)?) { - Logger.updates.debug("Updater did finish update cycle") - - onUpdateCheckEnd() + func updater(_ updater: SPUUpdater, didExtractUpdate item: SUAppcastItem) { + Logger.updates.log("Updater did extract update: \(item.displayVersionString)(\(item.versionString))") } - private func onUpdateCheckEnd() { - guard isUpdateBeingLoaded else { - // The update check end is already handled - return - } + func updater(_ updater: SPUUpdater, willInstallUpdate item: SUAppcastItem) { + Logger.updates.log("Updater will install update: \(item.displayVersionString)(\(item.versionString))") + } - // If the update is available, present it - if let updateCheckResult = updateCheckResult { - latestUpdate = Update(appcastItem: updateCheckResult.item, - isInstalled: updateCheckResult.isInstalled) + func updater(_ updater: SPUUpdater, didFinishUpdateCycleFor updateCheck: SPUUpdateCheck, error: (any Error)?) { + if error == nil { + Logger.updates.log("Updater did finish update cycle") + updateProgress = .updateCycleDone } else { - latestUpdate = nil + Logger.updates.log("Updater did finish update cycle with error") } - - // Clear cache - isUpdateBeingLoaded = false - updateCheckResult = nil } } diff --git a/DuckDuckGo/Updates/UpdateNotificationPresenter.swift b/DuckDuckGo/Updates/UpdateNotificationPresenter.swift index 5c68f0c64b..bea8259c16 100644 --- a/DuckDuckGo/Updates/UpdateNotificationPresenter.swift +++ b/DuckDuckGo/Updates/UpdateNotificationPresenter.swift @@ -26,7 +26,7 @@ final class UpdateNotificationPresenter { static let presentationTimeInterval: TimeInterval = 10 func showUpdateNotification(icon: NSImage, text: String, buttonText: String? = nil, presentMultiline: Bool = false) { - Logger.updates.debug("Notification presented: \(text)") + Logger.updates.log("Notification presented: \(text)") DispatchQueue.main.async { guard let windowController = WindowControllersManager.shared.lastKeyMainWindowController ?? WindowControllersManager.shared.mainWindowControllers.last, @@ -34,6 +34,12 @@ final class UpdateNotificationPresenter { return } + let parentViewController = windowController.mainViewController + + guard parentViewController.view.window?.isKeyWindow == true, (parentViewController.presentedViewControllers ?? []).isEmpty else { + return + } + let buttonAction: (() -> Void)? = { [weak self] in self?.openUpdatesPage() } @@ -49,8 +55,7 @@ final class UpdateNotificationPresenter { self?.openUpdatesPage() }) - viewController.show(onParent: windowController.mainViewController, - relativeTo: button) + viewController.show(onParent: parentViewController, relativeTo: button) } } diff --git a/DuckDuckGo/Updates/UpdateUserDriver.swift b/DuckDuckGo/Updates/UpdateUserDriver.swift new file mode 100644 index 0000000000..960825f46e --- /dev/null +++ b/DuckDuckGo/Updates/UpdateUserDriver.swift @@ -0,0 +1,214 @@ +// +// UpdateUserDriver.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 Sparkle +import PixelKit +import BrowserServicesKit +import Combine +import os.log + +#if SPARKLE + +enum UpdateState { + case upToDate + case updateCycle(UpdateCycleProgress) + + init(from update: Update?, progress: UpdateCycleProgress) { + if let update, !update.isInstalled { + self = .updateCycle(progress) + } else if progress.isFailed { + self = .updateCycle(progress) + } else { + self = .upToDate + } + } +} + +enum UpdateCycleProgress { + case updateCycleNotStarted + case updateCycleDidStart + case updateCycleDone + case downloadDidStart + case downloading(Double) + case extractionDidStart + case extracting(Double) + case readyToInstallAndRelaunch + case installationDidStart + case installing + case updaterError(Error) + + static var `default` = UpdateCycleProgress.updateCycleNotStarted + + var isDone: Bool { + switch self { + case .updateCycleDone: return true + default: return false + } + } + + var isIdle: Bool { + switch self { + case .updateCycleDone, .updateCycleNotStarted, .updaterError: return true + default: return false + } + } + + var isFailed: Bool { + switch self { + case .updaterError: return true + default: return false + } + } +} + +final class UpdateUserDriver: NSObject, SPUUserDriver { + enum Checkpoint: Equatable { + case download + case restart + } + + private var internalUserDecider: InternalUserDecider + + private var checkpoint: Checkpoint + private var onResuming: () -> Void = {} + + private var onSkipping: () -> Void = {} + + private var bytesToDownload: UInt64 = 0 + private var bytesDownloaded: UInt64 = 0 + + @Published var updateProgress = UpdateCycleProgress.default + public var updateProgressPublisher: Published.Publisher { $updateProgress } + + init(internalUserDecider: InternalUserDecider, + areAutomaticUpdatesEnabled: Bool) { + self.internalUserDecider = internalUserDecider + self.checkpoint = areAutomaticUpdatesEnabled ? .restart : .download + } + + func resume() { + onResuming() + } + + func cancelAndDismissCurrentUpdate() { + onSkipping() + } + + func show(_ request: SPUUpdatePermissionRequest) async -> SUUpdatePermissionResponse { +#if DEBUG + .init(automaticUpdateChecks: false, sendSystemProfile: false) +#else + .init(automaticUpdateChecks: true, sendSystemProfile: false) +#endif + } + + func showUserInitiatedUpdateCheck(cancellation: @escaping () -> Void) { + Logger.updates.log("Updater started performing the update check. (isInternalUser: \(self.internalUserDecider.isInternalUser)") + updateProgress = .updateCycleDidStart + } + + func showUpdateFound(with appcastItem: SUAppcastItem, state: SPUUserUpdateState, reply: @escaping (SPUUserUpdateChoice) -> Void) { + if appcastItem.isInformationOnlyUpdate { + reply(.dismiss) + } + + onSkipping = { reply(.skip) } + + if checkpoint == .download { + onResuming = { reply(.install) } + updateProgress = .updateCycleDone + } else { + reply(.install) + } + } + + func showUpdateReleaseNotes(with downloadData: SPUDownloadData) { + // no-op + } + + func showUpdateReleaseNotesFailedToDownloadWithError(_ error: any Error) { + // no-op + } + + func showUpdateNotFoundWithError(_ error: any Error, acknowledgement: @escaping () -> Void) { + acknowledgement() + } + + func showUpdaterError(_ error: any Error, acknowledgement: @escaping () -> Void) { + updateProgress = .updaterError(error) + acknowledgement() + } + + func showDownloadInitiated(cancellation: @escaping () -> Void) { + updateProgress = .downloadDidStart + } + + func showDownloadDidReceiveExpectedContentLength(_ expectedContentLength: UInt64) { + bytesDownloaded = 0 + bytesToDownload = expectedContentLength + } + + func showDownloadDidReceiveData(ofLength length: UInt64) { + bytesDownloaded += length + if bytesDownloaded > bytesToDownload { + bytesToDownload = bytesDownloaded + } + updateProgress = .downloading(Double(bytesDownloaded) / Double(bytesToDownload)) + } + + func showDownloadDidStartExtractingUpdate() { + updateProgress = .extractionDidStart + } + + func showExtractionReceivedProgress(_ progress: Double) { + updateProgress = .extracting(progress) + } + + func showReady(toInstallAndRelaunch reply: @escaping (SPUUserUpdateChoice) -> Void) { + onSkipping = { reply(.skip) } + + if checkpoint == .restart { + onResuming = { reply(.install) } + } else { + reply(.install) + } + + updateProgress = .updateCycleDone + } + + func showInstallingUpdate(withApplicationTerminated applicationTerminated: Bool, retryTerminatingApplication: @escaping () -> Void) { + updateProgress = .installationDidStart + } + + func showUpdateInstalledAndRelaunched(_ relaunched: Bool, acknowledgement: @escaping () -> Void) { + updateProgress = .installing + acknowledgement() + } + + func showUpdateInFocus() { + // no-op + } + + func dismissUpdateInstallation() { + guard !updateProgress.isFailed else { return } + updateProgress = .updateCycleDone + } +} + +#endif diff --git a/LocalPackages/BuildToolPlugins/Plugins/InputFilesChecker/InputFilesChecker.swift b/LocalPackages/BuildToolPlugins/Plugins/InputFilesChecker/InputFilesChecker.swift index f9ef9def11..8f91c6078c 100644 --- a/LocalPackages/BuildToolPlugins/Plugins/InputFilesChecker/InputFilesChecker.swift +++ b/LocalPackages/BuildToolPlugins/Plugins/InputFilesChecker/InputFilesChecker.swift @@ -21,11 +21,11 @@ import PackagePlugin import XcodeProjectPlugin let nonSandboxedExtraInputFiles: Set = [ - .init("BinaryOwnershipChecker.swift", .source), .init("BWEncryption.m", .source), .init("BWEncryptionOutput.m", .source), .init("BWManager.swift", .source), .init("UpdateController.swift", .source), + .init("UpdateUserDriver.swift", .source), .init("PFMoveApplication.m", .source), .init("DuckDuckGo VPN.app", .unknown), .init("DuckDuckGo Notifications.app", .unknown), @@ -50,7 +50,6 @@ let extraInputFiles: [TargetName: Set] = [ "DuckDuckGo Privacy Pro": nonSandboxedExtraInputFiles, "Unit Tests": [ - .init("BinaryOwnershipCheckerTests.swift", .source), .init("BWEncryptionTests.swift", .source), .init("WKWebViewPrivateMethodsAvailabilityTests.swift", .source) ], diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index cf68b2bc23..ff6f41d3e3 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: "202.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "202.1.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), .package(path: "../Freemium"), diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 05b4d8247e..b79450e207 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -32,7 +32,7 @@ let package = Package( .library(name: "VPNAppLauncher", targets: ["VPNAppLauncher"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "202.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "202.1.0"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.3"), .package(path: "../AppLauncher"), .package(path: "../UDSHelper"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index f172f69593..7525673019 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: "202.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "202.1.0"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/PopoverMessageView.swift b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/PopoverMessageView.swift index 918aa9a01b..809af385ce 100644 --- a/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/PopoverMessageView.swift +++ b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/PopoverMessageView.swift @@ -86,7 +86,7 @@ public struct PopoverMessageView: View { .frame(minHeight: 22) .lineLimit(nil) .if(viewModel.shouldPresentMultiline) { view in - view.frame(width: 150, alignment: .leading) + view.frame(width: 160, alignment: .leading) } if let text = viewModel.buttonText, diff --git a/UnitTests/Updates/BinaryOwnershipCheckerTests.swift b/UnitTests/Updates/BinaryOwnershipCheckerTests.swift deleted file mode 100644 index 76a8029c2f..0000000000 --- a/UnitTests/Updates/BinaryOwnershipCheckerTests.swift +++ /dev/null @@ -1,87 +0,0 @@ -// -// BinaryOwnershipCheckerTests.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 BrowserServicesKit -import XCTest - -@testable import DuckDuckGo_Privacy_Browser - -class BinaryOwnershipCheckerTests: XCTestCase { - - func testWhenUserIsOwner_ThenIsCurrentUserOwnerReturnsTrue() { - let mockFileManager = MockFileManager() - mockFileManager.attributes = [ - .ownerAccountID: NSNumber(value: getuid()) - ] - let checker = BinaryOwnershipChecker(fileManager: mockFileManager) - let isOwner = checker.isCurrentUserOwner() - - XCTAssertTrue(isOwner, "Expected the current user to be identified as the owner.") - } - - func testWhenUserIsNotOwner_ThenIsCurrentUserOwnerReturnsFalse() { - let mockFileManager = MockFileManager() - mockFileManager.attributes = [ - .ownerAccountID: NSNumber(value: getuid() + 1) // Simulate a different user - ] - let checker = BinaryOwnershipChecker(fileManager: mockFileManager) - let isOwner = checker.isCurrentUserOwner() - - XCTAssertFalse(isOwner, "Expected the current user not to be identified as the owner.") - } - - func testWhenFileManagerThrowsError_ThenIsCurrentUserOwnerReturnsFalse() { - let mockFileManager = MockFileManager() - mockFileManager.shouldThrowError = true - let checker = BinaryOwnershipChecker(fileManager: mockFileManager) - let isOwner = checker.isCurrentUserOwner() - - XCTAssertFalse(isOwner, "Expected the ownership check to fail and return false when an error occurs.") - } - - func testWhenOwnershipIsCheckedMultipleTimes_ThenResultIsCached() { - let mockFileManager = MockFileManager() - mockFileManager.attributes = [ - .ownerAccountID: NSNumber(value: getuid()) - ] - let checker = BinaryOwnershipChecker(fileManager: mockFileManager) - let isOwnerFirstCheck = checker.isCurrentUserOwner() - - mockFileManager.attributes = [ - .ownerAccountID: NSNumber(value: getuid() + 1) - ] - let isOwnerSecondCheck = checker.isCurrentUserOwner() - - XCTAssertTrue(isOwnerFirstCheck, "Expected the current user to be identified as the owner on first check.") - XCTAssertTrue(isOwnerSecondCheck, "Expected the cached result to be used, so the second check should return the same result as the first.") - } -} - -// Mock FileManager to simulate different file attributes and errors -class MockFileManager: FileManager { - - var attributes: [FileAttributeKey: Any]? - var shouldThrowError = false - - override func attributesOfItem(atPath path: String) throws -> [FileAttributeKey: Any] { - if shouldThrowError { - throw NSError(domain: NSCocoaErrorDomain, code: NSFileReadNoSuchFileError, userInfo: nil) - } - return attributes ?? [:] - } -} From b8ba6421c1000b42bed4545ea4b7251a52aebd4b Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 31 Oct 2024 21:09:02 +0100 Subject: [PATCH 15/22] Set version_check_wait_retry_limit to 1 (#3488) Task/Issue URL: https://app.asana.com/0/1203301625297703/1208664774795332/f --- fastlane/Fastfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 044c6099cf..61158943f5 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -161,7 +161,7 @@ platform :mac do deliver(common_deliver_arguments(options).merge({ skip_binary_upload: true, skip_metadata: false, - version_check_wait_retry_limit: 0 + version_check_wait_retry_limit: 1 })) end From fbdb8a5f155c6907aa862260520433621ee664a1 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Fri, 1 Nov 2024 01:34:08 +0500 Subject: [PATCH 16/22] Fix crash when opening permission popover for NewTab page address bar (#3484) Task/Issue URL: https://app.asana.com/0/1202406491309510/1208667754274335/f Description: Fixes crash when permission requested with NewTab address bar experiment caused by permission popover tried to be presented on the NewTab page address bar --- DuckDuckGo/Common/Extensions/NSViewExtension.swift | 6 ++++++ .../Extensions/WKWebViewConfigurationExtensions.swift | 2 +- .../View/AddressBarButtonsViewController.swift | 6 +++--- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/DuckDuckGo/Common/Extensions/NSViewExtension.swift b/DuckDuckGo/Common/Extensions/NSViewExtension.swift index 2e81660afc..c17edf489f 100644 --- a/DuckDuckGo/Common/Extensions/NSViewExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSViewExtension.swift @@ -85,6 +85,12 @@ extension NSView { set { isHidden = !newValue } } + var isVisible: Bool { + guard !isHiddenOrHasHiddenAncestor, + let window, window.isVisible else { return false } + return true + } + func makeMeFirstResponder() { guard let window = window else { Logger.general.error("\(self.className): Window not available") diff --git a/DuckDuckGo/Common/Extensions/WKWebViewConfigurationExtensions.swift b/DuckDuckGo/Common/Extensions/WKWebViewConfigurationExtensions.swift index 090e9c3121..9a89e51e4f 100644 --- a/DuckDuckGo/Common/Extensions/WKWebViewConfigurationExtensions.swift +++ b/DuckDuckGo/Common/Extensions/WKWebViewConfigurationExtensions.swift @@ -119,7 +119,7 @@ extension NSPopover { // https://app.asana.com/0/1201037661562251/1206407295280737/f @objc(swizzled_showRelativeToRect:ofView:preferredEdge:) private dynamic func swizzled_show(relativeTo positioningRect: NSRect, of positioningView: NSView, preferredEdge: NSRectEdge) { - if positioningView.superview == nil { + if positioningView.window == nil { var observer: Cancellable? observer = positioningView.observe(\.window) { positioningView, _ in if positioningView.window != nil { diff --git a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift index 44e6cf951e..63766e5838 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift @@ -61,7 +61,7 @@ final class AddressBarButtonsViewController: NSViewController { @IBOutlet weak var imageButtonWrapper: NSView! @IBOutlet weak var imageButton: NSButton! @IBOutlet weak var clearButton: NSButton! - @IBOutlet weak var buttonsContainer: NSStackView! + @IBOutlet private weak var buttonsContainer: NSStackView! @IBOutlet weak var animationWrapperView: NSView! var trackerAnimationView1: LottieAnimationView! @@ -72,7 +72,7 @@ final class AddressBarButtonsViewController: NSViewController { @IBOutlet weak var notificationAnimationView: NavigationBarBadgeAnimationView! - @IBOutlet weak var permissionButtons: NSView! + @IBOutlet private weak var permissionButtons: NSView! @IBOutlet weak var cameraButton: PermissionButton! { didSet { cameraButton.isHidden = true @@ -368,7 +368,7 @@ final class AddressBarButtonsViewController: NSViewController { return } } - guard button.isShown, permissionButtons.isShown else { return } + guard button.isVisible else { return } (popover.contentViewController as? PermissionAuthorizationViewController)?.query = query popover.show(relativeTo: button.bounds, of: button, preferredEdge: .maxY) From 8c54db2a24316ede8f3be7c1f796a2c059477b97 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Fri, 1 Nov 2024 05:22:49 +0000 Subject: [PATCH 17/22] Bump version to 1.112.0 (294) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 9ba329c56c..458552a5ee 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 293 +CURRENT_PROJECT_VERSION = 294 From 9280027bf0ee6860efefe16eaf01e19538396737 Mon Sep 17 00:00:00 2001 From: Thom Espach Date: Fri, 1 Nov 2024 11:41:24 +0000 Subject: [PATCH 18/22] Bug Fix: Phishing Detection Dataset Discrepancies (#3440) Task/Issue URL: https://app.asana.com/0/1204023833050360/1208567121137949/f Tech Design URL: CC: **Description**: In [Implement desktop integration efficacy tests - 5-7 days](https://app.asana.com/0/1207943168535188/1207205745934704/f) it was discovered that Swift's client-side caching results in out-of-date datasets and significant dataset discrepancies between different clients. For example, it's very common for the same request to return different results from the backend, resulting in a client believing they are updating to a newer revision than they are. Over time, this compounds and results in disparate versions of the same dataset across different clients, putting users at risk of landing on newer phishing pages. Fix: - Remove Client Side Caching in PhishingDetectionClient.swift - Ensure embedded dataset is used to replace the on-disk dataset when the revision of the embedded dataset > on disk dataset **Steps to test this PR**: 1. Check unit tests 3. Change on-disk revision: 4. `echo "1650000" > "/System/Volumes/Data/Users//Library/Application Support/com.duckduckgo.macos.browser.debug/revision.txt"` 5. Build the browser 6. Visit https://privacy-test-pages.site/security/badware/phishing.html 7. Ensure blocked 8. Check on-disk revision: 9. `cat "/System/Volumes/Data/Users//Library/Application Support/com.duckduckgo.macos.browser.debug/revision.txt"` 10. Should be > 1650000 **Definition of Done**: * [ ] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/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) --- 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 f4d4acb3db..cb8f57f37b 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -14684,7 +14684,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 202.1.0; + version = 202.2.0; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 150832d719..767783c47a 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" : "8a1bc5526e14c589ca2cc74e6e7d125952b79bc1", - "version" : "202.1.0" + "revision" : "d39d04cf36b8522f894eebc3e11ee5fe65d880fa", + "version" : "202.2.0" } }, { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index ff6f41d3e3..d651815a37 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: "202.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "202.2.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), .package(path: "../Freemium"), diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index b79450e207..ffbefbec02 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -32,7 +32,7 @@ let package = Package( .library(name: "VPNAppLauncher", targets: ["VPNAppLauncher"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "202.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "202.2.0"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.3"), .package(path: "../AppLauncher"), .package(path: "../UDSHelper"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 7525673019..cc4e284c38 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: "202.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "202.2.0"), .package(path: "../SwiftUIExtensions") ], targets: [ From ef1ca984f6e62453da8e62c66ee9ae2046609b17 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 1 Nov 2024 13:36:49 +0100 Subject: [PATCH 19/22] Update to subscription cookie (#3489) Task/Issue URL: https://app.asana.com/0/1108686900785972/1208264562025859/f **Description**: Initial PR -> https://github.com/duckduckgo/macos-browser/pull/3458 Update to how the subscription cookie operates: - constraint the cookie to `subscriptions.duckduckgo.com` - on sign out do not fully remove the cookie - just clear the value - gate the feature behind the `setAccessTokenCookieForSubscriptionDomains` privacy config feature flag --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 +-- DuckDuckGo/Application/AppDelegate.swift | 28 ++++++++++++++++++- ...riptionCookieManageEventPixelMapping.swift | 25 +++++++---------- .../Tab/Services/WebsiteDataStore.swift | 3 +- .../DataBrokerProtection/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 8 files changed, 45 insertions(+), 23 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index a3acccb424..a7737cf420 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -14510,7 +14510,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 201.0.0; + version = "201.0.0-1"; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 51a522e596..3d5c0df68d 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" : "e5946eee6af859690cc1cc5e51daef3c8368981b", - "version" : "201.0.0" + "revision" : "9506581ae99273681073f9993fc6d881d3edaa7f", + "version" : "201.0.0-1" } }, { diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index a3ef983fee..8546ec81b1 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -96,7 +96,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate { public let subscriptionManager: SubscriptionManager public let subscriptionUIHandler: SubscriptionUIHandling - public let subscriptionCookieManager: SubscriptionCookieManaging + private let subscriptionCookieManager: SubscriptionCookieManaging + private var subscriptionCookieManagerFeatureFlagCancellable: AnyCancellable? public let vpnSettings = VPNSettings(defaults: .netP) @@ -302,6 +303,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { vpnUninstaller: vpnUninstaller) } + // swiftlint:disable:next cyclomatic_complexity func applicationDidFinishLaunching(_ notification: Notification) { guard NSApp.runType.requiresEnvironment else { return } defer { @@ -344,6 +346,30 @@ final class AppDelegate: NSObject, NSApplicationDelegate { subscriptionManager.loadInitialData() + let privacyConfigurationManager = ContentBlocking.shared.privacyConfigurationManager + + // Enable subscriptionCookieManager if feature flag is present + if privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.setAccessTokenCookieForSubscriptionDomains) { + subscriptionCookieManager.enableSettingSubscriptionCookie() + } + + // Keep track of feature flag changes + subscriptionCookieManagerFeatureFlagCancellable = privacyConfigurationManager.updatesPublisher + .sink { [weak self, weak privacyConfigurationManager] in + guard let self, let privacyConfigurationManager else { return } + + let isEnabled = privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.setAccessTokenCookieForSubscriptionDomains) + + Task { [weak self] in + if isEnabled { + self?.subscriptionCookieManager.enableSettingSubscriptionCookie() + await self?.subscriptionCookieManager.refreshSubscriptionCookie() + } else { + await self?.subscriptionCookieManager.disableSettingSubscriptionCookie() + } + } + } + if [.normal, .uiTests].contains(NSApp.runType) { stateRestorationManager.applicationDidFinishLaunching() } diff --git a/DuckDuckGo/Subscription/SubscriptionCookieManageEventPixelMapping.swift b/DuckDuckGo/Subscription/SubscriptionCookieManageEventPixelMapping.swift index c18cf2e6d4..fc5128756d 100644 --- a/DuckDuckGo/Subscription/SubscriptionCookieManageEventPixelMapping.swift +++ b/DuckDuckGo/Subscription/SubscriptionCookieManageEventPixelMapping.swift @@ -24,21 +24,18 @@ import Subscription enum SubscriptionCookieManagerPixel: PixelKitEventV2 { case missingTokenOnSignIn - case missingCookieOnSignOut - case cookieRefreshedWithUpdate - case cookieRefreshedWithDelete + case cookieRefreshedWithAccessToken + case cookieRefreshedWithEmptyValue case failedToSetSubscriptionCookie var name: String { switch self { case .missingTokenOnSignIn: return "m_mac_privacy-pro_subscription-cookie-missing_token_on_sign_in" - case .missingCookieOnSignOut: - return "m_mac_privacy-pro_subscription-cookie-missing_cookie_on_sign_out" - case .cookieRefreshedWithUpdate: - return "m_mac_privacy-pro_subscription-cookie-refreshed_with_update" - case .cookieRefreshedWithDelete: - return "m_mac_privacy-pro_subscription-cookie-refreshed_with_delete" + case .cookieRefreshedWithAccessToken: + return "m_mac_privacy-pro_subscription-cookie-refreshed_with_access_token" + case .cookieRefreshedWithEmptyValue: + return "m_mac_privacy-pro_subscription-cookie-refreshed_with_empty_value" case .failedToSetSubscriptionCookie: return "m_mac_privacy-pro_subscription-cookie-failed_to_set_subscription_cookie" } @@ -61,12 +58,10 @@ public final class SubscriptionCookieManageEventPixelMapping: EventMapping Date: Fri, 1 Nov 2024 13:03:00 +0000 Subject: [PATCH 20/22] Bump version to 1.112.0 (295) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 458552a5ee..58a5e5422c 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 294 +CURRENT_PROJECT_VERSION = 295 From 4de7c6c96e5bc60ad872fa71c7d873be6c4bc14a Mon Sep 17 00:00:00 2001 From: Tom Strba <57389842+tomasstrba@users.noreply.github.com> Date: Fri, 1 Nov 2024 16:03:40 +0100 Subject: [PATCH 21/22] Marking latest Bitwarden versions as incompatible (#3492) Task/Issue URL: https://app.asana.com/0/1148564399326804/1208536171028711/f **Description**: [Marking latest Bitwarden versions as incompatible](https://github.com/duckduckgo/macos-browser/commit/e491edb37391bf2d72d8ba677cb13e2305e92d34) --- DuckDuckGo/Common/Localizables/UserText.swift | 4 +- DuckDuckGo/Localizable.xcstrings | 96 ++++++++++++------- .../Services/BWInstallationService.swift | 2 +- .../View/PreferencesAutofillView.swift | 2 +- 4 files changed, 64 insertions(+), 40 deletions(-) diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index aeb44a6e48..d87d504fb0 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -949,8 +949,8 @@ struct UserText { static let bitwardenError = NSLocalizedString("bitwarden.error", value: "Unable to find or connect to Bitwarden", comment: "This message appears when the application is unable to find or connect to Bitwarden, indicating a connection issue.") static let bitwardenNotInstalled = NSLocalizedString("bitwarden.not.installed", value: "Bitwarden app is not installed", comment: "") static let bitwardenOldVersion = NSLocalizedString("bitwarden.old.version", value: "Please update Bitwarden to the latest version", comment: "Message that warns user they need to update their password manager Bitwarden app vesion") - static let bitwardenIncompatible = NSLocalizedString("bitwarden.incompatible", value: "The following Bitwarden versions are incompatible with DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. Please update to a newer version by following these steps:", comment: "Message that warns user that specific Bitwarden app vesions are not compatible with this app") - static let bitwardenIncompatibleStep1 = NSLocalizedString("bitwarden.incompatible.step.1", value: "Download v2024.4.3", comment: "First step to downgrade Bitwarden") + static let bitwardenIncompatible = NSLocalizedString("bitwarden.incompatible", value: "The following Bitwarden versions are incompatible with DuckDuckGo: v2024.10.0, v2024.10.1, v2024.10.2. Please downgrade to an older version by following these steps:", comment: "Message that warns user that specific Bitwarden app vesions are not compatible with this app") + static let bitwardenIncompatibleStep1 = NSLocalizedString("bitwarden.incompatible.step.1", value: "Download v2024.9.0", comment: "First step to downgrade Bitwarden") static let bitwardenIncompatibleStep2 = NSLocalizedString("bitwarden.incompatible.step.2", value: "2. Open the downloaded DMG file and drag the Bitwarden application to\nthe /Applications folder.", comment: "Second step to downgrade Bitwarden") static let bitwardenIntegrationNotApproved = NSLocalizedString("bitwarden.integration.not.approved", value: "Integration with DuckDuckGo is not approved in Bitwarden app", comment: "While the user tries to connect the DuckDuckGo Browser to password manager Bitwarden This message indicates that the integration with DuckDuckGo has not been approved in the Bitwarden app.") static let bitwardenMissingHandshake = NSLocalizedString("bitwarden.missing.handshake", value: "Missing handshake", comment: "While the user tries to connect the DuckDuckGo Browser to password manager Bitwarden This message indicates a missing handshake (a way for two devices or systems to say hello to each other and agree to communicate or exchange information).") diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 0d45c59159..c00d38bc46 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -7612,55 +7612,55 @@ "localizations" : { "de" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "The following Bitwarden versions are incompatible with DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. Please update to a newer version by following these steps:" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "The following Bitwarden versions are incompatible with DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. Please update to a newer version by following these steps:" + "value" : "The following Bitwarden versions are incompatible with DuckDuckGo: v2024.10.0, v2024.10.1, v2024.10.2. Please downgrade to an older version by following these steps:" } }, "es" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "The following Bitwarden versions are incompatible with DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. Please update to a newer version by following these steps:" } }, "fr" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "The following Bitwarden versions are incompatible with DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. Please update to a newer version by following these steps:" } }, "it" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "The following Bitwarden versions are incompatible with DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. Please update to a newer version by following these steps:" } }, "nl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "The following Bitwarden versions are incompatible with DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. Please update to a newer version by following these steps:" } }, "pl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "The following Bitwarden versions are incompatible with DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. Please update to a newer version by following these steps:" } }, "pt" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "The following Bitwarden versions are incompatible with DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. Please update to a newer version by following these steps:" } }, "ru" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "The following Bitwarden versions are incompatible with DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. Please update to a newer version by following these steps:" } } @@ -7672,55 +7672,55 @@ "localizations" : { "de" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Download v2024.4.3" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "Download v2024.4.3" + "value" : "Download v2024.9.0" } }, "es" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Download v2024.4.3" } }, "fr" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Download v2024.4.3" } }, "it" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Download v2024.4.3" } }, "nl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Download v2024.4.3" } }, "pl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Download v2024.4.3" } }, "pt" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Download v2024.4.3" } }, "ru" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Download v2024.4.3" } } @@ -38787,55 +38787,55 @@ "localizations" : { "de" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Du bist bereit! Du kannst mich jederzeit im Dock antreffen.\nMöchtest du sehen, wie ich dich beschütze? Versuche, eine deiner Lieblingsseiten zu besuchen 👆\n\nBehalte die Adressleiste im Auge. Ich werde Tracker blockieren und die Sicherheit deiner Verbindung verbessern, wenn möglichu{00A0}🔒" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "You’re all set! You can find me hanging out in the Dock anytime.\n\nWant to see how I protect you? Try visiting one of your favorite sites 👆\n\nKeep watching the address bar as you go. I’ll be blocking trackers and upgrading the security of your connection when possibleu{00A0}🔒" + "value" : "You’re all set! You can find me hanging out in the Dock anytime.\n\nWant to see how I protect you? Try visiting one of your favorite sites 👆\n\nKeep watching the address bar as you go. I’ll be blocking trackers and upgrading the security of your connection when possible 🔒" } }, "es" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "¡Ya está todo listo! Puedes encontrarme en el Dock en cualquier momento.\n¿Quieres ver cómo te protejo? Prueba a visitar uno de tus sitios favoritos 👆\n\nNo pierdas de vista la barra de direcciones al navegar. Bloquearé los rastreadores y mejoraré la seguridad de tu conexión cuando sea posible{00A0}🔒" } }, "fr" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Tout est prêt ! Vous pouvez me trouver sur le Dock à tout moment.\nVous voulez voir comment je vous protège ? Essayez de visiter l'un de vos sites préférés 👆\n\nContinuez à regarder la barre d'adresse au fur et à mesure. Je bloquerai les traqueurs et mettrai à niveau la sécurité de votre connexion si possible 🔒" } }, "it" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Tutto pronto! Puoi trovarmi nel dock in qualsiasi momento.\nVuoi vedere come ti proteggo? Prova a visitare uno dei tuoi siti preferiti 👆\n\nContinua a controllare la barra degli indirizzi mentre esplori. Bloccherò i sistemi di tracciamento e aggiornerò la sicurezza della tua connessione quando possibile{00A0} 🔒" } }, "nl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Je bent helemaal klaar! Je kunt me altijd vinden in het Dock.\nWil je zien hoe ik je bescherm? Ga eens naar een van je favoriete websites 👆\n\nKijk tijdens het surfen goed naar de adresbalk. Ik blokkeer trackers en werk de beveiliging van je verbinding bij wanneer mogelijk 🔒" } }, "pl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Wszystko gotowe! W każdej chwili możesz mnie znaleźć w Docku.\nChcesz zobaczyć, jak Cię chronię? Spróbuj odwiedzić jedną z ulubionych stron 👆\n\nW międzyczasie obserwuj pasek adresu. Będę blokować mechanizmy śledzące i w miarę możliwości poprawiać bezpieczeństwo połączenia 🔒" } }, "pt" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Está tudo pronto! Podes encontrar-me na Dock em qualquer altura.\nQueres ver como te protejo? Experimenta visitar um dos teus sites favoritos 👆\n\nContinua a observar a barra de endereço à medida que vais avançando. Vou bloquear os rastreadores e melhorar a segurança da tua ligação sempre que possível 🔒" } }, "ru" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Готово! Теперь меня всегда можно найти на док-панели.\nВам интересно, как я защищаю вашу конфиденциальность? Зайдите на свой любимый сайт...👆\n\nИ следите за адресной строкой. По возможности я заблокирую все трекеры и сделаю соединение более безопасным {00A0}🔒" } } @@ -38847,55 +38847,55 @@ "localizations" : { "de" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Du bist bereit!\n\nMöchtest du sehen, wie ich dich beschütze? Versuche, eine deiner Lieblingsseiten zu besuchen 👆\n\nBehalte die Adressleiste im Auge. Ich werde Tracker blockieren und die Sicherheit deiner Verbindung verbessern, wenn möglich 🔒" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "You’re all set!\n\nWant to see how I protect you? Try visiting one of your favorite sites 👆\n\nKeep watching the address bar as you go. I’ll be blocking trackers and upgrading the security of your connection when possibleu{00A0}🔒" + "value" : "You’re all set!\n\nWant to see how I protect you? Try visiting one of your favorite sites 👆\n\nKeep watching the address bar as you go. I’ll be blocking trackers and upgrading the security of your connection when possible 🔒" } }, "es" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "¡Ya está todo listo!\n\n¿Quieres ver cómo te protejo? Prueba a visitar uno de tus sitios favoritos 👆\n\nSigue viendo la barra de direcciones sobre la marcha. Bloquearé los rastreadores y mejoraré la seguridad de tu conexión cuando sea posible 🔒" } }, "fr" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Tout est prêt !\n\nVous voulez voir comment je vous protège ? Essayez de visiter l'un de vos sites préférés 👆\n\n Continuez à regarder la barre d'adresse au fur et à mesure. Je bloquerai les traqueurs et mettrai à niveau la sécurité de votre connexion si possible 🔒" } }, "it" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Tutto pronto.\n\nVuoi vedere come ti proteggo? Prova a visitare uno dei tuoi siti preferiti 👆Continua a controllare la barra degli indirizzi mentre esplori. Bloccherò i sistemi di tracciamento e aggiornerò la sicurezza della tua connessione quando possibile 🔒" } }, "nl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Je bent helemaal klaar!\n\nWil je zien hoe ik je bescherm? Ga eens naar een van je favoriete websites 👆 \n\n Kijk tijdens het surfen goed naar de adresbalk. Ik blokkeer trackers en werk de beveiliging van je verbinding bij wanneer mogelijk 🔒" } }, "pl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Wszystko gotowe!\n\nChcesz zobaczyć, jak Cię chronię? Spróbuj odwiedzić jedną z ulubionych stron 👆\n\nW międzyczasie obserwuj pasek adresu. Będę blokować mechanizmy śledzące i w miarę możliwości poprawiać bezpieczeństwo połączenia 🔒" } }, "pt" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Estás tudo pronto!\n\nQueres ver como te protejo? Experimenta visitar um dos teus sites favoritos 👆\n\nContinua a observar a barra de endereço à medida que vais avançando. Vou bloquear os rastreadores e melhorar a segurança da tua ligação sempre que possível 🔒" } }, "ru" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Все готово!\n\nХотите увидеть, как я вас защищаю? Зайдите на один из любимых сайтов 👆\n\nСледите за адресной строкой. Я по возможности буду блокировать трекеры и обеспечивать вам более безопасное соединение 🔒" } } @@ -44206,6 +44206,18 @@ } } }, + "pinning.hide-aichat-shortcut" : { + "comment" : "Menu item for hiding the AI Chat shortcut", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Hide AI Chat Shortcut" + } + } + } + }, "pinning.hide-autofill-shortcut" : { "comment" : "Menu item for hiding the passwords shortcut", "extractionState" : "extracted_with_value", @@ -44446,6 +44458,18 @@ } } }, + "pinning.show-aichat-shortcut" : { + "comment" : "Menu item for showing the AI Chat shortcut", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Show AI Chat Shortcut" + } + } + } + }, "pinning.show-autofill-shortcut" : { "comment" : "Menu item for showing the passwords shortcut", "extractionState" : "extracted_with_value", diff --git a/DuckDuckGo/PasswordManager/Bitwarden/Services/BWInstallationService.swift b/DuckDuckGo/PasswordManager/Bitwarden/Services/BWInstallationService.swift index a1184661bc..774c111a41 100644 --- a/DuckDuckGo/PasswordManager/Bitwarden/Services/BWInstallationService.swift +++ b/DuckDuckGo/PasswordManager/Bitwarden/Services/BWInstallationService.swift @@ -42,7 +42,7 @@ final class LocalBitwardenInstallationService: BWInstallationService { static var bundlePath = "/Applications/Bitwarden.app" private lazy var bundleUrl = URL(fileURLWithPath: Self.bundlePath) static var minimumVersion = "2022.10.1" - static var incompatibleVersions = ["2024.3.0", "2024.3.2", "2024.4.0", "2024.4.1"] + static var incompatibleVersions = ["2024.3.0", "2024.3.2", "2024.4.0", "2024.4.1", "2024.10.0", "2024.10.1", "2024.10.2"] private lazy var manifestPath: String = { #if DEBUG diff --git a/DuckDuckGo/Preferences/View/PreferencesAutofillView.swift b/DuckDuckGo/Preferences/View/PreferencesAutofillView.swift index 83f705001e..0558a4a211 100644 --- a/DuckDuckGo/Preferences/View/PreferencesAutofillView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesAutofillView.swift @@ -357,7 +357,7 @@ struct BitwardenDowngradeInfoView: View, PreferencesTabOpening { HStack { Text("1.") Button(UserText.bitwardenIncompatibleStep1, action: { - openNewTab(with: URL(string: "https://github.com/bitwarden/clients/releases/download/desktop-v2024.4.3/Bitwarden-2024.4.3-universal.dmg")!) + openNewTab(with: URL(string: "https://github.com/bitwarden/clients/releases/download/desktop-v2024.9.0/Bitwarden-2024.9.0-universal.dmg")!) }).foregroundColor(.accentColor) } Text(UserText.bitwardenIncompatibleStep2) From 59ed00f5ebbcbdff09802330bb70ba61a0921729 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Fri, 1 Nov 2024 15:31:38 +0000 Subject: [PATCH 22/22] Bump version to 1.112.0 (296) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 58a5e5422c..ba0dd6986e 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 295 +CURRENT_PROJECT_VERSION = 296