Files
rippr/docs/v3/V3-02-settings-screen.md
uhryniuk b781ee36a8 Rename v4 to ROADMAP
v4 was always just the server-dependent programme (group rides, accounts, paid backup) plus V3-17. Renaming it to ROADMAP since it's about to become the home for an incoming list of bugs and features that need triage ahead of the next v3-style ticket batch -- that triage takes priority over the existing server-dependent items, none of which are blocking.
2026-08-23 11:32:29 -05:00

87 lines
4.1 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 (ROADMAP). 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`.