- Replace multiple boolean properties with single OfflineBookmarkSyncState enum - Add Use Case pattern for OfflineSyncManager with dependency injection - Simplify LocalBookmarksSyncView using state-driven UI with external bindings - Convert OfflineBookmarksViewModel to use @Observable instead of ObservableObject - Move credentials from Core Data to Keychain for better persistence - Implement comprehensive database migration for App Group containers - Add structured logging throughout sync operations and API calls Architecture improvements follow MVVM principles with clean separation of concerns.
86 lines
2.8 KiB
Swift
86 lines
2.8 KiB
Swift
import Foundation
|
|
import Security
|
|
|
|
class KeychainHelper {
|
|
static let shared = KeychainHelper()
|
|
private init() {}
|
|
|
|
private static let accessGroup = "8J69P655GN.de.ilyashallak.readeck"
|
|
|
|
@discardableResult
|
|
func saveToken(_ token: String) -> Bool {
|
|
saveString(token, forKey: "readeck_token")
|
|
}
|
|
|
|
func loadToken() -> String? {
|
|
loadString(forKey: "readeck_token")
|
|
}
|
|
|
|
@discardableResult
|
|
func saveEndpoint(_ endpoint: String) -> Bool {
|
|
saveString(endpoint, forKey: "readeck_endpoint")
|
|
}
|
|
|
|
func loadEndpoint() -> String? {
|
|
loadString(forKey: "readeck_endpoint")
|
|
}
|
|
|
|
@discardableResult
|
|
func saveUsername(_ username: String) -> Bool {
|
|
saveString(username, forKey: "readeck_username")
|
|
}
|
|
|
|
func loadUsername() -> String? {
|
|
loadString(forKey: "readeck_username")
|
|
}
|
|
|
|
@discardableResult
|
|
func savePassword(_ password: String) -> Bool {
|
|
saveString(password, forKey: "readeck_password")
|
|
}
|
|
|
|
func loadPassword() -> String? {
|
|
loadString(forKey: "readeck_password")
|
|
}
|
|
|
|
@discardableResult
|
|
func clearCredentials() -> Bool {
|
|
let tokenCleared = saveString("", forKey: "readeck_token")
|
|
let endpointCleared = saveString("", forKey: "readeck_endpoint")
|
|
let usernameCleared = saveString("", forKey: "readeck_username")
|
|
let passwordCleared = saveString("", forKey: "readeck_password")
|
|
return tokenCleared && endpointCleared && usernameCleared && passwordCleared
|
|
}
|
|
|
|
// MARK: - Private generic helpers
|
|
@discardableResult
|
|
private func saveString(_ value: String, forKey key: String) -> Bool {
|
|
guard let data = value.data(using: .utf8) else { return false }
|
|
let query: [String: Any] = [
|
|
kSecClass as String: kSecClassGenericPassword,
|
|
kSecAttrAccount as String: key,
|
|
kSecValueData as String: data,
|
|
kSecAttrAccessGroup as String: KeychainHelper.accessGroup
|
|
]
|
|
SecItemDelete(query as CFDictionary)
|
|
let status = SecItemAdd(query as CFDictionary, nil)
|
|
return status == errSecSuccess
|
|
}
|
|
|
|
private func loadString(forKey key: String) -> String? {
|
|
let query: [String: Any] = [
|
|
kSecClass as String: kSecClassGenericPassword,
|
|
kSecAttrAccount as String: key,
|
|
kSecAttrAccessGroup as String: KeychainHelper.accessGroup,
|
|
kSecReturnData as String: true,
|
|
kSecMatchLimit as String: kSecMatchLimitOne
|
|
]
|
|
var result: AnyObject?
|
|
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
if status == errSecSuccess, let data = result as? Data {
|
|
return String(data: data, encoding: .utf8)
|
|
}
|
|
return nil
|
|
}
|
|
}
|