Built together because V3-02's settings screen needed something real for
V3-03's units to control -- executed in reverse of the ticket numbering, but
both tickets are independently complete.
V3-03: UnitSystem (metric/imperial) lives in domain/models.dart alongside
Activity, defaulting from Platform.localeName on first launch. Storage stays
SI everywhere -- conversion happens only in ui/format.dart, at the last
possible moment, which is now documented as the file's central invariant.
Threaded through all three screens plus the speed histogram's bucket labels,
which relabel for display without changing how speedHistogram itself bins.
Export was deliberately left untouched: gpx()/geoJson() take no UnitSystem
parameter at all, a stronger guarantee than validating one would be.
V3-02: SettingsScreen reachable from the record screen. Map render toggle
(nothing previously exposed mapEnabledProvider to the user, despite it
existing since T15 -- there was nothing to "move off trip detail" as drafted),
unit selector, upload endpoint with http(s) validation, a copyable device id,
and a licences page via Flutter's built-in showLicensePage. Skipped
package_info_plus (static version string instead) and a privacy-policy link
(none published yet) as disproportionate to an S-sized ticket.
Neither ticket needed the ConfigNotifier the implementation notes proposed:
unitSystemProvider reuses the exact StateProvider-seeded-from-Config pattern
mapEnabledProvider already established, since Config mutates its own backing
SharedPreferences in place and re-assigning the same instance would never
notify a watcher anyway.
A real locale-dependent flake was caught, not just anticipated: a test
asserting a fresh Config defaults to metric failed, because this machine's own
locale resolves to a region in the imperial set. Fixed by seeding an explicit
value before asserting, and config_test.dart's locale test was written from
the start to only prove the fallback path doesn't throw, not to assert which
value it returns.
221 tests (211 -> 221): 13 format_test, 9 config_test, 9 settings_screen_test,
1 confirming the record screen's settings button is genuinely wired. Analyze
clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
T22 -- TelemetryUploader on package:http with MockClient standing in for
MockWebServer, tested against a real in-memory Drift database rather than a fake
DAO. Upload runs on its own timer, injected as a callback so the engine has no
opinion about HTTP and tests need no network. Config on shared_preferences with
a hand-rolled UUID v4. Still no UI for the endpoint, exactly as in the native
app. 7 tests.
T24 -- integration_test/app_test.dart, 4 tests passing on the iOS simulator.
These cover what widget tests cannot: Drift opening against real platform
storage, plugin registration, go_router driving a real Navigator, cold start.
T26 -- RELEASE-IOS.md. Usage strings are specific rather than generic, which is
the leading Guideline 5.1.1 rejection cause; privacy-label answers decided; a
pre-submission list covering the bundle-id switch back to com.rippr, the still
default app icon, a release build, and a demo video for review notes.
T25 written up as REAL-RIDE-CHECKLIST.md but outstanding by nature. Two items
decide real things: force-stopping mid-ride on Android confirms the 111 km
crash-gap bug is fixed, and parking 15 minutes mid-recording on iOS decides
whether geolocator is sufficient or the paid engine is needed.
Two prefer_initializing_formals lints suppressed with a reason: Dart forbids a
named parameter beginning with an underscore, so the suggested fix will not
compile.
178 tests passing, analyze clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>