T-iOS-15: AppEnvironment production DI (real Keychain/probe/transports), EventFanOut (engine.events → TerminalVM + GateVM + activity bridge), scenePhase lifecycle (.background→close, suspend→rebuild+reopen reclaims size), privacy shade on != .active, spike screen deleted, cold-start routing Walkthrough proxies: hosted live-server smoke over the production DI graph (probe→store→ attach→echo→close) + real-Keychain kSecAttrAccessible assertion (closes T-iOS-7 deferral) Deviation closed: TerminalViewModel.lastSentDims (valid-only) + alive-engine .active → notifyForegrounded(dims:) (orchestrator fix on behalf of T-iOS-11 owner) Env finding: repo under ~/Documents → TCC blocks sim-spawned node; SimServerHarness dual-mode (self-bootstrap / WEBTERM_SERVER_URL via simctl launchctl setenv) Verified: 214 pkg + 76 app + 10 integration tests green; verify agent 8/8 PASS
48 lines
1.7 KiB
Swift
48 lines
1.7 KiB
Swift
import SwiftUI
|
|
|
|
/// T-iOS-15 · Privacy shade (security-critical, plan §7 T-iOS-15).
|
|
///
|
|
/// iOS writes the app-switcher snapshot to DISK the moment the scene reaches
|
|
/// `.background` — but the switcher is usually ENTERED at `.inactive`. The
|
|
/// shade therefore covers whenever `scenePhase != .active` (the exact rule;
|
|
/// `.inactive`-only would let the `.background` snapshot capture terminal
|
|
/// content — API keys, tokens, source — into the on-disk switcher image).
|
|
/// Restored on `.active`.
|
|
///
|
|
/// Pure mapping split from the view so the rule is unit-testable for all
|
|
/// three phases (PrivacyShadeTests).
|
|
enum PrivacyShadePolicy {
|
|
static func isShadeVisible(for scenePhase: ScenePhase) -> Bool {
|
|
scenePhase != .active
|
|
}
|
|
}
|
|
|
|
/// Opaque cover rendered ABOVE the whole navigation tree (RootView ZStack) —
|
|
/// deliberately no animation: the snapshot moment must never race a fade.
|
|
///
|
|
/// Scope note (documented): sheets present in a separate presentation layer a
|
|
/// root overlay cannot cover — but no terminal bytes are ever rendered in a
|
|
/// sheet (pairing / plan-gate only), so the root-level shade covers every
|
|
/// surface that shows PTY content.
|
|
struct PrivacyShadeView: View {
|
|
var body: some View {
|
|
ZStack {
|
|
Color(.systemBackground)
|
|
.ignoresSafeArea()
|
|
VStack(spacing: ShadeMetrics.spacing) {
|
|
Image(systemName: "terminal.fill")
|
|
.font(.largeTitle)
|
|
.foregroundStyle(.secondary)
|
|
Text("WebTerm")
|
|
.font(.headline)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
.accessibilityHidden(true)
|
|
}
|
|
|
|
private enum ShadeMetrics {
|
|
static let spacing: CGFloat = 12
|
|
}
|
|
}
|