Files
web-terminal/ios/App/WebTerm/Wiring/RootView.swift
Yaojia Wang 07bcbf0c08 feat(ios): device enrollment flow + silent cert rotation (B3)
Wire the SecureEnclave enroll library into a real flow (login->bearer->CSR->
/device/enroll->keychain identity), presented on the existing mTLS path; add a
rotation scheduler. Atomic keychain replace (add-before-delete); renew body is
{csr}-only; renewal-failing surfaced in the UI. ClientTLS 48 tests pass.
2026-07-18 13:32:05 +02:00

225 lines
9.6 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import HostRegistry
import SwiftUI
/// T-iOS-15 / T-iPad-2 · App `NavigationStack` +
/// + sheets/scenePhase/deepLink 线iPad T-iPad-2
///
/// - `RootView` struct `@main` WebTermApp
/// `RootView(coordinator:)` `AdaptiveRootView`
/// accent tint`DS.Palette.accent`DS
/// App //`.borderedProminent`
/// - `AdaptiveRootView` `horizontalSizeClass` `StackRootView`compact
/// `SplitRootView`regulariPad
/// / scenePhase / deepLink / sheets **线**
/// ZStack
///
/// `StackRootView` `RootView` body **** compact
/// iPhone
/// `DS.*` token //
struct RootView: View {
@Bindable var coordinator: AppCoordinator
var body: some View {
AdaptiveRootView(coordinator: coordinator)
// DS tint Tokens.swift
// gate amber orange `.tint`
.tint(DS.Palette.accent)
// web DEFAULT_SETTINGS.theme = 'dark'
// --bg #100F0D
// accent/status
.preferredColorScheme(.dark)
}
}
/// T-iPad-2 · compact iPhone / iPad Slide Over
/// `RootView` `NavigationStack` 线
/// /scenePhase/deepLink/sheets `AdaptiveRootView`
struct StackRootView: View {
@Bindable var coordinator: AppCoordinator
@Environment(\.accessibilityReduceMotion) private var reduceMotion
var body: some View {
NavigationStack {
rootContent
.navigationDestination(isPresented: terminalBinding) {
terminalDestination
}
}
}
// MARK: - Route switch
@ViewBuilder private var rootContent: some View {
switch coordinator.route {
case .loading:
ProgressView()
case .pairing:
firstRunPairing
case .sessions:
sessionList
}
}
@ViewBuilder private var firstRunPairing: some View {
if let viewModel = coordinator.rootPairingViewModel {
PairingScreen(viewModel: viewModel) { host in
coordinator.completeFirstPairing(host)
}
} else {
ProgressView()
}
}
private var sessionList: some View {
SessionListScreen(
viewModel: coordinator.sessionList,
onOpen: { coordinator.open($0) },
onAddHost: { coordinator.presentAddHost() },
onDeviceCert: { coordinator.presentDeviceCert() },
onEnroll: { coordinator.presentEnrollment() }
)
.safeAreaInset(edge: .bottom) { continueLastBanner }
// B3 (HIGH) · A silently-failing device-cert renewal is surfaced here
// (top inset), so it is observable instead of buried in os.Logger.
.safeAreaInset(edge: .top) { certRenewalWarningBanner }
// / DS reduceMotion
.animation(
DS.Motion.gated(DS.Motion.base, reduceMotion: reduceMotion),
value: coordinator.continueLastSessionId
)
// T-iOS-26 · Projects RootView SessionListScreen
// T-iOS-23 W7 leading
// topBarTrailing hostMenu
.toolbar { ProjectsToolbarItem(coordinator: coordinator) }
}
// MARK: - "" highlight (cold start step 5)
@ViewBuilder private var continueLastBanner: some View {
if coordinator.continueLastSessionId != nil {
ContinueLastBanner { coordinator.openContinueLast() }
.transition(.move(edge: .bottom).combined(with: .opacity))
}
}
// MARK: - Device-cert renewal warning (B3 HIGH observability fix)
@ViewBuilder private var certRenewalWarningBanner: some View {
if coordinator.isCertificateRenewalFailing {
CertRenewalWarningBanner()
.transition(.move(edge: .top).combined(with: .opacity))
}
}
// MARK: - Terminal push
private var terminalBinding: Binding<Bool> {
Binding(
get: { coordinator.terminalController != nil },
set: { presented in
guard !presented else { return }
coordinator.closeTerminal() // back engine.close() (detach)
}
)
}
@ViewBuilder private var terminalDestination: some View {
if let controller = coordinator.terminalController {
TerminalContainerView(
controller: controller,
onNewSessionInCwd: { coordinator.openNewSessionInCurrentCwd() },
onKillSession: { coordinator.killCurrentSession() } // T-iPad-3
)
// T-iOS-29 · identity PER CONTROLLER: an in-place session switch
// (new-in-cwd, deep link) swaps the controller while the
// destination stays presented without a new identity SwiftUI
// keeps the old SwiftTerm UIView and the new ViewModel's output
// sink never attaches (makeUIView never re-runs). Also resets the
// container's per-session @State (plan-gate dismissal, timeline).
.id(controller.id)
}
}
}
// MARK: - Continue-last banner (shared stack + split, DRY)
/// 5 CTA stack split sidebar
/// DRY`.regularMaterial` + +
/// accent `DSButtonStyle(.primary)`
/// `action`= `coordinator.openContinueLast()`
struct ContinueLastBanner: View {
let action: () -> Void
var body: some View {
Button(action: action) {
Label(RootCopy.continueLast, systemImage: "arrow.uturn.forward.circle.fill")
}
.buttonStyle(DSButtonStyle(kind: .primary))
.accessibilityIdentifier("root.continueLastButton")
.padding(.horizontal, DS.Space.lg16)
.padding(.top, DS.Space.md12)
.padding(.bottom, DS.Space.sm8)
.background(.regularMaterial)
.overlay(alignment: .top) {
Rectangle()
.fill(DS.Palette.hairline)
.frame(height: DS.Stroke.hairline)
}
}
}
// MARK: - Device-cert renewal warning (B3 HIGH observability fix)
/// A quiet, non-blocking warning that the silent device-certificate renewal is
/// failing surfaced from `AppCoordinator.isCertificateRenewalFailing` so a
/// failing renewal is observable in the UI instead of only in os.Logger. Amber
/// (the `waiting`/needs-me status color) + an SF Symbol so meaning is never
/// carried by color alone. Non-interactive: the existing cert stays valid until
/// expiry, and the next foreground retries automatically.
struct CertRenewalWarningBanner: View {
var body: some View {
Label(RootCopy.certRenewalFailing, systemImage: "exclamationmark.shield")
.font(DS.Typography.caption)
.foregroundStyle(DS.Palette.statusWaiting)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, DS.Space.lg16)
.padding(.vertical, DS.Space.sm8)
.background(.regularMaterial)
.overlay(alignment: .bottom) {
Rectangle()
.fill(DS.Palette.hairline)
.frame(height: DS.Stroke.hairline)
}
.accessibilityIdentifier("root.certRenewalWarning")
}
}
// MARK: - Projects toolbar item (shared stack + split, DRY)
/// leading stack split disabled
/// `presentProjects` a11y id tint label accent
struct ProjectsToolbarItem: ToolbarContent {
@Bindable var coordinator: AppCoordinator
var body: some ToolbarContent {
ToolbarItem(placement: .topBarLeading) {
Button {
coordinator.presentProjects()
} label: {
Label(RootCopy.projects, systemImage: "folder")
}
.disabled(coordinator.sessionList.activeHost == nil)
.accessibilityIdentifier("sessions.projectsButton")
}
}
}
/// internal `SplitRootView` /DRY
enum RootCopy {
static let continueLast = "继续上次会话"
static let projects = "项目"
static let done = "完成"
/// B3 (HIGH) · Shown when the silent device-cert renewal keeps failing.
static let certRenewalFailing = "设备证书自动续期失败,将在下次前台重试"
}