diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme
index b272e5bb34..294ba1aeb6 100644
--- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme
+++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme
@@ -299,6 +299,10 @@
argument = "-NSConstraintBasedLayoutVisualizeMutuallyExclusiveConstraints YES"
isEnabled = "YES">
+
+
diff --git a/DuckDuckGo/Preferences/Model/AboutPreferences.swift b/DuckDuckGo/Preferences/Model/AboutPreferences.swift
index 63b5433ce8..c664a6bedb 100644
--- a/DuckDuckGo/Preferences/Model/AboutPreferences.swift
+++ b/DuckDuckGo/Preferences/Model/AboutPreferences.swift
@@ -71,7 +71,7 @@ final class AboutPreferences: ObservableObject, PreferencesTabOpening {
#if SPARKLE
func checkForUpdate() {
- updateController?.checkForUpdateIfNeeded()
+ updateController?.checkForUpdateSkippingRollout()
}
func runUpdate() {
diff --git a/DuckDuckGo/Preferences/View/PreferencesAboutView.swift b/DuckDuckGo/Preferences/View/PreferencesAboutView.swift
index 3ba655735a..d6fe08b06a 100644
--- a/DuckDuckGo/Preferences/View/PreferencesAboutView.swift
+++ b/DuckDuckGo/Preferences/View/PreferencesAboutView.swift
@@ -51,11 +51,6 @@ extension Preferences {
}
}
}
- .onAppear {
-#if SPARKLE && !DEBUG
- model.checkForUpdate()
-#endif
- }
}
}
diff --git a/DuckDuckGo/Updates/ReleaseNotesTabExtension.swift b/DuckDuckGo/Updates/ReleaseNotesTabExtension.swift
index 570ea39d05..d258c5e523 100644
--- a/DuckDuckGo/Updates/ReleaseNotesTabExtension.swift
+++ b/DuckDuckGo/Updates/ReleaseNotesTabExtension.swift
@@ -105,11 +105,11 @@ final class ReleaseNotesTabExtension: NavigationResponder {
@MainActor
func navigationDidFinish(_ navigation: Navigation) {
-#if !DEBUG
guard NSApp.runType != .uiTests, navigation.url == .releaseNotes else { return }
let updateController = Application.appDelegate.updateController!
- updateController.checkForUpdateIfNeeded()
-#endif
+ if updateController.latestUpdate?.needsLatestReleaseNote == true {
+ updateController.checkForUpdateSkippingRollout()
+ }
}
}
diff --git a/DuckDuckGo/Updates/ReleaseNotesUserScript.swift b/DuckDuckGo/Updates/ReleaseNotesUserScript.swift
index 1013958aba..ff07147751 100644
--- a/DuckDuckGo/Updates/ReleaseNotesUserScript.swift
+++ b/DuckDuckGo/Updates/ReleaseNotesUserScript.swift
@@ -113,7 +113,7 @@ extension ReleaseNotesUserScript {
@MainActor
private func retryUpdate(params: Any, original: WKScriptMessage) async throws -> Encodable? {
DispatchQueue.main.async { [weak self] in
- self?.updateController.checkForUpdateIfNeeded()
+ self?.updateController.checkForUpdateSkippingRollout()
}
return nil
}
diff --git a/DuckDuckGo/Updates/Update.swift b/DuckDuckGo/Updates/Update.swift
index d36a7b10d1..bfa43a8db6 100644
--- a/DuckDuckGo/Updates/Update.swift
+++ b/DuckDuckGo/Updates/Update.swift
@@ -34,6 +34,7 @@ final class Update {
let date: Date
let releaseNotes: [String]
let releaseNotesPrivacyPro: [String]
+ let needsLatestReleaseNote: Bool
var title: String {
let formatter = DateFormatter()
@@ -47,7 +48,8 @@ final class Update {
build: String,
date: Date,
releaseNotes: [String],
- releaseNotesPrivacyPro: [String]) {
+ releaseNotesPrivacyPro: [String],
+ needsLatestReleaseNote: Bool) {
self.isInstalled = isInstalled
self.type = type
self.version = version
@@ -55,12 +57,13 @@ final class Update {
self.date = date
self.releaseNotes = releaseNotes
self.releaseNotesPrivacyPro = releaseNotesPrivacyPro
+ self.needsLatestReleaseNote = needsLatestReleaseNote
}
}
extension Update {
- convenience init(appcastItem: SUAppcastItem, isInstalled: Bool) {
+ convenience init(appcastItem: SUAppcastItem, isInstalled: Bool, needsLatestReleaseNote: Bool) {
let isCritical = appcastItem.isCriticalUpdate
let version = appcastItem.displayVersionString
let build = appcastItem.versionString
@@ -73,7 +76,8 @@ extension Update {
build: build,
date: date,
releaseNotes: releaseNotes,
- releaseNotesPrivacyPro: releaseNotesPrivacyPro)
+ releaseNotesPrivacyPro: releaseNotesPrivacyPro,
+ needsLatestReleaseNote: needsLatestReleaseNote)
}
}
diff --git a/DuckDuckGo/Updates/UpdateController.swift b/DuckDuckGo/Updates/UpdateController.swift
index e45a117790..cd88d87159 100644
--- a/DuckDuckGo/Updates/UpdateController.swift
+++ b/DuckDuckGo/Updates/UpdateController.swift
@@ -42,7 +42,8 @@ protocol UpdateControllerProtocol: AnyObject {
var lastUpdateCheckDate: Date? { get }
- func checkForUpdateIfNeeded()
+ func checkForUpdateRespectingRollout()
+ func checkForUpdateSkippingRollout()
func runUpdate()
var areAutomaticUpdatesEnabled: Bool { get set }
@@ -64,13 +65,20 @@ final class UpdateController: NSObject, UpdateControllerProtocol {
struct UpdateCheckResult {
let item: SUAppcastItem
let isInstalled: Bool
+ let needsLatestReleaseNote: Bool
+
+ init(item: SUAppcastItem, isInstalled: Bool, needsLatestReleaseNote: Bool = false) {
+ self.item = item
+ self.isInstalled = isInstalled
+ self.needsLatestReleaseNote = needsLatestReleaseNote
+ }
}
private var cachedUpdateResult: UpdateCheckResult?
@Published private(set) var updateProgress = UpdateCycleProgress.default {
didSet {
if let cachedUpdateResult {
- latestUpdate = Update(appcastItem: cachedUpdateResult.item, isInstalled: cachedUpdateResult.isInstalled)
+ latestUpdate = Update(appcastItem: cachedUpdateResult.item, isInstalled: cachedUpdateResult.isInstalled, needsLatestReleaseNote: cachedUpdateResult.needsLatestReleaseNote)
hasPendingUpdate = latestUpdate?.isInstalled == false && updateProgress.isDone && userDriver?.isResumable == true
needsNotificationDot = hasPendingUpdate
}
@@ -101,6 +109,7 @@ final class UpdateController: NSObject, UpdateControllerProtocol {
if oldValue != areAutomaticUpdatesEnabled {
userDriver?.cancelAndDismissCurrentUpdate()
try? configureUpdater()
+ checkForUpdateSkippingRollout()
}
}
}
@@ -129,6 +138,7 @@ final class UpdateController: NSObject, UpdateControllerProtocol {
super.init()
try? configureUpdater()
+ checkForUpdateRespectingRollout()
}
func checkNewApplicationVersion() {
@@ -142,10 +152,18 @@ final class UpdateController: NSObject, UpdateControllerProtocol {
}
}
- func checkForUpdateIfNeeded() {
+ func checkForUpdateRespectingRollout() {
+ guard let updater, !updater.sessionInProgress else { return }
+
+ Logger.updates.log("Checking for updates respecting rollout")
+
+ updater.checkForUpdatesInBackground()
+ }
+
+ func checkForUpdateSkippingRollout() {
guard let updater, !updater.sessionInProgress else { return }
- Logger.updates.log("Checking for updates")
+ Logger.updates.log("Checking for updates skipping rollout")
updater.checkForUpdates()
}
@@ -168,14 +186,6 @@ final class UpdateController: NSObject, UpdateControllerProtocol {
.assign(to: \.updateProgress, onWeaklyHeld: self)
try updater?.start()
-
-#if DEBUG
- updater?.automaticallyChecksForUpdates = false
- updater?.automaticallyDownloadsUpdates = false
- updater?.updateCheckInterval = 0
-#else
- checkForUpdateIfNeeded()
-#endif
}
private func showUpdateNotificationIfNeeded() {
@@ -249,12 +259,18 @@ extension UpdateController: SPUUpdaterDelegate {
func updaterDidNotFindUpdate(_ updater: SPUUpdater, error: any Error) {
let nsError = error as NSError
- guard let item = nsError.userInfo["SULatestAppcastItemFound"] as? SUAppcastItem else { return }
+ guard let item = nsError.userInfo[SPULatestAppcastItemFoundKey] as? SUAppcastItem else { return }
Logger.updates.log("Updater did not find update: \(String(describing: item.displayVersionString))(\(String(describing: item.versionString)))")
PixelKit.fire(DebugEvent(GeneralPixel.updaterDidNotFindUpdate, error: error))
- cachedUpdateResult = UpdateCheckResult(item: item, isInstalled: true)
+ // Edge case: User upgrades to latest version within their rollout group
+ // But fetched release notes are outdated due to rollout group reset
+ let needsLatestReleaseNote = {
+ guard let reason = nsError.userInfo[SPUNoUpdateFoundReasonKey] as? Int else { return false }
+ return reason == Int(Sparkle.SPUNoUpdateFoundReason.onNewerThanLatestVersion.rawValue)
+ }()
+ cachedUpdateResult = UpdateCheckResult(item: item, isInstalled: true, needsLatestReleaseNote: needsLatestReleaseNote)
}
func updater(_ updater: SPUUpdater, didDownloadUpdate item: SUAppcastItem) {
@@ -274,6 +290,9 @@ extension UpdateController: SPUUpdaterDelegate {
if error == nil {
Logger.updates.log("Updater did finish update cycle")
updateProgress = .updateCycleDone
+ } else if let errorCode = (error as? NSError)?.code, errorCode == Int(Sparkle.SUError.noUpdateError.rawValue) {
+ Logger.updates.log("Updater did finish update cycle with no update found")
+ updateProgress = .updateCycleDone
} else {
Logger.updates.log("Updater did finish update cycle with error")
}