Unit tests, Lokalisierung, ShareExtension
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
import Foundation
|
||||
import Security
|
||||
|
||||
/// Shared Keychain service for passing credentials between the main app
|
||||
/// and the Share Extension via App Group "group.de.hanold.bookstax".
|
||||
///
|
||||
/// - The main app calls `saveCredentials` whenever a profile is activated.
|
||||
/// - The extension calls `loadCredentials` to authenticate API requests.
|
||||
/// - Accessibility is `afterFirstUnlock` so the extension can run while the
|
||||
/// device is locked after the user has unlocked it at least once.
|
||||
///
|
||||
/// Add this file to **both** the main app target and `BookStaxShareExtension`.
|
||||
enum ShareExtensionKeychainService {
|
||||
|
||||
private static let service = "de.hanold.bookstax.shared"
|
||||
private static let account = "activeCredentials"
|
||||
private static let accessGroup = "group.de.hanold.bookstax"
|
||||
|
||||
private struct Credentials: Codable {
|
||||
let serverURL: String
|
||||
let tokenId: String
|
||||
let tokenSecret: String
|
||||
}
|
||||
|
||||
// MARK: - Save (called from main app)
|
||||
|
||||
/// Persists the active profile credentials in the shared keychain.
|
||||
static func saveCredentials(serverURL: String, tokenId: String, tokenSecret: String) {
|
||||
guard let data = try? JSONEncoder().encode(
|
||||
Credentials(serverURL: serverURL, tokenId: tokenId, tokenSecret: tokenSecret)
|
||||
) else { return }
|
||||
|
||||
let baseQuery: [CFString: Any] = [
|
||||
kSecClass: kSecClassGenericPassword,
|
||||
kSecAttrService: service,
|
||||
kSecAttrAccount: account,
|
||||
kSecAttrAccessGroup: accessGroup
|
||||
]
|
||||
|
||||
// Try update first; add if not found.
|
||||
let updateStatus = SecItemUpdate(
|
||||
baseQuery as CFDictionary,
|
||||
[kSecValueData: data] as CFDictionary
|
||||
)
|
||||
if updateStatus == errSecItemNotFound {
|
||||
var addQuery = baseQuery
|
||||
addQuery[kSecValueData] = data
|
||||
addQuery[kSecAttrAccessible] = kSecAttrAccessibleAfterFirstUnlock
|
||||
SecItemAdd(addQuery as CFDictionary, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Load (called from Share Extension)
|
||||
|
||||
/// Returns the stored credentials, or `nil` if the user has not yet
|
||||
/// configured the main app.
|
||||
static func loadCredentials() -> (serverURL: String, tokenId: String, tokenSecret: String)? {
|
||||
let query: [CFString: Any] = [
|
||||
kSecClass: kSecClassGenericPassword,
|
||||
kSecAttrService: service,
|
||||
kSecAttrAccount: account,
|
||||
kSecAttrAccessGroup: accessGroup,
|
||||
kSecReturnData: true,
|
||||
kSecMatchLimit: kSecMatchLimitOne
|
||||
]
|
||||
var result: AnyObject?
|
||||
guard SecItemCopyMatching(query as CFDictionary, &result) == errSecSuccess,
|
||||
let data = result as? Data,
|
||||
let creds = try? JSONDecoder().decode(Credentials.self, from: data)
|
||||
else { return nil }
|
||||
return (creds.serverURL, creds.tokenId, creds.tokenSecret)
|
||||
}
|
||||
|
||||
// MARK: - Clear
|
||||
|
||||
/// Removes the shared credentials (e.g., on logout).
|
||||
static func clearCredentials() {
|
||||
let query: [CFString: Any] = [
|
||||
kSecClass: kSecClassGenericPassword,
|
||||
kSecAttrService: service,
|
||||
kSecAttrAccount: account,
|
||||
kSecAttrAccessGroup: accessGroup
|
||||
]
|
||||
SecItemDelete(query as CFDictionary)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user