diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 03707dda3a..a401054013 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -1222,7 +1222,8 @@ struct UserText { static let upToDate = NSLocalizedString("settings.up.to.date", value: "DuckDuckGo is up to date", comment: "Label informing users the app is currently up to date and no update is required.") static let newerVersionAvailable = NSLocalizedString("settings.newer.version.available", value: "Newer version available", comment: "Label informing users the newer version of the app is available to install.") static let lastChecked = NSLocalizedString("settings.last.checked", value: "Last checked", comment: "Label informing users what is the last time the app checked for the update.") - static let runUpdate = NSLocalizedString("settings.restart.to.update", value: "Update DuckDuckGo", comment: "Button label triggering restart and update of the application.") + static let restartToUpdate = NSLocalizedString("settings.restart.to.update", value: "Restart to Update", comment: "Button label triggering restart and update of the application.") + static let runUpdate = NSLocalizedString("settings.run.update", value: "Update DuckDuckGo", comment: "Button label triggering update of the application.") static let retryUpdate = NSLocalizedString("settings.retry.update", value: "Retry Update", comment: "Button label triggering a retry of the update.") static let browserUpdatedNotification = NSLocalizedString("notification.browser.updated", value: "Browser Updated", comment: "Notification informing user the app has been updated") static let browserDowngradedNotification = NSLocalizedString("notification.browser.downgraded", value: "Browser Downgraded", comment: "Notification informing user the app has been downgraded") diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index f9fc0a1f3c..d47f3bd0b7 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -55457,7 +55457,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "Update DuckDuckGo" + "value" : "Restart to Update" } }, "es" : { @@ -55516,6 +55516,18 @@ } } }, + "settings.run.update" : { + "comment" : "Button label triggering update of the application.", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Update DuckDuckGo" + } + } + } + }, "settings.up.to.date" : { "comment" : "Label informing users the app is currently up to date and no update is required.", "extractionState" : "extracted_with_value", diff --git a/DuckDuckGo/Preferences/Model/AboutPreferences.swift b/DuckDuckGo/Preferences/Model/AboutPreferences.swift index dafbb1e361..63b5433ce8 100644 --- a/DuckDuckGo/Preferences/Model/AboutPreferences.swift +++ b/DuckDuckGo/Preferences/Model/AboutPreferences.swift @@ -25,29 +25,11 @@ final class AboutPreferences: ObservableObject, PreferencesTabOpening { static let shared = AboutPreferences() #if SPARKLE - enum UpdateState { - - case upToDate - case updateCycle(UpdateCycleProgress) - - init(from update: Update?, progress: UpdateCycleProgress) { - if let update, !update.isInstalled { - self = .updateCycle(progress) - } else if progress.isFailed { - self = .updateCycle(progress) - } else { - self = .upToDate - } - } - } - @Published var updateState = UpdateState.upToDate -#if SPARKLE var updateController: UpdateControllerProtocol? { return Application.appDelegate.updateController } -#endif var areAutomaticUpdatesEnabled: Bool { get { diff --git a/DuckDuckGo/Preferences/View/PreferencesAboutView.swift b/DuckDuckGo/Preferences/View/PreferencesAboutView.swift index ee753f679a..9dc9ce618b 100644 --- a/DuckDuckGo/Preferences/View/PreferencesAboutView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesAboutView.swift @@ -248,7 +248,7 @@ extension Preferences { .buttonStyle(UpdateButtonStyle(enabled: true)) case .updateCycle(let progress): if hasPendingUpdate { - Button(UserText.runUpdate) { + Button(model.areAutomaticUpdatesEnabled ? UserText.restartToUpdate : UserText.runUpdate) { model.runUpdate() } .buttonStyle(UpdateButtonStyle(enabled: true)) diff --git a/DuckDuckGo/Updates/ReleaseNotesTabExtension.swift b/DuckDuckGo/Updates/ReleaseNotesTabExtension.swift index 41c88ea007..797975ead7 100644 --- a/DuckDuckGo/Updates/ReleaseNotesTabExtension.swift +++ b/DuckDuckGo/Updates/ReleaseNotesTabExtension.swift @@ -32,6 +32,14 @@ protocol ReleaseNotesUserScriptProvider { extension UserScripts: ReleaseNotesUserScriptProvider {} public struct ReleaseNotesValues: Codable { + enum Status: String { + case loaded + case loading + case updateReady + case updateDownloading + case updatePreparing + case updateError + } let status: String let currentVersion: String @@ -40,7 +48,7 @@ public struct ReleaseNotesValues: Codable { let releaseTitle: String? let releaseNotes: [String]? let releaseNotesPrivacyPro: [String]? - + let downloadProgress: Double? } final class ReleaseNotesTabExtension: NavigationResponder { @@ -115,49 +123,82 @@ extension TabExtensions { extension ReleaseNotesValues { - init(status: String, + init(status: Status, currentVersion: String, - lastUpdate: UInt) { - self.init(status: status, - currentVersion: currentVersion, - latestVersion: nil, - lastUpdate: lastUpdate, - releaseTitle: nil, - releaseNotes: nil, - releaseNotesPrivacyPro: nil) + latestVersion: String? = nil, + lastUpdate: UInt, + releaseTitle: String? = nil, + releaseNotes: [String]? = nil, + releaseNotesPrivacyPro: [String]? = nil, + downloadProgress: Double? = nil) { + self.status = status.rawValue + self.currentVersion = currentVersion + self.latestVersion = latestVersion + self.lastUpdate = lastUpdate + self.releaseTitle = releaseTitle + self.releaseNotes = releaseNotes + self.releaseNotesPrivacyPro = releaseNotesPrivacyPro + self.downloadProgress = downloadProgress } init(from updateController: UpdateController?) { let currentVersion = "\(AppVersion().versionNumber) (\(AppVersion().buildNumber))" let lastUpdate = UInt((updateController?.lastUpdateCheckDate ?? Date()).timeIntervalSince1970) - let status: String let latestVersion: String - guard let updateController, updateController.updateProgress.isIdle else { - self.init(status: "loading", + guard let updateController else { + self.init(status: .loaded, currentVersion: currentVersion, lastUpdate: lastUpdate) return } - if let latestUpdate = updateController.latestUpdate { - status = latestUpdate.isInstalled ? "loaded" : "updateReady" - latestVersion = "\(latestUpdate.version) (\(latestUpdate.build))" - self.init(status: status, - currentVersion: currentVersion, - latestVersion: latestVersion, - lastUpdate: lastUpdate, - releaseTitle: latestUpdate.title, - releaseNotes: latestUpdate.releaseNotes, - releaseNotesPrivacyPro: latestUpdate.releaseNotesPrivacyPro) - return - } else { - self.init(status: "loaded", + let updateState = UpdateState(from: updateController.latestUpdate, progress: updateController.updateProgress) + let hasPendingUpdate = updateController.hasPendingUpdate + + switch updateState { + case .upToDate: + self.init(status: .loaded, currentVersion: currentVersion, lastUpdate: lastUpdate) + case .updateCycle(let progress): + if let latestUpdate = updateController.latestUpdate { + latestVersion = "\(latestUpdate.version) (\(latestUpdate.build))" + let status = hasPendingUpdate ? .updateReady : progress.toStatus + self.init(status: status, + currentVersion: currentVersion, + latestVersion: latestVersion, + lastUpdate: lastUpdate, + releaseTitle: latestUpdate.title, + releaseNotes: latestUpdate.releaseNotes, + releaseNotesPrivacyPro: latestUpdate.releaseNotesPrivacyPro, + downloadProgress: progress.toDownloadProgress) + } else { + self.init(status: .loaded, + currentVersion: currentVersion, + lastUpdate: lastUpdate) + } + } + } +} + +private extension UpdateCycleProgress { + var toStatus: ReleaseNotesValues.Status { + switch self { + case .updateCycleDidStart: return .loading + case .downloadDidStart, .downloading: return .updateDownloading + case .extractionDidStart, .extracting, .readyToInstallAndRelaunch, .installationDidStart, .installing: return .updatePreparing + case .updaterError: return .updateError + case .updateCycleNotStarted, .updateCycleDone: return .updateReady } } + var toDownloadProgress: Double? { + guard case .downloading(let percentage) = self else { + return nil + } + return percentage + } } #else diff --git a/DuckDuckGo/Updates/ReleaseNotesUserScript.swift b/DuckDuckGo/Updates/ReleaseNotesUserScript.swift index 199d0da2d0..620c2ff3b9 100644 --- a/DuckDuckGo/Updates/ReleaseNotesUserScript.swift +++ b/DuckDuckGo/Updates/ReleaseNotesUserScript.swift @@ -43,6 +43,7 @@ final class ReleaseNotesUserScript: NSObject, Subfeature { case reportPageException case reportInitException case browserRestart + case retryUpdate } override init() { @@ -57,7 +58,8 @@ final class ReleaseNotesUserScript: NSObject, Subfeature { .initialSetup: initialSetup, .reportPageException: reportPageException, .reportInitException: reportInitException, - .browserRestart: browserRestart + .browserRestart: browserRestart, + .retryUpdate: retryUpdate, ] @MainActor @@ -108,6 +110,14 @@ extension ReleaseNotesUserScript { return InitialSetupResult(env: env, locale: Locale.current.identifier) } + @MainActor + private func retryUpdate(params: Any, original: WKScriptMessage) async throws -> Encodable? { + DispatchQueue.main.async { [weak self] in + self?.updateController.checkForUpdateIfNeeded() + } + return nil + } + struct InitialSetupResult: Encodable { let env: String let locale: String diff --git a/DuckDuckGo/Updates/UpdateUserDriver.swift b/DuckDuckGo/Updates/UpdateUserDriver.swift index a6c9ae034c..067197fc79 100644 --- a/DuckDuckGo/Updates/UpdateUserDriver.swift +++ b/DuckDuckGo/Updates/UpdateUserDriver.swift @@ -25,6 +25,21 @@ import os.log #if SPARKLE +enum UpdateState { + case upToDate + case updateCycle(UpdateCycleProgress) + + init(from update: Update?, progress: UpdateCycleProgress) { + if let update, !update.isInstalled { + self = .updateCycle(progress) + } else if progress.isFailed { + self = .updateCycle(progress) + } else { + self = .upToDate + } + } +} + enum UpdateCycleProgress { case updateCycleNotStarted case updateCycleDidStart