pi-remote-ios/Sources/App/AppState.swift

79 lines
2.5 KiB
Swift

// AppState.swift global app state, credential lifecycle
import Combine
import SwiftUI
@MainActor
final class AppState: ObservableObject {
static let shared = AppState()
@Published var credential: SidecarCredential? = nil
@Published var isLocked = false
@Published var lastForegroundedAt: Date = Date()
// T-2.10: background/foreground lifecycle publisher
// true = app entered background
// false = app returned to foreground
private let _lifecycleTransitions = PassthroughSubject<Bool, Never>()
var lifecycleTransitions: AnyPublisher<Bool, Never> {
_lifecycleTransitions.eraseToAnyPublisher()
}
private init() {
// Test-only overrides
let args = ProcessInfo.processInfo.arguments
if args.contains("--reset-state") {
Keychain.shared.delete(key: "piremote.credential")
UserDefaults.standard.removeObject(forKey: "faceid.enabled")
}
if args.contains("--enable-faceid") {
UserDefaults.standard.set(true, forKey: "faceid.enabled")
}
if args.contains("--force-lock") {
isLocked = true
}
// Try loading persisted credential on launch
credential = try? Keychain.shared.load(key: "piremote.credential")
// T-2.10: inject stub credential for UI tests that need MainTerminalView
if args.contains("--uitest-with-stub-connection") {
credential = SidecarCredential(
sidecarId: "stub",
host: "127.0.0.1",
port: 19991,
bearerToken: "stub-token",
tlsFingerprint: "stub",
sidecarName: "stub",
pairedAt: Date()
)
}
}
func didPair(credential: SidecarCredential) {
self.credential = credential
try? Keychain.shared.save(credential, key: "piremote.credential")
}
func unpair() {
credential = nil
Keychain.shared.delete(key: "piremote.credential")
}
// MARK: - Face ID gate
func appDidBackground() {
lastForegroundedAt = Date()
_lifecycleTransitions.send(true) // T-2.10
}
func appWillForeground() async {
_lifecycleTransitions.send(false) // T-2.10: always emit before Face-ID gate
let elapsed = Date().timeIntervalSince(lastForegroundedAt)
guard elapsed > 60 else { return } // within 60s no re-auth
isLocked = true
let ok = await FaceIDGate.authenticate()
isLocked = !ok
}
}