diff --git a/Coder-Desktop/Coder-Desktop/UpdaterService.swift b/Coder-Desktop/Coder-Desktop/UpdaterService.swift index ce7bc9d..c0f5eaa 100644 --- a/Coder-Desktop/Coder-Desktop/UpdaterService.swift +++ b/Coder-Desktop/Coder-Desktop/UpdaterService.swift @@ -2,42 +2,59 @@ import Sparkle import SwiftUI final class UpdaterService: NSObject, ObservableObject { - private lazy var inner: SPUStandardUpdaterController = .init( - startingUpdater: true, - updaterDelegate: self, - userDriverDelegate: self - ) - private var updater: SPUUpdater! + // The auto-updater can be entirely disabled by setting the + // `disableUpdater` UserDefaults key to `true`. This is designed for use in + // MDM configurations, where the value can be set to `true` permanently. + let disabled: Bool = UserDefaults.standard.bool(forKey: Keys.disableUpdater) + @Published var canCheckForUpdates = true @Published var autoCheckForUpdates: Bool! { didSet { if let autoCheckForUpdates, autoCheckForUpdates != oldValue { - updater.automaticallyChecksForUpdates = autoCheckForUpdates + inner?.updater.automaticallyChecksForUpdates = autoCheckForUpdates } } } @Published var updateChannel: UpdateChannel { didSet { - UserDefaults.standard.set(updateChannel.rawValue, forKey: Self.updateChannelKey) + UserDefaults.standard.set(updateChannel.rawValue, forKey: Keys.updateChannel) } } - static let updateChannelKey = "updateChannel" + private var inner: (controller: SPUStandardUpdaterController, updater: SPUUpdater)? override init() { - updateChannel = UserDefaults.standard.string(forKey: Self.updateChannelKey) + updateChannel = UserDefaults.standard.string(forKey: Keys.updateChannel) .flatMap { UpdateChannel(rawValue: $0) } ?? .stable super.init() - updater = inner.updater + + guard !disabled else { + return + } + + let inner = SPUStandardUpdaterController( + startingUpdater: true, + updaterDelegate: self, + userDriverDelegate: self + ) + + let updater = inner.updater + self.inner = (inner, updater) + autoCheckForUpdates = updater.automaticallyChecksForUpdates updater.publisher(for: \.canCheckForUpdates).assign(to: &$canCheckForUpdates) } func checkForUpdates() { - guard canCheckForUpdates else { return } - updater.checkForUpdates() + guard let inner, canCheckForUpdates else { return } + inner.updater.checkForUpdates() + } + + enum Keys { + static let disableUpdater = "disableUpdater" + static let updateChannel = "updateChannel" } } diff --git a/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift b/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift index 6ebcba9..de8fa86 100644 --- a/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift +++ b/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift @@ -56,7 +56,7 @@ final class CoderVPNService: NSObject, VPNService { var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "vpn") lazy var xpc: HelperXPCClient = .init(vpn: self) - @Published var tunnelState: VPNServiceState = .disabled { + @Published private(set) var tunnelState: VPNServiceState = .disabled { didSet { if tunnelState == .connecting { progress = .init(stage: .initial, downloadProgress: nil) @@ -80,9 +80,9 @@ final class CoderVPNService: NSObject, VPNService { return tunnelState } - @Published var progress: VPNProgress = .init(stage: .initial, downloadProgress: nil) + @Published private(set) var progress: VPNProgress = .init(stage: .initial, downloadProgress: nil) - @Published var menuState: VPNMenuState = .init() + @Published private(set) var menuState: VPNMenuState = .init() // Whether the VPN should start as soon as possible var startWhenReady: Bool = false diff --git a/Coder-Desktop/Coder-Desktop/Views/FileSync/FilePicker.swift b/Coder-Desktop/Coder-Desktop/Views/FileSync/FilePicker.swift index 9ec2623..24e938a 100644 --- a/Coder-Desktop/Coder-Desktop/Views/FileSync/FilePicker.swift +++ b/Coder-Desktop/Coder-Desktop/Views/FileSync/FilePicker.swift @@ -69,9 +69,9 @@ struct FilePicker: View { @MainActor class FilePickerModel: ObservableObject { - @Published var rootEntries: [FilePickerEntryModel] = [] - @Published var rootIsLoading: Bool = false - @Published var error: SDKError? + @Published private(set) var rootEntries: [FilePickerEntryModel] = [] + @Published private(set) var rootIsLoading: Bool = false + @Published private(set) var error: SDKError? // It's important that `AgentClient` is a reference type (class) // as we were having performance issues with a struct (unless it was a binding). @@ -153,9 +153,9 @@ class FilePickerEntryModel: Identifiable, Hashable, ObservableObject { let client: AgentClient - @Published var entries: [FilePickerEntryModel]? - @Published var isLoading = false - @Published var error: SDKError? + @Published private(set) var entries: [FilePickerEntryModel]? + @Published private(set) var isLoading = false + @Published private(set) var error: SDKError? @Published private var innerIsExpanded = false var isExpanded: Bool { get { innerIsExpanded } diff --git a/Coder-Desktop/Coder-Desktop/Views/Settings/GeneralTab.swift b/Coder-Desktop/Coder-Desktop/Views/Settings/GeneralTab.swift index 7af41e4..d779a9a 100644 --- a/Coder-Desktop/Coder-Desktop/Views/Settings/GeneralTab.swift +++ b/Coder-Desktop/Coder-Desktop/Views/Settings/GeneralTab.swift @@ -19,18 +19,25 @@ struct GeneralTab: View { Text("Start Coder Connect on launch") } } - Section { - Toggle(isOn: $updater.autoCheckForUpdates) { - Text("Automatically check for updates") - } - Picker("Update channel", selection: $updater.updateChannel) { - ForEach(UpdateChannel.allCases) { channel in - Text(channel.name).tag(channel) + if !updater.disabled { + Section { + Toggle(isOn: $updater.autoCheckForUpdates) { + Text("Automatically check for updates") + } + Picker("Update channel", selection: $updater.updateChannel) { + ForEach(UpdateChannel.allCases) { channel in + Text(channel.name).tag(channel) + } + } + HStack { + Spacer() + Button("Check for updates") { updater.checkForUpdates() }.disabled(!updater.canCheckForUpdates) } } - HStack { - Spacer() - Button("Check for updates") { updater.checkForUpdates() }.disabled(!updater.canCheckForUpdates) + } else { + Section { + Text("The app updater has been disabled by a device management policy.") + .foregroundColor(.secondary) } } }.formStyle(.grouped)