feat(ios): W4 app wiring — DI graph, event fan-out, lifecycle, privacy shade
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
This commit is contained in:
47
ios/App/WebTerm/Wiring/PrivacyShade.swift
Normal file
47
ios/App/WebTerm/Wiring/PrivacyShade.swift
Normal file
@@ -0,0 +1,47 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user