Skip to content

Commit fc0f5b0

Browse files
refactor: merge session & settings abstractions (#46)
Unfortunately necessary for #52, as we need the HTTP headers from settings when creating the protocol configuration (which is derived from the session). The class retains all the same invariants as before.
1 parent 250017b commit fc0f5b0

17 files changed

+119
-176
lines changed

Coder Desktop/Coder Desktop/Coder_DesktopApp.swift

Lines changed: 8 additions & 12 deletions
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

Lines changed: 0 additions & 29 deletions
This file was deleted.

Coder Desktop/Coder Desktop/State.swift

Lines changed: 43 additions & 41 deletions
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

Lines changed: 3 additions & 3 deletions
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

Lines changed: 5 additions & 5 deletions
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

Lines changed: 7 additions & 8 deletions
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

Lines changed: 2 additions & 2 deletions
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

Lines changed: 4 additions & 4 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy