# 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`.