Explorar o código

Add Settings screen with global theme toggle and StoreKit config.

Implements a reference-style Settings page, persists dark/light mode across launches, and wires StoreKit test configuration for upgrade/restore flows.

Made-with: Cursor
huzaifahayat12 hai 5 días
pai
achega
3216955c17

+ 82 - 0
zoom_app.xcodeproj/xcshareddata/xcschemes/zoom_app.xcscheme

@@ -0,0 +1,82 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<Scheme
3
+   LastUpgradeVersion = "2640"
4
+   version = "1.7">
5
+   <BuildAction
6
+      parallelizeBuildables = "YES"
7
+      buildImplicitDependencies = "YES"
8
+      buildArchitectures = "Automatic">
9
+      <BuildActionEntries>
10
+         <BuildActionEntry
11
+            buildForTesting = "YES"
12
+            buildForRunning = "YES"
13
+            buildForProfiling = "YES"
14
+            buildForArchiving = "YES"
15
+            buildForAnalyzing = "YES">
16
+            <BuildableReference
17
+               BuildableIdentifier = "primary"
18
+               BlueprintIdentifier = "1391CBFF2F8E456500B3B198"
19
+               BuildableName = "zoom_app.app"
20
+               BlueprintName = "zoom_app"
21
+               ReferencedContainer = "container:zoom_app.xcodeproj">
22
+            </BuildableReference>
23
+         </BuildActionEntry>
24
+      </BuildActionEntries>
25
+   </BuildAction>
26
+   <TestAction
27
+      buildConfiguration = "Debug"
28
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
29
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
30
+      shouldUseLaunchSchemeArgsEnv = "YES"
31
+      shouldAutocreateTestPlan = "YES">
32
+   </TestAction>
33
+   <LaunchAction
34
+      buildConfiguration = "Debug"
35
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
36
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
37
+      launchStyle = "0"
38
+      useCustomWorkingDirectory = "NO"
39
+      ignoresPersistentStateOnLaunch = "NO"
40
+      debugDocumentVersioning = "YES"
41
+      debugServiceExtension = "internal"
42
+      allowLocationSimulation = "YES"
43
+      queueDebuggingEnableBacktraceRecording = "Yes">
44
+      <BuildableProductRunnable
45
+         runnableDebuggingMode = "0">
46
+         <BuildableReference
47
+            BuildableIdentifier = "primary"
48
+            BlueprintIdentifier = "1391CBFF2F8E456500B3B198"
49
+            BuildableName = "zoom_app.app"
50
+            BlueprintName = "zoom_app"
51
+            ReferencedContainer = "container:zoom_app.xcodeproj">
52
+         </BuildableReference>
53
+      </BuildableProductRunnable>
54
+      <StoreKitConfigurationFileReference
55
+         identifier = "../../zoom_app/StoreKit.storekit">
56
+      </StoreKitConfigurationFileReference>
57
+   </LaunchAction>
58
+   <ProfileAction
59
+      buildConfiguration = "Release"
60
+      shouldUseLaunchSchemeArgsEnv = "YES"
61
+      savedToolIdentifier = ""
62
+      useCustomWorkingDirectory = "NO"
63
+      debugDocumentVersioning = "YES">
64
+      <BuildableProductRunnable
65
+         runnableDebuggingMode = "0">
66
+         <BuildableReference
67
+            BuildableIdentifier = "primary"
68
+            BlueprintIdentifier = "1391CBFF2F8E456500B3B198"
69
+            BuildableName = "zoom_app.app"
70
+            BlueprintName = "zoom_app"
71
+            ReferencedContainer = "container:zoom_app.xcodeproj">
72
+         </BuildableReference>
73
+      </BuildableProductRunnable>
74
+   </ProfileAction>
75
+   <AnalyzeAction
76
+      buildConfiguration = "Debug">
77
+   </AnalyzeAction>
78
+   <ArchiveAction
79
+      buildConfiguration = "Release"
80
+      revealArchiveInOrganizer = "YES">
81
+   </ArchiveAction>
82
+</Scheme>

+ 14 - 0
zoom_app/AppDelegate.swift

@@ -9,6 +9,7 @@ import Cocoa
9 9
 
10 10
 @main
11 11
 class AppDelegate: NSObject, NSApplicationDelegate {
12
+    private let darkModeDefaultsKey = "settings.darkModeEnabled"
12 13
 
13 14
     private enum DefaultWindowSize {
14 15
         static let width: CGFloat = 1020
@@ -19,6 +20,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
19 20
 
20 21
 
21 22
     func applicationDidFinishLaunching(_ aNotification: Notification) {
23
+        let hasValue = UserDefaults.standard.object(forKey: darkModeDefaultsKey) != nil
24
+        let darkEnabled = hasValue ? UserDefaults.standard.bool(forKey: darkModeDefaultsKey) : systemPrefersDarkMode()
25
+        if hasValue == false {
26
+            UserDefaults.standard.set(darkEnabled, forKey: darkModeDefaultsKey)
27
+        }
28
+        NSApp.appearance = NSAppearance(named: darkEnabled ? .darkAqua : .aqua)
29
+
22 30
         // Force a consistent launch size (avoid state restoration overriding it).
23 31
         DispatchQueue.main.async { [weak self] in
24 32
             self?.applyDefaultWindowSizeIfNeeded()
@@ -48,5 +56,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
48 56
         }
49 57
     }
50 58
 
59
+    private func systemPrefersDarkMode() -> Bool {
60
+        let global = UserDefaults.standard.persistentDomain(forName: UserDefaults.globalDomain)
61
+        let style = global?["AppleInterfaceStyle"] as? String
62
+        return style?.lowercased() == "dark"
63
+    }
64
+
51 65
 }
52 66
 

+ 82 - 0
zoom_app/StoreKit.storekit

@@ -0,0 +1,82 @@
1
+{
2
+  "identifier" : "D0E98E7D-0C63-4D0F-A999-EC6AAB6E3B7C",
3
+  "nonRenewingSubscriptions" : [],
4
+  "products" : [
5
+    {
6
+      "displayPrice" : "1100.00",
7
+      "familyShareable" : false,
8
+      "internalID" : "64D1B757-30B3-4D8A-AE2A-83B6E2B9B4A7",
9
+      "localizations" : [
10
+        {
11
+          "description" : "Unlock premium features with weekly access.",
12
+          "displayName" : "Premium Weekly",
13
+          "locale" : "en_US"
14
+        }
15
+      ],
16
+      "productID" : "com.mqldev.zoomapp.premium.weekly",
17
+      "referenceName" : "Premium Weekly",
18
+      "type" : "NonConsumable"
19
+    },
20
+    {
21
+      "displayPrice" : "2500.00",
22
+      "familyShareable" : false,
23
+      "internalID" : "5F0659F3-9E3E-4E7A-95A4-87F68F63AE64",
24
+      "localizations" : [
25
+        {
26
+          "description" : "Unlock premium features with monthly access.",
27
+          "displayName" : "Premium Monthly",
28
+          "locale" : "en_US"
29
+        }
30
+      ],
31
+      "productID" : "com.mqldev.zoomapp.premium.monthly",
32
+      "referenceName" : "Premium Monthly",
33
+      "type" : "NonConsumable"
34
+    },
35
+    {
36
+      "displayPrice" : "9900.00",
37
+      "familyShareable" : false,
38
+      "internalID" : "CE6E8B9A-65D4-4E3A-9EB2-5B81A410F1E0",
39
+      "localizations" : [
40
+        {
41
+          "description" : "Unlock premium features with yearly access.",
42
+          "displayName" : "Premium Yearly",
43
+          "locale" : "en_US"
44
+        }
45
+      ],
46
+      "productID" : "com.mqldev.zoomapp.premium.yearly",
47
+      "referenceName" : "Premium Yearly",
48
+      "type" : "NonConsumable"
49
+    },
50
+    {
51
+      "displayPrice" : "14900.00",
52
+      "familyShareable" : false,
53
+      "internalID" : "D01B7280-2C4C-4FB1-AC1F-4E61B70A1F6D",
54
+      "localizations" : [
55
+        {
56
+          "description" : "One-time premium purchase for lifetime access.",
57
+          "displayName" : "Premium Lifetime",
58
+          "locale" : "en_US"
59
+        }
60
+      ],
61
+      "productID" : "com.mqldev.zoomapp.premium.lifetime",
62
+      "referenceName" : "Premium Lifetime",
63
+      "type" : "NonConsumable"
64
+    }
65
+  ],
66
+  "settings" : {
67
+    "_applicationInternalID" : "6A5A2E7C-2D73-4D48-8D32-7D5580A4B2F1",
68
+    "_developerTeamID" : "",
69
+    "_disableDialogs" : false,
70
+    "_failTransactionsEnabled" : false,
71
+    "_lastSynchronizedDate" : 0,
72
+    "_locale" : "en_US",
73
+    "_renewalRate" : 0,
74
+    "_storefront" : "USA",
75
+    "_timeRate" : 0
76
+  },
77
+  "subscriptionGroups" : [],
78
+  "version" : {
79
+    "major" : 3,
80
+    "minor" : 0
81
+  }
82
+}

+ 640 - 17
zoom_app/ViewController.swift

@@ -8,24 +8,140 @@
8 8
 import Cocoa
9 9
 import CryptoKit
10 10
 import Network
11
+import StoreKit
11 12
 import WebKit
12 13
 
13 14
 class ViewController: NSViewController {
14 15
     private let googleOAuth = GoogleOAuthService.shared
15 16
     private let zoomOAuth = ZoomOAuthService.shared
16 17
     private let loginStateKey = "zoom_app.isLoggedIn"
18
+    private let darkModeDefaultsKey = "settings.darkModeEnabled"
19
+
20
+    private struct Palette {
21
+        let isDarkMode: Bool
22
+
23
+        var appBackground: NSColor {
24
+            isDarkMode
25
+                ? NSColor(calibratedRed: 10 / 255, green: 11 / 255, blue: 12 / 255, alpha: 1)
26
+                : NSColor(calibratedWhite: 0.96, alpha: 1)
27
+        }
28
+        var sidebarBackground: NSColor {
29
+            isDarkMode
30
+                ? NSColor(calibratedRed: 16 / 255, green: 17 / 255, blue: 19 / 255, alpha: 1)
31
+                : NSColor(calibratedWhite: 0.94, alpha: 1)
32
+        }
33
+        var sidebarActiveBackground: NSColor {
34
+            isDarkMode
35
+                ? NSColor(calibratedRed: 22 / 255, green: 23 / 255, blue: 26 / 255, alpha: 1)
36
+                : NSColor(calibratedWhite: 0.86, alpha: 1)
37
+        }
38
+        var cardBackground: NSColor {
39
+            isDarkMode
40
+                ? NSColor(calibratedRed: 20 / 255, green: 21 / 255, blue: 24 / 255, alpha: 1)
41
+                : NSColor.white
42
+        }
43
+        var secondaryCardBackground: NSColor {
44
+            isDarkMode
45
+                ? NSColor(calibratedRed: 22 / 255, green: 23 / 255, blue: 26 / 255, alpha: 1)
46
+                : NSColor(calibratedWhite: 0.98, alpha: 1)
47
+        }
48
+        var appShellBackground: NSColor { appBackground }
49
+        var contentShellBackground: NSColor { appBackground }
50
+        var topStripBackground: NSColor { chromeUnifiedBackground }
51
+        var chromeUnifiedBackground: NSColor {
52
+            isDarkMode
53
+                ? NSColor(calibratedRed: 22 / 255, green: 23 / 255, blue: 26 / 255, alpha: 1)
54
+                : NSColor(calibratedWhite: 0.92, alpha: 1)
55
+        }
56
+        var searchPillBackground: NSColor {
57
+            isDarkMode
58
+                ? NSColor.white.withAlphaComponent(0.06)
59
+                : NSColor.black.withAlphaComponent(0.06)
60
+        }
61
+        var meetingCardBackground: NSColor {
62
+            isDarkMode
63
+                ? NSColor(calibratedRed: 30 / 255, green: 34 / 255, blue: 42 / 255, alpha: 1)
64
+                : NSColor(calibratedWhite: 0.98, alpha: 1)
65
+        }
66
+
67
+        var sectionCard: NSColor {
68
+            isDarkMode
69
+                ? NSColor(calibratedRed: 18 / 255, green: 19 / 255, blue: 22 / 255, alpha: 1)
70
+                : NSColor.white
71
+        }
72
+        var inputBackground: NSColor {
73
+            isDarkMode
74
+                ? NSColor(calibratedRed: 24 / 255, green: 25 / 255, blue: 29 / 255, alpha: 1)
75
+                : NSColor(calibratedWhite: 0.97, alpha: 1)
76
+        }
77
+        var inputBorder: NSColor {
78
+            isDarkMode
79
+                ? NSColor.white.withAlphaComponent(0.08)
80
+                : NSColor.black.withAlphaComponent(0.10)
81
+        }
82
+
83
+        var primaryText: NSColor { isDarkMode ? NSColor(calibratedWhite: 0.98, alpha: 1) : NSColor(calibratedWhite: 0.10, alpha: 1) }
84
+        var secondaryText: NSColor { isDarkMode ? NSColor(calibratedWhite: 0.78, alpha: 1) : NSColor(calibratedWhite: 0.30, alpha: 1) }
85
+        var mutedText: NSColor { isDarkMode ? NSColor(calibratedWhite: 0.66, alpha: 1) : NSColor(calibratedWhite: 0.42, alpha: 1) }
86
+    }
87
+
88
+    private final class TopAlignedClipView: NSClipView {
89
+        override func constrainBoundsRect(_ proposedBounds: NSRect) -> NSRect {
90
+            var rect = super.constrainBoundsRect(proposedBounds)
91
+            rect.origin.x = 0
92
+            return rect
93
+        }
94
+    }
95
+
96
+    private final class HoverButton: NSButton {
97
+        var normalColor: NSColor = .clear { didSet { applyBackground() } }
98
+        var hoverColor: NSColor = .clear
99
+        private var tracking: NSTrackingArea?
100
+        private var hovering = false { didSet { applyBackground() } }
101
+
102
+        override func updateTrackingAreas() {
103
+            super.updateTrackingAreas()
104
+            if let tracking { removeTrackingArea(tracking) }
105
+            let area = NSTrackingArea(rect: bounds, options: [.activeInActiveApp, .mouseEnteredAndExited, .inVisibleRect], owner: self, userInfo: nil)
106
+            addTrackingArea(area)
107
+            tracking = area
108
+        }
109
+
110
+        override func mouseEntered(with event: NSEvent) {
111
+            hovering = true
112
+        }
113
+
114
+        override func mouseExited(with event: NSEvent) {
115
+            hovering = false
116
+        }
117
+
118
+        private func applyBackground() {
119
+            wantsLayer = true
120
+            layer?.backgroundColor = (hovering ? hoverColor : normalColor).cgColor
121
+        }
122
+    }
123
+
124
+    private var palette = Palette(isDarkMode: true)
125
+    private var darkModeEnabled: Bool {
126
+        get {
127
+            let hasValue = UserDefaults.standard.object(forKey: darkModeDefaultsKey) != nil
128
+            return hasValue ? UserDefaults.standard.bool(forKey: darkModeDefaultsKey) : systemPrefersDarkMode()
129
+        }
130
+        set { UserDefaults.standard.set(newValue, forKey: darkModeDefaultsKey) }
131
+    }
132
+
17 133
     private let sidebarWidth: CGFloat = 78
18
-    private let appBackground = NSColor(calibratedRed: 10 / 255, green: 11 / 255, blue: 12 / 255, alpha: 1)
19
-    private let sidebarBackground = NSColor(calibratedRed: 16 / 255, green: 17 / 255, blue: 19 / 255, alpha: 1)
20
-    private let sidebarActiveBackground = NSColor(calibratedRed: 22 / 255, green: 23 / 255, blue: 26 / 255, alpha: 1)
21
-    private let cardBackground = NSColor(calibratedRed: 20 / 255, green: 21 / 255, blue: 24 / 255, alpha: 1)
22
-    private let secondaryCardBackground = NSColor(calibratedRed: 22 / 255, green: 23 / 255, blue: 26 / 255, alpha: 1)
23
-    private let appShellBackground = NSColor(calibratedRed: 10 / 255, green: 11 / 255, blue: 12 / 255, alpha: 1)
24
-    private let contentShellBackground = NSColor(calibratedRed: 10 / 255, green: 11 / 255, blue: 12 / 255, alpha: 1)
25
-    private let topStripBackground = NSColor(calibratedRed: 22 / 255, green: 23 / 255, blue: 26 / 255, alpha: 1)
26
-    private let chromeUnifiedBackground = NSColor(calibratedRed: 22 / 255, green: 23 / 255, blue: 26 / 255, alpha: 1)
27
-    private let searchPillBackground = NSColor.white.withAlphaComponent(0.06)
28
-    private let meetingCardBackground = NSColor(calibratedRed: 30 / 255, green: 34 / 255, blue: 42 / 255, alpha: 1)
134
+    private var appBackground: NSColor { palette.appBackground }
135
+    private var sidebarBackground: NSColor { palette.sidebarBackground }
136
+    private var sidebarActiveBackground: NSColor { palette.sidebarActiveBackground }
137
+    private var cardBackground: NSColor { palette.cardBackground }
138
+    private var secondaryCardBackground: NSColor { palette.secondaryCardBackground }
139
+    private var appShellBackground: NSColor { palette.appShellBackground }
140
+    private var contentShellBackground: NSColor { palette.contentShellBackground }
141
+    private var topStripBackground: NSColor { palette.topStripBackground }
142
+    private var chromeUnifiedBackground: NSColor { palette.chromeUnifiedBackground }
143
+    private var searchPillBackground: NSColor { palette.searchPillBackground }
144
+    private var meetingCardBackground: NSColor { palette.meetingCardBackground }
29 145
     private let appShellCornerRadius: CGFloat = 20
30 146
     private let homeChromeHeaderHeight: CGFloat = 56
31 147
     private let nativeTrafficLightsLeading: CGFloat = 14
@@ -33,9 +149,9 @@ class ViewController: NSViewController {
33 149
     private let brandLeadingInset: CGFloat = 84
34 150
     private let accentBlue = NSColor(calibratedRed: 27 / 255, green: 115 / 255, blue: 232 / 255, alpha: 1)
35 151
     private let accentOrange = NSColor(calibratedRed: 254 / 255, green: 117 / 255, blue: 46 / 255, alpha: 1)
36
-    private let primaryText = NSColor(calibratedWhite: 0.98, alpha: 1)
37
-    private let secondaryText = NSColor(calibratedWhite: 0.78, alpha: 1)
38
-    private let mutedText = NSColor(calibratedWhite: 0.66, alpha: 1)
152
+    private var primaryText: NSColor { palette.primaryText }
153
+    private var secondaryText: NSColor { palette.secondaryText }
154
+    private var mutedText: NSColor { palette.mutedText }
39 155
 
40 156
     private let rootContainer = NSView()
41 157
     private var loginView: NSView?
@@ -62,6 +178,11 @@ class ViewController: NSViewController {
62 178
     private weak var homePlaceholderLabel: NSTextField?
63 179
     private weak var homeSearchField: NSTextField?
64 180
     private weak var homeSearchPill: NSView?
181
+    private weak var homeSettingsView: NSView?
182
+    private weak var settingsDarkModeSwitch: NSSwitch?
183
+    private weak var settingsUpgradeButton: NSButton?
184
+    private weak var settingsRestoreButton: NSButton?
185
+    private weak var settingsGoogleActionButton: NSButton?
65 186
     private var allScheduledMeetings: [ScheduledMeeting] = []
66 187
     private var selectedMeetingsDayStart: Date = Calendar.current.startOfDay(for: Date())
67 188
     private var selectedHomeSidebarItem: String = "Home"
@@ -89,9 +210,136 @@ class ViewController: NSViewController {
89 210
         case home
90 211
     }
91 212
 
213
+    private enum PremiumPlan: String, CaseIterable {
214
+        case weekly = "com.mqldev.zoomapp.premium.weekly"
215
+        case monthly = "com.mqldev.zoomapp.premium.monthly"
216
+        case yearly = "com.mqldev.zoomapp.premium.yearly"
217
+        case lifetime = "com.mqldev.zoomapp.premium.lifetime"
218
+
219
+        var displayName: String {
220
+            switch self {
221
+            case .weekly: return "Premium Weekly"
222
+            case .monthly: return "Premium Monthly"
223
+            case .yearly: return "Premium Yearly"
224
+            case .lifetime: return "Premium Lifetime"
225
+            }
226
+        }
227
+    }
228
+
229
+    private final class StoreKitCoordinator {
230
+        enum PurchaseOutcome {
231
+            case success
232
+            case pending
233
+            case cancelled
234
+            case failed(String)
235
+        }
236
+
237
+        private(set) var productsByID: [String: Product] = [:]
238
+        private(set) var activeProductIDs = Set<String>()
239
+
240
+        var hasPremiumAccess: Bool { !activeProductIDs.isEmpty }
241
+
242
+        private var transactionUpdatesTask: Task<Void, Never>?
243
+        var onEntitlementsChanged: ((Bool) -> Void)?
244
+
245
+        func start() async {
246
+            await refreshProducts()
247
+            await refreshEntitlements()
248
+            observeTransactionUpdatesIfNeeded()
249
+        }
250
+
251
+        func refreshProducts() async {
252
+            do {
253
+                let products = try await Product.products(for: PremiumPlan.allCases.map(\.rawValue))
254
+                productsByID = Dictionary(uniqueKeysWithValues: products.map { ($0.id, $0) })
255
+            } catch {
256
+                productsByID = [:]
257
+            }
258
+        }
259
+
260
+        func purchase(plan: PremiumPlan) async -> PurchaseOutcome {
261
+            guard let product = productsByID[plan.rawValue] else {
262
+                await refreshProducts()
263
+                guard let refreshed = productsByID[plan.rawValue] else {
264
+                    return .failed("Product not available. Check StoreKit configuration and product IDs.")
265
+                }
266
+                return await purchase(product: refreshed)
267
+            }
268
+            return await purchase(product: product)
269
+        }
270
+
271
+        func restorePurchases() async -> String {
272
+            do {
273
+                try await AppStore.sync()
274
+                await refreshEntitlements()
275
+                return hasPremiumAccess ? "Purchases restored successfully." : "No previous premium purchase was found for this Apple ID."
276
+            } catch {
277
+                return "Restore failed. \(error.localizedDescription)"
278
+            }
279
+        }
280
+
281
+        private func purchase(product: Product) async -> PurchaseOutcome {
282
+            do {
283
+                let result = try await product.purchase()
284
+                switch result {
285
+                case .success(let verification):
286
+                    guard case .verified(let transaction) = verification else { return .failed("Purchase verification failed.") }
287
+                    await transaction.finish()
288
+                    await refreshEntitlements()
289
+                    return .success
290
+                case .pending:
291
+                    return .pending
292
+                case .userCancelled:
293
+                    return .cancelled
294
+                @unknown default:
295
+                    return .failed("Unknown purchase state.")
296
+                }
297
+            } catch {
298
+                return .failed(error.localizedDescription)
299
+            }
300
+        }
301
+
302
+        private func observeTransactionUpdatesIfNeeded() {
303
+            guard transactionUpdatesTask == nil else { return }
304
+            transactionUpdatesTask = Task { [weak self] in
305
+                for await update in Transaction.updates {
306
+                    guard case .verified(let transaction) = update else { continue }
307
+                    if PremiumPlan.allCases.map(\.rawValue).contains(transaction.productID) {
308
+                        await self?.refreshEntitlements()
309
+                    }
310
+                    await transaction.finish()
311
+                }
312
+            }
313
+        }
314
+
315
+        @MainActor
316
+        private func refreshEntitlements() async {
317
+            var active = Set<String>()
318
+            for await entitlement in Transaction.currentEntitlements {
319
+                guard case .verified(let transaction) = entitlement else { continue }
320
+                if PremiumPlan.allCases.map(\.rawValue).contains(transaction.productID),
321
+                   transaction.revocationDate == nil {
322
+                    active.insert(transaction.productID)
323
+                }
324
+            }
325
+            let changed = active != activeProductIDs
326
+            activeProductIDs = active
327
+            if changed {
328
+                onEntitlementsChanged?(hasPremiumAccess)
329
+            }
330
+        }
331
+    }
332
+
333
+    private let storeKitCoordinator = StoreKitCoordinator()
334
+    private var storeKitStartupTask: Task<Void, Never>?
335
+
92 336
     override func viewDidLoad() {
93 337
         super.viewDidLoad()
338
+        palette = Palette(isDarkMode: darkModeEnabled)
339
+        NSApp.appearance = NSAppearance(named: darkModeEnabled ? .darkAqua : .aqua)
340
+        view.appearance = NSAppearance(named: darkModeEnabled ? .darkAqua : .aqua)
94 341
         setupUI()
342
+        startStoreKit()
95 343
     }
96 344
 
97 345
     override func viewDidAppear() {
@@ -474,6 +722,14 @@ class ViewController: NSViewController {
474 722
         alert.runModal()
475 723
     }
476 724
 
725
+    private func showSimpleAlert(title: String, message: String) {
726
+        let alert = NSAlert()
727
+        alert.alertStyle = .informational
728
+        alert.messageText = title
729
+        alert.informativeText = message
730
+        alert.runModal()
731
+    }
732
+
477 733
     private struct ScheduledMeeting {
478 734
         let title: String
479 735
         let start: Date
@@ -936,6 +1192,362 @@ class ViewController: NSViewController {
936 1192
         return root
937 1193
     }
938 1194
 
1195
+    private func makeSettingsView() -> NSView {
1196
+        let panel = NSView()
1197
+        panel.translatesAutoresizingMaskIntoConstraints = false
1198
+
1199
+        let scroll = NSScrollView()
1200
+        scroll.translatesAutoresizingMaskIntoConstraints = false
1201
+        scroll.drawsBackground = false
1202
+        scroll.hasHorizontalScroller = false
1203
+        scroll.hasVerticalScroller = true
1204
+        scroll.autohidesScrollers = true
1205
+        scroll.borderType = .noBorder
1206
+        scroll.scrollerStyle = .overlay
1207
+        scroll.automaticallyAdjustsContentInsets = false
1208
+        let clip = TopAlignedClipView()
1209
+        clip.drawsBackground = false
1210
+        scroll.contentView = clip
1211
+        panel.addSubview(scroll)
1212
+
1213
+        let content = NSView()
1214
+        content.translatesAutoresizingMaskIntoConstraints = false
1215
+        scroll.documentView = content
1216
+
1217
+        let card = roundedContainer(cornerRadius: 16, color: palette.sectionCard)
1218
+        card.translatesAutoresizingMaskIntoConstraints = false
1219
+        styleSurface(card, borderColor: palette.inputBorder, borderWidth: 1, shadow: true)
1220
+        content.addSubview(card)
1221
+
1222
+        let stack = NSStackView()
1223
+        stack.translatesAutoresizingMaskIntoConstraints = false
1224
+        stack.orientation = .vertical
1225
+        stack.spacing = 18
1226
+        stack.alignment = .leading
1227
+        card.addSubview(stack)
1228
+
1229
+        let pageTitle = textLabel("Settings", font: .systemFont(ofSize: 28, weight: .bold), color: primaryText)
1230
+        let pageSubtitle = textLabel("Manage appearance, account, and app options.", font: .systemFont(ofSize: 13, weight: .regular), color: secondaryText)
1231
+        stack.addArrangedSubview(pageTitle)
1232
+        stack.addArrangedSubview(pageSubtitle)
1233
+        stack.setCustomSpacing(24, after: pageSubtitle)
1234
+
1235
+        let appearanceTitle = textLabel("Appearance", font: .systemFont(ofSize: 16, weight: .semibold), color: primaryText)
1236
+        stack.addArrangedSubview(appearanceTitle)
1237
+        let darkModeRow = makeSettingsDarkModeRow()
1238
+        stack.addArrangedSubview(darkModeRow)
1239
+        darkModeRow.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
1240
+        stack.setCustomSpacing(24, after: darkModeRow)
1241
+
1242
+        let accountTitle = textLabel("Account", font: .systemFont(ofSize: 16, weight: .semibold), color: primaryText)
1243
+        stack.addArrangedSubview(accountTitle)
1244
+        let googleAccountRow = makeSettingsGoogleAccountRow()
1245
+        stack.addArrangedSubview(googleAccountRow)
1246
+        googleAccountRow.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
1247
+        stack.setCustomSpacing(24, after: googleAccountRow)
1248
+
1249
+        let appTitle = textLabel("App", font: .systemFont(ofSize: 16, weight: .semibold), color: primaryText)
1250
+        stack.addArrangedSubview(appTitle)
1251
+        let shareButton = makeSettingsActionButton(icon: "⤴︎", title: "Share App", action: .shareApp)
1252
+        stack.addArrangedSubview(shareButton)
1253
+        shareButton.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
1254
+
1255
+        let upgradeButton = makeSettingsActionButton(icon: "⬆︎", title: "Upgrade", action: .upgrade)
1256
+        stack.addArrangedSubview(upgradeButton)
1257
+        upgradeButton.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
1258
+        settingsUpgradeButton = upgradeButton
1259
+
1260
+        let restoreButton = makeSettingsActionButton(icon: "↺", title: "Restore Purchases", action: .restorePurchases)
1261
+        stack.addArrangedSubview(restoreButton)
1262
+        restoreButton.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
1263
+        settingsRestoreButton = restoreButton
1264
+        stack.setCustomSpacing(24, after: restoreButton)
1265
+
1266
+        let legalTitle = textLabel("Help & Legal", font: .systemFont(ofSize: 16, weight: .semibold), color: primaryText)
1267
+        stack.addArrangedSubview(legalTitle)
1268
+        let privacyButton = makeSettingsActionButton(icon: "🔒", title: "Privacy Policy", action: .privacyPolicy)
1269
+        stack.addArrangedSubview(privacyButton)
1270
+        privacyButton.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
1271
+        let supportButton = makeSettingsActionButton(icon: "💬", title: "Support", action: .support)
1272
+        stack.addArrangedSubview(supportButton)
1273
+        supportButton.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
1274
+        let termsButton = makeSettingsActionButton(icon: "📄", title: "Terms of Services", action: .termsOfServices)
1275
+        stack.addArrangedSubview(termsButton)
1276
+        termsButton.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
1277
+
1278
+        NSLayoutConstraint.activate([
1279
+            scroll.leadingAnchor.constraint(equalTo: panel.leadingAnchor),
1280
+            scroll.trailingAnchor.constraint(equalTo: panel.trailingAnchor),
1281
+            scroll.topAnchor.constraint(equalTo: panel.topAnchor),
1282
+            scroll.bottomAnchor.constraint(equalTo: panel.bottomAnchor),
1283
+
1284
+            content.leadingAnchor.constraint(equalTo: scroll.contentView.leadingAnchor),
1285
+            content.trailingAnchor.constraint(equalTo: scroll.contentView.trailingAnchor),
1286
+            content.topAnchor.constraint(equalTo: scroll.contentView.topAnchor),
1287
+            content.bottomAnchor.constraint(greaterThanOrEqualTo: scroll.contentView.bottomAnchor),
1288
+            content.widthAnchor.constraint(equalTo: scroll.contentView.widthAnchor),
1289
+
1290
+            card.centerXAnchor.constraint(equalTo: content.centerXAnchor),
1291
+            card.topAnchor.constraint(equalTo: content.topAnchor, constant: 36),
1292
+            content.bottomAnchor.constraint(greaterThanOrEqualTo: card.bottomAnchor, constant: 36),
1293
+            card.widthAnchor.constraint(lessThanOrEqualToConstant: 620),
1294
+            card.widthAnchor.constraint(greaterThanOrEqualToConstant: 460),
1295
+            card.leadingAnchor.constraint(greaterThanOrEqualTo: content.leadingAnchor, constant: 30),
1296
+            card.trailingAnchor.constraint(lessThanOrEqualTo: content.trailingAnchor, constant: -30),
1297
+
1298
+            stack.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 28),
1299
+            stack.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -28),
1300
+            stack.topAnchor.constraint(equalTo: card.topAnchor, constant: 24),
1301
+            stack.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -24)
1302
+        ])
1303
+
1304
+        updatePremiumButtons()
1305
+        return panel
1306
+    }
1307
+
1308
+    private enum SettingsAction: Int {
1309
+        case shareApp = 1
1310
+        case upgrade = 2
1311
+        case restorePurchases = 3
1312
+        case privacyPolicy = 4
1313
+        case support = 5
1314
+        case termsOfServices = 6
1315
+    }
1316
+
1317
+    private func makeSettingsDarkModeRow() -> NSView {
1318
+        let row = roundedContainer(cornerRadius: 10, color: palette.inputBackground)
1319
+        row.translatesAutoresizingMaskIntoConstraints = false
1320
+        row.heightAnchor.constraint(equalToConstant: 52).isActive = true
1321
+        styleSurface(row, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
1322
+
1323
+        let icon = textLabel("◐", font: NSFont.systemFont(ofSize: 18, weight: .medium), color: primaryText)
1324
+        let title = textLabel("Dark Mode", font: NSFont.systemFont(ofSize: 15, weight: .semibold), color: primaryText)
1325
+        let toggle = NSSwitch()
1326
+        toggle.translatesAutoresizingMaskIntoConstraints = false
1327
+        toggle.state = darkModeEnabled ? .on : .off
1328
+        toggle.target = self
1329
+        toggle.action = #selector(settingsDarkModeToggled(_:))
1330
+        settingsDarkModeSwitch = toggle
1331
+
1332
+        row.addSubview(icon)
1333
+        row.addSubview(title)
1334
+        row.addSubview(toggle)
1335
+        NSLayoutConstraint.activate([
1336
+            icon.leadingAnchor.constraint(equalTo: row.leadingAnchor, constant: 14),
1337
+            icon.centerYAnchor.constraint(equalTo: row.centerYAnchor),
1338
+            title.leadingAnchor.constraint(equalTo: icon.trailingAnchor, constant: 10),
1339
+            title.centerYAnchor.constraint(equalTo: row.centerYAnchor),
1340
+            toggle.trailingAnchor.constraint(equalTo: row.trailingAnchor, constant: -14),
1341
+            toggle.centerYAnchor.constraint(equalTo: row.centerYAnchor)
1342
+        ])
1343
+        return row
1344
+    }
1345
+
1346
+    private func makeSettingsGoogleAccountRow() -> NSView {
1347
+        let row = roundedContainer(cornerRadius: 10, color: palette.inputBackground)
1348
+        row.translatesAutoresizingMaskIntoConstraints = false
1349
+        styleSurface(row, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
1350
+
1351
+        let signedIn = isUserLoggedIn()
1352
+        let titleText = signedIn ? "Google account connected" : "Google account not connected"
1353
+        let subtitleText = signedIn ? "Signed in" : "Sign in to sync your meetings and calendar."
1354
+
1355
+        let title = textLabel(titleText, font: NSFont.systemFont(ofSize: 15, weight: .semibold), color: primaryText)
1356
+        let subtitle = textLabel(subtitleText, font: NSFont.systemFont(ofSize: 13, weight: .regular), color: secondaryText)
1357
+        subtitle.maximumNumberOfLines = 2
1358
+        subtitle.lineBreakMode = .byTruncatingTail
1359
+
1360
+        let actionButton = NSButton(title: signedIn ? "Sign Out" : "Sign in with Google", target: self, action: #selector(settingsGoogleActionButtonClicked(_:)))
1361
+        actionButton.translatesAutoresizingMaskIntoConstraints = false
1362
+        actionButton.bezelStyle = .rounded
1363
+        actionButton.controlSize = .regular
1364
+        settingsGoogleActionButton = actionButton
1365
+
1366
+        row.addSubview(title)
1367
+        row.addSubview(subtitle)
1368
+        row.addSubview(actionButton)
1369
+        NSLayoutConstraint.activate([
1370
+            row.heightAnchor.constraint(equalToConstant: 78),
1371
+            title.leadingAnchor.constraint(equalTo: row.leadingAnchor, constant: 14),
1372
+            title.topAnchor.constraint(equalTo: row.topAnchor, constant: 12),
1373
+            subtitle.leadingAnchor.constraint(equalTo: title.leadingAnchor),
1374
+            subtitle.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 4),
1375
+            subtitle.trailingAnchor.constraint(lessThanOrEqualTo: actionButton.leadingAnchor, constant: -14),
1376
+            actionButton.trailingAnchor.constraint(equalTo: row.trailingAnchor, constant: -14),
1377
+            actionButton.centerYAnchor.constraint(equalTo: row.centerYAnchor)
1378
+        ])
1379
+
1380
+        return row
1381
+    }
1382
+
1383
+    private func makeSettingsActionButton(icon: String, title: String, action: SettingsAction) -> NSButton {
1384
+        let button = HoverButton(title: "", target: self, action: #selector(settingsPageActionButtonClicked(_:)))
1385
+        button.translatesAutoresizingMaskIntoConstraints = false
1386
+        button.isBordered = false
1387
+        button.wantsLayer = true
1388
+        button.layer?.cornerRadius = 10
1389
+        button.normalColor = palette.inputBackground
1390
+        button.hoverColor = palette.isDarkMode ? NSColor.white.withAlphaComponent(0.07) : NSColor.black.withAlphaComponent(0.05)
1391
+        styleSurface(button, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
1392
+        button.heightAnchor.constraint(equalToConstant: 46).isActive = true
1393
+        button.tag = action.rawValue
1394
+
1395
+        let iconLabel = textLabel(icon, font: NSFont.systemFont(ofSize: 17, weight: .medium), color: primaryText)
1396
+        let titleLabel = textLabel(title, font: NSFont.systemFont(ofSize: 15, weight: .semibold), color: primaryText)
1397
+        button.addSubview(iconLabel)
1398
+        button.addSubview(titleLabel)
1399
+        NSLayoutConstraint.activate([
1400
+            iconLabel.leadingAnchor.constraint(equalTo: button.leadingAnchor, constant: 14),
1401
+            iconLabel.centerYAnchor.constraint(equalTo: button.centerYAnchor),
1402
+            titleLabel.leadingAnchor.constraint(equalTo: iconLabel.trailingAnchor, constant: 10),
1403
+            titleLabel.centerYAnchor.constraint(equalTo: button.centerYAnchor)
1404
+        ])
1405
+        return button
1406
+    }
1407
+
1408
+    @objc private func settingsPageActionButtonClicked(_ sender: NSButton) {
1409
+        guard let action = SettingsAction(rawValue: sender.tag) else { return }
1410
+        switch action {
1411
+        case .shareApp:
1412
+            showSimpleAlert(title: "Share", message: "Share action placeholder (match reference app behavior next).")
1413
+        case .upgrade:
1414
+            settingsUpgradePremiumTapped()
1415
+        case .restorePurchases:
1416
+            settingsRestorePurchasesTapped()
1417
+        case .privacyPolicy:
1418
+            showSimpleAlert(title: "Privacy Policy", message: "Add your Privacy Policy URL in the app and open it here.")
1419
+        case .support:
1420
+            showSimpleAlert(title: "Support", message: "Add your Support URL/email in the app and open it here.")
1421
+        case .termsOfServices:
1422
+            showSimpleAlert(title: "Terms", message: "Add your Terms URL in the app and open it here.")
1423
+        }
1424
+    }
1425
+
1426
+    @objc private func settingsGoogleActionButtonClicked(_ sender: NSButton) {
1427
+        if isUserLoggedIn() {
1428
+            logoutTapped()
1429
+            return
1430
+        }
1431
+        showSimpleAlert(title: "Sign in", message: "Please sign in from the login screen to connect your account.")
1432
+    }
1433
+
1434
+    private func roundedContainer(cornerRadius: CGFloat, color: NSColor) -> NSView {
1435
+        let v = NSView()
1436
+        v.wantsLayer = true
1437
+        v.layer?.backgroundColor = color.cgColor
1438
+        v.layer?.cornerRadius = cornerRadius
1439
+        return v
1440
+    }
1441
+
1442
+    private func styleSurface(_ view: NSView, borderColor: NSColor, borderWidth: CGFloat, shadow: Bool) {
1443
+        view.wantsLayer = true
1444
+        view.layer?.borderColor = borderColor.cgColor
1445
+        view.layer?.borderWidth = borderWidth
1446
+        if shadow {
1447
+            view.layer?.shadowColor = NSColor.black.cgColor
1448
+            view.layer?.shadowOpacity = palette.isDarkMode ? 0.22 : 0.12
1449
+            view.layer?.shadowRadius = 18
1450
+            view.layer?.shadowOffset = NSSize(width: 0, height: -2)
1451
+        } else {
1452
+            view.layer?.shadowOpacity = 0
1453
+        }
1454
+    }
1455
+
1456
+    private func textLabel(_ text: String, font: NSFont, color: NSColor) -> NSTextField {
1457
+        let label = NSTextField(labelWithString: text)
1458
+        label.translatesAutoresizingMaskIntoConstraints = false
1459
+        label.font = font
1460
+        label.textColor = color
1461
+        label.backgroundColor = .clear
1462
+        label.isBezeled = false
1463
+        label.isEditable = false
1464
+        label.isSelectable = false
1465
+        return label
1466
+    }
1467
+
1468
+    private func startStoreKit() {
1469
+        storeKitStartupTask?.cancel()
1470
+        storeKitCoordinator.onEntitlementsChanged = { [weak self] _ in
1471
+            DispatchQueue.main.async {
1472
+                self?.updatePremiumButtons()
1473
+            }
1474
+        }
1475
+        storeKitStartupTask = Task { [weak self] in
1476
+            await self?.storeKitCoordinator.start()
1477
+            await MainActor.run {
1478
+                self?.updatePremiumButtons()
1479
+            }
1480
+        }
1481
+    }
1482
+
1483
+    @MainActor
1484
+    private func updatePremiumButtons() {
1485
+        let isPremium = storeKitCoordinator.hasPremiumAccess
1486
+        settingsUpgradeButton?.title = isPremium ? "Premium Active" : "Upgrade"
1487
+        settingsUpgradeButton?.isEnabled = isPremium == false
1488
+        settingsUpgradeButton?.alphaValue = isPremium ? 0.6 : 1.0
1489
+        settingsRestoreButton?.isEnabled = true
1490
+    }
1491
+
1492
+    @objc private func settingsDarkModeToggled(_ sender: NSSwitch) {
1493
+        setDarkMode(sender.state == .on)
1494
+    }
1495
+
1496
+    private func setDarkMode(_ enabled: Bool) {
1497
+        darkModeEnabled = enabled
1498
+        palette = Palette(isDarkMode: enabled)
1499
+        NSApp.appearance = NSAppearance(named: enabled ? .darkAqua : .aqua)
1500
+        view.appearance = NSAppearance(named: enabled ? .darkAqua : .aqua)
1501
+
1502
+        if isUserLoggedIn() {
1503
+            let keepSelected = selectedHomeSidebarItem
1504
+            selectedHomeSidebarItem = keepSelected
1505
+            showHomeView(profile: nil)
1506
+        } else {
1507
+            showLoginView()
1508
+        }
1509
+    }
1510
+
1511
+    private func systemPrefersDarkMode() -> Bool {
1512
+        let global = UserDefaults.standard.persistentDomain(forName: UserDefaults.globalDomain)
1513
+        let style = global?["AppleInterfaceStyle"] as? String
1514
+        return style?.lowercased() == "dark"
1515
+    }
1516
+
1517
+    @objc private func settingsUpgradePremiumTapped() {
1518
+        guard storeKitCoordinator.hasPremiumAccess == false else { return }
1519
+        settingsUpgradeButton?.isEnabled = false
1520
+        settingsUpgradeButton?.alphaValue = 0.6
1521
+        Task { [weak self] in
1522
+            guard let self else { return }
1523
+            let result = await self.storeKitCoordinator.purchase(plan: .lifetime)
1524
+            await MainActor.run {
1525
+                self.updatePremiumButtons()
1526
+                switch result {
1527
+                case .success:
1528
+                    self.showSimpleAlert(title: "Purchase Complete", message: "Premium has been unlocked successfully.")
1529
+                case .pending:
1530
+                    self.showSimpleAlert(title: "Purchase Pending", message: "Your purchase is pending approval. You can continue once it completes.")
1531
+                case .cancelled:
1532
+                    break
1533
+                case .failed(let message):
1534
+                    self.showSimpleAlert(title: "Purchase Failed", message: message)
1535
+                }
1536
+            }
1537
+        }
1538
+    }
1539
+
1540
+    @objc private func settingsRestorePurchasesTapped() {
1541
+        Task { [weak self] in
1542
+            guard let self else { return }
1543
+            let message = await self.storeKitCoordinator.restorePurchases()
1544
+            await MainActor.run {
1545
+                self.updatePremiumButtons()
1546
+                self.showSimpleAlert(title: "Restore Purchases", message: message)
1547
+            }
1548
+        }
1549
+    }
1550
+
939 1551
     // MARK: - Home UI
940 1552
 
941 1553
     private func makeHomeView(profile: GoogleUserProfile?) -> NSView {
@@ -1174,6 +1786,9 @@ class ViewController: NSViewController {
1174 1786
         let placeholder = makeLabel("Coming soon", size: 22, color: secondaryText, weight: .semibold, centered: true)
1175 1787
         placeholder.isHidden = true
1176 1788
 
1789
+        let settingsView = makeSettingsView()
1790
+        settingsView.isHidden = selectedHomeSidebarItem != "Settings"
1791
+
1177 1792
         let contentColumn = NSView()
1178 1793
         contentColumn.translatesAutoresizingMaskIntoConstraints = false
1179 1794
         content.addSubview(topBar)
@@ -1192,7 +1807,7 @@ class ViewController: NSViewController {
1192 1807
         [searchIcon, searchField, searchHintLabel].forEach {
1193 1808
             searchPill.addSubview($0)
1194 1809
         }
1195
-        [welcome, timeTitle, dateTitle, actions, panel, panelHeader, meetingsStatus, meetingsDayNav, noMeeting, meetingsScrollView, refreshMeetingsButton, placeholder].forEach {
1810
+        [welcome, timeTitle, dateTitle, actions, panel, panelHeader, meetingsStatus, meetingsDayNav, noMeeting, meetingsScrollView, refreshMeetingsButton, placeholder, settingsView].forEach {
1196 1811
             $0.translatesAutoresizingMaskIntoConstraints = false
1197 1812
             contentColumn.addSubview($0)
1198 1813
         }
@@ -1298,7 +1913,12 @@ class ViewController: NSViewController {
1298 1913
             refreshMeetingsButton.heightAnchor.constraint(equalToConstant: 40),
1299 1914
 
1300 1915
             placeholder.centerXAnchor.constraint(equalTo: contentColumn.centerXAnchor),
1301
-            placeholder.centerYAnchor.constraint(equalTo: contentColumn.centerYAnchor)
1916
+            placeholder.centerYAnchor.constraint(equalTo: contentColumn.centerYAnchor),
1917
+
1918
+            settingsView.topAnchor.constraint(equalTo: contentColumn.topAnchor),
1919
+            settingsView.leadingAnchor.constraint(equalTo: contentColumn.leadingAnchor),
1920
+            settingsView.trailingAnchor.constraint(equalTo: contentColumn.trailingAnchor),
1921
+            settingsView.bottomAnchor.constraint(equalTo: contentColumn.bottomAnchor)
1302 1922
         ])
1303 1923
 
1304 1924
         timeLabel = timeTitle
@@ -1309,6 +1929,7 @@ class ViewController: NSViewController {
1309 1929
         homeActionsRow = actions
1310 1930
         homeMeetingsPanel = panel
1311 1931
         homePlaceholderLabel = placeholder
1932
+        homeSettingsView = settingsView
1312 1933
         meetingsDayHeaderLabel = panelHeader
1313 1934
         meetingsListStack = meetingsStack
1314 1935
         meetingsStatusLabel = meetingsStatus
@@ -1595,6 +2216,7 @@ class ViewController: NSViewController {
1595 2216
     @MainActor
1596 2217
     private func updateSelectedHomeSectionUI() {
1597 2218
         let isHome = selectedHomeSidebarItem == "Home"
2219
+        let isSettings = selectedHomeSidebarItem == "Settings"
1598 2220
         let title = selectedHomeSidebarItem
1599 2221
 
1600 2222
         homeWelcomeLabel?.stringValue = title
@@ -1613,7 +2235,8 @@ class ViewController: NSViewController {
1613 2235
             meetingsScrollView,
1614 2236
             refreshMeetingsButton
1615 2237
         ]
1616
-        dashboardViews.forEach { $0?.isHidden = isHome == false }
2238
+        dashboardViews.forEach { $0?.isHidden = isHome == false || isSettings }
2239
+        homeSettingsView?.isHidden = isSettings == false
1617 2240
 
1618 2241
         if isHome {
1619 2242
             homePlaceholderLabel?.isHidden = true