Improve update UI error details
This commit is contained in:
parent
17a3e2033f
commit
0441efc675
18 changed files with 915 additions and 82 deletions
|
|
@ -5,9 +5,14 @@ import Sparkle
|
|||
|
||||
class UpdateViewModel: ObservableObject {
|
||||
@Published var state: UpdateState = .idle
|
||||
@Published var overrideState: UpdateState?
|
||||
|
||||
var effectiveState: UpdateState {
|
||||
overrideState ?? state
|
||||
}
|
||||
|
||||
var text: String {
|
||||
switch state {
|
||||
switch effectiveState {
|
||||
case .idle:
|
||||
return ""
|
||||
case .permissionRequest:
|
||||
|
|
@ -33,12 +38,12 @@ class UpdateViewModel: ObservableObject {
|
|||
case .notFound:
|
||||
return "No Updates Available"
|
||||
case .error(let err):
|
||||
return err.error.localizedDescription
|
||||
return Self.userFacingErrorTitle(for: err.error)
|
||||
}
|
||||
}
|
||||
|
||||
var maxWidthText: String {
|
||||
switch state {
|
||||
switch effectiveState {
|
||||
case .downloading:
|
||||
return "Downloading: 100%"
|
||||
case .extracting:
|
||||
|
|
@ -49,7 +54,7 @@ class UpdateViewModel: ObservableObject {
|
|||
}
|
||||
|
||||
var iconName: String? {
|
||||
switch state {
|
||||
switch effectiveState {
|
||||
case .idle:
|
||||
return nil
|
||||
case .permissionRequest:
|
||||
|
|
@ -72,7 +77,7 @@ class UpdateViewModel: ObservableObject {
|
|||
}
|
||||
|
||||
var description: String {
|
||||
switch state {
|
||||
switch effectiveState {
|
||||
case .idle:
|
||||
return ""
|
||||
case .permissionRequest:
|
||||
|
|
@ -89,13 +94,13 @@ class UpdateViewModel: ObservableObject {
|
|||
return install.isAutoUpdate ? "Restart to Complete Update" : "Installing update and preparing to restart"
|
||||
case .notFound:
|
||||
return "You are running the latest version"
|
||||
case .error:
|
||||
return "An error occurred during the update process"
|
||||
case .error(let err):
|
||||
return Self.userFacingErrorMessage(for: err.error)
|
||||
}
|
||||
}
|
||||
|
||||
var badge: String? {
|
||||
switch state {
|
||||
switch effectiveState {
|
||||
case .updateAvailable(let update):
|
||||
let version = update.appcastItem.displayVersionString
|
||||
return version.isEmpty ? nil : version
|
||||
|
|
@ -113,7 +118,7 @@ class UpdateViewModel: ObservableObject {
|
|||
}
|
||||
|
||||
var iconColor: Color {
|
||||
switch state {
|
||||
switch effectiveState {
|
||||
case .idle:
|
||||
return .secondary
|
||||
case .permissionRequest:
|
||||
|
|
@ -132,7 +137,7 @@ class UpdateViewModel: ObservableObject {
|
|||
}
|
||||
|
||||
var backgroundColor: Color {
|
||||
switch state {
|
||||
switch effectiveState {
|
||||
case .permissionRequest:
|
||||
return Color(nsColor: NSColor.systemBlue.blended(withFraction: 0.3, of: .black) ?? .systemBlue)
|
||||
case .updateAvailable:
|
||||
|
|
@ -147,7 +152,7 @@ class UpdateViewModel: ObservableObject {
|
|||
}
|
||||
|
||||
var foregroundColor: Color {
|
||||
switch state {
|
||||
switch effectiveState {
|
||||
case .permissionRequest:
|
||||
return .white
|
||||
case .updateAvailable:
|
||||
|
|
@ -160,6 +165,165 @@ class UpdateViewModel: ObservableObject {
|
|||
return .primary
|
||||
}
|
||||
}
|
||||
|
||||
static func userFacingErrorTitle(for error: Swift.Error) -> String {
|
||||
let nsError = error as NSError
|
||||
if let networkError = networkError(from: nsError) {
|
||||
switch networkError.code {
|
||||
case NSURLErrorNotConnectedToInternet:
|
||||
return "No Internet Connection"
|
||||
case NSURLErrorTimedOut:
|
||||
return "Update Timed Out"
|
||||
case NSURLErrorCannotFindHost:
|
||||
return "Server Not Found"
|
||||
case NSURLErrorCannotConnectToHost:
|
||||
return "Server Unreachable"
|
||||
case NSURLErrorNetworkConnectionLost:
|
||||
return "Connection Lost"
|
||||
case NSURLErrorSecureConnectionFailed,
|
||||
NSURLErrorServerCertificateUntrusted,
|
||||
NSURLErrorServerCertificateHasBadDate,
|
||||
NSURLErrorServerCertificateHasUnknownRoot,
|
||||
NSURLErrorServerCertificateNotYetValid:
|
||||
return "Secure Connection Failed"
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
if nsError.domain == SUSparkleErrorDomain {
|
||||
switch nsError.code {
|
||||
case 2001:
|
||||
return "Couldn't Download Update"
|
||||
case 1000, 1002:
|
||||
return "Update Feed Error"
|
||||
case 4:
|
||||
return "Invalid Update Feed"
|
||||
case 3:
|
||||
return "Insecure Update Feed"
|
||||
case 1, 2, 3001, 3002:
|
||||
return "Update Signature Error"
|
||||
case 1003, 1005:
|
||||
return "App Location Issue"
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
return "Update Failed"
|
||||
}
|
||||
|
||||
static func userFacingErrorMessage(for error: Swift.Error) -> String {
|
||||
let nsError = error as NSError
|
||||
if let networkError = networkError(from: nsError) {
|
||||
switch networkError.code {
|
||||
case NSURLErrorNotConnectedToInternet:
|
||||
return "cmux can’t reach the update server. Check your internet connection and try again."
|
||||
case NSURLErrorTimedOut:
|
||||
return "The update server took too long to respond. Try again in a moment."
|
||||
case NSURLErrorCannotFindHost:
|
||||
return "The update server can’t be found. Check your connection or try again later."
|
||||
case NSURLErrorCannotConnectToHost:
|
||||
return "cmux couldn’t connect to the update server. Check your connection or try again later."
|
||||
case NSURLErrorNetworkConnectionLost:
|
||||
return "The network connection was lost while checking for updates. Try again."
|
||||
case NSURLErrorSecureConnectionFailed,
|
||||
NSURLErrorServerCertificateUntrusted,
|
||||
NSURLErrorServerCertificateHasBadDate,
|
||||
NSURLErrorServerCertificateHasUnknownRoot,
|
||||
NSURLErrorServerCertificateNotYetValid:
|
||||
return "A secure connection to the update server couldn’t be established. Try again later."
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
if nsError.domain == SUSparkleErrorDomain {
|
||||
switch nsError.code {
|
||||
case 2001:
|
||||
return "cmux couldn’t download the update feed. Check your connection and try again."
|
||||
case 1000, 1002:
|
||||
return "The update feed could not be read. Please try again later."
|
||||
case 4:
|
||||
return "The update feed URL is invalid. Please contact support."
|
||||
case 3:
|
||||
return "The update feed is insecure. Please contact support."
|
||||
case 1, 2, 3001, 3002:
|
||||
return "The update’s signature could not be verified. Please try again later."
|
||||
case 1003, 1005:
|
||||
return "Move cmux into Applications and relaunch to enable updates."
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
return nsError.localizedDescription
|
||||
}
|
||||
|
||||
static func errorDetails(for error: Swift.Error, technicalDetails: String?, feedURLString: String?) -> String {
|
||||
let nsError = error as NSError
|
||||
var lines: [String] = []
|
||||
lines.append("Message: \(nsError.localizedDescription)")
|
||||
lines.append("Domain: \(nsError.domain)")
|
||||
if nsError.domain == SUSparkleErrorDomain,
|
||||
let sparkleName = sparkleErrorCodeName(for: nsError.code) {
|
||||
lines.append("Code: \(sparkleName) (\(nsError.code))")
|
||||
} else {
|
||||
lines.append("Code: \(nsError.code)")
|
||||
}
|
||||
|
||||
if let url = nsError.userInfo[NSURLErrorFailingURLErrorKey] as? URL {
|
||||
lines.append("URL: \(url.absoluteString)")
|
||||
} else if let urlString = nsError.userInfo[NSURLErrorFailingURLStringErrorKey] as? String {
|
||||
lines.append("URL: \(urlString)")
|
||||
}
|
||||
|
||||
if let failure = nsError.userInfo[NSLocalizedFailureReasonErrorKey] as? String,
|
||||
!failure.isEmpty {
|
||||
lines.append("Failure: \(failure)")
|
||||
}
|
||||
if let recovery = nsError.userInfo[NSLocalizedRecoverySuggestionErrorKey] as? String,
|
||||
!recovery.isEmpty {
|
||||
lines.append("Recovery: \(recovery)")
|
||||
}
|
||||
|
||||
if let feedURLString, !feedURLString.isEmpty {
|
||||
lines.append("Feed: \(feedURLString)")
|
||||
}
|
||||
|
||||
if let technicalDetails, !technicalDetails.isEmpty {
|
||||
lines.append("Debug: \(technicalDetails)")
|
||||
}
|
||||
|
||||
lines.append("Log: \(UpdateLogStore.shared.logPath())")
|
||||
return lines.joined(separator: "\n")
|
||||
}
|
||||
|
||||
private static func networkError(from error: NSError) -> NSError? {
|
||||
if error.domain == NSURLErrorDomain {
|
||||
return error
|
||||
}
|
||||
if let underlying = error.userInfo[NSUnderlyingErrorKey] as? NSError,
|
||||
underlying.domain == NSURLErrorDomain {
|
||||
return underlying
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func sparkleErrorCodeName(for code: Int) -> String? {
|
||||
switch code {
|
||||
case 1: return "SUNoPublicDSAFoundError"
|
||||
case 2: return "SUInsufficientSigningError"
|
||||
case 3: return "SUInsecureFeedURLError"
|
||||
case 4: return "SUInvalidFeedURLError"
|
||||
case 1000: return "SUAppcastParseError"
|
||||
case 1001: return "SUNoUpdateError"
|
||||
case 1002: return "SUAppcastError"
|
||||
case 1003: return "SURunningFromDiskImageError"
|
||||
case 1005: return "SURunningTranslocated"
|
||||
case 2001: return "SUDownloadError"
|
||||
case 3001: return "SUSignatureError"
|
||||
case 3002: return "SUValidationError"
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum UpdateState: Equatable {
|
||||
|
|
@ -325,6 +489,20 @@ enum UpdateState: Equatable {
|
|||
let error: any Swift.Error
|
||||
let retry: () -> Void
|
||||
let dismiss: () -> Void
|
||||
let technicalDetails: String?
|
||||
let feedURLString: String?
|
||||
|
||||
init(error: any Swift.Error,
|
||||
retry: @escaping () -> Void,
|
||||
dismiss: @escaping () -> Void,
|
||||
technicalDetails: String? = nil,
|
||||
feedURLString: String? = nil) {
|
||||
self.error = error
|
||||
self.retry = retry
|
||||
self.dismiss = dismiss
|
||||
self.technicalDetails = technicalDetails
|
||||
self.feedURLString = feedURLString
|
||||
}
|
||||
}
|
||||
|
||||
struct Downloading {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue