fix: streamline keychain usage (#7)

This commit is contained in:
Owen Gretzinger
2025-07-14 17:44:25 -04:00
committed by GitHub
parent c60c6cfac5
commit 892ecd3201
10 changed files with 126 additions and 59 deletions
-8
View File
@@ -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()
}
}
}
+1 -1
View File
@@ -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
}
@@ -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
@@ -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)
}
}
}
}
+34 -17
View File
@@ -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
}
}
+2 -2
View File
@@ -25,7 +25,7 @@ class NotesGenerator {
return AsyncStream<String>(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
}
@@ -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(
@@ -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"
+1
View File
@@ -152,6 +152,7 @@ struct OnboardingView: View {
.onAppear {
checkPermissions()
// Load existing API key if available
settingsViewModel.loadAPIKey()
apiKey = settingsViewModel.settings.openAIKey
}
}
+1 -1
View File
@@ -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 {