From ce22955fc78835069fcb2c0c678e87b8c167844d Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Tue, 7 Nov 2023 09:38:28 +0000 Subject: [PATCH 01/16] Adding 'protectionsState' to breakage form submission (#2120) Co-authored-by: Shane Osbourne --- DuckDuckGo/BrokenSiteInfo.swift | 16 +++++++++++++--- DuckDuckGo/TabViewController.swift | 8 ++++++-- DuckDuckGoTests/BrokenSiteReportingTests.swift | 1 + 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/DuckDuckGo/BrokenSiteInfo.swift b/DuckDuckGo/BrokenSiteInfo.swift index ed96e5e460..063784c78f 100644 --- a/DuckDuckGo/BrokenSiteInfo.swift +++ b/DuckDuckGo/BrokenSiteInfo.swift @@ -24,6 +24,11 @@ public struct BrokenSiteInfo { static let allowedQueryReservedCharacters = CharacterSet(charactersIn: ",") + enum ProtectionsState: String { + case enabled = "1" + case disabled = "0" + } + private struct Keys { static let url = "siteUrl" static let category = "category" @@ -40,6 +45,7 @@ public struct BrokenSiteInfo { static let gpc = "gpc" static let ampUrl = "ampUrl" static let urlParametersRemoved = "urlParametersRemoved" + static let protectionsState = "protectionsState" } private let url: URL? @@ -54,12 +60,14 @@ public struct BrokenSiteInfo { private let manufacturer: String private let systemVersion: String private let gpc: Bool - + private let protectionsState: ProtectionsState + public init(url: URL?, httpsUpgrade: Bool, blockedTrackerDomains: [String], installedSurrogates: [String], isDesktop: Bool, tdsETag: String?, ampUrl: String?, urlParametersRemoved: Bool, + protected: Bool, model: String = UIDevice.current.model, manufacturer: String = "Apple", systemVersion: String = UIDevice.current.systemVersion, @@ -76,7 +84,8 @@ public struct BrokenSiteInfo { self.model = model self.manufacturer = manufacturer self.systemVersion = systemVersion - + self.protectionsState = protected ? .enabled : .disabled + if let gpcParam = gpc { self.gpc = gpcParam } else { @@ -101,7 +110,8 @@ public struct BrokenSiteInfo { Keys.model: model, Keys.gpc: gpc ? "true" : "false", Keys.ampUrl: ampUrl ?? "", - Keys.urlParametersRemoved: urlParametersRemoved ? "true" : "false" + Keys.urlParametersRemoved: urlParametersRemoved ? "true" : "false", + Keys.protectionsState: protectionsState.rawValue ] Pixel.fire(pixel: .brokenSiteReport, diff --git a/DuckDuckGo/TabViewController.swift b/DuckDuckGo/TabViewController.swift index 04a448e738..5e4da0f41b 100644 --- a/DuckDuckGo/TabViewController.swift +++ b/DuckDuckGo/TabViewController.swift @@ -866,7 +866,10 @@ class TabViewController: UIViewController { public func getCurrentWebsiteInfo() -> BrokenSiteInfo { let blockedTrackerDomains = privacyInfo?.trackerInfo.trackersBlocked.compactMap { $0.domain } ?? [] - + + let configuration = ContentBlocking.shared.privacyConfigurationManager.privacyConfig + let protected = configuration.isFeature(.contentBlocking, enabledForDomain: url?.host) + return BrokenSiteInfo(url: url, httpsUpgrade: httpsForced, blockedTrackerDomains: blockedTrackerDomains, @@ -874,7 +877,8 @@ class TabViewController: UIViewController { isDesktop: tabModel.isDesktop, tdsETag: ContentBlocking.shared.contentBlockingManager.currentMainRules?.etag ?? "", ampUrl: linkProtection.lastAMPURLString, - urlParametersRemoved: linkProtection.urlParametersRemoved) + urlParametersRemoved: linkProtection.urlParametersRemoved, + protected: protected) } public func print() { diff --git a/DuckDuckGoTests/BrokenSiteReportingTests.swift b/DuckDuckGoTests/BrokenSiteReportingTests.swift index b36c95b240..82b3d25c2a 100644 --- a/DuckDuckGoTests/BrokenSiteReportingTests.swift +++ b/DuckDuckGoTests/BrokenSiteReportingTests.swift @@ -75,6 +75,7 @@ final class BrokenSiteReportingTests: XCTestCase { tdsETag: test.blocklistVersion, ampUrl: nil, urlParametersRemoved: false, + protected: true, model: test.model ?? "", manufacturer: test.manufacturer ?? "", systemVersion: test.os ?? "", From 1c19d062e591979873b353b8136cd13bf585488c Mon Sep 17 00:00:00 2001 From: Christopher Brind Date: Tue, 7 Nov 2023 12:50:24 +0000 Subject: [PATCH 02/16] kill variant when receiving atb update (#2130) --- Core/StatisticsLoader.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Core/StatisticsLoader.swift b/Core/StatisticsLoader.swift index e852d70540..8b7a6e481c 100644 --- a/Core/StatisticsLoader.swift +++ b/Core/StatisticsLoader.swift @@ -134,6 +134,7 @@ public class StatisticsLoader { public func storeUpdateVersionIfPresent(_ atb: Atb) { if let updateVersion = atb.updateVersion { statisticsStore.atb = updateVersion + statisticsStore.variant = nil returnUserMeasurement.updateStoredATB(atb) } } From c0c34179e870dbd64cc4c6901ff6f4b36328fc72 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Tue, 7 Nov 2023 13:54:12 +0100 Subject: [PATCH 03/16] Fix syncing empty favorites folders (#2121) Task/Issue URL: https://app.asana.com/0/414235014887631/1205843304285892/f Description: Update bookmarks sync response handler to actually process favorites also when an empty folder is received from the server. --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index e49528f6ea..f35d46135c 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -9058,7 +9058,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 82.2.0; + version = 82.2.1; }; }; C14882EB27F211A000D59F0C /* XCRemoteSwiftPackageReference "SwiftSoup" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index b7ebada7d8..d9ed11f7d0 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/DuckDuckGo/BrowserServicesKit", "state": { "branch": null, - "revision": "86e4aba326ce06585b842ab13023ce08f86ac424", - "version": "82.2.0" + "revision": "0ac6d8e2153bec4ddd4e983915da6db09fcbed05", + "version": "82.2.1" } }, { From 910a4f8e53332e1010687afe8b16e09bd55fc3e4 Mon Sep 17 00:00:00 2001 From: Christopher Brind Date: Tue, 7 Nov 2023 13:51:05 +0000 Subject: [PATCH 04/16] fix favorite launch with keyboard bug (#2131) Task/Issue URL: https://app.asana.com/0/414709148257752/1205885847854331/f Tech Design URL: CC: Description: Fix bug that shows black bar when launching a favorite with the keyboard open. --- DuckDuckGo/MainViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index 2331fd1fb6..6dd1d6bb44 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -694,8 +694,8 @@ class MainViewController: UIViewController { allowContentUnderflow = false request() guard let tab = currentTab else { fatalError("no tab") } - select(tab: tab) dismissOmniBar() + select(tab: tab) } private func addTab(url: URL?, inheritedAttribution: AdClickAttributionLogic.State?) { From 81e555e467882d09a4b61081209000455a352556 Mon Sep 17 00:00:00 2001 From: Christopher Brind Date: Tue, 7 Nov 2023 19:33:56 +0000 Subject: [PATCH 05/16] re-enable keyboard shortcuts (#2132) Task/Issue URL: https://app.asana.com/0/414709148257752/1205886244765299/f Tech Design URL: CC: Description: Fix bug preventing keyboard shortcuts working correctly. --- DuckDuckGo/MainViewController+KeyCommands.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/DuckDuckGo/MainViewController+KeyCommands.swift b/DuckDuckGo/MainViewController+KeyCommands.swift index e1229209d2..f7fffded92 100644 --- a/DuckDuckGo/MainViewController+KeyCommands.swift +++ b/DuckDuckGo/MainViewController+KeyCommands.swift @@ -110,7 +110,13 @@ extension MainViewController { UIKeyCommand(title: "", action: #selector(keyboardEscape), input: UIKeyCommand.inputEscape, modifierFlags: []) ] - return [alwaysAvailable, browsingCommands, findInPageCommands, arrowKeys, other].flatMap { $0 } + let commands = [alwaysAvailable, browsingCommands, findInPageCommands, arrowKeys, other].flatMap { $0 } + if #available(iOS 15, *) { + commands.forEach { + $0.wantsPriorityOverSystemBehavior = true + } + } + return commands } @objc func keyboardMoveSelectionUp() { From 0f73f8a1e055d8a80eb4fd73875b82330a075b76 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Tue, 7 Nov 2023 19:38:22 -0800 Subject: [PATCH 06/16] Avoid AppTP DB initialization when disabled (#2090) Task/Issue URL: https://app.asana.com/0/1199333091098016/1205714053673490/f Tech Design URL: CC: Description: This PR disables AppTP database initialization when the feature is disabled. --- DuckDuckGo/AppDelegate.swift | 14 ++++++++++++ DuckDuckGo/HomeViewController.swift | 26 +++++++++++++++++----- DuckDuckGo/MainViewController.swift | 34 +++++++++++++++++++++++++++-- 3 files changed, 67 insertions(+), 7 deletions(-) diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index cc6917da35..d49f873e3d 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -62,7 +62,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { private lazy var privacyStore = PrivacyUserDefaults() private var bookmarksDatabase: CoreDataDatabase = BookmarksDatabase.make() + +#if APP_TRACKING_PROTECTION private var appTrackingProtectionDatabase: CoreDataDatabase = AppTrackingProtectionDatabase.make() +#endif + private var autoClear: AutoClear? private var showKeyboardIfSettingOn = true private var lastBackgroundDate: Date? @@ -183,6 +187,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { WidgetCenter.shared.reloadAllTimelines() } +#if APP_TRACKING_PROTECTION appTrackingProtectionDatabase.loadStore { context, error in guard context != nil else { if let error = error { @@ -199,6 +204,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } } } +#endif Favicons.shared.migrateFavicons(to: Favicons.Constants.maxFaviconSize) { WidgetCenter.shared.reloadAllTimelines() @@ -234,12 +240,20 @@ class AppDelegate: UIResponder, UIApplicationDelegate { syncService.initializeIfNeeded() self.syncService = syncService +#if APP_TRACKING_PROTECTION let main = MainViewController(bookmarksDatabase: bookmarksDatabase, bookmarksDatabaseCleaner: syncDataProviders.bookmarksAdapter.databaseCleaner, appTrackingProtectionDatabase: appTrackingProtectionDatabase, syncService: syncService, syncDataProviders: syncDataProviders, appSettings: AppDependencyProvider.shared.appSettings) +#else + let main = MainViewController(bookmarksDatabase: bookmarksDatabase, + bookmarksDatabaseCleaner: syncDataProviders.bookmarksAdapter.databaseCleaner, + syncService: syncService, + syncDataProviders: syncDataProviders, + appSettings: AppDependencyProvider.shared.appSettings) +#endif main.loadViewIfNeeded() window = UIWindow(frame: UIScreen.main.bounds) diff --git a/DuckDuckGo/HomeViewController.swift b/DuckDuckGo/HomeViewController.swift index f938dca3c1..4f97f8c46b 100644 --- a/DuckDuckGo/HomeViewController.swift +++ b/DuckDuckGo/HomeViewController.swift @@ -69,6 +69,7 @@ class HomeViewController: UIViewController { private let appTPHomeViewModel: AppTPHomeViewModel #endif +#if APP_TRACKING_PROTECTION static func loadFromStoryboard(model: Tab, favoritesViewModel: FavoritesListInteracting, appTPDatabase: CoreDataDatabase) -> HomeViewController { let storyboard = UIStoryboard(name: "Home", bundle: nil) let controller = storyboard.instantiateViewController(identifier: "HomeViewController", creator: { coder in @@ -76,18 +77,33 @@ class HomeViewController: UIViewController { }) return controller } - +#else + static func loadFromStoryboard(model: Tab, favoritesViewModel: FavoritesListInteracting) -> HomeViewController { + let storyboard = UIStoryboard(name: "Home", bundle: nil) + let controller = storyboard.instantiateViewController(identifier: "HomeViewController", creator: { coder in + HomeViewController(coder: coder, tabModel: model, favoritesViewModel: favoritesViewModel) + }) + return controller + } +#endif + +#if APP_TRACKING_PROTECTION required init?(coder: NSCoder, tabModel: Tab, favoritesViewModel: FavoritesListInteracting, appTPDatabase: CoreDataDatabase) { self.tabModel = tabModel self.favoritesViewModel = favoritesViewModel - -#if APP_TRACKING_PROTECTION self.appTPHomeViewModel = AppTPHomeViewModel(appTrackingProtectionDatabase: appTPDatabase) -#endif super.init(coder: coder) } - +#else + required init?(coder: NSCoder, tabModel: Tab, favoritesViewModel: FavoritesListInteracting) { + self.tabModel = tabModel + self.favoritesViewModel = favoritesViewModel + + super.init(coder: coder) + } +#endif + required init?(coder: NSCoder) { fatalError("Not implemented") } diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index 6dd1d6bb44..df77c8acc5 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -78,8 +78,11 @@ class MainViewController: UIViewController { let previewsSource = TabPreviewsSource() let appSettings: AppSettings private var launchTabObserver: LaunchTabNotification.Observer? - + +#if APP_TRACKING_PROTECTION private let appTrackingProtectionDatabase: CoreDataDatabase +#endif + let bookmarksDatabase: CoreDataDatabase private weak var bookmarksDatabaseCleaner: BookmarkDatabaseCleaner? private let favoritesViewModel: FavoritesListInteracting @@ -128,6 +131,7 @@ class MainViewController: UIViewController { var viewCoordinator: MainViewCoordinator! +#if APP_TRACKING_PROTECTION init( bookmarksDatabase: CoreDataDatabase, bookmarksDatabaseCleaner: BookmarkDatabaseCleaner, @@ -149,6 +153,27 @@ class MainViewController: UIViewController { bindSyncService() } +#else + init( + bookmarksDatabase: CoreDataDatabase, + bookmarksDatabaseCleaner: BookmarkDatabaseCleaner, + syncService: DDGSyncing, + syncDataProviders: SyncDataProviders, + appSettings: AppSettings + ) { + self.bookmarksDatabase = bookmarksDatabase + self.bookmarksDatabaseCleaner = bookmarksDatabaseCleaner + self.syncService = syncService + self.syncDataProviders = syncDataProviders + self.favoritesViewModel = FavoritesListViewModel(bookmarksDatabase: bookmarksDatabase) + self.bookmarksCachingSearch = BookmarksCachingSearch(bookmarksStore: CoreDataBookmarksSearchStore(bookmarksStore: bookmarksDatabase)) + self.appSettings = appSettings + + super.init(nibName: nil, bundle: nil) + + bindSyncService() + } +#endif fileprivate var tabCountInfo: TabCountInfo? @@ -554,10 +579,15 @@ class MainViewController: UIViewController { AppDependencyProvider.shared.homePageConfiguration.refresh() let tabModel = currentTab?.tabModel + +#if APP_TRACKING_PROTECTION let controller = HomeViewController.loadFromStoryboard(model: tabModel!, favoritesViewModel: favoritesViewModel, appTPDatabase: appTrackingProtectionDatabase) - +#else + let controller = HomeViewController.loadFromStoryboard(model: tabModel!, favoritesViewModel: favoritesViewModel) +#endif + homeController = controller controller.chromeDelegate = self From fbcd0d4639647136d1973b08fa4244d79e6b56e6 Mon Sep 17 00:00:00 2001 From: bwaresiak Date: Wed, 8 Nov 2023 12:12:54 +0100 Subject: [PATCH 07/16] Update test to match exact tracker (#2133) Task/Issue URL: https://app.asana.com/0/1205237866452338/1205902260676401/f Tech Design URL: CC: Description: Update URL in tests to reflect latest blocking rules. --- IntegrationTests/ContentBlockingRulesTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IntegrationTests/ContentBlockingRulesTests.swift b/IntegrationTests/ContentBlockingRulesTests.swift index a03e8bdcc6..723fa8bec8 100644 --- a/IntegrationTests/ContentBlockingRulesTests.swift +++ b/IntegrationTests/ContentBlockingRulesTests.swift @@ -33,10 +33,10 @@ class ContentBlockingRulesTests: XCTestCase { andTemporaryUnprotectedDomains: []) // Test tracker is set up to be blocked - if let rule = rules.findExactFilter(filter: "^(https?)?(wss?)?://([a-z0-9-]+\\.)*googleadservices\\.com(:?[0-9]+)?/.*") { + if let rule = rules.findExactFilter(filter: "^(https?)?(wss?)?://([a-z0-9-]+\\.)*bad\\.third-party\\.site(:?[0-9]+)?/.*") { XCTAssert(rule.action == .block()) } else { - XCTFail("Missing google ad services rule") + XCTFail("Missing tracking rule") } // Test exceptiions are set to ignore previous rules From 58a4021a81c1832f02d7facb17b50e71ddd9c6eb Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Wed, 8 Nov 2023 11:23:49 -0800 Subject: [PATCH 08/16] Update BSK for NetP change (#2134) Task/Issue URL: https://app.asana.com/0/0/1205909514194474/f Tech Design URL: CC: Description: This PR updates BSK to account for a NetP change. --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index f35d46135c..9930057c0b 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -9058,7 +9058,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 82.2.1; + version = 82.2.2; }; }; C14882EB27F211A000D59F0C /* XCRemoteSwiftPackageReference "SwiftSoup" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index d9ed11f7d0..39d7ce6624 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/DuckDuckGo/BrowserServicesKit", "state": { "branch": null, - "revision": "0ac6d8e2153bec4ddd4e983915da6db09fcbed05", - "version": "82.2.1" + "revision": "989e306052bc284a1202fad1087f8b88e515a966", + "version": "82.2.2" } }, { From aa7c242376dca12c33ac0d5f06738f80c0a5a423 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Thu, 9 Nov 2023 13:47:58 +0000 Subject: [PATCH 09/16] Update BSK (#2136) Task/Issue URL: https://app.asana.com/0/0/1205915133582459/f Description: Update BSK, no changes in iOS code, --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 9930057c0b..bafe4e69c6 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -9058,7 +9058,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 82.2.2; + version = 82.2.3; }; }; C14882EB27F211A000D59F0C /* XCRemoteSwiftPackageReference "SwiftSoup" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 39d7ce6624..0354822386 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/DuckDuckGo/BrowserServicesKit", "state": { "branch": null, - "revision": "989e306052bc284a1202fad1087f8b88e515a966", - "version": "82.2.2" + "revision": "f2936a65ef7685fe9c39d6a996c8391cdb3d95ff", + "version": "82.2.3" } }, { From f4c0eba4ac3008c63d91f27233ccdc0f642c33c0 Mon Sep 17 00:00:00 2001 From: Anh Do <18567+quanganhdo@users.noreply.github.com> Date: Thu, 9 Nov 2023 13:51:53 -0500 Subject: [PATCH 10/16] Support environment setting (#2140) --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index bafe4e69c6..b79bb0ad72 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -9058,7 +9058,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 82.2.3; + version = 82.3.0; }; }; C14882EB27F211A000D59F0C /* XCRemoteSwiftPackageReference "SwiftSoup" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0354822386..129f2539b2 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/DuckDuckGo/BrowserServicesKit", "state": { "branch": null, - "revision": "f2936a65ef7685fe9c39d6a996c8391cdb3d95ff", - "version": "82.2.3" + "revision": "c4d5f6df0340f0a5c109dcded9801ab676de7db5", + "version": "82.3.0" } }, { From 9ad737ce62da9878740a9713407d090b8ab6fed5 Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Fri, 10 Nov 2023 09:50:56 +0000 Subject: [PATCH 11/16] switch to true|false for protectionsState param (#2137) Co-authored-by: Shane Osbourne --- DuckDuckGo/BrokenSiteInfo.swift | 13 ++++--------- DuckDuckGo/TabViewController.swift | 4 ++-- DuckDuckGoTests/BrokenSiteReportingTests.swift | 2 +- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/DuckDuckGo/BrokenSiteInfo.swift b/DuckDuckGo/BrokenSiteInfo.swift index 063784c78f..9a1ed49e6e 100644 --- a/DuckDuckGo/BrokenSiteInfo.swift +++ b/DuckDuckGo/BrokenSiteInfo.swift @@ -24,11 +24,6 @@ public struct BrokenSiteInfo { static let allowedQueryReservedCharacters = CharacterSet(charactersIn: ",") - enum ProtectionsState: String { - case enabled = "1" - case disabled = "0" - } - private struct Keys { static let url = "siteUrl" static let category = "category" @@ -60,14 +55,14 @@ public struct BrokenSiteInfo { private let manufacturer: String private let systemVersion: String private let gpc: Bool - private let protectionsState: ProtectionsState + private let protectionsState: Bool public init(url: URL?, httpsUpgrade: Bool, blockedTrackerDomains: [String], installedSurrogates: [String], isDesktop: Bool, tdsETag: String?, ampUrl: String?, urlParametersRemoved: Bool, - protected: Bool, + protectionsState: Bool, model: String = UIDevice.current.model, manufacturer: String = "Apple", systemVersion: String = UIDevice.current.systemVersion, @@ -84,7 +79,7 @@ public struct BrokenSiteInfo { self.model = model self.manufacturer = manufacturer self.systemVersion = systemVersion - self.protectionsState = protected ? .enabled : .disabled + self.protectionsState = protectionsState if let gpcParam = gpc { self.gpc = gpcParam @@ -111,7 +106,7 @@ public struct BrokenSiteInfo { Keys.gpc: gpc ? "true" : "false", Keys.ampUrl: ampUrl ?? "", Keys.urlParametersRemoved: urlParametersRemoved ? "true" : "false", - Keys.protectionsState: protectionsState.rawValue + Keys.protectionsState: protectionsState ? "true" : "false" ] Pixel.fire(pixel: .brokenSiteReport, diff --git a/DuckDuckGo/TabViewController.swift b/DuckDuckGo/TabViewController.swift index 5e4da0f41b..6962907e0e 100644 --- a/DuckDuckGo/TabViewController.swift +++ b/DuckDuckGo/TabViewController.swift @@ -868,7 +868,7 @@ class TabViewController: UIViewController { let blockedTrackerDomains = privacyInfo?.trackerInfo.trackersBlocked.compactMap { $0.domain } ?? [] let configuration = ContentBlocking.shared.privacyConfigurationManager.privacyConfig - let protected = configuration.isFeature(.contentBlocking, enabledForDomain: url?.host) + let protectionsState = configuration.isFeature(.contentBlocking, enabledForDomain: url?.host) return BrokenSiteInfo(url: url, httpsUpgrade: httpsForced, @@ -878,7 +878,7 @@ class TabViewController: UIViewController { tdsETag: ContentBlocking.shared.contentBlockingManager.currentMainRules?.etag ?? "", ampUrl: linkProtection.lastAMPURLString, urlParametersRemoved: linkProtection.urlParametersRemoved, - protected: protected) + protectionsState: protectionsState) } public func print() { diff --git a/DuckDuckGoTests/BrokenSiteReportingTests.swift b/DuckDuckGoTests/BrokenSiteReportingTests.swift index 82b3d25c2a..e65fb7f268 100644 --- a/DuckDuckGoTests/BrokenSiteReportingTests.swift +++ b/DuckDuckGoTests/BrokenSiteReportingTests.swift @@ -75,7 +75,7 @@ final class BrokenSiteReportingTests: XCTestCase { tdsETag: test.blocklistVersion, ampUrl: nil, urlParametersRemoved: false, - protected: true, + protectionsState: true, model: test.model ?? "", manufacturer: test.manufacturer ?? "", systemVersion: test.os ?? "", From 0389ed604ccc13d79d814a6a2aca920c9a931e7a Mon Sep 17 00:00:00 2001 From: Lorenzo Mattei Date: Fri, 10 Nov 2023 15:41:18 +0100 Subject: [PATCH 12/16] Add Sync e2e test flows (#2127) --- .github/workflows/sync-end-to-end.yml | 63 ++++++++ .gitignore | 3 - .maestro/shared/set_internal_user.yaml | 10 ++ .maestro/shared/sync_create.yaml | 12 ++ .maestro/shared/sync_delete.yaml | 9 ++ .maestro/sync_tests/01_create_account.yaml | 29 ++++ .maestro/sync_tests/02_login_account.yaml | 45 ++++++ .maestro/sync_tests/03_recover_account.yaml | 51 ++++++ .maestro/sync_tests/04_sync_data.yaml | 162 ++++++++++++++++++++ 9 files changed, 381 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/sync-end-to-end.yml create mode 100644 .maestro/shared/set_internal_user.yaml create mode 100644 .maestro/shared/sync_create.yaml create mode 100644 .maestro/shared/sync_delete.yaml create mode 100644 .maestro/sync_tests/01_create_account.yaml create mode 100644 .maestro/sync_tests/02_login_account.yaml create mode 100644 .maestro/sync_tests/03_recover_account.yaml create mode 100644 .maestro/sync_tests/04_sync_data.yaml diff --git a/.github/workflows/sync-end-to-end.yml b/.github/workflows/sync-end-to-end.yml new file mode 100644 index 0000000000..df2a04df7c --- /dev/null +++ b/.github/workflows/sync-end-to-end.yml @@ -0,0 +1,63 @@ +name: Sync-End-to-End tests + +on: + schedule: + - cron: '0 5 * * *' # run at 5 AM UTC + +jobs: + sync-end-to-end-tests: + name: Sync End to end Tests + runs-on: macos-13 + + steps: + - name: Check out the code + uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Set cache key hash + run: | + has_only_tags=$(jq '[ .object.pins[].state | has("version") ] | all' DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved) + if [[ "$has_only_tags" == "true" ]]; then + echo "cache_key_hash=${{ hashFiles('DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}" >> $GITHUB_ENV + else + echo "Package.resolved contains dependencies specified by branch or commit, skipping cache." + fi + + - name: Cache SPM + if: env.cache_key_hash + uses: actions/cache@v3 + with: + path: DerivedData/SourcePackages + key: ${{ runner.os }}-spm-${{ env.cache_key_hash }} + restore-keys: | + ${{ runner.os }}-spm- + + - name: Select Xcode + run: sudo xcode-select -s /Applications/Xcode_$(<.xcode-version).app/Contents/Developer + + - name: Build for tests + run: | + set -o pipefail && xcodebuild \ + -scheme "DuckDuckGo" \ + -destination "platform=iOS Simulator,name=iPhone 14,OS=16.4" \ + -derivedDataPath "DerivedData" \ + | tee xcodebuild.log + + - name: Create test account for Sync and return the recovery code + uses: duckduckgo/sync_crypto/action@main + id: sync-recovery-code + with: + debug: true + + - name: Sync e2e tests + uses: mobile-dev-inc/action-maestro-cloud@v1.6.0 + with: + api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }} + app-file: DerivedData/Build/Products/Debug-iphonesimulator/DuckDuckGo.app + workspace: .maestro + include-tags: sync + env: | + CODE=${{ steps.sync-recovery-code.outputs.recovery-code }} + + diff --git a/.gitignore b/.gitignore index d6fc1c4859..f723942232 100644 --- a/.gitignore +++ b/.gitignore @@ -70,9 +70,6 @@ fastlane/report.xml fastlane/Preview.html fastlane/test_output -# Mestro -.maestro/**/shared - # DuckDuckGo Configuration/ExternalDeveloper.xcconfig diff --git a/.maestro/shared/set_internal_user.yaml b/.maestro/shared/set_internal_user.yaml new file mode 100644 index 0000000000..97d6e6c9c4 --- /dev/null +++ b/.maestro/shared/set_internal_user.yaml @@ -0,0 +1,10 @@ +appId: com.duckduckgo.mobile.ios +--- + +- scroll +- scroll +- scroll +- assertVisible: Debug Menu +- tapOn: Debug Menu +- tapOn: Internal User State +- tapOn: Settings \ No newline at end of file diff --git a/.maestro/shared/sync_create.yaml b/.maestro/shared/sync_create.yaml new file mode 100644 index 0000000000..8164466ac3 --- /dev/null +++ b/.maestro/shared/sync_create.yaml @@ -0,0 +1,12 @@ +appId: com.duckduckgo.mobile.ios +--- + +- assertVisible: Sync +- tapOn: Sync +- assertVisible: Sync +- tapOn: "0" +- assertVisible: Turn on Sync? +- tapOn: Turn on Sync +- tapOn: Sync Another Device +- tapOn: Show QR Code +- assertVisible: "Go to Settings > Sync in the DuckDuckGo App on a different device and scan this QR code to sync." \ No newline at end of file diff --git a/.maestro/shared/sync_delete.yaml b/.maestro/shared/sync_delete.yaml new file mode 100644 index 0000000000..a82919c953 --- /dev/null +++ b/.maestro/shared/sync_delete.yaml @@ -0,0 +1,9 @@ +appId: com.duckduckgo.mobile.ios +--- + +- assertVisible: Sync +- scroll +- tapOn: + point: 50%,91% # TODO: Revisit after new setup flow has been implemented. +- assertVisible: Delete Server Data? +- tapOn: Delete Server Data \ No newline at end of file diff --git a/.maestro/sync_tests/01_create_account.yaml b/.maestro/sync_tests/01_create_account.yaml new file mode 100644 index 0000000000..6e39ee2fd4 --- /dev/null +++ b/.maestro/sync_tests/01_create_account.yaml @@ -0,0 +1,29 @@ +appId: com.duckduckgo.mobile.ios +tags: + - sync + +--- + +- clearState +- launchApp +- runFlow: + when: + visible: + text: "Let’s Do It!" + index: 0 + file: ../shared/onboarding.yaml + +- tapOn: Settings +- runFlow: + file: ../shared/set_internal_user.yaml +- runFlow: + file: ../shared/sync_create.yaml + + +# Clean up +- tapOn: Back +- tapOn: Cancel +- tapOn: Not Now +- assertVisible: Sync +- runFlow: + file: ../shared/sync_delete.yaml \ No newline at end of file diff --git a/.maestro/sync_tests/02_login_account.yaml b/.maestro/sync_tests/02_login_account.yaml new file mode 100644 index 0000000000..aef46f2912 --- /dev/null +++ b/.maestro/sync_tests/02_login_account.yaml @@ -0,0 +1,45 @@ +appId: com.duckduckgo.mobile.ios +tags: + - sync + +--- + +# Create an account +- clearState +- launchApp +- runFlow: + when: + visible: + text: "Let’s Do It!" + index: 0 + file: ../shared/onboarding.yaml + +- tapOn: Settings +- runFlow: + file: ../shared/set_internal_user.yaml +- runFlow: + file: ../shared/sync_create.yaml + +# Copy Sync Code and Log Out +- tapOn: Back +- tapOn: Cancel +- assertVisible: Save Recovery Key +- tapOn: Copy Key +- tapOn: Not Now +- tapOn: "1" +- assertVisible: Turn Off Sync? +- tapOn: Remove + +# Login +- tapOn: "0" +- tapOn: Recover Your Synced Data +- tapOn: Manually Enter Code +- tapOn: Paste +- assertVisible: Device Synced! +- tapOn: Next +- tapOn: Not Now + +# Clean up +- assertVisible: Sync +- runFlow: + file: ../shared/sync_delete.yaml \ No newline at end of file diff --git a/.maestro/sync_tests/03_recover_account.yaml b/.maestro/sync_tests/03_recover_account.yaml new file mode 100644 index 0000000000..265684b884 --- /dev/null +++ b/.maestro/sync_tests/03_recover_account.yaml @@ -0,0 +1,51 @@ +appId: com.duckduckgo.mobile.ios +tags: + - sync + +--- + +- clearState +- launchApp +- runFlow: + when: + visible: + text: "Let’s Do It!" + index: 0 + file: ../shared/onboarding.yaml + +# This is a workaround to: +# - Put the code in the clipboard on Maestro Cloud +# - Prevent iOS from showing the Paste permission alert as Maestro can't handle it +- tapOn: + id: searchEntry +- inputText: ${CODE} +- longPressOn: + id: searchEntry +- tapOn: Select All +- tapOn: Cut +- tapOn: + id: searchEntry +- longPressOn: + id: searchEntry +- tapOn: Paste +- tapOn: Cancel +# + +# Recover Account test +- tapOn: Settings +- runFlow: + file: ../shared/set_internal_user.yaml +- assertVisible: Sync +- tapOn: Sync +- assertVisible: Sync +- tapOn: "0" +- assertVisible: Turn on Sync? +- tapOn: Recover Your Synced Data +- assertVisible: Scan QR Code +- tapOn: Manually Enter Code +- tapOn: Paste +- assertVisible: Device Synced! +- tapOn: Next +- tapOn: Not Now +- tapOn: Settings +- tapOn: Done \ No newline at end of file diff --git a/.maestro/sync_tests/04_sync_data.yaml b/.maestro/sync_tests/04_sync_data.yaml new file mode 100644 index 0000000000..643cd1a431 --- /dev/null +++ b/.maestro/sync_tests/04_sync_data.yaml @@ -0,0 +1,162 @@ +appId: com.duckduckgo.mobile.ios +tags: + - sync + +--- + +- clearState +- launchApp +- runFlow: + when: + visible: + text: "Let’s Do It!" + index: 0 + file: ../shared/onboarding.yaml + +# Add local favorite and bookmark +- tapOn: + id: searchEntry +- inputText: www.duckduckgo.com +- pressKey: Enter +- runFlow: + when: + visible: + text: "Got It" + commands: + - tapOn: Got It +- tapOn: Browsing Menu +- tapOn: Add Favorite +- tapOn: + id: searchEntry +- inputText: www.spreadprivacy.com +- pressKey: Enter +- tapOn: Browsing Menu +- tapOn: Add Bookmark + +# Add local login +- tapOn: Browsing Menu +- tapOn: Settings +- tapOn: Logins +- tapOn: Add 24 +- tapOn: Title +- inputText: My Personal Website +- tapOn: username@example.com +- inputText: me@mypersonalwebsite.com +- tapOn: example.com +- inputText: mypersonalwebsite.com +- tapOn: Save +- tapOn: Logins +- tapOn: Settings +- tapOn: Done + +# Sync data +# This is a workaround to: +# - Put the code in the clipboard on Maestro Cloud +# - Prevent iOS from showing the Paste permission alert as Maestro can't handle it +- tapOn: + id: searchEntry +- inputText: ${CODE} +- longPressOn: + id: searchEntry +- runFlow: + when: + visible: + text: searchEntry + commands: + - tapOn: searchEntry +- tapOn: Select All +- tapOn: Cut +- tapOn: + id: searchEntry +- longPressOn: + id: searchEntry +- tapOn: Paste +- tapOn: Cancel + +- tapOn: Close Tabs and Clear Data +- tapOn: Close Tabs and Clear Data +- runFlow: + when: + visible: + text: "Cancel" + commands: + - tapOn: Cancel +# + +- tapOn: Settings +- runFlow: + file: ../shared/set_internal_user.yaml +- assertVisible: Sync +- tapOn: Sync +- assertVisible: Sync +- tapOn: "0" +- assertVisible: Turn on Sync? +- tapOn: Recover Your Synced Data +- assertVisible: Scan QR Code +- tapOn: Manually Enter Code +- tapOn: Paste +- assertVisible: Device Synced! +- tapOn: Next +- tapOn: Not Now +- tapOn: Settings +- tapOn: Done + +# Verify bookmarks and favorites have been merged +- tapOn: Bookmarks + +- assertVisible: Spread Privacy +- assertVisible: Stack Overflow - Where Developers Learn, Share, & Build Careers +- assertVisible: DuckDuckGo — Privacy, simplified. +- assertVisible: DuckDuckGo · GitHub +- assertVisible: "Wolfram|Alpha: Computational Intelligence" +- assertVisible: news +- assertVisible: code +- assertVisible: sports +- tapOn: news +- assertVisible: Breaking News, Latest News and Videos | CNN +- assertVisible: News, sport and opinion from the Guardian's global edition | The Guardian +- tapOn: Bookmarks +- tapOn: code +- assertVisible: "GitHub - duckduckgo/Android: DuckDuckGo Android App" +- assertVisible: "GitHub - duckduckgo/iOS: DuckDuckGo iOS Application" +- tapOn: Bookmarks +- tapOn: sports +- assertVisible: NFL.com | Official Site of the National Football League +- assertVisible: AS.com - Diario online deportivo. Fútbol, motor y mucho más +- tapOn: Bookmarks +- tapOn: Favorites +- assertVisible: DuckDuckGo — Privacy, simplified. +- assertVisible: NFL.com | Official Site of the National Football League +- assertVisible: DuckDuckGo · GitHub +- assertVisible: Stack Overflow - Where Developers Learn, Share, & Build Careers +- tapOn: Done + +# Verify logins +- tapOn: Settings +- tapOn: Logins +- assertVisible: Unlock device to access saved Logins +- tapOn: Passcode field +- inputText: "0000" +- pressKey: Enter +- assertVisible: Dax Login +- tapOn: Dax Login +- assertVisible: daxthetest +- assertVisible: duckduckgo.com +- tapOn: Logins +- assertVisible: Github +- tapOn: Github +- assertVisible: githubusername +- assertVisible: github.com +- tapOn: Logins +- assertVisible: StackOverflow +- tapOn: StackOverflow +- assertVisible: stacker +- assertVisible: stackoverflow.com +- tapOn: Logins +- assertVisible: My Personal Website +- tapOn: My Personal Website +- assertVisible: me@mypersonalwebsite.com +- assertVisible: mypersonalwebsite.com +- tapOn: Logins +- tapOn: Settings +- tapOn: Done \ No newline at end of file From 02329f89dde9ae9481e2998aef3638a711a4da16 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Fri, 10 Nov 2023 17:01:53 +0100 Subject: [PATCH 13/16] Sync form factor specific favorites (#2029) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1201493110486074/1204926049616866/f Tech Design URL: https://app.asana.com/0/481882893211075/1205418608802079/f Description: This change introduces form factor specific favorites to be used when Sync is enabled. 1 favorites folder is replaced by 3 folders – mobile, desktop and unified. Users always see only 1 folder and it's their form-factor-specific one. Only users with Sync enabled get to choose whether they want to see the form-factor-specific favorites or unified (desktop + mobile) favorites. The setting itself is synced between devices in the Sync account. BookmarkEntity's isFavorite is replaced with isFavorite(on:) taking folder as a parameter. API for adding to favorites and removing from favorites was updated to take multiple favorites folders as needed. FavoritesDisplayMode enum is introduced to manage display mode in clients. Bookmarks and Favorites related view models used on iOS are updated to take favorites display mode and reload their data as the mode changes. In SyncDataProviders, an abstract SettingSyncHandler class was added to support adding an arbitrary setting (key-value pair) to Sync. It's subclassed by EmailProtectionSyncHandler and FavoritesDisplayModeSyncHandlerBase (that needs to be subclassed in client apps due to differences in User Defaults storage between iOS and macOS apps). --- Core/BookmarksCachingSearch.swift | 4 +- Core/BookmarksExporter.swift | 6 +- Core/BookmarksImporter.swift | 4 +- Core/BookmarksModelsErrorHandling.swift | 14 +- Core/LegacyBookmarksStoreMigration.swift | 9 +- Core/PixelEvent.swift | 12 + Core/SyncBookmarksAdapter.swift | 76 +++- Core/SyncCredentialsAdapter.swift | 35 ++ Core/SyncDataProviders.swift | 9 +- Core/SyncSettingsAdapter.swift | 8 +- Core/UserDefaultsPropertyWrapper.swift | 6 +- DuckDuckGo.xcodeproj/project.pbxproj | 24 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../AddOrEditBookmarkViewController.swift | 14 +- DuckDuckGo/AppDelegate.swift | 38 +- DuckDuckGo/AppSettings.swift | 7 + DuckDuckGo/AppUserDefaults.swift | 31 +- DuckDuckGo/Base.lproj/Settings.storyboard | 4 +- DuckDuckGo/BookmarkFolderCell.swift | 2 +- .../BookmarkFoldersTableViewController.swift | 4 +- DuckDuckGo/BookmarksDataSource.swift | 2 +- DuckDuckGo/BookmarksViewController.swift | 59 ++- .../FavoritesDisplayMode+UserDefaults.swift | 36 ++ DuckDuckGo/FavoritesDisplayModeStorage.swift | 40 ++ .../FavoritesDisplayModeSyncHandler.swift | 50 +++ DuckDuckGo/FavoritesViewController.swift | 31 +- DuckDuckGo/MainViewController+Segues.swift | 11 +- DuckDuckGo/MainViewController.swift | 74 +++- DuckDuckGo/RemoteMessaging.swift | 37 +- DuckDuckGo/SettingsViewController.swift | 7 +- .../Contents.json | 15 + .../Device-Mobile-Upload-96.svg | 17 + .../Sync-Pair-96.imageset/Contents.json | 15 + .../Sync-Pair-96.imageset/Sync-Pair-96.svg | 30 ++ .../Sync-Start-128.imageset/Contents.json | 15 + .../Sync-Start-128.svg | 15 + .../SyncAllDevices.imageset/Contents.json | 16 + .../SyncAllDevices.svg | 9 + .../SyncFavicons.imageset/Contents.json | 16 + .../SyncFavicons.imageset/SyncFavicons.svg | 8 + DuckDuckGo/SyncDebugViewController.swift | 23 ++ ...cSettingsViewController+SyncDelegate.swift | 59 +-- DuckDuckGo/SyncSettingsViewController.swift | 77 +++- DuckDuckGo/TabSwitcherViewController.swift | 2 + DuckDuckGo/TabViewController.swift | 2 + ...bViewControllerBrowsingMenuExtension.swift | 2 +- DuckDuckGo/UserText.swift | 14 +- DuckDuckGo/en.lproj/Localizable.strings | 30 ++ DuckDuckGoTests/AppSettingsMock.swift | 7 + .../BookmarkEditorViewModelTests.swift | 15 +- DuckDuckGoTests/BookmarkEntityTests.swift | 10 +- .../BookmarkListViewModelTests.swift | 52 ++- DuckDuckGoTests/BookmarkUtilsTests.swift | 4 +- DuckDuckGoTests/BookmarksExporterTests.swift | 4 +- DuckDuckGoTests/BookmarksImporterTests.swift | 2 +- DuckDuckGoTests/BookmarksMigrationTests.swift | 27 +- DuckDuckGoTests/BookmarksTestHelpers.swift | 16 +- .../FavoriteListViewModelTests.swift | 10 +- .../MenuBookmarksViewModelTests.swift | 12 +- .../SyncManagementViewModelTests.swift | 91 ++++- .../ViewModels/ScanOrPasteCodeViewModel.swift | 1 - .../ViewModels/SyncSettingsViewModel.swift | 59 ++- .../ViewModels/TurnOnSyncViewModel.swift | 61 --- .../SyncUI/Views/DeviceConnectedView.swift | 109 ++++-- .../Views/Internal/BackButtonModifier.swift | 18 + .../Views/Internal/ConnectModeView.swift | 2 + .../Views/Internal/QRCodeCopierView.swift | 5 +- .../Views/Internal/RemoveDeviceView.swift | 5 +- .../Views/Internal/SyncLabelButtonStyle.swift | 3 +- .../SyncUI/Views/Internal/UserText.swift | 67 ++-- .../Views/{Internal => }/PasteCodeView.swift | 26 +- .../SyncUI/Views/SaveRecoveryKeyView.swift | 19 +- .../SyncUI/Views/ScanOrPasteCodeView.swift | 68 ++-- .../SyncUI/Views/SyncSettingsView.swift | 352 +++++++++++++----- .../Sources/SyncUI/Views/TurnOnSyncView.swift | 162 -------- Widgets/Widgets.swift | 12 +- 76 files changed, 1621 insertions(+), 621 deletions(-) create mode 100644 DuckDuckGo/FavoritesDisplayMode+UserDefaults.swift create mode 100644 DuckDuckGo/FavoritesDisplayModeStorage.swift create mode 100644 DuckDuckGo/FavoritesDisplayModeSyncHandler.swift create mode 100644 DuckDuckGo/SyncAssets.xcassets/Device-Mobile-Upload-96.imageset/Contents.json create mode 100644 DuckDuckGo/SyncAssets.xcassets/Device-Mobile-Upload-96.imageset/Device-Mobile-Upload-96.svg create mode 100644 DuckDuckGo/SyncAssets.xcassets/Sync-Pair-96.imageset/Contents.json create mode 100644 DuckDuckGo/SyncAssets.xcassets/Sync-Pair-96.imageset/Sync-Pair-96.svg create mode 100644 DuckDuckGo/SyncAssets.xcassets/Sync-Start-128.imageset/Contents.json create mode 100644 DuckDuckGo/SyncAssets.xcassets/Sync-Start-128.imageset/Sync-Start-128.svg create mode 100644 DuckDuckGo/SyncAssets.xcassets/SyncAllDevices.imageset/Contents.json create mode 100644 DuckDuckGo/SyncAssets.xcassets/SyncAllDevices.imageset/SyncAllDevices.svg create mode 100644 DuckDuckGo/SyncAssets.xcassets/SyncFavicons.imageset/Contents.json create mode 100644 DuckDuckGo/SyncAssets.xcassets/SyncFavicons.imageset/SyncFavicons.svg delete mode 100644 LocalPackages/SyncUI/Sources/SyncUI/ViewModels/TurnOnSyncViewModel.swift rename LocalPackages/SyncUI/Sources/SyncUI/Views/{Internal => }/PasteCodeView.swift (87%) delete mode 100644 LocalPackages/SyncUI/Sources/SyncUI/Views/TurnOnSyncView.swift diff --git a/Core/BookmarksCachingSearch.swift b/Core/BookmarksCachingSearch.swift index 17b264b198..c43c3f2712 100644 --- a/Core/BookmarksCachingSearch.swift +++ b/Core/BookmarksCachingSearch.swift @@ -70,8 +70,8 @@ public class CoreDataBookmarksSearchStore: BookmarksSearchStore { fetchRequest.resultType = .dictionaryResultType fetchRequest.propertiesToFetch = [#keyPath(BookmarkEntity.title), #keyPath(BookmarkEntity.url), - #keyPath(BookmarkEntity.favoriteFolder), #keyPath(BookmarkEntity.objectID)] + fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(BookmarkEntity.favoriteFolders)] context.perform { let result = try? context.fetch(fetchRequest) as? [Dictionary] @@ -131,7 +131,7 @@ public class BookmarksCachingSearch: BookmarksStringSearch { self.init(objectID: objectID, title: title, url: url, - isFavorite: bookmark[#keyPath(BookmarkEntity.favoriteFolder)] != nil) + isFavorite: (bookmark[#keyPath(BookmarkEntity.favoriteFolders)] as? Set)?.isEmpty != true) } public func togglingFavorite() -> BookmarksStringSearchResult { diff --git a/Core/BookmarksExporter.swift b/Core/BookmarksExporter.swift index 53fb563af9..294b59a076 100644 --- a/Core/BookmarksExporter.swift +++ b/Core/BookmarksExporter.swift @@ -29,9 +29,11 @@ public enum BookmarksExporterError: Error { public struct BookmarksExporter { private(set) var coreDataStorage: CoreDataDatabase + private let favoritesDisplayMode: FavoritesDisplayMode - public init(coreDataStore: CoreDataDatabase) { + public init(coreDataStore: CoreDataDatabase, favoritesDisplayMode: FavoritesDisplayMode) { coreDataStorage = coreDataStore + self.favoritesDisplayMode = favoritesDisplayMode } public func exportBookmarksTo(url: URL) throws { @@ -64,7 +66,7 @@ public struct BookmarksExporter { content.append(Template.bookmark(level: level, title: entity.title!.escapedForHTML, url: entity.url!, - isFavorite: entity.isFavorite)) + isFavorite: entity.isFavorite(on: favoritesDisplayMode.displayedFolder))) } } return content diff --git a/Core/BookmarksImporter.swift b/Core/BookmarksImporter.swift index a98a7c6668..177dcf2955 100644 --- a/Core/BookmarksImporter.swift +++ b/Core/BookmarksImporter.swift @@ -42,8 +42,8 @@ final public class BookmarksImporter { private(set) var importedBookmarks: [BookmarkOrFolder] = [] private(set) var coreDataStorage: BookmarkCoreDataImporter - public init(coreDataStore: CoreDataDatabase) { - coreDataStorage = BookmarkCoreDataImporter(database: coreDataStore) + public init(coreDataStore: CoreDataDatabase, favoritesDisplayMode: FavoritesDisplayMode) { + coreDataStorage = BookmarkCoreDataImporter(database: coreDataStore, favoritesDisplayMode: favoritesDisplayMode) } func isDocumentInSafariFormat(_ document: Document) -> Bool { diff --git a/Core/BookmarksModelsErrorHandling.swift b/Core/BookmarksModelsErrorHandling.swift index aad7935bbe..5d25ef7c80 100644 --- a/Core/BookmarksModelsErrorHandling.swift +++ b/Core/BookmarksModelsErrorHandling.swift @@ -82,18 +82,22 @@ public extension BookmarkEditorViewModel { convenience init(editingEntityID: NSManagedObjectID, bookmarksDatabase: CoreDataDatabase, + favoritesDisplayMode: FavoritesDisplayMode, syncService: DDGSyncing?) { self.init(editingEntityID: editingEntityID, bookmarksDatabase: bookmarksDatabase, + favoritesDisplayMode: favoritesDisplayMode, errorEvents: BookmarksModelsErrorHandling(syncService: syncService)) } convenience init(creatingFolderWithParentID parentFolderID: NSManagedObjectID?, bookmarksDatabase: CoreDataDatabase, + favoritesDisplayMode: FavoritesDisplayMode, syncService: DDGSyncing?) { self.init(creatingFolderWithParentID: parentFolderID, bookmarksDatabase: bookmarksDatabase, + favoritesDisplayMode: favoritesDisplayMode, errorEvents: BookmarksModelsErrorHandling(syncService: syncService)) } } @@ -102,25 +106,25 @@ public extension BookmarkListViewModel { convenience init(bookmarksDatabase: CoreDataDatabase, parentID: NSManagedObjectID?, + favoritesDisplayMode: FavoritesDisplayMode, syncService: DDGSyncing?) { self.init(bookmarksDatabase: bookmarksDatabase, parentID: parentID, + favoritesDisplayMode: favoritesDisplayMode, errorEvents: BookmarksModelsErrorHandling(syncService: syncService)) } } public extension FavoritesListViewModel { - convenience init(bookmarksDatabase: CoreDataDatabase) { - self.init(bookmarksDatabase: bookmarksDatabase, - errorEvents: BookmarksModelsErrorHandling()) + convenience init(bookmarksDatabase: CoreDataDatabase, favoritesDisplayMode: FavoritesDisplayMode) { + self.init(bookmarksDatabase: bookmarksDatabase, errorEvents: BookmarksModelsErrorHandling(), favoritesDisplayMode: favoritesDisplayMode) } } public extension MenuBookmarksViewModel { convenience init(bookmarksDatabase: CoreDataDatabase, syncService: DDGSyncing?) { - self.init(bookmarksDatabase: bookmarksDatabase, - errorEvents: BookmarksModelsErrorHandling(syncService: syncService)) + self.init(bookmarksDatabase: bookmarksDatabase, errorEvents: BookmarksModelsErrorHandling(syncService: syncService)) } } diff --git a/Core/LegacyBookmarksStoreMigration.swift b/Core/LegacyBookmarksStoreMigration.swift index 248d96f12b..dfd6a4c371 100644 --- a/Core/LegacyBookmarksStoreMigration.swift +++ b/Core/LegacyBookmarksStoreMigration.swift @@ -39,7 +39,7 @@ public class LegacyBookmarksStoreMigration { } } else { // Initialize structure if needed - BookmarkUtils.prepareFoldersStructure(in: context) + BookmarkUtils.prepareLegacyFoldersStructure(in: context) if context.hasChanges { do { try context.save(onErrorFire: .bookmarksCouldNotPrepareDatabase) @@ -82,7 +82,8 @@ public class LegacyBookmarksStoreMigration { BookmarkUtils.prepareFoldersStructure(in: destination) guard let newRoot = BookmarkUtils.fetchRootFolder(destination), - let newFavoritesRoot = BookmarkUtils.fetchFavoritesFolder(destination) else { + let newFavoritesRoot = BookmarkUtils.fetchFavoritesFolder(withUUID: FavoritesFolderID.unified.rawValue, in: destination), + let newMobileFavoritesRoot = BookmarkUtils.fetchFavoritesFolder(withUUID: FavoritesFolderID.mobile.rawValue, in: destination) else { Pixel.fire(pixel: .bookmarksMigrationCouldNotPrepareDatabase) Thread.sleep(forTimeInterval: 2) fatalError("Could not write to Bookmarks DB") @@ -169,6 +170,8 @@ public class LegacyBookmarksStoreMigration { }() bookmark.addToFavorites(insertAt: 0, favoritesRoot: newFavoritesRoot) + bookmark.addToFavorites(insertAt: 0, + favoritesRoot: newMobileFavoritesRoot) } do { @@ -176,7 +179,7 @@ public class LegacyBookmarksStoreMigration { } catch { destination.reset() - BookmarkUtils.prepareFoldersStructure(in: destination) + BookmarkUtils.prepareLegacyFoldersStructure(in: destination) do { try destination.save(onErrorFire: .bookmarksMigrationCouldNotPrepareDatabaseOnFailedMigration) } catch { diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index e3b1b8dc82..a2005a22f2 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -473,10 +473,15 @@ extension Pixel { case bookmarksMigrationCouldNotPrepareDatabaseOnFailedMigration case bookmarksMigrationCouldNotValidateDatabase case bookmarksMigrationCouldNotRemoveOldStore + case bookmarksMigrationCouldNotPrepareMultipleFavoriteFolders case syncFailedToMigrate case syncFailedToLoadAccount case syncFailedToSetupEngine + case syncBookmarksCountLimitExceededDaily + case syncCredentialsCountLimitExceededDaily + case syncBookmarksRequestSizeLimitExceededDaily + case syncCredentialsRequestSizeLimitExceededDaily case syncSentUnauthenticatedRequest case syncMetadataCouldNotLoadDatabase @@ -489,6 +494,7 @@ extension Pixel { case bookmarksCleanupFailed case bookmarksCleanupAttemptedWhileSyncWasEnabled + case favoritesCleanupFailed case credentialsDatabaseCleanupFailed case credentialsCleanupAttemptedWhileSyncWasEnabled @@ -951,10 +957,15 @@ extension Pixel.Event { return "m_d_bookmarks_migration_could_not_prepare_database_on_failed_migration" case .bookmarksMigrationCouldNotValidateDatabase: return "m_d_bookmarks_migration_could_not_validate_database" case .bookmarksMigrationCouldNotRemoveOldStore: return "m_d_bookmarks_migration_could_not_remove_old_store" + case .bookmarksMigrationCouldNotPrepareMultipleFavoriteFolders: return "m_d_bookmarks_migration_could_not_prepare_multiple_favorite_folders" case .syncFailedToMigrate: return "m_d_sync_failed_to_migrate" case .syncFailedToLoadAccount: return "m_d_sync_failed_to_load_account" case .syncFailedToSetupEngine: return "m_d_sync_failed_to_setup_engine" + case .syncBookmarksCountLimitExceededDaily: return "m_d_sync_bookmarks_count_limit_exceeded_daily" + case .syncCredentialsCountLimitExceededDaily: return "m_d_sync_credentials_count_limit_exceeded_daily" + case .syncBookmarksRequestSizeLimitExceededDaily: return "m_d_sync_bookmarks_request_size_limit_exceeded_daily" + case .syncCredentialsRequestSizeLimitExceededDaily: return "m_d_sync_credentials_request_size_limit_exceeded_daily" case .syncSentUnauthenticatedRequest: return "m_d_sync_sent_unauthenticated_request" case .syncMetadataCouldNotLoadDatabase: return "m_d_sync_metadata_could_not_load_database" @@ -968,6 +979,7 @@ extension Pixel.Event { case .bookmarksCleanupFailed: return "m_d_bookmarks_cleanup_failed" case .bookmarksCleanupAttemptedWhileSyncWasEnabled: return "m_d_bookmarks_cleanup_attempted_while_sync_was_enabled" + case .favoritesCleanupFailed: return "m_d_favorites_cleanup_failed" case .credentialsDatabaseCleanupFailed: return "m_d_credentials_database_cleanup_failed_2" case .credentialsCleanupAttemptedWhileSyncWasEnabled: return "m_d_credentials_cleanup_attempted_while_sync_was_enabled" diff --git a/Core/SyncBookmarksAdapter.swift b/Core/SyncBookmarksAdapter.swift index 5578e2422c..5752c090dc 100644 --- a/Core/SyncBookmarksAdapter.swift +++ b/Core/SyncBookmarksAdapter.swift @@ -26,14 +26,38 @@ import Persistence import SyncDataProviders import WidgetKit +public protocol FavoritesDisplayModeStoring: AnyObject { + var favoritesDisplayMode: FavoritesDisplayMode { get set } +} + public final class SyncBookmarksAdapter { public private(set) var provider: BookmarksProvider? public let databaseCleaner: BookmarkDatabaseCleaner public let syncDidCompletePublisher: AnyPublisher public let widgetRefreshCancellable: AnyCancellable + public static let syncBookmarksPausedStateChanged = Notification.Name("com.duckduckgo.app.SyncPausedStateChanged") + public static let bookmarksSyncLimitReached = Notification.Name("com.duckduckgo.app.SyncBookmarksLimitReached") + + public var shouldResetBookmarksSyncTimestamp: Bool = false { + willSet { + assert(provider == nil, "Setting this value has no effect after provider has been instantiated") + } + } - public init(database: CoreDataDatabase) { + @UserDefaultsWrapper(key: .syncBookmarksPaused, defaultValue: false) + static public var isSyncBookmarksPaused: Bool { + didSet { + NotificationCenter.default.post(name: syncBookmarksPausedStateChanged, object: nil) + } + } + + @UserDefaultsWrapper(key: .syncBookmarksPausedErrorDisplayed, defaultValue: false) + static private var didShowBookmarksSyncPausedError: Bool + + public init(database: CoreDataDatabase, favoritesDisplayModeStorage: FavoritesDisplayModeStoring) { + self.database = database + self.favoritesDisplayModeStorage = favoritesDisplayModeStorage syncDidCompletePublisher = syncDidCompleteSubject.eraseToAnyPublisher() databaseCleaner = BookmarkDatabaseCleaner( bookmarkDatabase: database, @@ -49,6 +73,7 @@ public final class SyncBookmarksAdapter { databaseCleaner.cleanUpDatabaseNow() if shouldEnable { databaseCleaner.scheduleRegularCleaning() + handleFavoritesAfterDisablingSync() } else { databaseCleaner.cancelCleaningSchedule() } @@ -64,14 +89,33 @@ public final class SyncBookmarksAdapter { metadataStore: metadataStore, syncDidUpdateData: { [syncDidCompleteSubject] in syncDidCompleteSubject.send() + Self.isSyncBookmarksPaused = false + Self.didShowBookmarksSyncPausedError = false } ) + if shouldResetBookmarksSyncTimestamp { + provider.lastSyncTimestamp = nil + } syncErrorCancellable = provider.syncErrorPublisher .sink { error in switch error { case let syncError as SyncError: Pixel.fire(pixel: .syncBookmarksFailed, error: syncError) + switch syncError { + case .unexpectedStatusCode(409): + // If bookmarks count limit has been exceeded + Self.isSyncBookmarksPaused = true + DailyPixel.fire(pixel: .syncBookmarksCountLimitExceededDaily) + Self.notifyBookmarksSyncLimitReached() + case .unexpectedStatusCode(413): + // If bookmarks request size limit has been exceeded + Self.isSyncBookmarksPaused = true + DailyPixel.fire(pixel: .syncBookmarksRequestSizeLimitExceededDaily) + Self.notifyBookmarksSyncLimitReached() + default: + break + } default: let nsError = error as NSError if nsError.domain != NSURLErrorDomain { @@ -86,6 +130,36 @@ public final class SyncBookmarksAdapter { self.provider = provider } + static private func notifyBookmarksSyncLimitReached() { + if !Self.didShowBookmarksSyncPausedError { + NotificationCenter.default.post(name: Self.bookmarksSyncLimitReached, object: nil) + Self.didShowBookmarksSyncPausedError = true + } + } + + private func handleFavoritesAfterDisablingSync() { + let context = database.makeContext(concurrencyType: .privateQueueConcurrencyType) + + context.performAndWait { + do { + if favoritesDisplayModeStorage.favoritesDisplayMode.isDisplayUnified { + BookmarkUtils.copyFavorites(from: .unified, to: .mobile, clearingNonNativeFavoritesFolder: .desktop, in: context) + favoritesDisplayModeStorage.favoritesDisplayMode = .displayNative(.mobile) + } else { + BookmarkUtils.copyFavorites(from: .mobile, to: .unified, clearingNonNativeFavoritesFolder: .desktop, in: context) + } + try context.save() + } catch { + let nsError = error as NSError + let processedErrors = CoreDataErrorsParser.parse(error: nsError) + let params = processedErrors.errorPixelParameters + Pixel.fire(pixel: .favoritesCleanupFailed, error: error, withAdditionalParameters: params) + } + } + } + private var syncDidCompleteSubject = PassthroughSubject() private var syncErrorCancellable: AnyCancellable? + private let database: CoreDataDatabase + private let favoritesDisplayModeStorage: FavoritesDisplayModeStoring } diff --git a/Core/SyncCredentialsAdapter.swift b/Core/SyncCredentialsAdapter.swift index e4b96e01ef..e5c2101954 100644 --- a/Core/SyncCredentialsAdapter.swift +++ b/Core/SyncCredentialsAdapter.swift @@ -30,6 +30,17 @@ public final class SyncCredentialsAdapter { public private(set) var provider: CredentialsProvider? public let databaseCleaner: CredentialsDatabaseCleaner public let syncDidCompletePublisher: AnyPublisher + public static let syncCredentialsPausedStateChanged = SyncBookmarksAdapter.syncBookmarksPausedStateChanged + public static let credentialsSyncLimitReached = Notification.Name("com.duckduckgo.app.SyncCredentialsLimitReached") + + @UserDefaultsWrapper(key: .syncCredentialsPaused, defaultValue: false) + static public var isSyncCredentialsPaused: Bool { + didSet { + NotificationCenter.default.post(name: syncCredentialsPausedStateChanged, object: nil) + } + } + @UserDefaultsWrapper(key: .syncCredentialsPausedErrorDisplayed, defaultValue: false) + static private var didShowCredentialsSyncPausedError: Bool public init(secureVaultFactory: AutofillVaultFactory = AutofillSecureVaultFactory, secureVaultErrorReporter: SecureVaultErrorReporting) { syncDidCompletePublisher = syncDidCompleteSubject.eraseToAnyPublisher() @@ -63,6 +74,8 @@ public final class SyncCredentialsAdapter { metadataStore: metadataStore, syncDidUpdateData: { [weak self] in self?.syncDidCompleteSubject.send() + Self.isSyncCredentialsPaused = false + Self.didShowCredentialsSyncPausedError = false } ) @@ -71,6 +84,21 @@ public final class SyncCredentialsAdapter { switch error { case let syncError as SyncError: Pixel.fire(pixel: .syncCredentialsFailed, error: syncError) + + switch syncError { + case .unexpectedStatusCode(409): + // If credentials count limit has been exceeded + Self.isSyncCredentialsPaused = true + DailyPixel.fire(pixel: .syncCredentialsCountLimitExceededDaily) + Self.notifyCredentialsSyncLimitReached() + case .unexpectedStatusCode(413): + // If credentials request size limit has been exceeded + Self.isSyncCredentialsPaused = true + DailyPixel.fire(pixel: .syncCredentialsRequestSizeLimitExceededDaily) + Self.notifyCredentialsSyncLimitReached() + default: + break + } default: let nsError = error as NSError if nsError.domain != NSURLErrorDomain { @@ -91,6 +119,13 @@ public final class SyncCredentialsAdapter { } } + static private func notifyCredentialsSyncLimitReached() { + if !Self.didShowCredentialsSyncPausedError { + NotificationCenter.default.post(name: Self.credentialsSyncLimitReached, object: nil) + Self.didShowCredentialsSyncPausedError = true + } + } + private var syncDidCompleteSubject = PassthroughSubject() private var syncErrorCancellable: AnyCancellable? private let secureVaultErrorReporter: SecureVaultErrorReporting diff --git a/Core/SyncDataProviders.swift b/Core/SyncDataProviders.swift index e705889a54..a70b4acd4f 100644 --- a/Core/SyncDataProviders.swift +++ b/Core/SyncDataProviders.swift @@ -17,6 +17,7 @@ // limitations under the License. // +import Bookmarks import BrowserServicesKit import Combine import Common @@ -84,14 +85,16 @@ public class SyncDataProviders: DataProvidersSource { public init( bookmarksDatabase: CoreDataDatabase, secureVaultFactory: AutofillVaultFactory = AutofillSecureVaultFactory, - secureVaultErrorReporter: SecureVaultErrorReporting + secureVaultErrorReporter: SecureVaultErrorReporting, + settingHandlers: [SettingSyncHandler], + favoritesDisplayModeStorage: FavoritesDisplayModeStoring ) { self.bookmarksDatabase = bookmarksDatabase self.secureVaultFactory = secureVaultFactory self.secureVaultErrorReporter = secureVaultErrorReporter - bookmarksAdapter = SyncBookmarksAdapter(database: bookmarksDatabase) + bookmarksAdapter = SyncBookmarksAdapter(database: bookmarksDatabase, favoritesDisplayModeStorage: favoritesDisplayModeStorage) credentialsAdapter = SyncCredentialsAdapter(secureVaultFactory: secureVaultFactory, secureVaultErrorReporter: secureVaultErrorReporter) - settingsAdapter = SyncSettingsAdapter() + settingsAdapter = SyncSettingsAdapter(settingHandlers: settingHandlers) } private func initializeMetadataDatabaseIfNeeded() { diff --git a/Core/SyncSettingsAdapter.swift b/Core/SyncSettingsAdapter.swift index b6b5fd80ef..9bfe132bb4 100644 --- a/Core/SyncSettingsAdapter.swift +++ b/Core/SyncSettingsAdapter.swift @@ -30,7 +30,8 @@ public final class SyncSettingsAdapter { public private(set) var emailManager: EmailManager? public let syncDidCompletePublisher: AnyPublisher - public init() { + public init(settingHandlers: [SettingSyncHandler]) { + self.settingHandlers = settingHandlers syncDidCompletePublisher = syncDidCompleteSubject.eraseToAnyPublisher() } @@ -41,12 +42,14 @@ public final class SyncSettingsAdapter { guard provider == nil else { return } + let emailManager = EmailManager() + let emailProtectionSyncHandler = EmailProtectionSyncHandler(emailManager: emailManager) let provider = SettingsProvider( metadataDatabase: metadataDatabase, metadataStore: metadataStore, - emailManager: emailManager, + settingsHandlers: settingHandlers + [emailProtectionSyncHandler], syncDidUpdateData: { [weak self] in self?.syncDidCompleteSubject.send() } @@ -77,6 +80,7 @@ public final class SyncSettingsAdapter { self.emailManager = emailManager } + private let settingHandlers: [SettingSyncHandler] private var syncDidCompleteSubject = PassthroughSubject() private var syncErrorCancellable: AnyCancellable? } diff --git a/Core/UserDefaultsPropertyWrapper.swift b/Core/UserDefaultsPropertyWrapper.swift index 7949c231c3..78a4507ba0 100644 --- a/Core/UserDefaultsPropertyWrapper.swift +++ b/Core/UserDefaultsPropertyWrapper.swift @@ -69,7 +69,7 @@ public struct UserDefaultsWrapper { case downloadedTrackerDataSetCount = "com.duckduckgo.app.downloadedTrackerDataSetCount" case downloadedPrivacyConfigurationCount = "com.duckduckgo.app.downloadedPrivacyConfigurationCount" case textSize = "com.duckduckgo.ios.textSize" - + case emailWaitlistShouldReceiveNotifications = "com.duckduckgo.ios.showWaitlistNotification" case unseenDownloadsAvailable = "com.duckduckgo.app.unseenDownloadsAvailable" @@ -97,6 +97,10 @@ public struct UserDefaultsWrapper { case defaultBrowserUsageLastSeen = "com.duckduckgo.ios.default-browser-usage-last-seen" case syncEnvironment = "com.duckduckgo.ios.sync-environment" + case syncBookmarksPaused = "com.duckduckgo.ios.sync-bookmarksPaused" + case syncCredentialsPaused = "com.duckduckgo.ios.sync-credentialsPaused" + case syncBookmarksPausedErrorDisplayed = "com.duckduckgo.ios.sync-bookmarksPausedErrorDisplayed" + case syncCredentialsPausedErrorDisplayed = "com.duckduckgo.ios.sync-credentialsPausedErrorDisplayed" case networkProtectionDebugOptionAlwaysOnDisabled = "com.duckduckgo.network-protection.always-on.disabled" diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index b79bb0ad72..aace48e673 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -246,8 +246,12 @@ 31DD208427395A5A008FB313 /* VoiceSearchHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31DD208327395A5A008FB313 /* VoiceSearchHelper.swift */; }; 31E69A63280F4CB600478327 /* DuckUI in Frameworks */ = {isa = PBXBuildFile; productRef = 31E69A62280F4CB600478327 /* DuckUI */; }; 31EF52E1281B3BDC0034796E /* AutofillLoginListItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31EF52E0281B3BDC0034796E /* AutofillLoginListItemViewModel.swift */; }; + 373608902ABB1E6C00629E7F /* FavoritesDisplayModeStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3736088F2ABB1E6C00629E7F /* FavoritesDisplayModeStorage.swift */; }; + 373608922ABB430D00629E7F /* FavoritesDisplayMode+UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373608912ABB430D00629E7F /* FavoritesDisplayMode+UserDefaults.swift */; }; + 373608932ABB432600629E7F /* FavoritesDisplayMode+UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373608912ABB430D00629E7F /* FavoritesDisplayMode+UserDefaults.swift */; }; 37445F972A155F7C0029F789 /* SyncDataProviders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37445F962A155F7C0029F789 /* SyncDataProviders.swift */; }; 3760DFED299315EF0045A446 /* Waitlist in Frameworks */ = {isa = PBXBuildFile; productRef = 3760DFEC299315EF0045A446 /* Waitlist */; }; + 377D80222AB48554002AF251 /* FavoritesDisplayModeSyncHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377D80212AB48554002AF251 /* FavoritesDisplayModeSyncHandler.swift */; }; 379E877429E97C8D001C8BB0 /* BookmarksCleanupErrorHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379E877329E97C8D001C8BB0 /* BookmarksCleanupErrorHandling.swift */; }; 37CBCA9E2A8A659C0050218F /* SyncSettingsAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CBCA9D2A8A659C0050218F /* SyncSettingsAdapter.swift */; }; 37CEFCAC2A673B90001EF741 /* CredentialsCleanupErrorHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEFCAB2A673B90001EF741 /* CredentialsCleanupErrorHandling.swift */; }; @@ -1259,7 +1263,10 @@ 31CC224828369B38001654A4 /* AutofillLoginSettingsListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillLoginSettingsListViewController.swift; sourceTree = ""; }; 31DD208327395A5A008FB313 /* VoiceSearchHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceSearchHelper.swift; sourceTree = ""; }; 31EF52E0281B3BDC0034796E /* AutofillLoginListItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillLoginListItemViewModel.swift; sourceTree = ""; }; + 3736088F2ABB1E6C00629E7F /* FavoritesDisplayModeStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesDisplayModeStorage.swift; sourceTree = ""; }; + 373608912ABB430D00629E7F /* FavoritesDisplayMode+UserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FavoritesDisplayMode+UserDefaults.swift"; sourceTree = ""; }; 37445F962A155F7C0029F789 /* SyncDataProviders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncDataProviders.swift; sourceTree = ""; }; + 377D80212AB48554002AF251 /* FavoritesDisplayModeSyncHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesDisplayModeSyncHandler.swift; sourceTree = ""; }; 379E877329E97C8D001C8BB0 /* BookmarksCleanupErrorHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksCleanupErrorHandling.swift; sourceTree = ""; }; 37CBCA9D2A8A659C0050218F /* SyncSettingsAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncSettingsAdapter.swift; sourceTree = ""; }; 37CEFCAB2A673B90001EF741 /* CredentialsCleanupErrorHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialsCleanupErrorHandling.swift; sourceTree = ""; }; @@ -3329,6 +3336,14 @@ path = LocalPackages; sourceTree = ""; }; + 377D80202AB4853A002AF251 /* SettingSyncHandlers */ = { + isa = PBXGroup; + children = ( + 377D80212AB48554002AF251 /* FavoritesDisplayModeSyncHandler.swift */, + ); + name = SettingSyncHandlers; + sourceTree = ""; + }; 37DF000829F9C3F0002B7D3E /* Sync */ = { isa = PBXGroup; children = ( @@ -3985,6 +4000,7 @@ 85F98F8C296F0ED100742F4A /* Sync */ = { isa = PBXGroup; children = ( + 377D80202AB4853A002AF251 /* SettingSyncHandlers */, 85F98F97296F4CB100742F4A /* SyncAssets.xcassets */, 85F0E97229952D7A003D5181 /* DuckDuckGo Recovery Document.pdf */, 85DD44232976C7A8005CC388 /* Controllers */, @@ -4844,6 +4860,7 @@ F1D796EF1E7B07610019D451 /* BookmarksViewControllerCells.swift */, 85E58C2B28FDA94F006A801A /* FavoritesViewController.swift */, F1D796EB1E7AB8930019D451 /* SaveBookmarkActivity.swift */, + 3736088F2ABB1E6C00629E7F /* FavoritesDisplayModeStorage.swift */, ); name = Bookmarks; sourceTree = ""; @@ -5038,6 +5055,7 @@ 98B31291218CCB8C00E54DE1 /* AppDependencyProvider.swift */, 85BA58591F3506AE00C6E8CA /* AppSettings.swift */, 85BA58541F34F49E00C6E8CA /* AppUserDefaults.swift */, + 373608912ABB430D00629E7F /* FavoritesDisplayMode+UserDefaults.swift */, 850250B220D803F4002199C7 /* AtbAndVariantCleanup.swift */, 983EABB7236198F6003948D1 /* DatabaseMigration.swift */, 853C5F6021C277C7001F7A05 /* global.swift */, @@ -6244,6 +6262,7 @@ 980891A52237D4F500313A70 /* FeedbackNavigator.swift in Sources */, C1B7B52328941F2A0098FD6A /* RemoteMessagingStore.swift in Sources */, 1E8AD1C927BFAD1500ABA377 /* DirectoryMonitor.swift in Sources */, + 377D80222AB48554002AF251 /* FavoritesDisplayModeSyncHandler.swift in Sources */, 1E8AD1D127C000AB00ABA377 /* OngoingDownloadRow.swift in Sources */, 85058366219AE9EA00ED4EDB /* HomePageConfiguration.swift in Sources */, EE0153E12A6EABE0002A8B26 /* NetworkProtectionConvenienceInitialisers.swift in Sources */, @@ -6298,6 +6317,7 @@ F46FEC5727987A5F0061D9DF /* KeychainItemsDebugViewController.swift in Sources */, 02341FA62A4379CC008A1531 /* OnboardingStepViewModel.swift in Sources */, 850365F323DE087800D0F787 /* UIImageViewExtension.swift in Sources */, + 373608922ABB430D00629E7F /* FavoritesDisplayMode+UserDefaults.swift in Sources */, C160544129D6044D00B715A1 /* AutofillInterfaceUsernameTruncator.swift in Sources */, 02A54A9A2A094A17000C8FED /* AppTPHomeView.swift in Sources */, 31C70B5528045E3500FB6AD1 /* SecureVaultErrorReporter.swift in Sources */, @@ -6400,6 +6420,7 @@ 85010502292FB1000033978F /* FireproofFaviconUpdater.swift in Sources */, F1C4A70E1E57725800A6CA1B /* OmniBar.swift in Sources */, 981CA7EA2617797500E119D5 /* MainViewController+AddFavoriteFlow.swift in Sources */, + 373608902ABB1E6C00629E7F /* FavoritesDisplayModeStorage.swift in Sources */, 9872D205247DCAC100CEF398 /* TabPreviewsSource.swift in Sources */, F130D73A1E5776C500C45811 /* OmniBarDelegate.swift in Sources */, 85DFEDEF24C7EA3B00973FE7 /* SmallOmniBarState.swift in Sources */, @@ -6642,6 +6663,7 @@ buildActionMask = 2147483647; files = ( 853273AE24FEF49600E3C778 /* ColorExtension.swift in Sources */, + 373608932ABB432600629E7F /* FavoritesDisplayMode+UserDefaults.swift in Sources */, 853273B324FF114700E3C778 /* DeepLinks.swift in Sources */, 853273B424FFB36100E3C778 /* UIColorExtension.swift in Sources */, 853273AB24FEF27500E3C778 /* WidgetViews.swift in Sources */, @@ -9058,7 +9080,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 82.3.0; + version = 83.0.0; }; }; C14882EB27F211A000D59F0C /* XCRemoteSwiftPackageReference "SwiftSoup" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 129f2539b2..d9cc92bff7 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/DuckDuckGo/BrowserServicesKit", "state": { "branch": null, - "revision": "c4d5f6df0340f0a5c109dcded9801ab676de7db5", - "version": "82.3.0" + "revision": "f7e20cd37bbc0d25ae3c3f25ef52d319366613e7", + "version": "83.0.0" } }, { diff --git a/DuckDuckGo/AddOrEditBookmarkViewController.swift b/DuckDuckGo/AddOrEditBookmarkViewController.swift index 1b31051f25..188764d2ba 100644 --- a/DuckDuckGo/AddOrEditBookmarkViewController.swift +++ b/DuckDuckGo/AddOrEditBookmarkViewController.swift @@ -41,19 +41,23 @@ class AddOrEditBookmarkViewController: UIViewController { private let viewModel: BookmarkEditorViewModel private let bookmarksDatabase: CoreDataDatabase private let syncService: DDGSyncing + private let appSettings: AppSettings private var viewModelCancellable: AnyCancellable? init?(coder: NSCoder, editingEntityID: NSManagedObjectID, bookmarksDatabase: CoreDataDatabase, - syncService: DDGSyncing) { + syncService: DDGSyncing, + appSettings: AppSettings) { self.bookmarksDatabase = bookmarksDatabase self.viewModel = BookmarkEditorViewModel(editingEntityID: editingEntityID, bookmarksDatabase: bookmarksDatabase, + favoritesDisplayMode: appSettings.favoritesDisplayMode, syncService: syncService) self.syncService = syncService + self.appSettings = appSettings super.init(coder: coder) } @@ -61,13 +65,16 @@ class AddOrEditBookmarkViewController: UIViewController { init?(coder: NSCoder, parentFolderID: NSManagedObjectID?, bookmarksDatabase: CoreDataDatabase, - syncService: DDGSyncing) { + syncService: DDGSyncing, + appSettings: AppSettings) { self.bookmarksDatabase = bookmarksDatabase self.viewModel = BookmarkEditorViewModel(creatingFolderWithParentID: parentFolderID, bookmarksDatabase: bookmarksDatabase, + favoritesDisplayMode: appSettings.favoritesDisplayMode, syncService: syncService) self.syncService = syncService + self.appSettings = appSettings super.init(coder: coder) } @@ -138,7 +145,8 @@ class AddOrEditBookmarkViewController: UIViewController { coder: coder, parentFolderID: viewModel.bookmark.parent?.objectID, bookmarksDatabase: bookmarksDatabase, - syncService: syncService + syncService: syncService, + appSettings: appSettings ) else { fatalError("Failed to create controller") } diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index d49f873e3d..9b3a32e512 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -161,7 +161,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { DatabaseMigration.migrate(to: context) } - bookmarksDatabase.loadStore { context, error in + var shouldResetBookmarksSyncTimestamp = false + + bookmarksDatabase.loadStore { [weak self] context, error in guard let context = context else { if let error = error { Pixel.fire(pixel: .bookmarksCouldNotLoadDatabase, @@ -184,6 +186,21 @@ class AppDelegate: UIResponder, UIApplicationDelegate { to: context) legacyStorage?.removeStore() + do { + BookmarkUtils.migrateToFormFactorSpecificFavorites(byCopyingExistingTo: .mobile, in: context) + if context.hasChanges { + try context.save(onErrorFire: .bookmarksMigrationCouldNotPrepareMultipleFavoriteFolders) + if let syncDataProviders = self?.syncDataProviders { + syncDataProviders.bookmarksAdapter.shouldResetBookmarksSyncTimestamp = true + } else { + shouldResetBookmarksSyncTimestamp = true + } + } + } catch { + Thread.sleep(forTimeInterval: 1) + fatalError("Could not prepare Bookmarks DB structure") + } + WidgetCenter.shared.reloadAllTimelines() } @@ -235,7 +252,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate { ).wrappedValue ) ?? defaultEnvironment - syncDataProviders = SyncDataProviders(bookmarksDatabase: bookmarksDatabase, secureVaultErrorReporter: SecureVaultErrorReporter.shared) + syncDataProviders = SyncDataProviders( + bookmarksDatabase: bookmarksDatabase, + secureVaultErrorReporter: SecureVaultErrorReporter.shared, + settingHandlers: [FavoritesDisplayModeSyncHandler()], + favoritesDisplayModeStorage: FavoritesDisplayModeStorage() + ) + syncDataProviders.bookmarksAdapter.shouldResetBookmarksSyncTimestamp = shouldResetBookmarksSyncTimestamp + let syncService = DDGSync(dataProvidersSource: syncDataProviders, errorEvents: SyncErrorHandler(), log: .syncLog, environment: environment) syncService.initializeIfNeeded() self.syncService = syncService @@ -275,7 +299,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Having both in `didBecomeActive` can sometimes cause the exception when running on a physical device, so registration happens here. AppConfigurationFetch.registerBackgroundRefreshTaskHandler() WindowsBrowserWaitlist.shared.registerBackgroundRefreshTaskHandler() - RemoteMessaging.registerBackgroundRefreshTaskHandler(bookmarksDatabase: bookmarksDatabase) + RemoteMessaging.registerBackgroundRefreshTaskHandler( + bookmarksDatabase: bookmarksDatabase, + favoritesDisplayMode: AppDependencyProvider.shared.appSettings.favoritesDisplayMode + ) UNUserNotificationCenter.current().delegate = self @@ -479,7 +506,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { private func refreshRemoteMessages() { Task { - try? await RemoteMessaging.fetchAndProcess(bookmarksDatabase: self.bookmarksDatabase) + try? await RemoteMessaging.fetchAndProcess( + bookmarksDatabase: self.bookmarksDatabase, + favoritesDisplayMode: AppDependencyProvider.shared.appSettings.favoritesDisplayMode + ) } } diff --git a/DuckDuckGo/AppSettings.swift b/DuckDuckGo/AppSettings.swift index 45b1354046..0c3f6dd748 100644 --- a/DuckDuckGo/AppSettings.swift +++ b/DuckDuckGo/AppSettings.swift @@ -17,6 +17,8 @@ // limitations under the License. // +import Bookmarks + protocol AppSettings: AnyObject { var autocomplete: Bool { get set } var currentThemeName: ThemeName { get set } @@ -34,6 +36,8 @@ protocol AppSettings: AnyObject { var currentAddressBarPosition: AddressBarPosition { get set } var textSize: Int { get set } + + var favoritesDisplayMode: FavoritesDisplayMode { get set } var autofillCredentialsEnabled: Bool { get set } var autofillCredentialsSavePromptShowAtLeastOnce: Bool { get set } @@ -47,4 +51,7 @@ protocol AppSettings: AnyObject { var autoconsentPromptSeen: Bool { get set } var autoconsentEnabled: Bool { get set } + + var isSyncBookmarksPaused: Bool { get } + var isSyncCredentialsPaused: Bool { get } } diff --git a/DuckDuckGo/AppUserDefaults.swift b/DuckDuckGo/AppUserDefaults.swift index efbfe51d59..f06c8d7180 100644 --- a/DuckDuckGo/AppUserDefaults.swift +++ b/DuckDuckGo/AppUserDefaults.swift @@ -18,6 +18,7 @@ // import Foundation +import Bookmarks import Core import WidgetKit @@ -27,6 +28,9 @@ public class AppUserDefaults: AppSettings { public static let doNotSellStatusChange = Notification.Name("com.duckduckgo.app.DoNotSellStatusChange") public static let currentFireButtonAnimationChange = Notification.Name("com.duckduckgo.app.CurrentFireButtonAnimationChange") public static let textSizeChange = Notification.Name("com.duckduckgo.app.TextSizeChange") + public static let favoritesDisplayModeChange = Notification.Name("com.duckduckgo.app.FavoritesDisplayModeChange") + public static let syncPausedStateChanged = SyncBookmarksAdapter.syncBookmarksPausedStateChanged + public static let syncCredentialsPausedStateChanged = SyncCredentialsAdapter.syncCredentialsPausedStateChanged public static let autofillEnabledChange = Notification.Name("com.duckduckgo.app.AutofillEnabledChange") public static let didVerifyInternalUser = Notification.Name("com.duckduckgo.app.DidVerifyInternalUser") public static let inspectableWebViewsToggled = Notification.Name("com.duckduckgo.app.DidToggleInspectableWebViews") @@ -35,7 +39,7 @@ public class AppUserDefaults: AppSettings { private let groupName: String - private struct Keys { + struct Keys { static let autocompleteKey = "com.duckduckgo.app.autocompleteDisabledKey" static let currentThemeNameKey = "com.duckduckgo.app.currentThemeNameKey" @@ -62,6 +66,8 @@ public class AppUserDefaults: AppSettings { static let autofillCredentialsEnabled = "com.duckduckgo.ios.autofillCredentialsEnabled" static let autofillIsNewInstallForOnByDefault = "com.duckduckgo.ios.autofillIsNewInstallForOnByDefault" + + static let favoritesDisplayMode = "com.duckduckgo.ios.favoritesDisplayMode" } private struct DebugKeys { @@ -72,6 +78,10 @@ public class AppUserDefaults: AppSettings { return UserDefaults(suiteName: groupName) } + private var bookmarksUserDefaults: UserDefaults? { + UserDefaults(suiteName: "group.com.duckduckgo.bookmarks") + } + lazy var featureFlagger = AppDependencyProvider.shared.featureFlagger init(groupName: String = "group.com.duckduckgo.app") { @@ -195,6 +205,25 @@ public class AppUserDefaults: AppSettings { @UserDefaultsWrapper(key: .textSize, defaultValue: 100) var textSize: Int + @UserDefaultsWrapper(key: .syncBookmarksPaused, defaultValue: false) + var isSyncBookmarksPaused: Bool + + @UserDefaultsWrapper(key: .syncCredentialsPaused, defaultValue: false) + var isSyncCredentialsPaused: Bool + + public var favoritesDisplayMode: FavoritesDisplayMode { + get { + guard let string = userDefaults?.string(forKey: Keys.favoritesDisplayMode), let favoritesDisplayMode = FavoritesDisplayMode(string) else { + return .default + } + return favoritesDisplayMode + } + set { + userDefaults?.setValue(newValue.description, forKey: Keys.favoritesDisplayMode) + bookmarksUserDefaults?.setValue(newValue.description, forKey: Keys.favoritesDisplayMode) + } + } + private func setAutofillCredentialsEnabledAutomaticallyIfNecessary() { if autofillCredentialsHasBeenEnabledAutomaticallyIfNecessary { return diff --git a/DuckDuckGo/Base.lproj/Settings.storyboard b/DuckDuckGo/Base.lproj/Settings.storyboard index ac8a7668d7..679524ecd2 100644 --- a/DuckDuckGo/Base.lproj/Settings.storyboard +++ b/DuckDuckGo/Base.lproj/Settings.storyboard @@ -104,7 +104,7 @@ -