fix: streamline keychain usage (#7)
This commit is contained in:
@@ -19,14 +19,6 @@ struct ContentView: View {
|
|||||||
MeetingListView(settingsViewModel: settingsViewModel)
|
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -294,7 +294,7 @@ class AudioManager: NSObject, ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func connectToOpenAIRealtime(source: AudioSource) {
|
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")
|
print("❌ No OpenAI key found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,19 @@ class KeychainHelper {
|
|||||||
|
|
||||||
private init() {}
|
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
|
/// Saves a string value to the keychain
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - value: The string value to save
|
/// - 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,34 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct Settings: Codable {
|
struct Settings: Codable {
|
||||||
var openAIKey: String
|
// Only store API key in memory - will be loaded from keychain when needed
|
||||||
var userBlurb: String
|
var openAIKey: String = ""
|
||||||
var systemPrompt: String
|
|
||||||
var selectedTemplateId: UUID?
|
// Computed properties that access UserDefaults
|
||||||
var hasCompletedOnboarding: Bool
|
var userBlurb: String {
|
||||||
var hasAcceptedTerms: Bool
|
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
|
// System prompt default loading
|
||||||
static func defaultSystemPrompt() -> String {
|
static func defaultSystemPrompt() -> String {
|
||||||
@@ -35,17 +57,12 @@ struct Settings: Codable {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
init(openAIKey: String = "",
|
init(openAIKey: String = "") {
|
||||||
userBlurb: String = "",
|
|
||||||
systemPrompt: String = "",
|
|
||||||
selectedTemplateId: UUID? = nil,
|
|
||||||
hasCompletedOnboarding: Bool = false,
|
|
||||||
hasAcceptedTerms: Bool = false) {
|
|
||||||
self.openAIKey = openAIKey
|
self.openAIKey = openAIKey
|
||||||
self.userBlurb = userBlurb
|
}
|
||||||
self.systemPrompt = systemPrompt.isEmpty ? Settings.defaultSystemPrompt() : systemPrompt
|
|
||||||
self.selectedTemplateId = selectedTemplateId
|
// MARK: - Codable conformance for API key only
|
||||||
self.hasCompletedOnboarding = hasCompletedOnboarding
|
private enum CodingKeys: String, CodingKey {
|
||||||
self.hasAcceptedTerms = hasAcceptedTerms
|
case openAIKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ class NotesGenerator {
|
|||||||
return AsyncStream<String>(String.self) { continuation in
|
return AsyncStream<String>(String.self) { continuation in
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
guard let apiKey = KeychainHelper.shared.get(forKey: "openAIKey"), !apiKey.isEmpty else {
|
guard let apiKey = KeychainHelper.shared.getAPIKey(), !apiKey.isEmpty else {
|
||||||
continuation.finish()
|
continuation.finish()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -91,7 +91,7 @@ class NotesGenerator {
|
|||||||
/// Validates if OpenAI API key is configured
|
/// Validates if OpenAI API key is configured
|
||||||
/// - Returns: True if API key exists, false otherwise
|
/// - Returns: True if API key exists, false otherwise
|
||||||
func isConfigured() -> Bool {
|
func isConfigured() -> Bool {
|
||||||
guard let key = KeychainHelper.shared.get(forKey: "openAIKey"),
|
guard let key = KeychainHelper.shared.getAPIKey(),
|
||||||
!key.isEmpty else {
|
!key.isEmpty else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,8 +180,8 @@ class MeetingViewModel: ObservableObject {
|
|||||||
meeting.generatedNotes = ""
|
meeting.generatedNotes = ""
|
||||||
|
|
||||||
// Load settings for generation
|
// Load settings for generation
|
||||||
let userBlurb = KeychainHelper.shared.get(forKey: "userBlurb") ?? ""
|
let userBlurb = UserDefaultsManager.shared.userBlurb
|
||||||
let systemPrompt = KeychainHelper.shared.get(forKey: "systemPrompt") ?? Settings.defaultSystemPrompt()
|
let systemPrompt = UserDefaultsManager.shared.systemPrompt
|
||||||
|
|
||||||
// Use streaming generation
|
// Use streaming generation
|
||||||
let stream = NotesGenerator.shared.generateNotesStream(
|
let stream = NotesGenerator.shared.generateNotesStream(
|
||||||
|
|||||||
@@ -8,23 +8,13 @@ class SettingsViewModel: ObservableObject {
|
|||||||
@Published var templates: [NoteTemplate] = []
|
@Published var templates: [NoteTemplate] = []
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
loadSettings()
|
|
||||||
loadTemplates()
|
loadTemplates()
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadSettings() {
|
/// Loads the API key from keychain (only called when actually needed)
|
||||||
settings.openAIKey = KeychainHelper.shared.get(forKey: "openAIKey") ?? ""
|
func loadAPIKey() {
|
||||||
settings.userBlurb = KeychainHelper.shared.get(forKey: "userBlurb") ?? ""
|
if settings.openAIKey.isEmpty {
|
||||||
settings.systemPrompt = KeychainHelper.shared.get(forKey: "systemPrompt") ?? Settings.defaultSystemPrompt()
|
settings.openAIKey = KeychainHelper.shared.getAPIKey() ?? ""
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,22 +52,12 @@ class SettingsViewModel: ObservableObject {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let openAISaved = KeychainHelper.shared.save(settings.openAIKey, forKey: "openAIKey")
|
// Only save API key to keychain - other values are automatically saved to UserDefaults
|
||||||
let blurbSaved = KeychainHelper.shared.save(settings.userBlurb, forKey: "userBlurb")
|
// via computed properties when they're modified
|
||||||
let promptSaved = KeychainHelper.shared.save(settings.systemPrompt, forKey: "systemPrompt")
|
let openAISaved = KeychainHelper.shared.saveAPIKey(settings.openAIKey)
|
||||||
|
|
||||||
// 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")
|
|
||||||
}
|
|
||||||
|
|
||||||
if showMessage {
|
if showMessage {
|
||||||
if openAISaved && blurbSaved && promptSaved && templateIdSaved && onboardingSaved && termsSaved {
|
if openAISaved {
|
||||||
saveMessage = "Settings saved successfully!"
|
saveMessage = "Settings saved successfully!"
|
||||||
} else {
|
} else {
|
||||||
saveMessage = "Error saving settings"
|
saveMessage = "Error saving settings"
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ struct OnboardingView: View {
|
|||||||
.onAppear {
|
.onAppear {
|
||||||
checkPermissions()
|
checkPermissions()
|
||||||
// Load existing API key if available
|
// Load existing API key if available
|
||||||
|
settingsViewModel.loadAPIKey()
|
||||||
apiKey = settingsViewModel.settings.openAIKey
|
apiKey = settingsViewModel.settings.openAIKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,8 +155,8 @@ struct SettingsView: View {
|
|||||||
.navigationTitle("Settings")
|
.navigationTitle("Settings")
|
||||||
.frame(minWidth: 600, minHeight: 600)
|
.frame(minWidth: 600, minHeight: 600)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.loadSettings()
|
|
||||||
viewModel.loadTemplates()
|
viewModel.loadTemplates()
|
||||||
|
viewModel.loadAPIKey()
|
||||||
}
|
}
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
|||||||
Reference in New Issue
Block a user