fix(apple): actually show user-friendly alert messages (#8282)

Before, we would receive an `NSError` object and the type-matching
wouldn't take effect at all, causing the default alert to show every
time. This solves that by introducing a `UserFriendlyError` protocol
which is more robust against the two main `Error` and `NSError`
variants.
This commit is contained in:
Jamil
2025-02-28 14:12:24 +00:00
committed by GitHub
parent 280dc6c97b
commit ab7e805fdd
5 changed files with 116 additions and 94 deletions

View File

@@ -143,7 +143,7 @@ struct GrantVPNView: View {
NSApp.activate(ignoringOtherApps: true)
} catch {
Log.error(error)
await macOSAlert.show(for: error)
macOSAlert.show(for: error)
}
}
}
@@ -165,7 +165,7 @@ struct GrantVPNView: View {
}
} catch {
Log.error(error)
await macOSAlert.show(for: error)
macOSAlert.show(for: error)
}
}
}

View File

@@ -683,7 +683,7 @@ public final class MenuBar: NSObject, ObservableObject {
try await WebAuthSession.signIn(store: store)
} catch {
Log.error(error)
await macOSAlert.show(for: error)
macOSAlert.show(for: error)
}
}
}
@@ -713,7 +713,7 @@ public final class MenuBar: NSObject, ObservableObject {
}
} catch {
Log.error(error)
await macOSAlert.show(for: error)
macOSAlert.show(for: error)
}
}
}

View File

@@ -534,7 +534,7 @@ public struct SettingsView: View {
Log.error(error)
}
await macOSAlert.show(for: error)
macOSAlert.show(for: error)
}
self.isExportingLogs = false

View File

@@ -10,148 +10,136 @@ import SystemExtensions
import AppKit
import NetworkExtension
@MainActor
struct macOSAlert { // swiftlint:disable:this type_name
static func show(for error: Error) async {
guard let message = userMessage(for: error)
else { return }
// swiftlint:disable cyclomatic_complexity
// swiftlint:disable function_body_length
protocol UserFriendlyError {
func userMessage() -> String?
}
let alert = NSAlert()
alert.messageText = message
alert.alertStyle = .critical
_ = await withCheckedContinuation { continuation in
continuation.resume(returning: alert.runModal())
}
}
// NEVPNError
private static func userMessage(for error: NEVPNError) -> String? {
return {
switch error.code {
extension NEVPNError: UserFriendlyError {
func userMessage() -> String? {
switch code {
// Code 1
case .configurationDisabled:
return """
case .configurationDisabled:
return """
The VPN configuration appears to be disabled. Please remove the Firezone
VPN configuration in System Settings and try again.
"""
// Code 2
case .configurationInvalid:
return """
case .configurationInvalid:
return """
The VPN configuration appears to be invalid. Please remove the Firezone
VPN configuration in System Settings and try again.
"""
// Code 3
case .connectionFailed:
return """
case .connectionFailed:
return """
The VPN connection failed. Try signing in again.
"""
// Code 4
case .configurationStale:
return """
case .configurationStale:
return """
The VPN configuration appears to be stale. Please remove the Firezone
VPN configuration in System Settings and try again.
"""
// Code 5
case .configurationReadWriteFailed:
return """
case .configurationReadWriteFailed:
return """
Could not read or write the VPN configuration. Try removing the Firezone
VPN configuration from System Settings if this issue persists.
"""
// Code 6
case .configurationUnknown:
return """
case .configurationUnknown:
return """
An unknown VPN configuration error occurred. Try removing the Firezone
VPN configuration from System Settings if this issue persists.
"""
@unknown default:
return "\(error)"
}
}()
@unknown default:
return "\(self)"
}
}
}
// OSSystemExtensionError
// swiftlint:disable:next cyclomatic_complexity function_body_length
private static func userMessage(for error: OSSystemExtensionError) -> String? {
return {
switch error.code {
extension OSSystemExtensionError: UserFriendlyError {
func userMessage() -> String? {
switch code {
// Code 1
case .unknown:
return """
// Code 1
case .unknown:
return """
An unknown error occurred. Please try enabling the system extension again.
If the issue persists, contact your administrator.
"""
// Code 2
case .missingEntitlement:
return """
// Code 2
case .missingEntitlement:
return """
The system extension appears to be missing an entitlement. Please try
downloading and installing Firezone again.
"""
// Code 3
case .unsupportedParentBundleLocation:
return """
// Code 3
case .unsupportedParentBundleLocation:
return """
Please ensure Firezone.app is launched from the /Applications folder
and try again.
"""
// Code 4
case .extensionNotFound:
return """
// Code 4
case .extensionNotFound:
return """
The Firezone.app bundle seems corrupt. Please try downloading and
installing Firezone again.
"""
// Code 5
case .extensionMissingIdentifier:
return """
// Code 5
case .extensionMissingIdentifier:
return """
The system extension is missing its bundle identifier. Please try
downloading and installing Firezone again.
"""
// Code 6
case .duplicateExtensionIdentifer:
return """
// Code 6
case .duplicateExtensionIdentifer:
return """
The system extension appears to have been installed already. Please try
completely removing Firezone and all Firezone-related system extensions
and try again.
"""
// Code 7
case .unknownExtensionCategory:
return """
// Code 7
case .unknownExtensionCategory:
return """
The system extension doesn't belong to any recognizable category.
Please contact your administrator for assistance.
"""
// Code 8
case .codeSignatureInvalid:
return """
// Code 8
case .codeSignatureInvalid:
return """
The system extension contains an invalid code signature. Please ensure
your macOS version is up to date and system integrity protection (SIP)
is enabled and functioning properly.
"""
// Code 9
case .validationFailed:
return """
// Code 9
case .validationFailed:
return """
The system extension unexpectedly failed validation. Please try updating
to the latest version and contact your administrator if this issue
persists.
"""
// Code 10
case .forbiddenBySystemPolicy:
return """
// Code 10
case .forbiddenBySystemPolicy:
return """
The FirezoneNetworkExtension was blocked from loading by a system policy.
This will prevent Firezone from functioning. Please contact your
administrator for assistance.
@@ -160,37 +148,67 @@ struct macOSAlert { // swiftlint:disable:this type_name
Extension Identifier: dev.firezone.firezone.network-extension
"""
// Code 11
case .requestCanceled:
// This will happen if the user cancels
return nil
// Code 11
case .requestCanceled:
// This will happen if the user cancels
return """
You must enable the FirezoneNetworkExtension System Extension in System Settings to continue. Until you do,
all functionality will be disabled.
"""
// Code 12
case .requestSuperseded:
// This will happen if the user repeatedly clicks "Enable ..."
return """
// Code 12
case .requestSuperseded:
// This will happen if the user repeatedly clicks "Enable ..."
return """
You must enable the FirezoneNetworkExtension System Extension in System Settings to continue. Until you do,
all functionality will be disabled.
For more information and troubleshooting, please contact your administrator.
"""
// Code 13
case .authorizationRequired:
// This happens the first time we try to install the system extension.
// The user is prompted but we still get this.
return nil
// Code 13
case .authorizationRequired:
// This happens the first time we try to install the system extension.
// The user is prompted but we still get this.
return nil
@unknown default:
return "\(error)"
}
}()
@unknown default:
return "\(self)"
}
}
}
// Error (fallback case)
private static func userMessage(for error: Error) -> String? {
return "\(error)"
// Sometimes errors are passed to us as NSError objects, the Objective-C variant
extension NSError: UserFriendlyError {
func userMessage() -> String? {
switch domain {
case NEVPNErrorDomain:
let code = NEVPNError.Code(rawValue: self.code)! // SAFETY: These are the same error type
let err = NEVPNError(code)
return err.userMessage()
case OSSystemExtensionErrorDomain:
let code = OSSystemExtensionError.Code(rawValue: self.code)! // SAFETY: These are the same error type
let err = OSSystemExtensionError(code)
return err.userMessage()
default:
return "\(self)"
}
}
}
@MainActor
struct macOSAlert { // swiftlint:disable:this type_name
static func show(for error: Error) {
let message = (error as UserFriendlyError).userMessage() ?? "\(error)"
let alert = NSAlert()
alert.messageText = "An error occurred."
alert.informativeText = message
alert.alertStyle = .critical
alert.runModal()
}
}
#endif
// swiftlint:enable cyclomatic_complexity
// swiftlint:enable function_body_length

View File

@@ -20,6 +20,10 @@ export default function Apple() {
<Entries downloadLinks={downloadLinks} title="macOS / iOS">
{/* When you cut a release, remove any solved issues from the "known issues" lists over in `client-apps`. This must not be done when the issue's PR merges. */}
<Unreleased>
<ChangeItem pull="8282">
Shows friendlier and more-human alert messages when something goes
wrong.
</ChangeItem>
<ChangeItem pull="8286">
Fixes a bug that prevented certain Resource fields from being updated
when they were updated in the admin portal.