Skip to content

Commit de12d33

Browse files
committed
refactor: merge session & settings abstractions
1 parent 270c7d0 commit de12d33

17 files changed

+119
-176
lines changed

Coder Desktop/Coder Desktop/Coder_DesktopApp.swift

+8-12
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,14 @@ struct DesktopApp: App {
1111
EmptyView()
1212
}
1313
Window("Sign In", id: Windows.login.rawValue) {
14-
LoginForm<SecureSession>()
15-
.environmentObject(appDelegate.session)
16-
.environmentObject(appDelegate.settings)
14+
LoginForm()
15+
.environmentObject(appDelegate.state)
1716
}
1817
.windowResizability(.contentSize)
1918
SwiftUI.Settings {
2019
SettingsView<CoderVPNService>()
2120
.environmentObject(appDelegate.vpn)
22-
.environmentObject(appDelegate.settings)
21+
.environmentObject(appDelegate.state)
2322
}
2423
.windowResizability(.contentSize)
2524
}
@@ -29,28 +28,25 @@ struct DesktopApp: App {
2928
class AppDelegate: NSObject, NSApplicationDelegate {
3029
private var menuBarExtra: FluidMenuBarExtra?
3130
let vpn: CoderVPNService
32-
let session: SecureSession
33-
let settings: Settings
31+
let state: AppState
3432

3533
override init() {
3634
vpn = CoderVPNService()
37-
settings = Settings()
38-
session = SecureSession(onChange: vpn.configureTunnelProviderProtocol)
35+
state = AppState(onChange: vpn.configureTunnelProviderProtocol)
3936
}
4037

4138
func applicationDidFinishLaunching(_: Notification) {
4239
menuBarExtra = FluidMenuBarExtra(title: "Coder Desktop", image: "MenuBarIcon") {
43-
VPNMenu<CoderVPNService, SecureSession>().frame(width: 256)
40+
VPNMenu<CoderVPNService>().frame(width: 256)
4441
.environmentObject(self.vpn)
45-
.environmentObject(self.session)
46-
.environmentObject(self.settings)
42+
.environmentObject(self.state)
4743
}
4844
}
4945

5046
// This function MUST eventually call `NSApp.reply(toApplicationShouldTerminate: true)`
5147
// or return `.terminateNow`
5248
func applicationShouldTerminate(_: NSApplication) -> NSApplication.TerminateReply {
53-
if !settings.stopVPNOnQuit { return .terminateNow }
49+
if !state.stopVPNOnQuit { return .terminateNow }
5450
Task {
5551
await vpn.stop()
5652
NSApp.reply(toApplicationShouldTerminate: true)

Coder Desktop/Coder Desktop/Preview Content/PreviewSession.swift

-29
This file was deleted.

Coder Desktop/Coder Desktop/State.swift

+43-41
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,20 @@ import KeychainAccess
44
import NetworkExtension
55
import SwiftUI
66

7-
protocol Session: ObservableObject {
8-
var hasSession: Bool { get }
9-
var baseAccessURL: URL? { get }
10-
var sessionToken: String? { get }
11-
12-
func store(baseAccessURL: URL, sessionToken: String)
13-
func clear()
14-
func tunnelProviderProtocol() -> NETunnelProviderProtocol?
15-
}
16-
17-
class SecureSession: ObservableObject, Session {
7+
class AppState: ObservableObject {
188
let appId = Bundle.main.bundleIdentifier!
199

2010
// Stored in UserDefaults
2111
@Published private(set) var hasSession: Bool {
2212
didSet {
13+
guard persistent else { return }
2314
UserDefaults.standard.set(hasSession, forKey: Keys.hasSession)
2415
}
2516
}
2617

2718
@Published private(set) var baseAccessURL: URL? {
2819
didSet {
20+
guard persistent else { return }
2921
UserDefaults.standard.set(baseAccessURL, forKey: Keys.baseAccessURL)
3022
}
3123
}
@@ -37,6 +29,27 @@ class SecureSession: ObservableObject, Session {
3729
}
3830
}
3931

32+
@Published var useLiteralHeaders: Bool = UserDefaults.standard.bool(forKey: Keys.useLiteralHeaders) {
33+
didSet {
34+
guard persistent else { return }
35+
UserDefaults.standard.set(useLiteralHeaders, forKey: Keys.useLiteralHeaders)
36+
}
37+
}
38+
39+
@Published var literalHeaders: [LiteralHeader] {
40+
didSet {
41+
guard persistent else { return }
42+
try? UserDefaults.standard.set(JSONEncoder().encode(literalHeaders), forKey: Keys.literalHeaders)
43+
}
44+
}
45+
46+
@Published var stopVPNOnQuit: Bool = UserDefaults.standard.bool(forKey: Keys.stopVPNOnQuit) {
47+
didSet {
48+
guard persistent else { return }
49+
UserDefaults.standard.set(stopVPNOnQuit, forKey: Keys.stopVPNOnQuit)
50+
}
51+
}
52+
4053
func tunnelProviderProtocol() -> NETunnelProviderProtocol? {
4154
if !hasSession { return nil }
4255
let proto = NETunnelProviderProtocol()
@@ -49,37 +62,50 @@ class SecureSession: ObservableObject, Session {
4962
}
5063

5164
private let keychain: Keychain
65+
private let persistent: Bool
5266

5367
let onChange: ((NETunnelProviderProtocol?) -> Void)?
5468

55-
public init(onChange: ((NETunnelProviderProtocol?) -> Void)? = nil) {
69+
public init(onChange: ((NETunnelProviderProtocol?) -> Void)? = nil,
70+
persistent: Bool = true)
71+
{
72+
self.persistent = persistent
5673
self.onChange = onChange
5774
keychain = Keychain(service: Bundle.main.bundleIdentifier!)
58-
_hasSession = Published(initialValue: UserDefaults.standard.bool(forKey: Keys.hasSession))
59-
_baseAccessURL = Published(initialValue: UserDefaults.standard.url(forKey: Keys.baseAccessURL))
75+
_hasSession = Published(initialValue: persistent ? UserDefaults.standard.bool(forKey: Keys.hasSession) : false)
76+
_baseAccessURL = Published(
77+
initialValue: persistent ? UserDefaults.standard.url(forKey: Keys.baseAccessURL) : nil
78+
)
79+
_literalHeaders = Published(
80+
initialValue: persistent ? UserDefaults.standard.data(
81+
forKey: Keys.literalHeaders
82+
).flatMap { try? JSONDecoder().decode([LiteralHeader].self, from: $0) } ?? [] : []
83+
)
6084
if hasSession {
6185
_sessionToken = Published(initialValue: keychainGet(for: Keys.sessionToken))
6286
}
6387
}
6488

65-
public func store(baseAccessURL: URL, sessionToken: String) {
89+
public func login(baseAccessURL: URL, sessionToken: String) {
6690
hasSession = true
6791
self.baseAccessURL = baseAccessURL
6892
self.sessionToken = sessionToken
6993
if let onChange { onChange(tunnelProviderProtocol()) }
7094
}
7195

72-
public func clear() {
96+
public func clearSession() {
7397
hasSession = false
7498
sessionToken = nil
7599
if let onChange { onChange(tunnelProviderProtocol()) }
76100
}
77101

78102
private func keychainGet(for key: String) -> String? {
79-
try? keychain.getString(key)
103+
guard persistent else { return nil }
104+
return try? keychain.getString(key)
80105
}
81106

82107
private func keychainSet(_ value: String?, for key: String) {
108+
guard persistent else { return }
83109
if let value {
84110
try? keychain.set(value, key: key)
85111
} else {
@@ -91,31 +117,7 @@ class SecureSession: ObservableObject, Session {
91117
static let hasSession = "hasSession"
92118
static let baseAccessURL = "baseAccessURL"
93119
static let sessionToken = "sessionToken"
94-
}
95-
}
96-
97-
class Settings: ObservableObject {
98-
private let store: UserDefaults
99-
@AppStorage(Keys.useLiteralHeaders) var useLiteralHeaders = false
100120

101-
@Published var literalHeaders: [LiteralHeader] {
102-
didSet {
103-
try? store.set(JSONEncoder().encode(literalHeaders), forKey: Keys.literalHeaders)
104-
}
105-
}
106-
107-
@AppStorage(Keys.stopVPNOnQuit) var stopVPNOnQuit = true
108-
109-
init(store: UserDefaults = .standard) {
110-
self.store = store
111-
_literalHeaders = Published(
112-
initialValue: store.data(
113-
forKey: Keys.literalHeaders
114-
).flatMap { try? JSONDecoder().decode([LiteralHeader].self, from: $0) } ?? []
115-
)
116-
}
117-
118-
enum Keys {
119121
static let useLiteralHeaders = "UseLiteralHeaders"
120122
static let literalHeaders = "LiteralHeaders"
121123
static let stopVPNOnQuit = "StopVPNOnQuit"

Coder Desktop/Coder Desktop/Views/Agents.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import SwiftUI
22

3-
struct Agents<VPN: VPNService, S: Session>: View {
3+
struct Agents<VPN: VPNService>: View {
44
@EnvironmentObject var vpn: VPN
5-
@EnvironmentObject var session: S
5+
@EnvironmentObject var state: AppState
66
@State private var viewAll = false
77
private let defaultVisibleRows = 5
88

@@ -15,7 +15,7 @@ struct Agents<VPN: VPNService, S: Session>: View {
1515
let items = vpn.menuState.sorted
1616
let visibleItems = viewAll ? items[...] : items.prefix(defaultVisibleRows)
1717
ForEach(visibleItems, id: \.id) { agent in
18-
MenuItemView(item: agent, baseAccessURL: session.baseAccessURL!)
18+
MenuItemView(item: agent, baseAccessURL: state.baseAccessURL!)
1919
.padding(.horizontal, Theme.Size.trayMargin)
2020
}
2121
if items.count == 0 {

Coder Desktop/Coder Desktop/Views/AuthButton.swift

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
import SwiftUI
22

3-
struct AuthButton<VPN: VPNService, S: Session>: View {
4-
@EnvironmentObject var session: S
3+
struct AuthButton<VPN: VPNService>: View {
4+
@EnvironmentObject var state: AppState
55
@EnvironmentObject var vpn: VPN
66
@Environment(\.openWindow) var openWindow
77

88
var body: some View {
99
Button {
10-
if session.hasSession {
10+
if state.hasSession {
1111
Task {
1212
await vpn.stop()
13-
session.clear()
13+
state.clearSession()
1414
}
1515
} else {
1616
openWindow(id: .login)
1717
}
1818
} label: {
1919
ButtonRowView {
20-
Text(session.hasSession ? "Sign out" : "Sign in")
20+
Text(state.hasSession ? "Sign out" : "Sign in")
2121
}
2222
}.buttonStyle(.plain)
2323
}

Coder Desktop/Coder Desktop/Views/LoginForm.swift

+7-8
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import CoderSDK
22
import SwiftUI
33

4-
struct LoginForm<S: Session>: View {
5-
@EnvironmentObject var session: S
6-
@EnvironmentObject var settings: Settings
4+
struct LoginForm: View {
5+
@EnvironmentObject var state: AppState
76
@Environment(\.dismiss) private var dismiss
87

98
@State private var baseAccessURL: String = ""
@@ -38,7 +37,7 @@ struct LoginForm<S: Session>: View {
3837
}
3938
.animation(.easeInOut, value: currentPage)
4039
.onAppear {
41-
baseAccessURL = session.baseAccessURL?.absoluteString ?? baseAccessURL
40+
baseAccessURL = state.baseAccessURL?.absoluteString ?? baseAccessURL
4241
sessionToken = ""
4342
}
4443
.alert("Error", isPresented: Binding(
@@ -72,14 +71,14 @@ struct LoginForm<S: Session>: View {
7271
}
7372
loading = true
7473
defer { loading = false }
75-
let client = Client(url: url, token: sessionToken, headers: settings.literalHeaders.map { $0.toSDKHeader() })
74+
let client = Client(url: url, token: sessionToken, headers: state.literalHeaders.map { $0.toSDKHeader() })
7675
do {
7776
_ = try await client.user("me")
7877
} catch {
7978
loginError = .failedAuth(error)
8079
return
8180
}
82-
session.store(baseAccessURL: url, sessionToken: sessionToken)
81+
state.login(baseAccessURL: url, sessionToken: sessionToken)
8382
dismiss()
8483
}
8584

@@ -219,7 +218,7 @@ enum LoginField: Hashable {
219218

220219
#if DEBUG
221220
#Preview {
222-
LoginForm<PreviewSession>()
223-
.environmentObject(PreviewSession())
221+
LoginForm()
222+
.environmentObject(AppState())
224223
}
225224
#endif

Coder Desktop/Coder Desktop/Views/Settings/GeneralTab.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ import LaunchAtLogin
22
import SwiftUI
33

44
struct GeneralTab: View {
5-
@EnvironmentObject var settings: Settings
5+
@EnvironmentObject var state: AppState
66
var body: some View {
77
Form {
88
Section {
99
LaunchAtLogin.Toggle("Launch at Login")
1010
}
1111
Section {
12-
Toggle(isOn: $settings.stopVPNOnQuit) {
12+
Toggle(isOn: $state.stopVPNOnQuit) {
1313
Text("Stop VPN on Quit")
1414
}
1515
}

Coder Desktop/Coder Desktop/Views/Settings/LiteralHeaderModal.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import SwiftUI
33
struct LiteralHeaderModal: View {
44
var existingHeader: LiteralHeader?
55

6-
@EnvironmentObject var settings: Settings
6+
@EnvironmentObject var state: AppState
77
@Environment(\.dismiss) private var dismiss
88

99
@State private var header: String = ""
@@ -35,11 +35,11 @@ struct LiteralHeaderModal: View {
3535
func submit() {
3636
defer { dismiss() }
3737
if let existingHeader {
38-
settings.literalHeaders.removeAll { $0 == existingHeader }
38+
state.literalHeaders.removeAll { $0 == existingHeader }
3939
}
4040
let newHeader = LiteralHeader(header: header, value: value)
41-
if !settings.literalHeaders.contains(newHeader) {
42-
settings.literalHeaders.append(newHeader)
41+
if !state.literalHeaders.contains(newHeader) {
42+
state.literalHeaders.append(newHeader)
4343
}
4444
}
4545
}

0 commit comments

Comments
 (0)