Files
web-terminal/ios/App/WebTerm/Wiring/PrivacyShade.swift
Yaojia Wang cc4d3129cc 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
2026-07-05 01:04:42 +02:00

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