Files
mayday/Mayday/Services/KeychainService.swift
copilot-swe-agent[bot] 1eb21c71ce feat: add complete Mayday iOS Xcode project
- Swift 6, SwiftUI, MVVM + async/await architecture
- iOS 17.0 minimum deployment target
- Two targets: Mayday app + MaydayLiveActivity widget extension
- Models: UserResponse, TokenPair, AppNotification, SessionResponse, AlertAttributes
- Services: HTTPClient (actor), AuthService, KeychainService, NotificationsAPIService, PushNotificationService
- ViewModels: AuthViewModel, NotificationsViewModel, SettingsViewModel
- Views: Login/Register/VerifyEmail, NotificationsList/Detail, Settings/ChangePassword/Sessions
- APNs push notifications with UIApplicationDelegate
- ActivityKit Live Activities for Dynamic Island + Lock Screen
- Keychain (Security framework) token storage
- 30-second polling with pagination for notifications
- Xcode project file (project.pbxproj) with correct build phases for both targets

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-13 23:04:35 +00:00

75 lines
2.3 KiB
Swift

import Foundation
import Security
final class KeychainService: Sendable {
static let shared = KeychainService()
private let accessTokenKey = "mayday.access_token"
private let refreshTokenKey = "mayday.refresh_token"
private let expiresAtKey = "mayday.expires_at"
private init() {}
func saveTokens(_ tokens: TokenPair) throws {
try save(tokens.accessToken, forKey: accessTokenKey)
try save(tokens.refreshToken, forKey: refreshTokenKey)
let expiresAtString = ISO8601DateFormatter().string(from: tokens.expiresAt)
try save(expiresAtString, forKey: expiresAtKey)
}
func loadAccessToken() -> String? {
load(forKey: accessTokenKey)
}
func loadRefreshToken() -> String? {
load(forKey: refreshTokenKey)
}
func clearTokens() {
delete(forKey: accessTokenKey)
delete(forKey: refreshTokenKey)
delete(forKey: expiresAtKey)
}
private func save(_ value: String, forKey key: String) throws {
let data = Data(value.utf8)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data
]
SecItemDelete(query as CFDictionary)
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else {
throw KeychainError.saveFailed(status)
}
}
private func load(forKey key: String) -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
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,
let string = String(data: data, encoding: .utf8) else { return nil }
return string
}
private func delete(forKey key: String) {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key
]
SecItemDelete(query as CFDictionary)
}
}
enum KeychainError: Error {
case saveFailed(OSStatus)
}