Skip to content

Commit

Permalink
macOS VPN: Allow extension relocated in macOS 15 Sequoia (#3334)
Browse files Browse the repository at this point in the history
Task/Issue URL:
https://app.asana.com/0/1199230911884351/1208377130239025/f

## Description

Fixing the onboarding for macOS 15 Sequoia

## Known limitations

- The asset isn't final, it's just meant to be a quick fix for Sequoia
users. It's both single-size and light-mode only.
  • Loading branch information
diegoreymendez authored Sep 25, 2024
1 parent bdb130b commit cb01690
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ final class UserText {
// MARK: - Onboarding

static let networkProtectionOnboardingInstallExtensionTitle = NSLocalizedString("network.protection.onboarding.install.extension.title", value: "Install VPN System Extension", comment: "Title for the onboarding install-vpn-extension step")
static let networkProtectionOnboardingAllowExtensionDescPrefixForSequoia = NSLocalizedString("network.protection.onboarding.allow.extension.desc.sequoia.prefix", value: "Click ", comment: "Non-bold description for the onboarding allow-extension description")
static let networkProtectionOnboardingAllowExtensionDescEmphasized = NSLocalizedString("network.protection.onboarding.allow.extension.desc.sequoia.emphasized", value: "Open System Settings", comment: "'Allow' word between the prefix and suffix for the onboarding allow-extension description")
static let networkProtectionOnboardingAllowExtensionDescSuffixForSequoia = NSLocalizedString("network.protection.onboarding.allow.extension.desc.sequoia.suffix", value: ", then enable the DuckDuckGo VPN network extension.", comment: "Non-bold description for the onboarding allow-extension description")
static let networkProtectionOnboardingAllowExtensionDescPrefix = NSLocalizedString("network.protection.onboarding.allow.extension.desc.prefix", value: "Open System Settings to Privacy & Security. Scroll and select ", comment: "Non-bold prefix for the onboarding allow-extension description")
static let networkProtectionOnboardingAllowExtensionDescAllow = NSLocalizedString("network.protection.onboarding.allow.extension.desc.allow", value: "Allow", comment: "'Allow' word between the prefix and suffix for the onboarding allow-extension description")
static let networkProtectionOnboardingAllowExtensionDescSuffix = NSLocalizedString("network.protection.onboarding.allow.extension.desc.suffix", value: " for DuckDuckGo software.", comment: "Non-bold suffix for the onboarding allow-extension description")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public enum NetworkProtectionAsset: String, CaseIterable {
case statusbarBrandedVPNIssueIcon = "statusbar-branded-vpn-issue"

// Images
case enableSysexImage = "enable-sysex-bottom"
case allowSysexScreenshot = "allow-sysex-screenshot"
case allowSysexScreenshotBigSur = "allow-sysex-screenshot-bigsur"

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "bottom.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,15 @@ struct PromptActionView: View {
.padding(.horizontal, 10)

if let actionScreenshot = model.actionScreenshot {
Image(actionScreenshot)
// This is done this way because the change was introduced as a hotfix
// for macOS Sequoia and we want to avoid breakage
if #available(macOS 15, *) {
Image(actionScreenshot)
.resizable()
.aspectRatio(contentMode: .fit)
} else {
Image(actionScreenshot)
}
}
}
.cornerRadius(8)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,19 @@ extension OnboardingStep {
func description(isMenuBar: Bool) -> [StyledTextFragment] {
switch self {
case .userNeedsToAllowExtension:
return [
.init(text: UserText.networkProtectionOnboardingAllowExtensionDescPrefix),
.init(text: UserText.networkProtectionOnboardingAllowExtensionDescAllow, isEmphasized: true),
.init(text: UserText.networkProtectionOnboardingAllowExtensionDescSuffix),
]
if #available(macOS 15, *) {
return [
.init(text: UserText.networkProtectionOnboardingAllowExtensionDescPrefixForSequoia),
.init(text: UserText.networkProtectionOnboardingAllowExtensionDescEmphasized, isEmphasized: true),
.init(text: UserText.networkProtectionOnboardingAllowExtensionDescSuffixForSequoia),
]
} else {
return [
.init(text: UserText.networkProtectionOnboardingAllowExtensionDescPrefix),
.init(text: UserText.networkProtectionOnboardingAllowExtensionDescAllow, isEmphasized: true),
.init(text: UserText.networkProtectionOnboardingAllowExtensionDescSuffix),
]
}
case .userNeedsToAllowVPNConfiguration:
if isMenuBar {
return [
Expand Down Expand Up @@ -148,7 +156,9 @@ extension OnboardingStep {
var actionScreenshot: NetworkProtectionAsset? {
switch self {
case .userNeedsToAllowExtension:
if #available(macOS 12, *) {
if #available(macOS 15, *) {
return .enableSysexImage
} else if #available(macOS 12, *) {
return .allowSysexScreenshot
} else {
return .allowSysexScreenshotBigSur
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ final class NetworkProtectionAssetTests: XCTestCase {
.statusbarDebugVPNOnIcon: "statusbar-debug-vpn-on",
.statusbarBrandedVPNOffIcon: "statusbar-branded-vpn-off",
.statusbarBrandedVPNIssueIcon: "statusbar-branded-vpn-issue",
.enableSysexImage: "enable-sysex-bottom",
.allowSysexScreenshot: "allow-sysex-screenshot",
.allowSysexScreenshotBigSur: "allow-sysex-screenshot-bigsur",
.accordionViewCheckmark: "Check-16D"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,13 @@ public enum SystemExtensionRequestError: Error {

public struct SystemExtensionManager {

private static let systemSettingsSecurityURL = "x-apple.systempreferences:com.apple.preference.security?Security"
private static var systemSettingsSecurityURL: String {
if #available(macOS 15, *) {
return "x-apple.systempreferences:com.apple.LoginItems-Settings.extension?ExtensionItems"
} else {
return "x-apple.systempreferences:com.apple.preference.security?Security"
}
}

private let extensionBundleID: String
private let manager: OSSystemExtensionManager
Expand All @@ -47,22 +53,8 @@ public struct SystemExtensionManager {
/// - Returns: The system extension version when it's updated, otherwise `nil`.
///
public func activate(waitingForUserApproval: @escaping () -> Void) async throws -> String? {
/// Documenting a workaround for the issue discussed in https://app.asana.com/0/0/1205275221447702/f
/// Background: For a lot of users, the system won't show the system-extension-blocked alert if there's a previous request
/// to activate the extension. You can see active requests in your console using command `systemextensionsctl list`.
///
/// Proposed workaround: Just open system settings into the right section when we detect a previous activation request already exists.
///
/// Tradeoffs: Unfortunately we don't know if the previous request was sent out by the currently runing-instance of this App
/// or if an activation request was made, and then the App was reopened.
/// This means we don't know if we'll be notified when the previous activation request completes or fails. Because we
/// need to update our UI once the extension is allowed, we can't avoid sending a new activation request every time.
/// For the users that don't see the alert come up more than once this should be invisible. For users (like myself) that
/// see the alert every single time, they'll see both the alert and system settings being opened automatically.
///
if hasPendingActivationRequests() {
openSystemSettingsSecurity()
}

workaroundToActivateBeforeSequoia()

let activationRequest = SystemExtensionRequest.activationRequest(
forExtensionWithIdentifier: extensionBundleID,
Expand All @@ -74,6 +66,36 @@ public struct SystemExtensionManager {
return activationRequest.version
}

/// Workaround to help make activation easier for users.
///
/// Documenting a workaround for the issue discussed in https://app.asana.com/0/0/1205275221447702/f
///
/// ## Background:
///
/// For a lot of users, the system won't show the system-extension-blocked alert if there's a previous request
/// to activate the extension. You can see active requests in your console using command
/// `systemextensionsctl list`.
///
/// Proposed workaround: Just open system settings into the right section when we detect a previous
/// activation request already exists.
///
/// ## Tradeoffs
///
/// Unfortunately we don't know if the previous request was sent out by the currently runing-instance of this App
/// or if an activation request was made, and then the App was reopened.
///
/// This means we don't know if we'll be notified when the previous activation request completes or fails. Because we
/// need to update our UI once the extension is allowed, we can't avoid sending a new activation request every time.
///
/// For the users that don't see the alert come up more than once this should be invisible. For users (like myself) that
/// see the alert every single time, they'll see both the alert and system settings being opened automatically.
///
private func workaroundToActivateBeforeSequoia() {
if hasPendingActivationRequests() {
openSystemSettingsSecurity()
}
}

public func deactivate() async throws {
try await SystemExtensionRequest.deactivationRequest(
forExtensionWithIdentifier: extensionBundleID,
Expand Down Expand Up @@ -186,19 +208,23 @@ extension SystemExtensionRequest: OSSystemExtensionRequestDelegate {
case .completed:
updateVersionNumberIfMissing()
continuation?.resume()
continuation = nil
case .willCompleteAfterReboot:
continuation?.resume(throwing: SystemExtensionRequestError.willActivateAfterReboot)
continuation = nil
return
@unknown default:
// Not much we can do about this, so we just let the owning app decide
// what to do about this.
continuation?.resume(throwing: SystemExtensionRequestError.unknownRequestResult)
continuation = nil
return
}
}

func request(_ request: OSSystemExtensionRequest, didFailWithError error: Error) {
continuation?.resume(throwing: error)
continuation = nil
}

}

0 comments on commit cb01690

Please sign in to comment.