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:
1 parent
ba1624eb9e
commit
bdfb79b891
19 files changed
+2413
No files matched your search
@@ -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)
|
||||
}
|
||||
Reference in new issue
Block a user