Files
tianma-if cd4f010d8f feat(ios): add native SwiftUI client with Android UI parity
Replace the abandoned RN iOS path with a SwiftUI app (iOS 17+) that reuses
the shared TipTap editor bundle, GRDB local mirror/outbox, and Android-aligned
shell chrome for list, create, detail, and Me. Package the editor HTML in the
app resources so Markdown renders, present edit from the workspace root with a
UIKit FAB, and keep caret stable while typing.
2026-08-07 02:52:01 +08:00

75 lines
2.9 KiB
Swift

import Foundation
import Security
/// Keychain-backed storage with UserDefaults fallback when Keychain is unavailable
/// (unsigned simulator installs, missing entitlements, etc.).
enum KeychainStore {
static let service = "org.edgeever.mobile"
enum Key {
static let session = "edgeever.mobile.session"
static let deviceId = "edgeever.mobile.device-id"
}
private static let fallbackPrefix = "edgeever.keychain.fallback."
static func setString(_ value: String, forKey key: String) throws {
let data = Data(value.utf8)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
]
SecItemDelete(query as CFDictionary)
var add = query
add[kSecValueData as String] = data
add[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
let status = SecItemAdd(add as CFDictionary, nil)
if status == errSecSuccess {
UserDefaults.standard.removeObject(forKey: fallbackPrefix + key)
return
}
// Fallback for environments where Keychain is restricted.
UserDefaults.standard.set(value, forKey: fallbackPrefix + key)
if status != errSecDuplicateItem && status != errSecInteractionNotAllowed && status != errSecMissingEntitlement {
// Still record unusual codes but do not fail the app bootstrap path.
#if DEBUG
print("KeychainStore.setString fallback status=\(status) key=\(key)")
#endif
}
}
static func string(forKey key: String) throws -> 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 item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
if status == errSecSuccess, let data = item as? Data, let value = String(data: data, encoding: .utf8) {
return value
}
if status == errSecItemNotFound || status != errSecSuccess {
return UserDefaults.standard.string(forKey: fallbackPrefix + key)
}
return UserDefaults.standard.string(forKey: fallbackPrefix + key)
}
static func delete(forKey key: String) throws {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
]
SecItemDelete(query as CFDictionary)
UserDefaults.standard.removeObject(forKey: fallbackPrefix + key)
}
}
enum KeychainError: Error {
case unhandled(OSStatus)
}