diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index 16dac94aa4..6b1a028efb 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -475,6 +475,10 @@ extension Pixel { case syncFailedToMigrate case syncFailedToLoadAccount case syncFailedToSetupEngine + case syncBookmarksCountLimitExceededDaily + case syncCredentialsCountLimitExceededDaily + case syncBookmarksRequestSizeLimitExceededDaily + case syncCredentialsRequestSizeLimitExceededDaily case syncSentUnauthenticatedRequest case syncMetadataCouldNotLoadDatabase @@ -952,6 +956,10 @@ extension Pixel.Event { 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" diff --git a/Core/SyncBookmarksAdapter.swift b/Core/SyncBookmarksAdapter.swift index 0bc525ca30..8ed20b5e11 100644 --- a/Core/SyncBookmarksAdapter.swift +++ b/Core/SyncBookmarksAdapter.swift @@ -36,6 +36,14 @@ public final class SyncBookmarksAdapter { public let databaseCleaner: BookmarkDatabaseCleaner public let syncDidCompletePublisher: AnyPublisher public let widgetRefreshCancellable: AnyCancellable + public static let syncBookmarksPausedStateChanged = Notification.Name("com.duckduckgo.app.SyncPausedStateChanged") + + @UserDefaultsWrapper(key: .syncBookmarksPaused, defaultValue: false) + static private var isSyncBookmarksPaused: Bool { + didSet { + NotificationCenter.default.post(name: syncBookmarksPausedStateChanged, object: nil) + } + } public init(database: CoreDataDatabase, favoritesDisplayModeStorage: FavoritesDisplayModeStoring) { self.database = database @@ -71,6 +79,7 @@ public final class SyncBookmarksAdapter { metadataStore: metadataStore, syncDidUpdateData: { [syncDidCompleteSubject] in syncDidCompleteSubject.send() + Self.isSyncBookmarksPaused = false } ) @@ -79,6 +88,16 @@ public final class SyncBookmarksAdapter { switch error { case let syncError as SyncError: Pixel.fire(pixel: .syncBookmarksFailed, error: syncError) + // If bookmarks count limit has been exceeded + if syncError == .unexpectedStatusCode(409) { + Self.isSyncBookmarksPaused = true + DailyPixel.fire(pixel: .syncBookmarksCountLimitExceededDaily) + } + // If bookmarks request size limit has been exceeded + if syncError == .unexpectedStatusCode(413) { + Self.isSyncBookmarksPaused = true + DailyPixel.fire(pixel: .syncBookmarksRequestSizeLimitExceededDaily) + } default: let nsError = error as NSError if nsError.domain != NSURLErrorDomain { diff --git a/Core/SyncCredentialsAdapter.swift b/Core/SyncCredentialsAdapter.swift index e4b96e01ef..1ab12c0e72 100644 --- a/Core/SyncCredentialsAdapter.swift +++ b/Core/SyncCredentialsAdapter.swift @@ -30,6 +30,14 @@ public final class SyncCredentialsAdapter { public private(set) var provider: CredentialsProvider? public let databaseCleaner: CredentialsDatabaseCleaner public let syncDidCompletePublisher: AnyPublisher + public static let syncCredentialsPausedStateChanged = SyncBookmarksAdapter.syncBookmarksPausedStateChanged + + @UserDefaultsWrapper(key: .syncCredentialsPaused, defaultValue: false) + static private var isSyncCredentialsPaused: Bool { + didSet { + NotificationCenter.default.post(name: syncCredentialsPausedStateChanged, object: nil) + } + } public init(secureVaultFactory: AutofillVaultFactory = AutofillSecureVaultFactory, secureVaultErrorReporter: SecureVaultErrorReporting) { syncDidCompletePublisher = syncDidCompleteSubject.eraseToAnyPublisher() @@ -63,6 +71,7 @@ public final class SyncCredentialsAdapter { metadataStore: metadataStore, syncDidUpdateData: { [weak self] in self?.syncDidCompleteSubject.send() + Self.isSyncCredentialsPaused = false } ) @@ -71,6 +80,16 @@ public final class SyncCredentialsAdapter { switch error { case let syncError as SyncError: Pixel.fire(pixel: .syncCredentialsFailed, error: syncError) + // If credentials count limit has been exceeded + if syncError == .unexpectedStatusCode(409) { + Self.isSyncCredentialsPaused = true + DailyPixel.fire(pixel: .syncCredentialsCountLimitExceededDaily) + } + // If credentials request size limit has been exceeded + if syncError == .unexpectedStatusCode(413) { + Self.isSyncCredentialsPaused = true + DailyPixel.fire(pixel: .syncCredentialsRequestSizeLimitExceededDaily) + } default: let nsError = error as NSError if nsError.domain != NSURLErrorDomain { diff --git a/Core/UserDefaultsPropertyWrapper.swift b/Core/UserDefaultsPropertyWrapper.swift index 02e3fc09d5..1043f8db93 100644 --- a/Core/UserDefaultsPropertyWrapper.swift +++ b/Core/UserDefaultsPropertyWrapper.swift @@ -97,6 +97,8 @@ 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 networkProtectionDebugOptionAlwaysOnDisabled = "com.duckduckgo.network-protection.always-on.disabled" } diff --git a/DuckDuckGo/AppSettings.swift b/DuckDuckGo/AppSettings.swift index aba8536f09..b0de01863a 100644 --- a/DuckDuckGo/AppSettings.swift +++ b/DuckDuckGo/AppSettings.swift @@ -50,4 +50,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 d40dfe53d5..c77c49e9cd 100644 --- a/DuckDuckGo/AppUserDefaults.swift +++ b/DuckDuckGo/AppUserDefaults.swift @@ -29,6 +29,8 @@ public class AppUserDefaults: AppSettings { 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") @@ -188,6 +190,12 @@ 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 { diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index 898044b527..5c08ed8291 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -876,7 +876,7 @@ class MainViewController: UIViewController { suggestionTrayController?.didHide() } - fileprivate func launchAutofillLogins(with currentTabUrl: URL? = nil) { + func launchAutofillLogins(with currentTabUrl: URL? = nil) { let appSettings = AppDependencyProvider.shared.appSettings let autofillSettingsViewController = AutofillLoginSettingsListViewController( appSettings: appSettings, diff --git a/DuckDuckGo/SyncSettingsViewController+SyncDelegate.swift b/DuckDuckGo/SyncSettingsViewController+SyncDelegate.swift index a869f7d50a..75e3f2e990 100644 --- a/DuckDuckGo/SyncSettingsViewController+SyncDelegate.swift +++ b/DuckDuckGo/SyncSettingsViewController+SyncDelegate.swift @@ -25,6 +25,18 @@ import AVFoundation extension SyncSettingsViewController: SyncManagementViewModelDelegate { + func launchAutofillViewController() { + guard let mainVC = view.window?.rootViewController as? MainViewController else { return } + self.dismiss(animated: true) + mainVC.launchAutofillLogins() + } + + func launchBookmarksViewController() { + guard let mainVC = view.window?.rootViewController as? MainViewController else { return } + self.dismiss(animated: true) + mainVC.segueToBookmarks() + } + func updateDeviceName(_ name: String) { Task { @MainActor in rootView.model.devices = [] diff --git a/DuckDuckGo/SyncSettingsViewController.swift b/DuckDuckGo/SyncSettingsViewController.swift index a5a80aeff0..6bba63102b 100644 --- a/DuckDuckGo/SyncSettingsViewController.swift +++ b/DuckDuckGo/SyncSettingsViewController.swift @@ -56,6 +56,7 @@ class SyncSettingsViewController: UIHostingController { self.init(rootView: SyncSettingsView(model: viewModel)) setUpFavoritesDisplayModeSwitch(viewModel, appSettings) + setUpSyncPaused(viewModel, appSettings) refreshForState(syncService.authState) syncService.authStatePublisher @@ -95,6 +96,18 @@ class SyncSettingsViewController: UIHostingController { .store(in: &cancellables) } + private func setUpSyncPaused(_ viewModel: SyncSettingsViewModel, _ appSettings: AppSettings) { + viewModel.isSyncBookmarksPaused = appSettings.isSyncBookmarksPaused + viewModel.isSyncCredentialsPaused = appSettings.isSyncCredentialsPaused + NotificationCenter.default.publisher(for: AppUserDefaults.Notifications.syncPausedStateChanged) + .receive(on: DispatchQueue.main) + .sink { _ in + viewModel.isSyncBookmarksPaused = appSettings.isSyncBookmarksPaused + viewModel.isSyncCredentialsPaused = appSettings.isSyncCredentialsPaused + } + .store(in: &cancellables) + } + override func viewDidLoad() { super.viewDidLoad() applyTheme(ThemeManager.shared.currentTheme) diff --git a/DuckDuckGoTests/AppSettingsMock.swift b/DuckDuckGoTests/AppSettingsMock.swift index 00687a6ef4..3725c2ebc9 100644 --- a/DuckDuckGoTests/AppSettingsMock.swift +++ b/DuckDuckGoTests/AppSettingsMock.swift @@ -22,6 +22,10 @@ import Foundation @testable import DuckDuckGo class AppSettingsMock: AppSettings { + + var isSyncBookmarksPaused: Bool = false + + var isSyncCredentialsPaused: Bool = false var autofillCredentialsEnabled: Bool = false diff --git a/DuckDuckGoTests/SyncManagementViewModelTests.swift b/DuckDuckGoTests/SyncManagementViewModelTests.swift index f85a9a79b1..7964c0910c 100644 --- a/DuckDuckGoTests/SyncManagementViewModelTests.swift +++ b/DuckDuckGoTests/SyncManagementViewModelTests.swift @@ -82,6 +82,26 @@ class SyncManagementViewModelTests: XCTestCase, SyncManagementViewModelDelegate ]) } + func testWhenManageBookmarksCalled_BookmarksVCIsLaunched() { + model.manageBookmarks() + + // You can either test one individual call was made x number of times or check for a whole number of calls + monitor.assert(#selector(launchBookmarksViewController).description, calls: 1) + monitor.assertCalls([ + #selector(launchBookmarksViewController).description: 1 + ]) + } + + func testWhenManageLogindCalled_AutofillVCIsLaunched() { + model.manageLogins() + + // You can either test one individual call was made x number of times or check for a whole number of calls + monitor.assert(#selector(launchAutofillViewController).description, calls: 1) + monitor.assertCalls([ + #selector(launchAutofillViewController).description: 1 + ]) + } + // MARK: Delegate functions func showSyncWithAnotherDeviceEnterText() { @@ -148,6 +168,14 @@ class SyncManagementViewModelTests: XCTestCase, SyncManagementViewModelDelegate monitor.incrementCalls(function: #function.cleaningFunctionName()) } + func launchBookmarksViewController() { + monitor.incrementCalls(function: #function.cleaningFunctionName()) + } + + func launchAutofillViewController() { + monitor.incrementCalls(function: #function.cleaningFunctionName()) + } + } // MARK: An idea... can be made more public if works out diff --git a/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/SyncSettingsViewModel.swift b/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/SyncSettingsViewModel.swift index 4fda055e90..3e13d1c9a1 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/SyncSettingsViewModel.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/SyncSettingsViewModel.swift @@ -36,6 +36,8 @@ public protocol SyncManagementViewModelDelegate: AnyObject { func updateDeviceName(_ name: String) func refreshDevices(clearDevices: Bool) func updateOptions() + func launchBookmarksViewController() + func launchAutofillViewController() } public class SyncSettingsViewModel: ObservableObject { @@ -77,6 +79,8 @@ public class SyncSettingsViewModel: ObservableObject { @Published public var isFaviconsSyncEnabled = false @Published public var isUnifiedFavoritesEnabled = true @Published public var isSyncingDevices = false + @Published public var isSyncBookmarksPaused = false + @Published public var isSyncCredentialsPaused = false @Published var isBusy = false @Published var recoveryCode = "" @@ -148,4 +152,12 @@ public class SyncSettingsViewModel: ObservableObject { delegate?.createAccountAndStartSyncing(optionsViewModel: self) } + public func manageBookmarks() { + delegate?.launchBookmarksViewController() + } + + public func manageLogins() { + delegate?.launchAutofillViewController() + } + } diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/Internal/UserText.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/Internal/UserText.swift index 387e61c277..1fc5c3535b 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Views/Internal/UserText.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/Internal/UserText.swift @@ -112,5 +112,12 @@ struct UserText { return "\"\(name)\" will no longer be able to access your synced data." } + static let syncLimitExceededTitle = "⚠️ Sync Paused" + static let bookmarksLimitExceededDescription = "Bookmark limit exceeded. Delete some to resume syncing." + static let credentialsLimitExceededDescription = "Logins limit exceeded. Delete some to resume syncing." + static let bookmarksLimitExceededAction = "Manage Bookmarks" + static let credentialsLimitExceededAction = "Manage Logins" + + } // swiftlint:enable line_length diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/SyncSettingsView.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/SyncSettingsView.swift index a4aa3b898b..f2fff4bf6c 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Views/SyncSettingsView.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/SyncSettingsView.swift @@ -40,10 +40,20 @@ public struct SyncSettingsView: View { } } else { List { + workInProgress() + if model.isSyncEnabled { turnOffSync() + if $model.isSyncBookmarksPaused.wrappedValue { + syncPaused(for: .bookmarks) + } + + if $model.isSyncCredentialsPaused.wrappedValue { + syncPaused(for: .credentials) + } + devices() syncNewDevice() @@ -58,9 +68,7 @@ public struct SyncSettingsView: View { deleteAllData() } else { - - workInProgress() - + syncWithAnotherDeviceView() singleDeviceSetUpView() @@ -87,54 +95,118 @@ public struct SyncSettingsView: View { @State var selectedDevice: SyncSettingsViewModel.Device? @ViewBuilder - func devices() -> some View { + func workInProgress() -> some View { Section { - if model.devices.isEmpty { - ProgressView() - .padding() + EmptyView() + } footer: { + VStack(alignment: .leading, spacing: 4) { + Text("Work in Progress") + .font(.system(size: 12, weight: .semibold)) + .foregroundColor(.black) + + // swiftlint:disable line_length + Text("This feature is viewable to internal users only and is still being developed and tested. Currently you can create accounts, connect and manage devices, and sync bookmarks, favorites, Autofill logins and Email Protection status. **[More Info](https://app.asana.com/0/1201493110486074/1203756800930481/f)**") + .foregroundColor(.black) + .font(.system(size: 11, weight: .regular)) + // swiftlint:enable line_length } + .padding() + .background(RoundedRectangle(cornerRadius: 8).foregroundColor(.yellow)) + .padding(.bottom, 10) + } - ForEach(model.devices) { device in - Button { - selectedDevice = device - } label: { - HStack { - deviceTypeImage(device) - Text(device.name) - .foregroundColor(.primary) - Spacer() - if device.isThisDevice { - Text(UserText.thisDevice) - .foregroundColor(.secondary) - } - } - } + } + +} + +// Sync Set up Views +extension SyncSettingsView { + @ViewBuilder + func recoverYourDataView() -> some View { + Section { + Button(UserText.recoverYourData) { + model.showRecoverDataView() } - } header: { - Text(UserText.connectedDevicesTitle) } - .sheet(item: $selectedDevice) { device in - Group { - if device.isThisDevice { - EditDeviceView(model: model.createEditDeviceModel(device)) - } else { - RemoveDeviceView(model: model.createRemoveDeviceModel(device)) + } + + @ViewBuilder + func footerView() -> some View { + Section {} footer: { + Text(UserText.syncSettingsFooter) + .daxFootnoteRegular() + .foregroundColor(.secondary) + } + } + + @ViewBuilder + func singleDeviceSetUpView() -> some View { + Section { + HStack { + VStack(alignment: .leading, spacing: 4) { + Text(UserText.singleDeviceSetUpTitle) + .daxBodyBold() + Text(UserText.singleDeviceSetUpInstruction) + .daxBodyRegular() } + Spacer() + Image("Device-Mobile-Upload-96") + } - .modifier { - if #available(iOS 16.0, *) { - $0.presentationDetents([.medium]) - } else { - $0 + if model.isBusy { + SwiftUI.ProgressView() + } else { + Button(UserText.turnSyncOn) { + model.startSyncPressed() } } } - .onReceive(timer) { _ in - if selectedDevice == nil { - model.delegate?.refreshDevices(clearDevices: false) + } + + @ViewBuilder + func syncWithAnotherDeviceView() -> some View { + Section { + HStack { + VStack(alignment: .leading, spacing: 4) { + Text(UserText.syncWithAnotherDeviceTitle) + .daxBodyBold() + Text(UserText.syncWithAnotherDeviceMessage) + .daxBodyRegular() + } + Spacer() + Image("Sync-Pair-96") + + } + Button(UserText.scanQRCode) { + model.scanQRCode() + } + Button(UserText.enterTextCode) { + model.showEnterTextView() } } + } +} +// Sync Enabled Views +extension SyncSettingsView { + @ViewBuilder + func deleteAllData() -> some View { + Section { + Button(UserText.settingsDeleteAllButton) { + model.deleteAllData() + } + } + } + + @ViewBuilder + func saveRecoveryPDF() -> some View { + Section { + Button(UserText.settingsSaveRecoveryPDFButton) { + model.saveRecoveryPDF() + } + } footer: { + Text(UserText.settingsRecoveryPDFWarning) + } } @ViewBuilder @@ -172,69 +244,54 @@ public struct SyncSettingsView: View { } @ViewBuilder - func saveRecoveryPDF() -> some View { - Section { - Button(UserText.settingsSaveRecoveryPDFButton) { - model.saveRecoveryPDF() - } - } footer: { - Text(UserText.settingsRecoveryPDFWarning) - } - } - - @ViewBuilder - func deleteAllData() -> some View { + func devices() -> some View { Section { - Button(UserText.settingsDeleteAllButton) { - model.deleteAllData() + if model.devices.isEmpty { + ProgressView() + .padding() } - } - } - - @ViewBuilder - func workInProgress() -> some View { - Section { - EmptyView() - } footer: { - VStack(alignment: .leading, spacing: 4) { - Text("Work in Progress") - .font(.system(size: 12, weight: .semibold)) - .foregroundColor(.black) - // swiftlint:disable line_length - Text("This feature is viewable to internal users only and is still being developed and tested. Currently you can create accounts, connect and manage devices, and sync bookmarks, favorites, Autofill logins and Email Protection status. **[More Info](https://app.asana.com/0/1201493110486074/1203756800930481/f)**") - .foregroundColor(.black) - .font(.system(size: 11, weight: .regular)) - // swiftlint:enable line_length + ForEach(model.devices) { device in + Button { + selectedDevice = device + } label: { + HStack { + deviceTypeImage(device) + Text(device.name) + .foregroundColor(.primary) + Spacer() + if device.isThisDevice { + Text(UserText.thisDevice) + .foregroundColor(.secondary) + } + } + } } - .padding() - .background(RoundedRectangle(cornerRadius: 8).foregroundColor(.yellow)) - .padding(.bottom, 10) + } header: { + Text(UserText.connectedDevicesTitle) } - - } - - @ViewBuilder - func syncWithAnotherDeviceView() -> some View { - Section { - HStack { - VStack(alignment: .leading, spacing: 4) { - Text(UserText.syncWithAnotherDeviceTitle) - .daxBodyBold() - Text(UserText.syncWithAnotherDeviceMessage) - .daxBodyRegular() + .sheet(item: $selectedDevice) { device in + Group { + if device.isThisDevice { + EditDeviceView(model: model.createEditDeviceModel(device)) + } else { + RemoveDeviceView(model: model.createRemoveDeviceModel(device)) } - Spacer() - Image("Sync-Pair-96") - } - Button(UserText.scanQRCode) { - model.scanQRCode() + .modifier { + if #available(iOS 16.0, *) { + $0.presentationDetents([.medium]) + } else { + $0 + } } - Button(UserText.enterTextCode) { - model.showEnterTextView() + } + .onReceive(timer) { _ in + if selectedDevice == nil { + model.delegate?.refreshDevices(clearDevices: false) } } + } @ViewBuilder @@ -251,47 +308,46 @@ public struct SyncSettingsView: View { } @ViewBuilder - func singleDeviceSetUpView() -> some View { - Section { - HStack { - VStack(alignment: .leading, spacing: 4) { - Text(UserText.singleDeviceSetUpTitle) - .daxBodyBold() - Text(UserText.singleDeviceSetUpInstruction) - .daxBodyRegular() - } - Spacer() - Image("Device-Mobile-Upload-96") - + func syncPaused(for itemType: LimitedItemType) -> some View { + var explanation: String { + switch itemType { + case .bookmarks: + return UserText.bookmarksLimitExceededDescription + case .credentials: + return UserText.credentialsLimitExceededDescription } - if model.isBusy { - SwiftUI.ProgressView() - } else { - Button(UserText.turnSyncOn) { - model.startSyncPressed() - } + } + var buttonTitle: String { + switch itemType { + case .bookmarks: + return UserText.bookmarksLimitExceededAction + case .credentials: + return UserText.credentialsLimitExceededAction } } - } - @ViewBuilder - func recoverYourDataView() -> some View { Section { - Button(UserText.recoverYourData) { - model.showRecoverDataView() + VStack(alignment: .leading, spacing: 4) { + Text(UserText.syncLimitExceededTitle) + .daxBodyBold() + Text(explanation) + .daxBodyRegular() + } + Button(buttonTitle) { + switch itemType { + case .bookmarks: + model.manageBookmarks() + case .credentials: + model.manageLogins() + } } } } - @ViewBuilder - func footerView() -> some View { - Section {} footer: { - Text(UserText.syncSettingsFooter) - .daxFootnoteRegular() - .foregroundColor(.secondary) - } + enum LimitedItemType { + case bookmarks + case credentials } - } // Extension to apply custom view modifier @@ -318,10 +374,8 @@ public struct OptionsView: View { } } } - } header: { Text(UserText.options) } } - }