- iOS: Add KeychainHelper.swift for secure token storage - iOS: Add AuthService.swift (CMAuthService) with login/register/refresh/logout - iOS: Add LoginView.swift (CMLoginView) with ChronoMind theme - iOS: Wire auth gate in ChronoMindApp.swift (LoginView vs ContentView) - iOS: Add Account section to SettingsView with email/plan/sign-out - iOS: Add Cloud group + 3 files to Xcode project.pbxproj - Android: Add AuthService.kt with Hilt @Singleton, login/register/refresh/logout - Android: Add LoginScreen.kt with Compose login/register form - Android: Wire auth gate in MainActivity via Hilt-injected AuthService - Android: Add Account section to SettingsScreen via HiltViewModel - Android: Add x-product-id header to PlatformApiClient
53 lines
1.9 KiB
Swift
53 lines
1.9 KiB
Swift
// ── Keychain Helper ───────────────────────────────────────────
|
|
// Lightweight wrapper for storing auth tokens securely in iOS Keychain.
|
|
|
|
import Foundation
|
|
import Security
|
|
|
|
enum KeychainHelper {
|
|
|
|
private static let service = "com.saravana.chronomind"
|
|
|
|
@discardableResult
|
|
static func save(key: String, value: String) -> Bool {
|
|
guard let data = value.data(using: .utf8) else { return false }
|
|
delete(key: key)
|
|
|
|
let query: [String: Any] = [
|
|
kSecClass as String: kSecClassGenericPassword,
|
|
kSecAttrService as String: service,
|
|
kSecAttrAccount as String: key,
|
|
kSecValueData as String: data,
|
|
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock,
|
|
]
|
|
|
|
return SecItemAdd(query as CFDictionary, nil) == errSecSuccess
|
|
}
|
|
|
|
static func read(key: String) -> String? {
|
|
let query: [String: Any] = [
|
|
kSecClass as String: kSecClassGenericPassword,
|
|
kSecAttrService as String: service,
|
|
kSecAttrAccount as String: key,
|
|
kSecReturnData as String: true,
|
|
kSecMatchLimit as String: kSecMatchLimitOne,
|
|
]
|
|
|
|
var result: AnyObject?
|
|
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
guard status == errSecSuccess, let data = result as? Data else { return nil }
|
|
return String(data: data, encoding: .utf8)
|
|
}
|
|
|
|
@discardableResult
|
|
static func delete(key: String) -> Bool {
|
|
let query: [String: Any] = [
|
|
kSecClass as String: kSecClassGenericPassword,
|
|
kSecAttrService as String: service,
|
|
kSecAttrAccount as String: key,
|
|
]
|
|
let status = SecItemDelete(query as CFDictionary)
|
|
return status == errSecSuccess || status == errSecItemNotFound
|
|
}
|
|
}
|