mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-11 19:50:05 +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.
40 lines
1.2 KiB
Swift
40 lines
1.2 KiB
Swift
import HapiProtocol
|
|
import OSLog
|
|
import SwiftUI
|
|
|
|
@main
|
|
struct HapiApp: App {
|
|
@State private var model = AppModel()
|
|
@Environment(\.scenePhase) private var scenePhase
|
|
|
|
private let log = Logger(subsystem: "run.hapi.companion", category: "app")
|
|
|
|
var body: some Scene {
|
|
WindowGroup {
|
|
RootView()
|
|
.environment(model)
|
|
.onOpenURL { url in
|
|
handleIncomingURL(url)
|
|
}
|
|
}
|
|
.onChange(of: scenePhase, initial: true) { _, phase in
|
|
model.handleScenePhase(phase)
|
|
}
|
|
}
|
|
|
|
/// `hapicompanion://bind?hub=<url>&code=<accessToken>` — parse and route
|
|
/// through `AppModel`: unpaired → pairing confirm; already paired to that
|
|
/// hub → switch with a notice; a different hub → add-hub confirm.
|
|
///
|
|
/// Never log the URL itself: the query carries the access token.
|
|
private func handleIncomingURL(_ url: URL) {
|
|
guard let link = BindLink.parse(url.absoluteString) else {
|
|
log.warning("Ignoring malformed URL (scheme: \(url.scheme ?? "none", privacy: .public))")
|
|
return
|
|
}
|
|
if !model.handleBindLink(link) {
|
|
log.warning("Bind link carried an unusable hub URL")
|
|
}
|
|
}
|
|
}
|