Follows the Android package rename: updates the pubspec name, the package: imports in the tests, the web title/manifest strings, and the project name in the README. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
149 lines
8.1 KiB
Markdown
149 lines
8.1 KiB
Markdown
# Car Control — Phone App (Flutter)
|
||
|
||
A Flutter client for the Car Control maintenance tracker. Talks **only** to the
|
||
API Server (same contract and auth as the web app — a PocketBase token relayed by
|
||
the server, not a JWT the server mints). At full feature parity with the web app
|
||
(data export/import is the only deliberate omission).
|
||
|
||
Project name `drivervault_phone`, package id `com.drivervault.phoneapp`.
|
||
**Android is the supported target** — the older Flutter-web build path is
|
||
deprecated.
|
||
|
||
## Features
|
||
|
||
- **Login** — email/password against `/api/auth/login`, password show/hide, and a
|
||
collapsible **Server settings** section to override the API base URL on-device.
|
||
- **Biometric / face sign-in + app lock** — see the dedicated section below.
|
||
- **Dashboard** — car list with next-due status badges (date + km, worst-of),
|
||
a "shared" chip on cars owned by someone else, pull-to-refresh, **Add car**
|
||
FAB, Settings gear, and an admin action (admins only).
|
||
- **Car detail** — all spec fields (incl. VIN and transmission / differential /
|
||
brake / coolant specs), a **share** sheet (owner only), quick odometer update,
|
||
edit car, and delete car (type-to-confirm; cascades). Actions are gated by the
|
||
caller's access level (read-only vs write vs owner). Tabs, in the web app's
|
||
order:
|
||
- **Service history** — date/odometer plus which parts were changed, with the
|
||
next-due date/km derived by the server.
|
||
- **Technical check history** — the mandatory roadworthiness inspections
|
||
(przegląd techniczny, MOT, TÜV). Result, cost, station, and the certificate's
|
||
valid-until, which overrides the car's interval when present. A failed check
|
||
derives no next date.
|
||
- **Maintenance** — workshop visits and repairs outside the routine schedule:
|
||
type/status, workshop, parts used, labour + parts cost, invoice number and a
|
||
warranty-until badge.
|
||
- **Fuel** — refills with a summary panel (average / best / worst consumption,
|
||
cost per km, price per litre). Consumption is measured between full tanks, so
|
||
partial fills roll into the next full one and a flagged missed fill leaves its
|
||
window uncomputed rather than reporting a fictional figure.
|
||
- **Documents** — insurance, registration, road tax and the rest, with a
|
||
renewal badge driven by the server's expiry assessment.
|
||
- **Parts catalog** — the per-car parts list.
|
||
- **Reminders** — date- and/or odometer-triggered, one-off or recurring, with a
|
||
**Mark done** action. Reminders the server derived from a document or service
|
||
record are shown read-only.
|
||
- **Attachments** — one optional file per service record, technical check,
|
||
workshop visit, refill, document and part (PDF or image, up to 10MB). Picked
|
||
with `file_picker`, fetched back through the API Server — never a public URL —
|
||
and opened with the phone's own viewer via `open_filex`.
|
||
- **Settings** — account (name / email verification / password), appearance
|
||
(theme + dark mode, **language**, **region**, date format, **currency**, font
|
||
size), profile (avatar via `image_picker`, bio), **Security** (biometric
|
||
toggle), and the account-deletion state machine. Auth relays PocketBase's own
|
||
stateless tokens, so there is no per-device session list to show or revoke.
|
||
- **Admin** — user management screen (list / create / role / reset password /
|
||
delete), gated by the admin role.
|
||
|
||
Sharing/ownership: `Car.access` drives `isOwner` / `canWrite` / `isReadOnly`
|
||
getters that gate the UI, mirroring the server's access checks.
|
||
|
||
## Language, region & currency
|
||
|
||
Language and region are two pickers over the one stored BCP-47 tag (`en-US`), so
|
||
the pair can be mixed — English in Poland, say. Currency is display-only: nothing
|
||
is converted, so changing it reinterprets existing amounts rather than
|
||
recalculating them. All three lists mirror the API's (`validCurrencies` and the
|
||
locale pattern in `internal/api/me.go`), with two deliberate differences from the
|
||
web app:
|
||
|
||
- **Luxembourgish (`lb`) and Romansh (`rm`) are not offered.** `intl` ships no
|
||
date/number symbols for them and *throws* rather than falling back, which would
|
||
take out every date on screen. The browser has full ICU data behind it and has
|
||
no such limit, so the web app can list them.
|
||
- **Labels are hand-kept** (endonyms for languages, English for regions and
|
||
currencies) because Dart has no `Intl.DisplayNames`.
|
||
|
||
Because the server only validates a locale's *shape* (`^[a-z]{2}-[A-Z]{2}$`), a
|
||
tag the phone cannot render can still arrive from the web. `format.dart` resolves
|
||
through a supported-language check and falls back to `en-US` instead of throwing;
|
||
`test/models_format_test.dart` covers it.
|
||
|
||
## Biometric / face sign-in & app lock
|
||
|
||
Fingerprint and face-recognition sign-in via `local_auth`, with credentials kept
|
||
in Android Keystore–backed secure storage (`flutter_secure_storage`).
|
||
|
||
- After a successful password login the app offers to **enable biometric login**;
|
||
the entered (known-good) credentials are stored securely.
|
||
- The login screen then shows **"Sign in with face recognition / fingerprint"**
|
||
buttons (labels reflect the enrolled biometric kinds) and auto-prompts once.
|
||
On success the stored credentials are replayed against the normal login API, so
|
||
each biometric sign-in mints a fresh session. Stale credentials (e.g. after a
|
||
password change) auto-disable biometric login.
|
||
- **App lock** — the token persists, so a valid session normally restores silently.
|
||
When biometric login is enabled the app instead starts **locked** (and re-locks
|
||
when backgrounded) and shows a lock screen requiring a biometric unlock. A
|
||
**30-second grace period** means quick app-switches don't re-lock; a full app
|
||
close (process kill) always locks on next launch. "Use password instead" on the
|
||
lock screen logs out and returns to the login form.
|
||
- Manage it under **Settings → Security** (enabling re-confirms the password).
|
||
|
||
Android host requirements (already configured, don't revert):
|
||
`MainActivity` extends **`FlutterFragmentActivity`** (required by `local_auth`),
|
||
and `AndroidManifest.xml` declares `android.permission.USE_BIOMETRIC`.
|
||
|
||
## Configure the API endpoint
|
||
|
||
The app talks to `kDefaultApiBase` (see `lib/config.dart`), default
|
||
`http://localhost:8080/api`. Override at build time with `--dart-define`, or at
|
||
runtime from the login screen's **Server settings** (persisted as `cc_server_url`).
|
||
|
||
## Run & build
|
||
|
||
```bash
|
||
flutter pub get
|
||
|
||
# run on a connected device against a LAN server
|
||
flutter run -d <device> --dart-define=API_BASE=http://10.2.1.101:8080/api
|
||
|
||
# build a debug APK for a real phone on the LAN
|
||
flutter build apk --debug --dart-define=API_BASE=http://10.2.1.101:8080/api
|
||
adb install -r build/app/outputs/flutter-apk/app-debug.apk
|
||
adb shell monkey -p com.drivervault.phoneapp -c android.intent.category.LAUNCHER 1
|
||
```
|
||
|
||
Notes:
|
||
- `android/gradle.properties` sets `kotlin.incremental=false` — required because
|
||
the project lives on drive `E:` while Gradle/Kotlin caches are on `C:` (the
|
||
incremental compiler can't compute cross-root relative paths on Windows).
|
||
- `AndroidManifest.xml` sets `android:usesCleartextTraffic="true"` because the API
|
||
base is a plain-HTTP LAN URL.
|
||
- The API Server must be running and reachable at the configured URL.
|
||
|
||
## Structure
|
||
|
||
```
|
||
lib/
|
||
├── config.dart # default API base URL (kDefaultApiBase)
|
||
├── models.dart # Car (+ access getters), ServiceRecord, Part, AuthUser, UserProfile, Session
|
||
├── api.dart # ApiClient — the only thing that calls the API Server
|
||
├── auth.dart # AuthService (token persistence, app-lock flag, ChangeNotifier)
|
||
├── biometric.dart # BiometricAuth — local_auth + secure storage; biometricAuth singleton
|
||
├── app_settings.dart # AppSettings (theme/locale/date/font), persisted; drives MaterialApp
|
||
├── format.dart # date/km formatting + next-service status (worst-of date/km)
|
||
├── main.dart # app root; routes Login / Lock / Dashboard; lifecycle-based re-lock
|
||
└── screens/
|
||
├── login_screen.dart dashboard_screen.dart car_detail_screen.dart
|
||
├── car_form_sheet.dart settings_screen.dart admin_users_screen.dart
|
||
└── lock_screen.dart
|
||
```
|