diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index bf83796dcf..307d5f5ada 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -371,6 +371,35 @@ final class AppDelegate: NSObject, NSApplicationDelegate, FileDownloadManagerDel } subscribeSyncQueueToScreenLockedNotifications() + subscribeToSyncFeatureFlags(syncService) + } + + @UserDefaultsWrapper(key: .syncDidShowSyncPausedByFeatureFlagAlert, defaultValue: false) + private var syncDidShowSyncPausedByFeatureFlagAlert: Bool + + private func subscribeToSyncFeatureFlags(_ syncService: DDGSync) { + syncFeatureFlagsCancellable = syncService.featureFlagsPublisher + .dropFirst() + .map { $0.contains(.dataSyncing) } + .receive(on: DispatchQueue.main) + .sink { [weak self, weak syncService] isDataSyncingAvailable in + if isDataSyncingAvailable { + self?.syncDidShowSyncPausedByFeatureFlagAlert = false + } else if syncService?.authState == .active, self?.syncDidShowSyncPausedByFeatureFlagAlert == false { + let isSyncUIVisible = syncService?.featureFlags.contains(.userInterface) == true + let alert = NSAlert.dataSyncingDisabledByFeatureFlag(showLearnMore: isSyncUIVisible) + let response = alert.runModal() + self?.syncDidShowSyncPausedByFeatureFlagAlert = true + + switch response { + case .alertSecondButtonReturn: + alert.window.sheetParent?.endSheet(alert.window) + WindowControllersManager.shared.showPreferencesTab(withSelectedPane: .sync) + default: + break + } + } + } } private func subscribeSyncQueueToScreenLockedNotifications() { diff --git a/DuckDuckGo/Common/Extensions/NSAlertExtension.swift b/DuckDuckGo/Common/Extensions/NSAlertExtension.swift index 06d86aa2fc..628ddeddfc 100644 --- a/DuckDuckGo/Common/Extensions/NSAlertExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSAlertExtension.swift @@ -221,6 +221,18 @@ extension NSAlert { return alert } + static func dataSyncingDisabledByFeatureFlag(showLearnMore: Bool, upgradeRequired: Bool = false) -> NSAlert { + let alert = NSAlert() + alert.messageText = UserText.syncPausedTitle + alert.informativeText = upgradeRequired ? UserText.syncUnavailableMessageUpgradeRequired : UserText.syncUnavailableMessage + alert.alertStyle = .warning + alert.addButton(withTitle: UserText.ok) + if showLearnMore { + alert.addButton(withTitle: UserText.learnMore) + } + return alert + } + static func customConfigurationAlert(configurationUrl: String) -> NSAlert { let alert = NSAlert() alert.messageText = "Set custom configuration URL:" diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index d4084d6d47..0eb91fe989 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -503,6 +503,9 @@ struct UserText { static let syncBookmarkPausedAlertDescription = NSLocalizedString("alert.sync-bookmarks-paused-description", value: "You have exceeded the bookmarks sync limit. Try deleting some bookmarks. Until this is resolved your bookmarks will not be backed up.", comment: "Description for alert shown when sync bookmarks paused for too many items") static let syncCredentialsPausedAlertTitle = NSLocalizedString("alert.sync-credentials-paused-title", value: "Passwords Sync is Paused", comment: "Title for alert shown when sync credentials paused for too many items") static let syncCredentialsPausedAlertDescription = NSLocalizedString("alert.sync-credentials-paused-description", value: "You have exceeded the passwords sync limit. Try deleting some passwords. Until this is resolved your passwords will not be backed up.", comment: "Description for alert shown when sync credentials paused for too many items") + static let syncPausedTitle = NSLocalizedString("alert.sync.warning.sync-paused", value: "Sync & Backup is Paused", comment: "Title of the warning message") + static let syncUnavailableMessage = NSLocalizedString("alert.sync.warning.sync-unavailable-message", value: "Sorry, but Sync & Backup is currently unavailable. Please try again later.", comment: "Data syncing unavailable warning message") + static let syncUnavailableMessageUpgradeRequired = NSLocalizedString("alert.sync.warning.data-syncing-disabled-upgrade-required", value: "Sorry, but Sync & Backup is no longer available in this app version. Please update DuckDuckGo to the latest version to continue.", comment: "Data syncing unavailable warning message") static let defaultBrowser = NSLocalizedString("preferences.default-browser", value: "Default Browser", comment: "Show default browser preferences") static let appearance = NSLocalizedString("preferences.appearance", value: "Appearance", comment: "Show appearance preferences") static let privacy = NSLocalizedString("preferences.privacy", value: "Privacy", comment: "Show privacy browser preferences") diff --git a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift index 7bcf40d4b8..9ce0d13a89 100644 --- a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift +++ b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift @@ -176,6 +176,7 @@ public struct UserDefaultsWrapper { case syncIsEligibleForFaviconsFetcherOnboarding = "sync.is-eligible-for-favicons-fetcher-onboarding" case syncDidPresentFaviconsFetcherOnboarding = "sync.did-present-favicons-fetcher-onboarding" case syncDidMigrateToImprovedListsHandling = "sync.did-migrate-to-improved-lists-handling" + case syncDidShowSyncPausedByFeatureFlagAlert = "sync.did-show-sync-paused-by-feature-flag-alert" } enum RemovedKeys: String, CaseIterable { diff --git a/DuckDuckGo/Preferences/Model/PreferencesSection.swift b/DuckDuckGo/Preferences/Model/PreferencesSection.swift index ad061c7ed0..4e32a5af5f 100644 --- a/DuckDuckGo/Preferences/Model/PreferencesSection.swift +++ b/DuckDuckGo/Preferences/Model/PreferencesSection.swift @@ -92,6 +92,7 @@ enum PreferencePaneIdentifier: String, Equatable, Hashable, Identifiable { self.init(rawValue: path) } + @MainActor var displayName: String { switch self { case .general: @@ -99,7 +100,9 @@ enum PreferencePaneIdentifier: String, Equatable, Hashable, Identifiable { case .sync: let isSyncBookmarksPaused = UserDefaults.standard.bool(forKey: UserDefaultsWrapper.Key.syncBookmarksPaused.rawValue) let isSyncCredentialsPaused = UserDefaults.standard.bool(forKey: UserDefaultsWrapper.Key.syncCredentialsPaused.rawValue) - if isSyncBookmarksPaused || isSyncCredentialsPaused { + let syncService = NSApp.delegateTyped.syncService + let isDataSyncingDisabled = syncService?.featureFlags.contains(.dataSyncing) == false && syncService?.authState == .active + if isSyncBookmarksPaused || isSyncCredentialsPaused || isDataSyncingDisabled { return UserText.sync + " ⚠️" } return UserText.sync diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/ManagementView/SyncEnabledView.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/ManagementView/SyncEnabledView.swift index 3878a7d693..53ffb0513e 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Views/ManagementView/SyncEnabledView.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/ManagementView/SyncEnabledView.swift @@ -154,7 +154,7 @@ struct SyncEnabledView: View where ViewModel: ManagementViewModel { if model.isDataSyncingAvailable { EmptyView() } else { - SyncWarningMessage(title: UserText.serviceUnavailable, message: UserText.warningSyncDisabled) + SyncWarningMessage(title: UserText.syncPausedTitle, message: UserText.syncUnavailableMessage) } } diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/ManagementView/SyncSetupView.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/ManagementView/SyncSetupView.swift index 1ea3bf81a7..133f1b4b2a 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Views/ManagementView/SyncSetupView.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/ManagementView/SyncSetupView.swift @@ -63,11 +63,8 @@ struct SyncSetupView: View where ViewModel: ManagementViewModel { @ViewBuilder fileprivate func syncUnavailableView() -> some View { - if !model.isDataSyncingAvailable || !model.isConnectingDevicesAvailable { - SyncWarningMessage(title: UserText.serviceUnavailable, message: UserText.warningSyncDisabled) - .padding(.top, 16) - } else if !model.isAccountCreationAvailable { - SyncWarningMessage(title: UserText.serviceUnavailable, message: UserText.warningAccountCreationDisabled) + if !model.isDataSyncingAvailable || !model.isConnectingDevicesAvailable || !model.isAccountCreationAvailable { + SyncWarningMessage(title: UserText.syncUnavailableTitle, message: UserText.syncUnavailableMessage) .padding(.top, 16) } else { EmptyView() diff --git a/LocalPackages/SyncUI/Sources/SyncUI/internal/UserText.swift b/LocalPackages/SyncUI/Sources/SyncUI/internal/UserText.swift index 2b7d2a1059..57d66f925e 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/internal/UserText.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/internal/UserText.swift @@ -159,7 +159,8 @@ enum UserText { static let keepFaviconsUpdated = NSLocalizedString("prefrences.sync.keep-favicons-updated", value: "Keep Bookmarks Icons Updated", comment: "Title of the confirmation button for favicons fetching") // Sync Feature Flags - static let serviceUnavailable = NSLocalizedString("sync.warning.service-unavailable", value: "Service Unavailable", comment: "Title of the warning message") - static let warningSyncDisabled = NSLocalizedString("sync.warning.sync-disabled", value: "We apologize, but the service is currently unavailable. Please try again later.", comment: "Sync unavailable warning message") - static let warningAccountCreationDisabled = NSLocalizedString("sync.warning.account-creation-disabled", value: "We apologize, but new account creation is currently unavailable for this service. Please try again later.", comment: "Sync unavailable warning message") + static let syncUnavailableTitle = NSLocalizedString("sync.warning.sync-unavailable", value: "Sync & Backup is Unavailable", comment: "Title of the warning message") + static let syncPausedTitle = NSLocalizedString("sync.warning.sync-paused", value: "Sync & Backup is Paused", comment: "Title of the warning message") + static let syncUnavailableMessage = NSLocalizedString("sync.warning.sync-unavailable-message", value: "Sorry, but Sync & Backup is currently unavailable. Please try again later.", comment: "Data syncing unavailable warning message") + static let syncUnavailableMessageUpgradeRequired = NSLocalizedString("sync.warning.data-syncing-disabled-upgrade-required", value: "Sorry, but Sync & Backup is no longer available in this app version. Please update DuckDuckGo to the latest version to continue.", comment: "Data syncing unavailable warning message") }