payfrit-beacon-ios/PayfritBeacon/Services/SecureStorage.swift
Schwifty cfa78679be feat: complete rebuild of PayfritBeacon iOS from scratch
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>
2026-03-22 17:13:36 +00:00

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)
}
}