mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 18:18:55 +00:00
refactor(apple): Add Settings model (#9155)
The settings fields are getting tedious to manage individually, so a helper class `Settings` is added which abstracts all of the view-related logic applicable to user-defined settings. When settings are saved, they are first applied to the `store`'s existing Configuration, and then that configuration is saved via a new consolidated IPC call `setConfiguration`. `actorName` has been moved to a GUI-only cached store since it does not need to live on `Configuration` any longer. This greatly simplifies both the view logic and the IPC interface. Notably, this does not handle the edge case where the configuration is updated while the Settings window is open. That is saved for a later time.
This commit is contained in:
@@ -56,13 +56,17 @@ class IPCClient {
|
||||
let encoder = PropertyListEncoder()
|
||||
let decoder = PropertyListDecoder()
|
||||
|
||||
func start(token: String? = nil) throws {
|
||||
var options: [String: NSObject] = [:]
|
||||
// Auto-connect
|
||||
func start() throws {
|
||||
try session().startTunnel(options: nil)
|
||||
}
|
||||
|
||||
// Pass token if provided
|
||||
if let token = token {
|
||||
options.merge(["token": token as NSObject]) { _, new in new }
|
||||
}
|
||||
// Sign in
|
||||
func start(token: String, accountSlug: String) throws {
|
||||
let options: [String: NSObject] = [
|
||||
"token": token as NSObject,
|
||||
"accountSlug": accountSlug as NSObject
|
||||
]
|
||||
|
||||
try session().startTunnel(options: options)
|
||||
}
|
||||
@@ -105,32 +109,8 @@ class IPCClient {
|
||||
}
|
||||
}
|
||||
|
||||
func setAuthURL(_ authURL: String) async throws {
|
||||
try await sendMessageWithoutResponse(ProviderMessage.setAuthURL(authURL))
|
||||
}
|
||||
|
||||
func setApiURL(_ apiURL: String) async throws {
|
||||
try await sendMessageWithoutResponse(ProviderMessage.setApiURL(apiURL))
|
||||
}
|
||||
|
||||
func setLogFilter(_ logFilter: String) async throws {
|
||||
try await sendMessageWithoutResponse(ProviderMessage.setLogFilter(logFilter))
|
||||
}
|
||||
|
||||
func setActorName(_ actorName: String) async throws {
|
||||
try await sendMessageWithoutResponse(ProviderMessage.setActorName(actorName))
|
||||
}
|
||||
|
||||
func setAccountSlug(_ accountSlug: String) async throws {
|
||||
try await sendMessageWithoutResponse(ProviderMessage.setAccountSlug(accountSlug))
|
||||
}
|
||||
|
||||
func setInternetResourceEnabled(_ enabled: Bool) async throws {
|
||||
try await sendMessageWithoutResponse(ProviderMessage.setInternetResourceEnabled(enabled))
|
||||
}
|
||||
|
||||
func setConnectOnStart(_ connectOnStart: Bool) async throws {
|
||||
try await sendMessageWithoutResponse(ProviderMessage.setConnectOnStart(connectOnStart))
|
||||
func setConfiguration(_ configuration: Configuration) async throws {
|
||||
try await sendMessageWithoutResponse(ProviderMessage.setConfiguration(configuration))
|
||||
}
|
||||
|
||||
func fetchResources() async throws -> ResourceList {
|
||||
|
||||
@@ -96,42 +96,46 @@ public class VPNConfigurationManager {
|
||||
|
||||
let ipcClient = IPCClient(session: session)
|
||||
|
||||
var userDict: [String: Any?] = [:]
|
||||
var migrated = false
|
||||
|
||||
if let actorName = legacyConfiguration["actorName"] {
|
||||
UserDefaults.standard.set(actorName, forKey: "actorName")
|
||||
migrated = true
|
||||
}
|
||||
|
||||
if let apiURL = legacyConfiguration["apiURL"] {
|
||||
try await ipcClient.setApiURL(apiURL)
|
||||
userDict[Configuration.Keys.apiURL] = apiURL
|
||||
migrated = true
|
||||
}
|
||||
|
||||
if let authURL = legacyConfiguration["authBaseURL"] {
|
||||
try await ipcClient.setAuthURL(authURL)
|
||||
migrated = true
|
||||
}
|
||||
|
||||
if let actorName = legacyConfiguration["actorName"] {
|
||||
try await ipcClient.setActorName(actorName)
|
||||
userDict[Configuration.Keys.authURL] = authURL
|
||||
migrated = true
|
||||
}
|
||||
|
||||
if let accountSlug = legacyConfiguration["accountSlug"] {
|
||||
try await ipcClient.setAccountSlug(accountSlug)
|
||||
userDict[Configuration.Keys.accountSlug] = accountSlug
|
||||
migrated = true
|
||||
}
|
||||
|
||||
if let logFilter = legacyConfiguration["logFilter"],
|
||||
!logFilter.isEmpty {
|
||||
try await ipcClient.setLogFilter(logFilter)
|
||||
userDict[Configuration.Keys.logFilter] = logFilter
|
||||
migrated = true
|
||||
}
|
||||
|
||||
if let internetResourceEnabled = legacyConfiguration["internetResourceEnabled"],
|
||||
["false", "true"].contains(internetResourceEnabled) {
|
||||
try await ipcClient.setInternetResourceEnabled(internetResourceEnabled == "true")
|
||||
userDict[Configuration.Keys.internetResourceEnabled] = internetResourceEnabled == "true"
|
||||
migrated = true
|
||||
}
|
||||
|
||||
if !migrated { return }
|
||||
|
||||
let configuration = Configuration(userDict: userDict, managedDict: [:])
|
||||
try await ipcClient.setConfiguration(configuration)
|
||||
|
||||
// Remove fields to prevent confusion if the user sees these in System Settings and wonders why they're stale.
|
||||
if let protocolConfiguration = manager.protocolConfiguration as? NETunnelProviderProtocol {
|
||||
protocolConfiguration.providerConfiguration = nil
|
||||
|
||||
@@ -11,11 +11,13 @@ public class Configuration: Codable {
|
||||
public static let defaultLogFilter = "info"
|
||||
#endif
|
||||
|
||||
public static let defaultAccountSlug = ""
|
||||
public static let defaultConnectOnStart = true
|
||||
|
||||
public struct Keys {
|
||||
public static let authURL = "authURL"
|
||||
public static let apiURL = "apiURL"
|
||||
public static let logFilter = "logFilter"
|
||||
public static let actorName = "actorName"
|
||||
public static let accountSlug = "accountSlug"
|
||||
public static let internetResourceEnabled = "internetResourceEnabled"
|
||||
public static let firezoneId = "firezoneId"
|
||||
@@ -24,7 +26,6 @@ public class Configuration: Codable {
|
||||
}
|
||||
|
||||
public var authURL: String?
|
||||
public var actorName: String?
|
||||
public var firezoneId: String?
|
||||
public var apiURL: String?
|
||||
public var logFilter: String?
|
||||
@@ -36,7 +37,6 @@ public class Configuration: Codable {
|
||||
private var overriddenKeys: Set<String> = []
|
||||
|
||||
public init(userDict: [String: Any?], managedDict: [String: Any?]) {
|
||||
self.actorName = userDict[Keys.actorName] as? String
|
||||
self.firezoneId = userDict[Keys.firezoneId] as? String
|
||||
|
||||
setValue(forKey: Keys.authURL, from: managedDict, and: userDict) { [weak self] in self?.authURL = $0 }
|
||||
@@ -58,6 +58,14 @@ public class Configuration: Codable {
|
||||
return overriddenKeys.contains(key)
|
||||
}
|
||||
|
||||
func applySettings(_ settings: Settings) {
|
||||
self.authURL = settings.authURL
|
||||
self.apiURL = settings.apiURL
|
||||
self.logFilter = settings.logFilter
|
||||
self.accountSlug = settings.accountSlug
|
||||
self.connectOnStart = settings.connectOnStart
|
||||
}
|
||||
|
||||
private func setValue<T>(
|
||||
forKey key: String,
|
||||
from managedDict: [String: Any?],
|
||||
|
||||
@@ -7,20 +7,11 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
// TODO: Can we simplify this / abstract it?
|
||||
// swiftlint:disable cyclomatic_complexity
|
||||
|
||||
public enum ProviderMessage: Codable {
|
||||
case getResourceList(Data)
|
||||
case getConfiguration(Data)
|
||||
case setConfiguration(Configuration)
|
||||
case signOut
|
||||
case setAuthURL(String)
|
||||
case setApiURL(String)
|
||||
case setLogFilter(String)
|
||||
case setActorName(String)
|
||||
case setAccountSlug(String)
|
||||
case setInternetResourceEnabled(Bool)
|
||||
case setConnectOnStart(Bool)
|
||||
case clearLogs
|
||||
case getLogFolderSize
|
||||
case exportLogs
|
||||
@@ -34,14 +25,8 @@ public enum ProviderMessage: Codable {
|
||||
enum MessageType: String, Codable {
|
||||
case getResourceList
|
||||
case getConfiguration
|
||||
case setConfiguration
|
||||
case signOut
|
||||
case setAuthURL
|
||||
case setApiURL
|
||||
case setLogFilter
|
||||
case setActorName
|
||||
case setAccountSlug
|
||||
case setInternetResourceEnabled
|
||||
case setConnectOnStart
|
||||
case clearLogs
|
||||
case getLogFolderSize
|
||||
case exportLogs
|
||||
@@ -52,33 +37,15 @@ public enum ProviderMessage: Codable {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
let type = try container.decode(MessageType.self, forKey: .type)
|
||||
switch type {
|
||||
case .setAuthURL:
|
||||
let value = try container.decode(String.self, forKey: .value)
|
||||
self = .setAuthURL(value)
|
||||
case .setApiURL:
|
||||
let value = try container.decode(String.self, forKey: .value)
|
||||
self = .setApiURL(value)
|
||||
case .setLogFilter:
|
||||
let value = try container.decode(String.self, forKey: .value)
|
||||
self = .setLogFilter(value)
|
||||
case .setActorName:
|
||||
let value = try container.decode(String.self, forKey: .value)
|
||||
self = .setActorName(value)
|
||||
case .setAccountSlug:
|
||||
let value = try container.decode(String.self, forKey: .value)
|
||||
self = .setAccountSlug(value)
|
||||
case .setInternetResourceEnabled:
|
||||
let value = try container.decode(Bool.self, forKey: .value)
|
||||
self = .setInternetResourceEnabled(value)
|
||||
case .setConnectOnStart:
|
||||
let value = try container.decode(Bool.self, forKey: .value)
|
||||
self = .setConnectOnStart(value)
|
||||
case .getResourceList:
|
||||
let value = try container.decode(Data.self, forKey: .value)
|
||||
self = .getResourceList(value)
|
||||
case .getConfiguration:
|
||||
let value = try container.decode(Data.self, forKey: .value)
|
||||
self = .getConfiguration(value)
|
||||
case .setConfiguration:
|
||||
let value = try container.decode(Configuration.self, forKey: .value)
|
||||
self = .setConfiguration(value)
|
||||
case .signOut:
|
||||
self = .signOut
|
||||
case .clearLogs:
|
||||
@@ -95,33 +62,15 @@ public enum ProviderMessage: Codable {
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
switch self {
|
||||
case .setAuthURL(let value):
|
||||
try container.encode(MessageType.setAuthURL, forKey: .type)
|
||||
try container.encode(value, forKey: .value)
|
||||
case .setApiURL(let value):
|
||||
try container.encode(MessageType.setApiURL, forKey: .type)
|
||||
try container.encode(value, forKey: .value)
|
||||
case .setLogFilter(let value):
|
||||
try container.encode(MessageType.setLogFilter, forKey: .type)
|
||||
try container.encode(value, forKey: .value)
|
||||
case .setActorName(let value):
|
||||
try container.encode(MessageType.setActorName, forKey: .type)
|
||||
try container.encode(value, forKey: .value)
|
||||
case .setAccountSlug(let value):
|
||||
try container.encode(MessageType.setAccountSlug, forKey: .type)
|
||||
try container.encode(value, forKey: .value)
|
||||
case .setInternetResourceEnabled(let value):
|
||||
try container.encode(MessageType.setInternetResourceEnabled, forKey: .type)
|
||||
try container.encode(value, forKey: .value)
|
||||
case .setConnectOnStart(let value):
|
||||
try container.encode(MessageType.setConnectOnStart, forKey: .type)
|
||||
try container.encode(value, forKey: .value)
|
||||
case .getResourceList(let value):
|
||||
try container.encode(MessageType.getResourceList, forKey: .type)
|
||||
try container.encode(value, forKey: .value)
|
||||
case .getConfiguration(let value):
|
||||
try container.encode(MessageType.getConfiguration, forKey: .type)
|
||||
try container.encode(value, forKey: .value)
|
||||
case .setConfiguration(let value):
|
||||
try container.encode(MessageType.setConfiguration, forKey: .type)
|
||||
try container.encode(value, forKey: .value)
|
||||
case .signOut:
|
||||
try container.encode(MessageType.signOut, forKey: .type)
|
||||
case .clearLogs:
|
||||
@@ -135,5 +84,3 @@ public enum ProviderMessage: Codable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:enable cyclomatic_complexity
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
//
|
||||
// Settings.swift
|
||||
// © 2025 Firezone, Inc.
|
||||
// LICENSE: Apache-2.0
|
||||
//
|
||||
// Settings represents the binding between our source-of-truth, Configuration, and user-configurable settings
|
||||
// available in the SettingsView.
|
||||
|
||||
import Foundation
|
||||
|
||||
class Settings {
|
||||
@Published var authURL: String
|
||||
@Published var apiURL: String
|
||||
@Published var logFilter: String
|
||||
@Published var accountSlug: String
|
||||
@Published var connectOnStart: Bool
|
||||
var isAuthURLOverridden = false
|
||||
var isApiURLOverridden = false
|
||||
var isLogFilterOverridden = false
|
||||
var isAccountSlugOverridden = false
|
||||
var isConnectOnStartOverridden = false
|
||||
|
||||
private var configuration: Configuration
|
||||
|
||||
init(from configuration: Configuration) {
|
||||
self.configuration = configuration
|
||||
self.authURL = configuration.authURL ?? Configuration.defaultAuthURL
|
||||
self.apiURL = configuration.apiURL ?? Configuration.defaultApiURL
|
||||
self.logFilter = configuration.logFilter ?? Configuration.defaultLogFilter
|
||||
self.accountSlug = configuration.accountSlug ?? Configuration.defaultAccountSlug
|
||||
self.connectOnStart = configuration.connectOnStart ?? Configuration.defaultConnectOnStart
|
||||
|
||||
self.isAuthURLOverridden = configuration.isOverridden(Configuration.Keys.authURL)
|
||||
self.isApiURLOverridden = configuration.isOverridden(Configuration.Keys.apiURL)
|
||||
self.isLogFilterOverridden = configuration.isOverridden(Configuration.Keys.logFilter)
|
||||
self.isAccountSlugOverridden = configuration.isOverridden(Configuration.Keys.accountSlug)
|
||||
self.isConnectOnStartOverridden = configuration.isOverridden(Configuration.Keys.connectOnStart)
|
||||
}
|
||||
|
||||
func areAllFieldsOverridden() -> Bool {
|
||||
return (isAuthURLOverridden &&
|
||||
isApiURLOverridden &&
|
||||
isLogFilterOverridden &&
|
||||
isAccountSlugOverridden &&
|
||||
isConnectOnStartOverridden)
|
||||
}
|
||||
|
||||
func isValid() -> Bool {
|
||||
guard let apiURL = URL(string: apiURL),
|
||||
apiURL.host != nil,
|
||||
["wss", "ws"].contains(apiURL.scheme),
|
||||
apiURL.pathComponents.isEmpty
|
||||
else {
|
||||
return false
|
||||
}
|
||||
|
||||
guard let authURL = URL(string: authURL),
|
||||
authURL.host != nil,
|
||||
["http", "https"].contains(authURL.scheme),
|
||||
authURL.pathComponents.isEmpty
|
||||
else {
|
||||
return false
|
||||
}
|
||||
|
||||
guard !logFilter.isEmpty
|
||||
else {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func isDefault() -> Bool {
|
||||
return (authURL == Configuration.defaultAuthURL &&
|
||||
apiURL == Configuration.defaultApiURL &&
|
||||
logFilter == Configuration.defaultLogFilter &&
|
||||
accountSlug == Configuration.defaultAccountSlug &&
|
||||
connectOnStart == Configuration.defaultConnectOnStart)
|
||||
}
|
||||
|
||||
func isSaved() -> Bool {
|
||||
return (
|
||||
authURL == configuration.authURL &&
|
||||
apiURL == configuration.apiURL &&
|
||||
logFilter == configuration.logFilter &&
|
||||
accountSlug == configuration.accountSlug &&
|
||||
connectOnStart == configuration.connectOnStart)
|
||||
}
|
||||
|
||||
func reset() {
|
||||
self.authURL = Configuration.defaultAuthURL
|
||||
self.apiURL = Configuration.defaultApiURL
|
||||
self.logFilter = Configuration.defaultLogFilter
|
||||
self.accountSlug = Configuration.defaultAccountSlug
|
||||
self.connectOnStart = Configuration.defaultConnectOnStart
|
||||
}
|
||||
}
|
||||
@@ -15,12 +15,15 @@ import AppKit
|
||||
|
||||
@MainActor
|
||||
// TODO: Move some state logic to view models
|
||||
// swiftlint:disable:next type_body_length
|
||||
public final class Store: ObservableObject {
|
||||
@Published private(set) var actorName: String
|
||||
@Published private(set) var favorites = Favorites()
|
||||
@Published private(set) var resourceList: ResourceList = .loading
|
||||
|
||||
// UserDefaults-backed app configuration that will publish updates to SwiftUI components
|
||||
// User-configurable settings
|
||||
@Published private(set) var settings: Settings?
|
||||
|
||||
// UserDefaults-backed app configuration
|
||||
@Published private(set) var configuration: Configuration?
|
||||
|
||||
// Enacapsulate Tunnel status here to make it easier for other components to observe
|
||||
@@ -46,6 +49,9 @@ public final class Store: ObservableObject {
|
||||
private var vpnConfigurationManager: VPNConfigurationManager?
|
||||
|
||||
public init() {
|
||||
// Load GUI-only cached state
|
||||
self.actorName = UserDefaults.standard.string(forKey: "actorName") ?? "Unknown user"
|
||||
|
||||
self.sessionNotification.signInHandler = {
|
||||
Task {
|
||||
do { try await WebAuthSession.signIn(store: self) } catch { Log.error(error) }
|
||||
@@ -84,10 +90,10 @@ public final class Store: ObservableObject {
|
||||
try await manager.maybeMigrateConfiguration()
|
||||
self.vpnConfigurationManager = manager
|
||||
try await setupTunnelObservers()
|
||||
try await manager.enableConfiguration()
|
||||
self.configuration = try await ipcClient().getConfiguration()
|
||||
Telemetry.firezoneId = configuration?.firezoneId
|
||||
|
||||
try await manager.enableConfiguration()
|
||||
if configuration?.connectOnStart ?? true {
|
||||
try ipcClient().start()
|
||||
}
|
||||
@@ -221,17 +227,18 @@ public final class Store: ObservableObject {
|
||||
}
|
||||
|
||||
func signIn(authResponse: AuthResponse) async throws {
|
||||
// Save actorName
|
||||
try await setActorName(authResponse.actorName)
|
||||
let actorName = authResponse.actorName
|
||||
let accountSlug = authResponse.accountSlug
|
||||
|
||||
// This will save the account slug even if overridden from MDM. This is what we want - if the admin removes the
|
||||
// override, this will take effect again.
|
||||
try await setAccountSlug(authResponse.accountSlug)
|
||||
// This is only shown in the GUI, cache it here
|
||||
UserDefaults.standard.set(actorName, forKey: "actorName")
|
||||
|
||||
Telemetry.accountSlug = accountSlug
|
||||
|
||||
try await manager().enableConfiguration()
|
||||
|
||||
// Bring the tunnel up and send it a token to start
|
||||
try ipcClient().start(token: authResponse.token)
|
||||
try ipcClient().start(token: authResponse.token, accountSlug: accountSlug)
|
||||
}
|
||||
|
||||
func signOut() async throws {
|
||||
@@ -249,53 +256,18 @@ public final class Store: ObservableObject {
|
||||
|
||||
// MARK: App configuration setters
|
||||
|
||||
func setActorName(_ actorName: String) async throws {
|
||||
try await ipcClient().setActorName(actorName)
|
||||
configuration?.actorName = actorName
|
||||
func applySettingsToConfiguration(_ settings: Settings) async throws {
|
||||
configuration?.applySettings(settings)
|
||||
try await setConfiguration(configuration)
|
||||
}
|
||||
|
||||
func setAccountSlug(_ accountSlug: String) async throws {
|
||||
try await ipcClient().setAccountSlug(accountSlug)
|
||||
configuration?.accountSlug = accountSlug
|
||||
|
||||
// Configure our Telemetry environment, closing if we're definitely not running against Firezone infrastructure.
|
||||
Telemetry.accountSlug = accountSlug
|
||||
}
|
||||
|
||||
func setAuthURL(_ authURL: String) async throws {
|
||||
try await ipcClient().setAuthURL(authURL)
|
||||
configuration?.authURL = authURL
|
||||
}
|
||||
|
||||
func setApiURL(_ apiURL: String) async throws {
|
||||
try await ipcClient().setApiURL(apiURL)
|
||||
configuration?.apiURL = apiURL
|
||||
|
||||
// Reconfigure our Telemetry environment in case it changed
|
||||
Telemetry.setEnvironmentOrClose(apiURL)
|
||||
}
|
||||
|
||||
func setLogFilter(_ logFilter: String) async throws {
|
||||
try await ipcClient().setLogFilter(logFilter)
|
||||
configuration?.logFilter = logFilter
|
||||
}
|
||||
|
||||
func setInternetResourceEnabled(_ internetResourceEnabled: Bool) async throws {
|
||||
try await ipcClient().setInternetResourceEnabled(internetResourceEnabled)
|
||||
private func setInternetResourceEnabled(_ internetResourceEnabled: Bool) async throws {
|
||||
configuration?.internetResourceEnabled = internetResourceEnabled
|
||||
}
|
||||
|
||||
func setConnectOnStart(_ connectOnStart: Bool) async throws {
|
||||
try await ipcClient().setConnectOnStart(connectOnStart)
|
||||
configuration?.connectOnStart = connectOnStart
|
||||
try await setConfiguration(configuration)
|
||||
}
|
||||
|
||||
// MARK: Private functions
|
||||
|
||||
private func start(token: String? = nil) throws {
|
||||
try ipcClient().start(token: token)
|
||||
}
|
||||
|
||||
private func beginConfigurationPolling() {
|
||||
// Ensure we're idempotent if called twice
|
||||
if self.configurationTimer != nil {
|
||||
@@ -378,4 +350,15 @@ public final class Store: ObservableObject {
|
||||
resourcesTimer = nil
|
||||
resourceList = ResourceList.loading
|
||||
}
|
||||
|
||||
private func setConfiguration(_ configuration: Configuration?) async throws {
|
||||
guard let configuration = configuration
|
||||
else {
|
||||
Log.warning("Tried to set configuration before it was initialized")
|
||||
return
|
||||
}
|
||||
|
||||
try await ipcClient().setConfiguration(configuration)
|
||||
self.configuration = configuration
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,6 +210,13 @@ public final class MenuBar: NSObject, ObservableObject {
|
||||
self.handleStatusChanged()
|
||||
}).store(in: &cancellables)
|
||||
|
||||
store.$configuration
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveValue: { [weak self] _ in
|
||||
self?.updateSignInMenuItems()
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
|
||||
updateChecker.$updateAvailable
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveValue: { [weak self] _ in
|
||||
@@ -350,7 +357,7 @@ public final class MenuBar: NSObject, ObservableObject {
|
||||
signOutMenuItem.isHidden = true
|
||||
settingsMenuItem.target = self
|
||||
case .connected, .reasserting, .connecting:
|
||||
let title = "Signed in as \(store.configuration?.actorName ?? "Unknown User")"
|
||||
let title = "Signed in as \(store.actorName)"
|
||||
signInMenuItem.title = title
|
||||
signInMenuItem.target = nil
|
||||
signOutMenuItem.isHidden = false
|
||||
@@ -358,6 +365,11 @@ public final class MenuBar: NSObject, ObservableObject {
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
|
||||
// Configuration must be initialized to manage settings
|
||||
if store.configuration == nil {
|
||||
settingsMenuItem.target = nil
|
||||
}
|
||||
}
|
||||
|
||||
func updateResourcesMenuItems() {
|
||||
|
||||
@@ -13,11 +13,20 @@ import SwiftUI
|
||||
|
||||
enum SettingsViewError: Error {
|
||||
case logFolderIsUnavailable
|
||||
case configurationNotInitialized
|
||||
|
||||
var localizedDescription: String {
|
||||
switch self {
|
||||
case .logFolderIsUnavailable:
|
||||
return "Log folder is unavailable."
|
||||
return """
|
||||
Log folder is unavailable.
|
||||
Try restarting your device or reinstalling Firezone if this issue persists.
|
||||
"""
|
||||
case .configurationNotInitialized:
|
||||
return """
|
||||
Configuration is not initialized.
|
||||
Try restarting your device or reinstalling Firezone if this issue persists.
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,153 +78,78 @@ class SettingsViewModel: ObservableObject {
|
||||
private let store: Store
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
@Published var authURL: String
|
||||
@Published var apiURL: String
|
||||
@Published var logFilter: String
|
||||
@Published var accountSlug: String
|
||||
@Published var connectOnStart: Bool
|
||||
@Published private(set) var isAuthURLOverridden = false
|
||||
@Published private(set) var isApiURLOverridden = false
|
||||
@Published private(set) var isLogFilterOverridden = false
|
||||
@Published private(set) var isAccountSlugOverridden = false
|
||||
@Published private(set) var isConnectOnStartOverridden = false
|
||||
@Published var settings: Settings
|
||||
|
||||
@Published private(set) var shouldDisableApplyButton = false
|
||||
@Published private(set) var shouldDisableResetButton = false
|
||||
@Published private(set) var areSettingsDefault = true
|
||||
@Published private(set) var areSettingsValid = true
|
||||
@Published private(set) var areSettingsSaved = true
|
||||
|
||||
init(store: Store) {
|
||||
self.store = store
|
||||
|
||||
self.authURL = store.configuration?.authURL ?? Configuration.defaultAuthURL
|
||||
self.apiURL = store.configuration?.apiURL ?? Configuration.defaultApiURL
|
||||
self.logFilter = store.configuration?.logFilter ?? Configuration.defaultLogFilter
|
||||
self.accountSlug = store.configuration?.accountSlug ?? ""
|
||||
self.connectOnStart = store.configuration?.connectOnStart ?? true
|
||||
|
||||
updateDerivedState()
|
||||
|
||||
// Update our state from our text fields
|
||||
Publishers.CombineLatest(
|
||||
Publishers.CombineLatest4($authURL, $apiURL, $logFilter, $accountSlug),
|
||||
$connectOnStart
|
||||
)
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.updateDerivedState()
|
||||
guard let configuration = store.configuration
|
||||
else {
|
||||
fatalError("Configuration must be initialized to view settings")
|
||||
}
|
||||
|
||||
self.settings = Settings(from: configuration)
|
||||
setupObservers()
|
||||
|
||||
// TODO: Handle updates to configuration while the SettingsView is open?
|
||||
}
|
||||
|
||||
func reset() {
|
||||
settings.reset()
|
||||
objectWillChange.send()
|
||||
updateDerivedState()
|
||||
}
|
||||
|
||||
func save() async throws {
|
||||
try await store.applySettingsToConfiguration(settings)
|
||||
|
||||
guard let configuration = store.configuration
|
||||
else {
|
||||
throw SettingsViewError.configurationNotInitialized
|
||||
}
|
||||
|
||||
self.settings = Settings(from: configuration)
|
||||
setupObservers()
|
||||
}
|
||||
|
||||
private func setupObservers() {
|
||||
// These all need to be the same underlying type
|
||||
Publishers.MergeMany([
|
||||
settings.$authURL,
|
||||
settings.$apiURL,
|
||||
settings.$accountSlug,
|
||||
settings.$logFilter
|
||||
])
|
||||
.receive(on: RunLoop.main)
|
||||
.sink(receiveValue: { [weak self] _ in
|
||||
self?.updateDerivedState()
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
|
||||
// Update our state from configuration updates
|
||||
store.$configuration
|
||||
settings.$connectOnStart
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak self] newConfiguration in
|
||||
self?.isAuthURLOverridden = newConfiguration?.isOverridden(Configuration.Keys.authURL) ?? false
|
||||
self?.isApiURLOverridden = newConfiguration?.isOverridden(Configuration.Keys.apiURL) ?? false
|
||||
self?.isLogFilterOverridden = newConfiguration?.isOverridden(Configuration.Keys.logFilter) ?? false
|
||||
self?.isAccountSlugOverridden = newConfiguration?.isOverridden(Configuration.Keys.accountSlug) ?? false
|
||||
self?.isConnectOnStartOverridden = newConfiguration?.isOverridden(Configuration.Keys.connectOnStart) ?? false
|
||||
|
||||
.sink(receiveValue: { [weak self] _ in
|
||||
self?.updateDerivedState()
|
||||
}
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private func isAuthURLValid() -> Bool {
|
||||
if let authURL = URL(string: authURL),
|
||||
authURL.host != nil,
|
||||
["https", "http"].contains(authURL.scheme),
|
||||
// Be restrictive - account slug will be appended
|
||||
authURL.pathComponents.isEmpty {
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private func isApiURLValid() -> Bool {
|
||||
if let apiURL = URL(string: apiURL),
|
||||
apiURL.host != nil,
|
||||
["wss", "ws"].contains(apiURL.scheme),
|
||||
// Be restrictive - account slug will be appended
|
||||
apiURL.pathComponents.isEmpty {
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private func isLogFilterValid() -> Bool {
|
||||
return !logFilter.isEmpty
|
||||
}
|
||||
|
||||
private func isAccountSlugValid() -> Bool {
|
||||
// URL automatically percent-encodes
|
||||
return true
|
||||
}
|
||||
|
||||
private func updateDerivedState() {
|
||||
self.areSettingsSaved = (self.authURL == store.configuration?.authURL &&
|
||||
self.apiURL == store.configuration?.apiURL &&
|
||||
self.logFilter == store.configuration?.logFilter &&
|
||||
self.accountSlug == store.configuration?.accountSlug &&
|
||||
self.connectOnStart == store.configuration?.connectOnStart)
|
||||
|
||||
self.areSettingsValid = isAuthURLValid() && isApiURLValid() && isLogFilterValid() && isAccountSlugValid()
|
||||
|
||||
self.areSettingsDefault = (self.authURL == Configuration.defaultAuthURL &&
|
||||
self.apiURL == Configuration.defaultApiURL &&
|
||||
self.logFilter == Configuration.defaultLogFilter &&
|
||||
self.accountSlug == "" &&
|
||||
self.connectOnStart == true)
|
||||
|
||||
self.shouldDisableApplyButton = (
|
||||
isAuthURLOverridden &&
|
||||
isApiURLOverridden &&
|
||||
isLogFilterOverridden &&
|
||||
isAccountSlugOverridden &&
|
||||
isConnectOnStartOverridden
|
||||
) || areSettingsSaved || !areSettingsValid
|
||||
settings.areAllFieldsOverridden() ||
|
||||
settings.isSaved() ||
|
||||
!settings.isValid()
|
||||
)
|
||||
|
||||
self.shouldDisableResetButton = (
|
||||
isAuthURLOverridden &&
|
||||
isApiURLOverridden &&
|
||||
isLogFilterOverridden &&
|
||||
isAccountSlugOverridden &&
|
||||
isConnectOnStartOverridden
|
||||
) || areSettingsDefault
|
||||
settings.areAllFieldsOverridden() ||
|
||||
settings.isDefault()
|
||||
)
|
||||
}
|
||||
|
||||
func applySettingsToStore() async throws {
|
||||
try await store.setApiURL(apiURL)
|
||||
try await store.setLogFilter(logFilter)
|
||||
try await store.setAuthURL(authURL)
|
||||
try await store.setAccountSlug(accountSlug)
|
||||
try await store.setConnectOnStart(connectOnStart)
|
||||
|
||||
updateDerivedState()
|
||||
}
|
||||
|
||||
func revertToDefaultSettings() {
|
||||
self.authURL = Configuration.defaultAuthURL
|
||||
self.apiURL = Configuration.defaultApiURL
|
||||
self.logFilter = Configuration.defaultLogFilter
|
||||
self.accountSlug = ""
|
||||
self.connectOnStart = true
|
||||
}
|
||||
|
||||
func reloadSettingsFromStore() {
|
||||
self.authURL = store.configuration?.authURL ?? Configuration.defaultAuthURL
|
||||
self.apiURL = store.configuration?.apiURL ?? Configuration.defaultApiURL
|
||||
self.logFilter = store.configuration?.logFilter ?? Configuration.defaultLogFilter
|
||||
self.accountSlug = store.configuration?.accountSlug ?? ""
|
||||
self.connectOnStart = store.configuration?.connectOnStart ?? true
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Move business logic to ViewModel to remove dependency on Store and fix body length
|
||||
@@ -298,7 +232,7 @@ public struct SettingsView: View {
|
||||
Image(systemName: "gearshape.2")
|
||||
Text("Advanced")
|
||||
}
|
||||
.badge(viewModel.areSettingsValid ? nil : "!")
|
||||
.badge(viewModel.settings.isValid() ? nil : "!")
|
||||
logsTab
|
||||
.tabItem {
|
||||
Image(systemName: "doc.text")
|
||||
@@ -321,9 +255,7 @@ public struct SettingsView: View {
|
||||
.disabled(viewModel.shouldDisableApplyButton)
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Cancel") {
|
||||
self.reloadSettings()
|
||||
}
|
||||
Button("Cancel") { dismiss() }
|
||||
}
|
||||
}
|
||||
.navigationTitle("Settings")
|
||||
@@ -372,7 +304,7 @@ public struct SettingsView: View {
|
||||
Button(
|
||||
"Reset to Defaults",
|
||||
action: {
|
||||
viewModel.revertToDefaultSettings()
|
||||
viewModel.reset()
|
||||
}
|
||||
)
|
||||
.disabled(viewModel.shouldDisableResetButton)
|
||||
@@ -412,7 +344,6 @@ public struct SettingsView: View {
|
||||
Text("Changing settings will sign you out and disconnect you from resources")
|
||||
}
|
||||
)
|
||||
.onDisappear(perform: { self.reloadSettings() })
|
||||
#else
|
||||
#error("Unsupported platform")
|
||||
#endif
|
||||
@@ -430,18 +361,18 @@ public struct SettingsView: View {
|
||||
.frame(width: 150, alignment: .trailing)
|
||||
TextField(
|
||||
"",
|
||||
text: $viewModel.accountSlug,
|
||||
text: $viewModel.settings.accountSlug,
|
||||
prompt: Text(PlaceholderText.accountSlug)
|
||||
)
|
||||
.disabled(viewModel.isAccountSlugOverridden)
|
||||
.disabled(viewModel.settings.isAccountSlugOverridden)
|
||||
.frame(width: 250)
|
||||
}
|
||||
.padding(.bottom, 10)
|
||||
Toggle(isOn: $viewModel.connectOnStart) {
|
||||
Toggle(isOn: $viewModel.settings.connectOnStart) {
|
||||
Text("Connect on launch")
|
||||
}
|
||||
.toggleStyle(.checkbox)
|
||||
.disabled(viewModel.isConnectOnStartOverridden)
|
||||
.disabled(viewModel.settings.isConnectOnStartOverridden)
|
||||
}
|
||||
.padding(10)
|
||||
Spacer()
|
||||
@@ -459,21 +390,21 @@ public struct SettingsView: View {
|
||||
.font(.caption)
|
||||
TextField(
|
||||
PlaceholderText.accountSlug,
|
||||
text: $viewModel.accountSlug
|
||||
text: $viewModel.settings.accountSlug
|
||||
)
|
||||
.autocorrectionDisabled()
|
||||
.textInputAutocapitalization(.never)
|
||||
.submitLabel(.done)
|
||||
.disabled(viewModel.isAccountSlugOverridden)
|
||||
.disabled(viewModel.settings.isAccountSlugOverridden)
|
||||
.padding(.bottom, 10)
|
||||
|
||||
Spacer()
|
||||
|
||||
Toggle(isOn: $viewModel.connectOnStart) {
|
||||
Toggle(isOn: $viewModel.settings.connectOnStart) {
|
||||
Text("Connect on launch")
|
||||
}
|
||||
.toggleStyle(.switch)
|
||||
.disabled(viewModel.isConnectOnStartOverridden)
|
||||
.disabled(viewModel.settings.isConnectOnStartOverridden)
|
||||
}
|
||||
},
|
||||
header: { Text("General Settings") },
|
||||
@@ -509,10 +440,10 @@ public struct SettingsView: View {
|
||||
.frame(width: 150, alignment: .trailing)
|
||||
TextField(
|
||||
"",
|
||||
text: $viewModel.authURL,
|
||||
text: $viewModel.settings.authURL,
|
||||
prompt: Text(PlaceholderText.authBaseURL)
|
||||
)
|
||||
.disabled(viewModel.isAuthURLOverridden)
|
||||
.disabled(viewModel.settings.isAuthURLOverridden)
|
||||
.frame(width: 250)
|
||||
}
|
||||
|
||||
@@ -522,10 +453,10 @@ public struct SettingsView: View {
|
||||
.frame(width: 150, alignment: .trailing)
|
||||
TextField(
|
||||
"",
|
||||
text: $viewModel.apiURL,
|
||||
text: $viewModel.settings.apiURL,
|
||||
prompt: Text(PlaceholderText.apiURL)
|
||||
)
|
||||
.disabled(viewModel.isApiURLOverridden)
|
||||
.disabled(viewModel.settings.isApiURLOverridden)
|
||||
.frame(width: 250)
|
||||
}
|
||||
|
||||
@@ -535,10 +466,10 @@ public struct SettingsView: View {
|
||||
.frame(width: 150, alignment: .trailing)
|
||||
TextField(
|
||||
"",
|
||||
text: $viewModel.logFilter,
|
||||
text: $viewModel.settings.logFilter,
|
||||
prompt: Text(PlaceholderText.logFilter)
|
||||
)
|
||||
.disabled(viewModel.isLogFilterOverridden)
|
||||
.disabled(viewModel.settings.isLogFilterOverridden)
|
||||
.frame(width: 250)
|
||||
}
|
||||
}
|
||||
@@ -559,12 +490,12 @@ public struct SettingsView: View {
|
||||
.font(.caption)
|
||||
TextField(
|
||||
PlaceholderText.authBaseURL,
|
||||
text: $viewModel.authURL
|
||||
text: $viewModel.settings.authURL
|
||||
)
|
||||
.autocorrectionDisabled()
|
||||
.textInputAutocapitalization(.never)
|
||||
.submitLabel(.done)
|
||||
.disabled(viewModel.isAuthURLOverridden)
|
||||
.disabled(viewModel.settings.isAuthURLOverridden)
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("API URL")
|
||||
@@ -572,12 +503,12 @@ public struct SettingsView: View {
|
||||
.font(.caption)
|
||||
TextField(
|
||||
PlaceholderText.apiURL,
|
||||
text: $viewModel.apiURL
|
||||
text: $viewModel.settings.apiURL
|
||||
)
|
||||
.autocorrectionDisabled()
|
||||
.textInputAutocapitalization(.never)
|
||||
.submitLabel(.done)
|
||||
.disabled(viewModel.isApiURLOverridden)
|
||||
.disabled(viewModel.settings.isApiURLOverridden)
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Log Filter")
|
||||
@@ -585,19 +516,19 @@ public struct SettingsView: View {
|
||||
.font(.caption)
|
||||
TextField(
|
||||
PlaceholderText.logFilter,
|
||||
text: $viewModel.logFilter
|
||||
text: $viewModel.settings.logFilter
|
||||
)
|
||||
.autocorrectionDisabled()
|
||||
.textInputAutocapitalization(.never)
|
||||
.submitLabel(.done)
|
||||
.disabled(viewModel.isLogFilterOverridden)
|
||||
.disabled(viewModel.settings.isLogFilterOverridden)
|
||||
}
|
||||
HStack {
|
||||
Spacer()
|
||||
Button(
|
||||
"Reset to Defaults",
|
||||
action: {
|
||||
viewModel.revertToDefaultSettings()
|
||||
viewModel.reset()
|
||||
}
|
||||
)
|
||||
.disabled(viewModel.shouldDisableResetButton)
|
||||
@@ -734,11 +665,6 @@ public struct SettingsView: View {
|
||||
dismiss()
|
||||
}
|
||||
|
||||
private func reloadSettings() {
|
||||
viewModel.reloadSettingsFromStore()
|
||||
dismiss()
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
private func exportLogsWithSavePanelOnMac() {
|
||||
self.isExportingLogs = true
|
||||
@@ -830,7 +756,7 @@ public struct SettingsView: View {
|
||||
}
|
||||
|
||||
private func saveSettings() async throws {
|
||||
try await viewModel.applySettingsToStore()
|
||||
try await viewModel.save()
|
||||
|
||||
if [.connected, .connecting, .reasserting].contains(store.status) {
|
||||
// TODO: Warn user instead of signing out
|
||||
|
||||
@@ -60,7 +60,7 @@ struct iOSNavigationView<Content: View>: View { // swiftlint:disable:this type_n
|
||||
private var authMenu: some View {
|
||||
Menu {
|
||||
if store.status == .connected {
|
||||
Text("Signed in as \(store.configuration?.actorName ?? "Unknown user")")
|
||||
Text("Signed in as \(store.actorName)")
|
||||
Button(
|
||||
action: {
|
||||
signOutButtonTapped()
|
||||
|
||||
@@ -19,9 +19,9 @@ class ConfigurationManager {
|
||||
|
||||
// We maintain a cache of the user dictionary to buffer against unnecessary reads from UserDefaults which
|
||||
// can cause deadlocks in rare cases.
|
||||
var userDict: [String: Any?]
|
||||
private var userDict: [String: Any?]
|
||||
|
||||
var managedDict: [String: Any?] {
|
||||
private var managedDict: [String: Any?] {
|
||||
userDefaults.dictionary(forKey: managedDictKey) ?? [:]
|
||||
}
|
||||
|
||||
@@ -33,39 +33,19 @@ class ConfigurationManager {
|
||||
Telemetry.firezoneId = userDict[Configuration.Keys.firezoneId] as? String
|
||||
}
|
||||
|
||||
func setAuthURL(_ authURL: String) {
|
||||
userDict[Configuration.Keys.authURL] = authURL
|
||||
// Save user-settable configuration
|
||||
func setConfiguration(_ configuration: Configuration) {
|
||||
userDict[Configuration.Keys.authURL] = configuration.authURL
|
||||
userDict[Configuration.Keys.apiURL] = configuration.apiURL
|
||||
userDict[Configuration.Keys.logFilter] = configuration.logFilter
|
||||
userDict[Configuration.Keys.accountSlug] = configuration.accountSlug
|
||||
userDict[Configuration.Keys.connectOnStart] = configuration.connectOnStart
|
||||
|
||||
saveUserDict()
|
||||
}
|
||||
|
||||
func setApiURL(_ apiURL: String) {
|
||||
userDict[Configuration.Keys.apiURL] = apiURL
|
||||
saveUserDict()
|
||||
}
|
||||
|
||||
func setLogFilter(_ logFilter: String) {
|
||||
userDict[Configuration.Keys.logFilter] = logFilter
|
||||
saveUserDict()
|
||||
}
|
||||
|
||||
func setActorName(_ actorName: String) {
|
||||
userDict[Configuration.Keys.actorName] = actorName
|
||||
saveUserDict()
|
||||
}
|
||||
|
||||
func setAccountSlug(_ accountSlug: String) {
|
||||
userDict[Configuration.Keys.accountSlug] = accountSlug
|
||||
saveUserDict()
|
||||
}
|
||||
|
||||
func setInternetResourceEnabled(_ internetResourceEnabled: Bool) {
|
||||
userDict[Configuration.Keys.internetResourceEnabled] = internetResourceEnabled
|
||||
saveUserDict()
|
||||
}
|
||||
|
||||
func setConnectOnStart(_ connectOnStart: Bool) {
|
||||
userDict[Configuration.Keys.connectOnStart] = connectOnStart
|
||||
saveUserDict()
|
||||
func toConfiguration() -> Configuration {
|
||||
return Configuration(userDict: userDict, managedDict: managedDict)
|
||||
}
|
||||
|
||||
// Firezone ID migration. Can be removed once most clients migrate past 1.4.15.
|
||||
|
||||
@@ -32,10 +32,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
// Initialize Telemetry as early as possible
|
||||
Telemetry.start()
|
||||
|
||||
self.configuration = Configuration(
|
||||
userDict: ConfigurationManager.shared.userDict,
|
||||
managedDict: ConfigurationManager.shared.managedDict
|
||||
)
|
||||
self.configuration = ConfigurationManager.shared.toConfiguration()
|
||||
|
||||
super.init()
|
||||
}
|
||||
@@ -78,10 +75,15 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
|
||||
let logFilter = legacyConfiguration?["logFilter"] ?? configuration.logFilter ?? Configuration.defaultLogFilter
|
||||
|
||||
guard let accountSlug = legacyConfiguration?["accountSlug"] ?? configuration.accountSlug
|
||||
// Prioritize passed accountSlug, updating saved account slug for next connect
|
||||
guard let accountSlug = options?["accountSlug"] as? String ??
|
||||
legacyConfiguration?["accountSlug"] ??
|
||||
configuration.accountSlug
|
||||
else {
|
||||
throw PacketTunnelProviderError.accountSlugIsInvalid
|
||||
}
|
||||
configuration.accountSlug = accountSlug
|
||||
ConfigurationManager.shared.setConfiguration(configuration)
|
||||
Telemetry.accountSlug = accountSlug
|
||||
|
||||
let enabled = legacyConfiguration?["internetResourceEnabled"]
|
||||
@@ -157,7 +159,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
// It would be helpful to be able to encapsulate Errors here. To do that
|
||||
// we need to update ProviderMessage to encode/decode Result to and from Data.
|
||||
// TODO: Move to a more abstract IPC protocol
|
||||
// swiftlint:disable:next cyclomatic_complexity function_body_length
|
||||
// swiftlint:disable:next cyclomatic_complexity
|
||||
override func handleAppMessage(_ message: Data, completionHandler: ((Data?) -> Void)? = nil) {
|
||||
do {
|
||||
let providerMessage = try PropertyListDecoder().decode(ProviderMessage.self, from: message)
|
||||
@@ -168,40 +170,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
let configurationPayload = configuration.toDataIfChanged(hash: hash)
|
||||
completionHandler?(configurationPayload)
|
||||
|
||||
case .setAuthURL(let authURL):
|
||||
configuration.authURL = authURL
|
||||
ConfigurationManager.shared.setAuthURL(authURL)
|
||||
completionHandler?(nil)
|
||||
|
||||
case .setApiURL(let apiURL):
|
||||
configuration.apiURL = apiURL
|
||||
ConfigurationManager.shared.setApiURL(apiURL)
|
||||
completionHandler?(nil)
|
||||
|
||||
case .setActorName(let actorName):
|
||||
configuration.actorName = actorName
|
||||
ConfigurationManager.shared.setActorName(actorName)
|
||||
completionHandler?(nil)
|
||||
|
||||
case .setAccountSlug(let accountSlug):
|
||||
configuration.accountSlug = accountSlug
|
||||
ConfigurationManager.shared.setAccountSlug(accountSlug)
|
||||
completionHandler?(nil)
|
||||
|
||||
case .setLogFilter(let logFilter):
|
||||
configuration.logFilter = logFilter
|
||||
ConfigurationManager.shared.setLogFilter(logFilter)
|
||||
completionHandler?(nil)
|
||||
|
||||
case .setInternetResourceEnabled(let enabled):
|
||||
configuration.internetResourceEnabled = enabled
|
||||
ConfigurationManager.shared.setInternetResourceEnabled(enabled)
|
||||
adapter?.setInternetResourceEnabled(enabled)
|
||||
completionHandler?(nil)
|
||||
|
||||
case .setConnectOnStart(let connectOnStart):
|
||||
configuration.connectOnStart = connectOnStart
|
||||
ConfigurationManager.shared.setConnectOnStart(connectOnStart)
|
||||
case .setConfiguration(let configuration):
|
||||
self.configuration = configuration
|
||||
ConfigurationManager.shared.setConfiguration(configuration)
|
||||
completionHandler?(nil)
|
||||
|
||||
case .signOut:
|
||||
|
||||
Reference in New Issue
Block a user