SwiftUI watch-only app in watch/, generated by XcodeGen. Codable models mirror gateway/src/contract.ts, with lenient string enums so new phases or decisions still decode. A DemoGateway serves fixtures from the docs/api.md examples for previews and demo mode. Swift Testing covers decoding the documented JSON, request encoding, notification action parsing and the small view helpers. Co-Authored-By: Claude Opus 5.5 <[email protected]>
93 lines
3.1 KiB
Swift
93 lines
3.1 KiB
Swift
// Pairing: the gateway URL and the 6 digit code that `wristcode pair` prints on the gateway.
|
|
// Shown whenever there is no device token, or the gateway answers 401.
|
|
|
|
import SwiftUI
|
|
|
|
struct PairView: View {
|
|
@Environment(AppModel.self) private var model
|
|
@State private var url = DeviceStore.lastGatewayURL
|
|
@State private var code = ""
|
|
@State private var sending = false
|
|
@State private var error: String?
|
|
|
|
private var digits: String { code.filter(\.isNumber) }
|
|
|
|
var body: some View {
|
|
ScrollView {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Text("WristCode")
|
|
.font(.system(size: 22, weight: .bold, design: .rounded))
|
|
.foregroundStyle(Theme.you)
|
|
Text("Run wristcode pair on the gateway, then enter the code.")
|
|
.font(.system(size: 13))
|
|
.foregroundStyle(Theme.secondaryText)
|
|
|
|
field("Gateway") {
|
|
TextField("Gateway URL", text: $url)
|
|
.textContentType(.URL)
|
|
}
|
|
field("Code") {
|
|
TextField("6 digits", text: $code)
|
|
.textContentType(.oneTimeCode)
|
|
}
|
|
|
|
Button(sending ? "Pairing" : "Pair") { pair() }
|
|
.buttonStyle(PillButtonStyle(fill: Theme.you))
|
|
.disabled(sending || digits.count != 6)
|
|
.opacity(digits.count == 6 ? 1 : 0.5)
|
|
.padding(.top, 4)
|
|
|
|
if let error {
|
|
Text(error)
|
|
.font(.system(size: 12))
|
|
.foregroundStyle(Theme.secondaryText)
|
|
}
|
|
|
|
Button("Try the demo") { model.startDemo() }
|
|
.buttonStyle(.plain)
|
|
.font(.system(size: 13, weight: .semibold))
|
|
.foregroundStyle(Theme.work)
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.top, 6)
|
|
}
|
|
.padding(.horizontal, 4)
|
|
}
|
|
.textInputAutocapitalization(.never)
|
|
.autocorrectionDisabled()
|
|
}
|
|
|
|
private func field(_ label: String, @ViewBuilder input: () -> some View) -> some View {
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(label)
|
|
.font(.system(size: 11, weight: .semibold))
|
|
.foregroundStyle(Theme.secondaryText)
|
|
input()
|
|
}
|
|
}
|
|
|
|
private func pair() {
|
|
guard let gatewayURL = DeviceStore.normalizeGatewayURL(url) else {
|
|
error = "That gateway URL doesn't look right."
|
|
return
|
|
}
|
|
sending = true
|
|
error = nil
|
|
Task {
|
|
do {
|
|
try await model.pair(gatewayURL: gatewayURL, code: digits)
|
|
} catch let failure as GatewayError where failure == .unauthorized {
|
|
error = "That code is wrong or expired."
|
|
} catch {
|
|
self.error = error.localizedDescription
|
|
}
|
|
sending = false
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview("Pair") {
|
|
PairView()
|
|
.environment(AppModel(mode: .unpaired))
|
|
.fontDesign(.rounded)
|
|
}
|