From 892ecd3201526c0a5a63be7112044cead2914bba Mon Sep 17 00:00:00 2001 From: Owen Gretzinger Date: Mon, 14 Jul 2025 17:44:25 -0400 Subject: [PATCH] fix: streamline keychain usage (#7) --- meetingnotes/ContentView.swift | 8 --- meetingnotes/Managers/AudioManager.swift | 2 +- meetingnotes/Managers/KeychainHelper.swift | 13 ++++ .../Managers/UserDefaultsManager.swift | 64 +++++++++++++++++++ meetingnotes/Models/Settings.swift | 51 ++++++++++----- meetingnotes/Services/NotesGenerator.swift | 4 +- .../ViewModels/MeetingViewModel.swift | 4 +- .../ViewModels/SettingsViewModel.swift | 36 +++-------- meetingnotes/Views/OnboardingView.swift | 1 + meetingnotes/Views/SettingsView.swift | 2 +- 10 files changed, 126 insertions(+), 59 deletions(-) create mode 100644 meetingnotes/Managers/UserDefaultsManager.swift diff --git a/meetingnotes/ContentView.swift b/meetingnotes/ContentView.swift index e48c687..90adfbd 100644 --- a/meetingnotes/ContentView.swift +++ b/meetingnotes/ContentView.swift @@ -19,14 +19,6 @@ struct ContentView: View { MeetingListView(settingsViewModel: settingsViewModel) } } - .onAppear { - // Force load settings to check onboarding status - settingsViewModel.loadSettings() - } - .onReceive(NotificationCenter.default.publisher(for: Notification.Name("OnboardingReset"))) { _ in - // Reload settings when onboarding is reset - settingsViewModel.loadSettings() - } } } diff --git a/meetingnotes/Managers/AudioManager.swift b/meetingnotes/Managers/AudioManager.swift index 0fc9ccc..4add68b 100644 --- a/meetingnotes/Managers/AudioManager.swift +++ b/meetingnotes/Managers/AudioManager.swift @@ -294,7 +294,7 @@ class AudioManager: NSObject, ObservableObject { } private func connectToOpenAIRealtime(source: AudioSource) { - guard let key = KeychainHelper.shared.get(forKey: "openAIKey"), !key.isEmpty else { + guard let key = KeychainHelper.shared.getAPIKey(), !key.isEmpty else { print("❌ No OpenAI key found") return } diff --git a/meetingnotes/Managers/KeychainHelper.swift b/meetingnotes/Managers/KeychainHelper.swift index 6728d06..e75538d 100644 --- a/meetingnotes/Managers/KeychainHelper.swift +++ b/meetingnotes/Managers/KeychainHelper.swift @@ -12,6 +12,19 @@ class KeychainHelper { private init() {} + /// Gets the API key directly from keychain + /// - Returns: The API key string if found, nil otherwise + func getAPIKey() -> String? { + return get(forKey: "openAIKey") + } + + /// Saves the API key to keychain + /// - Parameter apiKey: The API key to save + /// - Returns: True if the save was successful, false otherwise + func saveAPIKey(_ apiKey: String) -> Bool { + return save(apiKey, forKey: "openAIKey") + } + /// Saves a string value to the keychain /// - Parameters: /// - value: The string value to save diff --git a/meetingnotes/Managers/UserDefaultsManager.swift b/meetingnotes/Managers/UserDefaultsManager.swift new file mode 100644 index 0000000..2e54521 --- /dev/null +++ b/meetingnotes/Managers/UserDefaultsManager.swift @@ -0,0 +1,64 @@ +// UserDefaultsManager.swift +// Manages non-sensitive app settings using UserDefaults + +import Foundation + +/// Manages non-sensitive app settings using UserDefaults +class UserDefaultsManager { + static let shared = UserDefaultsManager() + + private let userDefaults = UserDefaults.standard + + private init() {} + + // MARK: - Keys + private enum Keys { + static let userBlurb = "userBlurb" + static let systemPrompt = "systemPrompt" + static let hasCompletedOnboarding = "hasCompletedOnboarding" + static let hasAcceptedTerms = "hasAcceptedTerms" + static let selectedTemplateId = "selectedTemplateId" + } + + // MARK: - User Blurb + var userBlurb: String { + get { userDefaults.string(forKey: Keys.userBlurb) ?? "" } + set { userDefaults.set(newValue, forKey: Keys.userBlurb) } + } + + // MARK: - System Prompt + var systemPrompt: String { + get { + let stored = userDefaults.string(forKey: Keys.systemPrompt) + return stored?.isEmpty == false ? stored! : Settings.defaultSystemPrompt() + } + set { userDefaults.set(newValue, forKey: Keys.systemPrompt) } + } + + // MARK: - Onboarding Status + var hasCompletedOnboarding: Bool { + get { userDefaults.bool(forKey: Keys.hasCompletedOnboarding) } + set { userDefaults.set(newValue, forKey: Keys.hasCompletedOnboarding) } + } + + // MARK: - Terms Acceptance + var hasAcceptedTerms: Bool { + get { userDefaults.bool(forKey: Keys.hasAcceptedTerms) } + set { userDefaults.set(newValue, forKey: Keys.hasAcceptedTerms) } + } + + // MARK: - Selected Template ID + var selectedTemplateId: UUID? { + get { + guard let uuidString = userDefaults.string(forKey: Keys.selectedTemplateId) else { return nil } + return UUID(uuidString: uuidString) + } + set { + if let uuid = newValue { + userDefaults.set(uuid.uuidString, forKey: Keys.selectedTemplateId) + } else { + userDefaults.removeObject(forKey: Keys.selectedTemplateId) + } + } + } +} \ No newline at end of file diff --git a/meetingnotes/Models/Settings.swift b/meetingnotes/Models/Settings.swift index 809f951..583c8b0 100644 --- a/meetingnotes/Models/Settings.swift +++ b/meetingnotes/Models/Settings.swift @@ -1,12 +1,34 @@ import Foundation struct Settings: Codable { - var openAIKey: String - var userBlurb: String - var systemPrompt: String - var selectedTemplateId: UUID? - var hasCompletedOnboarding: Bool - var hasAcceptedTerms: Bool + // Only store API key in memory - will be loaded from keychain when needed + var openAIKey: String = "" + + // Computed properties that access UserDefaults + var userBlurb: String { + get { UserDefaultsManager.shared.userBlurb } + set { UserDefaultsManager.shared.userBlurb = newValue } + } + + var systemPrompt: String { + get { UserDefaultsManager.shared.systemPrompt } + set { UserDefaultsManager.shared.systemPrompt = newValue } + } + + var selectedTemplateId: UUID? { + get { UserDefaultsManager.shared.selectedTemplateId } + set { UserDefaultsManager.shared.selectedTemplateId = newValue } + } + + var hasCompletedOnboarding: Bool { + get { UserDefaultsManager.shared.hasCompletedOnboarding } + set { UserDefaultsManager.shared.hasCompletedOnboarding = newValue } + } + + var hasAcceptedTerms: Bool { + get { UserDefaultsManager.shared.hasAcceptedTerms } + set { UserDefaultsManager.shared.hasAcceptedTerms = newValue } + } // System prompt default loading static func defaultSystemPrompt() -> String { @@ -35,17 +57,12 @@ struct Settings: Codable { return result } - init(openAIKey: String = "", - userBlurb: String = "", - systemPrompt: String = "", - selectedTemplateId: UUID? = nil, - hasCompletedOnboarding: Bool = false, - hasAcceptedTerms: Bool = false) { + init(openAIKey: String = "") { self.openAIKey = openAIKey - self.userBlurb = userBlurb - self.systemPrompt = systemPrompt.isEmpty ? Settings.defaultSystemPrompt() : systemPrompt - self.selectedTemplateId = selectedTemplateId - self.hasCompletedOnboarding = hasCompletedOnboarding - self.hasAcceptedTerms = hasAcceptedTerms + } + + // MARK: - Codable conformance for API key only + private enum CodingKeys: String, CodingKey { + case openAIKey } } \ No newline at end of file diff --git a/meetingnotes/Services/NotesGenerator.swift b/meetingnotes/Services/NotesGenerator.swift index eb866c5..9b40a2a 100644 --- a/meetingnotes/Services/NotesGenerator.swift +++ b/meetingnotes/Services/NotesGenerator.swift @@ -25,7 +25,7 @@ class NotesGenerator { return AsyncStream(String.self) { continuation in Task { do { - guard let apiKey = KeychainHelper.shared.get(forKey: "openAIKey"), !apiKey.isEmpty else { + guard let apiKey = KeychainHelper.shared.getAPIKey(), !apiKey.isEmpty else { continuation.finish() return } @@ -91,7 +91,7 @@ class NotesGenerator { /// Validates if OpenAI API key is configured /// - Returns: True if API key exists, false otherwise func isConfigured() -> Bool { - guard let key = KeychainHelper.shared.get(forKey: "openAIKey"), + guard let key = KeychainHelper.shared.getAPIKey(), !key.isEmpty else { return false } diff --git a/meetingnotes/ViewModels/MeetingViewModel.swift b/meetingnotes/ViewModels/MeetingViewModel.swift index f43ceff..cb7258a 100644 --- a/meetingnotes/ViewModels/MeetingViewModel.swift +++ b/meetingnotes/ViewModels/MeetingViewModel.swift @@ -180,8 +180,8 @@ class MeetingViewModel: ObservableObject { meeting.generatedNotes = "" // Load settings for generation - let userBlurb = KeychainHelper.shared.get(forKey: "userBlurb") ?? "" - let systemPrompt = KeychainHelper.shared.get(forKey: "systemPrompt") ?? Settings.defaultSystemPrompt() + let userBlurb = UserDefaultsManager.shared.userBlurb + let systemPrompt = UserDefaultsManager.shared.systemPrompt // Use streaming generation let stream = NotesGenerator.shared.generateNotesStream( diff --git a/meetingnotes/ViewModels/SettingsViewModel.swift b/meetingnotes/ViewModels/SettingsViewModel.swift index 47ea9bf..8acc239 100644 --- a/meetingnotes/ViewModels/SettingsViewModel.swift +++ b/meetingnotes/ViewModels/SettingsViewModel.swift @@ -8,23 +8,13 @@ class SettingsViewModel: ObservableObject { @Published var templates: [NoteTemplate] = [] init() { - loadSettings() loadTemplates() } - func loadSettings() { - settings.openAIKey = KeychainHelper.shared.get(forKey: "openAIKey") ?? "" - settings.userBlurb = KeychainHelper.shared.get(forKey: "userBlurb") ?? "" - settings.systemPrompt = KeychainHelper.shared.get(forKey: "systemPrompt") ?? Settings.defaultSystemPrompt() - - // Load onboarding status - settings.hasCompletedOnboarding = KeychainHelper.shared.get(forKey: "hasCompletedOnboarding") == "true" - settings.hasAcceptedTerms = KeychainHelper.shared.get(forKey: "hasAcceptedTerms") == "true" - - // Load selected template ID - if let templateIdString = KeychainHelper.shared.get(forKey: "selectedTemplateId"), - let templateId = UUID(uuidString: templateIdString) { - settings.selectedTemplateId = templateId + /// Loads the API key from keychain (only called when actually needed) + func loadAPIKey() { + if settings.openAIKey.isEmpty { + settings.openAIKey = KeychainHelper.shared.getAPIKey() ?? "" } } @@ -62,22 +52,12 @@ class SettingsViewModel: ObservableObject { return } - let openAISaved = KeychainHelper.shared.save(settings.openAIKey, forKey: "openAIKey") - let blurbSaved = KeychainHelper.shared.save(settings.userBlurb, forKey: "userBlurb") - let promptSaved = KeychainHelper.shared.save(settings.systemPrompt, forKey: "systemPrompt") - - // Save onboarding status - let onboardingSaved = KeychainHelper.shared.save(settings.hasCompletedOnboarding ? "true" : "false", forKey: "hasCompletedOnboarding") - let termsSaved = KeychainHelper.shared.save(settings.hasAcceptedTerms ? "true" : "false", forKey: "hasAcceptedTerms") - - // Save selected template ID - var templateIdSaved = true - if let templateId = settings.selectedTemplateId { - templateIdSaved = KeychainHelper.shared.save(templateId.uuidString, forKey: "selectedTemplateId") - } + // Only save API key to keychain - other values are automatically saved to UserDefaults + // via computed properties when they're modified + let openAISaved = KeychainHelper.shared.saveAPIKey(settings.openAIKey) if showMessage { - if openAISaved && blurbSaved && promptSaved && templateIdSaved && onboardingSaved && termsSaved { + if openAISaved { saveMessage = "Settings saved successfully!" } else { saveMessage = "Error saving settings" diff --git a/meetingnotes/Views/OnboardingView.swift b/meetingnotes/Views/OnboardingView.swift index ba07beb..e0d948a 100644 --- a/meetingnotes/Views/OnboardingView.swift +++ b/meetingnotes/Views/OnboardingView.swift @@ -152,6 +152,7 @@ struct OnboardingView: View { .onAppear { checkPermissions() // Load existing API key if available + settingsViewModel.loadAPIKey() apiKey = settingsViewModel.settings.openAIKey } } diff --git a/meetingnotes/Views/SettingsView.swift b/meetingnotes/Views/SettingsView.swift index ad690ed..de243ac 100644 --- a/meetingnotes/Views/SettingsView.swift +++ b/meetingnotes/Views/SettingsView.swift @@ -155,8 +155,8 @@ struct SettingsView: View { .navigationTitle("Settings") .frame(minWidth: 600, minHeight: 600) .onAppear { - viewModel.loadSettings() viewModel.loadTemplates() + viewModel.loadAPIKey() } .onDisappear { DispatchQueue.main.async {