mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-08 19:19:42 +00:00
- HapiProtocol/Pairing/BindLink: parses the companion deeplink (hapicompanion://bind?hub=&code=) and the web direct-access QR (?hub=&token=) with URLSearchParams form-decoding semantics, in lockstep with the Android port (tests mirror BindLinkTest.kt). - HapiClient/Auth/HubPairingService: normalize -> GET /health (reachability + protocolVersion) -> POST /api/auth -> persist Keychain + registry + active hub; unpair with fallback. Covered by PairingLogicTests through the HTTPPerforming seam. - App layer: AppModel (@Observable @MainActor pairing state machine: restore, pair, switch, sign out, deep-link routing, scenePhase, terminal-auth-failure banner) + HubSession (per-active-hub APIClient/ AuthManager/global SSEClient with suspend-resume and a connection state for the UI; store routing is TODO(M2)). - Pairing UI: welcome flow, VisionKit QR scanner (with Simulator/ permission fallbacks), manual entry (paste-friendly), shared confirm sheet with per-PairingFailure error states. - HapiApp routes hapicompanion:// through AppModel (paired hubs switch with a notice, never log the token); RootView switches unpaired/ paired and hosts the deep-link confirm sheet; HomePlaceholderView shows hub, connection dot, hub switcher (M2a replaces it with the session list). - Info.plist: NSCameraUsageDescription; README: pairing guide + manual test pass.
66 lines
2.0 KiB
Swift
66 lines
2.0 KiB
Swift
import HapiClient
|
|
import SwiftUI
|
|
|
|
/// Switches the app root on pairing state and hosts the presentation that
|
|
/// must survive that switch: the deep-link pairing confirm sheet and the
|
|
/// "already paired" notice.
|
|
struct RootView: View {
|
|
@Environment(AppModel.self) private var model
|
|
|
|
var body: some View {
|
|
@Bindable var model = model
|
|
Group {
|
|
switch model.state {
|
|
case .unpaired:
|
|
PairingFlowView()
|
|
case .paired:
|
|
if let session = model.session {
|
|
// TODO(M2a): replace HomePlaceholderView with SessionListView.
|
|
HomePlaceholderView(session: session)
|
|
} else {
|
|
// Defensive: .paired always carries a session; fall back
|
|
// to pairing rather than a dead screen.
|
|
PairingFlowView()
|
|
}
|
|
}
|
|
}
|
|
.sheet(item: $model.pendingPairing) { pending in
|
|
NavigationStack {
|
|
PairingConfirmView(pending: pending)
|
|
.toolbar {
|
|
ToolbarItem(placement: .cancellationAction) {
|
|
Button("Cancel") {
|
|
model.pendingPairing = nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.alert(
|
|
"Hub already paired",
|
|
isPresented: Binding(
|
|
get: { model.infoNotice != nil },
|
|
set: { presented in
|
|
if !presented {
|
|
model.infoNotice = nil
|
|
}
|
|
}
|
|
)
|
|
) {
|
|
Button("OK") {
|
|
model.infoNotice = nil
|
|
}
|
|
} message: {
|
|
Text(model.infoNotice ?? "")
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
RootView()
|
|
.environment(AppModel(
|
|
registry: HubRegistry(defaults: UserDefaults(suiteName: "preview") ?? .standard),
|
|
credentialStore: InMemoryCredentialStore()
|
|
))
|
|
}
|