Files
web-terminal/ios/App/WebTerm/Wiring/AdaptiveRootView.swift
Yaojia Wang e38e6d1689 feat(ios): device client-cert mTLS — ClientTLS package, transport wiring, install UX (C-iOS)
- ios/Packages/ClientTLS: SecIdentity wrapper, PKCS12 importer (typed errors), keychain store
  (AfterFirstUnlockThisDeviceOnly), pure MutualTLSChallengeResponder truth table, cross-platform
  X.509 DER summary. 14/14 tests.
- Both transports (SessionCore URLSessionTermTransport, App URLSessionHTTPTransport) + SessionThumbnail
  take a lazy @Sendable ()->ClientIdentity? provider: WS resolves per-connect, HTTP per client-cert
  challenge, so a freshly-imported cert applies without an app relaunch. AppEnvironment injects
  { store.loadedIdentityOrNil() }.
- ClientCertScreen (.fileImporter([.pkcs12]) + passphrase -> import -> keychain), reachable via a
  设备证书 entry in SessionListScreen.hostMenu. PairingViewModel gates tunnel-host probes on cert
  presence and re-maps mTLS-reject to a clientCertRejected message.
Verified: ClientTLS 14/14, SessionCore 93/93, xcodegen + xcodebuild BUILD SUCCEEDED.
2026-07-07 09:42:12 +02:00

130 lines
5.3 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 SwiftUI
/// T-iPad-2 · size-class `LayoutPolicy.mode` compact
/// iPhone / iPad Slide Over `StackRootView`
/// regular iPad / / Stage Manager `SplitRootView`
///
///
/// **线**PLAN_IOS_IPAD §5 T-iPad-2
/// - ZStack **** split detail
/// `scenePhase != .active` `PrivacyShadePolicy`
/// split detail stack push
/// - `.task` bootstrap / `.onChange(scenePhase)` / `.onOpenURL` / deep-link
/// alert / add-host sheet / Projects sheet `RootView`
/// compact
///
/// size class ****iPad Slide Over regularcompact
/// UI `terminalController`
/// `AppCoordinator` T-iOS-29 `.id`
///
struct AdaptiveRootView: View {
@Bindable var coordinator: AppCoordinator
@Environment(\.scenePhase) private var scenePhase
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
var body: some View {
ZStack {
layoutBranch
if PrivacyShadePolicy.isShadeVisible(for: scenePhase) {
PrivacyShadeView()
}
}
.task { await coordinator.bootstrap() }
.onChange(of: scenePhase) { _, phase in
coordinator.handleScenePhase(phase)
}
.onOpenURL { coordinator.handleDeepLink(url: $0) } // T-iOS-22
.alert(DeepLinkCopy.hintTitle, isPresented: deepLinkHintBinding) {
Button(DeepLinkCopy.hintConfirm) { coordinator.deepLink.clearHint() }
} message: {
Text(coordinator.deepLink.hintMessage ?? "")
}
.sheet(
isPresented: $coordinator.isAddHostPresented,
onDismiss: { coordinator.addHostDismissed() }
) {
addHostSheet
}
.sheet(
isPresented: $coordinator.isProjectsPresented,
onDismiss: { coordinator.projectsDismissed() }
) {
projectsSheet
}
.sheet(isPresented: $coordinator.isDeviceCertPresented) {
deviceCertSheet
}
}
// MARK: - Layout branch (the SOLE size-class consumer)
@ViewBuilder private var layoutBranch: some View {
// Split only makes sense once we're in the session list. `.loading` and
// `.pairing` (genuine iPad first-run, no paired host) get the full-screen
// stack flow regardless of size class a split sidebar has nothing to
// list yet and would strand the user on the not-paired empty state
// (T-iPad-5 finding). Route-gate the split branch.
switch LayoutPolicy.mode(horizontalSizeClass: horizontalSizeClass) {
case .split where coordinator.route == .sessions:
SplitRootView(coordinator: coordinator)
default:
StackRootView(coordinator: coordinator)
}
}
// MARK: - Deep-link hint alert (T-iOS-22)
/// unknown host / store failure `RootView`
private var deepLinkHintBinding: Binding<Bool> {
Binding(
get: { coordinator.deepLink.hintMessage != nil },
set: { presented in
guard !presented else { return }
coordinator.deepLink.clearHint()
}
)
}
// MARK: - Add-host sheet (multi-host entry, list header)
@ViewBuilder private var addHostSheet: some View {
if let viewModel = coordinator.addHostPairingViewModel {
NavigationStack {
PairingScreen(viewModel: viewModel) { host in
coordinator.completeAddHost(host)
}
}
}
}
// MARK: - Projects sheet (T-iOS-26)
/// NavigationStack sheet push
/// "" sheet
@ViewBuilder private var projectsSheet: some View {
if let viewModel = coordinator.projectsViewModel {
NavigationStack {
ProjectsScreen(viewModel: viewModel) { request in
coordinator.openProject(request)
}
}
}
}
// MARK: - Device certificate sheet (C-iOS-3, HIGH reachability fix)
/// NavigationStack`ClientCertScreen` `.navigationTitle`+
/// .p12 `ClientCertScreen`
/// `ClientCertViewModel`keychain store
@ViewBuilder private var deviceCertSheet: some View {
NavigationStack {
ClientCertScreen()
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button(RootCopy.done) { coordinator.isDeviceCertPresented = false }
}
}
}
}
}