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>
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user