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
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 9930057c0b..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.2.2;
+ 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 39d7ce6624..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": "989e306052bc284a1202fad1087f8b88e515a966",
- "version": "82.2.2"
+ "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 @@
-