diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 14f3786eea..c73d5f7243 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -14335,8 +14335,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { - kind = exactVersion; - version = 199.4.0; + kind = revision; + revision = d40a17055e1b67380886874f64daa882bbc7da0c; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 395ed3ae6f..713061aeaa 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "f1a033cc4b97ab6b4d845815e825ba179c02635a", - "version" : "199.4.0" + "revision" : "d40a17055e1b67380886874f64daa882bbc7da0c" } }, { @@ -41,8 +40,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "revision" : "1ed569676555d493c9c5575eaed22aa02569aac9", - "version" : "6.19.0" + "revision" : "32fa43a6ee861cb782771601f6885fb95006c02f" } }, { diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 58f928000a..e4515d79ed 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -1234,7 +1234,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 b013eaee4d..28f0c914d6 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -56297,7 +56297,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "Update DuckDuckGo" + "value" : "Restart to Update" } }, "es" : { @@ -56356,6 +56356,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..692545634b 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,8 @@ public struct ReleaseNotesValues: Codable { let releaseTitle: String? let releaseNotes: [String]? let releaseNotesPrivacyPro: [String]? - + let downloadProgress: Double? + let automaticUpdate: Bool? } final class ReleaseNotesTabExtension: NavigationResponder { @@ -115,49 +124,83 @@ 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, + automaticUpdate: Bool? = 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 + self.automaticUpdate = automaticUpdate } 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, let latestUpdate = updateController.latestUpdate else { + self.init(status: updateController?.updateProgress.toStatus ?? .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", - currentVersion: currentVersion, - lastUpdate: lastUpdate) + let updateState = UpdateState(from: updateController.latestUpdate, progress: updateController.updateProgress) + + let status: Status + let downloadProgress: Double? + switch updateState { + case .upToDate: + status = .loaded + downloadProgress = nil + case .updateCycle(let progress): + status = updateController.hasPendingUpdate ? .updateReady : progress.toStatus + downloadProgress = progress.toDownloadProgress } + + self.init(status: status, + currentVersion: currentVersion, + latestVersion: latestUpdate.versionString, + lastUpdate: lastUpdate, + releaseTitle: latestUpdate.title, + releaseNotes: latestUpdate.releaseNotes, + releaseNotesPrivacyPro: latestUpdate.releaseNotesPrivacyPro, + downloadProgress: downloadProgress, + automaticUpdate: updateController.areAutomaticUpdatesEnabled) + } +} + +private extension Update { + var versionString: String? { + "\(version) \(build)" } +} +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/UpdateController.swift b/DuckDuckGo/Updates/UpdateController.swift index 69bf87dc92..5cbe7969c1 100644 --- a/DuckDuckGo/Updates/UpdateController.swift +++ b/DuckDuckGo/Updates/UpdateController.swift @@ -173,6 +173,8 @@ final class UpdateController: NSObject, UpdateControllerProtocol { updater?.automaticallyChecksForUpdates = false updater?.automaticallyDownloadsUpdates = false updater?.updateCheckInterval = 0 +#else + checkForUpdateIfNeeded() #endif } 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