Files
wristcode/watch/README.md
T
gib 7ca0362745 Watch: status, environment and notifications screens
A gear on the feed opens a status overview that leads with what is wrong:
offline hosts, hosts on a fallback route, tokens expiring within 7 days,
and push problems, with healthy hosts folded into one row. Each host opens
its route ladder with Reconnect. The notifications page shows registration,
the last push, Send test and Unpair. Demo mode gains a Laptop on its VPN
fallback, and snapshot tests render the three screens.
2026-11-01 15:16:18 -05:00

106 lines
5.9 KiB
Markdown

# WristCode watch app
SwiftUI, watch only, Swift 6 with strict concurrency, and no dependencies. It talks to the gateway
over HTTPS + JSON as described in [docs/api.md](../docs/api.md); `WristCode/API/Models.swift`
mirrors [gateway/src/contract.ts](../gateway/src/contract.ts).
## Build and test
```sh
cd watch
xcodegen generate # the .xcodeproj is generated and gitignored
xcodebuild -scheme WristCode -destination 'platform=watchOS Simulator,name=Apple Watch Series 12 (46mm)' build
xcodebuild -scheme WristCode -destination 'platform=watchOS Simulator,name=Apple Watch Series 12 (46mm)' test
```
## TestFlight
The App Store only takes a watch-only app inside an empty iOS stub, so `project.yml` has two app
targets, the same pair Xcode's "Watch-only App" template makes:
| Target | Bundle ID | Job |
| --- | --- | --- |
| `WristCodeContainer` | `org.gbrown.wristcode` | iOS stub that owns the App Store Connect record and embeds the watch app |
| `WristCode` | `org.gbrown.wristcode.watchkitapp` | The watch app, and the APNs topic |
| `WristCodeWidget` | `org.gbrown.wristcode.watchkitapp.widget` | The complication, embedded in the watch app |
```sh
scripts/testflight.sh # archive, export and upload to App Store Connect
scripts/testflight.sh --no-upload # archive and export an .ipa to build/export-<n> only
```
The build number defaults to a `YYYYMMDDHHMM` timestamp (override it with `BUILD_NUMBER`). Signing is
automatic through the App Store Connect API key in `ASC_KEY_PATH`, `ASC_KEY_ID` and `ASC_ISSUER_ID`,
which default to the key in `~/.appstoreconnect/private_keys`. The entitlements file says
`aps-environment: development`; the App Store export re-signs it as `production`. Release builds
register their token with the gateway as `production`, Debug builds as `sandbox`.
## Demo mode
`DemoGateway` serves fixture data shaped like the docs/api.md examples, and it reacts to writes:
Allow moves a thread to working, a reply restarts a finished thread. The SwiftUI previews use it,
the pairing screen has a "Try the demo" button, and launch arguments open a screen directly:
```sh
xcrun simctl launch <udid> org.gbrown.wristcode.watchkitapp -demo # feed
xcrun simctl launch <udid> org.gbrown.wristcode.watchkitapp -demo -thread littleman/t-next # thread
xcrun simctl launch <udid> org.gbrown.wristcode.watchkitapp -demo -screen new # new thread
xcrun simctl launch <udid> org.gbrown.wristcode.watchkitapp -demo -screen status # status overview
xcrun simctl launch <udid> org.gbrown.wristcode.watchkitapp -demo -host laptop # one environment
xcrun simctl launch <udid> org.gbrown.wristcode.watchkitapp -demo -screen notifications # notifications
```
`screenshots/` holds the screens captured this way. The complication, the multi-select question and
the status screens are rendered by `SnapshotTests` instead, which only run when given a directory:
```sh
TEST_RUNNER_WC_SCREENSHOT_DIR=$PWD/screenshots xcodebuild -scheme WristCode \
-destination 'platform=watchOS Simulator,name=Apple Watch Series 12 (46mm)' test \
-only-testing:WristCodeTests/SnapshotTests
```
In demo mode the complication shows the demo counts too.
## Status and settings
The gear at the top left of the feed opens the status overview (`GET /v1/status`). It leads with
"All connected" or "Needs attention", lists only the issues worst first (offline hosts, a host on a
fallback route, tokens expiring within 7 days, push problems), and folds the healthy hosts into one
row. Each host opens its route ladder with Reconnect. The Notifications row shows whether this watch
is registered, the last push, Send test, the gateway, and Unpair. Each screen loads once when it
appears; nothing polls.
## Complication
`WristCodeWidget` shows how many threads wait on you, from `GET /v1/summary`, in the circular,
rectangular, inline and corner families. Tapping it opens the app, at the longest-waiting thread when
there is one (`wristcode://thread/<hostId>/<threadId>`). WidgetKit refreshes it about every 15 minutes;
the app reloads it sooner when its feed counts change, when a push arrives in the foreground, and
after a notification action.
The app and the extension share the Keychain access group `<team>.org.gbrown.wristcode.shared`, which
holds the device token, gateway URL, demo flag and last summary. It is not an App Group because
automatic signing with an App Store Connect API key can register bundle IDs and profiles but cannot
create or assign App Groups; a Keychain group needs no portal setup. Builds before the complication
kept the token in the app's own Keychain group; `DeviceStore.migrateToSharedGroup()` moves it on
launch, so a paired watch stays paired.
To add it: long-press the watch face, Edit, swipe to Complications, pick a slot, then WristCode.
## Layout
| Path | Job |
| --- | --- |
| `WristCodeApp.swift` | App entry, root navigation, `WKApplicationDelegate` for APNs registration |
| `AppModel.swift` | Paired, demo or unpaired; navigation path; the feed; 401 sends you back to pairing |
| `API/GatewayClient.swift` | `GatewayAPI` protocol and the URLSession client |
| `API/Models.swift` | Codable mirror of the contract |
| `API/Route.swift` | Screens the app can open, and their `wristcode://` URLs |
| `Auth/DeviceStore.swift` | Device token and gateway URL in the shared Keychain group |
| `Shared/SharedState.swift` | Demo flag and last summary in the shared Keychain group, for the complication |
| `Demo/DemoGateway.swift` | In-memory gateway with fixtures |
| `Views/` | Pair, Feed, Thread, New thread, Status, and the mock B theme |
| `Views/StatusReport.swift` | What the status overview says: headline, issues worst first, healthy hosts |
| `Notifications/NotificationHandler.swift` | Categories `approval`, `finished`, `failed`, `input`, `host`, `test` and their actions |
| `../WristCodeWidget/` | The complication: timeline provider, and one view per family |