V3-04 through V3-07, V3-10, V3-16 shipped complete; V3-11/V3-12/V3-14 shipped code-complete pending device/account verification; V3-08/V3-09 deferred behind a new V3-17 (self-hosted OSRM investigation). 316 tests passing, up from 221. The APK is a fresh release build (debug-signed, no release signing config exists yet) with two build fixes applied: core library desugaring enabled for flutter_local_notifications, and sentry_flutter bumped to 9.27.0 (8.14.2's bundled Kotlin plugin was incompatible with this project's Kotlin 2.4.0 toolchain).
87 lines
4.0 KiB
Markdown
87 lines
4.0 KiB
Markdown
# V3-02 — Settings screen
|
|
|
|
**Phase** Foundations · **Depends on** nothing (pairs with V3-01, V3-03) · **Size** S · **Status** Done
|
|
|
|
## Goal
|
|
One place for the preferences that currently have nowhere to live.
|
|
|
|
## Context
|
|
`Config` already holds `uploadEndpoint`, `deviceId` and `mapEnabled`, but there is no UI
|
|
for any of them. The map toggle sits on trip detail because a single switch did not justify
|
|
a screen. V3-01 and V3-03 both add preferences, which finally does justify one.
|
|
|
|
The upload endpoint has **never** had UI, in the native app or the port. This is where it
|
|
stops being a code-only setting.
|
|
|
|
## Design
|
|
Reached from the record screen. Sections:
|
|
|
|
- **Recording** — default activity (V3-01), units (V3-03)
|
|
- **Map** — render maps on trip detail; later, live map (V3-04)
|
|
- **Sync** — upload endpoint, with the device id shown read-only and copyable
|
|
- **About** — version, a link to the privacy policy, licences
|
|
|
|
Keep it plain. This is not a screen anyone should spend time in.
|
|
|
|
## Implementation
|
|
1. `SettingsScreen` + a `go_router` route
|
|
2. Make `Config` reactive — it is currently read once into a provider; settings need writes
|
|
to propagate. A `ConfigNotifier` over `shared_preferences`.
|
|
3. Move the map toggle off trip detail
|
|
4. Endpoint field validates it parses as a URL and is http(s)
|
|
|
|
## Acceptance criteria
|
|
- [ ] Every `Config` value is viewable and editable
|
|
- [ ] Changing the map toggle takes effect without an app restart
|
|
- [ ] An invalid endpoint is rejected with a readable message, not silently stored
|
|
- [ ] Device id is copyable — it is the only way to identify this install to a server
|
|
|
|
## Tests
|
|
- Widget: each control renders and writes through to `Config`
|
|
- Widget: invalid URL rejected
|
|
- Changing the map toggle rebuilds trip detail
|
|
|
|
## Risks
|
|
`Config` is currently loaded once at startup into a `StateProvider`. Making it writable
|
|
without introducing a second source of truth is the only subtle part.
|
|
|
|
## Out of scope
|
|
Account settings (v4). Theme selection (V3-16).
|
|
|
|
|
|
## Outcome
|
|
|
|
"Move the map toggle off trip detail" (implementation step 3) turned out to be moot —
|
|
`mapEnabledProvider` already existed and drove `RideMap`'s visibility, but **no widget
|
|
anywhere ever offered a control to change it**. There was nothing to move. Settings adds
|
|
the first one.
|
|
|
|
No `ConfigNotifier` was built — see V3-03's outcome for why the existing
|
|
`mapEnabledProvider`-style `StateProvider` pattern covers reactivity without it, and
|
|
`unitSystemProvider` was added the same way, in `providers.dart`, ahead of this ticket.
|
|
|
|
Two implementation-step items shipped differently than drafted, both to avoid adding a
|
|
dependency disproportionate to an `S`-sized settings screen:
|
|
|
|
- **No `package_info_plus`.** The About section's version string is a static literal
|
|
matching `pubspec.yaml`'s `1.0.0+1`, not a live package lookup. Fine today; would need
|
|
revisiting if the version ever needs to be authoritative from inside the running app
|
|
rather than copied by hand.
|
|
- **No privacy-policy link.** None is published yet (see `docs/LAUNCH.md`) and linking
|
|
to one that does not exist would be worse than omitting it. Shows a plain note instead.
|
|
Licences are still free: Flutter's built-in `showLicensePage` needed no new dependency.
|
|
|
|
Endpoint validation accepts `http://` and `https://` with a non-empty host, and treats
|
|
an **empty** field as valid — that is how upload gets disabled, not an error state. A
|
|
non-empty invalid value is rejected with inline `errorText` and never reaches `Config`;
|
|
proven by a widget test that types garbage, taps Save, and asserts `Config.uploadEndpoint`
|
|
is still empty afterwards.
|
|
|
|
`uploaderProvider` needed an explicit `ref.invalidate()` after saving the endpoint, for
|
|
the identical reason `unitSystemProvider` needed its own `StateProvider` rather than
|
|
reading through `configProvider` directly — a `Config` write never changes the `Config`
|
|
instance Riverpod is watching, so nothing downstream rebuilds unless told to.
|
|
|
|
10 tests: 9 in `settings_screen_test.dart`, 1 confirming the record screen's settings
|
|
button is genuinely wired (not just present) in `widget_test.dart`.
|