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 @@ -