103 lines
5.3 KiB
Markdown
103 lines
5.3 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 JWT auth as the web app). At full feature parity
|
||
with the web app (data export/import is the only deliberate omission).
|
||
|
||
Project name `carcontrol_phone`, package id `com.carcontrole.carcontrol_phone`.
|
||
**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), tabs for **Service history** and **Parts catalog**,
|
||
edit car, add/edit/delete service records and parts, a **share** sheet
|
||
(owner only), quick odometer update, and delete car (type-to-confirm; cascades).
|
||
Actions are gated by the caller's access level (read-only vs write vs owner).
|
||
- **Settings** — account (name / email verification / password), appearance
|
||
(theme + dark mode, locale, date format, font size), profile (avatar via
|
||
`image_picker`, bio), **Security** (biometric toggle), active sessions with
|
||
remote logout, and the account-deletion state machine.
|
||
- **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.
|
||
|
||
## 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 JWT 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.carcontrole.carcontrol_phone -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
|
||
```
|