100% fresh codebase — no legacy code carried over. Built against the Android beacon app as the behavioral spec. Architecture: - App: SwiftUI @main, AppState-driven navigation, Keychain storage - Views: LoginView (OTP + biometric), BusinessListView, ScanView (provisioning hub) - Models: Business, ServicePoint, BeaconConfig, BeaconType, DiscoveredBeacon - Services: APIClient (actor, async/await), BLEManager (CoreBluetooth scanner) - Provisioners: KBeacon, DXSmart (2-step auth + flashing), BlueCharm - Utils: UUIDFormatting, BeaconBanList, BeaconShardPool (64 shards) Matches Android feature parity: - 4-screen flow: Login → Business Select → Scan/Provision - 3 beacon types with correct GATT protocols and timeouts - Namespace allocation via beacon-sharding API - Smart service point naming (Table N auto-increment) - DXSmart special flow (connect → flash → user confirms → write) - Biometric auth, dev/prod build configs, DEV banner overlay Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
68 lines
2.2 KiB
Swift
68 lines
2.2 KiB
Swift
import Foundation
|
|
import Security
|
|
|
|
/// Keychain-backed secure storage for auth tokens
|
|
enum SecureStorage {
|
|
|
|
private static let service = "com.payfrit.beacon"
|
|
|
|
struct Session {
|
|
let token: String
|
|
let userId: String
|
|
}
|
|
|
|
static func saveSession(token: String, userId: String) {
|
|
save(key: "authToken", value: token)
|
|
save(key: "userId", value: userId)
|
|
}
|
|
|
|
static func loadSession() -> Session? {
|
|
guard let token = load(key: "authToken"),
|
|
let userId = load(key: "userId") else { return nil }
|
|
return Session(token: token, userId: userId)
|
|
}
|
|
|
|
static func clearSession() {
|
|
delete(key: "authToken")
|
|
delete(key: "userId")
|
|
}
|
|
|
|
// MARK: - Keychain Helpers
|
|
|
|
private static func save(key: String, value: String) {
|
|
guard let data = value.data(using: .utf8) else { return }
|
|
delete(key: key) // Remove old value first
|
|
|
|
let query: [String: Any] = [
|
|
kSecClass as String: kSecClassGenericPassword,
|
|
kSecAttrService as String: service,
|
|
kSecAttrAccount as String: key,
|
|
kSecValueData as String: data,
|
|
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
|
|
]
|
|
SecItemAdd(query as CFDictionary, nil)
|
|
}
|
|
|
|
private static func load(key: String) -> 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 result: AnyObject?
|
|
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
guard status == errSecSuccess, let data = result as? Data else { return nil }
|
|
return String(data: data, encoding: .utf8)
|
|
}
|
|
|
|
private static func delete(key: String) {
|
|
let query: [String: Any] = [
|
|
kSecClass as String: kSecClassGenericPassword,
|
|
kSecAttrService as String: service,
|
|
kSecAttrAccount as String: key
|
|
]
|
|
SecItemDelete(query as CFDictionary)
|
|
}
|
|
}
|