From 9ad737ce62da9878740a9713407d090b8ab6fed5 Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Fri, 10 Nov 2023 09:50:56 +0000 Subject: [PATCH 1/4] 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 2/4] 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 3/4] 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 @@ -