Watch app MVP: XcodeGen project, gateway client, pair, feed, thread, new thread, notifications

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]>
This commit is contained in:
gibandClaude Opus 5.5 committed 2026-11-01 02:33:15 -05:00
1 parent ba1624eb9e
commit bdfb79b891
19 files changed
+2413

No files matched your search

+92
View File
@@ -0,0 +1,92 @@
// 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)
}